feat(ai): add_social_login_row_v0 — social auth button row (69th tool)
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.
This commit is contained in:
parent
127eccdefb
commit
e0b926c256
|
|
@ -39,6 +39,7 @@ import {
|
|||
buildAttachmentRow,
|
||||
buildChatBubble,
|
||||
buildStatCard,
|
||||
buildSocialLoginRow,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -199,6 +200,8 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
|
|||
add_attachment_row_v0: (a) => buildAttachmentRow(a as Parameters<typeof buildAttachmentRow>[0]),
|
||||
add_chat_bubble_v0: (a) => buildChatBubble(a as Parameters<typeof buildChatBubble>[0]),
|
||||
add_stat_card_v0: (a) => buildStatCard(a as Parameters<typeof buildStatCard>[0]),
|
||||
add_social_login_row_v0: (a) =>
|
||||
buildSocialLoginRow(a as Parameters<typeof buildSocialLoginRow>[0]),
|
||||
};
|
||||
|
||||
interface ExecToolBody {
|
||||
|
|
|
|||
|
|
@ -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<typeof buildStatCard>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_social_login_row_v0',
|
||||
args: { providers: [{ name: 'google' }, { name: 'apple' }] },
|
||||
build: (a) => buildSocialLoginRow(a as unknown as Parameters<typeof buildSocialLoginRow>[0]),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<string, ElementShim> = {
|
|||
add_attachment_row_v0: wrap<AttachmentRowParams>(buildAttachmentRow),
|
||||
add_chat_bubble_v0: wrap<ChatBubbleParams>(buildChatBubble),
|
||||
add_stat_card_v0: wrap<StatCardParams>(buildStatCard),
|
||||
add_social_login_row_v0: wrap<SocialLoginRowParams>(buildSocialLoginRow),
|
||||
};
|
||||
|
||||
export function getElementShim(name: string): ElementShim | undefined {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
161
packages/pen-core/src/element-builders/social-login-row.ts
Normal file
161
packages/pen-core/src/element-builders/social-login-row.ts
Normal file
|
|
@ -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<string, string> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
175
packages/pen-mcp/src/__tests__/add-social-login-row-v0.test.ts
Normal file
175
packages/pen-mcp/src/__tests__/add-social-login-row-v0.test.ts
Normal file
|
|
@ -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<string> {
|
||||
const fp = join(TMP, name);
|
||||
await writeFile(fp, EMPTY, 'utf-8');
|
||||
return fp;
|
||||
}
|
||||
async function readDoc(fp: string): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(await readFile(fp, 'utf-8'));
|
||||
}
|
||||
function getRoot(doc: Record<string, unknown>): Record<string, unknown> {
|
||||
const pages = doc['pages'] as Array<{ children?: Record<string, unknown>[] }> | undefined;
|
||||
const top = doc['children'] as Record<string, unknown>[] | undefined;
|
||||
const root = (top ?? pages?.[0]?.children)?.[0];
|
||||
if (!root) throw new Error('no root');
|
||||
return root;
|
||||
}
|
||||
function findAllByRole(n: Record<string, unknown>, role: string): Record<string, unknown>[] {
|
||||
const out: Record<string, unknown>[] = [];
|
||||
if (n.role === role) out.push(n);
|
||||
const kids = (n.children ?? []) as Record<string, unknown>[];
|
||||
for (const c of kids) out.push(...findAllByRole(c, role));
|
||||
return out;
|
||||
}
|
||||
function findByRole(n: Record<string, unknown>, role: string): Record<string, unknown> | 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
|||
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 '';
|
||||
}
|
||||
|
|
|
|||
25
packages/pen-mcp/src/tools/add-social-login-row-v0.ts
Normal file
25
packages/pen-mcp/src/tools/add-social-login-row-v0.ts
Normal file
|
|
@ -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<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const r = buildSocialLoginRow(params);
|
||||
assignIdsRecursively(r);
|
||||
return insertElementTree({ binding: 'socialLogin', tree: r, ...params });
|
||||
}
|
||||
Loading…
Reference in a new issue