feat(ai): ship 5 element tools to reach 97 (filter_group / invite_row / activity_log / event_card / step_card)
Closes the obvious gaps remaining in the family: - add_filter_group_v0 — sidebar facet (heading + checkbox-style options with optional counts). Distinct from nav_chip_row (horizontal scrolling chips), tag (single applied chip), segmented_control (mutex tabs). - add_invite_row_v0 — pending invite row (avatar + email/role + status pill + trailing action). Distinct from member_row (a JOINED member, no status pill or action) and list_row (no avatar / status / action). - add_activity_log_v0 — single-line audit feed entry (optional tinted icon dot + actor in bold + action + right-aligned timestamp). Uses StyledTextSegment[] content for the bold/regular split. Distinct from timeline (multi-event vertical with connectors) and notification_row (title + body, no actor focus). - add_event_card_v0 — single calendar event tile (date column with month band + day number, then title + time + location). Distinct from calendar_grid (the full month grid) and card_row (no date column). - add_step_card_v0 — onboarding step card (numbered circle / check + title + description). Distinct from stepper (horizontal progress nav with connectors) and faq_item (collapsible Q&A header). 9 touchpoints per tool: pen-core builder + index + barrel + types, pen-mcp handler + dispatcher + ext-4 schema, apps/web shim + SERVER_BUILDERS, parity test (+5 cases), elements.md decision tree items 86-89 + 6 PREFER mappings with cross-links to existing tools, elements-cookbook.md arg-shape examples (8 entries across 5 tools).
This commit is contained in:
parent
a5b594cf69
commit
1fee613111
|
|
@ -63,6 +63,11 @@ import {
|
|||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildMemberRow,
|
||||
buildFilterGroup,
|
||||
buildInviteRow,
|
||||
buildActivityLog,
|
||||
buildEventCard,
|
||||
buildStepCard,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -249,6 +254,11 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
|
|||
add_profile_header_v0: (a) => buildProfileHeader(a as Parameters<typeof buildProfileHeader>[0]),
|
||||
add_setting_row_v0: (a) => buildSettingRow(a as Parameters<typeof buildSettingRow>[0]),
|
||||
add_member_row_v0: (a) => buildMemberRow(a as Parameters<typeof buildMemberRow>[0]),
|
||||
add_filter_group_v0: (a) => buildFilterGroup(a as Parameters<typeof buildFilterGroup>[0]),
|
||||
add_invite_row_v0: (a) => buildInviteRow(a as Parameters<typeof buildInviteRow>[0]),
|
||||
add_activity_log_v0: (a) => buildActivityLog(a as Parameters<typeof buildActivityLog>[0]),
|
||||
add_event_card_v0: (a) => buildEventCard(a as Parameters<typeof buildEventCard>[0]),
|
||||
add_step_card_v0: (a) => buildStepCard(a as Parameters<typeof buildStepCard>[0]),
|
||||
};
|
||||
|
||||
interface ExecToolBody {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ import {
|
|||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildMemberRow,
|
||||
buildFilterGroup,
|
||||
buildInviteRow,
|
||||
buildActivityLog,
|
||||
buildEventCard,
|
||||
buildStepCard,
|
||||
buildDatePicker,
|
||||
buildDivider,
|
||||
buildEmptyChart,
|
||||
|
|
@ -681,6 +686,59 @@ const CASES: BuilderCase[] = [
|
|||
},
|
||||
build: (a) => buildMemberRow(a as unknown as Parameters<typeof buildMemberRow>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_filter_group_v0',
|
||||
args: {
|
||||
title: 'Category',
|
||||
options: [
|
||||
{ label: 'Books', count: 42, selected: true },
|
||||
{ label: 'Music', count: 17 },
|
||||
],
|
||||
},
|
||||
build: (a) => buildFilterGroup(a as unknown as Parameters<typeof buildFilterGroup>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_invite_row_v0',
|
||||
args: {
|
||||
email: 'sarah@acme.com',
|
||||
role: 'Editor',
|
||||
status: 'pending',
|
||||
action_label: 'Resend',
|
||||
},
|
||||
build: (a) => buildInviteRow(a as unknown as Parameters<typeof buildInviteRow>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_activity_log_v0',
|
||||
args: {
|
||||
actor: 'Sarah Lee',
|
||||
action: 'merged pull request #142',
|
||||
timestamp: '2h ago',
|
||||
icon: 'git-merge',
|
||||
tone: 'success',
|
||||
},
|
||||
build: (a) => buildActivityLog(a as unknown as Parameters<typeof buildActivityLog>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_event_card_v0',
|
||||
args: {
|
||||
month: 'OCT',
|
||||
day: 15,
|
||||
title: 'Design review',
|
||||
time: '2:00 PM – 3:00 PM',
|
||||
location: 'Conference Room B',
|
||||
},
|
||||
build: (a) => buildEventCard(a as unknown as Parameters<typeof buildEventCard>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_step_card_v0',
|
||||
args: {
|
||||
number: 1,
|
||||
title: 'Connect your repo',
|
||||
description: 'Authorize GitHub so we can read your commits.',
|
||||
completed: true,
|
||||
},
|
||||
build: (a) => buildStepCard(a as unknown as Parameters<typeof buildStepCard>[0]),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ import {
|
|||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildMemberRow,
|
||||
buildFilterGroup,
|
||||
buildInviteRow,
|
||||
buildActivityLog,
|
||||
buildEventCard,
|
||||
buildStepCard,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -174,6 +179,11 @@ import {
|
|||
type ProfileHeaderParams,
|
||||
type SettingRowParams,
|
||||
type MemberRowParams,
|
||||
type FilterGroupParams,
|
||||
type InviteRowParams,
|
||||
type ActivityLogParams,
|
||||
type EventCardParams,
|
||||
type StepCardParams,
|
||||
type KbdParams,
|
||||
type LinkParams,
|
||||
type ListRowParams,
|
||||
|
|
@ -386,6 +396,11 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
|
|||
add_profile_header_v0: wrap<ProfileHeaderParams>(buildProfileHeader),
|
||||
add_setting_row_v0: wrap<SettingRowParams>(buildSettingRow),
|
||||
add_member_row_v0: wrap<MemberRowParams>(buildMemberRow),
|
||||
add_filter_group_v0: wrap<FilterGroupParams>(buildFilterGroup),
|
||||
add_invite_row_v0: wrap<InviteRowParams>(buildInviteRow),
|
||||
add_activity_log_v0: wrap<ActivityLogParams>(buildActivityLog),
|
||||
add_event_card_v0: wrap<EventCardParams>(buildEventCard),
|
||||
add_step_card_v0: wrap<StepCardParams>(buildStepCard),
|
||||
};
|
||||
|
||||
export function getElementShim(name: string): ElementShim | undefined {
|
||||
|
|
|
|||
|
|
@ -430,4 +430,64 @@ add_member_row_v0({
|
|||
initial: "PP",
|
||||
trailing: { kind: "menu" },
|
||||
})
|
||||
|
||||
add_filter_group_v0({
|
||||
title: "Category",
|
||||
options: [
|
||||
{ label: "Books", count: 42, selected: true },
|
||||
{ label: "Music", count: 17 },
|
||||
{ label: "Movies", count: 9 },
|
||||
],
|
||||
})
|
||||
|
||||
add_invite_row_v0({
|
||||
email: "sarah@acme.com",
|
||||
role: "Editor",
|
||||
status: "pending",
|
||||
action_label: "Resend",
|
||||
})
|
||||
|
||||
add_invite_row_v0({
|
||||
email: "alex@acme.com",
|
||||
role: "Viewer",
|
||||
status: "expired",
|
||||
action_label: "Revoke",
|
||||
})
|
||||
|
||||
add_activity_log_v0({
|
||||
actor: "Sarah Lee",
|
||||
action: "merged pull request #142",
|
||||
timestamp: "2h ago",
|
||||
icon: "git-merge",
|
||||
tone: "success",
|
||||
})
|
||||
|
||||
add_activity_log_v0({
|
||||
actor: "Marcus Chen",
|
||||
action: "deleted the staging environment",
|
||||
timestamp: "Yesterday",
|
||||
icon: "trash-2",
|
||||
tone: "danger",
|
||||
})
|
||||
|
||||
add_event_card_v0({
|
||||
month: "OCT",
|
||||
day: 15,
|
||||
title: "Design review",
|
||||
time: "2:00 PM – 3:00 PM",
|
||||
location: "Conference Room B",
|
||||
})
|
||||
|
||||
add_step_card_v0({
|
||||
number: 1,
|
||||
title: "Connect your repo",
|
||||
description: "Authorize GitHub so we can read your commits and surface them in your dashboard.",
|
||||
completed: true,
|
||||
})
|
||||
|
||||
add_step_card_v0({
|
||||
number: 2,
|
||||
title: "Invite teammates",
|
||||
description: "Add your team so they can collaborate on changes.",
|
||||
})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -275,8 +275,25 @@ Settings / preferences:
|
|||
People / lists:
|
||||
|
||||
84. Members / team list row (avatar + name + subtitle + role badge / kebab menu / status dot) → `add_member_row_v0`
|
||||
85. Pending invite list row (initial avatar + email/role + status pill + Resend action) → `add_invite_row_v0`
|
||||
|
||||
85. None match → fall through to `batch_design`
|
||||
Faceted search / filter sidebar:
|
||||
|
||||
86. Sidebar filter group (heading + checkbox-style option list with counts) → `add_filter_group_v0`
|
||||
|
||||
Audit / activity feed:
|
||||
|
||||
87. Activity log entry (actor in bold + action + timestamp + tinted icon dot) → `add_activity_log_v0`
|
||||
|
||||
Calendar:
|
||||
|
||||
88. Single event card (month/day column + title + time + location) → `add_event_card_v0`
|
||||
|
||||
Onboarding:
|
||||
|
||||
89. Numbered step card (circle/check + title + description) → `add_step_card_v0`
|
||||
|
||||
90. 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.
|
||||
|
||||
|
|
@ -374,6 +391,11 @@ PREFER an element tool when the spec says any of:
|
|||
- "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).
|
||||
- "facet panel", "filter sidebar", "category checklist", "brand filter", "applied filters group", "搜索筛选侧栏", "分面筛选", "类目筛选" → `add_filter_group_v0` (heading + vertical checkbox-style option list with optional counts). Different from `add_nav_chip_row_v0` (horizontal scrolling chips), `add_tag_v0` (single applied chip with × close), and `add_segmented_control_v0` (mutex pill tabs).
|
||||
- "pending invite", "invitation row", "team invite", "邀请列表行", "待接受邀请", "invitations table row" → `add_invite_row_v0` (initial avatar + email/role + status pill + trailing action). Different from `add_member_row_v0` (a JOINED member, no status pill or action slot) and `add_list_row_v0` (no avatar / status / action).
|
||||
- "audit log row", "activity feed entry", "recent activity item", "audit entry", "审计日志条目", "活动记录", "操作日志行" → `add_activity_log_v0` (single line: optional tinted icon dot + "<actor> <action>" + right-aligned timestamp). Different from `add_timeline_v0` (multi-event vertical with connecting line + per-event title + body) and `add_notification_row_v0` (title + body, no actor focus).
|
||||
- "event card", "agenda item", "meeting tile", "upcoming event", "calendar event row", "日程卡片", "会议条目", "活动卡片" → `add_event_card_v0` (date column with month band + day number, then title + time + location). Different from `add_calendar_grid_v0` (the full month grid) and `add_card_row_v0` (horizontally scrolling cards with title + subtitle + image, no date column).
|
||||
- "onboarding step", "how-it-works step", "tutorial step card", "setup checklist item", "操作步骤卡片", "教程步骤", "新手引导步骤" → `add_step_card_v0` (numbered circle / check + title + description, stacks vertically). Different from `add_stepper_v0` (horizontal progress nav with connectors) and `add_faq_item_v0` (collapsible Q&A header).
|
||||
|
||||
STILL use batch_design when:
|
||||
|
||||
|
|
|
|||
127
packages/pen-core/src/element-builders/activity-log.ts
Normal file
127
packages/pen-core/src/element-builders/activity-log.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export interface ActivityLogParams {
|
||||
/** Actor name (rendered bold). E.g. "Sarah Lee". */
|
||||
actor: string;
|
||||
/** Action verb-phrase (rendered as plain text after actor). E.g. "merged pull request #142". */
|
||||
action: string;
|
||||
/** Right-aligned relative timestamp (e.g. "2h ago", "Just now"). */
|
||||
timestamp: string;
|
||||
/** Optional leading lucide icon slug (16×16, in a small tinted dot). */
|
||||
icon?: string;
|
||||
/** Icon dot tone. Default "info". */
|
||||
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
||||
}
|
||||
|
||||
const ACTOR_FG = '#0F172A';
|
||||
const ACTION_FG = '#475569';
|
||||
const TS_FG = '#94A3B8';
|
||||
|
||||
const TONE_BG: Record<NonNullable<ActivityLogParams['tone']>, string> = {
|
||||
info: '#DBEAFE',
|
||||
success: '#DCFCE7',
|
||||
warning: '#FEF3C7',
|
||||
danger: '#FEE2E2',
|
||||
neutral: '#E2E8F0',
|
||||
};
|
||||
const TONE_FG: Record<NonNullable<ActivityLogParams['tone']>, string> = {
|
||||
info: '#1D4ED8',
|
||||
success: '#166534',
|
||||
warning: '#92400E',
|
||||
danger: '#991B1B',
|
||||
neutral: '#475569',
|
||||
};
|
||||
|
||||
/**
|
||||
* Audit / activity log entry — single line with optional small tinted
|
||||
* icon dot, then "<actor> <action>" in mixed weights, then a right-
|
||||
* aligned relative timestamp. Designed for stacking under a heading
|
||||
* to form a feed (e.g. "Recent activity", "Audit log").
|
||||
*
|
||||
* Distinct from `add_timeline_v0` (vertical multi-event with
|
||||
* connecting line + per-event title + body) and
|
||||
* `add_notification_row_v0` (title + body, no actor focus).
|
||||
*
|
||||
* Use for "audit log row", "activity feed entry", "recent activity",
|
||||
* "审计日志条目", "活动记录".
|
||||
*/
|
||||
export function buildActivityLog(params: ActivityLogParams): ElementTree {
|
||||
const tone = params.tone ?? 'info';
|
||||
const rowChildren: ElementTree[] = [];
|
||||
|
||||
if (params.icon) {
|
||||
rowChildren.push({
|
||||
type: 'frame',
|
||||
name: 'Icon Dot',
|
||||
role: 'activity-log-icon-dot',
|
||||
width: 28,
|
||||
height: 28,
|
||||
cornerRadius: 14,
|
||||
fill: [{ type: 'solid', color: TONE_BG[tone] }],
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
children: [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Icon',
|
||||
role: 'activity-log-icon',
|
||||
iconFontName: params.icon,
|
||||
iconFontFamily: 'lucide',
|
||||
width: 14,
|
||||
height: 14,
|
||||
fill: [{ type: 'solid', color: TONE_FG[tone] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
rowChildren.push({
|
||||
type: 'frame',
|
||||
name: 'Body',
|
||||
role: 'activity-log-body',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'vertical',
|
||||
gap: 0,
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Line',
|
||||
role: 'activity-log-line',
|
||||
content: [
|
||||
{ text: params.actor, fontWeight: 600, fill: ACTOR_FG },
|
||||
{ text: ` ${params.action}`, fontWeight: 400, fill: ACTION_FG },
|
||||
],
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: ACTION_FG }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
rowChildren.push({
|
||||
type: 'text',
|
||||
name: 'Timestamp',
|
||||
role: 'activity-log-timestamp',
|
||||
content: params.timestamp,
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
fill: [{ type: 'solid', color: TS_FG }],
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Activity Log Entry',
|
||||
role: 'activity-log',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: [10, 0],
|
||||
children: rowChildren,
|
||||
};
|
||||
}
|
||||
202
packages/pen-core/src/element-builders/event-card.ts
Normal file
202
packages/pen-core/src/element-builders/event-card.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export interface EventCardParams {
|
||||
/** Short month code shown in the date column (e.g. "OCT", "10月"). */
|
||||
month: string;
|
||||
/** Day-of-month (e.g. "15"). */
|
||||
day: string | number;
|
||||
/** Event title (15/600). */
|
||||
title: string;
|
||||
/** Optional time string (e.g. "2:00 PM – 3:30 PM", "14:00"). */
|
||||
time?: string;
|
||||
/** Optional location string (e.g. "Conference Room B"). */
|
||||
location?: string;
|
||||
/** Accent color for the date column header. Default "#2563EB". */
|
||||
accent?: string;
|
||||
}
|
||||
|
||||
const TITLE_FG = '#0F172A';
|
||||
const META_FG = '#64748B';
|
||||
const DATE_BG = '#F1F5F9';
|
||||
|
||||
/**
|
||||
* Calendar event tile — left date column (month + day) + right text
|
||||
* stack (title + optional time + optional location). Distinct from
|
||||
* `add_calendar_grid_v0` (the full month grid) and `add_card_row_v0`
|
||||
* (a horizontally scrolling card row with title + subtitle + image).
|
||||
*
|
||||
* Use for "upcoming event card", "meeting tile", "agenda item",
|
||||
* "calendar event row", "日程卡片", "会议条目".
|
||||
*/
|
||||
export function buildEventCard(params: EventCardParams): ElementTree {
|
||||
const accent = params.accent ?? '#2563EB';
|
||||
|
||||
const dateColumn: ElementTree = {
|
||||
type: 'frame',
|
||||
name: 'Date Column',
|
||||
role: 'event-card-date',
|
||||
width: 64,
|
||||
height: 'fit_content',
|
||||
cornerRadius: 8,
|
||||
fill: [{ type: 'solid', color: DATE_BG }],
|
||||
layout: 'vertical',
|
||||
alignItems: 'center',
|
||||
children: [
|
||||
{
|
||||
type: 'frame',
|
||||
name: 'Month Strip',
|
||||
role: 'event-card-month-strip',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
fill: [{ type: 'solid', color: accent }],
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: [4, 0],
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Month',
|
||||
role: 'event-card-month',
|
||||
content: params.month,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1,
|
||||
fill: [{ type: 'solid', color: '#FFFFFF' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'frame',
|
||||
name: 'Day Strip',
|
||||
role: 'event-card-day-strip',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: [8, 0],
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Day',
|
||||
role: 'event-card-day',
|
||||
content: String(params.day),
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
fill: [{ type: 'solid', color: TITLE_FG }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const stack: ElementTree[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Title',
|
||||
role: 'event-card-title',
|
||||
content: params.title,
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: TITLE_FG }],
|
||||
},
|
||||
];
|
||||
if (params.time) {
|
||||
stack.push({
|
||||
type: 'frame',
|
||||
name: 'Time Row',
|
||||
role: 'event-card-meta-row',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
children: [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Time Icon',
|
||||
role: 'event-card-time-icon',
|
||||
iconFontName: 'clock',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 14,
|
||||
height: 14,
|
||||
fill: [{ type: 'solid', color: META_FG }],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Time',
|
||||
role: 'event-card-time',
|
||||
content: params.time,
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
fill: [{ type: 'solid', color: META_FG }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (params.location) {
|
||||
stack.push({
|
||||
type: 'frame',
|
||||
name: 'Location Row',
|
||||
role: 'event-card-meta-row',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
children: [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Location Icon',
|
||||
role: 'event-card-location-icon',
|
||||
iconFontName: 'map-pin',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 14,
|
||||
height: 14,
|
||||
fill: [{ type: 'solid', color: META_FG }],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Location',
|
||||
role: 'event-card-location',
|
||||
content: params.location,
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
fill: [{ type: 'solid', color: META_FG }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Event Card',
|
||||
role: 'event-card',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'flex-start',
|
||||
gap: 14,
|
||||
padding: [12, 12],
|
||||
cornerRadius: 12,
|
||||
fill: [{ type: 'solid', color: '#FFFFFF' }],
|
||||
stroke: { thickness: 1, fill: [{ type: 'solid', color: '#E2E8F0' }] },
|
||||
children: [
|
||||
dateColumn,
|
||||
{
|
||||
type: 'frame',
|
||||
name: 'Text Stack',
|
||||
role: 'event-card-text',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'vertical',
|
||||
gap: 6,
|
||||
padding: [4, 0],
|
||||
children: stack,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
132
packages/pen-core/src/element-builders/filter-group.ts
Normal file
132
packages/pen-core/src/element-builders/filter-group.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export interface FilterGroupOption {
|
||||
label: string;
|
||||
/** Optional count badge (e.g. "(42)"). */
|
||||
count?: number;
|
||||
/** Whether this facet is currently selected. Default false. */
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface FilterGroupParams {
|
||||
/** Heading text above the option list (e.g. "Category", "Brand"). */
|
||||
title: string;
|
||||
/** Options. Each renders a checkbox-style row + optional count. */
|
||||
options: FilterGroupOption[];
|
||||
}
|
||||
|
||||
const TITLE_FG = '#0F172A';
|
||||
const LABEL_FG = '#334155';
|
||||
const COUNT_FG = '#94A3B8';
|
||||
const BOX_BORDER = '#CBD5E1';
|
||||
const BOX_BG_SELECTED = '#2563EB';
|
||||
const BOX_FG_SELECTED = '#FFFFFF';
|
||||
|
||||
function buildOptionRow(opt: FilterGroupOption): ElementTree {
|
||||
const selected = opt.selected === true;
|
||||
const box: ElementTree = {
|
||||
type: 'frame',
|
||||
name: 'Box',
|
||||
role: 'filter-group-checkbox',
|
||||
width: 16,
|
||||
height: 16,
|
||||
cornerRadius: 4,
|
||||
fill: [{ type: 'solid', color: selected ? BOX_BG_SELECTED : '#FFFFFF' }],
|
||||
stroke: {
|
||||
thickness: 1.5,
|
||||
fill: [{ type: 'solid', color: selected ? BOX_BG_SELECTED : BOX_BORDER }],
|
||||
},
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
children: selected
|
||||
? [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Check',
|
||||
role: 'filter-group-check',
|
||||
iconFontName: 'check',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 12,
|
||||
height: 12,
|
||||
fill: [{ type: 'solid', color: BOX_FG_SELECTED }],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
const rowChildren: ElementTree[] = [
|
||||
box,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Label',
|
||||
role: 'filter-group-label',
|
||||
content: opt.label,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: LABEL_FG }],
|
||||
},
|
||||
];
|
||||
if (opt.count !== undefined) {
|
||||
rowChildren.push({
|
||||
type: 'text',
|
||||
name: 'Count',
|
||||
role: 'filter-group-count',
|
||||
content: String(opt.count),
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
fill: [{ type: 'solid', color: COUNT_FG }],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: `Option: ${opt.label}`,
|
||||
role: 'filter-group-option',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: [6, 0],
|
||||
children: rowChildren,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar filter group / facet — heading text over a vertical list of
|
||||
* checkbox-style option rows (label + optional count). Distinct from
|
||||
* `add_nav_chip_row_v0` (horizontal scrolling chips, single-select),
|
||||
* `add_tag_v0` (single applied chip with × close), and
|
||||
* `add_segmented_control_v0` (mutex pill tabs).
|
||||
*
|
||||
* Use for "facet panel", "filter sidebar", "category checklist",
|
||||
* "brand filter", "applied filters group", "搜索筛选侧栏".
|
||||
*/
|
||||
export function buildFilterGroup(params: FilterGroupParams): ElementTree {
|
||||
const optionRows = params.options.map(buildOptionRow);
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Filter Group',
|
||||
role: 'filter-group',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'vertical',
|
||||
gap: 8,
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Title',
|
||||
role: 'filter-group-title',
|
||||
content: params.title,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1,
|
||||
fill: [{ type: 'solid', color: TITLE_FG }],
|
||||
},
|
||||
...optionRows,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -124,3 +124,12 @@ export {
|
|||
export { buildCookieBanner, type CookieBannerParams } from './cookie-banner.js';
|
||||
export { buildSettingRow, type SettingRowParams, type SettingRowTrailing } from './setting-row.js';
|
||||
export { buildMemberRow, type MemberRowParams, type MemberRowTrailing } from './member-row.js';
|
||||
export {
|
||||
buildFilterGroup,
|
||||
type FilterGroupOption,
|
||||
type FilterGroupParams,
|
||||
} from './filter-group.js';
|
||||
export { buildInviteRow, type InviteRowParams, type InviteStatus } from './invite-row.js';
|
||||
export { buildActivityLog, type ActivityLogParams } from './activity-log.js';
|
||||
export { buildEventCard, type EventCardParams } from './event-card.js';
|
||||
export { buildStepCard, type StepCardParams } from './step-card.js';
|
||||
|
|
|
|||
150
packages/pen-core/src/element-builders/invite-row.ts
Normal file
150
packages/pen-core/src/element-builders/invite-row.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export type InviteStatus = 'pending' | 'expired' | 'accepted';
|
||||
|
||||
export interface InviteRowParams {
|
||||
/** Invitee email (e.g. "sarah@acme.com"). */
|
||||
email: string;
|
||||
/** Optional invited role (e.g. "Editor", "Viewer"). */
|
||||
role?: string;
|
||||
/** Status. Default "pending". */
|
||||
status?: InviteStatus;
|
||||
/** Trailing primary action label (e.g. "Resend", "Revoke"). Default "Resend". */
|
||||
action_label?: string;
|
||||
}
|
||||
|
||||
const NAME_FG = '#0F172A';
|
||||
const SUB_FG = '#64748B';
|
||||
const STATUS_TONE: Record<InviteStatus, { bg: string; fg: string; text: string }> = {
|
||||
pending: { bg: '#FEF3C7', fg: '#92400E', text: 'Pending' },
|
||||
expired: { bg: '#FEE2E2', fg: '#991B1B', text: 'Expired' },
|
||||
accepted: { bg: '#DCFCE7', fg: '#166534', text: 'Accepted' },
|
||||
};
|
||||
const ACTION_FG = '#2563EB';
|
||||
|
||||
/**
|
||||
* Pending invite list row — avatar (initial from email) + (email over
|
||||
* role) + status pill + trailing action label. Distinct from
|
||||
* `add_member_row_v0` (an actual joined member with full name + role
|
||||
* badge) and `add_list_row_v0` (no avatar, no status pill, no action
|
||||
* label slot).
|
||||
*
|
||||
* Use for "pending invites list", "invitations table row", "team
|
||||
* invite row", "邀请列表行", "待接受邀请".
|
||||
*/
|
||||
export function buildInviteRow(params: InviteRowParams): ElementTree {
|
||||
const status = params.status ?? 'pending';
|
||||
const tone = STATUS_TONE[status];
|
||||
const actionLabel = params.action_label ?? 'Resend';
|
||||
const initial = (params.email.charAt(0) ?? '?').toUpperCase();
|
||||
|
||||
const avatar: ElementTree = {
|
||||
type: 'frame',
|
||||
name: 'Avatar',
|
||||
role: 'invite-row-avatar',
|
||||
width: 36,
|
||||
height: 36,
|
||||
cornerRadius: 18,
|
||||
fill: [{ type: 'solid', color: '#E2E8F0' }],
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Initial',
|
||||
role: 'invite-row-avatar-initial',
|
||||
content: initial,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fill: [{ type: 'solid', color: SUB_FG }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const textStackChildren: ElementTree[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Email',
|
||||
role: 'invite-row-email',
|
||||
content: params.email,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: NAME_FG }],
|
||||
},
|
||||
];
|
||||
if (params.role) {
|
||||
textStackChildren.push({
|
||||
type: 'text',
|
||||
name: 'Role',
|
||||
role: 'invite-row-role',
|
||||
content: params.role,
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: SUB_FG }],
|
||||
});
|
||||
}
|
||||
|
||||
const statusPill: ElementTree = {
|
||||
type: 'frame',
|
||||
name: 'Status Pill',
|
||||
role: 'invite-row-status',
|
||||
width: 'fit_content',
|
||||
height: 'fit_content',
|
||||
cornerRadius: 12,
|
||||
fill: [{ type: 'solid', color: tone.bg }],
|
||||
padding: [3, 10],
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Status Text',
|
||||
role: 'invite-row-status-text',
|
||||
content: tone.text,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
fill: [{ type: 'solid', color: tone.fg }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const action: ElementTree = {
|
||||
type: 'text',
|
||||
name: 'Action',
|
||||
role: 'invite-row-action',
|
||||
content: actionLabel,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fill: [{ type: 'solid', color: ACTION_FG }],
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Invite Row',
|
||||
role: 'invite-row',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: [12, 16],
|
||||
children: [
|
||||
avatar,
|
||||
{
|
||||
type: 'frame',
|
||||
name: 'Text Stack',
|
||||
role: 'invite-row-text',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'vertical',
|
||||
gap: 2,
|
||||
children: textStackChildren,
|
||||
},
|
||||
statusPill,
|
||||
action,
|
||||
],
|
||||
};
|
||||
}
|
||||
121
packages/pen-core/src/element-builders/step-card.ts
Normal file
121
packages/pen-core/src/element-builders/step-card.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export interface StepCardParams {
|
||||
/** Step number / index (e.g. 1, "01", "Step 1"). */
|
||||
number: string | number;
|
||||
/** Step title (16/600). */
|
||||
title: string;
|
||||
/** Description body (14/400, muted). */
|
||||
description: string;
|
||||
/** Whether the step is completed. Default false. Filled circle vs ring. */
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
const TITLE_FG = '#0F172A';
|
||||
const DESC_FG = '#475569';
|
||||
const ACCENT = '#2563EB';
|
||||
const RING_BG = '#FFFFFF';
|
||||
|
||||
/**
|
||||
* Onboarding / how-it-works step card — numbered circle (or check
|
||||
* when completed) on the left, title over description on the right.
|
||||
* Designed to stack vertically under a section header to form an
|
||||
* onboarding column. Distinct from `add_stepper_v0` (horizontal
|
||||
* progress nav with connectors), `add_list_row_v0` (no number /
|
||||
* description split), and `add_faq_item_v0` (collapsible Q&A).
|
||||
*
|
||||
* Use for "onboarding step", "how-it-works step", "tutorial step
|
||||
* card", "setup checklist item", "操作步骤卡片", "教程步骤".
|
||||
*/
|
||||
export function buildStepCard(params: StepCardParams): ElementTree {
|
||||
const completed = params.completed === true;
|
||||
const numberText = String(params.number);
|
||||
|
||||
const circle: ElementTree = {
|
||||
type: 'frame',
|
||||
name: 'Number Circle',
|
||||
role: 'step-card-circle',
|
||||
width: 36,
|
||||
height: 36,
|
||||
cornerRadius: 18,
|
||||
fill: [{ type: 'solid', color: completed ? ACCENT : RING_BG }],
|
||||
stroke: completed ? undefined : { thickness: 2, fill: [{ type: 'solid', color: ACCENT }] },
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
children: completed
|
||||
? [
|
||||
{
|
||||
type: 'icon_font',
|
||||
name: 'Check',
|
||||
role: 'step-card-check',
|
||||
iconFontName: 'check',
|
||||
iconFontFamily: 'lucide',
|
||||
width: 18,
|
||||
height: 18,
|
||||
fill: [{ type: 'solid', color: '#FFFFFF' }],
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Number',
|
||||
role: 'step-card-number',
|
||||
content: numberText,
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
fill: [{ type: 'solid', color: ACCENT }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'frame',
|
||||
name: 'Step Card',
|
||||
role: 'step-card',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'flex-start',
|
||||
gap: 14,
|
||||
padding: [4, 0],
|
||||
children: [
|
||||
circle,
|
||||
{
|
||||
type: 'frame',
|
||||
name: 'Body',
|
||||
role: 'step-card-body',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'vertical',
|
||||
gap: 4,
|
||||
padding: [4, 0, 0, 0],
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Title',
|
||||
role: 'step-card-title',
|
||||
content: params.title,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: TITLE_FG }],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Description',
|
||||
role: 'step-card-description',
|
||||
content: params.description,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
width: 'fill_container',
|
||||
textGrowth: 'fixed-width',
|
||||
fill: [{ type: 'solid', color: DESC_FG }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -279,6 +279,11 @@ export {
|
|||
buildProfileHeader,
|
||||
buildSettingRow,
|
||||
buildMemberRow,
|
||||
buildFilterGroup,
|
||||
buildInviteRow,
|
||||
buildActivityLog,
|
||||
buildEventCard,
|
||||
buildStepCard,
|
||||
cjkFontFamily,
|
||||
detectCjkScript,
|
||||
type ElementTree,
|
||||
|
|
@ -408,4 +413,11 @@ export {
|
|||
type SettingRowTrailing,
|
||||
type MemberRowParams,
|
||||
type MemberRowTrailing,
|
||||
type FilterGroupOption,
|
||||
type FilterGroupParams,
|
||||
type InviteRowParams,
|
||||
type InviteStatus,
|
||||
type ActivityLogParams,
|
||||
type EventCardParams,
|
||||
type StepCardParams,
|
||||
} from './element-builders/index.js';
|
||||
|
|
|
|||
|
|
@ -398,4 +398,153 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_4 = [
|
|||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_filter_group_v0',
|
||||
description:
|
||||
'Sidebar filter group / facet — heading + vertical list of checkbox-style options ' +
|
||||
'(label + optional count). Distinct from add_nav_chip_row_v0 (horizontal scrolling chips), ' +
|
||||
'add_tag_v0 (single applied chip with × close), and add_segmented_control_v0 (mutex pill ' +
|
||||
'tabs). Use for "facet panel", "filter sidebar", "category checklist", "brand filter", ' +
|
||||
'"搜索筛选侧栏". schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
title: { type: 'string', description: 'Heading text (e.g. "Category", "Brand").' },
|
||||
options: {
|
||||
type: 'array',
|
||||
description: 'Option rows. Each: { label: string, count?: number, selected?: boolean }.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: { type: 'string' },
|
||||
count: { type: 'number' },
|
||||
selected: { type: 'boolean' },
|
||||
},
|
||||
required: ['label'],
|
||||
},
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['title', 'options'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_invite_row_v0',
|
||||
description:
|
||||
'Pending invite list row — initial avatar + (email over optional role) + status pill ' +
|
||||
'(pending / expired / accepted) + trailing action label (e.g. "Resend"). Distinct from ' +
|
||||
'add_member_row_v0 (a JOINED member with full name + role badge) and add_list_row_v0 (no ' +
|
||||
'avatar / status / action slots). Use for "pending invites list", "invitations table row", ' +
|
||||
'"team invite row", "邀请列表行", "待接受邀请". schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
email: { type: 'string', description: 'Invitee email.' },
|
||||
role: { type: 'string', description: 'Optional invited role (e.g. "Editor").' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'expired', 'accepted'],
|
||||
description: 'Status pill. Default "pending".',
|
||||
},
|
||||
action_label: {
|
||||
type: 'string',
|
||||
description: 'Trailing action label (e.g. "Resend", "Revoke"). Default "Resend".',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['email'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_activity_log_v0',
|
||||
description:
|
||||
'Audit / activity feed entry — optional small tinted icon dot + "<actor> <action>" line ' +
|
||||
'(actor bold) + right-aligned relative timestamp. Stack vertically under a heading to form ' +
|
||||
'a feed. Distinct from add_timeline_v0 (multi-event vertical with connecting line + per-' +
|
||||
'event title + body) and add_notification_row_v0 (title + body, no actor focus). Use for ' +
|
||||
'"audit log row", "activity feed entry", "recent activity item", "审计日志条目", "活动记录". ' +
|
||||
'schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
actor: { type: 'string', description: 'Actor name (rendered bold).' },
|
||||
action: { type: 'string', description: 'Action verb-phrase (plain).' },
|
||||
timestamp: { type: 'string', description: 'Relative time (e.g. "2h ago").' },
|
||||
icon: { type: 'string', description: 'Optional lucide icon slug for the leading dot.' },
|
||||
tone: {
|
||||
type: 'string',
|
||||
enum: ['info', 'success', 'warning', 'danger', 'neutral'],
|
||||
description: 'Icon dot tone. Default "info".',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['actor', 'action', 'timestamp'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_event_card_v0',
|
||||
description:
|
||||
'Calendar event tile — left date column (month band + day number) + right text stack ' +
|
||||
'(title + optional time + optional location). Distinct from add_calendar_grid_v0 (full ' +
|
||||
'month grid) and add_card_row_v0 (horizontally scrolling cards with title + subtitle + ' +
|
||||
'image, no date column). Use for "upcoming event", "agenda item", "meeting tile", ' +
|
||||
'"日程卡片", "会议条目". schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
month: { type: 'string', description: 'Short month code (e.g. "OCT", "10月").' },
|
||||
day: { type: ['string', 'number'], description: 'Day-of-month (e.g. "15" or 15).' },
|
||||
title: { type: 'string', description: 'Event title.' },
|
||||
time: { type: 'string', description: 'Optional time string (e.g. "2:00 PM").' },
|
||||
location: { type: 'string', description: 'Optional location string.' },
|
||||
accent: {
|
||||
type: 'string',
|
||||
description: 'Accent color hex for the month band. Default "#2563EB".',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['month', 'day', 'title'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_step_card_v0',
|
||||
description:
|
||||
'Onboarding / how-it-works step card — numbered circle (or check when completed) + ' +
|
||||
'(title over description) body. Stack vertically to form an onboarding column. Distinct ' +
|
||||
'from add_stepper_v0 (horizontal progress nav with connectors) and add_faq_item_v0 ' +
|
||||
'(collapsible Q&A). Use for "onboarding step", "how-it-works step", "tutorial step card", ' +
|
||||
'"setup checklist item", "操作步骤卡片", "教程步骤". schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
number: {
|
||||
type: ['string', 'number'],
|
||||
description: 'Step number / index (e.g. 1, "01", "Step 1").',
|
||||
},
|
||||
title: { type: 'string', description: 'Step title (16/600).' },
|
||||
description: { type: 'string', description: 'Description body (14/400, muted).' },
|
||||
completed: {
|
||||
type: 'boolean',
|
||||
description: 'Filled circle + check icon when true. Default false (numbered ring).',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['number', 'title', 'description'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -107,6 +107,11 @@ 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 { handleAddMemberRowV0 } from '../tools/add-member-row-v0';
|
||||
import { handleAddFilterGroupV0 } from '../tools/add-filter-group-v0';
|
||||
import { handleAddInviteRowV0 } from '../tools/add-invite-row-v0';
|
||||
import { handleAddActivityLogV0 } from '../tools/add-activity-log-v0';
|
||||
import { handleAddEventCardV0 } from '../tools/add-event-card-v0';
|
||||
import { handleAddStepCardV0 } from '../tools/add-step-card-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';
|
||||
|
|
@ -325,6 +330,16 @@ async function dispatchElementToolCall(name: string, a: any): Promise<string> {
|
|||
return JSON.stringify(await handleAddSettingRowV0(a), null, 2);
|
||||
case 'add_member_row_v0':
|
||||
return JSON.stringify(await handleAddMemberRowV0(a), null, 2);
|
||||
case 'add_filter_group_v0':
|
||||
return JSON.stringify(await handleAddFilterGroupV0(a), null, 2);
|
||||
case 'add_invite_row_v0':
|
||||
return JSON.stringify(await handleAddInviteRowV0(a), null, 2);
|
||||
case 'add_activity_log_v0':
|
||||
return JSON.stringify(await handleAddActivityLogV0(a), null, 2);
|
||||
case 'add_event_card_v0':
|
||||
return JSON.stringify(await handleAddEventCardV0(a), null, 2);
|
||||
case 'add_step_card_v0':
|
||||
return JSON.stringify(await handleAddStepCardV0(a), null, 2);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
18
packages/pen-mcp/src/tools/add-activity-log-v0.ts
Normal file
18
packages/pen-mcp/src/tools/add-activity-log-v0.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { assignIdsRecursively, buildActivityLog, type ActivityLogParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddActivityLogV0Params extends ActivityLogParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export async function handleAddActivityLogV0(
|
||||
params: AddActivityLogV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildActivityLog(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'activityLog', tree, ...params });
|
||||
}
|
||||
18
packages/pen-mcp/src/tools/add-event-card-v0.ts
Normal file
18
packages/pen-mcp/src/tools/add-event-card-v0.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { assignIdsRecursively, buildEventCard, type EventCardParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddEventCardV0Params extends EventCardParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export async function handleAddEventCardV0(
|
||||
params: AddEventCardV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildEventCard(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'eventCard', tree, ...params });
|
||||
}
|
||||
18
packages/pen-mcp/src/tools/add-filter-group-v0.ts
Normal file
18
packages/pen-mcp/src/tools/add-filter-group-v0.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { assignIdsRecursively, buildFilterGroup, type FilterGroupParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddFilterGroupV0Params extends FilterGroupParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export async function handleAddFilterGroupV0(
|
||||
params: AddFilterGroupV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildFilterGroup(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'filterGroup', tree, ...params });
|
||||
}
|
||||
18
packages/pen-mcp/src/tools/add-invite-row-v0.ts
Normal file
18
packages/pen-mcp/src/tools/add-invite-row-v0.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { assignIdsRecursively, buildInviteRow, type InviteRowParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddInviteRowV0Params extends InviteRowParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export async function handleAddInviteRowV0(
|
||||
params: AddInviteRowV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildInviteRow(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'inviteRow', tree, ...params });
|
||||
}
|
||||
18
packages/pen-mcp/src/tools/add-step-card-v0.ts
Normal file
18
packages/pen-mcp/src/tools/add-step-card-v0.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { assignIdsRecursively, buildStepCard, type StepCardParams } from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddStepCardV0Params extends StepCardParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export async function handleAddStepCardV0(
|
||||
params: AddStepCardV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const tree = buildStepCard(params);
|
||||
assignIdsRecursively(tree);
|
||||
return insertElementTree({ binding: 'stepCard', tree, ...params });
|
||||
}
|
||||
Loading…
Reference in a new issue