feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 8)

Converts sidebar_nav, skeleton, social_login_row, spinner, stat_card,
stat_grid, status_badge, step_card, stepper, switch to theme-aware v1.
All 9 touchpoints wired; ext-8 shard extended for schema definitions.
ListTools count: 167 → 177. All 4116 tests pass.

Notable: spinner/stat_grid/status_badge/switch emit identical trees
across all theme modes (caller-param colors, status semantics, or iOS
HIG builder-private literals per spec §3.4) — theme param accepted
for API consistency only.
This commit is contained in:
Fini 2026-04-29 09:50:28 +08:00
parent e1a931e4ff
commit d547e6917b
28 changed files with 1869 additions and 1 deletions

View file

@ -168,6 +168,16 @@ import {
buildSegmentedControlV1,
buildSelectV1,
buildShareRowV1,
buildSidebarNavV1,
buildSkeletonV1,
buildSocialLoginRowV1,
buildSpinnerV1,
buildStatCardV1,
buildStatGridV1,
buildStatusBadgeV1,
buildStepCardV1,
buildStepperV1,
buildSwitchV1,
findNodeInTree,
insertNodeInTree,
type ElementTree,
@ -407,6 +417,17 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
buildSegmentedControlV1(a as Parameters<typeof buildSegmentedControlV1>[0]),
add_select_v1: (a) => buildSelectV1(a as Parameters<typeof buildSelectV1>[0]),
add_share_row_v1: (a) => buildShareRowV1(a as Parameters<typeof buildShareRowV1>[0]),
add_sidebar_nav_v1: (a) => buildSidebarNavV1(a as Parameters<typeof buildSidebarNavV1>[0]),
add_skeleton_v1: (a) => buildSkeletonV1(a as Parameters<typeof buildSkeletonV1>[0]),
add_social_login_row_v1: (a) =>
buildSocialLoginRowV1(a as Parameters<typeof buildSocialLoginRowV1>[0]),
add_spinner_v1: (a) => buildSpinnerV1(a as Parameters<typeof buildSpinnerV1>[0]),
add_stat_card_v1: (a) => buildStatCardV1(a as Parameters<typeof buildStatCardV1>[0]),
add_stat_grid_v1: (a) => buildStatGridV1(a as Parameters<typeof buildStatGridV1>[0]),
add_status_badge_v1: (a) => buildStatusBadgeV1(a as Parameters<typeof buildStatusBadgeV1>[0]),
add_step_card_v1: (a) => buildStepCardV1(a as Parameters<typeof buildStepCardV1>[0]),
add_stepper_v1: (a) => buildStepperV1(a as Parameters<typeof buildStepperV1>[0]),
add_switch_v1: (a) => buildSwitchV1(a as Parameters<typeof buildSwitchV1>[0]),
};
interface ExecToolBody {

View file

@ -177,6 +177,16 @@ import {
buildSegmentedControlV1,
buildSelectV1,
buildShareRowV1,
buildSidebarNavV1,
buildSkeletonV1,
buildSocialLoginRowV1,
buildSpinnerV1,
buildStatCardV1,
buildStatGridV1,
buildStatusBadgeV1,
buildStepCardV1,
buildStepperV1,
buildSwitchV1,
} from '@zseven-w/pen-core';
export interface BuilderCase {
@ -1210,4 +1220,67 @@ export const CASES: BuilderCase[] = [
},
build: (a) => buildShareRowV1(a as unknown as Parameters<typeof buildShareRowV1>[0]),
},
{
toolName: 'add_sidebar_nav_v1',
args: {
items: [
{ label: 'Dashboard', icon: 'layout-dashboard', active: true },
{ label: 'Settings', icon: 'settings' },
],
theme: 'light',
},
build: (a) => buildSidebarNavV1(a as unknown as Parameters<typeof buildSidebarNavV1>[0]),
},
{
toolName: 'add_skeleton_v1',
args: { rows: 3, theme: 'light' },
build: (a) => buildSkeletonV1(a as unknown as Parameters<typeof buildSkeletonV1>[0]),
},
{
toolName: 'add_social_login_row_v1',
args: { providers: [{ name: 'google' }, { name: 'apple' }], theme: 'light' },
build: (a) =>
buildSocialLoginRowV1(a as unknown as Parameters<typeof buildSocialLoginRowV1>[0]),
},
{
toolName: 'add_spinner_v1',
args: { size: 32, theme: 'light' },
build: (a) => buildSpinnerV1(a as unknown as Parameters<typeof buildSpinnerV1>[0]),
},
{
toolName: 'add_stat_card_v1',
args: { label: 'Revenue', value: '$12.4k', theme: 'light' },
build: (a) => buildStatCardV1(a as unknown as Parameters<typeof buildStatCardV1>[0]),
},
{
toolName: 'add_stat_grid_v1',
args: {
items: [
{ value: '1,284', label: 'Users' },
{ value: '$12.4k', label: 'Revenue' },
],
theme: 'light',
},
build: (a) => buildStatGridV1(a as unknown as Parameters<typeof buildStatGridV1>[0]),
},
{
toolName: 'add_status_badge_v1',
args: { label: 'Online', tone: 'success', theme: 'light' },
build: (a) => buildStatusBadgeV1(a as unknown as Parameters<typeof buildStatusBadgeV1>[0]),
},
{
toolName: 'add_step_card_v1',
args: { number: 1, title: 'Create account', description: 'Sign up with email.', theme: 'light' },
build: (a) => buildStepCardV1(a as unknown as Parameters<typeof buildStepCardV1>[0]),
},
{
toolName: 'add_stepper_v1',
args: { total: 3, current: 1, theme: 'light' },
build: (a) => buildStepperV1(a as unknown as Parameters<typeof buildStepperV1>[0]),
},
{
toolName: 'add_switch_v1',
args: { active: true, theme: 'light' },
build: (a) => buildSwitchV1(a as unknown as Parameters<typeof buildSwitchV1>[0]),
},
];

View file

