From e0b926c25668d21711c00a9daa077213f109c5a1 Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 25 Apr 2026 01:19:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20add=5Fsocial=5Flogin=5Frow=5Fv0=20?= =?UTF-8?q?=E2=80=94=20social=20auth=20button=20row=20(69th=20tool)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Continue with Google / Apple / Microsoft" row. Vertical default (stacked full-width 48px buttons) or horizontal (compact icon-only 48×48 pills). Known provider names (google, apple, github, microsoft, facebook, twitter, linkedin, discord, slack, gitlab, email, phone) auto-map to lucide icons; `icon` param overrides for SSO/SAML/Okta. --- apps/web/server/api/mcp/exec-tool.post.ts | 3 + .../ai/__tests__/shim-server-parity.test.ts | 6 + .../services/ai/element-tool-shims/index.ts | 3 + .../skills/phases/generation/elements.md | 10 +- .../pen-core/src/element-builders/index.ts | 5 + .../src/element-builders/social-login-row.ts | 161 ++++++++++++++++ packages/pen-core/src/index.ts | 3 + .../__tests__/add-social-login-row-v0.test.ts | 175 ++++++++++++++++++ .../__tests__/element-tools-contract.test.ts | 1 + .../src/routes/element-tool-defs-ext-3.ts | 44 +++++ .../pen-mcp/src/routes/element-tool-defs.ts | 3 + .../src/tools/add-social-login-row-v0.ts | 25 +++ 12 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 packages/pen-core/src/element-builders/social-login-row.ts create mode 100644 packages/pen-mcp/src/__tests__/add-social-login-row-v0.test.ts create mode 100644 packages/pen-mcp/src/tools/add-social-login-row-v0.ts diff --git a/apps/web/server/api/mcp/exec-tool.post.ts b/apps/web/server/api/mcp/exec-tool.post.ts index b3892332f..ee0eea7e8 100644 --- a/apps/web/server/api/mcp/exec-tool.post.ts +++ b/apps/web/server/api/mcp/exec-tool.post.ts @@ -39,6 +39,7 @@ import { buildAttachmentRow, buildChatBubble, buildStatCard, + buildSocialLoginRow, buildKbd, buildLink, buildListRow, @@ -199,6 +200,8 @@ const SERVER_BUILDERS: Record = { add_attachment_row_v0: (a) => buildAttachmentRow(a as Parameters[0]), add_chat_bubble_v0: (a) => buildChatBubble(a as Parameters[0]), add_stat_card_v0: (a) => buildStatCard(a as Parameters[0]), + add_social_login_row_v0: (a) => + buildSocialLoginRow(a as Parameters[0]), }; interface ExecToolBody { diff --git a/apps/web/src/services/ai/__tests__/shim-server-parity.test.ts b/apps/web/src/services/ai/__tests__/shim-server-parity.test.ts index c81cbd647..4140f0673 100644 --- a/apps/web/src/services/ai/__tests__/shim-server-parity.test.ts +++ b/apps/web/src/services/ai/__tests__/shim-server-parity.test.ts @@ -45,6 +45,7 @@ import { buildOtpInput, buildAttachmentRow, buildChatBubble, + buildSocialLoginRow, buildStatCard, buildKbd, buildLink, @@ -477,6 +478,11 @@ const CASES: BuilderCase[] = [ }, build: (a) => buildStatCard(a as unknown as Parameters[0]), }, + { + toolName: 'add_social_login_row_v0', + args: { providers: [{ name: 'google' }, { name: 'apple' }] }, + build: (a) => buildSocialLoginRow(a as unknown as Parameters[0]), + }, ]; /** diff --git a/apps/web/src/services/ai/element-tool-shims/index.ts b/apps/web/src/services/ai/element-tool-shims/index.ts index c4047c798..ae6286c87 100644 --- a/apps/web/src/services/ai/element-tool-shims/index.ts +++ b/apps/web/src/services/ai/element-tool-shims/index.ts @@ -57,6 +57,7 @@ import { buildAttachmentRow, buildChatBubble, buildStatCard, + buildSocialLoginRow, buildKbd, buildLink, buildListRow, @@ -126,6 +127,7 @@ import { type AttachmentRowParams, type ChatBubbleParams, type StatCardParams, + type SocialLoginRowParams, type KbdParams, type LinkParams, type ListRowParams, @@ -314,6 +316,7 @@ export const ELEMENT_SHIMS: Record = { add_attachment_row_v0: wrap(buildAttachmentRow), add_chat_bubble_v0: wrap(buildChatBubble), add_stat_card_v0: wrap(buildStatCard), + add_social_login_row_v0: wrap(buildSocialLoginRow), }; export function getElementShim(name: string): ElementShim | undefined { diff --git a/packages/pen-ai-skills/skills/phases/generation/elements.md b/packages/pen-ai-skills/skills/phases/generation/elements.md index c101398b0..6fee023c4 100644 --- a/packages/pen-ai-skills/skills/phases/generation/elements.md +++ b/packages/pen-ai-skills/skills/phases/generation/elements.md @@ -204,7 +204,11 @@ Dashboard KPIs: 62. Big-number stat card (standalone metric tile — label + huge value + optional delta/icon) → `add_stat_card_v0` -63. None match → fall through to `batch_design` +Auth / login: + +63. Social auth provider buttons ("Continue with Google / Apple / Microsoft", OAuth/SSO row, third-party sign-in) → `add_social_login_row_v0` + +64. 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. @@ -278,6 +282,7 @@ PREFER an element tool when the spec says any of: - "attachment", "attached file", "uploaded file", "file item", "file list row", "附件", "已上传文件" → `add_attachment_row_v0` (for upload-in-progress state, compose `add_progress_bar_v0` below) - "chat message", "message bubble", "conversation row", "iMessage bubble", "chat UI", "聊天气泡", "消息气泡" → `add_chat_bubble_v0` (side="left" for from-others, side="right" for from-self) - "KPI card", "big number card", "metric tile", "stat widget", "featured metric", "关键指标卡", "数据大屏卡片" → `add_stat_card_v0` (distinct from `add_stat_grid_v0` which is multi-cell side-by-side) +- "Continue with Google", "Sign in with Apple", "social login", "OAuth buttons", "SSO providers", "third-party login", "第三方登录", "社交登录", "OAuth 登录" → `add_social_login_row_v0` (orientation="vertical" for stacked full-width on mobile; orientation="horizontal" for the compact "or sign in with..." icon-only row) STILL use batch_design when: @@ -513,6 +518,9 @@ add_chat_bubble_v0({ message: "My order hasn't arrived.", side: "right", timesta add_stat_card_v0({ label: "Monthly revenue", value: "$12.4k", icon: "trending-up", delta: "+8% vs last week", trend: "up" }) add_stat_card_v0({ label: "Active users", value: "1,284", icon: "users" }) // no delta = static snapshot + +add_social_login_row_v0({ providers: [{ name: "Google" }, { name: "Apple" }, { name: "Microsoft" }] }) // stacked "Continue with X" buttons +add_social_login_row_v0({ providers: [{ name: "Google" }, { name: "GitHub" }, { name: "Slack" }], orientation: "horizontal" }) // compact icon-only row ``` ## Composition pattern diff --git a/packages/pen-core/src/element-builders/index.ts b/packages/pen-core/src/element-builders/index.ts index 51c6c90d7..7e64d99e4 100644 --- a/packages/pen-core/src/element-builders/index.ts +++ b/packages/pen-core/src/element-builders/index.ts @@ -84,3 +84,8 @@ export { buildOtpInput, type OtpInputParams } from './otp-input.js'; export { buildAttachmentRow, type AttachmentRowParams } from './attachment-row.js'; export { buildChatBubble, type ChatBubbleParams, type ChatBubbleSide } from './chat-bubble.js'; export { buildStatCard, type StatCardParams, type StatCardTrend } from './stat-card.js'; +export { + buildSocialLoginRow, + type SocialLoginRowParams, + type SocialLoginProvider, +} from './social-login-row.js'; diff --git a/packages/pen-core/src/element-builders/social-login-row.ts b/packages/pen-core/src/element-builders/social-login-row.ts new file mode 100644 index 000000000..6a85f62d2 --- /dev/null +++ b/packages/pen-core/src/element-builders/social-login-row.ts @@ -0,0 +1,161 @@ +import type { ElementTree } from './helpers.js'; + +export interface SocialLoginProvider { + /** + * Provider name. Displayed inline as "Continue with {name}". + * Known names ('google', 'apple', 'microsoft', 'github', + * 'facebook', 'twitter', 'x', 'linkedin') auto-resolve to the + * matching lucide icon. Override via explicit `icon`. + */ + name: string; + /** + * Optional lucide icon override. Takes precedence over the + * known-name mapping. Use this for providers we don't have a + * default mapping for (e.g. "Okta", "SAML SSO"). + */ + icon?: string; +} + +export interface SocialLoginRowParams { + /** Array of providers to render. 2-4 recommended; clamped to 6 max. */ + providers: SocialLoginProvider[]; + /** + * Layout orientation. Default 'vertical' (stacked full-width + * buttons) — the pattern on most modern mobile login screens. + * 'horizontal' renders icon-only pills side-by-side, the more + * compact "also sign in with..." pattern. + */ + orientation?: 'vertical' | 'horizontal'; + /** Button width in px. Default 320. Min 200. Ignored on horizontal orientation. */ + width?: number; +} + +const KNOWN_ICONS: Record = { + google: 'chrome', + apple: 'apple', + microsoft: 'monitor', + github: 'github', + gitlab: 'git-branch', + facebook: 'facebook', + twitter: 'twitter', + x: 'twitter', + linkedin: 'linkedin', + discord: 'message-circle', + slack: 'hash', + email: 'mail', + phone: 'smartphone', +}; + +/** + * Social-auth provider button row — the "Continue with Google / + * Apple / Microsoft" pattern from every login / signup screen. + * + * Structure (vertical, default): + * frame(width, fit_content, layout=vertical, gap=10, + * role='social-login-row') + * └ frame(fill_container, 48px high, cornerRadius=12, + * bg=#FFFFFF, stroke=#E2E8F0, layout=horizontal, + * alignItems=center, gap=12, padding=[0,16], + * role='social-login-button') + * ├ icon_font(provider.icon, 20×20, slate-700) + * └ text("Continue with {Name}", 14/500, slate-900) + * + * Structure (horizontal): same buttons but `layout='horizontal'` + * wrapper, buttons collapse to 48×48 square with only the icon + * (no label). The compact variant for "quick alt login" rows. + * + * Why it's a tool: enough variation in provider order / icon + * choice / orientation that models hand-crafting this via + * batch_design frequently invert the icon+label order or miss the + * button-height constraint. Narrow tool locks the shape. + */ +export function buildSocialLoginRow(params: SocialLoginRowParams): ElementTree { + const raw = Array.isArray(params.providers) ? params.providers : []; + if (raw.length === 0) { + throw new Error('buildSocialLoginRow: providers array must not be empty'); + } + const providers = raw.slice(0, 6); + const orientation = params.orientation ?? 'vertical'; + const isVertical = orientation === 'vertical'; + const width = Math.max(200, Math.floor(params.width ?? 320)); + + const buttons: ElementTree[] = providers.map((provider) => { + const lowerName = provider.name.toLowerCase(); + const icon = provider.icon ?? KNOWN_ICONS[lowerName] ?? 'log-in'; + const label = `Continue with ${provider.name}`; + // Capitalize properly: "google" → "Google". If already capitalized, + // keep the caller's casing. + const prettyLabel = + provider.name.charAt(0) === provider.name.charAt(0).toUpperCase() + ? label + : `Continue with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`; + + const iconNode: ElementTree = { + type: 'icon_font', + name: `${provider.name} Icon`, + role: 'social-login-button-icon', + iconFontName: icon, + iconFontFamily: 'lucide', + width: 20, + height: 20, + fill: [{ type: 'solid', color: '#334155' }], + }; + + if (isVertical) { + return { + type: 'frame', + name: `${provider.name} Button`, + role: 'social-login-button', + width: 'fill_container', + height: 48, + cornerRadius: 12, + layout: 'horizontal', + alignItems: 'center', + gap: 12, + paddingLeft: 16, + paddingRight: 16, + fill: [{ type: 'solid', color: '#FFFFFF' }], + stroke: { thickness: 1, fill: [{ type: 'solid', color: '#E2E8F0' }] }, + children: [ + iconNode, + { + type: 'text', + name: 'Label', + role: 'social-login-button-label', + content: prettyLabel, + fontSize: 14, + fontWeight: 500, + fill: [{ type: 'solid', color: '#0F172A' }], + }, + ], + }; + } + // Horizontal: icon-only square pill + return { + type: 'frame', + name: `${provider.name} Button`, + role: 'social-login-button-compact', + width: 48, + height: 48, + cornerRadius: 12, + layout: 'horizontal', + alignItems: 'center', + justifyContent: 'center', + fill: [{ type: 'solid', color: '#FFFFFF' }], + stroke: { thickness: 1, fill: [{ type: 'solid', color: '#E2E8F0' }] }, + children: [iconNode], + }; + }); + + return { + type: 'frame', + name: 'Social Login Row', + role: 'social-login-row', + width: isVertical ? width : 'fit_content', + height: 'fit_content', + layout: isVertical ? 'vertical' : 'horizontal', + alignItems: 'center', + gap: 10, + children: buttons, + }; +} diff --git a/packages/pen-core/src/index.ts b/packages/pen-core/src/index.ts index d192b0721..66b910085 100644 --- a/packages/pen-core/src/index.ts +++ b/packages/pen-core/src/index.ts @@ -255,6 +255,7 @@ export { buildAttachmentRow, buildChatBubble, buildStatCard, + buildSocialLoginRow, cjkFontFamily, detectCjkScript, type ElementTree, @@ -345,4 +346,6 @@ export { type ChatBubbleSide, type StatCardParams, type StatCardTrend, + type SocialLoginRowParams, + type SocialLoginProvider, } from './element-builders/index.js'; diff --git a/packages/pen-mcp/src/__tests__/add-social-login-row-v0.test.ts b/packages/pen-mcp/src/__tests__/add-social-login-row-v0.test.ts new file mode 100644 index 000000000..653b176df --- /dev/null +++ b/packages/pen-mcp/src/__tests__/add-social-login-row-v0.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { writeFile, unlink, readFile, mkdir } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { DESIGN_TOOL_DEFINITIONS, DESIGN_TOOL_NAMES } from '../routes/design-routes'; +import { handleAddSocialLoginRowV0 } from '../tools/add-social-login-row-v0'; +import { invalidateCache } from '../document-manager'; + +const TMP = join(tmpdir(), 'openpencil-add-social-login-row-v0'); +const EMPTY = JSON.stringify({ version: '1.0.0', children: [] }); + +async function fresh(name: string): Promise { + const fp = join(TMP, name); + await writeFile(fp, EMPTY, 'utf-8'); + return fp; +} +async function readDoc(fp: string): Promise> { + return JSON.parse(await readFile(fp, 'utf-8')); +} +function getRoot(doc: Record): Record { + const pages = doc['pages'] as Array<{ children?: Record[] }> | undefined; + const top = doc['children'] as Record[] | undefined; + const root = (top ?? pages?.[0]?.children)?.[0]; + if (!root) throw new Error('no root'); + return root; +} +function findAllByRole(n: Record, role: string): Record[] { + const out: Record[] = []; + if (n.role === role) out.push(n); + const kids = (n.children ?? []) as Record[]; + for (const c of kids) out.push(...findAllByRole(c, role)); + return out; +} +function findByRole(n: Record, role: string): Record | undefined { + return findAllByRole(n, role)[0]; +} + +beforeEach(async () => { + await mkdir(TMP, { recursive: true }); +}); +afterEach(async () => { + for (const f of ['s.op']) { + try { + const fp = join(TMP, f); + invalidateCache(fp); + await unlink(fp); + } catch {} + } +}); + +describe('add_social_login_row_v0', () => { + it('registered; required=[providers]', () => { + expect(DESIGN_TOOL_NAMES.has('add_social_login_row_v0')).toBe(true); + const def = DESIGN_TOOL_DEFINITIONS.find((t) => t.name === 'add_social_login_row_v0'); + expect(def?.inputSchema.required).toEqual(['providers']); + }); + + it('default orientation=vertical → full-width 48px buttons with icon + "Continue with X" label', async () => { + const fp = await fresh('s.op'); + await handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'google' }, { name: 'apple' }, { name: 'microsoft' }], + }); + const root = getRoot(await readDoc(fp)); + expect(root.role).toBe('social-login-row'); + expect(root.layout).toBe('vertical'); + + const buttons = findAllByRole(root, 'social-login-button'); + expect(buttons).toHaveLength(3); + expect(buttons[0].width).toBe('fill_container'); + expect(buttons[0].height).toBe(48); + + const labels = findAllByRole(root, 'social-login-button-label'); + expect(labels.map((l) => l.content)).toEqual([ + 'Continue with Google', + 'Continue with Apple', + 'Continue with Microsoft', + ]); + }); + + it('horizontal orientation → 48×48 icon-only compact pills, no labels', async () => { + const fp = await fresh('s.op'); + await handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'google' }, { name: 'github' }], + orientation: 'horizontal', + }); + const root = getRoot(await readDoc(fp)); + expect(root.layout).toBe('horizontal'); + + const compactButtons = findAllByRole(root, 'social-login-button-compact'); + expect(compactButtons).toHaveLength(2); + expect(compactButtons[0].width).toBe(48); + expect(compactButtons[0].height).toBe(48); + + // No labels in horizontal variant + expect(findAllByRole(root, 'social-login-button-label')).toHaveLength(0); + // Full-size "button" role not used for compact + expect(findAllByRole(root, 'social-login-button')).toHaveLength(0); + }); + + it('known provider names map to lucide icons (google → chrome, github → github)', async () => { + const fp = await fresh('s.op'); + await handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'google' }, { name: 'github' }], + }); + const root = getRoot(await readDoc(fp)); + const icons = findAllByRole(root, 'social-login-button-icon'); + expect(icons[0].iconFontName).toBe('chrome'); + expect(icons[1].iconFontName).toBe('github'); + expect(icons[0].iconFontFamily).toBe('lucide'); + }); + + it('explicit icon overrides known-name mapping', async () => { + const fp = await fresh('s.op'); + await handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'google', icon: 'star' }], + }); + const root = getRoot(await readDoc(fp)); + expect(findByRole(root, 'social-login-button-icon')!.iconFontName).toBe('star'); + }); + + it('unknown provider falls back to log-in icon', async () => { + const fp = await fresh('s.op'); + await handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'Okta' }], + }); + const root = getRoot(await readDoc(fp)); + expect(findByRole(root, 'social-login-button-icon')!.iconFontName).toBe('log-in'); + }); + + it('clamps to 6 providers max', async () => { + const fp = await fresh('s.op'); + const many = Array.from({ length: 10 }, (_, i) => ({ name: `p${i}` })); + await handleAddSocialLoginRowV0({ filePath: fp, providers: many }); + const root = getRoot(await readDoc(fp)); + expect(findAllByRole(root, 'social-login-button')).toHaveLength(6); + }); + + it('empty providers array throws (not silent success)', async () => { + const fp = await fresh('s.op'); + const before = await readFile(fp, 'utf-8'); + await expect(handleAddSocialLoginRowV0({ filePath: fp, providers: [] })).rejects.toThrow( + /providers.*must not be empty/, + ); + expect(await readFile(fp, 'utf-8')).toBe(before); + }); + + it('width clamps (< 200 → 200)', async () => { + const fp = await fresh('s.op'); + await handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'google' }], + width: 100, + }); + const root = getRoot(await readDoc(fp)); + expect(root.width).toBe(200); + }); + + it('throws on bogus parent_id AND leaves file untouched', async () => { + const fp = await fresh('s.op'); + const before = await readFile(fp, 'utf-8'); + await expect( + handleAddSocialLoginRowV0({ + filePath: fp, + providers: [{ name: 'google' }], + parent_id: 'nope', + }), + ).rejects.toThrow(/parent_id.*not found/); + expect(await readFile(fp, 'utf-8')).toBe(before); + }); +}); diff --git a/packages/pen-mcp/src/__tests__/element-tools-contract.test.ts b/packages/pen-mcp/src/__tests__/element-tools-contract.test.ts index 2d89d2c46..b546616e1 100644 --- a/packages/pen-mcp/src/__tests__/element-tools-contract.test.ts +++ b/packages/pen-mcp/src/__tests__/element-tools-contract.test.ts @@ -77,6 +77,7 @@ const ELEMENT_TOOL_NAMES = [ 'add_attachment_row_v0', 'add_chat_bubble_v0', 'add_stat_card_v0', + 'add_social_login_row_v0', ]; describe('element tools — v0-MUST contract', () => { diff --git a/packages/pen-mcp/src/routes/element-tool-defs-ext-3.ts b/packages/pen-mcp/src/routes/element-tool-defs-ext-3.ts index 1fbae55fd..6b2d258ff 100644 --- a/packages/pen-mcp/src/routes/element-tool-defs-ext-3.ts +++ b/packages/pen-mcp/src/routes/element-tool-defs-ext-3.ts @@ -409,4 +409,48 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_3 = [ required: ['label', 'value'], }, }, + { + name: 'add_social_login_row_v0', + description: + 'Social-auth provider button row — the "Continue with Google / Apple / Microsoft" pattern ' + + 'on login / signup screens. Two orientations: "vertical" (default, full-width stacked buttons ' + + 'with icon + "Continue with {Name}" label) and "horizontal" (compact icon-only square pills ' + + 'side-by-side). Known provider names (google / apple / microsoft / github / facebook / twitter ' + + '/ x / linkedin / discord / slack / email / phone) auto-resolve to lucide icons; override via ' + + '`providers[i].icon`. Use for "social login", "Sign in with Google", "OAuth buttons", "SSO row", ' + + '"第三方登录", "社交登录". schemaVersion 1.0', + inputSchema: { + type: 'object' as const, + properties: { + schemaVersion: schemaVersionProp, + filePath: filePathProp, + providers: { + type: 'array', + description: + 'Provider list, 1-6 items. Each item: { name: string, icon?: string }. Known names auto-map icons.', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + icon: { type: 'string', description: 'Optional lucide icon override' }, + }, + required: ['name'], + }, + }, + orientation: { + type: 'string', + enum: ['vertical', 'horizontal'], + description: + '"vertical" (default) = full-width stacked buttons w/ label. "horizontal" = compact icon-only pills.', + }, + width: { + type: 'number', + description: 'Button width in px (default 320, min 200, vertical only)', + }, + parent_id: parentIdProp, + pageId: pageIdProp, + }, + required: ['providers'], + }, + }, ]; diff --git a/packages/pen-mcp/src/routes/element-tool-defs.ts b/packages/pen-mcp/src/routes/element-tool-defs.ts index 2487cf291..f817ffa0c 100644 --- a/packages/pen-mcp/src/routes/element-tool-defs.ts +++ b/packages/pen-mcp/src/routes/element-tool-defs.ts @@ -83,6 +83,7 @@ import { handleAddOtpInputV0 } from '../tools/add-otp-input-v0'; import { handleAddAttachmentRowV0 } from '../tools/add-attachment-row-v0'; import { handleAddChatBubbleV0 } from '../tools/add-chat-bubble-v0'; import { handleAddStatCardV0 } from '../tools/add-stat-card-v0'; +import { handleAddSocialLoginRowV0 } from '../tools/add-social-login-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'; @@ -251,6 +252,8 @@ async function dispatchElementToolCall(name: string, a: any): Promise { return JSON.stringify(await handleAddChatBubbleV0(a), null, 2); case 'add_stat_card_v0': return JSON.stringify(await handleAddStatCardV0(a), null, 2); + case 'add_social_login_row_v0': + return JSON.stringify(await handleAddSocialLoginRowV0(a), null, 2); default: return ''; } diff --git a/packages/pen-mcp/src/tools/add-social-login-row-v0.ts b/packages/pen-mcp/src/tools/add-social-login-row-v0.ts new file mode 100644 index 000000000..740ab747e --- /dev/null +++ b/packages/pen-mcp/src/tools/add-social-login-row-v0.ts @@ -0,0 +1,25 @@ +import { + assignIdsRecursively, + buildSocialLoginRow, + type SocialLoginRowParams, +} from '@zseven-w/pen-core'; +import type { handleBatchDesign } from './batch-design'; +import { ensureParentExists, insertElementTree } from './element-tool-helpers'; + +export interface AddSocialLoginRowV0Params extends SocialLoginRowParams { + parent_id?: string; + filePath?: string; + pageId?: string; +} + +export type { SocialLoginProvider as AddSocialLoginProvider } from '@zseven-w/pen-core'; + +/** Social-auth button row. Tree build delegated to `buildSocialLoginRow`. */ +export async function handleAddSocialLoginRowV0( + params: AddSocialLoginRowV0Params, +): Promise>> { + await ensureParentExists(params); + const r = buildSocialLoginRow(params); + assignIdsRecursively(r); + return insertElementTree({ binding: 'socialLogin', tree: r, ...params }); +}