open-design/skills/login-flow/example.html
code-Y 84f768d4a2
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>
2026-05-10 20:38:33 +08:00

362 lines
9.9 KiB
HTML

<!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>