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

Adds theme-aware v1 builders for chart_bars, chart_line, chart_pie,
chat_bubble, checkbox, chip_input, code_block, color_swatch, combobox,
and comment. Group A (chart tools) maps bar/line color to chart-1 token
and pie default palette to chart-1..6 tokens in dark/system modes.
Group B (color_swatch) is theme-invariant — swatch color is caller-
supplied and passes through unchanged. Group C (chat_bubble, checkbox,
chip_input, code_block, combobox, comment) resolves surface/text/border
via semantic palette tokens. All light modes are byte-parity with v0.
This commit is contained in:
Fini 2026-04-29 09:50:23 +08:00
parent c0717d43c4
commit 957f845bf5
29 changed files with 1796 additions and 2 deletions

View file

@ -118,6 +118,16 @@ import {
buildCalendarGridV1,
buildAvatarGroupV1,
buildCalloutV1,
buildChartBarsV1,
buildChartLineV1,
buildChartPieV1,
buildChatBubbleV1,
buildCheckboxV1,
buildChipInputV1,
buildCodeBlockV1,
buildColorSwatchV1,
buildComboboxV1,
buildCommentV1,
findNodeInTree,
insertNodeInTree,
type ElementTree,
@ -300,6 +310,16 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
add_calendar_grid_v1: (a) => buildCalendarGridV1(a as Parameters<typeof buildCalendarGridV1>[0]),
add_avatar_group_v1: (a) => buildAvatarGroupV1(a as Parameters<typeof buildAvatarGroupV1>[0]),
add_callout_v1: (a) => buildCalloutV1(a as Parameters<typeof buildCalloutV1>[0]),
add_chart_bars_v1: (a) => buildChartBarsV1(a as Parameters<typeof buildChartBarsV1>[0]),
add_chart_line_v1: (a) => buildChartLineV1(a as Parameters<typeof buildChartLineV1>[0]),
add_chart_pie_v1: (a) => buildChartPieV1(a as Parameters<typeof buildChartPieV1>[0]),
add_chat_bubble_v1: (a) => buildChatBubbleV1(a as Parameters<typeof buildChatBubbleV1>[0]),
add_checkbox_v1: (a) => buildCheckboxV1(a as Parameters<typeof buildCheckboxV1>[0]),
add_chip_input_v1: (a) => buildChipInputV1(a as Parameters<typeof buildChipInputV1>[0]),
add_code_block_v1: (a) => buildCodeBlockV1(a as Parameters<typeof buildCodeBlockV1>[0]),
add_color_swatch_v1: (a) => buildColorSwatchV1(a as Parameters<typeof buildColorSwatchV1>[0]),
add_combobox_v1: (a) => buildComboboxV1(a as Parameters<typeof buildComboboxV1>[0]),
add_comment_v1: (a) => buildCommentV1(a as Parameters<typeof buildCommentV1>[0]),
};
interface ExecToolBody {

View file

@ -127,6 +127,16 @@ import {
buildCalendarGridV1,
buildAvatarGroupV1,
buildCalloutV1,
buildChartBarsV1,
buildChartLineV1,
buildChartPieV1,
buildChatBubbleV1,
buildCheckboxV1,
buildChipInputV1,
buildCodeBlockV1,
buildColorSwatchV1,
buildComboboxV1,
buildCommentV1,
} from '@zseven-w/pen-core';
export interface BuilderCase {
@ -843,4 +853,57 @@ export const CASES: BuilderCase[] = [
args: { body: 'This is a callout note.', tone: 'info', theme: 'light' },
build: (a) => buildCalloutV1(a as unknown as Parameters<typeof buildCalloutV1>[0]),
},
{
toolName: 'add_chart_bars_v1',
args: { values: [40, 80, 60], theme: 'light' },
build: (a) => buildChartBarsV1(a as unknown as Parameters<typeof buildChartBarsV1>[0]),
},
{
toolName: 'add_chart_line_v1',
args: { values: [30, 70, 50, 90], theme: 'light' },
build: (a) => buildChartLineV1(a as unknown as Parameters<typeof buildChartLineV1>[0]),
},
{
toolName: 'add_chart_pie_v1',
args: { values: [40, 30, 30], theme: 'light' },
build: (a) => buildChartPieV1(a as unknown as Parameters<typeof buildChartPieV1>[0]),
},
{
toolName: 'add_chat_bubble_v1',
args: { message: 'Hello!', side: 'left', theme: 'light' },
build: (a) => buildChatBubbleV1(a as unknown as Parameters<typeof buildChatBubbleV1>[0]),
},
{
toolName: 'add_checkbox_v1',
args: { label: 'Accept terms', checked: false, theme: 'light' },
build: (a) => buildCheckboxV1(a as unknown as Parameters<typeof buildCheckboxV1>[0]),
},
{
toolName: 'add_chip_input_v1',
args: { label: 'Tags', chips: ['react', 'typescript'], theme: 'light' },
build: (a) => buildChipInputV1(a as unknown as Parameters<typeof buildChipInputV1>[0]),
},
{
toolName: 'add_code_block_v1',
args: { code: 'const x = 1;', language: 'ts', theme: 'light' },
build: (a) => buildCodeBlockV1(a as unknown as Parameters<typeof buildCodeBlockV1>[0]),
},
{
toolName: 'add_color_swatch_v1',
args: { color: '#2563EB', label: 'Primary', theme: 'light' },
build: (a) => buildColorSwatchV1(a as unknown as Parameters<typeof buildColorSwatchV1>[0]),
},
{
toolName: 'add_combobox_v1',
args: {
options: [{ label: 'Option A' }, { label: 'Option B', highlighted: true }],
theme: 'light',
},
build: (a) => buildComboboxV1(a as unknown as Parameters<typeof buildComboboxV1>[0]),
},
{
toolName: 'add_comment_v1',
args: { author: 'Alice', body: 'Great work!', theme: 'light' },
build: (a) => buildCommentV1(a as unknown as Parameters<typeof buildCommentV1>[0]),
},
];

View file

@ -136,6 +136,16 @@ import {
buildCalendarGridV1,
buildAvatarGroupV1,
buildCalloutV1,
buildChartBarsV1,
buildChartLineV1,
buildChartPieV1,
buildChatBubbleV1,
buildCheckboxV1,
buildChipInputV1,
buildCodeBlockV1,
buildColorSwatchV1,
buildComboboxV1,
buildCommentV1,
type ActionMenuParams,
type ActivityRingParams,
type AlertParams,
@ -224,6 +234,16 @@ import {
type CalendarGridV1Params,
type AvatarGroupV1Params,
type CalloutV1Params,
type ChartBarsV1Params,
type ChartLineV1Params,
type ChartPieV1Params,
type ChatBubbleV1Params,
type CheckboxV1Params,
type ChipInputV1Params,
type CodeBlockV1Params,
type ColorSwatchV1Params,
type ComboboxV1Params,
type CommentV1Params,
type KbdParams,
type LinkParams,
type ListRowParams,
@ -461,6 +481,16 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
add_calendar_grid_v1: wrap<CalendarGridV1Params>(buildCalendarGridV1),
add_avatar_group_v1: wrap<AvatarGroupV1Params>(buildAvatarGroupV1),
add_callout_v1: wrap<CalloutV1Params>(buildCalloutV1),
add_chart_bars_v1: wrap<ChartBarsV1Params>(buildChartBarsV1),
add_chart_line_v1: wrap<ChartLineV1Params>(buildChartLineV1),
add_chart_pie_v1: wrap<ChartPieV1Params>(buildChartPieV1),
add_chat_bubble_v1: wrap<ChatBubbleV1Params>(buildChatBubbleV1),
add_checkbox_v1: wrap<CheckboxV1Params>(buildCheckboxV1),
add_chip_input_v1: wrap<ChipInputV1Params>(buildChipInputV1),
add_code_block_v1: wrap<CodeBlockV1Params>(buildCodeBlockV1),
add_color_swatch_v1: wrap<ColorSwatchV1Params>(buildColorSwatchV1),
add_combobox_v1: wrap<ComboboxV1Params>(buildComboboxV1),
add_comment_v1: wrap<CommentV1Params>(buildCommentV1),
};
export function getElementShim(name: string): ElementShim | undefined {

View file

@ -62,11 +62,11 @@ Containers and single elements:
5. Section header (big title + optional "See all" action) → `add_section_header_v0`
6. Bottom tab bar (inline flow, 3-5 nav items) → `add_bottom_nav_v0`
6b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_bottom_nav_v1`
6b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_bottom_nav_v1`
7. Mobile top bar (leading icon + centered title + trailing icon) → `add_top_nav_bar_v0`
8. Icon-only button (44×44, hit-target safe) → `add_icon_button_v0`
9. Apple-style progress ring with centered text → `add_activity_ring_v0`
9b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_activity_ring_v1`
9b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_activity_ring_v1`
Atoms (1-2 node building blocks):
@ -102,6 +102,7 @@ Controls (toggle / choice / tabs):
20. iOS/Material toggle switch (51×31, thumb floats) → `add_switch_v0`
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`
23. Horizontal top tabs with underline on active (fontWeight 600 + 2px sibling rectangle underline) → `add_tabs_v0`
24. iOS pill-style segmented control (equal-width segments, active floats white) → `add_segmented_control_v0`
@ -137,16 +138,21 @@ Content blocks:
37. Quoted passage with optional author attribution → `add_quote_block_v0`
38. Preformatted code block (fill_container, wraps, gray-50 bg) → `add_code_block_v0`
38b. Theme-aware variant (bg → surface2 in dark/system) → `add_code_block_v1`
Design-system:
39. Color swatch (colored square + optional token label) → `add_color_swatch_v0`
39b. Theme-aware variant (same tree; theme param for API consistency) → `add_color_swatch_v1`
Charts / data visualization:
40. Bar-chart skeleton (one rectangle per value, bottom-aligned) → `add_chart_bars_v0`
40b. Line-chart skeleton (polyline + optional dots) → `add_chart_line_v0`
40c. Pie-chart skeleton (colored ellipse slices via arc angles) → `add_chart_pie_v0`
40d. Theme-aware bar chart (chart-1 token in dark/system) → `add_chart_bars_v1`
40e. Theme-aware line chart (chart-1 token in dark/system) → `add_chart_line_v1`
40f. Theme-aware pie chart (chart-1..6 tokens in dark/system) → `add_chart_pie_v1`
Media / placeholder:
@ -156,6 +162,7 @@ Media / placeholder:
Social / UGC:
45. Comment row (circular avatar + author/timestamp header + body) → `add_comment_v0`
45b. Theme-aware variant (avatar bg → surface2, timestamp → textMuted in dark/system) → `add_comment_v1`
Chrome / modals:
@ -203,6 +210,7 @@ Collapsible content:
Tag / multi-value inputs:
54. Chip input / tag input (pills + removable × + inline caret, wrap layout) → `add_chip_input_v0`
54b. Theme-aware variant (chip bg → surface2, field bg → surface, placeholder → textSubtle) → `add_chip_input_v1`
Chart empty state:
@ -233,6 +241,7 @@ Attachments:
Messaging:
61. Chat message bubble (left=from-others slate bg / right=from-self accent bg; optional author + timestamp) → `add_chat_bubble_v0`
61b. Theme-aware variant (surface2 bg for left, accent bg for right, textMuted for author/timestamp) → `add_chat_bubble_v1`
Dashboard KPIs:
@ -287,6 +296,7 @@ Side panels & docking:
Forms (open state) & toolbars:
76. Open-state combobox / autocomplete (input + visible dropdown rows) → `add_combobox_v0`
76b. Theme-aware variant (surface/border/surface2 tokens in dark/system) → `add_combobox_v1`
77. Desktop toolbar (icon button row with optional dividers) → `add_toolbar_v0`
Doc / inline feedback:

View file

@ -0,0 +1,64 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface ChartBarsV1Params {
values: number[];
bar_width?: number;
gap?: number;
chart_height?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_chart_bars_v0.
* - `'dark'`: bar color uses chart-1 token from semantic palette (same hue, dark-mode variant).
* - `'system'`: emits `$color-chart-1` ref for bar fill.
*/
theme?: V1Theme;
}
/**
* Bar-chart skeleton — theme-aware version of buildChartBars.
* Light mode is byte-equal to add_chart_bars_v0.
*
* Bar color maps to chart-1 (blue series). In all three modes the
* chart-1 token resolves the same way — chart colors are single-value
* (no theme axis) — so light/dark produce the same hex (#3B82F6 in
* the palette, but v0 used #2563EB for byte-parity; light mode keeps
* #2563EB for the migration contract).
*/
export function buildChartBarsV1(params: ChartBarsV1Params): ElementTree {
const raw = Array.isArray(params.values) ? params.values : [];
if (raw.length === 0) {
throw new Error('buildChartBarsV1: values must contain at least one number');
}
const values = raw.map((v) => (Number.isFinite(v) ? Math.max(0, v) : 0));
const max = Math.max(1, ...values);
const barWidth = Math.max(4, Math.floor(params.bar_width ?? 24));
const gap = Math.max(0, Math.floor(params.gap ?? 12));
const chartHeight = Math.max(40, Math.floor(params.chart_height ?? 160));
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
// Light mode: byte-parity with v0 (#2563EB). Dark/system: use chart-1 token.
const barColor = theme === 'light' ? '#2563EB' : t.chartColors.chart1;
const children: ElementTree[] = values.map((v, i) => ({
type: 'rectangle',
name: `Bar ${i + 1}`,
role: 'chart-bar',
width: barWidth,
height: Math.max(2, Math.round((v / max) * chartHeight)),
cornerRadius: 4,
fill: [{ type: 'solid', color: barColor }],
}));
return {
type: 'frame',
name: 'Chart Bars',
role: 'chart-bars',
width: 'fit_content',
height: chartHeight,
layout: 'horizontal',
alignItems: 'flex-end',
gap,
children,
};
}

View file

@ -0,0 +1,101 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface ChartLineV1Params {
values: number[];
/** Width per data point slot (px). Default 32, min 8. */
point_spacing?: number;
/** Chart height (px). Default 160, min 40. */
chart_height?: number;
/** Whether to emit a filled dot at each data point. Default true. */
dots?: boolean;
/**
* Stroke color for the line. Default #2563EB (Tailwind blue-600).
* Only used when `theme='light'` (v0 byte-parity). For dark/system
* modes, chart-1 token is used instead (ignores this field).
*/
stroke_color?: string;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_chart_line_v0.
* - `'dark'`: line/dot color uses chart-1 token from semantic palette.
* - `'system'`: emits `$color-chart-1` ref for line and dot fills.
*/
theme?: V1Theme;
}
/**
* Line-chart skeleton — theme-aware version of buildChartLine.
* Light mode is byte-equal to add_chart_line_v0.
*
* Stroke/dot color maps to chart-1 (blue series). Light mode preserves
* v0's #2563EB (or caller-supplied stroke_color) for byte-parity.
*/
export function buildChartLineV1(params: ChartLineV1Params): ElementTree {
const raw = Array.isArray(params.values) ? params.values : [];
if (raw.length === 0) {
throw new Error('buildChartLineV1: values must contain at least one number');
}
const values = raw.map((v) => (Number.isFinite(v) ? Math.max(0, v) : 0));
const max = Math.max(1, ...values);
const spacing = Math.max(8, Math.floor(params.point_spacing ?? 32));
const chartHeight = Math.max(40, Math.floor(params.chart_height ?? 160));
const dots = params.dots ?? true;
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
// Light mode: byte-parity with v0 (stroke_color param or #2563EB).
// Dark/system: use chart-1 token.
const strokeColor = theme === 'light' ? (params.stroke_color ?? '#2563EB') : t.chartColors.chart1;
const totalWidth = spacing * values.length;
const points = values.map((v, i) => {
const x = i * spacing + spacing / 2;
const yRaw = chartHeight - (v / max) * chartHeight;
const y = v > 0 ? Math.min(yRaw, chartHeight - 2) : chartHeight - 2;
return { x, y };
});
const dPath = points
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`)
.join(' ');
const children: ElementTree[] = [
{
type: 'path',
name: 'Line',
role: 'chart-line-path',
d: dPath,
width: totalWidth,
height: chartHeight,
fill: [],
stroke: { thickness: 2, fill: [{ type: 'solid', color: strokeColor }] },
},
];
if (dots) {
for (let i = 0; i < points.length; i++) {
const p = points[i];
children.push({
type: 'ellipse',
name: `Dot ${i + 1}`,
role: 'chart-line-dot',
x: p.x - 4,
y: p.y - 4,
width: 8,
height: 8,
fill: [{ type: 'solid', color: strokeColor }],
});
}
}
return {
type: 'frame',
name: 'Chart Line',
role: 'chart-line',
width: totalWidth,
height: chartHeight,
layout: 'none',
children,
};
}

View file

@ -0,0 +1,105 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface ChartPieV1Params {
/** Slice values (any numeric scale; normalized internally). */
values: number[];
/** Pie diameter (px, width=height). Default 160, min 40. */
diameter?: number;
/**
* Optional per-slice colors (hex). If fewer than values.length,
* the default chart palette fills the rest.
*
* Note: In dark/system modes, caller-supplied colors are passed
* through unchanged (they are treated as intentional brand colors).
* Only the default palette rotates through chart tokens.
*/
colors?: string[];
/** Inner cut-out radius as fraction of outer (0..0.9). Default 0 (full pie). */
inner_radius_ratio?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_chart_pie_v0 (uses v0 DEFAULT_PALETTE).
* - `'dark'`: default palette uses chart-1..6 tokens from semantic palette.
* - `'system'`: default palette emits `$color-chart-1..6` refs.
*
* When `colors` param is supplied, those values are always used as-is
* regardless of theme (caller takes ownership of color choice).
*/
theme?: V1Theme;
}
// v0 byte-parity palette — MUST match buildChartPie's DEFAULT_PALETTE exactly
const V0_DEFAULT_PALETTE = ['#2563EB', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'];
/**
* Pie-chart skeleton — theme-aware version of buildChartPie.
* Light mode is byte-equal to add_chart_pie_v0.
*
* Default slice colors map to chart-1..6 tokens in dark/system modes.
* Caller-supplied `colors` are passed through unchanged in all modes.
*/
export function buildChartPieV1(params: ChartPieV1Params): ElementTree {
const raw = Array.isArray(params.values) ? params.values : [];
if (raw.length === 0) {
throw new Error('buildChartPieV1: values must contain at least one number');
}
const values = raw.map((v) => (Number.isFinite(v) ? Math.max(0, v) : 0));
const total = values.reduce((s, v) => s + v, 0);
if (total <= 0) {
throw new Error('buildChartPieV1: values must sum to > 0');
}
const diameter = Math.max(40, Math.floor(params.diameter ?? 160));
const innerRatio = Math.max(0, Math.min(0.9, params.inner_radius_ratio ?? 0));
const innerRadius = innerRatio > 0 ? innerRatio : undefined;
const callerColors = params.colors ?? [];
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
// Build default palette for this theme
const defaultPalette =
theme === 'light'
? V0_DEFAULT_PALETTE
: [
t.chartColors.chart1,
t.chartColors.chart2,
t.chartColors.chart3,
t.chartColors.chart4,
t.chartColors.chart5,
t.chartColors.chart6,
];
const children: ElementTree[] = [];
let currentAngle = -90;
for (let i = 0; i < values.length; i++) {
const fraction = values[i] / total;
const sweep = fraction * 360;
// Caller-supplied colors take precedence; fall back to default palette
const color = callerColors[i] ?? defaultPalette[i % defaultPalette.length];
const slice: ElementTree = {
type: 'ellipse',
name: `Slice ${i + 1}`,
role: 'chart-pie-slice',
x: 0,
y: 0,
width: diameter,
height: diameter,
startAngle: currentAngle,
sweepAngle: sweep,
fill: [{ type: 'solid', color }],
};
if (innerRadius !== undefined) slice.innerRadius = innerRadius;
children.push(slice);
currentAngle += sweep;
}
return {
type: 'frame',
name: 'Chart Pie',
role: 'chart-pie',
width: diameter,
height: diameter,
layout: 'none',
children,
};
}

View file

@ -0,0 +1,123 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export type ChatBubbleV1Side = 'left' | 'right';
export interface ChatBubbleV1Params {
/** Message body text. Required. */
message: string;
/**
* Which side the bubble aligns to.
* - `'left'` (default): from-someone-else.
* - `'right'`: from-self. Author suppressed. Accent-colored surface.
*/
side?: ChatBubbleV1Side;
/** Sender display name. ONLY rendered on `side: 'left'`. */
author?: string;
/** Relative time string (e.g. "2m", "Just now"). Rendered below the bubble. */
timestamp?: string;
/** Max bubble width in px. Default 280. Clamped 160..480. */
max_width?: number;
/**
* Accent color for self-side bubbles (right). Default #2563EB.
* Only used in light mode (v0 byte-parity). Dark/system modes use
* the semantic accent token ($color-accent / dark-mode hex).
*/
accent_color?: string;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_chat_bubble_v0.
* - `'dark'`: dark-mode fills using semantic palette tokens.
* - `'system'`: emits `$color-*` refs for all fill fields.
*/
theme?: V1Theme;
}
/**
* Chat bubble — theme-aware version of buildChatBubble.
* Light mode is byte-equal to add_chat_bubble_v0.
*
* Left-side (from-others): surface2 bg, textPrimary text, textMuted author/timestamp.
* Right-side (from-self): accent bg, white text (white kept across themes — text on
* accent is always white). Author/timestamp: textMuted in dark/system.
*/
export function buildChatBubbleV1(params: ChatBubbleV1Params): ElementTree {
const side: ChatBubbleV1Side = params.side ?? 'left';
const maxWidth = Math.max(160, Math.min(480, Math.floor(params.max_width ?? 280)));
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
const isLight = theme === 'light';
const isSelf = side === 'right';
// Light mode: byte-parity with v0
const accent = isLight ? (params.accent_color ?? '#2563EB') : t.colors.accent;
const surfaceBg = isSelf ? accent : isLight ? '#F1F5F9' : t.colors.surface2;
const textColor = isSelf ? '#FFFFFF' : isLight ? '#0F172A' : t.colors.textPrimary;
const mutedColor = isLight ? '#64748B' : t.colors.textMuted;
const alignItems: 'flex-start' | 'flex-end' = isSelf ? 'flex-end' : 'flex-start';
const outerChildren: ElementTree[] = [];
if (!isSelf && params.author) {
outerChildren.push({
type: 'text',
name: 'Author',
role: 'chat-bubble-author',
content: params.author,
fontSize: 12,
fontWeight: 500,
fill: [{ type: 'solid', color: mutedColor }],
});
}
outerChildren.push({
type: 'frame',
name: 'Surface',
role: 'chat-bubble-surface',
width: maxWidth,
height: 'fit_content',
cornerRadius: 16,
layout: 'vertical',
padding: [10, 14],
fill: [{ type: 'solid', color: surfaceBg }],
children: [
{
type: 'text',
name: 'Message',
role: 'chat-bubble-message',
content: params.message,
fontSize: 14,
fontWeight: 400,
lineHeight: 1.4,
width: 'fill_container',
textGrowth: 'fixed-width',
fill: [{ type: 'solid', color: textColor }],
},
],
});
if (params.timestamp) {
outerChildren.push({
type: 'text',
name: 'Timestamp',
role: 'chat-bubble-timestamp',
content: params.timestamp,
fontSize: 11,
fontWeight: 400,
fill: [{ type: 'solid', color: mutedColor }],
});
}
return {
type: 'frame',
name: 'Chat Bubble',
role: isSelf ? 'chat-bubble-right' : 'chat-bubble-left',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
alignItems,
gap: 4,
children: outerChildren,
};
}

View file

@ -0,0 +1,80 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface CheckboxV1Params {
label: string;
checked?: boolean;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_checkbox_v0.
* - `'dark'`: accent fill for checked, dark border for unchecked.
* - `'system'`: emits `$color-*` refs for all fill fields.
*/
theme?: V1Theme;
}
/**
* Checkbox + label pair — theme-aware version of buildCheckbox.
* Light mode is byte-equal to add_checkbox_v0.
*
* Checked state: accent fill + white check icon.
* Unchecked state: transparent fill + border stroke (border token).
* v0's unchecked stroke was #9CA3AF (gray-400); mapped to border token in dark/system.
*/
export function buildCheckboxV1(params: CheckboxV1Params): ElementTree {
const checked = params.checked === true;
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
const isLight = theme === 'light';
// Light mode: byte-parity with v0
const accentColor = isLight ? '#2563EB' : t.colors.accent;
const borderColor = isLight ? '#9CA3AF' : t.colors.border;
const box: ElementTree = {
type: 'frame',
name: checked ? 'Checkbox (checked)' : 'Checkbox',
role: checked ? 'checkbox-checked' : 'checkbox',
width: 20,
height: 20,
cornerRadius: 4,
layout: 'horizontal',
alignItems: 'center',
justifyContent: 'center',
children: [] as ElementTree[],
};
if (checked) {
box.fill = [{ type: 'solid', color: accentColor }];
(box.children as ElementTree[]).push({
type: 'icon_font',
name: 'Check',
iconFontName: 'check',
iconFontFamily: 'lucide',
width: 14,
height: 14,
fill: [{ type: 'solid', color: '#FFFFFF' }],
});
} else {
box.fill = [];
box.stroke = { thickness: 1.5, fill: [{ type: 'solid', color: borderColor }] };
}
return {
type: 'frame',
name: `Checkbox Row (${params.label})`,
role: 'checkbox-row',
layout: 'horizontal',
alignItems: 'center',
gap: 8,
children: [
box,
{
type: 'text',
name: 'Label',
role: 'label',
content: params.label,
fontSize: 14,
fontWeight: 400,
},
],
};
}

View file

@ -0,0 +1,125 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface ChipInputV1Params {
/** Field label shown above the input. Required. */
label: string;
/**
* Current chip values. Each becomes a pill with a small × icon.
* Empty array is valid (placeholder-only state).
*/
chips?: string[];
/** Placeholder text shown when no chips OR after the last chip. */
placeholder?: string;
/** When true, appends " *" to the label. */
required?: boolean;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_chip_input_v0.
* - `'dark'`: dark fills for chip bg, field bg, border, icon, placeholder.
* - `'system'`: emits `$color-*` refs for all fill fields.
*/
theme?: V1Theme;
}
/**
* Chip / tag input — theme-aware version of buildChipInput.
* Light mode is byte-equal to add_chip_input_v0.
*
* Color mapping:
* chip bg (#F1F5F9) → surface2
* chip icon (#64748B) → textMuted
* field bg (#FFFFFF) → surface
* field border (#E2E8F0) → border
* placeholder (#94A3B8) → textSubtle
*/
export function buildChipInputV1(params: ChipInputV1Params): ElementTree {
const labelText = params.required === true ? `${params.label} *` : params.label;
const chips = params.chips ?? [];
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
const isLight = theme === 'light';
const chipBg = isLight ? '#F1F5F9' : t.colors.surface2;
const chipIconColor = isLight ? '#64748B' : t.colors.textMuted;
const fieldBg = isLight ? '#FFFFFF' : t.colors.surface;
const fieldBorder = isLight ? '#E2E8F0' : t.colors.border;
const placeholderColor = isLight ? '#94A3B8' : t.colors.textSubtle;
const fieldChildren: ElementTree[] = chips.map((value) => ({
type: 'frame',
name: `Chip (${value})`,
role: 'chip',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'center',
gap: 4,
padding: [6, 4, 6, 10],
cornerRadius: 16,
fill: [{ type: 'solid', color: chipBg }],
children: [
{
type: 'text',
name: 'Value',
content: value,
fontSize: 13,
fontWeight: 500,
},
{
type: 'icon_font',
name: 'Remove',
iconFontName: 'x',
iconFontFamily: 'lucide',
width: 14,
height: 14,
fill: [{ type: 'solid', color: chipIconColor }],
},
],
}));
fieldChildren.push({
type: 'text',
name: 'Caret',
role: 'chip-input-caret',
content: params.placeholder ?? (chips.length === 0 ? 'Add tag…' : ''),
fontSize: 14,
fontWeight: 400,
fill: [{ type: 'solid', color: placeholderColor }],
});
return {
type: 'frame',
name: 'Chip Input',
role: 'chip-input',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
gap: 6,
children: [
{
type: 'text',
name: 'Label',
role: 'chip-input-label',
content: labelText,
fontSize: 13,
fontWeight: 500,
},
{
type: 'frame',
name: 'Field',
role: 'chip-input-field',
width: 'fill_container',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'center',
layoutWrap: 'wrap',
gap: 6,
padding: [8, 12],
cornerRadius: 8,
fill: [{ type: 'solid', color: fieldBg }],
stroke: { thickness: 1, fill: [{ type: 'solid', color: fieldBorder }] },
children: fieldChildren,
},
],
};
}

View file

@ -0,0 +1,57 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface CodeBlockV1Params {
code: string;
language?: string;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_code_block_v0.
* - `'dark'`: dark background for the code block surface.
* - `'system'`: emits `$color-surface-2` ref for background fill.
*/
theme?: V1Theme;
}
/**
* Preformatted code block — theme-aware version of buildCodeBlock.
* Light mode is byte-equal to add_code_block_v0.
*
* Background (#F3F4F6 = gray-100) maps to surface2 token in dark/system modes.
* Code text has no explicit fill in v0 (relies on canvas default) so we
* keep it unfilled in all modes — theme-agnostic.
*/
export function buildCodeBlockV1(params: CodeBlockV1Params): ElementTree {
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
const isLight = theme === 'light';
// Light: byte-parity (#F3F4F6). Dark/system: surface2 token.
const bgColor = isLight ? '#F3F4F6' : t.colors.surface2;
const nameSuffix = params.language ? ` (${params.language})` : '';
return {
type: 'frame',
name: `Code Block${nameSuffix}`,
role: 'code-block',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
padding: [12, 16],
cornerRadius: 8,
fill: [{ type: 'solid', color: bgColor }],
children: [
{
type: 'text',
name: 'Code',
role: 'code',
content: params.code,
fontSize: 13,
fontWeight: 400,
lineHeight: 1.5,
width: 'fill_container',
textGrowth: 'fixed-width',
},
],
};
}

View file

@ -0,0 +1,64 @@
import type { ElementTree } from './helpers.js';
import type { V1Theme } from './resolve-theme.js';
export interface ColorSwatchV1Params {
color: string;
label?: string;
size?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_color_swatch_v0.
* - `'dark'`: identical tree — color swatch passes `color` through unchanged.
* - `'system'`: identical tree — color swatch passes `color` through unchanged.
*
* Note: `color` is intentionally theme-agnostic — the whole point of a
* design-system swatch is to display a specific color. No fill tokens are
* applied; the caller supplies the swatch color directly. Theme parameter
* exists only for API consistency with other v1 tools.
*/
theme?: V1Theme;
}
/**
* Design-system color swatch — theme-aware version of buildColorSwatch.
* Light mode is byte-equal to add_color_swatch_v0.
*
* This v1 is structurally identical across all theme modes — the swatch
* color is caller-supplied and not tokenized. Theme parameter exists for
* consistency; dark/system produce the same tree as light.
*/
export function buildColorSwatchV1(params: ColorSwatchV1Params): ElementTree {
const size = Math.max(16, Math.floor(params.size ?? 64));
const children: ElementTree[] = [
{
type: 'frame',
name: 'Swatch Square',
role: 'color-swatch-square',
width: size,
height: size,
cornerRadius: 12,
fill: [{ type: 'solid', color: params.color }],
},
];
if (params.label) {
children.push({
type: 'text',
name: 'Label',
role: 'color-swatch-label',
content: params.label,
fontSize: 12,
fontWeight: 500,
});
}
return {
type: 'frame',
name: 'Color Swatch',
role: 'color-swatch',
width: 'fit_content',
height: 'fit_content',
layout: 'vertical',
alignItems: 'center',
gap: 8,
children,
};
}

View file

@ -0,0 +1,162 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface ComboboxV1Option {
label: string;
/** Mark a row as the currently highlighted suggestion (surface2 bg). */
highlighted?: boolean;
}
export interface ComboboxV1Params {
/** Placeholder shown when value is empty. */
placeholder?: string;
/** Pre-filled query text. */
value?: string;
/** Suggestion rows shown in the open dropdown. Required (1+). */
options: ComboboxV1Option[];
/** Optional label above the field (e.g. "Country"). */
label?: string;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_combobox_v0.
* - `'dark'`: dark surface/text/border for all fill fields.
* - `'system'`: emits `$color-*` refs for all fill fields.
*/
theme?: V1Theme;
}
/**
* Combobox / autocomplete — theme-aware version of buildCombobox.
* Light mode is byte-equal to add_combobox_v0.
*
* Color mapping:
* label text (#0F172A) → textPrimary
* input bg (#FFFFFF) → surface
* input stroke (#2563EB) → accent (focus ring — intentionally accent in all modes)
* search icon (#64748B) → textMuted
* value text (#0F172A) → textPrimary
* placeholder (#94A3B8) → textSubtle
* dropdown bg (#FFFFFF) → surface
* dropdown border (#E2E8F0) → border
* option hover bg (#F1F5F9) → surface2
* option text (#0F172A) → textPrimary
* shadow color (#0F172A1A) → kept as-is (shadow is theme-agnostic in v0)
*/
export function buildComboboxV1(params: ComboboxV1Params): ElementTree {
const placeholder = params.placeholder ?? 'Search...';
const value = params.value ?? '';
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
const isLight = theme === 'light';
const labelTextColor = isLight ? '#0F172A' : t.colors.textPrimary;
const inputBg = isLight ? '#FFFFFF' : t.colors.surface;
const inputStroke = isLight ? '#2563EB' : t.colors.accent;
const searchIconColor = isLight ? '#64748B' : t.colors.textMuted;
const valueColor = isLight ? '#0F172A' : t.colors.textPrimary;
const placeholderColor = isLight ? '#94A3B8' : t.colors.textSubtle;
const dropdownBg = isLight ? '#FFFFFF' : t.colors.surface;
const dropdownBorder = isLight ? '#E2E8F0' : t.colors.border;
const optionHoverBg = isLight ? '#F1F5F9' : t.colors.surface2;
const optionTextColor = isLight ? '#0F172A' : t.colors.textPrimary;
const stackChildren: ElementTree[] = [];
if (params.label) {
stackChildren.push({
type: 'text',
name: 'Label',
role: 'combobox-label',
content: params.label,
fontSize: 13,
fontWeight: 500,
fill: [{ type: 'solid', color: labelTextColor }],
});
}
stackChildren.push({
type: 'frame',
name: 'Input',
role: 'combobox-input',
width: 'fill_container',
height: 44,
cornerRadius: 8,
layout: 'horizontal',
alignItems: 'center',
gap: 8,
padding: [0, 12],
fill: [{ type: 'solid', color: inputBg }],
stroke: { thickness: 1, fill: [{ type: 'solid', color: inputStroke }] },
children: [
{
type: 'icon_font',
name: 'Search Icon',
iconFontName: 'search',
iconFontFamily: 'lucide',
width: 16,
height: 16,
fill: [{ type: 'solid', color: searchIconColor }],
},
{
type: 'text',
name: 'Value',
role: 'combobox-value',
content: value || placeholder,
fontSize: 14,
fontWeight: 400,
fill: [{ type: 'solid', color: value ? valueColor : placeholderColor }],
},
],
});
stackChildren.push({
type: 'frame',
name: 'Dropdown',
role: 'combobox-dropdown',
width: 'fill_container',
height: 'fit_content',
cornerRadius: 8,
layout: 'vertical',
fill: [{ type: 'solid', color: dropdownBg }],
stroke: { thickness: 1, fill: [{ type: 'solid', color: dropdownBorder }] },
effects: [
{
type: 'shadow',
offsetX: 0,
offsetY: 4,
blur: 12,
spread: 0,
color: '#0F172A1A',
},
],
children: params.options.map((opt, i) => ({
type: 'frame',
name: `Option ${i + 1}`,
role: opt.highlighted ? 'combobox-option-active' : 'combobox-option',
width: 'fill_container',
height: 36,
layout: 'horizontal',
alignItems: 'center',
padding: [0, 12],
...(opt.highlighted ? { fill: [{ type: 'solid', color: optionHoverBg }] } : {}),
children: [
{
type: 'text',
name: 'Label',
role: 'combobox-option-label',
content: opt.label,
fontSize: 14,
fontWeight: opt.highlighted ? 500 : 400,
fill: [{ type: 'solid', color: optionTextColor }],
},
],
})),
});
return {
type: 'frame',
name: 'Combobox',
role: 'combobox',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
gap: 6,
children: stackChildren,
};
}

View file

@ -0,0 +1,141 @@
import type { ElementTree } from './helpers.js';
import { resolveTheme, type V1Theme } from './resolve-theme.js';
export interface CommentV1Params {
/** Author display name (shown in bold inline with timestamp). */
author: string;
/** Relative timestamp string ("2 hours ago", "Just now", "3d"). */
timestamp?: string;
/** Body text. Supports multi-line (rendered as a single text node). */
body: string;
/**
* Avatar initial (1-2 chars). When absent, the avatar is rendered
* as a blank circle (placeholder for a user photo).
*/
avatar_initial?: string;
/** Avatar diameter in px. Default 40. Min 24. */
avatar_size?: number;
/**
* Theme mode.
* - `'light'` (default): byte-parity with add_comment_v0.
* - `'dark'`: dark fills for avatar bg, initial text, timestamp.
* - `'system'`: emits `$color-*` refs for all fill fields.
*/
theme?: V1Theme;
}
/**
* Comment row — theme-aware version of buildComment.
* Light mode is byte-equal to add_comment_v0.
*
* Color mapping:
* avatar bg (#E2E8F0) → surface2 (light gray placeholder)
* initial text (#475569) → textBody
* timestamp (#64748B) → textMuted
*
* Author text and body text have no explicit fill in v0; kept unfilled
* in all modes for byte-parity.
*/
export function buildCommentV1(params: CommentV1Params): ElementTree {
const avatarSize = Math.max(24, Math.floor(params.avatar_size ?? 40));
const avatarRadius = avatarSize / 2;
const theme = params.theme ?? 'light';
const t = resolveTheme(theme);
const isLight = theme === 'light';
const avatarBg = isLight ? '#E2E8F0' : t.colors.surface2;
const initialColor = isLight ? '#475569' : t.colors.textBody;
const timestampColor = isLight ? '#64748B' : t.colors.textMuted;
const avatarChildren: ElementTree[] = [];
if (params.avatar_initial) {
avatarChildren.push({
type: 'text',
name: 'Initial',
role: 'avatar-initial',
content: params.avatar_initial.slice(0, 2).toUpperCase(),
fontSize: Math.max(10, Math.round(avatarSize * 0.4)),
fontWeight: 600,
fill: [{ type: 'solid', color: initialColor }],
});
}
const headerChildren: ElementTree[] = [
{
type: 'text',
name: 'Author',
role: 'comment-author',
content: params.author,
fontSize: 14,
fontWeight: 600,
},
];
if (params.timestamp) {
headerChildren.push({
type: 'text',
name: 'Timestamp',
role: 'comment-timestamp',
content: params.timestamp,
fontSize: 12,
fontWeight: 400,
fill: [{ type: 'solid', color: timestampColor }],
});
}
return {
type: 'frame',
name: 'Comment',
role: 'comment',
width: 'fill_container',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'start',
gap: 12,
children: [
{
type: 'frame',
name: 'Avatar',
role: 'comment-avatar',
width: avatarSize,
height: avatarSize,
cornerRadius: avatarRadius,
layout: 'horizontal',
alignItems: 'center',
justifyContent: 'center',
fill: [{ type: 'solid', color: avatarBg }],
children: avatarChildren,
},
{
type: 'frame',
name: 'Body Column',
role: 'comment-body-column',
width: 'fill_container',
height: 'fit_content',
layout: 'vertical',
gap: 4,
children: [
{
type: 'frame',
name: 'Header',
role: 'comment-header',
width: 'fit_content',
height: 'fit_content',
layout: 'horizontal',
alignItems: 'baseline',
gap: 6,
children: headerChildren,
},
{
type: 'text',
name: 'Body',
role: 'comment-body',
content: params.body,
fontSize: 14,
fontWeight: 400,
lineHeight: 1.5,
},
],
},
],
};
}

View file

@ -174,3 +174,17 @@ export {
type AvatarGroupV1Params,
} from './avatar-group-v1.js';
export { buildCalloutV1, type CalloutV1Tone, type CalloutV1Params } from './callout-v1.js';
export { buildChartBarsV1, type ChartBarsV1Params } from './chart-bars-v1.js';
export { buildChartLineV1, type ChartLineV1Params } from './chart-line-v1.js';
export { buildChartPieV1, type ChartPieV1Params } from './chart-pie-v1.js';
export {
buildChatBubbleV1,
type ChatBubbleV1Params,
type ChatBubbleV1Side,
} from './chat-bubble-v1.js';
export { buildCheckboxV1, type CheckboxV1Params } from './checkbox-v1.js';
export { buildChipInputV1, type ChipInputV1Params } from './chip-input-v1.js';
export { buildCodeBlockV1, type CodeBlockV1Params } from './code-block-v1.js';
export { buildColorSwatchV1, type ColorSwatchV1Params } from './color-swatch-v1.js';
export { buildComboboxV1, type ComboboxV1Option, type ComboboxV1Params } from './combobox-v1.js';
export { buildCommentV1, type CommentV1Params } from './comment-v1.js';

View file

@ -472,4 +472,26 @@ export {
buildCalloutV1,
type CalloutV1Tone,
type CalloutV1Params,
buildChartBarsV1,
type ChartBarsV1Params,
buildChartLineV1,
type ChartLineV1Params,
buildChartPieV1,
type ChartPieV1Params,
buildChatBubbleV1,
type ChatBubbleV1Params,
type ChatBubbleV1Side,
buildCheckboxV1,
type CheckboxV1Params,
buildChipInputV1,
type ChipInputV1Params,
buildCodeBlockV1,
type CodeBlockV1Params,
buildColorSwatchV1,
type ColorSwatchV1Params,
buildComboboxV1,
type ComboboxV1Option,
type ComboboxV1Params,
buildCommentV1,
type CommentV1Params,
} from './element-builders/index.js';

View file

@ -109,6 +109,16 @@ const ELEMENT_TOOL_NAMES = [
'add_calendar_grid_v1',
'add_avatar_group_v1',
'add_callout_v1',
'add_chart_bars_v1',
'add_chart_line_v1',
'add_chart_pie_v1',
'add_chat_bubble_v1',
'add_checkbox_v1',
'add_chip_input_v1',
'add_code_block_v1',
'add_color_swatch_v1',
'add_combobox_v1',
'add_comment_v1',
];
describe('element tools — v0-MUST contract', () => {

View file

@ -471,4 +471,328 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_5 = [
required: ['body'],
},
},
{
name: 'add_chart_bars_v1',
description:
'Theme-aware bar-chart skeleton (v1). theme="light" (default): byte-parity with ' +
'add_chart_bars_v0. theme="dark" / "system": bar color maps to chart-1 token. ' +
'Bottom-aligned rectangles proportional to max value. No axes / labels. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
values: {
type: 'array',
description: 'Numeric values (≥1). Proportional to tallest bar.',
items: { type: 'number' },
},
bar_width: { type: 'number', description: 'Bar width px. Default 24, min 4.' },
gap: { type: 'number', description: 'Gap between bars px. Default 12.' },
chart_height: { type: 'number', description: 'Chart height px. Default 160, min 40.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['values'],
},
},
{
name: 'add_chart_line_v1',
description:
'Theme-aware line-chart skeleton (v1). theme="light" (default): byte-parity with ' +
'add_chart_line_v0. theme="dark" / "system": line/dot color maps to chart-1 token. ' +
'Polyline through N data points with optional dots. No axes / labels. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
values: {
type: 'array',
description: 'Numeric values (≥1). Y-axis inverted (0 = bottom).',
items: { type: 'number' },
},
point_spacing: { type: 'number', description: 'Width per data point slot px. Default 32.' },
chart_height: { type: 'number', description: 'Chart height px. Default 160, min 40.' },
dots: { type: 'boolean', description: 'Emit dot ellipses at each point. Default true.' },
stroke_color: {
type: 'string',
description: 'Line color (light mode only). Default #2563EB.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['values'],
},
},
{
name: 'add_chart_pie_v1',
description:
'Theme-aware pie/donut-chart skeleton (v1). theme="light" (default): byte-parity with ' +
'add_chart_pie_v0 (v0 default palette). theme="dark" / "system": default palette uses ' +
'chart-1..6 tokens. Caller-supplied `colors` always pass through unchanged. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
values: {
type: 'array',
description: 'Slice values (≥1, sum > 0). Normalized internally.',
items: { type: 'number' },
},
diameter: { type: 'number', description: 'Pie diameter px. Default 160, min 40.' },
colors: {
type: 'array',
description: 'Optional per-slice hex colors. Falls back to default chart palette.',
items: { type: 'string' },
},
inner_radius_ratio: {
type: 'number',
description: 'Donut hole size as fraction of outer radius (0..0.9). Default 0.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['values'],
},
},
{
name: 'add_chat_bubble_v1',
description:
'Theme-aware chat bubble (v1). theme="light" (default): byte-parity with add_chat_bubble_v0. ' +
'theme="dark" / "system": left-side → surface2 bg + textPrimary text; ' +
'right-side (self) → accent bg + white text. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
message: { type: 'string', description: 'Message body text.' },
side: {
type: 'string',
enum: ['left', 'right'],
description: '"left" = from-others (default), "right" = from-self (accent bg).',
},
author: {
type: 'string',
description: 'Sender display name. Only rendered on left-side bubbles.',
},
timestamp: { type: 'string', description: 'Relative time string (e.g. "2m").' },
max_width: {
type: 'number',
description: 'Max bubble width px. Default 280, clamped 160..480.',
},
accent_color: {
type: 'string',
description: 'Right-side accent color (light mode only). Default #2563EB.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['message'],
},
},
{
name: 'add_checkbox_v1',
description:
'Theme-aware checkbox + label pair (v1). theme="light" (default): byte-parity with ' +
'add_checkbox_v0. theme="dark" / "system": accent fill for checked, border token for unchecked. ' +
'20×20 box with cornerRadius=4. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
label: { type: 'string', description: 'Label text. Required.' },
checked: { type: 'boolean', description: 'Checked state. Default false.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['label'],
},
},
{
name: 'add_chip_input_v1',
description:
'Theme-aware chip / tag input (v1). theme="light" (default): byte-parity with ' +
'add_chip_input_v0. theme="dark" / "system": chip bg → surface2, field bg → surface, ' +
'border → border token, placeholder → textSubtle. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
label: { type: 'string', description: 'Field label shown above input. Required.' },
chips: {
type: 'array',
description: 'Current chip values. Each becomes a pill with × icon.',
items: { type: 'string' },
},
placeholder: {
type: 'string',
description: 'Placeholder text after last chip. Default "Add tag…".',
},
required: {
type: 'boolean',
description: 'When true, appends " *" to the label.',
},
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['label'],
},
},
{
name: 'add_code_block_v1',
description:
'Theme-aware preformatted code block (v1). theme="light" (default): byte-parity with ' +
'add_code_block_v0 (gray-100 bg). theme="dark" / "system": bg maps to surface2 token. ' +
'fill_container frame with monospace text. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
code: { type: 'string', description: 'Code content (verbatim, newlines preserved).' },
language: { type: 'string', description: 'Language tag for frame name only.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['code'],
},
},
{
name: 'add_color_swatch_v1',
description:
'Theme-aware design-system color swatch (v1). theme="light" (default): byte-parity with ' +
'add_color_swatch_v0. All theme modes produce identical trees — swatch color is caller-supplied ' +
'and not tokenized. Accepts hex or $variable ref. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
color: {
type: 'string',
description: 'Swatch color hex (#2563EB) or $variable ref ($color-primary). Required.',
},
label: { type: 'string', description: 'Optional label shown below the swatch.' },
size: { type: 'number', description: 'Swatch square size px. Default 64, min 16.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light". All modes produce identical output.',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['color'],
},
},
{
name: 'add_combobox_v1',
description:
'Theme-aware combobox / autocomplete (v1). theme="light" (default): byte-parity with ' +
'add_combobox_v0. theme="dark" / "system": input/dropdown bg → surface, option hover → surface2, ' +
'border → border token, focus ring → accent. Open-state input with suggestion list. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
options: {
type: 'array',
description: 'Suggestion rows (1+). Each: { label, highlighted? }.',
items: {
type: 'object',
properties: {
label: { type: 'string' },
highlighted: {
type: 'boolean',
description: 'When true, renders with surface2 bg.',
},
},
required: ['label'],
},
},
placeholder: { type: 'string', description: 'Placeholder text. Default "Search...".' },
value: { type: 'string', description: 'Pre-filled query text.' },
label: { type: 'string', description: 'Optional label above the field.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['options'],
},
},
{
name: 'add_comment_v1',
description:
'Theme-aware comment row (v1). theme="light" (default): byte-parity with add_comment_v0. ' +
'theme="dark" / "system": avatar bg → surface2, initial text → textBody, timestamp → textMuted. ' +
'Circular avatar + (author + timestamp) header + body text. schemaVersion 1.0',
inputSchema: {
type: 'object' as const,
properties: {
schemaVersion: schemaVersionProp,
filePath: filePathProp,
author: { type: 'string', description: 'Author display name. Required.' },
body: { type: 'string', description: 'Comment body text. Required.' },
timestamp: { type: 'string', description: 'Relative timestamp ("2 hours ago").' },
avatar_initial: {
type: 'string',
description: '1-2 char avatar initial. Absent → blank circle.',
},
avatar_size: { type: 'number', description: 'Avatar diameter px. Default 40, min 24.' },
theme: {
type: 'string',
enum: ['light', 'dark', 'system'],
description: 'Theme variant. Default "light".',
},
parent_id: parentIdProp,
pageId: pageIdProp,
},
required: ['author', 'body'],
},
},
];

View file

@ -132,6 +132,16 @@ import { handleAddAttachmentRowV1 } from '../tools/add-attachment-row-v1';
import { handleAddCalendarGridV1 } from '../tools/add-calendar-grid-v1';
import { handleAddAvatarGroupV1 } from '../tools/add-avatar-group-v1';
import { handleAddCalloutV1 } from '../tools/add-callout-v1';
import { handleAddChartBarsV1 } from '../tools/add-chart-bars-v1';
import { handleAddChartLineV1 } from '../tools/add-chart-line-v1';
import { handleAddChartPieV1 } from '../tools/add-chart-pie-v1';
import { handleAddChatBubbleV1 } from '../tools/add-chat-bubble-v1';
import { handleAddCheckboxV1 } from '../tools/add-checkbox-v1';
import { handleAddChipInputV1 } from '../tools/add-chip-input-v1';
import { handleAddCodeBlockV1 } from '../tools/add-code-block-v1';
import { handleAddColorSwatchV1 } from '../tools/add-color-swatch-v1';
import { handleAddComboboxV1 } from '../tools/add-combobox-v1';
import { handleAddCommentV1 } from '../tools/add-comment-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';
@ -402,6 +412,26 @@ async function dispatchElementToolCall(name: string, a: any): Promise<string> {
return JSON.stringify(await handleAddAvatarGroupV1(a), null, 2);
case 'add_callout_v1':
return JSON.stringify(await handleAddCalloutV1(a), null, 2);
case 'add_chart_bars_v1':
return JSON.stringify(await handleAddChartBarsV1(a), null, 2);
case 'add_chart_line_v1':
return JSON.stringify(await handleAddChartLineV1(a), null, 2);
case 'add_chart_pie_v1':
return JSON.stringify(await handleAddChartPieV1(a), null, 2);
case 'add_chat_bubble_v1':
return JSON.stringify(await handleAddChatBubbleV1(a), null, 2);
case 'add_checkbox_v1':
return JSON.stringify(await handleAddCheckboxV1(a), null, 2);
case 'add_chip_input_v1':
return JSON.stringify(await handleAddChipInputV1(a), null, 2);
case 'add_code_block_v1':
return JSON.stringify(await handleAddCodeBlockV1(a), null, 2);
case 'add_color_swatch_v1':
return JSON.stringify(await handleAddColorSwatchV1(a), null, 2);
case 'add_combobox_v1':
return JSON.stringify(await handleAddComboboxV1(a), null, 2);
case 'add_comment_v1':
return JSON.stringify(await handleAddCommentV1(a), null, 2);
default:
return '';
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildChartBarsV1, type ChartBarsV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddChartBarsV1Params extends ChartBarsV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Bar-chart skeleton (v1) — theme-aware variant of add_chart_bars_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Bar color maps to chart-1 token in dark/system modes.
* Tree build delegated to `buildChartBarsV1` in `@zseven-w/pen-core`.
*/
export async function handleAddChartBarsV1(
params: AddChartBarsV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildChartBarsV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cb', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildChartLineV1, type ChartLineV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddChartLineV1Params extends ChartLineV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Line-chart skeleton (v1) — theme-aware variant of add_chart_line_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Line/dot color maps to chart-1 token in dark/system modes.
* Tree build delegated to `buildChartLineV1` in `@zseven-w/pen-core`.
*/
export async function handleAddChartLineV1(
params: AddChartLineV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildChartLineV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cl', tree, ...params });
}

View file

@ -0,0 +1,25 @@
import { assignIdsRecursively, buildChartPieV1, type ChartPieV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddChartPieV1Params extends ChartPieV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Pie-chart skeleton (v1) — theme-aware variant of add_chart_pie_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Default slice palette maps to chart-1..6 tokens in dark/system modes.
* Caller-supplied `colors` are always passed through unchanged.
* Tree build delegated to `buildChartPieV1` in `@zseven-w/pen-core`.
*/
export async function handleAddChartPieV1(
params: AddChartPieV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildChartPieV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cp', tree, ...params });
}

View file

@ -0,0 +1,28 @@
import {
assignIdsRecursively,
buildChatBubbleV1,
type ChatBubbleV1Params,
} from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddChatBubbleV1Params extends ChatBubbleV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Chat bubble (v1) — theme-aware variant of add_chat_bubble_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Left-side: surface2 bg, textPrimary text. Right-side: accent bg, white text.
* Tree build delegated to `buildChatBubbleV1` in `@zseven-w/pen-core`.
*/
export async function handleAddChatBubbleV1(
params: AddChatBubbleV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildChatBubbleV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cb', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildCheckboxV1, type CheckboxV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddCheckboxV1Params extends CheckboxV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Checkbox + label (v1) — theme-aware variant of add_checkbox_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Checked: accent fill + white icon. Unchecked: transparent + border stroke.
* Tree build delegated to `buildCheckboxV1` in `@zseven-w/pen-core`.
*/
export async function handleAddCheckboxV1(
params: AddCheckboxV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildCheckboxV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'ch', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildChipInputV1, type ChipInputV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddChipInputV1Params extends ChipInputV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Chip / tag input (v1) — theme-aware variant of add_chip_input_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Chip bg → surface2, field bg → surface, border → border token.
* Tree build delegated to `buildChipInputV1` in `@zseven-w/pen-core`.
*/
export async function handleAddChipInputV1(
params: AddChipInputV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildChipInputV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'ci', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildCodeBlockV1, type CodeBlockV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddCodeBlockV1Params extends CodeBlockV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Preformatted code block (v1) — theme-aware variant of add_code_block_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Background (#F3F4F6) maps to surface2 token in dark/system modes.
* Tree build delegated to `buildCodeBlockV1` in `@zseven-w/pen-core`.
*/
export async function handleAddCodeBlockV1(
params: AddCodeBlockV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildCodeBlockV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cod', tree, ...params });
}

View file

@ -0,0 +1,28 @@
import {
assignIdsRecursively,
buildColorSwatchV1,
type ColorSwatchV1Params,
} from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddColorSwatchV1Params extends ColorSwatchV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Design-system color swatch (v1) — theme-aware variant of add_color_swatch_v0.
* Supports 'light', 'dark', and 'system' theme modes (all produce identical trees —
* the swatch color is caller-supplied and not tokenized).
* Tree build delegated to `buildColorSwatchV1` in `@zseven-w/pen-core`.
*/
export async function handleAddColorSwatchV1(
params: AddColorSwatchV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildColorSwatchV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cs', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildComboboxV1, type ComboboxV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddComboboxV1Params extends ComboboxV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Combobox / autocomplete (v1) — theme-aware variant of add_combobox_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Input/dropdown bg → surface, option hover → surface2, stroke → border/accent.
* Tree build delegated to `buildComboboxV1` in `@zseven-w/pen-core`.
*/
export async function handleAddComboboxV1(
params: AddComboboxV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildComboboxV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cmb', tree, ...params });
}

View file

@ -0,0 +1,24 @@
import { assignIdsRecursively, buildCommentV1, type CommentV1Params } from '@zseven-w/pen-core';
import type { handleBatchDesign } from './batch-design';
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
export interface AddCommentV1Params extends CommentV1Params {
parent_id?: string;
filePath?: string;
pageId?: string;
}
/**
* Comment row (v1) — theme-aware variant of add_comment_v0.
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
* Avatar bg → surface2, initial text → textBody, timestamp → textMuted.
* Tree build delegated to `buildCommentV1` in `@zseven-w/pen-core`.
*/
export async function handleAddCommentV1(
params: AddCommentV1Params,
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
await ensureParentExists(params);
const tree = buildCommentV1(params);
assignIdsRecursively(tree);
return insertElementTree({ binding: 'cmt', tree, ...params });
}