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:
parent
c0717d43c4
commit
957f845bf5
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
64
packages/pen-core/src/element-builders/chart-bars-v1.ts
Normal file
64
packages/pen-core/src/element-builders/chart-bars-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
101
packages/pen-core/src/element-builders/chart-line-v1.ts
Normal file
101
packages/pen-core/src/element-builders/chart-line-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
105
packages/pen-core/src/element-builders/chart-pie-v1.ts
Normal file
105
packages/pen-core/src/element-builders/chart-pie-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
123
packages/pen-core/src/element-builders/chat-bubble-v1.ts
Normal file
123
packages/pen-core/src/element-builders/chat-bubble-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
80
packages/pen-core/src/element-builders/checkbox-v1.ts
Normal file
80
packages/pen-core/src/element-builders/checkbox-v1.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
125
packages/pen-core/src/element-builders/chip-input-v1.ts
Normal file
125
packages/pen-core/src/element-builders/chip-input-v1.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
57
packages/pen-core/src/element-builders/code-block-v1.ts
Normal file
57
packages/pen-core/src/element-builders/code-block-v1.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
64
packages/pen-core/src/element-builders/color-swatch-v1.ts
Normal file
64
packages/pen-core/src/element-builders/color-swatch-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
162
packages/pen-core/src/element-builders/combobox-v1.ts
Normal file
162
packages/pen-core/src/element-builders/combobox-v1.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
141
packages/pen-core/src/element-builders/comment-v1.ts
Normal file
141
packages/pen-core/src/element-builders/comment-v1.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
|||
24
packages/pen-mcp/src/tools/add-chart-bars-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-chart-bars-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-chart-line-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-chart-line-v1.ts
Normal 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 });
|
||||
}
|
||||
25
packages/pen-mcp/src/tools/add-chart-pie-v1.ts
Normal file
25
packages/pen-mcp/src/tools/add-chart-pie-v1.ts
Normal 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 });
|
||||
}
|
||||
28
packages/pen-mcp/src/tools/add-chat-bubble-v1.ts
Normal file
28
packages/pen-mcp/src/tools/add-chat-bubble-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-checkbox-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-checkbox-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-chip-input-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-chip-input-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-code-block-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-code-block-v1.ts
Normal 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 });
|
||||
}
|
||||
28
packages/pen-mcp/src/tools/add-color-swatch-v1.ts
Normal file
28
packages/pen-mcp/src/tools/add-color-swatch-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-combobox-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-combobox-v1.ts
Normal 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 });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-comment-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-comment-v1.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Reference in a new issue