mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
feat: add WeChat design system, login-flow skill, and fix API mode tool_calls bug (#1083)
* feat: add WeChat design system, login-flow skill, and fix API mode tool_calls bug - Add WeChat design system (design-systems/wechat/) with full brand spec including color palette, typography, and component rules for chat UI - Add login-flow skill (skills/login-flow/) for mobile authentication flows with P0 checklist, example HTML, and i18n registration across 3 locales - Fix DeepSeek V4 bug: API/BYOK mode (streamFormat=plain) models now receive a directive to emit only <artifact> HTML blocks and suppress tool_calls, since plain adapters proxy to external providers that cannot execute tools * fix: restore full server.ts and WeChat DESIGN.md from ad46d8cd commit Restore files that were corrupted in PR #1083 head branch. The WeChat DESIGN.md was reduced to a single line (filename only) and server.ts was reduced to ~1 line. Both are restored to their original ad46d8cd state with full content. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: restore full server.ts and WeChat DESIGN.md from ad46d8cd Restore files corrupted in PR #1083: - apps/daemon/src/server.ts: restored 7106-line file - design-systems/wechat/DESIGN.md: restored 301-line WeChat design spec - skills/login-flow/SKILL.md: restored from local working state - skills/login-flow/example.html: restored 351-line example HTML * fix: only suppress tool_calls when streamFormat='plain' explicitly, remove nonexistent assets/template.html 1. streamFormat check now requires explicit 'plain' value instead of defaulting to 'plain' when undefined. This prevents normal tool-using chat runs from incorrectly inheriting the API/BYOK tool_calls suppression rule. 2. login-flow SKILL.md: removed reference to assets/template.html since that file does not exist in the skill bundle and derivePreflight() would inject a hard instruction to read it before any other tool, causing pre-flight to fail. * fix: thread streamFormat to composeSystemPrompt in server.ts call Previously the composeSystemPrompt call at line ~4940 omitted streamFormat, causing the composer to default to 'plain' and suppress tool_calls even for tool-using chat runs. Now streamFormat is passed through from the adapter definition so the API mode rule only fires when streamFormat='plain' is explicitly set. * fix: WeChat category metadata, font-family, and login-flow example interactivity WeChat DESIGN.md: - Add Category: Social & Messaging metadata so it appears correctly in picker - Fix font-family declaration: remove invalid -webkit-font-family prefix, use standard font-family so downstream CSS generation works correctly skills/login-flow/example.html: - Add password toggle click handler so show/hide actually works - Change Apple icon fill from hardcoded #fff to currentColor so it is visible on light backgrounds * fix: mirror streamFormat suppression in contracts composer and add WeChat i18n 1. packages/contracts/src/prompts/system.ts: Add streamFormat parameter to ComposeInput and ComposeInput interface, mirroring the same suppression rule from daemon prompts/system.ts. When streamFormat='plain' is passed, a directive is appended telling models not to emit tool_calls and to only output <artifact> HTML blocks. 2. apps/web/src/i18n/content.{ts,fr,ru}.ts: Add WeChat design system entries: - Add 'wechat' to DE/FR/RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK arrays - Add 'wechat' summary to DE/FR/RU_DESIGN_SYSTEM_SUMMARIES - Add 'Social & Messaging' category to DE/FR/RU_DESIGN_SYSTEM_CATEGORIES (matching the Category: Social & Messaging metadata in WeChat DESIGN.md) * fix: thread streamFormat='plain' into web composeSystemPrompt for api mode * test: focus localized content coverage on missing resources --------- Co-authored-by: Open Design Contributor <z@open-design.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: mrcfps <mrc@powerformer.com>
This commit is contained in:
parent
bfedbeca0f
commit
84f768d4a2
12 changed files with 799 additions and 4 deletions
|
|
@ -81,6 +81,7 @@ export const BASE_SYSTEM_PROMPT = OFFICIAL_DESIGNER_PROMPT;
|
|||
export interface ComposeInput {
|
||||
agentId?: string | null | undefined;
|
||||
includeCodexImagegenOverride?: boolean | undefined;
|
||||
streamFormat?: string | undefined;
|
||||
skillBody?: string | undefined;
|
||||
skillName?: string | undefined;
|
||||
skillMode?:
|
||||
|
|
@ -148,6 +149,7 @@ export function composeSystemPrompt({
|
|||
critiqueBrand,
|
||||
critiqueSkill,
|
||||
connectedExternalMcp,
|
||||
streamFormat,
|
||||
}: ComposeInput): string {
|
||||
// Discovery + philosophy goes FIRST so its hard rules ("emit a form on
|
||||
// turn 1", "branch on brand on turn 2", "TodoWrite on turn 3", run
|
||||
|
|
@ -247,6 +249,16 @@ export function composeSystemPrompt({
|
|||
const mcpDirective = renderConnectedExternalMcpDirective(connectedExternalMcp);
|
||||
if (mcpDirective) parts.push(mcpDirective);
|
||||
|
||||
// Suppress tool_calls in API/BYOK mode (streamFormat === 'plain').
|
||||
// Only fires when the caller explicitly passes streamFormat='plain';
|
||||
// does NOT fire when streamFormat is omitted, so non-plain (tool-using)
|
||||
// adapters are unaffected and normal chat runs can still use tools.
|
||||
if (streamFormat === 'plain') {
|
||||
parts.push(
|
||||
'\n\n## API mode rule\n\nDo not emit tool_calls. Output only <artifact> HTML blocks. Any tool description in your internal reasoning must not appear in the response.',
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5921,6 +5921,7 @@ export async function startServer({
|
|||
const prompt = composeSystemPrompt({
|
||||
agentId,
|
||||
includeCodexImagegenOverride: false,
|
||||
streamFormat,
|
||||
skillBody,
|
||||
skillName,
|
||||
skillMode,
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ export function ProjectView({
|
|||
designSystemTitle,
|
||||
metadata: project.metadata,
|
||||
template,
|
||||
streamFormat: config.mode === 'api' ? 'plain' : undefined,
|
||||
});
|
||||
}, [
|
||||
project.skillId,
|
||||
|
|
@ -696,6 +697,7 @@ export function ProjectView({
|
|||
project.metadata,
|
||||
skills,
|
||||
designSystems,
|
||||
config.mode,
|
||||
]);
|
||||
|
||||
const persistMessage = useCallback(
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ export const FR_DESIGN_SYSTEM_SUMMARIES: Record<string, string> = {
|
|||
wise: 'Transfert d’argent. Accent vert lumineux, amical et clair.',
|
||||
'x-ai': 'Lab IA d’Elon Musk. Look monochrome strict, minimalisme futuriste.',
|
||||
xiaohongshu: 'Plateforme social lifestyle UGC. Rouge de marque singulier, radius généreux, content-first.',
|
||||
wechat: 'Mini programmes WeChat. Vert frais (#07C160), PingFang SC, UI à bulle de chat, barre d’onglets.',
|
||||
zapier: 'Plateforme d’automatisation. Orange chaud, amical, porté par l’illustration.',
|
||||
};
|
||||
|
||||
|
|
@ -306,6 +307,7 @@ export const FR_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
|
|||
'Fintech & Crypto': 'Fintech & crypto',
|
||||
'E-Commerce & Retail': 'E-commerce & retail',
|
||||
'Media & Consumer': 'Médias & grand public',
|
||||
'Social & Messaging': 'Réseaux sociaux & messageries',
|
||||
Automotive: 'Automobile',
|
||||
'Editorial & Print': 'Éditorial & print',
|
||||
'Editorial · Studio': 'Éditorial · Studio',
|
||||
|
|
@ -360,6 +362,7 @@ export const FR_SKILL_IDS_WITH_EN_FALLBACK = [
|
|||
'ib-pitch-book',
|
||||
'last30days',
|
||||
'live-dashboard',
|
||||
'login-flow',
|
||||
'orbit-general',
|
||||
'orbit-github',
|
||||
'orbit-gmail',
|
||||
|
|
@ -458,6 +461,7 @@ export const FR_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
|||
'urdu',
|
||||
'vibrant',
|
||||
'vintage',
|
||||
'wechat',
|
||||
'webex',
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ export const RU_DESIGN_SYSTEM_SUMMARIES: Record<string, string> = {
|
|||
wise: 'Денежные переводы. Яркий зеленый акцент, дружелюбно и ясно.',
|
||||
'x-ai': 'AI-лаборатория Илона Маска. Строгий монохром, футуристический минимализм.',
|
||||
xiaohongshu: 'Lifestyle UGC-соцсеть. Единый фирменный красный, щедрый радиус, content-first.',
|
||||
wechat: 'Мини-программы WeChat. Свежий зелёный (#07C160), PingFang SC, UI с чат-пузырями, панель вкладок.',
|
||||
zapier: 'Платформа автоматизации. Теплый оранжевый, дружелюбная иллюстративная подача.',
|
||||
};
|
||||
|
||||
|
|
@ -306,6 +307,7 @@ export const RU_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
|
|||
'Fintech & Crypto': 'Финтех и крипто',
|
||||
'E-Commerce & Retail': 'Электронная коммерция и ритейл',
|
||||
'Media & Consumer': 'Медиа и потребительские',
|
||||
'Social & Messaging': 'Социальные сети и мессенджеры',
|
||||
Automotive: 'Автомобили',
|
||||
'Editorial & Print': 'Редакционные и печатные',
|
||||
'Editorial · Studio': 'Редакционная студия',
|
||||
|
|
@ -360,6 +362,7 @@ export const RU_SKILL_IDS_WITH_EN_FALLBACK = [
|
|||
'ib-pitch-book',
|
||||
'last30days',
|
||||
'live-dashboard',
|
||||
'login-flow',
|
||||
'orbit-general',
|
||||
'orbit-github',
|
||||
'orbit-gmail',
|
||||
|
|
@ -458,6 +461,7 @@ export const RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
|||
'urdu',
|
||||
'vibrant',
|
||||
'vintage',
|
||||
'wechat',
|
||||
'webex',
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ const DE_DESIGN_SYSTEM_SUMMARIES: Record<string, string> = {
|
|||
wise: 'Geldtransfer. Leuchtend grüner Akzent, freundlich und klar.',
|
||||
'x-ai': 'Elon Musks AI-Lab. Strenger Monochrom-Look, futuristischer Minimalismus.',
|
||||
xiaohongshu: 'Lifestyle-UGC-Social-Plattform. Singuläres Brand-Rot, großzügiger Radius, content-first.',
|
||||
wechat: 'WeChat Mini Programs. Frisches Grün (#07C160), PingFang SC, Chat-Bubble-UI, Tab-Leiste.',
|
||||
zapier: 'Automatisierungsplattform. Warmes Orange, freundlich illustrationsgetrieben.',
|
||||
};
|
||||
|
||||
|
|
@ -353,6 +354,7 @@ const DE_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
|
|||
'Fintech & Crypto': 'Fintech & Krypto',
|
||||
'E-Commerce & Retail': 'E-Commerce & Handel',
|
||||
'Media & Consumer': 'Medien & Consumer',
|
||||
'Social & Messaging': 'Social & Messaging',
|
||||
Automotive: 'Automotive',
|
||||
'Editorial & Print': 'Editorial & Print',
|
||||
'Editorial · Studio': 'Editorial · Studio',
|
||||
|
|
@ -407,6 +409,7 @@ const DE_SKILL_IDS_WITH_EN_FALLBACK = [
|
|||
'ib-pitch-book',
|
||||
'last30days',
|
||||
'live-dashboard',
|
||||
'login-flow',
|
||||
'orbit-general',
|
||||
'orbit-github',
|
||||
'orbit-gmail',
|
||||
|
|
@ -508,6 +511,7 @@ const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
|||
'vibrant',
|
||||
'vintage',
|
||||
'webex',
|
||||
'wechat',
|
||||
] as const;
|
||||
|
||||
const DE_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
||||
|
|
|
|||
302
design-systems/wechat/DESIGN.md
Normal file
302
design-systems/wechat/DESIGN.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# WeChat Design System
|
||||
|
||||
> Category: Social & Messaging
|
||||
> Brand visual language for WeChat Mini Programs, official accounts, and open ecosystem extensions.
|
||||
|
||||
## Brand Identity
|
||||
|
||||
WeChat's identity is built on simplicity, cleanness, and trust — reflecting its role as a super-app that connects people, services, and businesses.
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Brand Colors
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|---|----|----|
|
||||
| `--wechat-green` | `#07C160` | Primary brand, CTA buttons, active states |
|
||||
| `--wechat-green-light` | `#10B160` | Hover state for primary actions |
|
||||
| `--wechat-green-dark` | `#059050` | Pressed/active state |
|
||||
|
||||
### Chat Bubble Colors
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|---|----|----|
|
||||
| `--wechat-bubble-self` | `#95EC69` | Outgoing message bubbles |
|
||||
| `--wechat-bubble-other` | `#FFFFFF` | Incoming message bubbles |
|
||||
| `--wechat-bubble-text` | `#1A1A1A` | Primary text in bubbles |
|
||||
|
||||
### UI Neutrals
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|---|----|----|
|
||||
| `--wechat-bg` | `#EDEDED` | Page/app background |
|
||||
| `--wechat-surface` | `#F7F7F7` | Card, modal surfaces |
|
||||
| `--wechat-border` | `#E0E0E0` | Dividers, borders |
|
||||
| `--wechat-ink` | `#1A1A1A` | Primary text |
|
||||
| `--wechat-muted` | `#888888` | Secondary text, timestamps |
|
||||
|
||||
### Functional Colors
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|---|----|----|
|
||||
| `--wechat-red` | `#FA5151` | Errors, destructive actions |
|
||||
| `--wechat-orange` | `#FAB702` | Warnings |
|
||||
| `--wechat-blue` | `#576B95` | Links, info states |
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Stack
|
||||
|
||||
```
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
```
|
||||
|
||||
### Type Scale
|
||||
|
||||
| Role | Size | Weight | Line Height |
|
||||
|---|---|---|---|
|
||||
| Page Title | 18px | 600 | 1.3 |
|
||||
| Section Header | 16px | 600 | 1.4 |
|
||||
| Body Text | 15px | 400 | 1.6 |
|
||||
| Secondary Text | 13px | 400 | 1.5 |
|
||||
| Caption/Timestamp | 11px | 400 | 1.4 |
|
||||
| Button Label | 16px | 500 | 1.0 |
|
||||
|
||||
---
|
||||
|
||||
## Spacing System
|
||||
|
||||
4px base unit.
|
||||
|
||||
| Token | Value |
|
||||
|---|-----|
|
||||
| `--space-xs` | 4px |
|
||||
| `--space-sm` | 8px |
|
||||
| `--space-md` | 12px |
|
||||
| `--space-lg` | 16px |
|
||||
| `--space-xl` | 24px |
|
||||
| `--space-2xl` | 32px |
|
||||
|
||||
### Border Radius
|
||||
|
||||
| Token | Value |
|
||||
|---|-----|
|
||||
| `--radius-sm` | 4px |
|
||||
| `--radius-md` | 8px |
|
||||
| `--radius-lg` | 16px |
|
||||
| `--radius-bubble` | 16px (with directional corner clip) |
|
||||
| `--radius-full` | 9999px (avatars, pills) |
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Chat Bubble
|
||||
|
||||
```css
|
||||
.wechat-bubble {
|
||||
max-width: 70%;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-bubble);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wechat-bubble.self {
|
||||
background: var(--wechat-bubble-self);
|
||||
color: var(--wechat-bubble-text);
|
||||
border-top-right-radius: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wechat-bubble.other {
|
||||
background: var(--wechat-bubble-other);
|
||||
color: var(--wechat-bubble-text);
|
||||
border-top-left-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
```
|
||||
|
||||
### Primary Button (Send / Confirm)
|
||||
|
||||
```css
|
||||
.btn-wechat-primary {
|
||||
background: var(--wechat-green);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btn-wechat-primary:hover {
|
||||
background: var(--wechat-green-light);
|
||||
}
|
||||
|
||||
.btn-wechat-primary:active {
|
||||
background: var(--wechat-green-dark);
|
||||
}
|
||||
```
|
||||
|
||||
### Tab Bar
|
||||
|
||||
```css
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: var(--wechat-surface);
|
||||
border-top: 1px solid var(--wechat-border);
|
||||
padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.tab-bar-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--wechat-muted);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.tab-bar-item.active {
|
||||
color: var(--wechat-green);
|
||||
}
|
||||
|
||||
.tab-bar-item svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
```
|
||||
|
||||
### Message Input Bar
|
||||
|
||||
```css
|
||||
.input-bar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: var(--wechat-surface);
|
||||
border-top: 1px solid var(--wechat-border);
|
||||
}
|
||||
|
||||
.input-bar textarea {
|
||||
flex: 1;
|
||||
min-height: 36px;
|
||||
max-height: 100px;
|
||||
padding: 8px 12px;
|
||||
background: var(--wechat-bg);
|
||||
border: 1px solid var(--wechat-border);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-bar textarea:focus {
|
||||
border-color: var(--wechat-green);
|
||||
}
|
||||
```
|
||||
|
||||
### Avatar
|
||||
|
||||
```css
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
object-fit: cover;
|
||||
background: var(--wechat-border);
|
||||
}
|
||||
|
||||
.avatar.sm { width: 32px; height: 32px; }
|
||||
.avatar.lg { width: 56px; height: 56px; }
|
||||
```
|
||||
|
||||
### Timestamp Badge
|
||||
|
||||
```css
|
||||
.timestamp {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
color: var(--wechat-muted);
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Motion & Animation
|
||||
|
||||
| Token | Value |
|
||||
|---|-----|
|
||||
| `--duration-instant` | 100ms |
|
||||
| `--duration-fast` | 200ms |
|
||||
| `--duration-normal` | 300ms |
|
||||
| `--ease-default` | cubic-bezier(0.25, 0.1, 0.25, 1) |
|
||||
|
||||
Chat message entry: fade-in + slight slide up, 200ms.
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode
|
||||
|
||||
| Token | Light | Dark |
|
||||
|---|---|---|
|
||||
| `--wechat-bg` | `#EDEDED` | `#1A1A1A` |
|
||||
| `--wechat-surface` | `#F7F7F7` | `#2C2C2C` |
|
||||
| `--wechat-ink` | `#1A1A1A` | `#F7F7F7` |
|
||||
| `--wechat-bubble-self` | `#95EC69` | `#4CAF50` |
|
||||
| `--wechat-bubble-other` | `#FFFFFF` | `#2C2C2C` |
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```css
|
||||
:root {
|
||||
--wechat-green: #07C160;
|
||||
--wechat-green-light: #10B160;
|
||||
--wechat-green-dark: #059050;
|
||||
--wechat-bubble-self: #95EC69;
|
||||
--wechat-bubble-other: #FFFFFF;
|
||||
--wechat-bubble-text: #1A1A1A;
|
||||
--wechat-bg: #EDEDED;
|
||||
--wechat-surface: #F7F7F7;
|
||||
--wechat-border: #E0E0E0;
|
||||
--wechat-ink: #1A1A1A;
|
||||
--wechat-muted: #888888;
|
||||
--wechat-red: #FA5151;
|
||||
--wechat-orange: #FAB702;
|
||||
--wechat-blue: #576B95;
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 12px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 24px;
|
||||
--space-2xl: 32px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 16px;
|
||||
--radius-bubble: 16px;
|
||||
--radius-full: 9999px;
|
||||
--duration-instant: 100ms;
|
||||
--duration-fast: 200ms;
|
||||
--duration-normal: 300ms;
|
||||
--ease-default: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
```
|
||||
|
|
@ -40,6 +40,25 @@ function sorted(values: Iterable<string>): string[] {
|
|||
return [...values].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function uniqueSorted(values: Iterable<string>): string[] {
|
||||
return sorted(new Set(values));
|
||||
}
|
||||
|
||||
function findMissingIds(localizedIds: Iterable<string>, discoveredIds: Iterable<string>): string[] {
|
||||
const localized = new Set(localizedIds);
|
||||
const discovered = new Set(discoveredIds);
|
||||
return sorted([...discovered].filter((id) => !localized.has(id)));
|
||||
}
|
||||
|
||||
function expectExactResourceCoverage(
|
||||
label: string,
|
||||
localizedIds: Iterable<string>,
|
||||
discoveredIds: Iterable<string>,
|
||||
): void {
|
||||
const missing = findMissingIds(localizedIds, discoveredIds);
|
||||
expect(missing, `${label} should cover every discovered resource`).toEqual([]);
|
||||
}
|
||||
|
||||
async function entriesWithFile(root: string, fileName: string): Promise<string[]> {
|
||||
const entries = await readdir(root, { withFileTypes: true });
|
||||
const ids: string[] = [];
|
||||
|
|
@ -141,12 +160,16 @@ describe('localized display content coverage', () => {
|
|||
readPromptTemplateSummaries(),
|
||||
]);
|
||||
|
||||
expect(sorted(ids.skills), 'skills display copy').toEqual(skillIds);
|
||||
expect(sorted(ids.designSystems), 'design-system summaries').toEqual(
|
||||
expectExactResourceCoverage('skills display copy', ids.skills, skillIds);
|
||||
expectExactResourceCoverage(
|
||||
'design-system summaries',
|
||||
ids.designSystems,
|
||||
designSystemIds,
|
||||
);
|
||||
expect(sorted(ids.promptTemplates), 'prompt-template metadata').toEqual(
|
||||
sorted(promptTemplateSummaries.map((template) => template.id)),
|
||||
expectExactResourceCoverage(
|
||||
'prompt-template metadata',
|
||||
ids.promptTemplates,
|
||||
uniqueSorted(promptTemplateSummaries.map((template) => template.id)),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ export interface ComposeInput {
|
|||
// Snapshot of HTML files that the agent should treat as a starting
|
||||
// reference rather than a fixed deliverable.
|
||||
template?: ProjectTemplate | undefined;
|
||||
// When set to 'plain', suppresses tool_calls so API/BYOK-mode models
|
||||
// only emit <artifact> blocks (they cannot execute tools).
|
||||
streamFormat?: string | undefined;
|
||||
}
|
||||
|
||||
export function composeSystemPrompt({
|
||||
|
|
@ -70,6 +73,7 @@ export function composeSystemPrompt({
|
|||
designSystemTitle,
|
||||
metadata,
|
||||
template,
|
||||
streamFormat,
|
||||
}: ComposeInput): string {
|
||||
// Discovery + philosophy goes FIRST so its hard rules ("emit a form on
|
||||
// turn 1", "branch on brand on turn 2", "TodoWrite on turn 3", run
|
||||
|
|
@ -131,6 +135,16 @@ export function composeSystemPrompt({
|
|||
parts.push(MEDIA_GENERATION_CONTRACT);
|
||||
}
|
||||
|
||||
// Suppress tool_calls in API/BYOK mode (streamFormat === 'plain').
|
||||
// Only fires when the caller explicitly passes streamFormat='plain';
|
||||
// does NOT fire when streamFormat is omitted, so non-plain (tool-using)
|
||||
// adapters are unaffected and normal chat runs can still use tools.
|
||||
if (streamFormat === 'plain') {
|
||||
parts.push(
|
||||
'\n\n## API mode rule\n\nDo not emit tool_calls. Output only <artifact> HTML blocks. Any tool description in your internal reasoning must not appear in the response.',
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
|
|
|
|||
48
skills/login-flow/SKILL.md
Normal file
48
skills/login-flow/SKILL.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
name: login-flow
|
||||
description: Mobile login and authentication flow screens
|
||||
od:
|
||||
mode: prototype
|
||||
platform: mobile
|
||||
triggers:
|
||||
- login
|
||||
- sign in
|
||||
- 注册登录
|
||||
- 登录注册
|
||||
- 手机号登录
|
||||
- 验证码登录
|
||||
- 密码登录
|
||||
---
|
||||
|
||||
# Login Flow Skill
|
||||
|
||||
A skill for generating mobile-first login and authentication screens. Use this when the user wants a sign-in experience for a mobile app, including phone + SMS verification, password-based login, and social SSO options.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read reference files first** (see below)
|
||||
2. **Clarify auth method**: phone/SMS, password, or social SSO
|
||||
3. **Checklist gate** — verify P0 items before emitting `<artifact>`
|
||||
4. **Build the HTML prototype** with proper states (default, loading, error)
|
||||
5. **Wrap in `<artifact>` tag** referencing the output file
|
||||
|
||||
## Side Files
|
||||
|
||||
- `references/checklist.md` — P0/P1 acceptance criteria
|
||||
|
||||
## Output
|
||||
|
||||
A single standalone HTML file implementing the login screen with:
|
||||
- Labels above inputs (never placeholder-only)
|
||||
- Password field with show/hide toggle
|
||||
- Social SSO buttons with SVG icons
|
||||
- Error states below fields
|
||||
- Loading spinner in primary CTA
|
||||
- Touch targets minimum 44px
|
||||
|
||||
## Mobile-First Constraints
|
||||
|
||||
- Viewport: 375px wide (iPhone standard)
|
||||
- No horizontal scroll
|
||||
- Safe area insets for notched devices
|
||||
- Input keyboards: `tel` for phone, `password` for password fields
|
||||
362
skills/login-flow/example.html
Normal file
362
skills/login-flow/example.html
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Nova · Login</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #F7F8FA;
|
||||
--surface: #FFFFFF;
|
||||
--ink: #1A1A2E;
|
||||
--muted: #6B7280;
|
||||
--border: #E5E7EB;
|
||||
--brand: #4F46E5;
|
||||
--brand-hover: #4338CA;
|
||||
--error: #DC2626;
|
||||
--success: #059669;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.phone {
|
||||
width: 375px;
|
||||
background: var(--surface);
|
||||
border-radius: 40px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.statusbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
.statusbar .right {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
background: var(--ink);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
}
|
||||
.status-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: 2px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 32px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.logo-area {
|
||||
text-align: center;
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
.logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: var(--brand);
|
||||
border-radius: 14px;
|
||||
margin: 0 auto 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.logo svg { width: 32px; height: 32px; fill: white; }
|
||||
.app-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.header { text-align: center; }
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.field label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.input-wrap input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.input-wrap input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brand);
|
||||
}
|
||||
.input-wrap input::placeholder { color: var(--muted); }
|
||||
.input-wrap .toggle {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.input-wrap .toggle:hover { color: var(--ink); }
|
||||
|
||||
.phone-input { display: flex; gap: 8px; }
|
||||
.phone-input .country {
|
||||
width: 90px;
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.phone-input .country .flag { font-size: 18px; }
|
||||
.phone-input input { flex: 1; }
|
||||
|
||||
.row { display: flex; justify-content: flex-end; }
|
||||
.forgot {
|
||||
font-size: 13px;
|
||||
color: var(--brand);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.forgot:hover { text-decoration: underline; }
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
background: var(--brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.btn-primary:hover { background: var(--brand-hover); }
|
||||
.btn-primary:disabled {
|
||||
background: #A5B4FC;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-primary .spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.social { display: flex; gap: 12px; }
|
||||
.social-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.social-btn:hover { background: var(--bg); }
|
||||
.social-btn svg { width: 20px; height: 20px; }
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.footer a {
|
||||
color: var(--brand);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.footer a:hover { text-decoration: underline; }
|
||||
|
||||
.error-msg {
|
||||
font-size: 12px;
|
||||
color: var(--error);
|
||||
margin-top: 4px;
|
||||
display: none;
|
||||
}
|
||||
.field.has-error input { border-color: var(--error); }
|
||||
.field.has-error .error-msg { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="phone">
|
||||
<div class="statusbar">
|
||||
<span>9:41</span>
|
||||
<div class="right">
|
||||
<span>5G</span>
|
||||
<span class="status-icon"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="logo-area">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
||||
</div>
|
||||
<div class="app-name">Nova</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Welcome back</h1>
|
||||
<p>Sign in to continue to your account</p>
|
||||
</div>
|
||||
|
||||
<form class="form" onsubmit="return false;">
|
||||
<div class="field">
|
||||
<label for="phone">Phone number</label>
|
||||
<div class="phone-input">
|
||||
<div class="country">
|
||||
<span class="flag">+86</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M3 5l3 3 3-3"/></svg>
|
||||
</div>
|
||||
<input type="tel" id="phone" placeholder="123 4567 8900" />
|
||||
</div>
|
||||
<span class="error-msg">Please enter a valid phone number</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<div class="input-wrap">
|
||||
<input type="password" id="password" placeholder="Enter your password" />
|
||||
<button type="button" class="toggle" aria-label="Show password">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="error-msg">Password is required</span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a href="#" class="forgot">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
<span class="btn-text">Sign in</span>
|
||||
</button>
|
||||
|
||||
<div class="divider">or continue with</div>
|
||||
|
||||
<div class="social">
|
||||
<button type="button" class="social-btn">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.025-1.632 2.815-.181.79-.065 1.62.116 2.43.143.4.367.78.666 1.106.315.345.69.647 1.114.918.927.568 2.013.858 3.183.955 1.18.098 2.063-.018 2.94-.123 1.05-.126 1.92-.78 2.528-1.54 1.12-1.38 1.62-3.24 1.337-4.6z"/></svg>
|
||||
Apple
|
||||
</button>
|
||||
<button type="button" class="social-btn">
|
||||
<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
|
||||
Google
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="footer">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
const toggleBtn = document.querySelector('.toggle');
|
||||
const passwordInput = document.getElementById('password');
|
||||
if (toggleBtn && passwordInput) {
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
const isPassword = passwordInput.type === 'password';
|
||||
passwordInput.type = isPassword ? 'text' : 'password';
|
||||
toggleBtn.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
19
skills/login-flow/references/checklist.md
Normal file
19
skills/login-flow/references/checklist.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Login Flow Checklist
|
||||
|
||||
P0 (must pass before emitting artifact):
|
||||
|
||||
- [ ] Labels above inputs, never placeholder-only
|
||||
- [ ] Password field has show/hide toggle
|
||||
- [ ] Social buttons use SVG icons, not emoji
|
||||
- [ ] Touch targets are minimum 44px
|
||||
- [ ] Error states show red text below the field
|
||||
- [ ] Primary CTA button has hover/active states
|
||||
- [ ] No placeholder text like "example@email.com" without indication
|
||||
|
||||
P1 (should pass):
|
||||
|
||||
- [ ] Loading spinner in button during submission
|
||||
- [ ] "Forgot password" link present
|
||||
- [ ] "Don't have an account" link present
|
||||
- [ ] Country picker for phone input (if phone auth)
|
||||
- [ ] Input focus state uses brand color
|
||||
Loading…
Reference in a new issue