feat(ai): add_setting_row_v0 — settings menu row (91st tool)
Leading icon + (title over optional subtitle) + trailing control with 4 variants: chevron / value text / switch / badge. Distinct from add_list_row_v0 (trailing is always icon, no switch/value/badge) and add_form_field_v0 (label-above-input for forms). Wires all 9 touchpoints: pen-core builder + index + barrel re-export, pen-mcp handler + dispatcher case + ext-4 schema, apps/web shim + Nitro SERVER_BUILDERS, elements.md decision tree #83 + PREFER mapping, elements-cookbook arg-shape examples, plus shim-server parity case.
This commit is contained in:
parent
e656b52f95
commit
af1ddd1ad2
|
|
@ -61,6 +61,7 @@ import {
|
|||
buildLegendItem,
|
||||
buildInboxMessage,
|
||||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -245,6 +246,7 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
|
|||
add_legend_item_v0: (a) => buildLegendItem(a as Parameters<typeof buildLegendItem>[0]),
|
||||
add_inbox_message_v0: (a) => buildInboxMessage(a as Parameters<typeof buildInboxMessage>[0]),
|
||||
add_profile_header_v0: (a) => buildProfileHeader(a as Parameters<typeof buildProfileHeader>[0]),
|
||||
add_setting_row_v0: (a) => buildSettingRow(a as Parameters<typeof buildSettingRow>[0]),
|
||||
};
|
||||
|
||||
interface ExecToolBody {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
buildLegendItem,
|
||||
buildInboxMessage,
|
||||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildDatePicker,
|
||||
buildDivider,
|
||||
buildEmptyChart,
|
||||
|
|
@ -659,6 +660,16 @@ const CASES: BuilderCase[] = [
|
|||
args: { name: 'Sarah Lee', handle: '@sarah', bio: 'Designer at Acme.', initial: 'SL' },
|
||||
build: (a) => buildProfileHeader(a as unknown as Parameters<typeof buildProfileHeader>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_setting_row_v0',
|
||||
args: {
|
||||
title: 'Notifications',
|
||||
subtitle: 'Push, email, in-app',
|
||||
leading_icon: 'bell',
|
||||
trailing: { kind: 'switch', on: true },
|
||||
},
|
||||
build: (a) => buildSettingRow(a as unknown as Parameters<typeof buildSettingRow>[0]),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ import {
|
|||
buildLegendItem,
|
||||
buildInboxMessage,
|
||||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -170,6 +171,7 @@ import {
|
|||
type LegendItemParams,
|
||||
type InboxMessageParams,
|
||||
type ProfileHeaderParams,
|
||||
type SettingRowParams,
|
||||
type KbdParams,
|
||||
type LinkParams,
|
||||
type ListRowParams,
|
||||
|
|
@ -380,6 +382,7 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
|
|||
add_legend_item_v0: wrap<LegendItemParams>(buildLegendItem),
|
||||
add_inbox_message_v0: wrap<InboxMessageParams>(buildInboxMessage),
|
||||
add_profile_header_v0: wrap<ProfileHeaderParams>(buildProfileHeader),
|
||||
add_setting_row_v0: wrap<SettingRowParams>(buildSettingRow),
|
||||
};
|
||||
|
||||
export function getElementShim(name: string): ElementShim | undefined {
|
||||
|
|
|
|||
|
|
@ -390,4 +390,22 @@ add_inbox_message_v0({
|
|||
timestamp: "10:42 AM",
|
||||
unread: true,
|
||||
})
|
||||
|
||||
add_setting_row_v0({
|
||||
title: "Notifications",
|
||||
subtitle: "Push, email, in-app",
|
||||
leading_icon: "bell",
|
||||
trailing: { kind: "switch", on: true },
|
||||
})
|
||||
|
||||
add_setting_row_v0({
|
||||
title: "Language",
|
||||
leading_icon: "globe",
|
||||
trailing: { kind: "value", value: "English" },
|
||||
})
|
||||
|
||||
add_setting_row_v0({
|
||||
title: "What's new",
|
||||
trailing: { kind: "badge", value: "New" },
|
||||
})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -268,7 +268,11 @@ Mail / inbox:
|
|||
|
||||
82. Inbox / email list row (sender + subject + preview + unread dot) → `add_inbox_message_v0`
|
||||
|
||||
83. None match → fall through to `batch_design`
|
||||
Settings / preferences:
|
||||
|
||||
83. Settings menu row (icon + title + subtitle + trailing chevron / value / switch / badge) → `add_setting_row_v0`
|
||||
|
||||
84. None match → fall through to `batch_design`
|
||||
|
||||
**Disambiguation**: if you need a ROW of 3 metrics that should NOT scroll (e.g. a stats strip inside a card), use `add_stat_grid_v0`, NOT `add_metric_row_v0`. The grid uses `fill_container` per cell so it never overflows; the metric row uses fixed-px cells + scroll wrapper.
|
||||
|
||||
|
|
@ -364,6 +368,7 @@ PREFER an element tool when the spec says any of:
|
|||
- "share row", "share to social", "share buttons", "post share", "send via", "分享按钮组" → `add_share_row_v0` (horizontal circular icon buttons each labeled below). Different from `add_social_login_row_v0` (sign-in CTAs).
|
||||
- "chart legend", "legend item", "legend entry", "数据图例", "图例条目" → `add_legend_item_v0` (marker + label + optional value). Different from `add_status_badge_v0` (semantic dot + text).
|
||||
- "inbox row", "email row", "message list cell", "mail item", "email preview", "邮件条目", "收件箱条目" → `add_inbox_message_v0` (sender + subject + preview + timestamp + unread dot). Different from `add_notification_row_v0` (single title + body) and `add_list_row_v0` (no timestamp).
|
||||
- "settings row", "preference row", "menu item with toggle", "settings list item", "设置项", "偏好项", "开关行" → `add_setting_row_v0` (icon + title/subtitle + trailing chevron/value/switch/badge). Different from `add_list_row_v0` (trailing always icon, no switch/value/badge variants) and `add_form_field_v0` (label-above-input pattern for forms).
|
||||
|
||||
STILL use batch_design when:
|
||||
|
||||
|
|
|
|||
|
|
@ -122,3 +122,4 @@ export {
|
|||
type InputWithActionKind,
|
||||
} from './input-with-action.js';
|
||||
export { buildCookieBanner, type CookieBannerParams } from './cookie-banner.js';
|
||||
export { buildSettingRow, type SettingRowParams, type SettingRowTrailing } from './setting-row.js';
|
||||
|
|
|
|||
195
packages/pen-core/src/element-builders/setting-row.ts
Normal file
195
packages/pen-core/src/element-builders/setting-row.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export type SettingRowTrailing =
|
||||
| { kind: 'chevron' }
|
||||
| { kind: 'value'; value: string }
|
||||
| { kind: 'switch'; on: boolean }
|
||||
| { kind: 'badge'; value: string };
|
||||
|
||||
export interface SettingRowParams {
|
||||
/** Title text on the left (15/500). */
|
||||
title: string;
|
||||
/** Optional second-line text below the title (13/400, muted). */
|
||||
subtitle?: string;
|
||||
/** Optional leading lucide icon slug (24×24). */
|
||||
leading_icon?: string;
|
||||
/**
|
||||
* Trailing control. Default: chevron.
|
||||
* - chevron: a `chevron-right` icon (link-style row)
|
||||
* - value: muted text (e.g. "English") with no chevron
|
||||
* - switch: 36×22 pill, on/off
|
||||
* - badge: small colored capsule with text (e.g. "New")
|
||||
*/
|
||||
trailing?: SettingRowTrailing;
|
||||
}
|
||||
|
||||
const MUTED = '#64748B';
|
||||
const FG = '#0F172A';
|
||||
const PRIMARY = '#2563EB';
|
||||
const TRACK_OFF = '#CBD5E1';
|
||||
const KNOB = '#FFFFFF';
|
||||
const BADGE_BG = '#DBEAFE';
|
||||
const BADGE_FG = '#1D4ED8';
|
||||
|
||||
function buildTrailing(t: SettingRowTrailing): ElementTree {
|
||||
if (t.kind === 'value') {
|
||||
return {
|
||||
type: 'text',
|
||||
name: 'Trailing Value',
|
||||
role: 'setting-row-value',
|
||||
content: t.value,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
fill: [{ type: 'solid', color: MUTED }],
|
||||
};
|
||||
}
|
||||
if (t.kind === 'switch') {
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Switch',
|
||||
role: 'setting-row-switch',
|
||||
width: 36,
|
||||
height: 22,
|
||||
cornerRadius: 11,
|
||||
fill: [{ type: 'solid', color: t.on ? PRIMARY : TRACK_OFF }],
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: t.on ? 'flex-end' : 'flex-start',
|
||||
padding: [0, 3],
|
||||
children: [
|
||||
{
|
||||
type: 'frame',
|
||||
name: 'Knob',
|
||||
role: 'setting-row-switch-knob',
|
||||
width: 16,
|
||||
height: 16,
|
||||
cornerRadius: 8,
|
||||
fill: [{ type: 'solid', color: KNOB }],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (t.kind === 'badge') {
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Badge',
|
||||
role: 'setting-row-badge',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
cornerRadius: 4,
|
||||
fill: [{ type: 'solid', color: BADGE_BG }],
|
||||
padding: [2, 8],
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Badge Text',
|
||||
role: 'setting-row-badge-text',
|
||||
content: t.value,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
fill: [{ type: 'solid', color: BADGE_FG }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'icon_font',
|
||||
name: 'Chevron',
|
||||
role: 'setting-row-chevron',
|
||||
iconFontName: 'chevron-right',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 20,
|
||||
height: 20,
|
||||
fill: [{ type: 'solid', color: MUTED }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings menu row — leading icon + (title over optional subtitle) +
|
||||
* trailing control. Distinct from `add_list_row_v0` (generic; trailing
|
||||
* is always an icon, no switch/value/badge variants) and
|
||||
* `add_form_field_v0` (label-above-input, used inside forms not menus).
|
||||
*
|
||||
* Structure:
|
||||
* frame(horizontal, fill_container, fit_content, padding=[14,16],
|
||||
* gap=12, alignItems=center, role='setting-row')
|
||||
* ├ [icon_font 24×24] (if leading_icon)
|
||||
* ├ frame(vertical, fill_container, gap=2, role='setting-row-text')
|
||||
* │ ├ text(title, 15/500)
|
||||
* │ └ text(subtitle, 13/400, muted) (if subtitle)
|
||||
* └ <trailing> (chevron | value text | switch | badge)
|
||||
*
|
||||
* The text-stack uses fill_container so the title can wrap and push
|
||||
* row height naturally — same no-overlap invariant as `add_list_row_v0`.
|
||||
*/
|
||||
export function buildSettingRow(params: SettingRowParams): ElementTree {
|
||||
const trailing = params.trailing ?? { kind: 'chevron' };
|
||||
const rowChildren: ElementTree[] = [];
|
||||
|
||||
if (params.leading_icon) {
|
||||
rowChildren.push({
|
||||
type: 'icon_font',
|
||||
name: 'Leading Icon',
|
||||
role: 'setting-row-icon',
|
||||
iconFontName: params.leading_icon,
|
||||
iconFontFamily: 'lucide',
|
||||
width: 24,
|
||||
height: 24,
|
||||
fill: [{ type: 'solid', color: FG }],
|
||||
});
|
||||
}
|
||||
|
||||
const textStackChildren: ElementTree[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Title',
|
||||
role: 'setting-row-title',
|
||||
content: params.title,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: FG }],
|
||||
},
|
||||
];
|
||||
if (params.subtitle) {
|
||||
textStackChildren.push({
|
||||
type: 'text',
|
||||
name: 'Subtitle',
|
||||
role: 'setting-row-subtitle',
|
||||
content: params.subtitle,
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: MUTED }],
|
||||
});
|
||||
}
|
||||
|
||||
rowChildren.push({
|
||||
type: 'frame',
|
||||
name: 'Text Stack',
|
||||
role: 'setting-row-text',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'vertical',
|
||||
gap: 2,
|
||||
children: textStackChildren,
|
||||
});
|
||||
|
||||
rowChildren.push(buildTrailing(trailing));
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Setting Row',
|
||||
role: 'setting-row',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: [14, 16],
|
||||
children: rowChildren,
|
||||
};
|
||||
}
|
||||
|
|
@ -277,6 +277,7 @@ export {
|
|||
buildLegendItem,
|
||||
buildInboxMessage,
|
||||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
cjkFontFamily,
|
||||
detectCjkScript,
|
||||
type ElementTree,
|
||||
|
|
@ -402,4 +403,6 @@ export {
|
|||
type LegendItemParams,
|
||||
type InboxMessageParams,
|
||||
type ProfileHeaderParams,
|
||||
type SettingRowParams,
|
||||
type SettingRowTrailing,
|
||||
} from './element-builders/index.js';
|
||||
|
|
|
|||
|
|
@ -299,4 +299,49 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_4 = [
|
|||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_setting_row_v0',
|
||||
description:
|
||||
'Settings menu row — leading icon + (title over optional subtitle) + trailing control ' +
|
||||
'(chevron / value text / switch / badge). Distinct from add_list_row_v0 (generic, trailing ' +
|
||||
'is always an icon — no switch/value/badge variants) and add_form_field_v0 (label-above-' +
|
||||
'input pattern for forms, not menus). Use for "settings row", "preferences row", "menu ' +
|
||||
'item with toggle", "设置项", "偏好项". schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
title: { type: 'string', description: 'Title (left, 15/500).' },
|
||||
subtitle: { type: 'string', description: 'Optional second-line text (13/400, muted).' },
|
||||
leading_icon: { type: 'string', description: 'Optional leading lucide icon slug (24×24).' },
|
||||
trailing: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Trailing control. Default: { kind: "chevron" }. Variants: ' +
|
||||
'{ kind: "chevron" } | { kind: "value", value: string } | ' +
|
||||
'{ kind: "switch", on: boolean } | { kind: "badge", value: string }.',
|
||||
properties: {
|
||||
kind: {
|
||||
type: 'string',
|
||||
enum: ['chevron', 'value', 'switch', 'badge'],
|
||||
description: 'Trailing variant.',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Required for kind="value" and kind="badge".',
|
||||
},
|
||||
on: {
|
||||
type: 'boolean',
|
||||
description: 'Required for kind="switch".',
|
||||
},
|
||||
},
|
||||
required: ['kind'],
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ import { handleAddInlineActionV0 } from '../tools/add-inline-action-v0';
|
|||
import { handleAddLegendItemV0 } from '../tools/add-legend-item-v0';
|
||||
import { handleAddInboxMessageV0 } from '../tools/add-inbox-message-v0';
|
||||
import { handleAddProfileHeaderV0 } from '../tools/add-profile-header-v0';
|
||||
import { handleAddSettingRowV0 } from '../tools/add-setting-row-v0';
|
||||
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';
|
||||
|
|
@ -319,6 +320,8 @@ async function dispatchElementToolCall(name: string, a: any): Promise<string> {
|
|||
return JSON.stringify(await handleAddInboxMessageV0(a), null, 2);
|
||||
case 'add_profile_header_v0':
|
||||
return JSON.stringify(await handleAddProfileHeaderV0(a), null, 2);
|
||||
case 'add_setting_row_v0':
|
||||
return JSON.stringify(await handleAddSettingRowV0(a), null, 2);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
18
packages/pen-mcp/src/tools/add-setting-row-v0.ts
Normal file
18
packages/pen-mcp/src/tools/add-setting-row-v0.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { assignIdsRecursively, buildSettingRow, type SettingRowParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddSettingRowV0Params extends SettingRowParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export async function handleAddSettingRowV0(
|
||||
params: AddSettingRowV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const row = buildSettingRow(params);
|
||||
assignIdsRecursively(row);
|
||||
return insertElementTree({ binding: 'settingRow', tree: row, ...params });
|
||||
}
|
||||
Loading…
Reference in a new issue