@ -186,6 +186,16 @@ import {
buildSegmentedControlV1,
buildSelectV1,
buildShareRowV1,
buildSidebarNavV1,
buildSkeletonV1,
buildSocialLoginRowV1,
buildSpinnerV1,
buildStatCardV1,
buildStatGridV1,
buildStatusBadgeV1,
buildStepCardV1,
buildStepperV1,
buildSwitchV1,
type ActionMenuParams,
type ActivityRingParams,
type AlertParams,
@ -324,6 +334,16 @@ import {
type SegmentedControlV1Params,
type SelectV1Params,
type ShareRowV1Params,
type SidebarNavV1Params,
type SkeletonV1Params,
type SocialLoginRowV1Params,
type SpinnerV1Params,
type StatCardV1Params,
type StatGridV1Params,
type StatusBadgeV1Params,
type StepCardV1Params,
type StepperV1Params,
type SwitchV1Params,
type KbdParams,
type LinkParams,
type ListRowParams,
@ -611,6 +631,16 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
add_segmented_control_v1: wrap<SegmentedControlV1Params>(buildSegmentedControlV1),
add_select_v1: wrap<SelectV1Params>(buildSelectV1),
add_share_row_v1: wrap<ShareRowV1Params>(buildShareRowV1),
add_sidebar_nav_v1: wrap<SidebarNavV1Params>(buildSidebarNavV1),
add_skeleton_v1: wrap<SkeletonV1Params>(buildSkeletonV1),
add_social_login_row_v1: wrap<SocialLoginRowV1Params>(buildSocialLoginRowV1),
add_spinner_v1: wrap<SpinnerV1Params>(buildSpinnerV1),
add_stat_card_v1: wrap<StatCardV1Params>(buildStatCardV1),
add_stat_grid_v1: wrap<StatGridV1Params>(buildStatGridV1),
add_status_badge_v1: wrap<StatusBadgeV1Params>(buildStatusBadgeV1),
add_step_card_v1: wrap<StepCardV1Params>(buildStepCardV1),
add_stepper_v1: wrap<StepperV1Params>(buildStepperV1),
add_switch_v1: wrap<SwitchV1Params>(buildSwitchV1),
};
export function getElementShim(name: string): ElementShim | undefined {

View file

@ -59,6 +59,7 @@ Rows (horizontal, in-card or scrolling):
3. Row of filter chips / category tabs (label + optional icon, active state) → `add_nav_chip_row_v0` (scroll)
3b. Theme-aware variant (no hardcoded colors; all modes identical — accepts theme for API consistency) → `add_nav_chip_row_v1`
4. Non-scrolling 2-5 stats inline (auto-share width) → `add_stat_grid_v0`
4b. Theme-aware variant (no explicit fills in v0; all modes identical — accepts theme for API consistency) → `add_stat_grid_v1`
Containers and single elements:
@ -109,6 +110,7 @@ Forms:
Controls (toggle / choice / tabs):
20. iOS/Material toggle switch (51×31, thumb floats) → `add_switch_v0`
20b. Theme-aware variant (iOS HIG values #34C759/#E5E5EA are builder-private per §3.4; all modes identical) → `add_switch_v1`
21. Checkbox + inline label (20×20 box, `check` icon inside when checked) → `add_checkbox_v0`
21b. Theme-aware variant (accent fill checked, border token unchecked) → `add_checkbox_v1`
22. Radio button + inline label (20×20 ring, dot inside when selected) → `add_radio_v0`
@ -134,6 +136,7 @@ Floating / nav / wizard:
30. Breadcrumb trail with chevron separators (last crumb auto-active) → `add_breadcrumb_v0`
30b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_breadcrumb_v1`
31. Horizontal numbered stepper (circles + fill_container connectors) → `add_stepper_v0`
31b. Theme-aware variant (pending fill → border; pending number → textMuted; accent + done-white stay hardcoded) → `add_stepper_v1`
Ratings & pagination:
@ -191,10 +194,12 @@ Chrome / modals:
Status / presence:
47. Status badge (small colored dot + short label, "● Online" pattern, tone-enum'd) → `add_status_badge_v0`
47b. Theme-aware variant (tone colors are status semantics kept hardcoded; all modes identical) → `add_status_badge_v1`
Feedback / loading:
48. Loading spinner (static ring + 3/4 arc) → `add_spinner_v0`
48b. Theme-aware variant (track_color/active_color are caller params; accepts theme for API consistency; all modes identical) → `add_spinner_v1`
49. Tooltip pill (dark pill + white text, hover-hint appearance) → `add_tooltip_v0`
Analytics / KPIs:
@ -219,6 +224,7 @@ Calendars:
Loading / placeholder:
43. Loading skeleton (N gray rectangles, last row ~60% width) → `add_skeleton_v0`
43b. Theme-aware variant (row fill → surface2 in dark/system so bars are visible on dark bg) → `add_skeleton_v1`
Pagination:
@ -271,10 +277,12 @@ Messaging:
Dashboard KPIs:
62. Big-number stat card (standalone metric tile — label + huge value + optional delta/icon) → `add_stat_card_v0`
62b. Theme-aware variant (bg → surface; border → border; label → textMuted; icon → textSubtle; value → textPrimary; delta tones stay hardcoded) → `add_stat_card_v1`
Auth / login:
63. Social auth provider buttons ("Continue with Google / Apple / Microsoft", OAuth/SSO row, third-party sign-in) → `add_social_login_row_v0`
63b. Theme-aware variant (button bg → surface; border → border; icon → textMuted; label → textPrimary in dark/system) → `add_social_login_row_v1`
Pricing / monetization:
@ -300,6 +308,7 @@ Compliance / disclosure:
Desktop / dashboard rails:
69. Persistent vertical sidebar (left rail with icon+label rows, optional brand title, active item gets pill bg) → `add_sidebar_nav_v0`
69b. Theme-aware variant (bg → surface; title + active label → textPrimary; inactive → textMuted; active item bg → surface2) → `add_sidebar_nav_v1`
Presence / collaboration:
@ -381,6 +390,7 @@ Calendar:
Onboarding:
89. Numbered step card (circle/check + title + description) → `add_step_card_v0`
89b. Theme-aware variant (title → textPrimary; desc → textMuted; incomplete ring bg → surface; accent stays hardcoded) → `add_step_card_v1`
90. None match → fall through to `batch_design`
@ -393,6 +403,7 @@ PREFER an element tool when the spec says any of:
- "horizontal scrolling cards", "swipeable row", "chip row", "pills" → `add_card_row_v0`
- "metric tiles", "KPI cards", "dashboard stats" (SCROLLING row) → `add_metric_row_v0`
- "stats row", "3 metrics side by side", "summary bar" (NON-scrolling grid) → `add_stat_grid_v0`
- "dark stats grid", "dark-mode stat grid", "theme-aware stat grid", "暗色指标网格" → `add_stat_grid_v1` (no explicit fills; all modes identical — accepts theme for API consistency).
- "category filter chips", "quick-access shortcuts" → `add_nav_chip_row_v0`
- "section title with See all / View more" → `add_section_header_v0`
- "dark section header", "dark-mode section title", "theme-aware section header", "暗色版块标题" → `add_section_header_v1` (no color change; accepts theme for API consistency)
@ -420,6 +431,7 @@ PREFER an element tool when the spec says any of:
- "dropdown", "select", "picker", "combo box", "下拉选择", "选择器" → `add_select_v0`
- "dark select", "dark-mode dropdown", "theme-aware select", "暗色下拉" → `add_select_v1` (placeholder → textSubtle in dark/system)
- "skeleton", "loading placeholder", "shimmer", "loading state", "placeholder lines", "骨架屏", "加载中占位" → `add_skeleton_v0`
- "dark skeleton", "dark-mode skeleton", "theme-aware skeleton", "暗色骨架屏" → `add_skeleton_v1` (row fill → surface2 so bars are visible on dark bg).
- "line chart", "trend chart", "折线图" → `add_chart_line_v0`
- "pie chart", "donut chart", "饼图" → `add_chart_pie_v0`
- "image placeholder", "photo slot", "upload zone", "hero image area", "cover placeholder", "图片占位" → `add_image_placeholder_v0`
@ -430,9 +442,12 @@ PREFER an element tool when the spec says any of:
- "dark toast", "dark-mode snackbar", "theme-aware toast", "system theme toast", "暗色 toast", "暗色浮层通知" → `add_toast_v1` (accepts `theme` param; toasts use INVERTED contrast — `"dark"` gives a light pill with dark fg. Use `"system"` when `applySemanticPalette(doc)` is seeded)
- "dark empty chart", "dark-mode no-data placeholder", "theme-aware chart empty state", "暗色空图表", "暗色无数据占位" → `add_empty_chart_v1` (accepts `theme` param; use inside dark-theme dashboards so the empty slot doesn't punch a light rectangle into dark surfaces. Use `"system"` when `applySemanticPalette(doc)` is seeded)
- "status", "online indicator", "presence dot", "health status", "busy indicator", "状态", "在线" → `add_status_badge_v0`
- "dark status badge", "dark-mode status indicator", "theme-aware status badge", "暗色状态徽标" → `add_status_badge_v1` (tone colors are status semantics kept hardcoded; all modes identical).
- "spinner", "loading spinner", "progress circle", "loader", "加载圈" → `add_spinner_v0`
- "dark spinner", "dark-mode spinner", "theme-aware spinner", "暗色加载圈" → `add_spinner_v1` (track_color/active_color are caller params; accepts theme for API consistency).
- "tooltip", "hover hint", "help tip", "提示浮层" → `add_tooltip_v0`
- "toggle", "switch", "on/off", "开关" → `add_switch_v0`
- "dark toggle", "dark switch", "theme-aware switch", "暗色开关" → `add_switch_v1` (iOS HIG values kept hardcoded; all modes identical — accepts theme for API consistency).
- "checkbox", "agreement", "select option", "复选框" → `add_checkbox_v0`
- "radio", "single choice", "单选" → `add_radio_v0` (stack multiple in a vertical parent)
- "dark radio", "dark-mode radio button", "theme-aware radio", "暗色单选" → `add_radio_v1` (accent stays brand-invariant; unselected ring → border token)
@ -447,6 +462,7 @@ PREFER an element tool when the spec says any of:
- "FAB", "floating action button", "compose button", "新建按钮" → `add_fab_v0`
- "breadcrumb", "nav path", "面包屑" → `add_breadcrumb_v0`
- "stepper", "progress steps", "wizard nav", "步骤条" → `add_stepper_v0`
- "dark stepper", "dark-mode stepper", "theme-aware stepper", "暗色步骤条" → `add_stepper_v1` (pending fill → border; pending number → textMuted in dark/system).
- "rating", "review stars", "评分" → `add_rating_stars_v0`
- "dark rating", "dark-mode stars", "theme-aware rating", "暗色评分" → `add_rating_stars_v1` (no color change; accepts theme for API consistency)
- "carousel dots", "slide indicator", "轮播指示" → `add_carousel_dots_v0`
@ -471,7 +487,9 @@ PREFER an element tool when the spec says any of:
- "attachment", "attached file", "uploaded file", "file item", "file list row", "附件", "已上传文件" → `add_attachment_row_v0` (for upload-in-progress state, compose `add_progress_bar_v0` below)
- "chat message", "message bubble", "conversation row", "iMessage bubble", "chat UI", "聊天气泡", "消息气泡" → `add_chat_bubble_v0` (side="left" for from-others, side="right" for from-self)
- "KPI card", "big number card", "metric tile", "stat widget", "featured metric", "关键指标卡", "数据大屏卡片" → `add_stat_card_v0` (distinct from `add_stat_grid_v0` which is multi-cell side-by-side)
- "dark stat card", "dark KPI card", "dark-mode metric tile", "暗色指标卡" → `add_stat_card_v1` (bg → surface; border → border; label → textMuted; icon → textSubtle; value → textPrimary in dark/system).
- "Continue with Google", "Sign in with Apple", "social login", "OAuth buttons", "SSO providers", "third-party login", "第三方登录", "社交登录", "OAuth 登录" → `add_social_login_row_v0` (orientation="vertical" for stacked full-width on mobile; orientation="horizontal" for the compact "or sign in with..." icon-only row)
- "dark social login", "dark-mode social auth buttons", "theme-aware social login", "暗色第三方登录" → `add_social_login_row_v1` (button bg → surface; border → border; icon → textMuted; label → textPrimary in dark/system).
- "pricing card", "plan card", "SaaS tier", "subscription plan", "pricing tier", "billing card", "价格卡", "套餐卡", "定价卡片" → `add_pricing_card_v0` (set one tile's `emphasis: "featured"` to visually recommend it — auto-gets "Most popular" badge unless `badge` overrides). For a 3-tier pricing section, call this 3× under the same parent section.
- "slider", "range input", "volume control", "opacity slider", "brightness slider", "filter slider", "滑块", "滑动条", "音量条" → `add_range_slider_v0` (single-handle; set `show_value=true` + `value_suffix="%"` to render the readout). For a dual-handle range (min+max), still fall through to batch_design.
- "dark slider", "dark-mode range input", "theme-aware slider", "暗色滑块" → `add_range_slider_v1` (accent invariant; thumb bg → surface; remaining → border; label/value tokenized)
@ -479,6 +497,7 @@ PREFER an element tool when the spec says any of:
- "newsletter signup", "subscribe form", "subscribe to newsletter", "promo code input", "apply discount", "send message input", "chat composer", "search with submit", "订阅", "应用优惠码", "发送消息" → `add_input_with_action_v0` (action_kind="text" for "Subscribe" pill button, action_kind="icon" for chat send arrow).
- "cookie banner", "cookie consent", "GDPR banner", "CCPA banner", "privacy notice", "cookie disclosure", "cookie 提示", "隐私同意条" → `add_cookie_banner_v0` (set `show_settings_link: true` for fine-grained GDPR consent UX). Caller positions sticky-bottom; the tool emits the banner card itself.
- "sidebar", "side nav", "sidebar nav", "left rail", "dashboard nav rail", "admin sidebar", "settings sidebar", "docs sidebar", "vertical nav", "侧边栏", "侧边导航", "左侧导航" → `add_sidebar_nav_v0` (desktop persistent rail; pass `title` for a brand row above the items, mark current page with `active: true` on the item).
- "dark sidebar", "dark-mode sidebar nav", "theme-aware sidebar", "暗色侧边栏" → `add_sidebar_nav_v1` (bg → surface; active label → textPrimary; inactive → textMuted; active item bg → surface2 in dark/system).
- "stacked avatars", "avatar group", "avatar stack", "team avatars", "5 contributors", "online users", "+N more", "viewers row", "presence indicator", "成员头像", "团队头像", "在线用户", "头像组" → `add_avatar_group_v0` (renders up to `max_visible` ringed avatar circles + a "+N" overflow tile; pen-core flex doesn't allow negative gap so the white ring + 4px gap is the affordance, not literal overlap).
- "data table row", "table row", "table header row", "customer row", "order row", "report row", "transaction row", "users table", "数据表行", "表格行", "表头行" → `add_data_table_row_v0` (desktop pattern; pass `header: true` for the column-header row, `selected: true` to tint a hover/selected body row). For row separators stack `add_divider_v0` between rows.
- "filter chip", "filter tag", "applied filter", "selected criterion", "category pill", "removable tag", "Status: Active ×", "可移除标签", "筛选标签" → `add_tag_v0` (single chip with optional × close icon, default removable=true; pass `tone` for accent / success / warning / error palettes).
@ -510,6 +529,7 @@ PREFER an element tool when the spec says any of:
- "audit log row", "activity feed entry", "recent activity item", "audit entry", "审计日志条目", "活动记录", "操作日志行" → `add_activity_log_v0` (single line: optional tinted icon dot + "<actor> <action>" + right-aligned timestamp).
- "event card", "agenda item", "meeting tile", "upcoming event", "calendar event row", "日程卡片", "会议条目", "活动卡片" → `add_event_card_v0` (date column with month band + day number, then title + time + location).
- "onboarding step", "how-it-works step", "tutorial step card", "setup checklist item", "操作步骤卡片", "教程步骤", "新手引导步骤" → `add_step_card_v0` (numbered circle / check + title + description, stacks vertically).
- "dark step card", "dark-mode onboarding step", "theme-aware step card", "暗色步骤卡片" → `add_step_card_v1` (title → textPrimary; desc → textMuted; ring bg → surface in dark/system).
STILL use batch_design when (emit a SINGLE batch_design for the whole response — do NOT mix with element-tool calls):

View file

@ -244,3 +244,25 @@ export { buildSectionHeaderV1, type SectionHeaderV1Params } from './section-head
export { buildSegmentedControlV1, type SegmentedControlV1Params } from './segmented-control-v1.js';
export { buildSelectV1, type SelectV1Params } from './select-v1.js';
export { buildShareRowV1, type ShareRowV1Params } from './share-row-v1.js';
export {
buildSidebarNavV1,
type SidebarNavV1Item,
type SidebarNavV1Params,
} from './sidebar-nav-v1.js';
export { buildSkeletonV1, type SkeletonV1Params } from './skeleton-v1.js';
export {
buildSocialLoginRowV1,
type SocialLoginV1Provider,
type SocialLoginRowV1Params,
} from './social-login-row-v1.js';
export { buildSpinnerV1, type SpinnerV1Params } from './spinner-v1.js';
export { buildStatCardV1, type StatCardV1Params, type StatCardV1Trend } from './stat-card-v1.js';
export { buildStatGridV1, type StatGridV1Item, type StatGridV1Params } from './stat-grid-v1.js';
export {
buildStatusBadgeV1,
type StatusBadgeV1Params,
type StatusBadgeV1Tone,
} from './status-badge-v1.js';
export { buildStepCardV1, type StepCardV1Params } from './step-card-v1.js';
export { buildStepperV1, type StepperV1Params } from './stepper-v1.js';
export { buildSwitchV1, type SwitchV1Params } from './switch-v1.js';

View file

@ -0,0 +1,138 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface SidebarNavV1Item {
label: string;
icon: string;
active?: boolean;
}
export interface SidebarNavV1Params {
items: SidebarNavV1Item[];
/** Optional brand / section title row above the items. */
title?: string;
/** Sidebar width in px. Default 240. Clamped 180..320. */
width?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_sidebar_nav_v0.
* - `'dark'`: sidebar bg → surface; title + active label → textPrimary;
* inactive label → textMuted; active item bg → surface2.
* - `'system'`: $color-* refs for all color slots.
*/
theme?: V1Theme;
}
/**
* Sidebar navigation (v1) — theme-aware variant of buildSidebarNav.
* Light mode is byte-equal to add_sidebar_nav_v0.
*
* Color mapping:
* sidebar bg (#FFFFFF) → surface token
* title text (#0F172A slate-900) → textPrimary token
* active label (#0F172A slate-900) → textPrimary token
* active item fill (#F1F5F9 slate-100) → surface2 token
* inactive label (#475569 slate-600) → textMuted token
*/
export function buildSidebarNavV1(params: SidebarNavV1Params): ElementTree {
const width = Math.min(320, Math.max(180, Math.floor(params.width ?? 240)));
const theme = params.theme ?? 'light';
const isLight = theme === 'light';
const t = resolveTheme(theme);
// Sidebar bg: light → #FFFFFF, dark/system → surface
const sidebarBg = isLight ? '#FFFFFF' : t.colors.surface;
// Title text: light → slate-900 (#0F172A), dark/system → textPrimary
const titleColor = isLight ? '#0F172A' : t.colors.textPrimary;
// Active label: light → slate-900 (#0F172A), dark/system → textPrimary
const activeLabelColor = isLight ? '#0F172A' : t.colors.textPrimary;
// Active item bg: light → slate-100 (#F1F5F9), dark/system → surface2
const activeItemBg = isLight ? '#F1F5F9' : t.colors.surface2;
// Inactive label: light → slate-600 (#475569), dark/system → textMuted
const inactiveLabelColor = isLight ? '#475569' : t.colors.textMuted;
const children: ElementTree[] = [];
if (params.title) {
children.push({
type: 'frame',
name: 'Sidebar Title',
role: 'sidebar-nav-title',
width: 'fill_container',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'center',
padding: [8, 12, 24, 12],
children: [
{
type: 'text',
name: 'Title',
role: 'sidebar-nav-title-text',
content: params.title,
fontSize: 16,
fontWeight: 700,
fill: [{ type: 'solid', color: titleColor }],
},
],
});
}
for (const item of params.items) {
children.push(buildItemV1(item, activeLabelColor, activeItemBg, inactiveLabelColor));
}
return {
type: 'frame',
name: 'Sidebar Nav',
role: 'sidebar-nav',
width,
height: 'fill_container',
layout: 'vertical',
gap: 4,
padding: [16, 12],
fill: [{ type: 'solid', color: sidebarBg }],
children,
};
}
function buildItemV1(
item: SidebarNavV1Item,
activeLabelColor: string,
activeItemBg: string,
inactiveLabelColor: string,
): ElementTree {
const active = item.active === true;
const node: ElementTree = {
type: 'frame',
name: `Item (${item.label})`,
role: active ? 'sidebar-nav-item-active' : 'sidebar-nav-item',
width: 'fill_container',
height: 40,
cornerRadius: 8,
layout: 'horizontal',
alignItems: 'center',
gap: 12,
padding: [0, 12],
children: [
{
type: 'icon_font',
name: 'Icon',
role: 'sidebar-nav-icon',
iconFontName: item.icon,
iconFontFamily: 'lucide',
width: 18,
height: 18,
},
{
type: 'text',
name: 'Label',
role: 'sidebar-nav-label',
content: item.label,
fontSize: 14,
fontWeight: active ? 600 : 500,
fill: [{ type: 'solid', color: active ? activeLabelColor : inactiveLabelColor }],
},
],
};
if (active) {
node.fill = [{ type: 'solid', color: activeItemBg }];
}
return node;
}

View file

@ -0,0 +1,79 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface SkeletonV1Params {
/** Number of skeleton rows to emit (clamped 1..20). Default 3. */
rows?: number;
/** Height per row in px (clamped 4..48). Default 16. */
row_height?: number;
/** Gap between rows in px (clamped 0..32). Default 12. */
row_gap?: number;
/**
* When true, the LAST row is 60% width (simulates an unfinished
* paragraph line — the classic "shimmer" pattern). Default true.
*/
last_row_short?: boolean;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_skeleton_v0 (slate-200 rows).
* - `'dark'`: row fill → surface2 (#334155) — visible against dark page bg.
* - `'system'`: $color-surface-2 ref for row fills.
*/
theme?: V1Theme;
}
/**
* Loading skeleton (v1) — theme-aware variant of buildSkeleton.
* Light mode is byte-equal to add_skeleton_v0.
*
* Color mapping:
* skeleton row fill (#E2E8F0 slate-200) → surface2 token
*/
export function buildSkeletonV1(params: SkeletonV1Params): ElementTree {
const rows = Math.max(1, Math.min(20, params.rows ?? 3));
const rowHeight = Math.max(4, Math.min(48, params.row_height ?? 16));
const rowGap = Math.max(0, Math.min(32, params.row_gap ?? 12));
const lastShort = params.last_row_short ?? true;
const theme = params.theme ?? 'light';
const isLight = theme === 'light';
const t = resolveTheme(theme);
// Row fill: light → slate-200 (#E2E8F0), dark/system → surface2
const rowFill = isLight ? '#E2E8F0' : t.colors.surface2;
const children: ElementTree[] = [];
for (let i = 0; i < rows; i++) {
const isLast = i === rows - 1;
if (isLast && lastShort && rows > 1) {
children.push({
type: 'rectangle',
role: 'skeleton-row',
width: 220,
height: rowHeight,
cornerRadius: 4,
fill: [{ type: 'solid', color: rowFill }],
});
} else {
children.push({
type: 'rectangle',
role: 'skeleton-row',
width: 'fill_container',
height: rowHeight,
cornerRadius: 4,
fill: [{ type: 'solid', color: rowFill }],
});
}
}
return {
type: 'frame',
name: 'Skeleton',
role: 'skeleton',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
gap: rowGap,
alignItems: 'start',
children,
};
}

View file

@ -0,0 +1,163 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface SocialLoginV1Provider {
/**
* Provider name. Displayed inline as "Continue with {name}".
* Known names ('google', 'apple', 'microsoft', 'github',
* 'facebook', 'twitter', 'x', 'linkedin') auto-resolve to the
* matching lucide icon. Override via explicit `icon`.
*/
name: string;
/**
* Optional lucide icon override. Takes precedence over the
* known-name mapping.
*/
icon?: string;
}
export interface SocialLoginRowV1Params {
/** Array of providers to render. 2-4 recommended; clamped to 6 max. */
providers: SocialLoginV1Provider[];
/**
* Layout orientation. Default 'vertical' (stacked full-width buttons).
* 'horizontal' renders icon-only pills side-by-side.
*/
orientation?: 'vertical' | 'horizontal';
/** Button width in px. Default 320. Min 200. Ignored on horizontal orientation. */
width?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_social_login_row_v0.
* - `'dark'`: button bg → surface; border → border; icon fill → textMuted;
* label → textPrimary.
* - `'system'`: $color-* refs for all color slots.
*/
theme?: V1Theme;
}
const KNOWN_ICONS: Record<string, string> = {
google: 'chrome',
apple: 'apple',
microsoft: 'monitor',
github: 'github',
gitlab: 'git-branch',
facebook: 'facebook',
twitter: 'twitter',
x: 'twitter',
linkedin: 'linkedin',
discord: 'message-circle',
slack: 'hash',
email: 'mail',
phone: 'smartphone',
};
/**
* Social-auth provider button row (v1) — theme-aware variant of buildSocialLoginRow.
* Light mode is byte-equal to add_social_login_row_v0.
*
* Color mapping:
* button bg (#FFFFFF white) → surface token
* border (#E2E8F0 slate-200) → border token
* icon fill (#334155 slate-700) → textMuted token
* label text (#0F172A slate-900) → textPrimary token
*/
export function buildSocialLoginRowV1(params: SocialLoginRowV1Params): ElementTree {
const raw = Array.isArray(params.providers) ? params.providers : [];
if (raw.length === 0) {
throw new Error('buildSocialLoginRowV1: providers array must not be empty');
}
const providers = raw.slice(0, 6);
const orientation = params.orientation ?? 'vertical';
const isVertical = orientation === 'vertical';
const width = Math.max(200, Math.floor(params.width ?? 320));
const theme = params.theme ?? 'light';
const isLight = theme === 'light';
const t = resolveTheme(theme);
// Button bg: light → white (#FFFFFF), dark/system → surface
const buttonBg = isLight ? '#FFFFFF' : t.colors.surface;
// Border: light → slate-200 (#E2E8F0), dark/system → border
const borderColor = isLight ? '#E2E8F0' : t.colors.border;
// Icon fill: light → slate-700 (#334155), dark/system → textMuted
const iconFill = isLight ? '#334155' : t.colors.textMuted;
// Label: light → slate-900 (#0F172A), dark/system → textPrimary
const labelColor = isLight ? '#0F172A' : t.colors.textPrimary;
const buttons: ElementTree[] = providers.map((provider) => {
const lowerName = provider.name.toLowerCase();
const icon = provider.icon ?? KNOWN_ICONS[lowerName] ?? 'log-in';
const label = `Continue with ${provider.name}`;
const prettyLabel =
provider.name.charAt(0) === provider.name.charAt(0).toUpperCase()
? label
: `Continue with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`;
const iconNode: ElementTree = {
type: 'icon_font',
name: `${provider.name} Icon`,
role: 'social-login-button-icon',
iconFontName: icon,
iconFontFamily: 'lucide',
width: 20,
height: 20,
fill: [{ type: 'solid', color: iconFill }],
};
if (isVertical) {
return {
type: 'frame',
name: `${provider.name} Button`,
role: 'social-login-button',
width: 'fill_container',
height: 48,
cornerRadius: 12,
layout: 'horizontal',
alignItems: 'center',
gap: 12,
padding: [0, 16],
fill: [{ type: 'solid', color: buttonBg }],
stroke: { thickness: 1, fill: [{ type: 'solid', color: borderColor }] },
children: [
iconNode,
{
type: 'text',
name: 'Label',
role: 'social-login-button-label',
content: prettyLabel,
fontSize: 14,
fontWeight: 500,
fill: [{ type: 'solid', color: labelColor }],
},
],
};
}
// Horizontal: icon-only square pill
return {
type: 'frame',
name: `${provider.name} Button`,
role: 'social-login-button-compact',
width: 48,
height: 48,
cornerRadius: 12,
layout: 'horizontal',
alignItems: 'center',
justifyContent: 'center',
fill: [{ type: 'solid', color: buttonBg }],
stroke: { thickness: 1, fill: [{ type: 'solid', color: borderColor }] },
children: [iconNode],
};
});
return {
type: 'frame',
name: 'Social Login Row',
role: 'social-login-row',
width: isVertical ? width : 'fit_content',
height: 'fit_content',
layout: isVertical ? 'vertical' : 'horizontal',
alignItems: 'center',
gap: 10,
children: buttons,
};
}

View file

@ -0,0 +1,77 @@
import type { ElementTree } from './helpers.js';
import type { V1Theme } from './resolve-theme.js';
export interface SpinnerV1Params {
/** Outer diameter in px. Default 32. Clamped 16..128. */
size?: number;
/** Stroke thickness in px. Default 3. Clamped 1..16. */
thickness?: number;
/** Track color (the static ring). Default #E2E8F0 (slate-200). */
track_color?: string;
/** Active arc color. Default #2563EB (blue-600). */
active_color?: string;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_spinner_v0.
* - `'dark'`: identical (track_color/active_color are caller-provided or
* iOS/Material defaults — these are UI state colors, not surface colors).
* - `'system'`: identical.
*
* NOTE: track_color and active_color are caller-overridable parameters and
* are treated as builder-private defaults (#E2E8F0 / #2563EB). They are
* not tokenized across theme modes — use explicit track_color/active_color
* overrides if you need theme-specific spinner colors.
*/
theme?: V1Theme;
}
/**
* Loading spinner (v1) — theme-aware variant of buildSpinner.
* Light mode is byte-equal to add_spinner_v0.
*
* track_color and active_color are caller-controllable defaults.
* Since no surface/text colors are hardcoded in the v0 builder body
* (only the param defaults #E2E8F0 and #2563EB), all theme modes
* emit the same tree — the caller provides explicit colors when needed.
*/
export function buildSpinnerV1(params: SpinnerV1Params): ElementTree {
const size = Math.max(16, Math.min(128, Math.floor(params.size ?? 32)));
const thickness = Math.max(1, Math.min(16, Math.floor(params.thickness ?? 3)));
const trackColor = params.track_color ?? '#E2E8F0';
const activeColor = params.active_color ?? '#2563EB';
return {
type: 'frame',
name: 'Spinner',
role: 'spinner',
width: size,
height: size,
layout: 'none',
children: [
{
type: 'ellipse',
name: 'Track',
role: 'spinner-track',
x: 0,
y: 0,
width: size,
height: size,
fill: [],
stroke: { thickness, fill: [{ type: 'solid', color: trackColor }] },
},
{
type: 'ellipse',
name: 'Active Arc',
role: 'spinner-arc',
x: 0,
y: 0,
width: size,
height: size,
startAngle: -90,
sweepAngle: 270,
fill: [],
stroke: { thickness, fill: [{ type: 'solid', color: activeColor }] },
},
],
};
}

View file

@ -0,0 +1,146 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export type StatCardV1Trend = 'up' | 'down' | 'flat';
export interface StatCardV1Params {
/** Small label above the big number (e.g. "Monthly revenue"). Required. */
label: string;
/** The big number / primary metric (e.g. "$12.4k", "1,284", "98.2%"). Required. */
value: string;
/** Optional leading icon (lucide name, e.g. "trending-up" / "users" / "flame"). */
icon?: string;
/**
* Optional delta text shown beneath the big number (e.g. "+8% vs last week").
* Rendered in a tone-matched color.
*/
delta?: string;
/** Trend direction for the delta tone. Default 'flat'. */
trend?: StatCardV1Trend;
/** Card width in px. Default 240. Min 160. */
width?: number;
/** Corner radius. Default 16. */
corner_radius?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_stat_card_v0.
* - `'dark'`: card bg → surface; border → border; label → textMuted;
* icon fill → textSubtle; value → textPrimary. Delta tone colors
* (success/destructive/slate) are kept hardcoded — status semantics.
* - `'system'`: $color-* refs for surface/border/text slots.
*/
theme?: V1Theme;
}
/**
* Stat card (v1) — theme-aware variant of buildStatCard.
* Light mode is byte-equal to add_stat_card_v0.
*
* Color mapping:
* card bg (#FFFFFF white) → surface token
* border (#E2E8F0 slate-200) → border token
* label text (#64748B slate-500) → textMuted token
* icon fill (#94A3B8 slate-400) → textSubtle token
* value text (#0F172A slate-900) → textPrimary token
*
* Delta colors stay hardcoded (status semantics, theme-independent):
* up → #10B981 (emerald-500)
* down → #EF4444 (red-500)
* flat → #64748B (slate-500)
*/
export function buildStatCardV1(params: StatCardV1Params): ElementTree {
const width = Math.max(160, Math.floor(params.width ?? 240));
const cornerRadius = Math.max(0, Math.floor(params.corner_radius ?? 16));
const trend: StatCardV1Trend = params.trend ?? 'flat';
const deltaColor = trend === 'up' ? '#10B981' : trend === 'down' ? '#EF4444' : '#64748B';
const theme = params.theme ?? 'light';
const isLight = theme === 'light';
const t = resolveTheme(theme);
// Card bg: light → #FFFFFF, dark/system → surface
const cardBg = isLight ? '#FFFFFF' : t.colors.surface;
// Border: light → slate-200 (#E2E8F0), dark/system → border
const borderColor = isLight ? '#E2E8F0' : t.colors.border;
// Label text: light → slate-500 (#64748B), dark/system → textMuted
const labelColor = isLight ? '#64748B' : t.colors.textMuted;
// Icon fill: light → slate-400 (#94A3B8), dark/system → textSubtle
const iconColor = isLight ? '#94A3B8' : t.colors.textSubtle;
// Value text: light → slate-900 (#0F172A), dark/system → textPrimary
const valueColor = isLight ? '#0F172A' : t.colors.textPrimary;
const headerChildren: ElementTree[] = [
{
type: 'text',
name: 'Label',
role: 'stat-card-label',
content: params.label.toUpperCase(),
fontSize: 12,
fontWeight: 500,
letterSpacing: 1,
fill: [{ type: 'solid', color: labelColor }],
},
];
if (params.icon) {
headerChildren.push({
type: 'icon_font',
name: 'Icon',
role: 'stat-card-icon',
iconFontName: params.icon,
iconFontFamily: 'lucide',
width: 18,
height: 18,
fill: [{ type: 'solid', color: iconColor }],
});
}
const cardChildren: ElementTree[] = [
{
type: 'frame',
name: 'Header',
role: 'stat-card-header',
width: 'fill_container',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'center',
justifyContent: 'space-between',
children: headerChildren,
},
{
type: 'text',
name: 'Value',
role: 'stat-card-value',
content: params.value,
fontSize: 32,
fontWeight: 700,
lineHeight: 1.1,
fill: [{ type: 'solid', color: valueColor }],
},
];
if (params.delta) {
cardChildren.push({
type: 'text',
name: 'Delta',
role: 'stat-card-delta',
content: params.delta,
fontSize: 12,
fontWeight: 500,
fill: [{ type: 'solid', color: deltaColor }],
});
}
return {
type: 'frame',
name: 'Stat Card',
role: 'stat-card',
width,
height: 'fit_content',
cornerRadius,
layout: 'vertical',
gap: 12,
padding: 20,
fill: [{ type: 'solid', color: cardBg }],
stroke: { thickness: 1, fill: [{ type: 'solid', color: borderColor }] },
children: cardChildren,
};
}

View file

@ -0,0 +1,88 @@
import type { ElementTree } from './helpers.js';
import type { V1Theme } from './resolve-theme.js';
export interface StatGridV1Item {
value: string;
label: string;
icon?: string;
}
export interface StatGridV1Params {
items: StatGridV1Item[];
gap?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_stat_grid_v0.
* - `'dark'`: identical (no explicit fill colors in v0 — text inherits).
* - `'system'`: identical.
*/
theme?: V1Theme;
}
/**
* Stat grid (v1) — theme-aware variant of buildStatGrid.
* No hardcoded colors in v0 (text inherits canvas theme color),
* so all theme modes are identical and byte-parity with v0 is guaranteed.
* Accepts theme param for API consistency.
*
* Non-scrolling stat grid — 2-5 items share the row via fill_container.
*/
export function buildStatGridV1(params: StatGridV1Params): ElementTree {
const gap = params.gap ?? 16;
const cells = params.items.map((item) => buildCellV1(item));
return {
type: 'frame',
name: 'Stat Grid',
role: 'stat-grid',
width: 'fill_container',
height: 'fit_content',
layout: 'horizontal',
gap,
alignItems: 'center',
justifyContent: 'space_between',
children: cells,
};
}
function buildCellV1(item: StatGridV1Item): ElementTree {
const children: ElementTree[] = [];
if (item.icon) {
children.push({
type: 'icon_font',
name: 'Icon',
iconFontName: item.icon,
iconFontFamily: 'lucide',
width: 20,
height: 20,
});
}
children.push({
type: 'text',
name: 'Value',
role: 'heading',
content: item.value,
fontSize: 24,
fontWeight: 700,
width: 'fill_container',
});
children.push({
type: 'text',
name: 'Label',
role: 'body',
content: item.label,
fontSize: 12,
fontWeight: 500,
width: 'fill_container',
});
return {
type: 'frame',
name: 'Stat Cell',
role: 'stat-cell',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
alignItems: 'center',
gap: 4,
children,
};
}

View file

@ -0,0 +1,86 @@
import type { ElementTree } from './helpers.js';
import type { V1Theme } from './resolve-theme.js';
export type StatusBadgeV1Tone = 'success' | 'warning' | 'error' | 'info' | 'neutral';
export interface StatusBadgeV1Params {
label: string;
/**
* Semantic tone — picks the dot color. Default 'neutral'.
*/
tone?: StatusBadgeV1Tone;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_status_badge_v0.
* - `'dark'`: identical (dot colors are status semantics, theme-independent).
* - `'system'`: identical.
*
* NOTE: status tone colors (success=emerald, warning=amber, error=red,
* info=blue, neutral=slate) are intentionally theme-independent and kept
* hardcoded across all theme modes per spec §3.4.
*/
theme?: V1Theme;
}
/**
* Status badge (v1) — theme-aware variant of buildStatusBadge.
* Light mode is byte-equal to add_status_badge_v0.
*
* Dot colors are status semantics (not surface colors), kept hardcoded
* across all theme modes. Accepts theme param for API consistency.
*
* Structure:
* frame(fit_content, horizontal, gap=6, alignItems=center)
* ├ frame(8×8, cornerRadius=4, fill=<toneColor>, role='status-dot')
* └ text(label, 13/500)
*/
export function buildStatusBadgeV1(params: StatusBadgeV1Params): ElementTree {
const tone: StatusBadgeV1Tone = params.tone ?? 'neutral';
const dotColor = dotColorForTone(tone);
return {
type: 'frame',
name: 'Status Badge',
role: 'status-badge',
width: 'fit_content',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'center',
gap: 6,
children: [
{
type: 'frame',
name: 'Status Dot',
role: 'status-dot',
width: 8,
height: 8,
cornerRadius: 4,
fill: [{ type: 'solid', color: dotColor }],
},
{
type: 'text',
name: 'Label',
role: 'status-label',
content: params.label,
fontSize: 13,
fontWeight: 500,
},
],
};
}
function dotColorForTone(tone: StatusBadgeV1Tone): string {
switch (tone) {
case 'success':
return '#10B981'; // emerald-500
case 'warning':
return '#F59E0B'; // amber-500
case 'error':
return '#EF4444'; // red-500
case 'info':
return '#3B82F6'; // blue-500
case 'neutral':
default:
return '#94A3B8'; // slate-400
}
}

View file

@ -0,0 +1,142 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface StepCardV1Params {
/**
* Step index — short label rendered inside the 36px marker circle.
* Keep it 1–3 characters (e.g. 1, "01", "1.1").
*/
number: string | number;
/** Step title (16/600). */
title: string;
/** Description body (14/400, muted). */
description: string;
/** Whether the step is completed. Default false. Filled circle vs ring. */
completed?: boolean;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_step_card_v0.
* - `'dark'`: title text → textPrimary; description → textMuted;
* incomplete circle bg → surface (instead of #FFFFFF). Accent stays
* hardcoded (#2563EB) — brand color, theme-independent.
* - `'system'`: $color-* refs for title, description, and incomplete circle bg.
*/
theme?: V1Theme;
}
const ACCENT = '#2563EB';
/**
* Step card (v1) — theme-aware variant of buildStepCard.
* Light mode is byte-equal to add_step_card_v0.
*
* Color mapping:
* title text (#0F172A slate-900) → textPrimary token
* desc text (#475569 slate-600) → textMuted token
* ring bg (#FFFFFF white) → surface token (incomplete state only)
* accent (#2563EB blue-600) → kept hardcoded (brand color)
* check icon (#FFFFFF white) → kept hardcoded (on-accent always white)
*/
export function buildStepCardV1(params: StepCardV1Params): ElementTree {
const completed = params.completed === true;
const numberText = String(params.number);
const theme = params.theme ?? 'light';
const isLight = theme === 'light';
const t = resolveTheme(theme);
// Title text: light → slate-900 (#0F172A), dark/system → textPrimary
const titleColor = isLight ? '#0F172A' : t.colors.textPrimary;
// Description text: light → slate-600 (#475569), dark/system → textMuted
const descColor = isLight ? '#475569' : t.colors.textMuted;
// Incomplete circle bg: light → #FFFFFF, dark/system → surface
const ringBg = isLight ? '#FFFFFF' : t.colors.surface;
const circle: ElementTree = {
type: 'frame',
name: 'Number Circle',
role: 'step-card-circle',
width: 36,
height: 36,
cornerRadius: 18,
fill: [{ type: 'solid', color: completed ? ACCENT : ringBg }],
stroke: completed ? undefined : { thickness: 2, fill: [{ type: 'solid', color: ACCENT }] },
clipContent: true,
layout: 'horizontal',
alignItems: 'center',
justifyContent: 'center',
children: completed
? [
{
type: 'icon_font',
name: 'Check',
role: 'step-card-check',
iconFontName: 'check',
iconFontFamily: 'lucide',
width: 18,
height: 18,
fill: [{ type: 'solid', color: '#FFFFFF' }],
},
]
: [
{
type: 'text',
name: 'Number',
role: 'step-card-number',
content: numberText,
fontSize: 15,
fontWeight: 700,
fill: [{ type: 'solid', color: ACCENT }],
},
],
};
return {
type: 'frame',
name: 'Step Card',
role: 'step-card',
width: 'fill_container',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'flex-start',
gap: 14,
padding: [4, 0],
children: [
circle,
{
type: 'frame',
name: 'Body',
role: 'step-card-body',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
gap: 4,
padding: [4, 0, 0, 0],
children: [
{
type: 'text',
name: 'Title',
role: 'step-card-title',
content: params.title,
fontSize: 16,
fontWeight: 600,
width: 'fill_container',
textGrowth: 'fixed-width',
fill: [{ type: 'solid', color: titleColor }],
},
{
type: 'text',
name: 'Description',
role: 'step-card-description',
content: params.description,
fontSize: 14,
fontWeight: 400,
lineHeight: 1.5,
width: 'fill_container',
textGrowth: 'fixed-width',
fill: [{ type: 'solid', color: descColor }],
},
],
},
],
};
}

View file

@ -0,0 +1,89 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface StepperV1Params {
total: number;
current?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_stepper_v0.
* - `'dark'`: pending circle fill → border (#334155); pending number → textMuted;
* pending connector → border. Accent (#2563EB) and white (#FFFFFF on done)
* stay hardcoded — brand/UI constants.
* - `'system'`: $color-* refs for pending fill, pending number color, pending connector.
*/
theme?: V1Theme;
}
/**
* Stepper (v1) — theme-aware variant of buildStepper.
* Light mode is byte-equal to add_stepper_v0.
*
* Color mapping:
* done circle fill (#2563EB blue-600) → kept hardcoded (accent/brand)
* done number text (#FFFFFF white) → kept hardcoded (on-accent)
* done connector (#2563EB blue-600) → kept hardcoded (accent/brand)
* pending circle fill (#E5E7EB gray-200) → border token
* pending number text (#6B7280 gray-500) → textMuted token
* pending connector (#E5E7EB gray-200) → border token
*/
export function buildStepperV1(params: StepperV1Params): ElementTree {
const total = Math.max(1, Math.floor(params.total));
const current = Math.max(0, Math.min(total - 1, Math.floor(params.current ?? 0)));
const theme = params.theme ?? 'light';
const isLight = theme === 'light';
const t = resolveTheme(theme);
// Pending circle fill: light → gray-200 (#E5E7EB), dark/system → border
const pendingFill = isLight ? '#E5E7EB' : t.colors.border;
// Pending number text: light → gray-500 (#6B7280), dark/system → textMuted
const pendingText = isLight ? '#6B7280' : t.colors.textMuted;
const children: ElementTree[] = [];
for (let i = 0; i < total; i += 1) {
const done = i <= current;
children.push({
type: 'frame',
name: `Step ${i + 1}`,
role: done ? 'step-active' : 'step',
width: 24,
height: 24,
cornerRadius: 12,
fill: [{ type: 'solid', color: done ? '#2563EB' : pendingFill }],
layout: 'horizontal',
alignItems: 'center',
justifyContent: 'center',
children: [
{
type: 'text',
name: 'Number',
content: String(i + 1),
fontSize: 13,
fontWeight: 600,
fill: [{ type: 'solid', color: done ? '#FFFFFF' : pendingText }],
},
],
});
if (i < total - 1) {
const doneConnector = i < current;
children.push({
type: 'rectangle',
name: `Connector ${i}`,
role: doneConnector ? 'step-connector-active' : 'step-connector',
width: 'fill_container',
height: 2,
fill: [{ type: 'solid', color: doneConnector ? '#2563EB' : pendingFill }],
});
}
}
return {
type: 'frame',
name: 'Stepper',
role: 'stepper',
width: 'fill_container',
layout: 'horizontal',
alignItems: 'center',
gap: 0,
children,
};
}

View file

@ -0,0 +1,56 @@
import type { ElementTree } from './helpers.js';
import type { V1Theme } from './resolve-theme.js';
export interface SwitchV1Params {
active?: boolean;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_switch_v0.
* - `'dark'`: identical (iOS HIG values #34C759/#E5E5EA are builder-private
* literals per spec §3.4 — not tokenized across theme modes).
* - `'system'`: identical.
*
* NOTE: #34C759 (iOS green) and #E5E5EA (iOS off-gray) are iOS HIG
* builder-private constants. #FFFFFF (thumb) is a gray-scale constant.
* All stay hardcoded across all theme modes per spec §3.4.
*/
theme?: V1Theme;
}
/**
* Toggle switch (v1) — theme-aware variant of buildSwitch.
* Light mode is byte-equal to add_switch_v0.
*
* iOS/Material toggle switch. Fixed 51×31 (iOS HIG), thumb 27×27 white.
* active=true → iOS green track + thumb pushed right.
*
* #34C759 and #E5E5EA are iOS HIG builder-private literals — not surface
* colors — so they remain hardcoded across all three theme modes.
*/
export function buildSwitchV1(params: SwitchV1Params): ElementTree {
const active = params.active === true;
return {
type: 'frame',
name: active ? 'Switch (on)' : 'Switch (off)',
role: 'switch',
width: 51,
height: 31,
cornerRadius: 16,
fill: [{ type: 'solid', color: active ? '#34C759' : '#E5E5EA' }],
layout: 'horizontal',
alignItems: 'center',
justifyContent: active ? 'flex-end' : 'flex-start',
padding: [2],
children: [
{
type: 'frame',
name: 'Thumb',
role: 'switch-thumb',
width: 27,
height: 27,
cornerRadius: 14,
fill: [{ type: 'solid', color: '#FFFFFF' }],
},
],
};
}

View file

@ -579,4 +579,29 @@ export {
type SelectV1Params,
buildShareRowV1,
type ShareRowV1Params,
buildSidebarNavV1,
type SidebarNavV1Item,
type SidebarNavV1Params,
buildSkeletonV1,
type SkeletonV1Params,
buildSocialLoginRowV1,
type SocialLoginV1Provider,
type SocialLoginRowV1Params,
buildSpinnerV1,
type SpinnerV1Params,
buildStatCardV1,
type StatCardV1Params,
type StatCardV1Trend,
buildStatGridV1,
type StatGridV1Item,
type StatGridV1Params,
buildStatusBadgeV1,
type StatusBadgeV1Params,
type StatusBadgeV1Tone,
buildStepCardV1,
type StepCardV1Params,
buildStepperV1,
type StepperV1Params,
buildSwitchV1,
type SwitchV1Params,
} from './element-builders/index.js';

View file

@ -1,6 +1,8 @@
// Extension tool definitions — shard 8 of 8 (siblings: base / ext /
// ext-2 / ext-3 / ext-4 / ext-5 / ext-6 / ext-7). Houses P3 batch-7 v1 tools
// (search_bar, segmented_control, select, share_row, range_slider).
// (search_bar, segmented_control, select, share_row, range_slider) and
// P3 batch-8 v1 tools (sidebar_nav, skeleton, social_login_row, spinner,
// stat_card, stat_grid, status_badge, step_card, stepper, switch).
// Each shard caps at the repo's 800-line ceiling.
//
// When adding a new tool: pick whichever shard has the fewest tools
@ -190,4 +192,339 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_8 = [
required: [],
},
},
{
name: 'add_sidebar_nav_v1',
description:
'Theme-aware vertical sidebar navigation (v1). theme="light" (default): byte-parity ' +
'with add_sidebar_nav_v0. theme="dark": bg → surface (#1E293B); title + active label ' +
'→ textPrimary; inactive label → textMuted; active item bg → surface2. ' +
'theme="system": $color-* refs. Desktop dashboard / docs / admin left rail. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
items: {
type: 'array',
items: {
type: 'object',
properties: {
label: { type: 'string' },
icon: { type: 'string', description: 'Lucide icon slug.' },
active: { type: 'boolean', description: 'Marks item as currently selected.' },
},
required: ['label', 'icon'],
},
description: 'Navigation items. Required.',
},
title: { type: 'string', description: 'Optional brand/section title above items.' },
width: {
type: 'number',
description: 'Sidebar width in px. Default 240. Clamped 180..320.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['items'],
},
},
{
name: 'add_skeleton_v1',
description:
'Theme-aware loading skeleton (v1). theme="light" (default): byte-parity with ' +
'add_skeleton_v0 (slate-200 rows). theme="dark": row fill → surface2 (#334155) — ' +
'visible on dark bg. theme="system": $color-surface-2 ref. Stacked gray bars ' +
'mimicking text lines. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
rows: {
type: 'number',
description: 'Number of skeleton rows (clamped 1..20). Default 3.',
},
row_height: {
type: 'number',
description: 'Row height in px (clamped 4..48). Default 16.',
},
row_gap: {
type: 'number',
description: 'Gap between rows in px (clamped 0..32). Default 12.',
},
last_row_short: {
type: 'boolean',
description: 'Last row is 60% width (paragraph-end pattern). Default true.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: [],
},
},
{
name: 'add_social_login_row_v1',
description:
'Theme-aware social-auth provider button row (v1). theme="light" (default): byte-parity ' +
'with add_social_login_row_v0. theme="dark": button bg → surface; border → border; ' +
'icon fill → textMuted; label → textPrimary. theme="system": $color-* refs. ' +
'"Continue with Google / Apple / GitHub" pattern. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
providers: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Provider name (e.g. "google", "apple").' },
icon: { type: 'string', description: 'Optional lucide icon override.' },
},
required: ['name'],
},
description: 'Providers to render (2-4 recommended, max 6). Required.',
},
orientation: {
type: 'string',
enum: ['vertical', 'horizontal'],
description: 'Layout orientation. Default "vertical" (stacked full-width buttons).',
},
width: {
type: 'number',
description: 'Button width in px (vertical only). Default 320. Min 200.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['providers'],
},
},
{
name: 'add_spinner_v1',
description:
'Theme-aware loading spinner (v1). theme="light" (default): byte-parity with ' +
'add_spinner_v0. All theme modes identical — track_color/active_color are caller-' +
'overridable params (default #E2E8F0 track, #2563EB arc). Accepts theme for API ' +
'consistency. 3/4-sweep arc; static still-frame. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
size: {
type: 'number',
description: 'Outer diameter in px (clamped 16..128). Default 32.',
},
thickness: {
type: 'number',
description: 'Stroke thickness in px (clamped 1..16). Default 3.',
},
track_color: { type: 'string', description: 'Static ring color. Default "#E2E8F0".' },
active_color: { type: 'string', description: 'Active arc color. Default "#2563EB".' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light". All modes identical for this tool.',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: [],
},
},
{
name: 'add_stat_card_v1',
description:
'Theme-aware big-number stat card (v1). theme="light" (default): byte-parity with ' +
'add_stat_card_v0. theme="dark": bg → surface; border → border; label → textMuted; ' +
'icon → textSubtle; value → textPrimary. Delta tones (success/error/flat) stay ' +
'hardcoded — status semantics. theme="system": $color-* refs. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
label: { type: 'string', description: 'Metric label shown above value. Required.' },
value: { type: 'string', description: 'Primary metric value (e.g. "$12.4k"). Required.' },
icon: { type: 'string', description: 'Optional lucide icon in header corner.' },
delta: { type: 'string', description: 'Optional delta text (e.g. "+8% vs last week").' },
trend: {
type: 'string',
enum: ['up', 'down', 'flat'],
description: 'Trend direction for delta tone. Default "flat".',
},
width: { type: 'number', description: 'Card width in px. Default 240. Min 160.' },
corner_radius: { type: 'number', description: 'Corner radius. Default 16.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['label', 'value'],
},
},
{
name: 'add_stat_grid_v1',
description:
'Theme-aware non-scrolling stat grid (v1). theme="light" (default): byte-parity with ' +
'add_stat_grid_v0. All modes identical — no explicit fill colors in v0 (text inherits). ' +
'Accepts theme for API consistency. 2-5 fill_container cells side-by-side. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
items: {
type: 'array',
items: {
type: 'object',
properties: {
value: { type: 'string', description: 'Metric value (e.g. "1,284").' },
label: { type: 'string', description: 'Metric label.' },
icon: { type: 'string', description: 'Optional lucide icon slug.' },
},
required: ['value', 'label'],
},
description: 'Stat items (2-5). Required.',
},
gap: { type: 'number', description: 'Gap between cells in px. Default 16.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light". All modes identical for this tool.',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['items'],
},
},
{
name: 'add_status_badge_v1',
description:
'Theme-aware status indicator pill (v1). theme="light" (default): byte-parity with ' +
'add_status_badge_v0. All modes identical — dot colors are status semantics ' +
'(success=emerald, warning=amber, error=red, info=blue, neutral=slate), kept ' +
'hardcoded. Accepts theme for API consistency. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
label: { type: 'string', description: 'Status label text. Required.' },
tone: {
type: 'string',
enum: ['success', 'warning', 'error', 'info', 'neutral'],
description: 'Status tone. Default "neutral".',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light". All modes identical for this tool.',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['label'],
},
},
{
name: 'add_step_card_v1',
description:
'Theme-aware onboarding step card (v1). theme="light" (default): byte-parity with ' +
'add_step_card_v0. theme="dark": title → textPrimary; description → textMuted; ' +
'incomplete circle bg → surface. Accent (#2563EB) and check icon white stay hardcoded. ' +
'theme="system": $color-* refs. Numbered circle + title + description. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
number: {
description: 'Step index shown in circle (1-3 chars, e.g. 1, "01"). Required.',
oneOf: [{ type: 'string' }, { type: 'number' }],
},
title: { type: 'string', description: 'Step title. Required.' },
description: { type: 'string', description: 'Step description body. Required.' },
completed: { type: 'boolean', description: 'Filled accent circle vs ring. Default false.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['number', 'title', 'description'],
},
},
{
name: 'add_stepper_v1',
description:
'Theme-aware horizontal numbered stepper (v1). theme="light" (default): byte-parity ' +
'with add_stepper_v0. theme="dark": pending circle fill → border (#334155); pending ' +
'number → textMuted; pending connector → border. Accent (#2563EB) and done-state ' +
'white stay hardcoded. theme="system": $color-* refs for pending slots. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
total: { type: 'number', description: 'Total step count. Required.' },
current: { type: 'number', description: 'Current active step index (0-based). Default 0.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['total'],
},
},
{
name: 'add_switch_v1',
description:
'Theme-aware iOS/Material toggle switch (v1). theme="light" (default): byte-parity ' +
'with add_switch_v0. All modes identical — #34C759 (iOS green active) and #E5E5EA ' +
'(iOS gray inactive) are builder-private iOS HIG literals (spec §3.4), not tokenized. ' +
'#FFFFFF thumb stays hardcoded. 51×31px, cornerRadius=16. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
active: { type: 'boolean', description: 'Switch on/off state. Default false.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light". All modes identical for this tool.',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: [],
},
},
];

View file

@ -182,6 +182,16 @@ import { handleAddSectionHeaderV1 } from '../tools/add-section-header-v1';
import { handleAddSegmentedControlV1 } from '../tools/add-segmented-control-v1';
import { handleAddSelectV1 } from '../tools/add-select-v1';
import { handleAddShareRowV1 } from '../tools/add-share-row-v1';
import { handleAddSidebarNavV1 } from '../tools/add-sidebar-nav-v1';
import { handleAddSkeletonV1 } from '../tools/add-skeleton-v1';
import { handleAddSocialLoginRowV1 } from '../tools/add-social-login-row-v1';
import { handleAddSpinnerV1 } from '../tools/add-spinner-v1';
import { handleAddStatCardV1 } from '../tools/add-stat-card-v1';
import { handleAddStatGridV1 } from '../tools/add-stat-grid-v1';
import { handleAddStatusBadgeV1 } from '../tools/add-status-badge-v1';
import { handleAddStepCardV1 } from '../tools/add-step-card-v1';
import { handleAddStepperV1 } from '../tools/add-stepper-v1';
import { handleAddSwitchV1 } from '../tools/add-switch-v1';
import { recordElementToolCall } from '../metrics/element-tool-metrics';
import { ELEMENT_TOOL_DEFINITIONS_BASE } from './element-tool-defs-base';
import { ELEMENT_TOOL_DEFINITIONS_EXT } from './element-tool-defs-ext';
@ -558,6 +568,26 @@ async function dispatchElementToolCall(name: string, a: any): Promise<string> {
return JSON.stringify(await handleAddSelectV1(a), null, 2);
case 'add_share_row_v1':
return JSON.stringify(await handleAddShareRowV1(a), null, 2);
case 'add_sidebar_nav_v1':
return JSON.stringify(await handleAddSidebarNavV1(a), null, 2);
case 'add_skeleton_v1':
return JSON.stringify(await handleAddSkeletonV1(a), null, 2);
case 'add_social_login_row_v1':
return JSON.stringify(await handleAddSocialLoginRowV1(a), null, 2);
case 'add_spinner_v1':
return JSON.stringify(await handleAddSpinnerV1(a), null, 2);
case 'add_stat_card_v1':
return JSON.stringify(await handleAddStatCardV1(a), null, 2);
case 'add_stat_grid_v1':
return JSON.stringify(await handleAddStatGridV1(a), null, 2);
case 'add_status_badge_v1':
return JSON.stringify(await handleAddStatusBadgeV1(a), null, 2);
case 'add_step_card_v1':
return JSON.stringify(await handleAddStepCardV1(a), null, 2);
case 'add_stepper_v1':
return JSON.stringify(await handleAddStepperV1(a), null, 2);
case 'add_switch_v1':
return JSON.stringify(await handleAddSwitchV1(a), null, 2);
default:
return '';
}

View file

@ -0,0 +1,28 @@
import {
assignIdsRecursively,
buildSidebarNavV1,
type SidebarNavV1Params,
} from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddSidebarNavV1Params extends SidebarNavV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
export type { SidebarNavV1Item as AddSidebarNavV1Item } from '@zseven-w/pen-core';
/**
* Persistent vertical sidebar navigation (v1) — theme-aware variant of add_sidebar_nav_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
*/
export async function handleAddSidebarNavV1(
params: AddSidebarNavV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildSidebarNavV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'sidebarNav', tree, ...params });
}

View file

@ -0,0 +1,22 @@
import { assignIdsRecursively, buildSkeletonV1, type SkeletonV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddSkeletonV1Params extends SkeletonV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Loading skeleton (v1) — theme-aware variant of add_skeleton_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
*/
export async function handleAddSkeletonV1(
params: AddSkeletonV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildSkeletonV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'skeleton', tree, ...params });
}

View file

@ -0,0 +1,28 @@
import {
assignIdsRecursively,
buildSocialLoginRowV1,
type SocialLoginRowV1Params,
} from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddSocialLoginRowV1Params extends SocialLoginRowV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
export type { SocialLoginV1Provider as AddSocialLoginV1Provider } from '@zseven-w/pen-core';
/**
* Social-auth provider button row (v1) — theme-aware variant of add_social_login_row_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
*/
export async function handleAddSocialLoginRowV1(
params: AddSocialLoginRowV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildSocialLoginRowV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'socialLoginRow', tree, ...params });
}

View file

@ -0,0 +1,23 @@
import { assignIdsRecursively, buildSpinnerV1, type SpinnerV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddSpinnerV1Params extends SpinnerV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Loading spinner (v1) — theme-aware variant of add_spinner_v0.
* Accepts theme param for API consistency. track_color/active_color are
* caller-overridable; no surface colors are hardcoded in the builder.
*/
export async function handleAddSpinnerV1(
params: AddSpinnerV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildSpinnerV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'spinner', tree, ...params });
}

View file

@ -0,0 +1,23 @@
import { assignIdsRecursively, buildStatCardV1, type StatCardV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddStatCardV1Params extends StatCardV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Big-number stat card (v1) — theme-aware variant of add_stat_card_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Delta tone colors (success/error/flat) are kept hardcoded — status semantics.
*/
export async function handleAddStatCardV1(
params: AddStatCardV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildStatCardV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'statCard', tree, ...params });
}

View file

@ -0,0 +1,25 @@
import { assignIdsRecursively, buildStatGridV1, type StatGridV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddStatGridV1Params extends StatGridV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
export type { StatGridV1Item as AddStatGridV1Item } from '@zseven-w/pen-core';
/**
* Stat grid (v1) — theme-aware variant of add_stat_grid_v0.
* No hardcoded colors in v0 (text inherits). All modes are byte-parity with v0.
* Accepts theme param for API consistency.
*/
export async function handleAddStatGridV1(
params: AddStatGridV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildStatGridV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'statGrid', tree, ...params });
}

View file

@ -0,0 +1,27 @@
import {
assignIdsRecursively,
buildStatusBadgeV1,
type StatusBadgeV1Params,
} from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddStatusBadgeV1Params extends StatusBadgeV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Status badge (v1) — theme-aware variant of add_status_badge_v0.
* Dot tone colors are status semantics (kept hardcoded). All modes byte-parity with v0.
* Accepts theme param for API consistency.
*/
export async function handleAddStatusBadgeV1(
params: AddStatusBadgeV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildStatusBadgeV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'statusBadge', tree, ...params });
}

View file

@ -0,0 +1,23 @@
import { assignIdsRecursively, buildStepCardV1, type StepCardV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddStepCardV1Params extends StepCardV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Onboarding step card (v1) — theme-aware variant of add_step_card_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Accent (#2563EB) and check-icon white (#FFFFFF) stay hardcoded.
*/
export async function handleAddStepCardV1(
params: AddStepCardV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildStepCardV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'stepCard', tree, ...params });
}

View file

@ -0,0 +1,23 @@
import { assignIdsRecursively, buildStepperV1, type StepperV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddStepperV1Params extends StepperV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Horizontal numbered stepper (v1) — theme-aware variant of add_stepper_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Accent (#2563EB) and done-state white (#FFFFFF) stay hardcoded.
*/
export async function handleAddStepperV1(
params: AddStepperV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildStepperV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'stepper', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildSwitchV1, type SwitchV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddSwitchV1Params extends SwitchV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Toggle switch (v1) — theme-aware variant of add_switch_v0.
* iOS HIG values (#34C759 active, #E5E5EA inactive, #FFFFFF thumb) are builder-private
* literals (spec §3.4) — kept hardcoded across all theme modes.
* Accepts theme param for API consistency.
*/
export async function handleAddSwitchV1(
params: AddSwitchV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildSwitchV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'switch', tree, ...params });
}