feat(element-tools): add 5 atom v1 tools with theme parameter (P3 batch 1)
avatar-v1, badge-v1, divider-v1, body_text-v1, icon_label-v1 — each with full 9-touchpoint coverage (pen-core builder + index + pen-mcp handler + schema shard + dispatcher + apps/web shim + SERVER_BUILDERS + parity test + elements.md). Light mode is byte-equal to v0; dark/system modes produce identical output since all 5 tools emit zero hardcoded color fills — theme param accepted for API consistency across all v1 tools. New shard element-tool-defs-ext-5.ts created (ext-4 was at 739 lines). All 2026 pen-core + pen-mcp tests pass; format:check + tsc clean.
This commit is contained in:
parent
4962d6176e
commit
ddfb3fa81b
|
|
@ -103,6 +103,11 @@ import {
|
|||
buildSettingRowV1,
|
||||
buildMemberRowV1,
|
||||
buildActivityLogV1,
|
||||
buildAvatarV1,
|
||||
buildBadgeV1,
|
||||
buildDividerV1,
|
||||
buildBodyTextV1,
|
||||
buildIconLabelV1,
|
||||
findNodeInTree,
|
||||
insertNodeInTree,
|
||||
type ElementTree,
|
||||
|
|
@ -269,6 +274,11 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
|
|||
add_setting_row_v1: (a) => buildSettingRowV1(a as Parameters<typeof buildSettingRowV1>[0]),
|
||||
add_member_row_v1: (a) => buildMemberRowV1(a as Parameters<typeof buildMemberRowV1>[0]),
|
||||
add_activity_log_v1: (a) => buildActivityLogV1(a as Parameters<typeof buildActivityLogV1>[0]),
|
||||
add_avatar_v1: (a) => buildAvatarV1(a as Parameters<typeof buildAvatarV1>[0]),
|
||||
add_badge_v1: (a) => buildBadgeV1(a as Parameters<typeof buildBadgeV1>[0]),
|
||||
add_divider_v1: (a) => buildDividerV1(a as Parameters<typeof buildDividerV1>[0]),
|
||||
add_body_text_v1: (a) => buildBodyTextV1(a as Parameters<typeof buildBodyTextV1>[0]),
|
||||
add_icon_label_v1: (a) => buildIconLabelV1(a as Parameters<typeof buildIconLabelV1>[0]),
|
||||
};
|
||||
|
||||
interface ExecToolBody {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,11 @@ import {
|
|||
buildSettingRowV1,
|
||||
buildMemberRowV1,
|
||||
buildActivityLogV1,
|
||||
buildAvatarV1,
|
||||
buildBadgeV1,
|
||||
buildDividerV1,
|
||||
buildBodyTextV1,
|
||||
buildIconLabelV1,
|
||||
} from '@zseven-w/pen-core';
|
||||
|
||||
export interface BuilderCase {
|
||||
|
|
@ -747,4 +752,29 @@ export const CASES: BuilderCase[] = [
|
|||
},
|
||||
build: (a) => buildActivityLogV1(a as unknown as Parameters<typeof buildActivityLogV1>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_avatar_v1',
|
||||
args: { initial: 'SL', size: 48, theme: 'light' },
|
||||
build: (a) => buildAvatarV1(a as unknown as Parameters<typeof buildAvatarV1>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_badge_v1',
|
||||
args: { label: 'NEW', theme: 'light' },
|
||||
build: (a) => buildBadgeV1(a as unknown as Parameters<typeof buildBadgeV1>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_divider_v1',
|
||||
args: { orientation: 'horizontal', thickness: 1, theme: 'light' },
|
||||
build: (a) => buildDividerV1(a as unknown as Parameters<typeof buildDividerV1>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_body_text_v1',
|
||||
args: { content: 'The quick brown fox jumps over the lazy dog.', theme: 'light' },
|
||||
build: (a) => buildBodyTextV1(a as unknown as Parameters<typeof buildBodyTextV1>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_icon_label_v1',
|
||||
args: { icon: 'star', label: 'Favorites', theme: 'light' },
|
||||
build: (a) => buildIconLabelV1(a as unknown as Parameters<typeof buildIconLabelV1>[0]),
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@ import {
|
|||
buildSettingRowV1,
|
||||
buildMemberRowV1,
|
||||
buildActivityLogV1,
|
||||
buildAvatarV1,
|
||||
buildBadgeV1,
|
||||
buildDividerV1,
|
||||
buildBodyTextV1,
|
||||
buildIconLabelV1,
|
||||
type ActionMenuParams,
|
||||
type ActivityRingParams,
|
||||
type AlertParams,
|
||||
|
|
@ -194,6 +199,11 @@ import {
|
|||
type SettingRowV1Params,
|
||||
type MemberRowV1Params,
|
||||
type ActivityLogV1Params,
|
||||
type AvatarV1Params,
|
||||
type BadgeV1Params,
|
||||
type DividerV1Params,
|
||||
type BodyTextV1Params,
|
||||
type IconLabelV1Params,
|
||||
type KbdParams,
|
||||
type LinkParams,
|
||||
type ListRowParams,
|
||||
|
|
@ -416,6 +426,11 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
|
|||
add_setting_row_v1: wrap<SettingRowV1Params>(buildSettingRowV1),
|
||||
add_member_row_v1: wrap<MemberRowV1Params>(buildMemberRowV1),
|
||||
add_activity_log_v1: wrap<ActivityLogV1Params>(buildActivityLogV1),
|
||||
add_avatar_v1: wrap<AvatarV1Params>(buildAvatarV1),
|
||||
add_badge_v1: wrap<BadgeV1Params>(buildBadgeV1),
|
||||
add_divider_v1: wrap<DividerV1Params>(buildDividerV1),
|
||||
add_body_text_v1: wrap<BodyTextV1Params>(buildBodyTextV1),
|
||||
add_icon_label_v1: wrap<IconLabelV1Params>(buildIconLabelV1),
|
||||
};
|
||||
|
||||
export function getElementShim(name: string): ElementShim | undefined {
|
||||
|
|
|
|||
|
|
@ -69,8 +69,11 @@ Containers and single elements:
|
|||
Atoms (1-2 node building blocks):
|
||||
|
||||
10. Hairline separator between list rows / sections → `add_divider_v0`
|
||||
10b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_divider_v1`
|
||||
11. Short inline pill / tag / "NEW" / "BETA" / count badge → `add_badge_v0`
|
||||
11b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_badge_v1`
|
||||
12. Circular avatar (with optional initial / empty for later image fill) → `add_avatar_v0`
|
||||
12b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_avatar_v1`
|
||||
|
||||
Text + button primitives:
|
||||
|
||||
|
|
@ -78,10 +81,12 @@ Text + button primitives:
|
|||
14. Heading with enforced fontSize/lineHeight per level + AUTO CJK script detection (SC/JP/KR) → `add_heading_v0`
|
||||
14b. Theme-aware variant (supports `theme: 'light' | 'dark' | 'system'`) → `add_heading_v1`
|
||||
15. Body text (Inter everywhere — CJK gets lineHeight 1.6 + letterSpacing 0, Latin 1.5) → `add_body_text_v0`
|
||||
15b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_body_text_v1`
|
||||
|
||||
Composition:
|
||||
|
||||
16. Icon + text inline pair (menu items, breadcrumbs, status indicators) → `add_icon_label_v0`
|
||||
16b. Theme-aware variant (accepts `theme` param for API consistency; output identical across themes) → `add_icon_label_v1`
|
||||
17. iOS/Material list row (leading icon + title/subtitle stack + trailing icon) → `add_list_row_v0`
|
||||
|
||||
Forms:
|
||||
|
|
|
|||
67
packages/pen-core/src/__tests__/avatar-v1.test.ts
Normal file
67
packages/pen-core/src/__tests__/avatar-v1.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildAvatar } from '../element-builders/avatar.js';
|
||||
import { buildAvatarV1 } from '../element-builders/avatar-v1.js';
|
||||
|
||||
function stripTheme<T extends Record<string, unknown>>(obj: T): Omit<T, 'theme'> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { theme: _t, ...rest } = obj;
|
||||
return rest;
|
||||
}
|
||||
|
||||
describe('buildAvatarV1 — byte-parity with v0 (light)', () => {
|
||||
it('no initial: frame shape matches v0', () => {
|
||||
const v0 = buildAvatar({}) as Record<string, unknown>;
|
||||
const v1 = buildAvatarV1({}) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('with initial: full tree matches v0', () => {
|
||||
const v0 = buildAvatar({ initial: 'SL', size: 48 }) as Record<string, unknown>;
|
||||
const v1 = buildAvatarV1({ initial: 'SL', size: 48 }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('explicit theme=light still matches v0', () => {
|
||||
const v0 = buildAvatar({ initial: 'A' }) as Record<string, unknown>;
|
||||
const v1 = buildAvatarV1({ initial: 'A', theme: 'light' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('cornerRadius = size/2', () => {
|
||||
const v1 = buildAvatarV1({ size: 60 }) as Record<string, unknown>;
|
||||
expect(v1.cornerRadius).toBe(30);
|
||||
});
|
||||
|
||||
it('initial fontSize = max(12, round(size * 0.4))', () => {
|
||||
const v1 = buildAvatarV1({ initial: 'X', size: 40 }) as Record<string, unknown>;
|
||||
const children = v1.children as Array<Record<string, unknown>>;
|
||||
expect(children[0].fontSize).toBe(16); // max(12, round(40 * 0.4)) = 16
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAvatarV1 — dark mode (no-color tool, identical to light)', () => {
|
||||
it('theme=dark output identical to theme=light', () => {
|
||||
const light = buildAvatarV1({ initial: 'SL', size: 48 }) as Record<string, unknown>;
|
||||
const dark = buildAvatarV1({ initial: 'SL', size: 48, theme: 'dark' }) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(dark)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAvatarV1 — system mode (no-color tool, identical to light)', () => {
|
||||
it('theme=system output identical to theme=light', () => {
|
||||
const light = buildAvatarV1({ initial: 'SL', size: 48 }) as Record<string, unknown>;
|
||||
const system = buildAvatarV1({ initial: 'SL', size: 48, theme: 'system' }) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(system)));
|
||||
});
|
||||
|
||||
it('no $color-* refs emitted (theme param is accepted but has no effect)', () => {
|
||||
const system = JSON.stringify(buildAvatarV1({ initial: 'SL', theme: 'system' }));
|
||||
expect(system).not.toContain('$color-');
|
||||
});
|
||||
});
|
||||
56
packages/pen-core/src/__tests__/badge-v1.test.ts
Normal file
56
packages/pen-core/src/__tests__/badge-v1.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildBadge } from '../element-builders/badge.js';
|
||||
import { buildBadgeV1 } from '../element-builders/badge-v1.js';
|
||||
|
||||
function stripTheme<T extends Record<string, unknown>>(obj: T): Omit<T, 'theme'> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { theme: _t, ...rest } = obj;
|
||||
return rest;
|
||||
}
|
||||
|
||||
describe('buildBadgeV1 — byte-parity with v0 (light)', () => {
|
||||
it('output matches v0 (no theme)', () => {
|
||||
const v0 = buildBadge({ label: 'NEW' }) as Record<string, unknown>;
|
||||
const v1 = buildBadgeV1({ label: 'NEW' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('explicit theme=light still matches v0', () => {
|
||||
const v0 = buildBadge({ label: 'BETA' }) as Record<string, unknown>;
|
||||
const v1 = buildBadgeV1({ label: 'BETA', theme: 'light' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('cornerRadius=999 (full pill)', () => {
|
||||
const v1 = buildBadgeV1({ label: '42' }) as Record<string, unknown>;
|
||||
expect(v1.cornerRadius).toBe(999);
|
||||
});
|
||||
|
||||
it('label child: fontSize=11, fontWeight=600', () => {
|
||||
const v1 = buildBadgeV1({ label: 'BETA' }) as Record<string, unknown>;
|
||||
const children = v1.children as Array<Record<string, unknown>>;
|
||||
expect(children[0].fontSize).toBe(11);
|
||||
expect(children[0].fontWeight).toBe(600);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildBadgeV1 — dark mode (no-color tool, identical to light)', () => {
|
||||
it('theme=dark output identical to theme=light', () => {
|
||||
const light = buildBadgeV1({ label: 'NEW' }) as Record<string, unknown>;
|
||||
const dark = buildBadgeV1({ label: 'NEW', theme: 'dark' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(dark)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildBadgeV1 — system mode (no-color tool, identical to light)', () => {
|
||||
it('theme=system output identical to theme=light', () => {
|
||||
const light = buildBadgeV1({ label: 'NEW' }) as Record<string, unknown>;
|
||||
const system = buildBadgeV1({ label: 'NEW', theme: 'system' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(system)));
|
||||
});
|
||||
|
||||
it('no $color-* refs emitted', () => {
|
||||
const system = JSON.stringify(buildBadgeV1({ label: 'TEST', theme: 'system' }));
|
||||
expect(system).not.toContain('$color-');
|
||||
});
|
||||
});
|
||||
81
packages/pen-core/src/__tests__/body-text-v1.test.ts
Normal file
81
packages/pen-core/src/__tests__/body-text-v1.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildBodyText } from '../element-builders/body-text.js';
|
||||
import { buildBodyTextV1 } from '../element-builders/body-text-v1.js';
|
||||
|
||||
function stripTheme<T extends Record<string, unknown>>(obj: T): Omit<T, 'theme'> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { theme: _t, ...rest } = obj;
|
||||
return rest;
|
||||
}
|
||||
|
||||
const LATIN_TEXT = 'The quick brown fox jumps over the lazy dog.';
|
||||
const CJK_TEXT = '快速的棕色狐狸跳过了懒惰的狗。';
|
||||
|
||||
describe('buildBodyTextV1 — byte-parity with v0 (light)', () => {
|
||||
it('Latin content matches v0', () => {
|
||||
const v0 = buildBodyText({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
const v1 = buildBodyTextV1({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('CJK content matches v0 (lineHeight=1.6, letterSpacing=0)', () => {
|
||||
const v0 = buildBodyText({ content: CJK_TEXT }) as Record<string, unknown>;
|
||||
const v1 = buildBodyTextV1({ content: CJK_TEXT }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('explicit theme=light still matches v0', () => {
|
||||
const v0 = buildBodyText({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
const v1 = buildBodyTextV1({ content: LATIN_TEXT, theme: 'light' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('Latin: lineHeight=1.5, no letterSpacing', () => {
|
||||
const v1 = buildBodyTextV1({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
expect(v1.lineHeight).toBe(1.5);
|
||||
expect(v1.letterSpacing).toBeUndefined();
|
||||
});
|
||||
|
||||
it('CJK: lineHeight=1.6, letterSpacing=0', () => {
|
||||
const v1 = buildBodyTextV1({ content: CJK_TEXT }) as Record<string, unknown>;
|
||||
expect(v1.lineHeight).toBe(1.6);
|
||||
expect(v1.letterSpacing).toBe(0);
|
||||
});
|
||||
|
||||
it('always fontFamily=Inter, width=fill_container, textGrowth=fixed-width', () => {
|
||||
const v1 = buildBodyTextV1({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
expect(v1.fontFamily).toBe('Inter');
|
||||
expect(v1.width).toBe('fill_container');
|
||||
expect(v1.textGrowth).toBe('fixed-width');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildBodyTextV1 — dark mode (no-color tool, identical to light)', () => {
|
||||
it('theme=dark output identical to theme=light (Latin)', () => {
|
||||
const light = buildBodyTextV1({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
const dark = buildBodyTextV1({ content: LATIN_TEXT, theme: 'dark' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(dark)));
|
||||
});
|
||||
|
||||
it('theme=dark output identical to theme=light (CJK)', () => {
|
||||
const light = buildBodyTextV1({ content: CJK_TEXT }) as Record<string, unknown>;
|
||||
const dark = buildBodyTextV1({ content: CJK_TEXT, theme: 'dark' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(dark)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildBodyTextV1 — system mode (no-color tool, identical to light)', () => {
|
||||
it('theme=system output identical to theme=light', () => {
|
||||
const light = buildBodyTextV1({ content: LATIN_TEXT }) as Record<string, unknown>;
|
||||
const system = buildBodyTextV1({ content: LATIN_TEXT, theme: 'system' }) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(system)));
|
||||
});
|
||||
|
||||
it('no $color-* refs emitted', () => {
|
||||
const system = JSON.stringify(buildBodyTextV1({ content: LATIN_TEXT, theme: 'system' }));
|
||||
expect(system).not.toContain('$color-');
|
||||
});
|
||||
});
|
||||
75
packages/pen-core/src/__tests__/divider-v1.test.ts
Normal file
75
packages/pen-core/src/__tests__/divider-v1.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildDivider } from '../element-builders/divider.js';
|
||||
import { buildDividerV1 } from '../element-builders/divider-v1.js';
|
||||
|
||||
function stripTheme<T extends Record<string, unknown>>(obj: T): Omit<T, 'theme'> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { theme: _t, ...rest } = obj;
|
||||
return rest;
|
||||
}
|
||||
|
||||
describe('buildDividerV1 — byte-parity with v0 (light)', () => {
|
||||
it('default horizontal matches v0', () => {
|
||||
const v0 = buildDivider({}) as Record<string, unknown>;
|
||||
const v1 = buildDividerV1({}) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('vertical orientation matches v0', () => {
|
||||
const v0 = buildDivider({ orientation: 'vertical' }) as Record<string, unknown>;
|
||||
const v1 = buildDividerV1({ orientation: 'vertical' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('custom thickness matches v0', () => {
|
||||
const v0 = buildDivider({ thickness: 2 }) as Record<string, unknown>;
|
||||
const v1 = buildDividerV1({ thickness: 2 }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('explicit theme=light still matches v0', () => {
|
||||
const v0 = buildDivider({ orientation: 'horizontal', thickness: 1 }) as Record<string, unknown>;
|
||||
const v1 = buildDividerV1({
|
||||
orientation: 'horizontal',
|
||||
thickness: 1,
|
||||
theme: 'light',
|
||||
}) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('horizontal: width=fill_container, height=1', () => {
|
||||
const v1 = buildDividerV1({}) as Record<string, unknown>;
|
||||
expect(v1.width).toBe('fill_container');
|
||||
expect(v1.height).toBe(1);
|
||||
});
|
||||
|
||||
it('vertical: width=1, height=fill_container', () => {
|
||||
const v1 = buildDividerV1({ orientation: 'vertical' }) as Record<string, unknown>;
|
||||
expect(v1.width).toBe(1);
|
||||
expect(v1.height).toBe('fill_container');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildDividerV1 — dark mode (no-color tool, identical to light)', () => {
|
||||
it('theme=dark output identical to theme=light', () => {
|
||||
const light = buildDividerV1({ orientation: 'horizontal' }) as Record<string, unknown>;
|
||||
const dark = buildDividerV1({ orientation: 'horizontal', theme: 'dark' }) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(dark)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildDividerV1 — system mode (no-color tool, identical to light)', () => {
|
||||
it('theme=system output identical to theme=light', () => {
|
||||
const light = buildDividerV1({}) as Record<string, unknown>;
|
||||
const system = buildDividerV1({ theme: 'system' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(system)));
|
||||
});
|
||||
|
||||
it('no $color-* refs emitted', () => {
|
||||
const system = JSON.stringify(buildDividerV1({ theme: 'system' }));
|
||||
expect(system).not.toContain('$color-');
|
||||
});
|
||||
});
|
||||
75
packages/pen-core/src/__tests__/icon-label-v1.test.ts
Normal file
75
packages/pen-core/src/__tests__/icon-label-v1.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildIconLabel } from '../element-builders/icon-label.js';
|
||||
import { buildIconLabelV1 } from '../element-builders/icon-label-v1.js';
|
||||
|
||||
function stripTheme<T extends Record<string, unknown>>(obj: T): Omit<T, 'theme'> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { theme: _t, ...rest } = obj;
|
||||
return rest;
|
||||
}
|
||||
|
||||
const BASIC = { icon: 'star', label: 'Favorites' };
|
||||
|
||||
describe('buildIconLabelV1 — byte-parity with v0 (light)', () => {
|
||||
it('default params match v0', () => {
|
||||
const v0 = buildIconLabel(BASIC) as Record<string, unknown>;
|
||||
const v1 = buildIconLabelV1(BASIC) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('custom gap matches v0', () => {
|
||||
const v0 = buildIconLabel({ ...BASIC, gap: 12 }) as Record<string, unknown>;
|
||||
const v1 = buildIconLabelV1({ ...BASIC, gap: 12 }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('explicit theme=light still matches v0', () => {
|
||||
const v0 = buildIconLabel(BASIC) as Record<string, unknown>;
|
||||
const v1 = buildIconLabelV1({ ...BASIC, theme: 'light' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(v1))).toBe(JSON.stringify(v0));
|
||||
});
|
||||
|
||||
it('icon child: icon_font with lucide family, 16×16', () => {
|
||||
const v1 = buildIconLabelV1(BASIC) as Record<string, unknown>;
|
||||
const children = v1.children as Array<Record<string, unknown>>;
|
||||
const icon = children[0];
|
||||
expect(icon.type).toBe('icon_font');
|
||||
expect(icon.iconFontFamily).toBe('lucide');
|
||||
expect(icon.width).toBe(16);
|
||||
expect(icon.height).toBe(16);
|
||||
});
|
||||
|
||||
it('label child: fontSize=14, fontWeight=500', () => {
|
||||
const v1 = buildIconLabelV1(BASIC) as Record<string, unknown>;
|
||||
const children = v1.children as Array<Record<string, unknown>>;
|
||||
const label = children[1];
|
||||
expect(label.fontSize).toBe(14);
|
||||
expect(label.fontWeight).toBe(500);
|
||||
});
|
||||
|
||||
it('default gap=8', () => {
|
||||
const v1 = buildIconLabelV1(BASIC) as Record<string, unknown>;
|
||||
expect(v1.gap).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildIconLabelV1 — dark mode (no-color tool, identical to light)', () => {
|
||||
it('theme=dark output identical to theme=light', () => {
|
||||
const light = buildIconLabelV1(BASIC) as Record<string, unknown>;
|
||||
const dark = buildIconLabelV1({ ...BASIC, theme: 'dark' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(dark)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildIconLabelV1 — system mode (no-color tool, identical to light)', () => {
|
||||
it('theme=system output identical to theme=light', () => {
|
||||
const light = buildIconLabelV1(BASIC) as Record<string, unknown>;
|
||||
const system = buildIconLabelV1({ ...BASIC, theme: 'system' }) as Record<string, unknown>;
|
||||
expect(JSON.stringify(stripTheme(light))).toBe(JSON.stringify(stripTheme(system)));
|
||||
});
|
||||
|
||||
it('no $color-* refs emitted', () => {
|
||||
const system = JSON.stringify(buildIconLabelV1({ ...BASIC, theme: 'system' }));
|
||||
expect(system).not.toContain('$color-');
|
||||
});
|
||||
});
|
||||
53
packages/pen-core/src/element-builders/avatar-v1.ts
Normal file
53
packages/pen-core/src/element-builders/avatar-v1.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
import type { V1Theme } from './resolve-theme.js';
|
||||
|
||||
export interface AvatarV1Params {
|
||||
initial?: string;
|
||||
size?: number;
|
||||
/**
|
||||
* Theme mode.
|
||||
* - `'light'` (default): byte-parity with add_avatar_v0.
|
||||
* - `'dark'`: byte-parity with light (avatar has no hardcoded color fills).
|
||||
* - `'system'`: byte-parity with light (no $color-* refs needed).
|
||||
*
|
||||
* `buildAvatar` emits no fill literals — the frame and text nodes carry no
|
||||
* fill, relying on the canvas default / parent context. Theme param is
|
||||
* accepted for API consistency with other v1 tools.
|
||||
*/
|
||||
theme?: V1Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Circular avatar with optional centered initial — theme-aware version of
|
||||
* buildAvatar. Light mode is byte-equal to add_avatar_v0.
|
||||
*
|
||||
* Since buildAvatar emits no hardcoded color fills, all three theme modes
|
||||
* produce identical output. The theme parameter exists for API consistency
|
||||
* so callers can pass theme uniformly across all v1 element tools.
|
||||
*/
|
||||
export function buildAvatarV1(params: AvatarV1Params): ElementTree {
|
||||
const size = params.size ?? 40;
|
||||
const children: ElementTree[] = [];
|
||||
if (params.initial) {
|
||||
children.push({
|
||||
type: 'text',
|
||||
name: 'Initial',
|
||||
role: 'heading',
|
||||
content: params.initial,
|
||||
fontSize: Math.max(12, Math.round(size * 0.4)),
|
||||
fontWeight: 600,
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Avatar',
|
||||
role: 'avatar',
|
||||
width: size,
|
||||
height: size,
|
||||
cornerRadius: size / 2,
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
children,
|
||||
};
|
||||
}
|
||||
49
packages/pen-core/src/element-builders/badge-v1.ts
Normal file
49
packages/pen-core/src/element-builders/badge-v1.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
import type { V1Theme } from './resolve-theme.js';
|
||||
|
||||
export interface BadgeV1Params {
|
||||
label: string;
|
||||
/**
|
||||
* Theme mode.
|
||||
* - `'light'` (default): byte-parity with add_badge_v0.
|
||||
* - `'dark'`: byte-parity with light (badge has no hardcoded color fills).
|
||||
* - `'system'`: byte-parity with light (no $color-* refs needed).
|
||||
*
|
||||
* `buildBadge` emits no fill literals — colors are applied by the caller
|
||||
* via a follow-up batch_design U-op. Theme param is accepted for API
|
||||
* consistency with other v1 tools.
|
||||
*/
|
||||
theme?: V1Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Short inline badge / pill / tag — theme-aware version of buildBadge.
|
||||
* Light mode is byte-equal to add_badge_v0.
|
||||
*
|
||||
* Since buildBadge emits no hardcoded color fills, all three theme modes
|
||||
* produce identical output. The theme parameter exists for API consistency
|
||||
* so callers can pass theme uniformly across all v1 element tools.
|
||||
*/
|
||||
export function buildBadgeV1(params: BadgeV1Params): ElementTree {
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Badge',
|
||||
role: 'badge',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
padding: [4, 10],
|
||||
cornerRadius: 999,
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Label',
|
||||
role: 'label',
|
||||
content: params.label,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
47
packages/pen-core/src/element-builders/body-text-v1.ts
Normal file
47
packages/pen-core/src/element-builders/body-text-v1.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { detectCjkScript } from './cjk-detect.js';
|
||||
import type { ElementTree } from './helpers.js';
|
||||
import type { V1Theme } from './resolve-theme.js';
|
||||
|
||||
export interface BodyTextV1Params {
|
||||
content: string;
|
||||
/**
|
||||
* Theme mode.
|
||||
* - `'light'` (default): byte-parity with add_body_text_v0.
|
||||
* - `'dark'`: byte-parity with light (body text has no hardcoded color fills).
|
||||
* - `'system'`: byte-parity with light (no $color-* refs needed).
|
||||
*
|
||||
* `buildBodyText` emits no fill literals — text color is inherited from
|
||||
* the canvas default. Theme param is accepted for API consistency with
|
||||
* other v1 tools.
|
||||
*/
|
||||
theme?: V1Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Body / description text — theme-aware version of buildBodyText.
|
||||
* Light mode is byte-equal to add_body_text_v0.
|
||||
*
|
||||
* CJK detection is preserved: CJK content gets lineHeight=1.6 + letterSpacing=0;
|
||||
* Latin content gets lineHeight=1.5 with no letterSpacing override.
|
||||
*
|
||||
* Since buildBodyText emits no hardcoded color fills, all three theme modes
|
||||
* produce identical output. The theme parameter exists for API consistency
|
||||
* so callers can pass theme uniformly across all v1 element tools.
|
||||
*/
|
||||
export function buildBodyTextV1(params: BodyTextV1Params): ElementTree {
|
||||
const isCjk = detectCjkScript(params.content) !== null;
|
||||
const body: ElementTree = {
|
||||
type: 'text',
|
||||
name: 'Body',
|
||||
role: 'body',
|
||||
content: params.content,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
fontFamily: 'Inter',
|
||||
lineHeight: isCjk ? 1.6 : 1.5,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
};
|
||||
if (isCjk) body.letterSpacing = 0;
|
||||
return body;
|
||||
}
|
||||
40
packages/pen-core/src/element-builders/divider-v1.ts
Normal file
40
packages/pen-core/src/element-builders/divider-v1.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
import type { V1Theme } from './resolve-theme.js';
|
||||
|
||||
export type DividerV1Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
export interface DividerV1Params {
|
||||
orientation?: DividerV1Orientation;
|
||||
thickness?: number;
|
||||
/**
|
||||
* Theme mode.
|
||||
* - `'light'` (default): byte-parity with add_divider_v0.
|
||||
* - `'dark'`: byte-parity with light (divider has no hardcoded color fills).
|
||||
* - `'system'`: byte-parity with light (no $color-* refs needed).
|
||||
*
|
||||
* `buildDivider` emits no fill literals — colors are inherited from ambient
|
||||
* theme / Style Guide via a follow-up batch_design U-op. Theme param is
|
||||
* accepted for API consistency with other v1 tools.
|
||||
*/
|
||||
theme?: V1Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hairline divider — theme-aware version of buildDivider.
|
||||
* Light mode is byte-equal to add_divider_v0.
|
||||
*
|
||||
* Since buildDivider emits no hardcoded color fills, all three theme modes
|
||||
* produce identical output. The theme parameter exists for API consistency
|
||||
* so callers can pass theme uniformly across all v1 element tools.
|
||||
*/
|
||||
export function buildDividerV1(params: DividerV1Params): ElementTree {
|
||||
const orientation = params.orientation ?? 'horizontal';
|
||||
const thickness = params.thickness ?? 1;
|
||||
return {
|
||||
type: 'rectangle',
|
||||
name: 'Divider',
|
||||
role: 'divider',
|
||||
width: orientation === 'horizontal' ? 'fill_container' : thickness,
|
||||
height: orientation === 'horizontal' ? thickness : 'fill_container',
|
||||
};
|
||||
}
|
||||
59
packages/pen-core/src/element-builders/icon-label-v1.ts
Normal file
59
packages/pen-core/src/element-builders/icon-label-v1.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
import type { V1Theme } from './resolve-theme.js';
|
||||
|
||||
export interface IconLabelV1Params {
|
||||
icon: string;
|
||||
label: string;
|
||||
gap?: number;
|
||||
/**
|
||||
* Theme mode.
|
||||
* - `'light'` (default): byte-parity with add_icon_label_v0.
|
||||
* - `'dark'`: byte-parity with light (icon_label has no hardcoded color fills).
|
||||
* - `'system'`: byte-parity with light (no $color-* refs needed).
|
||||
*
|
||||
* `buildIconLabel` emits no fill literals — icon and text colors are
|
||||
* inherited from the canvas default / parent context. Theme param is
|
||||
* accepted for API consistency with other v1 tools.
|
||||
*/
|
||||
theme?: V1Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomic icon + label pair — theme-aware version of buildIconLabel.
|
||||
* Light mode is byte-equal to add_icon_label_v0.
|
||||
*
|
||||
* Since buildIconLabel emits no hardcoded color fills, all three theme modes
|
||||
* produce identical output. The theme parameter exists for API consistency
|
||||
* so callers can pass theme uniformly across all v1 element tools.
|
||||
*/
|
||||
export function buildIconLabelV1(params: IconLabelV1Params): ElementTree {
|
||||
const gap = params.gap ?? 8;
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Icon Label',
|
||||
role: 'icon-label',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap,
|
||||
children: [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Icon',
|
||||
iconFontName: params.icon,
|
||||
iconFontFamily: 'lucide',
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Label',
|
||||
role: 'label',
|
||||
content: params.label,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -147,3 +147,8 @@ export {
|
|||
type MemberRowV1Trailing,
|
||||
} from './member-row-v1.js';
|
||||
export { buildActivityLogV1, type ActivityLogV1Params } from './activity-log-v1.js';
|
||||
export { buildAvatarV1, type AvatarV1Params } from './avatar-v1.js';
|
||||
export { buildBadgeV1, type BadgeV1Params } from './badge-v1.js';
|
||||
export { buildDividerV1, type DividerV1Orientation, type DividerV1Params } from './divider-v1.js';
|
||||
export { buildBodyTextV1, type BodyTextV1Params } from './body-text-v1.js';
|
||||
export { buildIconLabelV1, type IconLabelV1Params } from './icon-label-v1.js';
|
||||
|
|
|
|||
|
|
@ -436,4 +436,15 @@ export {
|
|||
type MemberRowV1Trailing,
|
||||
buildActivityLogV1,
|
||||
type ActivityLogV1Params,
|
||||
buildAvatarV1,
|
||||
type AvatarV1Params,
|
||||
buildBadgeV1,
|
||||
type BadgeV1Params,
|
||||
buildDividerV1,
|
||||
type DividerV1Orientation,
|
||||
type DividerV1Params,
|
||||
buildBodyTextV1,
|
||||
type BodyTextV1Params,
|
||||
buildIconLabelV1,
|
||||
type IconLabelV1Params,
|
||||
} from './element-builders/index.js';
|
||||
|
|
|
|||
149
packages/pen-mcp/src/routes/element-tool-defs-ext-5.ts
Normal file
149
packages/pen-mcp/src/routes/element-tool-defs-ext-5.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// Extension tool definitions — shard 5 of 5 (siblings: base / ext /
|
||||
// ext-2 / ext-3 / ext-4). Houses P3 batch-1 v1 tools (avatar, badge,
|
||||
// divider, body_text, icon_label). Each shard caps at the repo's
|
||||
// 800-line ceiling.
|
||||
//
|
||||
// When adding a new tool: pick whichever shard has the fewest tools
|
||||
// to keep the split balanced. Run `wc -l` on all shard files before
|
||||
// committing.
|
||||
|
||||
import {
|
||||
schemaVersionProp,
|
||||
filePathProp,
|
||||
parentIdProp,
|
||||
pageIdProp,
|
||||
} from './element-tool-def-props';
|
||||
|
||||
export const ELEMENT_TOOL_DEFINITIONS_EXT_5 = [
|
||||
{
|
||||
name: 'add_avatar_v1',
|
||||
description:
|
||||
'Theme-aware circular avatar (v1). theme="light" (default): byte-parity with ' +
|
||||
'add_avatar_v0 (no fill attrs — inherits from parent context). theme="dark" / "system": ' +
|
||||
'identical output (avatar has no hardcoded color fills). Accepts theme param for API ' +
|
||||
'consistency across all v1 tools. schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
initial: { type: 'string', description: 'Optional centered initial (1-2 chars).' },
|
||||
size: { type: 'number', description: 'Avatar diameter px. Default 40.' },
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: ['light', 'dark', 'system'],
|
||||
description: 'Theme variant. Default "light". All modes produce identical output.',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_badge_v1',
|
||||
description:
|
||||
'Theme-aware short inline badge / pill / tag (v1). theme="light" (default): byte-parity ' +
|
||||
'with add_badge_v0 (no fill attrs — caller applies colors via batch_design U-op). ' +
|
||||
'theme="dark" / "system": identical output (badge has no hardcoded color fills). ' +
|
||||
'cornerRadius=999 (full pill), fontSize=11, fontWeight=600. schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
label: { type: 'string', description: 'Badge text (e.g. "NEW", "BETA", "42").' },
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: ['light', 'dark', 'system'],
|
||||
description: 'Theme variant. Default "light". All modes produce identical output.',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['label'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_divider_v1',
|
||||
description:
|
||||
'Theme-aware hairline divider (v1). theme="light" (default): byte-parity with ' +
|
||||
'add_divider_v0 (no fill attrs — colors inherited from ambient theme). theme="dark" / ' +
|
||||
'"system": identical output (divider has no hardcoded color fills). Horizontal = ' +
|
||||
'fill_container × thickness px; vertical = thickness × fill_container. schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
orientation: {
|
||||
type: 'string',
|
||||
enum: ['horizontal', 'vertical'],
|
||||
description: 'Divider direction. Default "horizontal".',
|
||||
},
|
||||
thickness: { type: 'number', description: 'Thickness in px. Default 1.' },
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: ['light', 'dark', 'system'],
|
||||
description: 'Theme variant. Default "light". All modes produce identical output.',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_body_text_v1',
|
||||
description:
|
||||
'Theme-aware body / description text (v1). theme="light" (default): byte-parity with ' +
|
||||
'add_body_text_v0 (no fill attrs — text color inherited from canvas default). ' +
|
||||
'theme="dark" / "system": identical output (body text has no hardcoded color fills). ' +
|
||||
'CJK auto-detection applies in all modes (lineHeight=1.6 + letterSpacing=0 for CJK, ' +
|
||||
'lineHeight=1.5 for Latin). Always Inter, width=fill_container, textGrowth=fixed-width. ' +
|
||||
'schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
content: { type: 'string', description: 'Paragraph text content.' },
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: ['light', 'dark', 'system'],
|
||||
description: 'Theme variant. Default "light". All modes produce identical output.',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_icon_label_v1',
|
||||
description:
|
||||
'Theme-aware atomic icon + label pair (v1). theme="light" (default): byte-parity with ' +
|
||||
'add_icon_label_v0 (no fill attrs — icon and text colors inherited from parent context). ' +
|
||||
'theme="dark" / "system": identical output (icon_label has no hardcoded color fills). ' +
|
||||
'Horizontal layout, alignItems=center, icon leads (16×16 lucide), label 14/500. ' +
|
||||
'schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
icon: { type: 'string', description: 'Lucide icon slug (e.g. "star", "user").' },
|
||||
label: { type: 'string', description: 'Label text.' },
|
||||
gap: { type: 'number', description: 'Gap between icon and label px. Default 8.' },
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: ['light', 'dark', 'system'],
|
||||
description: 'Theme variant. Default "light". All modes produce identical output.',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['icon', 'label'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -117,12 +117,18 @@ import { handleAddCardRowV1 } from '../tools/add-card-row-v1';
|
|||
import { handleAddSettingRowV1 } from '../tools/add-setting-row-v1';
|
||||
import { handleAddMemberRowV1 } from '../tools/add-member-row-v1';
|
||||
import { handleAddActivityLogV1 } from '../tools/add-activity-log-v1';
|
||||
import { handleAddAvatarV1 } from '../tools/add-avatar-v1';
|
||||
import { handleAddBadgeV1 } from '../tools/add-badge-v1';
|
||||
import { handleAddDividerV1 } from '../tools/add-divider-v1';
|
||||
import { handleAddBodyTextV1 } from '../tools/add-body-text-v1';
|
||||
import { handleAddIconLabelV1 } from '../tools/add-icon-label-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';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_EXT_2 } from './element-tool-defs-ext-2';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_EXT_3 } from './element-tool-defs-ext-3';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_EXT_4 } from './element-tool-defs-ext-4';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_EXT_5 } from './element-tool-defs-ext-5';
|
||||
|
||||
export const ELEMENT_TOOL_DEFINITIONS = [
|
||||
...ELEMENT_TOOL_DEFINITIONS_BASE,
|
||||
|
|
@ -130,6 +136,7 @@ export const ELEMENT_TOOL_DEFINITIONS = [
|
|||
...ELEMENT_TOOL_DEFINITIONS_EXT_2,
|
||||
...ELEMENT_TOOL_DEFINITIONS_EXT_3,
|
||||
...ELEMENT_TOOL_DEFINITIONS_EXT_4,
|
||||
...ELEMENT_TOOL_DEFINITIONS_EXT_5,
|
||||
];
|
||||
|
||||
export const ELEMENT_TOOL_NAMES: ReadonlySet<string> = new Set(
|
||||
|
|
@ -355,6 +362,16 @@ async function dispatchElementToolCall(name: string, a: any): Promise<string> {
|
|||
return JSON.stringify(await handleAddMemberRowV1(a), null, 2);
|
||||
case 'add_activity_log_v1':
|
||||
return JSON.stringify(await handleAddActivityLogV1(a), null, 2);
|
||||
case 'add_avatar_v1':
|
||||
return JSON.stringify(await handleAddAvatarV1(a), null, 2);
|
||||
case 'add_badge_v1':
|
||||
return JSON.stringify(await handleAddBadgeV1(a), null, 2);
|
||||
case 'add_divider_v1':
|
||||
return JSON.stringify(await handleAddDividerV1(a), null, 2);
|
||||
case 'add_body_text_v1':
|
||||
return JSON.stringify(await handleAddBodyTextV1(a), null, 2);
|
||||
case 'add_icon_label_v1':
|
||||
return JSON.stringify(await handleAddIconLabelV1(a), null, 2);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
24
packages/pen-mcp/src/tools/add-avatar-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-avatar-v1.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { assignIdsRecursively, buildAvatarV1, type AvatarV1Params } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddAvatarV1Params extends AvatarV1Params {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Circular avatar with optional initial (v1) — theme-aware variant of add_avatar_v0.
|
||||
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
|
||||
* Since buildAvatar emits no color fills, all three modes produce identical output.
|
||||
* Tree build delegated to `buildAvatarV1` in `@zseven-w/pen-core`.
|
||||
*/
|
||||
export async function handleAddAvatarV1(
|
||||
params: AddAvatarV1Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildAvatarV1(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'av', tree, ...params });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-badge-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-badge-v1.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { assignIdsRecursively, buildBadgeV1, type BadgeV1Params } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddBadgeV1Params extends BadgeV1Params {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Short inline badge / pill / tag (v1) — theme-aware variant of add_badge_v0.
|
||||
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
|
||||
* Since buildBadge emits no color fills, all three modes produce identical output.
|
||||
* Tree build delegated to `buildBadgeV1` in `@zseven-w/pen-core`.
|
||||
*/
|
||||
export async function handleAddBadgeV1(
|
||||
params: AddBadgeV1Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildBadgeV1(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'bg', tree, ...params });
|
||||
}
|
||||
25
packages/pen-mcp/src/tools/add-body-text-v1.ts
Normal file
25
packages/pen-mcp/src/tools/add-body-text-v1.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { assignIdsRecursively, buildBodyTextV1, type BodyTextV1Params } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddBodyTextV1Params extends BodyTextV1Params {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Body / description text (v1) — theme-aware variant of add_body_text_v0.
|
||||
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
|
||||
* CJK auto-detection is preserved in all modes.
|
||||
* Since buildBodyText emits no color fills, all three modes produce identical output.
|
||||
* Tree build delegated to `buildBodyTextV1` in `@zseven-w/pen-core`.
|
||||
*/
|
||||
export async function handleAddBodyTextV1(
|
||||
params: AddBodyTextV1Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildBodyTextV1(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'bt', tree, ...params });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-divider-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-divider-v1.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { assignIdsRecursively, buildDividerV1, type DividerV1Params } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddDividerV1Params extends DividerV1Params {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hairline divider (v1) — theme-aware variant of add_divider_v0.
|
||||
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
|
||||
* Since buildDivider emits no color fills, all three modes produce identical output.
|
||||
* Tree build delegated to `buildDividerV1` in `@zseven-w/pen-core`.
|
||||
*/
|
||||
export async function handleAddDividerV1(
|
||||
params: AddDividerV1Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildDividerV1(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'dv', tree, ...params });
|
||||
}
|
||||
24
packages/pen-mcp/src/tools/add-icon-label-v1.ts
Normal file
24
packages/pen-mcp/src/tools/add-icon-label-v1.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { assignIdsRecursively, buildIconLabelV1, type IconLabelV1Params } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddIconLabelV1Params extends IconLabelV1Params {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomic icon + label pair (v1) — theme-aware variant of add_icon_label_v0.
|
||||
* Supports 'light' (v0 byte-parity), 'dark', and 'system' theme modes.
|
||||
* Since buildIconLabel emits no color fills, all three modes produce identical output.
|
||||
* Tree build delegated to `buildIconLabelV1` in `@zseven-w/pen-core`.
|
||||
*/
|
||||
export async function handleAddIconLabelV1(
|
||||
params: AddIconLabelV1Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildIconLabelV1(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'il', tree, ...params });
|
||||
}
|
||||
Loading…
Reference in a new issue