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:
parent
e1a931e4ff
commit
d547e6917b
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
138
packages/pen-core/src/element-builders/sidebar-nav-v1.ts
Normal file
138
packages/pen-core/src/element-builders/sidebar-nav-v1.ts
Normal 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;
|
||||
}
|
||||
79
packages/pen-core/src/element-builders/skeleton-v1.ts
Normal file
79
packages/pen-core/src/element-builders/skeleton-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
163
packages/pen-core/src/element-builders/social-login-row-v1.ts
Normal file
163
packages/pen-core/src/element-builders/social-login-row-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
77
packages/pen-core/src/element-builders/spinner-v1.ts
Normal file
77
packages/pen-core/src/element-builders/spinner-v1.ts
Normal 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 }] },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
146
packages/pen-core/src/element-builders/stat-card-v1.ts
Normal file
146
packages/pen-core/src/element-builders/stat-card-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
88
packages/pen-core/src/element-builders/stat-grid-v1.ts
Normal file
88
packages/pen-core/src/element-builders/stat-grid-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
86
packages/pen-core/src/element-builders/status-badge-v1.ts
Normal file
86
packages/pen-core/src/element-builders/status-badge-v1.ts
Normal 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
|
||||
}
|
||||
}
|
||||
142
packages/pen-core/src/element-builders/step-card-v1.ts
Normal file
142
packages/pen-core/src/element-builders/step-card-v1.ts
Normal 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 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
89
packages/pen-core/src/element-builders/stepper-v1.ts
Normal file
89
packages/pen-core/src/element-builders/stepper-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
56
packages/pen-core/src/element-builders/switch-v1.ts
Normal file
56
packages/pen-core/src/element-builders/switch-v1.ts
Normal 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' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
|||
28
packages/pen-mcp/src/tools/add-sidebar-nav-v1.ts
Normal file
28
packages/pen-mcp/src/tools/add-sidebar-nav-v1.ts
Normal 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 });
|
||||
}
|
||||
22
packages/pen-mcp/src/tools/add-skeleton-v1.ts
Normal file
22
packages/pen-mcp/src/tools/add-skeleton-v1.ts
Normal 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 });
|
||||
}
|
||||
28
packages/pen-mcp/src/tools/add-social-login-row-v1.ts
Normal file
28
packages/pen-mcp/src/tools/add-social-login-row-v1.ts
Normal 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 });
|
||||
}
|
||||
23
packages/pen-mcp/src/tools/add-spinner-v1.ts
Normal file
23
packages/pen-mcp/src/tools/add-spinner-v1.ts
Normal 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 });
|
||||
}
|
||||
23
packages/pen-mcp/src/tools/add-stat-card-v1.ts
Normal file
23
packages/pen-mcp/src/tools/add-stat-card-v1.ts
Normal 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 });
|
||||
}
|
||||
25
packages/pen-mcp/src/tools/add-stat-grid-v1.ts
Normal file
25
packages/pen-mcp/src/tools/add-stat-grid-v1.ts
Normal 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 });
|
||||
}
|
||||
27
packages/pen-mcp/src/tools/add-status-badge-v1.ts
Normal file
27
packages/pen-mcp/src/tools/add-status-badge-v1.ts
Normal 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 });
|
||||
}
|
||||
23
packages/pen-mcp/src/tools/add-step-card-v1.ts
Normal file
23
packages/pen-mcp/src/tools/add-step-card-v1.ts
Normal 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 });
|
||||
}
|
||||
23
packages/pen-mcp/src/tools/add-stepper-v1.ts
Normal file
23
packages/pen-mcp/src/tools/add-stepper-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-switch-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-switch-v1.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Reference in a new issue