mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Improve responsive preview and design handoff outputs (#1224)
* feat: improve responsive design handoff * feat: refine cross-platform design outputs Changelog:\n- Add auto-fit responsive preview behavior for tablet/mobile frames.\n- Add landing page and OS widgets metadata options with project header chips.\n- Strengthen prompt contracts for modern breakpoints, app-specific modules, CJX-ready UX, and final product surfaces.\n- Require cross-platform outputs to use separate platform files instead of tabbed demo selectors.\n- Add DESIGN-MANIFEST.json plus richer handoff guidance to daemon/client exports.\n- Update archive/export tests for manifest and responsive viewport matrix. * feat: enforce screen-file design outputs Changelog:\n- Enforce screen-file-first generation for landing pages, app screens, platform surfaces, and OS widgets.\n- Update design handoff and manifest exports so coding tools map each screen file to separate routes/surfaces.\n- Strengthen minimal-brief visual guidance to avoid monochrome or unstyled design outputs. * fix: address responsive handoff review feedback * fix: address handoff review blockers * fix: preserve proxy auth and normalized export entry * fix: narrow frame wrapper filter to directory paths only * fix: make artifact save failure banner generic --------- Co-authored-by: Huy Hoàng <macos@MacBook-Pro-Hoang.local>
This commit is contained in:
parent
93865f71e7
commit
140a4e1ff6
45 changed files with 2068 additions and 176 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Responsive design handoff improvements: tablet/mobile preview auto-fit, 2025–2026 responsive viewport matrix, landing page and OS widgets metadata chips, stricter cross-platform file output contracts, and DESIGN-HANDOFF/DESIGN-MANIFEST exports for coding-tool implementation.
|
||||
- Screen-file-first design handoff policy: landing pages, app/product screens, platform surfaces, and OS widget surfaces are exported as distinct HTML files with matching handoff/manifest guidance instead of being merged into one long artifact.
|
||||
|
||||
## [0.6.0] - 2026-05-09
|
||||
|
||||
A connectivity-and-iteration release: Open Design becomes a fully bidirectional MCP citizen (external MCP client with 39 templates), ships **Cloudflare Pages deployment** for generated artifacts (with custom domains), advances Critique Theater to **Phase 6** (interrupt + project-keyed run registry), and lands a redesigned top bar, draggable file tabs, batch delete, **vector PDF export**, **agent-callable research/search**, and **Orbit activity summaries**. Hyperframes learns the HTML-in-Canvas API. New BYOK provider (Ollama Cloud), new agent capabilities (Gemini 3 preview + GPT-5.1 codex picker + DeepSeek v4), new design systems (BMW M, Slack, Cisco, Webex, Mission Control, Urdu Modern), eight new skill bundles, and Turkish + Thai locales. 136 merged PRs since 0.5.0.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
// Daemon-backed app preferences (onboarding state, agent/skill/DS selection).
|
||||
//
|
||||
// The web frontend pushes non-sensitive preferences here via PUT
|
||||
// /api/app-config; the daemon persists them to <dataDir>/app-config.json
|
||||
// (where dataDir defaults to <projectRoot>/.od but follows OD_DATA_DIR when
|
||||
// set, keeping test and multi-namespace runs isolated).
|
||||
// This survives browser storage resets and origin changes so onboarding
|
||||
// and agent selection don't reappear unexpectedly.
|
||||
// The web frontend pushes preferences here via PUT /api/app-config; the
|
||||
// daemon persists them to <dataDir>/app-config.json (where dataDir defaults
|
||||
// to <projectRoot>/.od but follows OD_DATA_DIR when set, keeping test and
|
||||
// multi-namespace runs isolated). This survives browser storage resets and
|
||||
// origin changes so onboarding and agent selection don't reappear unexpectedly.
|
||||
//
|
||||
// `agentCliEnv` is intentionally limited by allowlist below. It may include
|
||||
// proxy/auth overrides for local CLIs (for example ANTHROPIC_BASE_URL +
|
||||
// ANTHROPIC_API_KEY for Claude Code, or OPENAI_BASE_URL + OPENAI_API_KEY for
|
||||
// Codex). Those values are local-only and should not be logged or returned
|
||||
// outside this machine.
|
||||
|
||||
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
|
@ -85,8 +90,8 @@ function validateTelemetry(raw: unknown): TelemetryPrefs | undefined {
|
|||
}
|
||||
|
||||
const AGENT_CLI_ENV_KEYS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
||||
['claude', new Set(['CLAUDE_CONFIG_DIR', 'CLAUDE_BIN'])],
|
||||
['codex', new Set(['CODEX_HOME', 'CODEX_BIN'])],
|
||||
['claude', new Set(['CLAUDE_CONFIG_DIR', 'CLAUDE_BIN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_API_KEY'])],
|
||||
['codex', new Set(['CODEX_HOME', 'CODEX_BIN', 'OPENAI_BASE_URL', 'OPENAI_API_KEY'])],
|
||||
['copilot', new Set(['COPILOT_BIN'])],
|
||||
['cursor-agent', new Set(['CURSOR_AGENT_BIN'])],
|
||||
['deepseek', new Set(['DEEPSEEK_BIN'])],
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import {
|
|||
|
||||
const FORBIDDEN_SEGMENT = /^$|^\.\.?$/;
|
||||
const RESERVED_PROJECT_FILE_SEGMENTS = new Set(['.live-artifacts']);
|
||||
const DESIGN_HANDOFF_FILENAME = 'DESIGN-HANDOFF.md';
|
||||
const DESIGN_MANIFEST_FILENAME = 'DESIGN-MANIFEST.json';
|
||||
export const projectFileRenameTestHooks = {
|
||||
beforeCommit: null as null | ((paths: { source: string; target: string }) => Promise<void> | void),
|
||||
};
|
||||
|
|
@ -191,6 +193,8 @@ export async function buildProjectArchive(projectsRoot, projectId, root, metadat
|
|||
binary: true,
|
||||
});
|
||||
}
|
||||
addDesignHandoff(zip, entries, archiveBaseName || path.basename(projectRoot));
|
||||
addDesignManifest(zip, entries, archiveBaseName || path.basename(projectRoot));
|
||||
// Level 6 is the zlib default — balances speed and ratio for typical
|
||||
// project trees (HTML/CSS/JS plus a handful of assets). Level 9 buys
|
||||
// <5% on already-compressed PNGs/fonts at 2-3× CPU; level 1 produces
|
||||
|
|
@ -343,6 +347,233 @@ async function collectArchiveEntries(dir, relDir, out) {
|
|||
}
|
||||
}
|
||||
|
||||
function addDesignHandoff(zip, entries, projectLabel) {
|
||||
if (entries.some((entry) => entry.relPath === DESIGN_HANDOFF_FILENAME)) return;
|
||||
zip.file(DESIGN_HANDOFF_FILENAME, buildDesignHandoff(entries, projectLabel), {
|
||||
date: new Date(0),
|
||||
binary: false,
|
||||
});
|
||||
}
|
||||
|
||||
function addDesignManifest(zip, entries, projectLabel) {
|
||||
if (entries.some((entry) => entry.relPath === DESIGN_MANIFEST_FILENAME)) return;
|
||||
zip.file(DESIGN_MANIFEST_FILENAME, buildDesignManifest(entries, projectLabel), {
|
||||
date: new Date(0),
|
||||
binary: false,
|
||||
});
|
||||
}
|
||||
|
||||
// A file is treated as a preview-chrome wrapper only when it lives inside
|
||||
// a frames/ or device-frames/ directory, or its filename is an unambiguous
|
||||
// wrapper template (browser-chrome.html, device-frame.html). Filenames
|
||||
// like phone.html or iphone-upgrade.html are legitimate product-screen
|
||||
// deliverables and must not be dropped from manifest screens.
|
||||
const FRAME_WRAPPER_FILE_RE = /(^|\/)(frames?\/|device-frames?\/)|(^|\/)(browser-chrome|device-frame)\.html?$/i;
|
||||
|
||||
function isFrameWrapperHtmlFile(file: string): boolean {
|
||||
return FRAME_WRAPPER_FILE_RE.test(file);
|
||||
}
|
||||
|
||||
function projectFileMap(entries) {
|
||||
const files = entries.map((entry) => entry.relPath).sort((a, b) => a.localeCompare(b));
|
||||
const htmlFiles = files.filter((name) => /\.html?$/i.test(name));
|
||||
const screenHtmlFiles = htmlFiles.filter((name) => !isFrameWrapperHtmlFile(name));
|
||||
const cssFiles = files.filter((name) => /\.css$/i.test(name));
|
||||
const jsFiles = files.filter((name) => /\.[cm]?[jt]sx?$/i.test(name));
|
||||
const assetFiles = files.filter((name) => !htmlFiles.includes(name) && !cssFiles.includes(name) && !jsFiles.includes(name));
|
||||
const entryFile = screenHtmlFiles.find((name) => /(^|\/)index\.html$/i.test(name))
|
||||
|| screenHtmlFiles[0]
|
||||
|| htmlFiles.find((name) => /(^|\/)index\.html$/i.test(name))
|
||||
|| htmlFiles[0]
|
||||
|| files[0]
|
||||
|| 'index.html';
|
||||
return { files, htmlFiles, screenHtmlFiles, cssFiles, jsFiles, assetFiles, entryFile };
|
||||
}
|
||||
|
||||
function buildDesignManifest(entries, projectLabel) {
|
||||
const { files, htmlFiles, screenHtmlFiles, cssFiles, jsFiles, assetFiles, entryFile } = projectFileMap(entries);
|
||||
const screenFiles = screenHtmlFiles.length > 0 ? screenHtmlFiles : [entryFile];
|
||||
return JSON.stringify({
|
||||
schema: 'open-design.design-manifest.v1',
|
||||
title: projectLabel || 'Open Design project',
|
||||
entryFile,
|
||||
sourceFiles: {
|
||||
all: files,
|
||||
html: htmlFiles,
|
||||
css: cssFiles,
|
||||
scriptsAndComponents: jsFiles,
|
||||
assets: assetFiles,
|
||||
},
|
||||
screens: screenFiles.map((file) => {
|
||||
const isIndex = /(^|\/)index\.html?$/i.test(file);
|
||||
const isLanding = /(^|\/)(landing|marketing)\.html?$/i.test(file) || /landing|marketing/i.test(file);
|
||||
const isOsWidget = /widget|live-activity|lock-screen|home-screen/i.test(file);
|
||||
const isApp = /app|dashboard|workspace|generator|translator|editor|screen/i.test(file);
|
||||
return {
|
||||
file,
|
||||
role: isIndex && screenFiles.length > 1 ? 'launcher-overview' : isLanding ? 'landing-page' : isOsWidget ? 'os-widget-surface' : isApp ? 'product-screen' : 'screen',
|
||||
implementationNote: isIndex && screenFiles.length > 1
|
||||
? 'Use this as the navigation/overview entry only; implement each linked screen file as its own route/surface.'
|
||||
: 'Preserve visual hierarchy, responsive behavior, and interactive states from this screen.',
|
||||
};
|
||||
}),
|
||||
screenFilePolicy: {
|
||||
mode: 'screen-file-first',
|
||||
entryFileRole: screenFiles.length > 1 && /(^|\/)index\.html?$/i.test(entryFile) ? 'launcher-overview' : 'primary-screen',
|
||||
rules: [
|
||||
'Each distinct user-facing screen or surface must be delivered and implemented as its own file/route.',
|
||||
'If a landing page is present or requested, keep it in landing.html and do not merge it into the product app screen.',
|
||||
'When multiple HTML screens exist, index.html is a launcher/overview only; it must not be treated as the combined final UI.',
|
||||
'Keep product app screens, landing pages, platform screens, and OS widget surfaces separate in production code.',
|
||||
],
|
||||
},
|
||||
appModules: [
|
||||
'Identify domain-specific in-app modules from the exported UI; do not reduce them to generic cards.',
|
||||
'For each major module, implement purpose, default/loading/empty/error/success states, and responsive behavior.',
|
||||
'Keep app modules separate from OS home-screen widgets in the production component model.',
|
||||
],
|
||||
osWidgets: [
|
||||
'If the export includes home-screen, lock-screen, Live Activity, tablet glance, or Android widget surfaces, implement them as platform quick-access surfaces outside the app UI.',
|
||||
'If none are present, do not invent OS widgets unless the product requirements request them.',
|
||||
],
|
||||
landingPage: {
|
||||
detection: 'Inspect files and screen names for a marketing/landing page surface. If present, keep it separate from product app screens.',
|
||||
requiredSections: ['hero', 'value props', 'product proof/screenshots', 'feature proof', 'CTA'],
|
||||
},
|
||||
tokens: {
|
||||
source: cssFiles.length > 0 ? cssFiles : [entryFile],
|
||||
required: ['background', 'surface', 'foreground', 'muted text', 'border', 'accent', 'radius', 'shadow', 'spacing', 'type scale', 'motion'],
|
||||
note: 'Extract/freeze tokens before framework implementation so coding tools do not substitute default theme colors or typography.',
|
||||
},
|
||||
interactions: {
|
||||
source: jsFiles.length > 0 ? jsFiles : [entryFile],
|
||||
requiredStates: ['default', 'hover', 'focus', 'active', 'disabled', 'loading', 'empty', 'error', 'success'],
|
||||
requiredBehaviors: ['forms/validation where present', 'tabs/filters where present', 'dialogs/sheets/drawers where present', 'copy/generate/share actions where present', 'player or quick controls where present'],
|
||||
note: 'If the prototype is static, derive missing behavior from visible controls and document it before coding.',
|
||||
},
|
||||
responsiveViewports: [
|
||||
{ name: 'mobile-compact', width: 360, height: 800, category: 'mobile', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'mobile-standard', width: 390, height: 844, category: 'mobile', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'mobile-large', width: 430, height: 932, category: 'mobile', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'foldable-small-tablet', width: 600, height: 960, category: 'foldable-tablet', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'tablet-portrait', width: 820, height: 1180, category: 'tablet', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'tablet-landscape', width: 1024, height: 768, category: 'tablet', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'laptop', width: 1366, height: 768, category: 'desktop', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'desktop', width: 1440, height: 900, category: 'desktop', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'wide', width: 1920, height: 1080, category: 'wide', mustAvoidHorizontalScroll: true },
|
||||
],
|
||||
implementationChecklist: [
|
||||
'Open entryFile first and map screens, modules, tokens, and interactions.',
|
||||
'Extract tokens before writing framework components.',
|
||||
'Implement app-specific modules with real states instead of generic card grids.',
|
||||
'Preserve or rebuild JS interactions for meaningful UX actions.',
|
||||
'Validate screenshots at desktop/tablet/mobile viewports with no horizontal overflow.',
|
||||
'Keep landing pages, in-app modules, and OS widgets as separate implementation surfaces.',
|
||||
],
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
function buildDesignHandoff(entries, projectLabel) {
|
||||
const { files, htmlFiles, cssFiles, jsFiles, assetFiles, entryFile } = projectFileMap(entries);
|
||||
const accentLikelyBrandLed =
|
||||
files.some((name) => /(design|brand|tokens?|theme|style|tailwind|variables)\.(css|scss|sass|less|json|ts|tsx|js|jsx|md)$/i.test(name)) ||
|
||||
cssFiles.length > 0;
|
||||
const hasResponsiveClues =
|
||||
htmlFiles.length > 0 ||
|
||||
cssFiles.length > 0 ||
|
||||
files.some((name) => /(screens?|pages?|components?|app|src)\//i.test(name));
|
||||
const list = (items) => items.length > 0 ? items.map((name) => `- \`${name}\``).join('\n') : '- None detected';
|
||||
|
||||
return `# ${projectLabel || 'Open Design project'} implementation handoff
|
||||
|
||||
This archive is the source of truth for turning the design into production code. Start from \`${entryFile}\`, then preserve the visual system, responsive behavior, and interactions found in the exported files.
|
||||
|
||||
## Implementation target
|
||||
- Build production UI from the exported design, not a loose reinterpretation.
|
||||
- Preserve typography scale, spacing rhythm, color tokens, border radii, shadows, motion timing, and component states.
|
||||
- Replace static placeholders only when the target app has real data or functional equivalents.
|
||||
- Keep generated product UI free of Open Design chrome, preview labels, or design-process annotations.
|
||||
- Treat this handoff as a visual contract: if implementation choices conflict, match the exported pixels and behavior first, then refactor internals.
|
||||
|
||||
## Source map
|
||||
- Primary entry: \`${entryFile}\`
|
||||
- HTML screens detected: ${htmlFiles.length}
|
||||
- Stylesheets detected: ${cssFiles.length}
|
||||
- Script/component files detected: ${jsFiles.length}
|
||||
- Supporting assets detected: ${assetFiles.length}
|
||||
|
||||
## Responsive contract
|
||||
Validate the implementation across this 2025–2026 viewport matrix:
|
||||
- Mobile compact: 360×800
|
||||
- Mobile standard: 390×844
|
||||
- Mobile large: 430×932
|
||||
- Foldable / small tablet: 600×960
|
||||
- Tablet portrait: 820×1180
|
||||
- Tablet landscape: 1024×768
|
||||
- Laptop: 1366×768
|
||||
- Desktop: 1440×900
|
||||
- Wide desktop: 1920×1080
|
||||
|
||||
For responsive web exports, treat these as a modern breakpoint system for one adaptive web experience, not three fixed screenshots. Do not split responsive web into unrelated native app screens unless the project explicitly includes native targets. Use semantic layout thresholds, fluid \`clamp()\` type/spacing, and container queries where component width matters more than viewport width. ${hasResponsiveClues ? 'Preserve any CSS media queries, container queries, fluid \`clamp()\` scales, and layout changes already present in the exported files.' : 'If responsive rules are not present in the export, add them in the target implementation before shipping.'}
|
||||
|
||||
## Design fidelity contract
|
||||
- Extract reusable tokens before writing components: background, surface, foreground, muted text, border, accent, radius, shadow, spacing, type scale, and motion duration/easing.
|
||||
- Map product screens, in-app modules/components, optional landing page, and optional OS widget surfaces before coding. Keep these surfaces separate in the target architecture.
|
||||
- Match layout geometry: max-widths, gutters, grid columns, card proportions, sticky/fixed elements, and viewport-specific navigation.
|
||||
- Preserve real copy, labels, and data shown in the export. Do not replace specific text with generic marketing filler.
|
||||
- Preserve interactive affordances: hover, focus, pressed, disabled, loading, validation, copy/share, tab/accordion, modal/sheet, and keyboard states where present.
|
||||
- Preserve accessibility semantics when converting: headings stay hierarchical, controls remain buttons/links/inputs, focus states stay visible.
|
||||
- Do not keep prototype-only annotations, frame labels, or Open Design chrome in the production UI.
|
||||
|
||||
## CJX-ready UX contract
|
||||
- Use \`${DESIGN_MANIFEST_FILENAME}\` as the machine-readable map for screens, app modules, OS widgets, landing pages, tokens, interactions, and viewport checks.
|
||||
- Screen-file-first: when multiple user-facing surfaces exist, implement each HTML screen as its own route/file. Treat \`index.html\` as a launcher/overview when the manifest marks it that way, not as a combined final UI.
|
||||
- If \`landing.html\`, app screens, platform screens, or OS widget files exist, preserve those boundaries in the target app instead of merging them into one page.
|
||||
- A single self-contained \`${entryFile}\` is acceptable only when the export truly contains one user-facing screen and its CSS/JS are structured enough to extract tokens, components, states, and behavior.
|
||||
- If separate \`css/\` or \`js/\` files exist, treat them as source of truth for token/component/interactions before porting to React, Vue, SwiftUI, Compose, or another target stack.
|
||||
- In-app modules/components are product UI blocks inside the app. OS widgets are home-screen/lock-screen/quick-access surfaces outside the app. Do not merge those concepts.
|
||||
|
||||
## Color and brand contract
|
||||
- Use the exported design tokens and product/domain context as the color source of truth.
|
||||
- Do not introduce warm beige / cream / peach / pink / orange-brown background washes unless they are already explicit brand/reference colors in the export.
|
||||
- ${accentLikelyBrandLed ? 'A stylesheet or design/token file was detected; inspect it for canonical color variables before choosing framework theme tokens.' : 'No obvious token stylesheet was detected; sample colors from the entry file and convert them into named tokens before coding.'}
|
||||
|
||||
## Implementation sequence for AI coding tools
|
||||
1. Open \`${entryFile}\` and \`${DESIGN_MANIFEST_FILENAME}\`; identify every screen file, launcher/overview file, app module, and interaction before coding.
|
||||
2. If multiple HTML screens exist, map them to separate routes/surfaces first; do not merge \`landing.html\`, product app screens, platform screens, or OS widgets into one route.
|
||||
3. Extract a token table from CSS/root styles and inline styles before building framework components.
|
||||
4. Build product screens and domain-specific in-app modules from largest layout regions down to controls; avoid starting with isolated atoms that lose spatial intent.
|
||||
5. Port responsive behavior across the modern viewport matrix and test each semantic breakpoint before cleanup.
|
||||
6. Port interactions and states, then replace static placeholders only with real app data or functional equivalents.
|
||||
7. Keep optional landing page and OS widget surfaces as separate surfaces if present.
|
||||
8. Compare final screenshots against the export at 360×800, 390×844, 430×932, 820×1180, 1024×768, 1366×768, 1440×900, and 1920×1080 before declaring done.
|
||||
|
||||
## Entry points
|
||||
${list(htmlFiles)}
|
||||
|
||||
## Styles
|
||||
${list(cssFiles)}
|
||||
|
||||
## Scripts/components
|
||||
${list(jsFiles)}
|
||||
|
||||
## Assets and supporting files
|
||||
${list(assetFiles)}
|
||||
|
||||
## Coding checklist for AI tools
|
||||
1. Inspect \`${entryFile}\` and \`${DESIGN_MANIFEST_FILENAME}\` first and identify reusable components before coding.
|
||||
2. Implement each user-facing screen file as its own route/surface; keep launcher, landing, app, platform, and OS widget files separate.
|
||||
3. Extract design tokens into the target stack: colors, type scale, spacing, radius, shadows, and motion.
|
||||
4. Implement layout with real 2025–2026 responsive breakpoints, fluid type/spacing, and container-query-aware component behavior; test with no horizontal overflow.
|
||||
5. Preserve interactive controls, hover/focus/pressed states, form behavior, validation, and copy actions where present.
|
||||
6. Implement domain-specific in-app modules with real states; do not flatten them into generic cards.
|
||||
7. Keep landing page, product screens, and OS widget/quick-access surfaces separate when present.
|
||||
8. Confirm the production result visually matches the exported design before refactoring internals.
|
||||
9. Reject implementation shortcuts that flatten the design into generic cards, generic gradients, placeholder stats, or framework-default typography.
|
||||
10. If a detail is ambiguous, keep the exported HTML/CSS/JS behavior rather than inventing a new pattern.
|
||||
`;
|
||||
}
|
||||
|
||||
export async function readProjectFile(projectsRoot, projectId, name, metadata?) {
|
||||
const dir = resolveProjectDir(projectsRoot, projectId, metadata);
|
||||
const file = await resolveSafeReal(dir, name);
|
||||
|
|
|
|||
|
|
@ -55,31 +55,31 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
|
|||
id: 'editorial-monocle',
|
||||
label: 'Editorial — Monocle / FT magazine',
|
||||
mood:
|
||||
'Print-magazine feel. Generous whitespace, large serif headlines, restrained palette of off-white paper + ink + a single warm accent. Confident, quietly intelligent.',
|
||||
'Print-magazine feel for explicitly editorial or publishing briefs. Generous whitespace, large serif headlines, restrained palette of neutral paper + ink + a single brand-justified accent. Do not use this as the default for commerce, SaaS, dashboards, or product utilities.',
|
||||
references: ['Monocle', 'The Financial Times Weekend', 'NYT Magazine', 'It\'s Nice That'],
|
||||
displayFont: "'Iowan Old Style', 'Charter', Georgia, serif",
|
||||
bodyFont:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
palette: {
|
||||
bg: 'oklch(97% 0.012 80)', // off-white paper
|
||||
surface: 'oklch(99% 0.005 80)',
|
||||
fg: 'oklch(20% 0.02 60)', // ink
|
||||
muted: 'oklch(48% 0.015 60)',
|
||||
border: 'oklch(89% 0.012 80)',
|
||||
accent: 'oklch(58% 0.16 35)', // warm rust / clay
|
||||
bg: 'oklch(98% 0.004 95)', // neutral paper, not beige wash
|
||||
surface: 'oklch(100% 0.002 95)',
|
||||
fg: 'oklch(20% 0.018 70)', // ink
|
||||
muted: 'oklch(48% 0.012 70)',
|
||||
border: 'oklch(90% 0.006 95)',
|
||||
accent: 'oklch(52% 0.10 28)', // restrained editorial red; override from brand when available
|
||||
},
|
||||
posture: [
|
||||
'serif display, sans body, mono for metadata only',
|
||||
'no shadows, no rounded cards — borders + whitespace do the work',
|
||||
'one decisive image, cropped only at the bottom',
|
||||
'kicker / eyebrow in mono uppercase, one accent color, used at most twice',
|
||||
'kicker / eyebrow in mono uppercase, one accent color, used at most twice; never create peach/pink/orange-beige page washes unless the brand/reference requires them',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'modern-minimal',
|
||||
label: 'Modern minimal — Linear / Vercel',
|
||||
mood:
|
||||
'Quiet, precise, software-native. System fonts, near-greyscale palette, a single saturated accent. The chrome disappears so content is the only thing that registers.',
|
||||
'Quiet, precise, software-native. System fonts, crisp neutral foundations, and a small but visible product palette (primary + secondary + status/accent) so the interface feels shipped rather than greyscale. The chrome stays restrained while interaction states, illustrations, charts, and product moments carry color.',
|
||||
references: ['Linear', 'Vercel', 'Notion 2024', 'Stripe docs'],
|
||||
displayFont:
|
||||
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif",
|
||||
|
|
@ -97,34 +97,34 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
|
|||
'tight letter-spacing on display sizes (-0.02em)',
|
||||
'hairline borders only, no shadows except dropdowns/modals',
|
||||
'mono numerics with `font-variant-numeric: tabular-nums`',
|
||||
'sticky frosted nav, content-led layouts (no hero illustrations)',
|
||||
'one accent: links + primary CTA, nothing else',
|
||||
'sticky frosted nav, content-led layouts with one product illustration, device mockup, or data visualization when it clarifies the product',
|
||||
'controlled color system: primary action color + one secondary signal + status colors; avoid monochrome/unstyled outputs, but never flood every card with gradients',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'warm-soft',
|
||||
label: 'Warm & soft — Stripe pre-2020 / Headspace',
|
||||
id: 'human-approachable',
|
||||
label: 'Human / approachable — Airbnb / Duolingo systems',
|
||||
mood:
|
||||
'Cream backgrounds, soft accent, gentle radii. Reads like a thoughtful product magazine — friendly without being cute. Good for fintech, wellness, indie SaaS.',
|
||||
references: ['Stripe pre-2020', 'Headspace', 'Substack', 'Mercury'],
|
||||
'Friendly and tactile without the generic cozy canvas. Uses a clean neutral background, product-led color system, generous radii, and clear hierarchy. Good for consumer tools, marketplaces, wellness, education, translation, AI assistants, and indie SaaS when the brand has not supplied a palette.',
|
||||
references: ['Airbnb', 'Duolingo product surfaces', 'Miro', 'Mercury'],
|
||||
displayFont:
|
||||
"'Tiempos Headline', 'Newsreader', 'Iowan Old Style', Georgia, serif",
|
||||
"'Söhne', 'Avenir Next', -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
||||
bodyFont:
|
||||
"'Söhne', -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
||||
"-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
|
||||
palette: {
|
||||
bg: 'oklch(97% 0.018 70)', // warm cream
|
||||
surface: 'oklch(99% 0.008 70)',
|
||||
fg: 'oklch(22% 0.02 50)',
|
||||
muted: 'oklch(50% 0.018 50)',
|
||||
border: 'oklch(90% 0.014 70)',
|
||||
accent: 'oklch(64% 0.13 28)', // terracotta
|
||||
bg: 'oklch(98% 0.004 240)',
|
||||
surface: 'oklch(100% 0 0)',
|
||||
fg: 'oklch(20% 0.02 240)',
|
||||
muted: 'oklch(50% 0.018 240)',
|
||||
border: 'oklch(90% 0.006 240)',
|
||||
accent: 'oklch(56% 0.12 170)', // brand-safe teal
|
||||
},
|
||||
posture: [
|
||||
'serif display, soft sans body',
|
||||
'gentle radii (12–16px), no hard 0px corners on content cards',
|
||||
'single accent used for primary CTA + one editorial flourish (a quote mark, a stat)',
|
||||
'soft inner glow on hero cards rather than drop shadows',
|
||||
'avoid icons; use real screenshots / photographs / illustrations',
|
||||
'sans display with strong weight contrast, system body for readability',
|
||||
'comfortable radii (12–18px) paired with crisp grid alignment',
|
||||
'primary action color plus a secondary/domain accent and clear status colors; use color to separate panels, states, and product moments',
|
||||
'subtle elevation only on interactive cards; tasteful gradients/glows are allowed for hero/device/product moments, never as a full-page beige/pastel wash',
|
||||
'avoid generic pastel/beige gradients; use real product screenshots, data, or labelled placeholders',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -165,7 +165,7 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
|
|||
bodyFont:
|
||||
"ui-monospace, 'IBM Plex Mono', 'JetBrains Mono', Menlo, monospace",
|
||||
palette: {
|
||||
bg: 'oklch(96% 0.004 100)', // off-white printer paper
|
||||
bg: 'oklch(98% 0.004 240)', // neutral printer paper
|
||||
surface: 'oklch(100% 0 0)',
|
||||
fg: 'oklch(15% 0.02 100)',
|
||||
muted: 'oklch(40% 0.02 100)',
|
||||
|
|
|
|||
|
|
@ -42,12 +42,12 @@ When the user opens a new project or sends a fresh design brief, your **very fir
|
|||
"questions": [
|
||||
{ "id": "output", "label": "What are we making?", "type": "radio", "required": true,
|
||||
"options": ["Slide deck / pitch", "Single web prototype / landing", "Multi-screen app prototype", "Dashboard / tool UI", "Editorial / marketing page", "Other — I'll describe"] },
|
||||
{ "id": "platform", "label": "Primary surface", "type": "radio",
|
||||
"options": ["Mobile (iOS/Android)", "Desktop web", "Tablet", "Responsive — all sizes", "Fixed canvas (1920×1080)"] },
|
||||
{ "id": "platform", "label": "Target platform", "type": "checkbox", "maxSelections": 4,
|
||||
"options": ["Responsive web", "Desktop web", "iOS app", "Android app", "Tablet app", "Desktop app", "Fixed canvas (1920×1080)"] },
|
||||
{ "id": "audience", "label": "Who is this for?", "type": "text",
|
||||
"placeholder": "e.g. early-stage investors, dev-tools buyers, internal exec review" },
|
||||
{ "id": "tone", "label": "Visual tone", "type": "checkbox", "maxSelections": 2,
|
||||
"options": ["Editorial / magazine", "Modern minimal", "Playful / illustrative", "Tech / utility", "Luxury / refined", "Brutalist / experimental", "Soft / warm"] },
|
||||
"options": ["Editorial / magazine", "Modern minimal", "Playful / illustrative", "Tech / utility", "Luxury / refined", "Brutalist / experimental", "Human / approachable"] },
|
||||
{ "id": "brand", "label": "Brand context", "type": "radio",
|
||||
"options": ["Pick a direction for me", "I have a brand spec — I'll share it", "Match a reference site / screenshot — I'll attach it"] },
|
||||
{ "id": "scale", "label": "Roughly how much?", "type": "text",
|
||||
|
|
@ -64,7 +64,7 @@ Form authoring rules:
|
|||
- \`type\` is one of: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`.
|
||||
- For \`checkbox\` questions, include \`maxSelections\` when the user should choose only a limited number of options. Do not encode limits only in the label text.
|
||||
- Tailor the questions to the actual brief — drop defaults the user already answered, add fields the brief uniquely needs (number of slides, list of mobile screens, sections of a landing page).
|
||||
- **Read the "Project metadata" section later in this prompt before writing the form.** That block lists what the user already chose at create time (kind, fidelity, speakerNotes, animations, template). Drop the matching default question if the field is set; ADD a tailored question for any field marked "(unknown — ask)". For example, on a deck with \`speakerNotes: (unknown — ask…)\`, include a yes/no on speaker notes; on a template project where animations is unknown, include a motion radio. Don't re-ask the kind itself if metadata.kind is set — the user already told you.
|
||||
- **Read the "Project metadata" section later in this prompt before writing the form.** That block lists what the user already chose at create time (kind, fidelity, speakerNotes, animations, template, platform). Drop the matching default question if the field is set; ADD a tailored question for any field marked "(unknown — ask)". For example, on a deck with \`speakerNotes: (unknown — ask…)\`, include a yes/no on speaker notes; on a template project where animations is unknown, include a motion radio; on a cross-platform project, ask which screens need native variants instead of re-asking platform. Don't re-ask the kind itself if metadata.kind is set — the user already told you.
|
||||
- Keep it under ~7 questions. Second batch in a follow-up form if needed.
|
||||
- Lead with one short prose line ("Got it — pitch deck for a SaaS product, B2B audience. Tell me the rest:") then the form. Do **not** write a long pre-amble.
|
||||
- After \`</question-form>\`, **stop your turn**. Do not write code. Do not start tools. Do not narrate "I'll wait."
|
||||
|
|
@ -113,7 +113,7 @@ Run brand-spec extraction *before* TodoWrite — five steps, each in its own \`B
|
|||
- Six color tokens (\`--bg\`, \`--surface\`, \`--fg\`, \`--muted\`, \`--border\`, \`--accent\`) in OKLch
|
||||
- Display + body + mono font stacks
|
||||
- 3–5 layout posture rules you observed (radii, border weight, accent budget)
|
||||
5. **Vocalise.** State the system you'll use in one sentence ("warm cream background, single rust accent at oklch(58% 0.15 35), Newsreader display + system body") so the user can redirect cheaply.
|
||||
5. **Vocalise.** State the system you'll use in one sentence ("deep navy product canvas, single electric-cyan accent at oklch(68% 0.16 220), geometric display + system body") so the user can redirect cheaply.
|
||||
|
||||
Then proceed to RULE 3.
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ The standard plan template (adapt the middle steps to the brief):
|
|||
- 2. (if branch B) Confirm brand-spec.md + bind to :root
|
||||
(if branch A) Bind chosen direction's palette to :root
|
||||
(else) Pick a direction matching the tone, bind to :root
|
||||
- 3. Plan section/slide/screen list with rhythm (state list aloud before writing)
|
||||
- 3. Plan section/slide/screen list with platform variants and rhythm (state list aloud before writing)
|
||||
- 4. Copy the seed template to project root
|
||||
- 5. Paste & fill the planned layouts/screens/slides
|
||||
- 6. Replace [REPLACE] placeholders with real, specific copy from the brief
|
||||
|
|
@ -181,6 +181,7 @@ ${renderDirectionSpecBlock()}
|
|||
|
||||
### A. Embody the specialist
|
||||
Pick the persona before writing CSS:
|
||||
- **Responsive / cross-platform prototype** → product systems designer. Define shared information architecture first, then explicit modern breakpoint variants: mobile compact (360px), mobile standard/large (390–430px), foldable/small tablet (600–744px), tablet portrait (768–834px), tablet landscape/large tablet (1024–1180px), laptop (1280–1366px), desktop (1440–1536px), and wide (1920px). Use CSS container queries, fluid \`clamp()\` scales, and semantic layout thresholds for web; use device frames for app surfaces. Never merely shrink desktop cards into a phone viewport. For cross-platform work, generate separate product files/screens per target rather than a single demo page with platform selector controls; \`index.html\` should only be an overview/launcher when multiple files exist.
|
||||
- **Slide deck** → slide designer. Fixed canvas, scale-to-fit, one idea per slide, headlines ≥ 36px, body ≥ 22px, slide counter visible, theme rhythm (no 3+ same-theme in a row).
|
||||
- **Mobile app prototype** → interaction designer. Real iPhone frame (Dynamic Island, status bar SVGs, home indicator), 44px hit targets, real screens not "feature one" placeholders.
|
||||
- **Landing / marketing** → brand designer. One hero, 3–6 sections, real copy, *one* decisive flourish.
|
||||
|
|
@ -204,6 +205,8 @@ Every prototype / mobile / deck skill ships:
|
|||
- ❌ Filler copy — "Feature One / Feature Two", lorem ipsum
|
||||
- ❌ An icon next to every heading
|
||||
- ❌ A gradient on every background
|
||||
- ❌ Warm beige / cream / peach / pink / orange-brown page backgrounds unless the user's brand, screenshots, or selected direction explicitly require them
|
||||
- ❌ Product artifacts that expose designer settings, viewport selectors, platform toggles, target-count badges, "demo controls", or generated-design metadata as if they were app UI
|
||||
|
||||
When you don't have a real value, leave a short honest placeholder (\`—\`, a grey block, a labelled stub) instead of inventing one. An honest placeholder beats a fake stat.
|
||||
|
||||
|
|
@ -214,13 +217,23 @@ Default to 2–3 differentiated directions on the same brief — different colou
|
|||
Show something visible early, even if it is a wireframe with grey blocks and labelled placeholders. The user redirects cheaply at this stage. Wrap the first pass in a visible artifact and *say* it is a wireframe.
|
||||
|
||||
### F. Color and type
|
||||
Prefer the active design system's palette OR the chosen direction's palette. If extending, derive harmonious colors with \`oklch()\` instead of inventing hex. Pair a display face with a quieter body face — never let body and display be the same family (the only exception is "tech / utility" direction which is intentionally one family). One accent colour, used at most twice per screen.
|
||||
Prefer the active design system's palette OR the chosen direction's palette. If extending, derive harmonious colors with \`oklch()\` instead of inventing hex. The background must be selected from the user's product domain, brand assets, screenshots, or chosen direction — never from generic app chrome or a default cozy canvas. For product utilities, marketplaces, dashboards, and SaaS, start from neutral or brand-colored foundations; do not fall back to warm beige / peach / pink / orange-brown Claude-style canvases just because no brand was provided. Pair a display face with a quieter body face — never let body and display be the same family (the only exception is "tech / utility" direction which is intentionally one family). One accent colour, used at most twice per screen.
|
||||
|
||||
### G. Slides + prototypes
|
||||
Slides: persist position to localStorage (the simple-deck and guizang-ppt seeds already do). Tag slides with \`data-screen-label="01 Title"\`. Slide numbers are 1-indexed. Theme rhythm: no 3+ same-theme in a row.
|
||||
Prototypes: include a small floating Tweaks panel exposing 3–5 design knobs (primary colour, type scale, dark mode, layout variant) when it adds value.
|
||||
Product prototypes: do **not** include floating Tweaks panels, platform/settings choosers, theme knobs, viewport toggles, or other designer/demo controls in the artifact. If variation controls are useful for internal iteration, keep them out of final product files unless the user explicitly asks for a design-system/spec dashboard.
|
||||
|
||||
### H. Multi-device + multi-screen layouts — use shared frames
|
||||
### H. Cross-platform + multi-device layouts — use platform contracts and shared frames
|
||||
When the user selects multiple platform targets or metadata says \`platform: responsive\`, design the same product across surfaces instead of one web-only page. Apply these contracts:
|
||||
|
||||
- **Responsive web**: include desktop, tablet, and mobile states for the same web product. Use semantic layout regions, fluid type with \`clamp()\`, breakpoint/container-query adaptations, and verify no horizontal scroll at 360px / 390px / 430px / 600px / 820px / 1024px / 1366px / 1440px / 1920px. The mobile layout must be redesigned for small screens with usable spacing, prioritised content, and real product navigation — not a squeezed desktop or tiny centered poster.
|
||||
- **iOS app**: create a dedicated iOS product file/screen (for example \`mobile-ios.html\`) with an iPhone frame, Dynamic Island/status/home indicators, 44px minimum hit targets, iOS-safe bottom navigation or sheet patterns, and no Android-only Material navigation.
|
||||
- **Android app**: create a dedicated Android product file/screen (for example \`mobile-android.html\`) with a Pixel frame, status bar + nav bar, 48dp hit targets, Material navigation patterns, and no iOS-only chrome.
|
||||
- **Tablet**: create a dedicated tablet product file/screen (for example \`tablet.html\`) with split panes, sidebars, inspectors, and larger touch targets; do not simply scale the phone UI up or let tablet layouts overflow horizontally.
|
||||
- **Desktop app**: include desktop chrome/sidebar density, keyboard-friendly states, resizable panes, and hover/focus states.
|
||||
- **App-specific modules/components**: every product/app prototype must include domain-specific in-app modules by default (not optional): player controls for media, streak/check-in modules for habits, cart/order/coupon modules for commerce, balance/transaction/budget modules for finance, etc. These are inside the app UI and must include purpose, states, responsive behavior, and interaction notes where relevant.
|
||||
- **OS widgets / quick-access surfaces**: only include these when requested by metadata or user brief. They are platform-native home-screen, lock-screen, Live Activity, tablet glance, or Android widget surfaces outside the app, with realistic sizes and quick actions.
|
||||
- **CJX-ready UX**: artifacts must be implementation-ready. Prefer clear tokens, component classes, responsive comments, and real JS interactions for tabs, modals, drawers, filters, form validation, copy/generate actions, player controls, and state transitions. A self-contained \`index.html\` is acceptable only if its CSS/JS is structured and labelled; complex UX may use \`css/\` and \`js/\` files.
|
||||
When the brief calls for showing the SAME product across multiple devices (desktop + tablet + phone) or showing MULTIPLE screens of the same app side-by-side (onboarding 1 → 2 → 3, or feed → detail → checkout), do NOT re-draw a phone/laptop frame from scratch. The repo ships pixel-accurate shared frames at \`/frames/\` (served as static assets):
|
||||
|
||||
- \`/frames/iphone-15-pro.html\` — 390 × 844, Dynamic Island
|
||||
|
|
@ -251,7 +264,7 @@ Then in \`index.html\` use:
|
|||
width="390" height="844" loading="lazy"></iframe>
|
||||
\`\`\`
|
||||
|
||||
The single-screen \`mobile-app\` skill already inlines the iPhone frame in its seed; you only need the shared frames for the multi-device / multi-screen case. Don't re-draw — use these.
|
||||
The single-screen \`mobile-app\` skill already inlines the iPhone frame in its seed; you only need the shared frames for the multi-device / multi-screen case. Don't re-draw — use these. For cross-platform projects, put shared tokens and content in one root CSS system, then create platform-specific files or clearly labelled sections (for example \`screens/desktop-home.html\`, \`screens/ios-home.html\`, \`screens/android-home.html\`) so reviewers can compare native adaptations side by side.
|
||||
|
||||
### I. Restraint over ornament
|
||||
"One thousand no's for every yes." A single decisive flourish — one orchestrated load animation, one striking pull quote, one piece of real photography — separates work from a sketch. Three competing flourishes turn it back into noise.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ PDFs, PPTX, DOCX: you can extract them via Bash (\`unzip\`, \`pdftotext\`, etc.)
|
|||
- Keep individual files under ~1000 lines. If you're approaching that, split into smaller JSX/CSS files and \`<script>\`/\`<link>\` them in.
|
||||
- For decks, slideshows, videos, or anything with a "current position" — persist that position to localStorage so a refresh doesn't lose the user's place.
|
||||
- Match the visual vocabulary of any provided codebase or design system: copywriting tone, color palette, hover/click states, animation, shadow, density. Think out loud about what you observe before you start writing.
|
||||
- **Color usage**: prefer the active design system's palette. If you must extend it, define harmonious colors with \`oklch()\` rather than inventing hex from scratch.
|
||||
- **Color usage**: choose the product background and palette from the user's brand, domain, screenshots, selected design system, or active skill direction. Do not inherit Open Design app chrome colors. Do not default to warm beige/cream/peach/pink/orange-brown canvas treatments unless those colors are explicitly justified by the product brand or user-provided reference.
|
||||
- Don't use \`scrollIntoView\` — it can break the embedded preview. Use other DOM scroll methods.
|
||||
|
||||
## Content guidelines
|
||||
|
|
@ -68,7 +68,7 @@ PDFs, PPTX, DOCX: you can extract them via Bash (\`unzip\`, \`pdftotext\`, etc.)
|
|||
- **Ask before adding material.** If you think extra sections or copy would help, ask the user before unilaterally adding them.
|
||||
- **Vocalize the system up front.** After exploring resources, state the system you'll use (background colors, type scale, layout patterns) before you start building. This gives the user a chance to redirect cheaply.
|
||||
- **Use appropriate scales.** 1920×1080 slide text is never smaller than 24px. Mobile hit targets are at least 44px. 12pt minimum for print.
|
||||
- **Avoid AI slop tropes:** aggressive gradient backgrounds, gratuitous emoji, rounded boxes with a left-border accent, SVG-as-illustration when a placeholder would do, overused fonts (Inter, Roboto, Arial, Fraunces).
|
||||
- **Avoid AI slop tropes:** aggressive gradient backgrounds; gratuitous emoji; rounded boxes with a left-border accent; SVG-as-illustration when a placeholder would do; overused fonts (Inter, Roboto, Arial, Fraunces); and the generic warm beige/peach/pink/orange-brown “AI canvas” look when it is not brand-led.
|
||||
- **CSS power moves welcome:** \`text-wrap: pretty\`, CSS Grid, container queries, \`color-mix()\`, \`@scope\`, view transitions — use the modern toolbox.
|
||||
|
||||
## React + Babel (inline JSX)
|
||||
|
|
|
|||
|
|
@ -43,8 +43,12 @@ type ProjectMetadata = {
|
|||
fidelity?: string | null;
|
||||
speakerNotes?: boolean | null;
|
||||
animations?: boolean | null;
|
||||
includeLandingPage?: boolean | null;
|
||||
includeOsWidgets?: boolean | null;
|
||||
templateId?: string | null;
|
||||
templateLabel?: string | null;
|
||||
platform?: string | null;
|
||||
platformTargets?: string[] | null;
|
||||
inspirationDesignSystemIds?: string[];
|
||||
imageModel?: string | null;
|
||||
imageAspect?: string | null;
|
||||
|
|
@ -447,6 +451,54 @@ function renderMetadataBlock(
|
|||
);
|
||||
lines.push('');
|
||||
lines.push(`- **kind**: ${metadata.kind}`);
|
||||
if (metadata.platform) {
|
||||
lines.push(`- **platform**: ${metadata.platform}`);
|
||||
} else if (metadata.kind === 'prototype' || metadata.kind === 'template' || metadata.kind === 'other') {
|
||||
lines.push('- **platform**: (unknown — ask: responsive web, desktop web, iOS app, Android app, tablet app, or desktop app?)');
|
||||
}
|
||||
if (Array.isArray(metadata.platformTargets) && metadata.platformTargets.length > 0) {
|
||||
lines.push(`- **platformTargets**: ${metadata.platformTargets.join(', ')}`);
|
||||
}
|
||||
if (metadata.platform === 'responsive' || metadata.platformTargets?.includes('responsive')) {
|
||||
lines.push(
|
||||
'- **responsive web contract**: `responsive` means one web product experience that adapts across modern browser/device ranges, not only legacy desktop/tablet/mobile buckets. It is not an iOS app, Android app, or native tablet app target. Show responsive behavior through real product layout changes; do not render viewport labels as user-facing product content. Cover 2025–2026 breakpoints: mobile compact 360px, mobile standard 390–430px, foldable/small tablet 600–744px, tablet portrait 768–834px, tablet landscape/large tablet 1024–1180px, laptop 1280–1366px, desktop 1440–1536px, and wide 1920px. Use fluid `clamp()` scales, container queries where useful, and explicit layout changes at semantic thresholds. Verify no horizontal scroll at 360px, 390px, 430px, 768px, 820px, 1024px, 1366px, 1440px, and 1920px unless the brief explicitly asks for a pan/board canvas.',
|
||||
);
|
||||
}
|
||||
if ((metadata.platformTargets?.length ?? 0) > 1) {
|
||||
lines.push(
|
||||
'- **cross-platform deliverable rule**: each selected target keeps the same product goal but MUST be delivered as its own product screen/file when more than one concrete target is selected. Use clear files such as `landing.html` (if enabled), `mobile-ios.html`, `mobile-android.html`, `tablet.html`, `desktop.html`, plus shared `css/` and `js/` when useful. `index.html` may be a launcher/overview that links to these files, but it must not be the only place where mobile/tablet/desktop designs live. Do not collapse cross-platform work into a single tabbed demo, selector UI, comparison board, platform map, or labelled documentation section inside one mock product page.',
|
||||
);
|
||||
}
|
||||
if (metadata.kind === 'prototype' || metadata.kind === 'template' || metadata.kind === 'other') {
|
||||
lines.push(
|
||||
'- **screen-file-first rule**: each distinct user-facing screen or surface MUST be delivered as its own HTML file unless the user explicitly asks for a single-page scroll or single-file artifact. Do not combine landing pages, product app screens, dashboards, history, pricing, settings, mobile app, tablet app, desktop app, or OS widget surfaces into one long page. Use `index.html` as a launcher/overview that links to screen files when more than one screen exists; it may summarize the product and show screen cards, but it must not contain the full design for every screen.',
|
||||
);
|
||||
lines.push(
|
||||
'- **product-realism rule**: final artifacts must look like real end-user product UI. Do not render project metadata, screen counts, target counts, state counts, "demo only" labels, "settings" panels for choosing platforms, "full design target" badges, viewport/device selector controls, theme/style knobs, platform output maps, behavior-spec sections, or design-process cards inside the product unless the user explicitly asks for a design spec/dashboard. Any navigation/tabs inside the artifact must be real product navigation, not designer controls for switching generated mockups.',
|
||||
);
|
||||
lines.push(
|
||||
'- **visual-system rule**: when the user does not specify colors, layout, or visual direction, you must still make an intentional product-appropriate visual system. Infer a palette from the product category and audience with at least: neutral surface tokens, a primary action color, a secondary/domain accent, and status colors. Avoid plain monochrome/unstyled greyscale outputs. Use tasteful gradients, illustrations, iconography, device/product mockups, and colored state moments where they clarify the product, while still avoiding generic beige/peach/pink/brown AI washes.',
|
||||
);
|
||||
lines.push(
|
||||
'- **app-specific modules rule**: include domain-specific in-app modules/components by default (cards, panels, controls, charts, lists, quick actions, status modules, mini players, checkout/cart summaries, etc. as appropriate). These are product UI modules, not OS home-screen widgets. Give each major module a clear purpose, states, and responsive behavior instead of generic card grids.',
|
||||
);
|
||||
lines.push(
|
||||
'- **CJX-ready UX rule**: the artifact must be implementation-ready, not a static screenshot. Structure CSS tokens/components/responsive sections clearly; include real JavaScript behavior for meaningful UX such as tabs, dialogs, drawers, filters, generation/copy actions, validation, playback controls, or state transitions. If keeping a self-contained `index.html`, put the CSS/JS in clearly labelled blocks; for complex UX, generate `css/` and `js/` files when useful.',
|
||||
);
|
||||
lines.push(
|
||||
'- **interaction-fidelity rule**: when the requested screen includes user input, generation, copying, validation, login, checkout, filtering, or any action verb, build real interactive controls for that screen. Do not substitute static text rows, prefilled-only mockups, screenshot-like device frames, or decorative state cards for editable inputs and working actions.',
|
||||
);
|
||||
}
|
||||
if (metadata.includeLandingPage) {
|
||||
lines.push(
|
||||
'- **includeLandingPage**: true — create `landing.html` as a separate responsive marketing companion surface in addition to the selected product/app screens. Do not implement the landing page only as a section inside `index.html`, even for responsive-web-only projects. If there is a working product/app screen, create it as a separate file such as `app.html`, `dashboard.html`, or a domain-specific screen name. `index.html` should be a lightweight launcher/overview when multiple files exist. Include hero, value props, product screenshots/device mockups, proof/features, and an appropriate CTA such as waitlist, download, or contact sales.',
|
||||
);
|
||||
}
|
||||
if (metadata.includeOsWidgets) {
|
||||
lines.push(
|
||||
'- **includeOsWidgets**: true — add platform-native OS home-screen / lock-screen / quick-access widget surfaces where relevant. These are outside-the-app widgets (for example iOS WidgetKit, Android home screen widget, Live Activity/lock screen, tablet glance panel), not in-app cards. Include realistic widget sizes and direct quick actions for the domain.',
|
||||
);
|
||||
}
|
||||
if (metadata.intent === 'live-artifact') {
|
||||
lines.push(
|
||||
'- **intent**: live-artifact — the user chose New live artifact. The first output should be a live artifact/dashboard/report, not a one-off static mockup. Prefer the `live-artifact` skill workflow when available, keep source data compact, and register through the daemon live-artifact tool path once that wrapper/tooling is available.',
|
||||
|
|
|
|||
|
|
@ -259,12 +259,12 @@ describe('app-config', () => {
|
|||
agentCliEnv: {
|
||||
claude: {
|
||||
CLAUDE_CONFIG_DIR: ' ~/.claude-2 ',
|
||||
ANTHROPIC_API_KEY: 'sk-should-not-persist',
|
||||
ANTHROPIC_API_KEY: ' sk-proxy-anthropic ',
|
||||
},
|
||||
codex: {
|
||||
CODEX_HOME: '~/.codex-alt',
|
||||
CODEX_BIN: '~/bin/codex-next',
|
||||
OPENAI_API_KEY: 'sk-should-not-persist',
|
||||
OPENAI_API_KEY: ' sk-proxy-openai ',
|
||||
},
|
||||
gemini: {
|
||||
GEMINI_API_KEY: 'should-not-persist',
|
||||
|
|
@ -278,8 +278,8 @@ describe('app-config', () => {
|
|||
const cfg = await readAppConfig(dataDir);
|
||||
|
||||
expect(cfg.agentCliEnv).toEqual({
|
||||
claude: { CLAUDE_CONFIG_DIR: '~/.claude-2' },
|
||||
codex: { CODEX_HOME: '~/.codex-alt', CODEX_BIN: '~/bin/codex-next' },
|
||||
claude: { CLAUDE_CONFIG_DIR: '~/.claude-2', ANTHROPIC_API_KEY: 'sk-proxy-anthropic' },
|
||||
codex: { CODEX_HOME: '~/.codex-alt', CODEX_BIN: '~/bin/codex-next', OPENAI_API_KEY: 'sk-proxy-openai' },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ describe('buildProjectArchive', () => {
|
|||
.filter((entry) => !entry.dir)
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
expect(fileEntries).toEqual(['frames/phone.html', 'index.html', 'src/app.css']);
|
||||
expect(fileEntries).toEqual(['DESIGN-HANDOFF.md', 'DESIGN-MANIFEST.json', 'frames/phone.html', 'index.html', 'src/app.css']);
|
||||
});
|
||||
|
||||
it('zips the whole project when no root is given', async () => {
|
||||
|
|
@ -46,6 +46,8 @@ describe('buildProjectArchive', () => {
|
|||
const fileEntries = Object.values(zip.files)
|
||||
.filter((entry) => !entry.dir)
|
||||
.map((entry) => entry.name);
|
||||
expect(fileEntries).toContain('DESIGN-HANDOFF.md');
|
||||
expect(fileEntries).toContain('DESIGN-MANIFEST.json');
|
||||
expect(fileEntries).toContain('README.md');
|
||||
expect(fileEntries).toContain('ui-design/index.html');
|
||||
expect(fileEntries).toContain('ui-design/src/app.css');
|
||||
|
|
@ -86,4 +88,111 @@ describe('buildProjectArchive', () => {
|
|||
const zip = await JSZip.loadAsync(buffer);
|
||||
expect(Object.keys(zip.files)).toContain('index.html');
|
||||
});
|
||||
|
||||
it('adds an AI-coding handoff guide to project archives', async () => {
|
||||
const { buffer } = await buildProjectArchive(projectsRoot, projectId, 'ui-design');
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const handoff = await zip.file('DESIGN-HANDOFF.md')?.async('string');
|
||||
expect(handoff).toContain('implementation handoff');
|
||||
expect(handoff).toContain('Mobile compact: 360×800');
|
||||
expect(handoff).toContain('Tablet portrait: 820×1180');
|
||||
expect(handoff).toContain('Wide desktop: 1920×1080');
|
||||
expect(handoff).toContain('Design fidelity contract');
|
||||
expect(handoff).toContain('CJX-ready UX contract');
|
||||
expect(handoff).toContain('DESIGN-MANIFEST.json');
|
||||
expect(handoff).toContain('in-app modules/components');
|
||||
expect(handoff).toContain('OS widgets are home-screen/lock-screen/quick-access surfaces');
|
||||
expect(handoff).toContain('Color and brand contract');
|
||||
expect(handoff).toContain('Do not introduce warm beige / cream / peach / pink / orange-brown background washes');
|
||||
expect(handoff).toContain('Build product screens and domain-specific in-app modules');
|
||||
});
|
||||
|
||||
it('adds a machine-readable design manifest to project archives', async () => {
|
||||
const { buffer } = await buildProjectArchive(projectsRoot, projectId, 'ui-design');
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const manifestRaw = await zip.file('DESIGN-MANIFEST.json')?.async('string');
|
||||
const manifest = JSON.parse(manifestRaw || '{}');
|
||||
expect(manifest.schema).toBe('open-design.design-manifest.v1');
|
||||
expect(manifest.entryFile).toBe('index.html');
|
||||
expect(manifest.sourceFiles.css).toEqual(['src/app.css']);
|
||||
expect(manifest.sourceFiles.html).toEqual(['frames/phone.html', 'index.html']);
|
||||
expect(manifest.screens.map((screen: { file: string }) => screen.file)).toEqual(['index.html']);
|
||||
expect(manifest.appModules.join(' ')).toContain('domain-specific in-app modules');
|
||||
expect(manifest.osWidgets.join(' ')).toContain('home-screen');
|
||||
expect(manifest.responsiveViewports).toContainEqual({
|
||||
name: 'tablet-portrait',
|
||||
width: 820,
|
||||
height: 1180,
|
||||
category: 'tablet',
|
||||
mustAvoidHorizontalScroll: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not classify plain home.html as a landing page in daemon archive manifests', async () => {
|
||||
const dir = path.join(projectsRoot, projectId, 'product-app');
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(path.join(dir, 'home.html'), '<!doctype html>home');
|
||||
await writeFile(path.join(dir, 'dashboard.html'), '<!doctype html>dashboard');
|
||||
await writeFile(path.join(dir, 'marketing.html'), '<!doctype html>marketing');
|
||||
|
||||
const { buffer } = await buildProjectArchive(projectsRoot, projectId, 'product-app');
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const manifestRaw = await zip.file('DESIGN-MANIFEST.json')?.async('string');
|
||||
const manifest = JSON.parse(manifestRaw || '{}');
|
||||
const screens = new Map(manifest.screens.map((screen: { file: string; role: string }) => [screen.file, screen.role]));
|
||||
|
||||
expect(screens.get('home.html')).not.toBe('landing-page');
|
||||
expect(screens.get('marketing.html')).toBe('landing-page');
|
||||
expect(screens.get('dashboard.html')).toBe('product-screen');
|
||||
});
|
||||
|
||||
it('keeps frame wrapper HTML out of daemon archive manifest screens', async () => {
|
||||
const dir = path.join(projectsRoot, projectId, 'framed-app');
|
||||
await mkdir(path.join(dir, 'frames'), { recursive: true });
|
||||
await writeFile(path.join(dir, 'index.html'), '<!doctype html>app');
|
||||
await writeFile(path.join(dir, 'frames', 'iphone-15-pro.html'), '<!doctype html>frame');
|
||||
await writeFile(path.join(dir, 'browser-chrome.html'), '<!doctype html>browser frame');
|
||||
|
||||
const { buffer } = await buildProjectArchive(projectsRoot, projectId, 'framed-app');
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const manifestRaw = await zip.file('DESIGN-MANIFEST.json')?.async('string');
|
||||
const manifest = JSON.parse(manifestRaw || '{}');
|
||||
|
||||
expect(manifest.sourceFiles.html).toEqual(['browser-chrome.html', 'frames/iphone-15-pro.html', 'index.html']);
|
||||
expect(manifest.screens.map((screen: { file: string }) => screen.file)).toEqual(['index.html']);
|
||||
});
|
||||
|
||||
it('does not overwrite an existing design handoff file', async () => {
|
||||
const dir = path.join(projectsRoot, projectId, 'custom-handoff');
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(path.join(dir, 'index.html'), '<!doctype html>hi');
|
||||
await writeFile(path.join(dir, 'DESIGN-HANDOFF.md'), '# custom handoff');
|
||||
const { buffer } = await buildProjectArchive(projectsRoot, projectId, 'custom-handoff');
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const handoff = await zip.file('DESIGN-HANDOFF.md')?.async('string');
|
||||
expect(handoff).toBe('# custom handoff');
|
||||
});
|
||||
|
||||
it('keeps phone.html and iphone-upgrade.html as real screens when outside frames/ directory', async () => {
|
||||
// phone.html as a carrier storefront, iphone-upgrade.html as a product
|
||||
// surface — they must not be silently dropped from manifest screens.
|
||||
const dir = path.join(projectsRoot, projectId, 'carrier-app');
|
||||
await mkdir(path.join(dir, 'frames'), { recursive: true });
|
||||
await writeFile(path.join(dir, 'phone.html'), '<!doctype html>phone storefront');
|
||||
await writeFile(path.join(dir, 'iphone-upgrade.html'), '<!doctype html>upgrade screen');
|
||||
await writeFile(path.join(dir, 'frames', 'device-shell.html'), '<!doctype html>frame');
|
||||
|
||||
const { buffer } = await buildProjectArchive(projectsRoot, projectId, 'carrier-app');
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const manifestRaw = await zip.file('DESIGN-MANIFEST.json')?.async('string');
|
||||
const manifest = JSON.parse(manifestRaw || '{}');
|
||||
|
||||
const screenFiles = manifest.screens.map((screen: { file: string }) => screen.file);
|
||||
expect(screenFiles).toContain('phone.html');
|
||||
expect(screenFiles).toContain('iphone-upgrade.html');
|
||||
// frame wrapper inside frames/ is still excluded from screens
|
||||
expect(screenFiles).not.toContain('frames/device-shell.html');
|
||||
// but still present in sourceFiles.html
|
||||
expect(manifest.sourceFiles.html).toContain('frames/device-shell.html');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -100,6 +100,19 @@ describe('composeSystemPrompt', () => {
|
|||
expect(prompt).toContain('`references/html-in-canvas.md`');
|
||||
});
|
||||
|
||||
it('does not add the responsive web contract to deck metadata without platform fields', () => {
|
||||
const prompt = composeSystemPrompt({
|
||||
metadata: {
|
||||
kind: 'deck',
|
||||
speakerNotes: true,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(prompt).toContain('- **kind**: deck');
|
||||
expect(prompt).not.toContain('**responsive web contract**');
|
||||
expect(prompt).not.toContain('**platformTargets**');
|
||||
});
|
||||
|
||||
describe('artifact handoff no-emit clauses (#1143)', () => {
|
||||
it('drops the absolute "non-negotiable" framing in favor of conditional language', () => {
|
||||
const prompt = composeSystemPrompt({});
|
||||
|
|
|
|||
|
|
@ -86,6 +86,15 @@ type TranslateFn = (key: keyof Dict, vars?: Record<string, string | number>) =>
|
|||
type SlideState = { active: number; count: number };
|
||||
type BoardTool = 'inspect' | 'pod';
|
||||
type StrokePoint = { x: number; y: number };
|
||||
type PreviewViewportId = 'desktop' | 'tablet' | 'mobile';
|
||||
type PreviewCanvasSize = { width: number; height: number };
|
||||
type PreviewViewportPreset = {
|
||||
id: PreviewViewportId;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
labelKey: keyof Dict;
|
||||
titleKey: keyof Dict;
|
||||
};
|
||||
type DeployProviderOption = {
|
||||
id: WebDeployProviderId;
|
||||
labelKey: 'fileViewer.vercelProvider' | 'fileViewer.cloudflarePagesProvider';
|
||||
|
|
@ -117,6 +126,29 @@ type DeployResultCard = {
|
|||
message?: string;
|
||||
};
|
||||
const MAX_BRIDGE_COORDINATE = 1_000_000;
|
||||
const PREVIEW_VIEWPORT_PRESETS: PreviewViewportPreset[] = [
|
||||
{
|
||||
id: 'desktop',
|
||||
width: null,
|
||||
height: null,
|
||||
labelKey: 'fileViewer.viewportDesktop',
|
||||
titleKey: 'fileViewer.viewportDesktopTitle',
|
||||
},
|
||||
{
|
||||
id: 'tablet',
|
||||
width: 820,
|
||||
height: 1180,
|
||||
labelKey: 'fileViewer.viewportTablet',
|
||||
titleKey: 'fileViewer.viewportTabletTitle',
|
||||
},
|
||||
{
|
||||
id: 'mobile',
|
||||
width: 390,
|
||||
height: 844,
|
||||
labelKey: 'fileViewer.viewportMobile',
|
||||
titleKey: 'fileViewer.viewportMobileTitle',
|
||||
},
|
||||
];
|
||||
|
||||
// The five basic style facets the inspect panel exposes. Kept narrow on
|
||||
// purpose — open-slide's design tokens panel only edits global tokens, so
|
||||
|
|
@ -262,6 +294,111 @@ function setMarkdownCodeBlockCopiedState(block: HTMLElement, copied: boolean, t:
|
|||
existingToast?.remove();
|
||||
}
|
||||
|
||||
function PreviewViewportControls({
|
||||
viewport,
|
||||
onViewport,
|
||||
t,
|
||||
tabIndex,
|
||||
}: {
|
||||
viewport: PreviewViewportId;
|
||||
onViewport: (viewport: PreviewViewportId) => void;
|
||||
t: TranslateFn;
|
||||
tabIndex?: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="viewer-viewport-switcher" role="group" aria-label={t('fileViewer.viewportAria')}>
|
||||
{PREVIEW_VIEWPORT_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
className={`viewer-action viewer-viewport-button${viewport === preset.id ? ' active' : ''}`}
|
||||
aria-pressed={viewport === preset.id}
|
||||
title={t(preset.titleKey)}
|
||||
tabIndex={tabIndex}
|
||||
onClick={() => onViewport(preset.id)}
|
||||
>
|
||||
{t(preset.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function previewViewportStyle(
|
||||
viewport: PreviewViewportId,
|
||||
previewScale = 1,
|
||||
canvasSize?: PreviewCanvasSize,
|
||||
): CSSProperties & Record<string, string | number> {
|
||||
const preset = PREVIEW_VIEWPORT_PRESETS.find((item) => item.id === viewport) ?? PREVIEW_VIEWPORT_PRESETS[0]!;
|
||||
if (!preset.width) return {};
|
||||
const effectiveScale = effectivePreviewScale(viewport, previewScale, canvasSize);
|
||||
return {
|
||||
'--preview-viewport-width': `${preset.width}px`,
|
||||
'--preview-viewport-height': `${preset.height}px`,
|
||||
'--preview-scale': effectiveScale,
|
||||
'--preview-user-scale': previewScale,
|
||||
};
|
||||
}
|
||||
|
||||
export function effectivePreviewScale(
|
||||
viewport: PreviewViewportId,
|
||||
previewScale: number,
|
||||
canvasSize?: PreviewCanvasSize,
|
||||
) {
|
||||
if (viewport === 'desktop') return previewScale;
|
||||
const preset = PREVIEW_VIEWPORT_PRESETS.find((item) => item.id === viewport);
|
||||
if (!preset?.width || !preset.height || !canvasSize?.width || !canvasSize.height) return previewScale;
|
||||
const canvasPadding = 48;
|
||||
const availableWidth = Math.max(1, canvasSize.width - canvasPadding);
|
||||
const availableHeight = Math.max(1, canvasSize.height - canvasPadding);
|
||||
const fitScale = Math.min(1, availableWidth / preset.width, availableHeight / preset.height);
|
||||
return Math.min(previewScale, fitScale);
|
||||
}
|
||||
|
||||
function previewScaleShellStyle(
|
||||
viewport: PreviewViewportId,
|
||||
previewScale: number,
|
||||
): CSSProperties & Record<string, string | number> {
|
||||
if (viewport === 'desktop') {
|
||||
return {
|
||||
width: `${100 / previewScale}%`,
|
||||
height: `${100 / previewScale}%`,
|
||||
transform: `scale(${previewScale})`,
|
||||
transformOrigin: '0 0',
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 'var(--preview-viewport-width)',
|
||||
height: 'var(--preview-viewport-height)',
|
||||
transform: 'scale(var(--preview-scale, 1))',
|
||||
transformOrigin: '0 0',
|
||||
};
|
||||
}
|
||||
|
||||
function usePreviewCanvasSize<T extends HTMLElement>() {
|
||||
const ref = useRef<T | null>(null);
|
||||
const [size, setSize] = useState<PreviewCanvasSize | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const measure = () => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
setSize({ width: rect.width, height: rect.height });
|
||||
};
|
||||
measure();
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
window.addEventListener('resize', measure);
|
||||
return () => window.removeEventListener('resize', measure);
|
||||
}, []);
|
||||
|
||||
return [ref, size] as const;
|
||||
}
|
||||
|
||||
function ensureMarkdownCodeBlockControls(root: HTMLElement, t: TranslateFn) {
|
||||
for (const block of root.querySelectorAll<HTMLElement>(`[${MARKDOWN_CODE_BLOCK_ATTR}]`)) {
|
||||
let button = block.querySelector<HTMLButtonElement>(`.${MARKDOWN_COPY_BUTTON_CLASS}`);
|
||||
|
|
@ -390,6 +527,9 @@ export function LiveArtifactViewer({
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [previewViewport, setPreviewViewport] = useState<PreviewViewportId>('desktop');
|
||||
const [previewBodyRef, previewBodySize] = usePreviewCanvasSize<HTMLDivElement>();
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [refreshError, setRefreshError] = useState<string | null>(null);
|
||||
const [refreshSuccess, setRefreshSuccess] = useState<string | null>(null);
|
||||
|
|
@ -397,8 +537,6 @@ export function LiveArtifactViewer({
|
|||
const [refreshHistory, setRefreshHistory] = useState<LiveArtifactRefreshLogEntry[]>([]);
|
||||
const [presentMenuOpen, setPresentMenuOpen] = useState(false);
|
||||
const [inTabPresent, setInTabPresent] = useState(false);
|
||||
const previewBodyRef = useRef<HTMLDivElement | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const presentWrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chromeActionsHost, setChromeActionsHost] = useState<HTMLElement | null>(null);
|
||||
useEffect(() => {
|
||||
|
|
@ -681,6 +819,13 @@ export function LiveArtifactViewer({
|
|||
data-active={mode === 'preview' ? 'true' : 'false'}
|
||||
aria-hidden={mode === 'preview' ? undefined : true}
|
||||
>
|
||||
<span className="viewer-divider" aria-hidden />
|
||||
<PreviewViewportControls
|
||||
viewport={previewViewport}
|
||||
onViewport={setPreviewViewport}
|
||||
t={t}
|
||||
tabIndex={mode === 'preview' ? 0 : -1}
|
||||
/>
|
||||
<span className="viewer-divider" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -742,7 +887,7 @@ export function LiveArtifactViewer({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="viewer-body">
|
||||
<div className="viewer-body" ref={previewBodyRef}>
|
||||
{refreshError ? (
|
||||
<LiveArtifactRefreshNotice
|
||||
tone="error"
|
||||
|
|
@ -771,22 +916,20 @@ export function LiveArtifactViewer({
|
|||
/>
|
||||
) : null}
|
||||
{mode === 'preview' ? (
|
||||
<div ref={previewBodyRef} className="live-artifact-preview-frame-host">
|
||||
<div
|
||||
style={{
|
||||
width: `${100 / previewScale}%`,
|
||||
height: `${100 / previewScale}%`,
|
||||
transform: `scale(${previewScale})`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
data-testid="live-artifact-preview-frame"
|
||||
title={liveArtifact.title}
|
||||
sandbox="allow-scripts allow-popups"
|
||||
src={previewUrl}
|
||||
/>
|
||||
<div
|
||||
className={`live-artifact-preview-layer preview-viewport preview-viewport-${previewViewport}`}
|
||||
style={previewViewportStyle(previewViewport, previewScale, previewBodySize)}
|
||||
>
|
||||
<div className="preview-frame-clip">
|
||||
<div style={previewScaleShellStyle(previewViewport, previewScale)}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
data-testid="live-artifact-preview-frame"
|
||||
title={liveArtifact.title}
|
||||
sandbox="allow-scripts allow-popups"
|
||||
src={previewUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : loading ? (
|
||||
|
|
@ -3026,6 +3169,7 @@ function HtmlViewer({
|
|||
const [source, setSource] = useState<string | null>(liveHtml ?? null);
|
||||
const [inlinedSource, setInlinedSource] = useState<string | null>(null);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [previewViewport, setPreviewViewport] = useState<PreviewViewportId>('desktop');
|
||||
const [zoomMenuOpen, setZoomMenuOpen] = useState(false);
|
||||
const zoomMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [presentMenuOpen, setPresentMenuOpen] = useState(false);
|
||||
|
|
@ -3238,7 +3382,8 @@ function HtmlViewer({
|
|||
const [slideState, setSlideState] = useState<SlideState | null>(
|
||||
() => htmlPreviewSlideState.get(previewStateKey) ?? null,
|
||||
);
|
||||
const previewBodyRef = useRef<HTMLDivElement | null>(null);
|
||||
const [previewBodyRef, previewBodySize] = usePreviewCanvasSize<HTMLDivElement>();
|
||||
const overlayPreviewScale = effectivePreviewScale(previewViewport, previewScale, previewBodySize);
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const shareRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chromeActionsHost, setChromeActionsHost] = useState<HTMLElement | null>(null);
|
||||
|
|
@ -4518,6 +4663,12 @@ function HtmlViewer({
|
|||
<span>{t('fileViewer.edit')}</span>
|
||||
</button>
|
||||
<span className="viewer-divider" aria-hidden />
|
||||
<PreviewViewportControls
|
||||
viewport={previewViewport}
|
||||
onViewport={setPreviewViewport}
|
||||
t={t}
|
||||
/>
|
||||
<span className="viewer-divider" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
className="icon-only"
|
||||
|
|
@ -4770,7 +4921,10 @@ function HtmlViewer({
|
|||
{source === null ? (
|
||||
<div className="viewer-empty">{t('fileViewer.loading')}</div>
|
||||
) : mode === 'preview' ? (
|
||||
<div className={manualEditMode ? 'manual-edit-workspace' : 'comment-preview-layer'}>
|
||||
<div
|
||||
className={`${manualEditMode ? 'manual-edit-workspace' : 'comment-preview-layer'} preview-viewport preview-viewport-${previewViewport}`}
|
||||
style={previewViewportStyle(previewViewport, previewScale, previewBodySize)}
|
||||
>
|
||||
{manualEditMode ? (
|
||||
<ManualEditPanel
|
||||
targets={manualEditTargets}
|
||||
|
|
@ -4800,12 +4954,7 @@ function HtmlViewer({
|
|||
) : null}
|
||||
<div className={manualEditMode ? 'manual-edit-canvas' : 'comment-frame-clip'}>
|
||||
<div
|
||||
style={{
|
||||
width: `${100 / previewScale}%`,
|
||||
height: `${100 / previewScale}%`,
|
||||
transform: `scale(${previewScale})`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
style={previewScaleShellStyle(previewViewport, previewScale)}
|
||||
>
|
||||
{useUrlLoadPreview ? (
|
||||
<iframe
|
||||
|
|
@ -4846,7 +4995,7 @@ function HtmlViewer({
|
|||
hoveredTarget={hoveredCommentTarget}
|
||||
activeTarget={activeCommentTarget}
|
||||
boardTool={boardTool}
|
||||
scale={previewScale}
|
||||
scale={overlayPreviewScale}
|
||||
strokePoints={strokePoints}
|
||||
onOpenComment={(comment, snapshot) => {
|
||||
setActiveCommentTarget(snapshot);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type {
|
|||
MediaAspect,
|
||||
ProjectKind,
|
||||
ProjectMetadata,
|
||||
ProjectPlatform,
|
||||
ProjectTemplate,
|
||||
MediaProviderCredentials,
|
||||
PromptTemplateSummary,
|
||||
|
|
@ -79,6 +80,45 @@ type PromptTemplatePick = {
|
|||
|
||||
type TranslateFn = (key: keyof Dict, vars?: Record<string, string | number>) => string;
|
||||
|
||||
type NewProjectPlatform = Exclude<ProjectPlatform, 'auto'>;
|
||||
|
||||
const DESIGN_PLATFORMS: Array<{
|
||||
value: NewProjectPlatform;
|
||||
label: string;
|
||||
hint: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'responsive',
|
||||
label: 'Responsive web',
|
||||
hint: 'One web experience adapted for desktop, tablet, and mobile browsers',
|
||||
},
|
||||
{
|
||||
value: 'web-desktop',
|
||||
label: 'Desktop web',
|
||||
hint: 'Browser-first product or landing page',
|
||||
},
|
||||
{
|
||||
value: 'mobile-ios',
|
||||
label: 'iOS app',
|
||||
hint: 'iPhone frames and iOS interaction rules',
|
||||
},
|
||||
{
|
||||
value: 'mobile-android',
|
||||
label: 'Android app',
|
||||
hint: 'Pixel frames and Material interaction rules',
|
||||
},
|
||||
{
|
||||
value: 'tablet',
|
||||
label: 'Tablet app',
|
||||
hint: 'Native-style tablet experience with split views',
|
||||
},
|
||||
{
|
||||
value: 'desktop-app',
|
||||
label: 'Desktop app',
|
||||
hint: 'macOS/Windows app chrome',
|
||||
},
|
||||
];
|
||||
|
||||
export type CreateTab = 'prototype' | 'live-artifact' | 'deck' | 'template' | 'image' | 'video' | 'audio' | 'other';
|
||||
|
||||
export interface CreateInput {
|
||||
|
|
@ -198,6 +238,9 @@ export function NewProjectPanel({
|
|||
const [fidelity, setFidelity] = useState<'wireframe' | 'high-fidelity'>(
|
||||
'high-fidelity',
|
||||
);
|
||||
const [platformTargets, setPlatformTargets] = useState<NewProjectPlatform[]>(['responsive']);
|
||||
const [includeLandingPage, setIncludeLandingPage] = useState(false);
|
||||
const [includeOsWidgets, setIncludeOsWidgets] = useState(false);
|
||||
const [speakerNotes, setSpeakerNotes] = useState(false);
|
||||
const [animations, setAnimations] = useState(false);
|
||||
const [templateId, setTemplateId] = useState<string | null>(null);
|
||||
|
|
@ -449,6 +492,9 @@ export function NewProjectPanel({
|
|||
const metadata = buildMetadata({
|
||||
tab,
|
||||
fidelity,
|
||||
platformTargets,
|
||||
includeLandingPage,
|
||||
includeOsWidgets,
|
||||
speakerNotes,
|
||||
animations,
|
||||
templateId,
|
||||
|
|
@ -626,6 +672,20 @@ export function NewProjectPanel({
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{tab === 'prototype' || tab === 'live-artifact' || tab === 'template' || tab === 'other' ? (
|
||||
<PlatformPicker value={platformTargets} onChange={setPlatformTargets} />
|
||||
) : null}
|
||||
|
||||
{tab === 'prototype' || tab === 'live-artifact' || tab === 'template' || tab === 'other' ? (
|
||||
<SurfaceOptions
|
||||
includeLandingPage={includeLandingPage}
|
||||
includeOsWidgets={includeOsWidgets}
|
||||
osWidgetsAvailable={platformTargetsSupportOsWidgets(platformTargets)}
|
||||
onIncludeLandingPage={setIncludeLandingPage}
|
||||
onIncludeOsWidgets={setIncludeOsWidgets}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{tab === 'prototype' || tab === 'live-artifact' ? (
|
||||
<FidelityPicker value={fidelity} onChange={setFidelity} />
|
||||
) : null}
|
||||
|
|
@ -790,6 +850,88 @@ export function NewProjectPanel({
|
|||
);
|
||||
}
|
||||
|
||||
function PlatformPicker({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: NewProjectPlatform[];
|
||||
onChange: (v: NewProjectPlatform[]) => void;
|
||||
}) {
|
||||
function togglePlatform(next: NewProjectPlatform) {
|
||||
const active = value.includes(next);
|
||||
const updated = active
|
||||
? value.filter((item) => item !== next)
|
||||
: [...value, next];
|
||||
onChange(updated.length > 0 ? updated : ['responsive']);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="newproj-section">
|
||||
<label className="newproj-label">Target platforms</label>
|
||||
<p className="platform-picker-hint">
|
||||
Pick one or more. Responsive web covers browser breakpoints only; add iOS,
|
||||
Android, tablet app, or desktop app for native cross-platform variants.
|
||||
</p>
|
||||
<div className="platform-grid">
|
||||
{DESIGN_PLATFORMS.map((option) => {
|
||||
const active = value.includes(option.value);
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`newproj-card platform-card${active ? ' active' : ''}`}
|
||||
onClick={() => togglePlatform(option.value)}
|
||||
title={option.hint}
|
||||
aria-pressed={active}
|
||||
>
|
||||
<span className="platform-card-title">{option.label}</span>
|
||||
<span className="platform-card-hint">{option.hint}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SurfaceOptions({
|
||||
includeLandingPage,
|
||||
includeOsWidgets,
|
||||
osWidgetsAvailable,
|
||||
onIncludeLandingPage,
|
||||
onIncludeOsWidgets,
|
||||
}: {
|
||||
includeLandingPage: boolean;
|
||||
includeOsWidgets: boolean;
|
||||
osWidgetsAvailable: boolean;
|
||||
onIncludeLandingPage: (v: boolean) => void;
|
||||
onIncludeOsWidgets: (v: boolean) => void;
|
||||
}) {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="newproj-section surface-options">
|
||||
<label className="newproj-label">{t('newproj.surfaceOptionsLabel')}</label>
|
||||
<ToggleRow
|
||||
label={t('newproj.includeLandingPage')}
|
||||
hint={t('newproj.includeLandingPageHint')}
|
||||
checked={includeLandingPage}
|
||||
onChange={onIncludeLandingPage}
|
||||
/>
|
||||
<ToggleRow
|
||||
label={t('newproj.includeOsWidgets')}
|
||||
hint={
|
||||
osWidgetsAvailable
|
||||
? t('newproj.includeOsWidgetsHint')
|
||||
: t('newproj.includeOsWidgetsDisabledHint')
|
||||
}
|
||||
checked={osWidgetsAvailable && includeOsWidgets}
|
||||
disabled={!osWidgetsAvailable}
|
||||
onChange={onIncludeOsWidgets}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FidelityPicker({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -984,18 +1126,21 @@ function ToggleRow({
|
|||
hint,
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
hint?: string;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`toggle-row${checked ? ' on' : ''}`}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`toggle-row${checked ? ' on' : ''}${disabled ? ' disabled' : ''}`}
|
||||
onClick={() => { if (!disabled) onChange(!checked); }}
|
||||
aria-pressed={checked}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="toggle-row-text">
|
||||
<span className="toggle-row-label">{label}</span>
|
||||
|
|
@ -2039,6 +2184,9 @@ function OptionCards<T extends string | number>({
|
|||
function buildMetadata(input: {
|
||||
tab: CreateTab;
|
||||
fidelity: 'wireframe' | 'high-fidelity';
|
||||
platformTargets: NewProjectPlatform[];
|
||||
includeLandingPage: boolean;
|
||||
includeOsWidgets: boolean;
|
||||
speakerNotes: boolean;
|
||||
animations: boolean;
|
||||
templateId: string | null;
|
||||
|
|
@ -2057,12 +2205,25 @@ function buildMetadata(input: {
|
|||
promptTemplate: PromptTemplatePick | null;
|
||||
}): ProjectMetadata {
|
||||
const kind: ProjectKind = input.tab === 'live-artifact' ? 'prototype' : input.tab;
|
||||
const selectedPlatforms = normalizeSelectedPlatforms(input.platformTargets);
|
||||
const concreteTargets = platformTargetsFor(selectedPlatforms);
|
||||
const canIncludeOsWidgets = platformTargetsSupportOsWidgets(concreteTargets);
|
||||
const surfaceOptions = {
|
||||
...(input.includeLandingPage ? { includeLandingPage: true } : {}),
|
||||
...(input.includeOsWidgets && canIncludeOsWidgets ? { includeOsWidgets: true } : {}),
|
||||
};
|
||||
const base = {
|
||||
platform: selectedPlatforms[0],
|
||||
platformTargets: concreteTargets,
|
||||
...surfaceOptions,
|
||||
};
|
||||
const inspirations = input.inspirationIds.length > 0
|
||||
? { inspirationDesignSystemIds: input.inspirationIds }
|
||||
: {};
|
||||
if (input.tab === 'prototype' || input.tab === 'live-artifact') {
|
||||
return {
|
||||
kind,
|
||||
...base,
|
||||
fidelity: input.fidelity,
|
||||
...(input.tab === 'live-artifact' ? { intent: 'live-artifact' as const } : {}),
|
||||
...inspirations,
|
||||
|
|
@ -2073,13 +2234,14 @@ function buildMetadata(input: {
|
|||
}
|
||||
if (input.tab === 'template') {
|
||||
if (input.templateId == null) {
|
||||
return { kind, animations: input.animations, ...inspirations };
|
||||
return { kind, ...base, animations: input.animations, ...inspirations };
|
||||
}
|
||||
const tpl = input.templates.find((x) => x.id === input.templateId);
|
||||
// The fallback label is consumed by the agent prompt rather than the
|
||||
// UI, so we keep it in English to match the rest of the prompt corpus.
|
||||
return {
|
||||
kind,
|
||||
...base,
|
||||
animations: input.animations,
|
||||
templateId: input.templateId,
|
||||
templateLabel: tpl?.name ?? 'Saved template',
|
||||
|
|
@ -2116,7 +2278,56 @@ function buildMetadata(input: {
|
|||
...inspirations,
|
||||
};
|
||||
}
|
||||
return { kind: 'other', ...inspirations };
|
||||
return { kind: 'other', ...base, ...inspirations };
|
||||
}
|
||||
|
||||
function normalizeSelectedPlatforms(platforms: NewProjectPlatform[]): NewProjectPlatform[] {
|
||||
const seen = new Set<NewProjectPlatform>();
|
||||
for (const platform of platforms) {
|
||||
if (DESIGN_PLATFORMS.some((option) => option.value === platform)) {
|
||||
seen.add(platform);
|
||||
}
|
||||
}
|
||||
return seen.size > 0 ? [...seen] : ['responsive'];
|
||||
}
|
||||
|
||||
function platformTargetsSupportOsWidgets(platforms: ProjectPlatform[] | NewProjectPlatform[]): boolean {
|
||||
return platforms.some((platform) =>
|
||||
platform === 'mobile-ios'
|
||||
|| platform === 'mobile-android'
|
||||
|| platform === 'tablet',
|
||||
);
|
||||
}
|
||||
|
||||
function platformTargetsFor(platforms: NewProjectPlatform[]): ProjectPlatform[] {
|
||||
const targets = new Set<ProjectPlatform>();
|
||||
for (const platform of platforms) {
|
||||
switch (platform) {
|
||||
case 'responsive':
|
||||
targets.add('responsive');
|
||||
break;
|
||||
case 'web-desktop':
|
||||
targets.add('web-desktop');
|
||||
break;
|
||||
case 'mobile-ios':
|
||||
targets.add('mobile-ios');
|
||||
break;
|
||||
case 'mobile-android':
|
||||
targets.add('mobile-android');
|
||||
break;
|
||||
case 'tablet':
|
||||
targets.add('tablet');
|
||||
break;
|
||||
case 'desktop-app':
|
||||
targets.add('desktop-app');
|
||||
break;
|
||||
default: {
|
||||
const exhaustive: never = platform;
|
||||
targets.add(exhaustive);
|
||||
}
|
||||
}
|
||||
}
|
||||
return targets.size > 0 ? [...targets] : ['responsive'];
|
||||
}
|
||||
|
||||
function buildPromptTemplateMetadata(
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ import type {
|
|||
PreviewComment,
|
||||
PreviewCommentTarget,
|
||||
ProjectFile,
|
||||
ProjectPlatform,
|
||||
ProjectTemplate,
|
||||
LiveArtifactEventItem,
|
||||
LiveArtifactSummary,
|
||||
|
|
@ -239,6 +240,56 @@ function projectEventToAgentEvent(evt: ProjectEvent): LiveArtifactEventItem['eve
|
|||
};
|
||||
}
|
||||
|
||||
const PLATFORM_LABELS: Record<ProjectPlatform, string> = {
|
||||
auto: 'Auto',
|
||||
responsive: 'Responsive web',
|
||||
'web-desktop': 'Desktop web',
|
||||
'mobile-ios': 'iOS app',
|
||||
'mobile-android': 'Android app',
|
||||
tablet: 'Tablet app',
|
||||
'desktop-app': 'Desktop app',
|
||||
};
|
||||
|
||||
function labelProjectPlatform(platform: ProjectPlatform | string): string {
|
||||
return PLATFORM_LABELS[platform as ProjectPlatform] ?? platform;
|
||||
}
|
||||
|
||||
function projectTargetPlatforms(project: Project): string[] {
|
||||
const targets = project.metadata?.platformTargets;
|
||||
if (Array.isArray(targets) && targets.length > 0) {
|
||||
return [...new Set(targets)].map(labelProjectPlatform);
|
||||
}
|
||||
if (project.metadata?.platform) {
|
||||
return [labelProjectPlatform(project.metadata.platform)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
type ProjectFeatureChip = {
|
||||
label: string;
|
||||
title: string;
|
||||
tone: 'landing' | 'widgets';
|
||||
};
|
||||
|
||||
function projectFeatureChips(project: Project): ProjectFeatureChip[] {
|
||||
const chips: ProjectFeatureChip[] = [];
|
||||
if (project.metadata?.includeLandingPage) {
|
||||
chips.push({
|
||||
label: 'Landing page',
|
||||
title: 'Landing page companion surface is enabled for this project',
|
||||
tone: 'landing',
|
||||
});
|
||||
}
|
||||
if (project.metadata?.includeOsWidgets) {
|
||||
chips.push({
|
||||
label: 'OS widgets',
|
||||
title: 'Home-screen, lock-screen, or quick-access OS widget surfaces are enabled',
|
||||
tone: 'widgets',
|
||||
});
|
||||
}
|
||||
return chips;
|
||||
}
|
||||
|
||||
export function ProjectView({
|
||||
project,
|
||||
routeFileName,
|
||||
|
|
@ -1705,18 +1756,17 @@ export function ProjectView({
|
|||
// this for tool-emitted files; this handles the artifact-tag path.
|
||||
requestOpenFile(file.name);
|
||||
} else {
|
||||
// writeProjectTextFile collapses non-OK responses (including
|
||||
// 422 ARTIFACT_REGRESSION from reject-mode stub-guard) to null.
|
||||
// Surfacing the structured error requires changing that helper's
|
||||
// return contract for all callers — out of scope here. Until then,
|
||||
// a generic banner makes the failure observable instead of silent.
|
||||
// Allow the user to retry by clearing the saved-artifact ref so a
|
||||
// retry attempt re-enters this code path.
|
||||
// writeProjectTextFile collapses all failure paths (non-OK HTTP
|
||||
// responses, network errors, and stub-guard 422s) to null — the
|
||||
// helper's return contract would need to be widened to distinguish
|
||||
// them, which is out of scope here. Show a generic banner so the
|
||||
// failure is observable rather than silent; the daemon logs carry
|
||||
// the structured details for any specific error type.
|
||||
// Clear the saved-artifact ref so the user can retry.
|
||||
savedArtifactRef.current = '';
|
||||
setError(
|
||||
`Couldn't save artifact "${fileName}". The daemon refused the write — ` +
|
||||
'this is most likely OD_ARTIFACT_STUB_GUARD=reject catching a placeholder body. ' +
|
||||
'Check the daemon logs for the structured ARTIFACT_REGRESSION details.',
|
||||
`Couldn't save artifact "${fileName}". The write failed — ` +
|
||||
'check the daemon logs for details.',
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -1937,6 +1987,13 @@ export function ProjectView({
|
|||
return [skill, ds].filter(Boolean).join(' · ') || t('project.metaFreeform');
|
||||
}, [skills, designTemplates, designSystems, project.skillId, project.designSystemId, t]);
|
||||
|
||||
const targetPlatforms = useMemo(() => projectTargetPlatforms(project), [project]);
|
||||
const targetPlatformsLabel = targetPlatforms.join(', ');
|
||||
const visibleTargetPlatforms = targetPlatforms.slice(0, 5);
|
||||
const hiddenTargetPlatformCount = Math.max(0, targetPlatforms.length - visibleTargetPlatforms.length);
|
||||
const featureChips = useMemo(() => projectFeatureChips(project), [project]);
|
||||
const featureChipsLabel = featureChips.map((chip) => chip.label).join(', ');
|
||||
|
||||
const isDeck = useMemo(
|
||||
() =>
|
||||
(skills.find((s) => s.id === project.skillId) ??
|
||||
|
|
@ -2266,6 +2323,7 @@ export function ProjectView({
|
|||
)}
|
||||
>
|
||||
<div className="app-project-title">
|
||||
<span className="app-project-title-line">
|
||||
<span
|
||||
className="title editable"
|
||||
data-testid="project-title"
|
||||
|
|
@ -2284,6 +2342,44 @@ export function ProjectView({
|
|||
{project.name}
|
||||
</span>
|
||||
<span className="meta" data-testid="project-meta">{projectMeta}</span>
|
||||
</span>
|
||||
{targetPlatforms.length > 0 ? (
|
||||
<span
|
||||
className="project-target-platforms"
|
||||
data-testid="project-target-platforms"
|
||||
title={`Target platforms: ${targetPlatformsLabel}`}
|
||||
>
|
||||
<span className="project-target-platforms-label">Targets</span>
|
||||
{visibleTargetPlatforms.map((platform) => (
|
||||
<span className="project-target-platform-chip" key={platform}>
|
||||
{platform}
|
||||
</span>
|
||||
))}
|
||||
{hiddenTargetPlatformCount > 0 ? (
|
||||
<span className="project-target-platform-chip is-count">
|
||||
+{hiddenTargetPlatformCount}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
{featureChips.length > 0 ? (
|
||||
<span
|
||||
className="project-feature-chips"
|
||||
data-testid="project-feature-chips"
|
||||
title={`Enabled design outputs: ${featureChipsLabel}`}
|
||||
>
|
||||
<span className="project-feature-chips-label">Includes</span>
|
||||
{featureChips.map((chip) => (
|
||||
<span
|
||||
className={`project-feature-chip is-${chip.tone}`}
|
||||
key={chip.tone}
|
||||
title={chip.title}
|
||||
>
|
||||
{chip.label}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</AppChromeHeader>
|
||||
<ProjectActionsToolbar
|
||||
|
|
|
|||
|
|
@ -288,6 +288,19 @@ const AGENT_CLI_ENV_FIELDS = [
|
|||
labelKey: 'settings.cliEnvClaudeConfigDir',
|
||||
placeholder: '~/.claude-2',
|
||||
},
|
||||
{
|
||||
agentId: 'claude',
|
||||
envKey: 'ANTHROPIC_BASE_URL',
|
||||
labelKey: 'settings.cliEnvClaudeBaseUrl',
|
||||
placeholder: 'https://your-proxy.example.com',
|
||||
},
|
||||
{
|
||||
agentId: 'claude',
|
||||
envKey: 'ANTHROPIC_API_KEY',
|
||||
labelKey: 'settings.cliEnvClaudeApiKey',
|
||||
placeholder: 'Paste proxy API key',
|
||||
secret: true,
|
||||
},
|
||||
{
|
||||
agentId: 'codex',
|
||||
envKey: 'CODEX_HOME',
|
||||
|
|
@ -300,6 +313,19 @@ const AGENT_CLI_ENV_FIELDS = [
|
|||
labelKey: 'settings.cliEnvCodexBin',
|
||||
placeholder: '/absolute/path/to/codex',
|
||||
},
|
||||
{
|
||||
agentId: 'codex',
|
||||
envKey: 'OPENAI_BASE_URL',
|
||||
labelKey: 'settings.cliEnvCodexBaseUrl',
|
||||
placeholder: 'https://your-proxy.example.com/v1',
|
||||
},
|
||||
{
|
||||
agentId: 'codex',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
labelKey: 'settings.cliEnvCodexApiKey',
|
||||
placeholder: 'Paste proxy API key',
|
||||
secret: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
function defaultApiProtocolConfig(protocol: ApiProtocol): ApiProtocolConfig {
|
||||
|
|
@ -2033,10 +2059,11 @@ export function SettingsDialog({
|
|||
<label className="field" key={`${field.agentId}:${field.envKey}`}>
|
||||
<span className="field-label">{t(field.labelKey)}</span>
|
||||
<input
|
||||
type="text"
|
||||
type={'secret' in field && field.secret ? 'password' : 'text'}
|
||||
value={cfg.agentCliEnv?.[field.agentId]?.[field.envKey] ?? ''}
|
||||
placeholder={field.placeholder}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
onChange={(e) =>
|
||||
setCfg((c) =>
|
||||
updateAgentCliEnvValue(
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const ar: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'مخصص (اكتب أدناه)...',
|
||||
'settings.modelCustomLabel': 'معرف النموذج المخصص',
|
||||
'settings.modelCustomPlaceholder': 'مثلاً: anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -791,6 +795,13 @@ export const ar: Dict = {
|
|||
'fileViewer.zoomOut': 'تصغير',
|
||||
'fileViewer.zoomIn': 'تكبير',
|
||||
'fileViewer.resetZoom': 'إعادة تعيين الزوم',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'إعادة تحميل',
|
||||
'fileViewer.previousSlide': 'الشريحة السابقة',
|
||||
'fileViewer.nextSlide': 'الشريحة التالية',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const de: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Benutzerdefiniert (unten eingeben)…',
|
||||
'settings.modelCustomLabel': 'Benutzerdefinierte Modell-ID',
|
||||
'settings.modelCustomPlaceholder': 'z. B. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -679,6 +683,13 @@ export const de: Dict = {
|
|||
'fileViewer.zoomOut': 'Verkleinern',
|
||||
'fileViewer.zoomIn': 'Vergrößern',
|
||||
'fileViewer.resetZoom': 'Zoom zurücksetzen',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Neu laden',
|
||||
'fileViewer.previousSlide': 'Vorherige Slide',
|
||||
'fileViewer.nextSlide': 'Nächste Slide',
|
||||
|
|
|
|||
|
|
@ -141,12 +141,16 @@ export const en: Dict = {
|
|||
'settings.reasoningPicker': 'Reasoning effort',
|
||||
'settings.modelPickerHint':
|
||||
'Fetched from the CLI when it exposes a `models` command. "Default" leaves the choice to the CLI’s own config; "Custom…" lets you type any model id the CLI accepts.',
|
||||
'settings.cliEnvTitle': 'CLI config locations',
|
||||
'settings.cliEnvTitle': 'CLI proxy and config',
|
||||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'Optional per-agent environment for packaged app runs, detection, and local proxy auth. Secrets are stored in local app config and only passed to the selected CLI.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Custom (type below)…',
|
||||
'settings.modelCustomLabel': 'Custom model id',
|
||||
'settings.modelCustomPlaceholder': 'e.g. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -398,6 +402,15 @@ export const en: Dict = {
|
|||
'newproj.toggleAnimations': 'Include animations',
|
||||
'newproj.toggleAnimationsHint':
|
||||
'Add motion (entrance, hover, transitions) on top of the template.',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': 'Template',
|
||||
'newproj.noTemplatesTitle': 'No templates yet',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -860,6 +873,13 @@ export const en: Dict = {
|
|||
'fileViewer.zoomOut': 'Zoom out',
|
||||
'fileViewer.zoomIn': 'Zoom in',
|
||||
'fileViewer.resetZoom': 'Reset zoom',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Reload',
|
||||
'fileViewer.previousSlide': 'Previous slide',
|
||||
'fileViewer.nextSlide': 'Next slide',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const esES: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Personalizado (escribe abajo)…',
|
||||
'settings.modelCustomLabel': 'Id de modelo personalizado',
|
||||
'settings.modelCustomPlaceholder': 'p. ej., anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -680,6 +684,13 @@ export const esES: Dict = {
|
|||
'fileViewer.zoomOut': 'Reducir zoom',
|
||||
'fileViewer.zoomIn': 'Aumentar zoom',
|
||||
'fileViewer.resetZoom': 'Restablecer zoom',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Recargar',
|
||||
'fileViewer.previousSlide': 'Diapositiva anterior',
|
||||
'fileViewer.nextSlide': 'Diapositiva siguiente',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const fa: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'سفارشی (در زیر تایپ کنید)…',
|
||||
'settings.modelCustomLabel': 'شناسه مدل سفارشی',
|
||||
'settings.modelCustomPlaceholder': 'مثلاً anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -345,6 +349,15 @@ export const fa: Dict = {
|
|||
'newproj.toggleAnimations': 'افزودن انیمیشن',
|
||||
'newproj.toggleAnimationsHint':
|
||||
'افزودن حرکت (ورود، هاور، انتقال) بر روی قالب.',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': 'قالب',
|
||||
'newproj.noTemplatesTitle': 'هنوز هیچ قالبی وجود ندارد',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -806,6 +819,13 @@ export const fa: Dict = {
|
|||
'fileViewer.zoomOut': 'کوچکنمایی',
|
||||
'fileViewer.zoomIn': 'بزرگنمایی',
|
||||
'fileViewer.resetZoom': 'بازنشانی زوم',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'بارگذاری مجدد',
|
||||
'fileViewer.previousSlide': 'اسلاید قبلی',
|
||||
'fileViewer.nextSlide': 'اسلاید بعدی',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const fr: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Personnalisé (saisir ci-dessous)…',
|
||||
'settings.modelCustomLabel': 'Identifiant du modèle personnalisé',
|
||||
'settings.modelCustomPlaceholder': 'ex. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -791,6 +795,13 @@ export const fr: Dict = {
|
|||
'fileViewer.zoomOut': 'Zoom arrière',
|
||||
'fileViewer.zoomIn': 'Zoom avant',
|
||||
'fileViewer.resetZoom': 'Réinitialiser le zoom',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Recharger',
|
||||
'fileViewer.previousSlide': 'Diapositive précédente',
|
||||
'fileViewer.nextSlide': 'Diapositive suivante',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const hu: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Egyedi (gépeld be alább)…',
|
||||
'settings.modelCustomLabel': 'Egyedi modell-id',
|
||||
'settings.modelCustomPlaceholder': 'pl. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -791,6 +795,13 @@ export const hu: Dict = {
|
|||
'fileViewer.zoomOut': 'Kicsinyítés',
|
||||
'fileViewer.zoomIn': 'Nagyítás',
|
||||
'fileViewer.resetZoom': 'Nagyítás visszaállítása',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Újratöltés',
|
||||
'fileViewer.previousSlide': 'Előző dia',
|
||||
'fileViewer.nextSlide': 'Következő dia',
|
||||
|
|
|
|||
|
|
@ -137,8 +137,12 @@ export const id: Dict = {
|
|||
'settings.cliEnvTitle': 'Lokasi konfigurasi CLI',
|
||||
'settings.cliEnvHint': 'Atur direktori konfigurasi non-rahasia untuk menjalankan aplikasi paket dan deteksi agent.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Direktori konfigurasi Claude Code',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Home Codex',
|
||||
'settings.cliEnvCodexBin': 'Path executable Codex',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Kustom (isi di bawah)...',
|
||||
'settings.modelCustomLabel': 'ID model kustom',
|
||||
'settings.modelCustomPlaceholder': 'mis. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -442,6 +446,15 @@ export const id: Dict = {
|
|||
'newproj.toggleSpeakerNotesHint': 'Kurangi teks di slide; simpan poin bicara di catatan.',
|
||||
'newproj.toggleAnimations': 'Sertakan animasi',
|
||||
'newproj.toggleAnimationsHint': 'Tambahkan motion, hover, dan transisi di atas templat.',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': 'Templat',
|
||||
'newproj.noTemplatesTitle': 'Belum ada templat',
|
||||
'newproj.noTemplatesBody': 'Buka proyek apa pun, lalu pakai menu Share di penampil berkas untuk mengubahnya menjadi templat.',
|
||||
|
|
@ -901,6 +914,13 @@ export const id: Dict = {
|
|||
'fileViewer.zoomOut': 'Perkecil',
|
||||
'fileViewer.zoomIn': 'Perbesar',
|
||||
'fileViewer.resetZoom': 'Atur ulang zoom',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Muat ulang file',
|
||||
'fileViewer.previousSlide': 'Slide sebelumnya',
|
||||
'fileViewer.nextSlide': 'Slide berikutnya',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const ja: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'カスタム(下に入力)…',
|
||||
'settings.modelCustomLabel': 'カスタムモデル ID',
|
||||
'settings.modelCustomPlaceholder': '例: anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -678,6 +682,13 @@ export const ja: Dict = {
|
|||
'fileViewer.zoomOut': 'ズームアウト',
|
||||
'fileViewer.zoomIn': 'ズームイン',
|
||||
'fileViewer.resetZoom': 'ズームをリセット',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': '再読み込み',
|
||||
'fileViewer.previousSlide': '前のスライド',
|
||||
'fileViewer.nextSlide': '次のスライド',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const ko: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': '직접 입력…',
|
||||
'settings.modelCustomLabel': '사용자 지정 모델 ID',
|
||||
'settings.modelCustomPlaceholder': '예: anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -791,6 +795,13 @@ export const ko: Dict = {
|
|||
'fileViewer.zoomOut': '축소',
|
||||
'fileViewer.zoomIn': '확대',
|
||||
'fileViewer.resetZoom': '배율 초기화',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': '새로고침',
|
||||
'fileViewer.previousSlide': '이전 슬라이드',
|
||||
'fileViewer.nextSlide': '다음 슬라이드',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const pl: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Własny (wpisz poniżej)…',
|
||||
'settings.modelCustomLabel': 'Własne ID modelu',
|
||||
'settings.modelCustomPlaceholder': 'np. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -791,6 +795,13 @@ export const pl: Dict = {
|
|||
'fileViewer.zoomOut': 'Pomniejsz',
|
||||
'fileViewer.zoomIn': 'Powiększ',
|
||||
'fileViewer.resetZoom': 'Resetuj powiększenie',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Odśwież',
|
||||
'fileViewer.previousSlide': 'Poprzedni slajd',
|
||||
'fileViewer.nextSlide': 'Następny slajd',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const ptBR: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Personalizado (digite abaixo)…',
|
||||
'settings.modelCustomLabel': 'Id do modelo personalizado',
|
||||
'settings.modelCustomPlaceholder': 'ex.: anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -343,6 +347,15 @@ export const ptBR: Dict = {
|
|||
'newproj.toggleAnimations': 'Incluir animações',
|
||||
'newproj.toggleAnimationsHint':
|
||||
'Adicionar movimento (entrada, hover, transições) sobre o template.',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': 'Template',
|
||||
'newproj.noTemplatesTitle': 'Ainda não há templates',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -805,6 +818,13 @@ export const ptBR: Dict = {
|
|||
'fileViewer.zoomOut': 'Diminuir zoom',
|
||||
'fileViewer.zoomIn': 'Aumentar zoom',
|
||||
'fileViewer.resetZoom': 'Redefinir zoom',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Recarregar',
|
||||
'fileViewer.previousSlide': 'Slide anterior',
|
||||
'fileViewer.nextSlide': 'Próximo slide',
|
||||
|
|
|
|||
|
|
@ -138,8 +138,12 @@ export const ru: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Пользовательская (введите ниже)…',
|
||||
'settings.modelCustomLabel': 'Пользовательский ID модели',
|
||||
'settings.modelCustomPlaceholder': 'например, anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -343,6 +347,15 @@ export const ru: Dict = {
|
|||
'newproj.toggleAnimations': 'Включить анимации',
|
||||
'newproj.toggleAnimationsHint':
|
||||
'Добавить анимации (появление, наведение, переходы) поверх шаблона.',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': 'Шаблон',
|
||||
'newproj.noTemplatesTitle': 'Шаблонов пока нет',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -805,6 +818,13 @@ export const ru: Dict = {
|
|||
'fileViewer.zoomOut': 'Уменьшить',
|
||||
'fileViewer.zoomIn': 'Увеличить',
|
||||
'fileViewer.resetZoom': 'Сбросить масштаб',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Перезагрузить',
|
||||
'fileViewer.previousSlide': 'Предыдущий слайд',
|
||||
'fileViewer.nextSlide': 'Следующий слайд',
|
||||
|
|
|
|||
|
|
@ -782,6 +782,13 @@ export const tr: Dict = {
|
|||
'fileViewer.zoomOut': 'Yakınlaş',
|
||||
'fileViewer.zoomIn': 'Uzaklaş',
|
||||
'fileViewer.resetZoom': 'Uzaklığı sıfırla',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Yeniden yükle',
|
||||
'fileViewer.previousSlide': 'Önceki slayt',
|
||||
'fileViewer.nextSlide': 'Sonraki slayt',
|
||||
|
|
|
|||
|
|
@ -139,8 +139,12 @@ export const uk: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'Set non-secret config directories for packaged app runs and agent detection.',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex executable path',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': 'Власна (введіть нижче)…',
|
||||
'settings.modelCustomLabel': 'Власне ID моделі',
|
||||
'settings.modelCustomPlaceholder': 'напр. anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -345,6 +349,15 @@ export const uk: Dict = {
|
|||
'newproj.toggleAnimations': 'Включити анімації',
|
||||
'newproj.toggleAnimationsHint':
|
||||
'Додати рух (вхід, наведення, переходи) поверх шаблону.',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': 'Шаблон',
|
||||
'newproj.noTemplatesTitle': 'Шаблони ще не створені',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -824,6 +837,13 @@ export const uk: Dict = {
|
|||
'fileViewer.zoomOut': 'Зменшити',
|
||||
'fileViewer.zoomIn': 'Збільшити',
|
||||
'fileViewer.resetZoom': 'Скинути масштаб',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': 'Перезавантажити',
|
||||
'fileViewer.previousSlide': 'Попередній слайд',
|
||||
'fileViewer.nextSlide': 'Наступний слайд',
|
||||
|
|
|
|||
|
|
@ -144,8 +144,12 @@ export const zhCN: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'为打包版应用运行和 agent 检测设置非敏感配置目录。',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code 配置目录',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex 可执行文件路径',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': '自定义(在下方填写)…',
|
||||
'settings.modelCustomLabel': '自定义模型 id',
|
||||
'settings.modelCustomPlaceholder': '例如 anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -395,6 +399,15 @@ export const zhCN: Dict = {
|
|||
'newproj.toggleSpeakerNotesHint': '减少幻灯片上的文字,要点放到备注中。',
|
||||
'newproj.toggleAnimations': '加入动效',
|
||||
'newproj.toggleAnimationsHint': '在模板基础上叠加动效(入场、悬停、过渡)。',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': '模板',
|
||||
'newproj.noTemplatesTitle': '还没有模板',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -849,6 +862,13 @@ export const zhCN: Dict = {
|
|||
'fileViewer.zoomOut': '缩小',
|
||||
'fileViewer.zoomIn': '放大',
|
||||
'fileViewer.resetZoom': '重置缩放',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': '重新加载',
|
||||
'fileViewer.previousSlide': '上一张',
|
||||
'fileViewer.nextSlide': '下一张',
|
||||
|
|
|
|||
|
|
@ -137,8 +137,12 @@ export const zhTW: Dict = {
|
|||
'settings.cliEnvHint':
|
||||
'為打包版應用執行和 agent 偵測設定非敏感設定目錄。',
|
||||
'settings.cliEnvClaudeConfigDir': 'Claude Code 設定目錄',
|
||||
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
|
||||
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
|
||||
'settings.cliEnvCodexHome': 'Codex home',
|
||||
'settings.cliEnvCodexBin': 'Codex 可執行檔路徑',
|
||||
'settings.cliEnvCodexBaseUrl': 'Codex/OpenAI proxy base URL',
|
||||
'settings.cliEnvCodexApiKey': 'Codex/OpenAI proxy API key',
|
||||
'settings.modelCustom': '自訂(在下方填寫)…',
|
||||
'settings.modelCustomLabel': '自訂模型 id',
|
||||
'settings.modelCustomPlaceholder': '例如 anthropic/claude-sonnet-4-6',
|
||||
|
|
@ -388,6 +392,15 @@ export const zhTW: Dict = {
|
|||
'newproj.toggleSpeakerNotesHint': '減少投影片上的文字,重點放到備忘稿中。',
|
||||
'newproj.toggleAnimations': '加入動畫效果',
|
||||
'newproj.toggleAnimationsHint': '在範本基礎上疊加動畫效果(入場、懸停、過渡)。',
|
||||
'newproj.surfaceOptionsLabel': 'Companion surfaces',
|
||||
'newproj.includeLandingPage': 'Include landing page',
|
||||
'newproj.includeLandingPageHint':
|
||||
'Add a responsive marketing page for ads, waitlists, launch campaigns, app downloads, or product explanation.',
|
||||
'newproj.includeOsWidgets': 'Include OS widgets',
|
||||
'newproj.includeOsWidgetsHint':
|
||||
'Add platform-native home screen, lock screen, or quick-access widgets for mobile/tablet apps.',
|
||||
'newproj.includeOsWidgetsDisabledHint':
|
||||
'Available when iOS, Android, or tablet app is selected as a target platform.',
|
||||
'newproj.templateLabel': '範本',
|
||||
'newproj.noTemplatesTitle': '還沒有範本',
|
||||
'newproj.noTemplatesBody':
|
||||
|
|
@ -842,6 +855,13 @@ export const zhTW: Dict = {
|
|||
'fileViewer.zoomOut': '縮小',
|
||||
'fileViewer.zoomIn': '放大',
|
||||
'fileViewer.resetZoom': '重設縮放',
|
||||
'fileViewer.viewportAria': 'Preview viewport',
|
||||
'fileViewer.viewportDesktop': 'Desktop',
|
||||
'fileViewer.viewportDesktopTitle': 'Full-width desktop preview',
|
||||
'fileViewer.viewportTablet': 'Tablet',
|
||||
'fileViewer.viewportTabletTitle': 'Tablet preview at 820 × 1180 (modern portrait baseline)',
|
||||
'fileViewer.viewportMobile': 'Mobile',
|
||||
'fileViewer.viewportMobileTitle': 'Mobile preview at 390 × 844',
|
||||
'fileViewer.reloadAria': '重新載入',
|
||||
'fileViewer.previousSlide': '上一張',
|
||||
'fileViewer.nextSlide': '下一張',
|
||||
|
|
|
|||
|
|
@ -165,8 +165,12 @@ export interface Dict {
|
|||
'settings.cliEnvTitle': string;
|
||||
'settings.cliEnvHint': string;
|
||||
'settings.cliEnvClaudeConfigDir': string;
|
||||
'settings.cliEnvClaudeBaseUrl': string;
|
||||
'settings.cliEnvClaudeApiKey': string;
|
||||
'settings.cliEnvCodexHome': string;
|
||||
'settings.cliEnvCodexBin': string;
|
||||
'settings.cliEnvCodexBaseUrl': string;
|
||||
'settings.cliEnvCodexApiKey': string;
|
||||
'settings.modelCustom': string;
|
||||
'settings.modelCustomLabel': string;
|
||||
'settings.modelCustomPlaceholder': string;
|
||||
|
|
@ -632,6 +636,12 @@ export interface Dict {
|
|||
'newproj.toggleSpeakerNotesHint': string;
|
||||
'newproj.toggleAnimations': string;
|
||||
'newproj.toggleAnimationsHint': string;
|
||||
'newproj.surfaceOptionsLabel': string;
|
||||
'newproj.includeLandingPage': string;
|
||||
'newproj.includeLandingPageHint': string;
|
||||
'newproj.includeOsWidgets': string;
|
||||
'newproj.includeOsWidgetsHint': string;
|
||||
'newproj.includeOsWidgetsDisabledHint': string;
|
||||
'newproj.templateLabel': string;
|
||||
'newproj.noTemplatesTitle': string;
|
||||
'newproj.noTemplatesBody': string;
|
||||
|
|
@ -1118,6 +1128,13 @@ export interface Dict {
|
|||
'fileViewer.zoomOut': string;
|
||||
'fileViewer.zoomIn': string;
|
||||
'fileViewer.resetZoom': string;
|
||||
'fileViewer.viewportAria': string;
|
||||
'fileViewer.viewportDesktop': string;
|
||||
'fileViewer.viewportDesktopTitle': string;
|
||||
'fileViewer.viewportTablet': string;
|
||||
'fileViewer.viewportTabletTitle': string;
|
||||
'fileViewer.viewportMobile': string;
|
||||
'fileViewer.viewportMobileTitle': string;
|
||||
'fileViewer.reloadAria': string;
|
||||
'fileViewer.previousSlide': string;
|
||||
'fileViewer.nextSlide': string;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&display=swap');
|
||||
|
||||
/* ============================================================
|
||||
Open Design — visual language modeled on claude.ai/design
|
||||
Open Design — neutral product workspace
|
||||
============================================================ */
|
||||
:root {
|
||||
/* Surface palette — warmer paper, hairline borders, soft shadows.
|
||||
The entry view and project view share the same warm cream backdrop
|
||||
(--bg) so transitioning between them feels seamless. --bg-app is kept
|
||||
as an alias for compatibility but now resolves to the same value. */
|
||||
--bg: #faf9f7;
|
||||
--bg-app: #faf9f7;
|
||||
/* Surface palette — neutral app chrome that does not bias generated artifacts.
|
||||
Keep warm/brand colors out of preview backgrounds; generated product UI
|
||||
must choose its own palette through the active skill/design brief. */
|
||||
--bg: #f6f7f9;
|
||||
--bg-app: #f6f7f9;
|
||||
--bg-panel: #ffffff;
|
||||
--bg-subtle: #f4f2ed;
|
||||
--bg-muted: #ece9e2;
|
||||
--bg-subtle: #eef1f5;
|
||||
--bg-muted: #e4e8ef;
|
||||
--bg-elevated: #ffffff;
|
||||
--border: #ebe8e1;
|
||||
--border-strong: #d8d4cb;
|
||||
--border-soft: #f1eee7;
|
||||
--border: #e1e5eb;
|
||||
--border-strong: #c9d0da;
|
||||
--border-soft: #edf0f4;
|
||||
|
||||
--text: #1a1916;
|
||||
--text-strong: #0d0c0a;
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
--text-soft: #989590;
|
||||
--text-faint: #b3b0a8;
|
||||
|
||||
/* Accent — Claude rust/burnt-sienna. */
|
||||
/* Accent — Open Design action color for app chrome only. */
|
||||
--accent: #c96442;
|
||||
--accent-strong: #b45a3b;
|
||||
--accent-soft: #f5d8cb;
|
||||
|
|
@ -305,10 +304,8 @@ code {
|
|||
--app-chrome-traffic-space: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
max-height: 40px;
|
||||
padding: 4px 14px;
|
||||
min-height: 48px;
|
||||
padding: 5px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
gap: 12px;
|
||||
|
|
@ -377,6 +374,8 @@ code {
|
|||
align-items: center;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.app-chrome-drag {
|
||||
min-width: 24px;
|
||||
|
|
@ -526,11 +525,19 @@ code {
|
|||
}
|
||||
.app-project-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
max-height: 22px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-project-title-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-project-title .title {
|
||||
|
|
@ -548,18 +555,74 @@ code {
|
|||
.app-project-title .meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 11.5px;
|
||||
line-height: 18px;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.app-project-title .meta::before {
|
||||
content: '·';
|
||||
margin: 0 8px;
|
||||
.project-target-platforms,
|
||||
.project-feature-chips {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
max-width: min(100%, 280px);
|
||||
height: 22px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.project-feature-chips {
|
||||
max-width: min(100%, 260px);
|
||||
}
|
||||
.project-target-platforms-label,
|
||||
.project-feature-chips-label {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.55;
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.project-target-platform-chip,
|
||||
.project-feature-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
max-width: 92px;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
|
||||
border-radius: 999px;
|
||||
color: var(--text-muted);
|
||||
background: color-mix(in srgb, var(--bg-subtle) 88%, transparent);
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.project-feature-chip.is-landing {
|
||||
color: color-mix(in srgb, var(--accent) 72%, var(--text-strong));
|
||||
border-color: color-mix(in srgb, var(--accent) 26%, transparent);
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg-subtle));
|
||||
}
|
||||
.project-feature-chip.is-widgets {
|
||||
color: color-mix(in srgb, #0891b2 72%, var(--text-strong));
|
||||
border-color: color-mix(in srgb, #0891b2 26%, transparent);
|
||||
background: color-mix(in srgb, #0891b2 10%, var(--bg-subtle));
|
||||
}
|
||||
.project-target-platform-chip.is-count {
|
||||
min-width: 28px;
|
||||
max-width: 36px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-strong);
|
||||
background: color-mix(in srgb, var(--accent, #7c5cff) 12%, var(--bg-subtle));
|
||||
}
|
||||
|
||||
.topbar {
|
||||
|
|
@ -3865,6 +3928,35 @@ code {
|
|||
}
|
||||
.newproj-name { width: 100%; }
|
||||
.newproj-section { display: flex; flex-direction: column; gap: 6px; }
|
||||
.platform-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.platform-picker-hint {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.platform-card {
|
||||
min-height: 64px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.platform-card-title {
|
||||
color: var(--text-strong);
|
||||
font-size: 12.5px;
|
||||
font-weight: 650;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.platform-card-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.newproj-label {
|
||||
font-size: 11.5px;
|
||||
color: var(--text-muted);
|
||||
|
|
@ -4030,6 +4122,7 @@ code {
|
|||
@media (max-width: 560px) {
|
||||
.newproj-model-grid,
|
||||
.newproj-option-grid,
|
||||
.platform-grid,
|
||||
.newproj-option-grid.aspect-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
|
@ -4223,6 +4316,14 @@ code {
|
|||
}
|
||||
.toggle-row:hover { border-color: var(--border-strong); }
|
||||
.toggle-row.on { border-color: var(--accent); background: var(--accent-tint); }
|
||||
.toggle-row.disabled,
|
||||
.toggle-row:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.toggle-row.disabled:hover,
|
||||
.toggle-row:disabled:hover { border-color: var(--border); }
|
||||
.toggle-row-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
@ -8609,12 +8710,79 @@ button.connector-action.is-loading {
|
|||
border: none;
|
||||
background: white;
|
||||
}
|
||||
.viewer-viewport-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.viewer-viewport-button {
|
||||
min-height: 26px;
|
||||
padding-inline: 8px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
.viewer-viewport-button.active {
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
background: var(--bg-panel);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
.live-artifact-preview-layer,
|
||||
.comment-preview-layer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.preview-viewport:not(.preview-viewport-desktop) {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
linear-gradient(45deg, color-mix(in srgb, var(--border) 28%, transparent) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, color-mix(in srgb, var(--border) 28%, transparent) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, color-mix(in srgb, var(--border) 28%, transparent) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, color-mix(in srgb, var(--border) 28%, transparent) 75%),
|
||||
var(--bg-subtle);
|
||||
background-size: 16px 16px;
|
||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
||||
}
|
||||
.preview-viewport:not(.preview-viewport-desktop) .preview-frame-clip,
|
||||
.preview-viewport:not(.preview-viewport-desktop) .comment-frame-clip,
|
||||
.preview-viewport:not(.preview-viewport-desktop).manual-edit-workspace .manual-edit-canvas {
|
||||
width: calc(var(--preview-viewport-width) * var(--preview-scale, 1));
|
||||
height: calc(var(--preview-viewport-height) * var(--preview-scale, 1));
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.22);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.preview-viewport:not(.preview-viewport-desktop) .preview-frame-clip > div,
|
||||
.preview-viewport:not(.preview-viewport-desktop) .comment-frame-clip > div,
|
||||
.preview-viewport:not(.preview-viewport-desktop).manual-edit-workspace .manual-edit-canvas > div {
|
||||
will-change: transform;
|
||||
}
|
||||
.preview-viewport:not(.preview-viewport-desktop) .preview-frame-clip,
|
||||
.preview-viewport:not(.preview-viewport-desktop) .comment-frame-clip {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
}
|
||||
.preview-viewport-mobile .preview-frame-clip,
|
||||
.preview-viewport-mobile .comment-frame-clip,
|
||||
.preview-viewport-mobile.manual-edit-workspace .manual-edit-canvas {
|
||||
border-radius: 28px;
|
||||
}
|
||||
.preview-frame-clip,
|
||||
.comment-frame-clip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
@ -15021,6 +15189,11 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
background: var(--bg);
|
||||
}
|
||||
|
||||
.manual-edit-workspace.preview-viewport:not(.preview-viewport-desktop) {
|
||||
grid-template-columns: 240px calc(var(--preview-viewport-width) * var(--preview-scale, 1)) 344px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.manual-edit-canvas {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// - PDF : open the artifact in a popup window and trigger window.print().
|
||||
// The user picks "Save as PDF" from the system print dialog.
|
||||
// - HTML : download the artifact as a single .html file via a Blob URL.
|
||||
// - ZIP : pack the artifact into a stored-mode ZIP (see ./zip.ts).
|
||||
// - ZIP : pack the artifact with a coding handoff guide (see ./zip.ts).
|
||||
// - MD : download the artifact's source verbatim with a `.md` extension
|
||||
// so it can be ingested by markdown-aware tooling (LLM context
|
||||
// windows, vault apps, etc.). No conversion is performed — the
|
||||
|
|
@ -16,6 +16,9 @@ import { buildSrcdoc, type SrcdocOptions } from './srcdoc';
|
|||
import { buildReactComponentSrcdoc } from './react-component';
|
||||
import { buildZip } from './zip';
|
||||
|
||||
const DESIGN_HANDOFF_FILENAME = 'DESIGN-HANDOFF.md';
|
||||
const DESIGN_MANIFEST_FILENAME = 'DESIGN-MANIFEST.json';
|
||||
|
||||
function safeFilename(name: string, fallback: string): string {
|
||||
const slug = (name || fallback)
|
||||
.replace(/[^\w.\-]+/g, '-')
|
||||
|
|
@ -43,14 +46,262 @@ export function exportAsHtml(html: string, title: string): void {
|
|||
triggerDownload(blob, `${safeFilename(title, 'artifact')}.html`);
|
||||
}
|
||||
|
||||
// A file is treated as a preview-chrome wrapper only when it lives inside
|
||||
// a frames/ or device-frames/ directory, or its filename is an unambiguous
|
||||
// wrapper template (browser-chrome.html, device-frame.html). Filenames
|
||||
// like phone.html or iphone-upgrade.html are legitimate product-screen
|
||||
// deliverables and must not be dropped from manifest screens.
|
||||
const FRAME_WRAPPER_FILE_RE = /(^|\/)(frames?\/|device-frames?\/)|(^|\/)(browser-chrome|device-frame)\.html?$/i;
|
||||
|
||||
function isFrameWrapperHtmlFile(file: string): boolean {
|
||||
return FRAME_WRAPPER_FILE_RE.test(file);
|
||||
}
|
||||
|
||||
type DesignFileMap = {
|
||||
files: string[];
|
||||
htmlFiles: string[];
|
||||
screenHtmlFiles: string[];
|
||||
cssFiles: string[];
|
||||
jsFiles: string[];
|
||||
assetFiles: string[];
|
||||
entryFile: string;
|
||||
};
|
||||
|
||||
function designFileMap(entryFile: string, files?: string[]): DesignFileMap {
|
||||
const all = Array.from(new Set([entryFile, ...(files ?? [])])).sort((a, b) => a.localeCompare(b));
|
||||
const htmlFiles = all.filter((name) => /\.html?$/i.test(name));
|
||||
const screenHtmlFiles = htmlFiles.filter((name) => !isFrameWrapperHtmlFile(name));
|
||||
const cssFiles = all.filter((name) => /\.css$/i.test(name));
|
||||
const jsFiles = all.filter((name) => /\.[cm]?[jt]sx?$/i.test(name));
|
||||
const assetFiles = all.filter((name) => !htmlFiles.includes(name) && !cssFiles.includes(name) && !jsFiles.includes(name));
|
||||
const preferredEntryFile = !isFrameWrapperHtmlFile(entryFile)
|
||||
? entryFile
|
||||
: screenHtmlFiles.find((name) => /(^|\/)index\.html$/i.test(name)) || screenHtmlFiles[0] || entryFile;
|
||||
return { files: all, htmlFiles, screenHtmlFiles, cssFiles, jsFiles, assetFiles, entryFile: preferredEntryFile };
|
||||
}
|
||||
|
||||
export function buildDesignManifestContent(opts: {
|
||||
title: string;
|
||||
entryFile: string;
|
||||
files?: string[];
|
||||
kind?: 'html' | 'react';
|
||||
}): string {
|
||||
const title = opts.title || 'Open Design artifact';
|
||||
const requestedEntryFile = opts.entryFile || 'index.html';
|
||||
const { files, htmlFiles, screenHtmlFiles, cssFiles, jsFiles, assetFiles, entryFile } = designFileMap(requestedEntryFile, opts.files);
|
||||
const screenFiles = screenHtmlFiles.length > 0 ? screenHtmlFiles : [entryFile];
|
||||
return JSON.stringify({
|
||||
schema: 'open-design.design-manifest.v1',
|
||||
title,
|
||||
kind: opts.kind ?? 'html',
|
||||
entryFile,
|
||||
sourceFiles: {
|
||||
all: files,
|
||||
html: htmlFiles,
|
||||
css: cssFiles,
|
||||
scriptsAndComponents: jsFiles,
|
||||
assets: assetFiles,
|
||||
},
|
||||
screens: screenFiles.map((file) => {
|
||||
const isIndex = /(^|\/)index\.html?$/i.test(file);
|
||||
const isLanding = /(^|\/)(landing|marketing)\.html?$/i.test(file) || /landing|marketing/i.test(file);
|
||||
const isOsWidget = /widget|live-activity|lock-screen|home-screen/i.test(file);
|
||||
const isApp = /app|dashboard|workspace|generator|translator|editor|screen/i.test(file);
|
||||
return {
|
||||
file,
|
||||
role: isIndex && screenFiles.length > 1 ? 'launcher-overview' : isLanding ? 'landing-page' : isOsWidget ? 'os-widget-surface' : isApp ? 'product-screen' : 'screen',
|
||||
implementationNote: isIndex && screenFiles.length > 1
|
||||
? 'Use this as the navigation/overview entry only; implement each linked screen file as its own route/surface.'
|
||||
: 'Preserve visual hierarchy, responsive behavior, and interactive states from this screen.',
|
||||
};
|
||||
}),
|
||||
screenFilePolicy: {
|
||||
mode: 'screen-file-first',
|
||||
entryFileRole: screenFiles.length > 1 && /(^|\/)index\.html?$/i.test(entryFile) ? 'launcher-overview' : 'primary-screen',
|
||||
rules: [
|
||||
'Each distinct user-facing screen or surface must be delivered and implemented as its own file/route.',
|
||||
'If a landing page is present or requested, keep it in landing.html and do not merge it into the product app screen.',
|
||||
'When multiple HTML screens exist, index.html is a launcher/overview only; it must not be treated as the combined final UI.',
|
||||
'Keep product app screens, landing pages, platform screens, and OS widget surfaces separate in production code.',
|
||||
],
|
||||
},
|
||||
appModules: [
|
||||
'Identify domain-specific in-app modules from the exported UI; do not reduce them to generic cards.',
|
||||
'For each major module, implement purpose, default/loading/empty/error/success states, and responsive behavior.',
|
||||
'Keep app modules separate from OS home-screen widgets in the production component model.',
|
||||
],
|
||||
osWidgets: [
|
||||
'If the export includes home-screen, lock-screen, Live Activity, tablet glance, or Android widget surfaces, implement them as platform quick-access surfaces outside the app UI.',
|
||||
'If none are present, do not invent OS widgets unless the product requirements request them.',
|
||||
],
|
||||
landingPage: {
|
||||
detection: 'Inspect files and screen names for a marketing/landing page surface. If present, keep it separate from product app screens.',
|
||||
requiredSections: ['hero', 'value props', 'product proof/screenshots', 'feature proof', 'CTA'],
|
||||
},
|
||||
tokens: {
|
||||
source: cssFiles.length > 0 ? cssFiles : [entryFile],
|
||||
required: ['background', 'surface', 'foreground', 'muted text', 'border', 'accent', 'radius', 'shadow', 'spacing', 'type scale', 'motion'],
|
||||
note: 'Extract/freeze tokens before framework implementation so coding tools do not substitute default theme colors or typography.',
|
||||
},
|
||||
interactions: {
|
||||
source: jsFiles.length > 0 ? jsFiles : [entryFile],
|
||||
requiredStates: ['default', 'hover', 'focus', 'active', 'disabled', 'loading', 'empty', 'error', 'success'],
|
||||
requiredBehaviors: ['forms/validation where present', 'tabs/filters where present', 'dialogs/sheets/drawers where present', 'copy/generate/share actions where present', 'player or quick controls where present'],
|
||||
note: 'If the prototype is static, derive missing behavior from visible controls and document it before coding.',
|
||||
},
|
||||
responsiveViewports: [
|
||||
{ name: 'mobile-compact', width: 360, height: 800, category: 'mobile', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'mobile-standard', width: 390, height: 844, category: 'mobile', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'mobile-large', width: 430, height: 932, category: 'mobile', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'foldable-small-tablet', width: 600, height: 960, category: 'foldable-tablet', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'tablet-portrait', width: 820, height: 1180, category: 'tablet', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'tablet-landscape', width: 1024, height: 768, category: 'tablet', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'laptop', width: 1366, height: 768, category: 'desktop', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'desktop', width: 1440, height: 900, category: 'desktop', mustAvoidHorizontalScroll: true },
|
||||
{ name: 'wide', width: 1920, height: 1080, category: 'wide', mustAvoidHorizontalScroll: true },
|
||||
],
|
||||
implementationChecklist: [
|
||||
'Open entryFile first and map screens, modules, tokens, and interactions.',
|
||||
'Extract tokens before writing framework components.',
|
||||
'Implement app-specific modules with real states instead of generic card grids.',
|
||||
'Preserve or rebuild JS interactions for meaningful UX actions.',
|
||||
'Validate screenshots at desktop/tablet/mobile viewports with no horizontal overflow.',
|
||||
'Keep landing pages, in-app modules, and OS widgets as separate implementation surfaces.',
|
||||
],
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
export function buildDesignHandoffContent(opts: {
|
||||
title: string;
|
||||
entryFile: string;
|
||||
files?: string[];
|
||||
kind?: 'html' | 'react';
|
||||
}): string {
|
||||
const title = opts.title || 'Open Design artifact';
|
||||
const requestedEntryFile = opts.entryFile || 'index.html';
|
||||
const { files, htmlFiles, cssFiles, jsFiles, assetFiles, entryFile } = designFileMap(requestedEntryFile, opts.files);
|
||||
const accentLikelyBrandLed =
|
||||
files.some((name) => /(design|brand|tokens?|theme|style|tailwind|variables)\.(css|scss|sass|less|json|ts|tsx|js|jsx|md)$/i.test(name)) ||
|
||||
cssFiles.length > 0;
|
||||
const hasResponsiveClues =
|
||||
htmlFiles.length > 0 ||
|
||||
cssFiles.length > 0 ||
|
||||
files.some((name) => /(screens?|pages?|components?|app|src)\//i.test(name));
|
||||
const list = (items: string[]) => items.length > 0 ? items.map((name) => `- \`${name}\``).join('\n') : '- None detected';
|
||||
const sourceNote = opts.kind === 'react'
|
||||
? 'Use the exported React source as the component contract, then preserve the rendered visual behavior in the target app.'
|
||||
: `Start from \`${entryFile}\`, then preserve the visual system, responsive behavior, and interactions found in the exported files.`;
|
||||
|
||||
return `# ${title} implementation handoff
|
||||
|
||||
This archive is the source of truth for turning the design into production code. ${sourceNote}
|
||||
|
||||
## Implementation target
|
||||
- Build production UI from the exported design, not a loose reinterpretation.
|
||||
- Preserve typography scale, spacing rhythm, color tokens, border radii, shadows, motion timing, and component states.
|
||||
- Replace static placeholders only when the target app has real data or functional equivalents.
|
||||
- Keep generated product UI free of Open Design chrome, preview labels, or design-process annotations.
|
||||
- Treat this handoff as a visual contract: if implementation choices conflict, match the exported pixels and behavior first, then refactor internals.
|
||||
|
||||
## Source map
|
||||
- Primary entry: \`${entryFile}\`
|
||||
- HTML screens detected: ${htmlFiles.length}
|
||||
- Stylesheets detected: ${cssFiles.length}
|
||||
- Script/component files detected: ${jsFiles.length}
|
||||
- Supporting assets detected: ${assetFiles.length}
|
||||
|
||||
## Responsive contract
|
||||
Validate the implementation across this 2025–2026 viewport matrix:
|
||||
- Mobile compact: 360×800
|
||||
- Mobile standard: 390×844
|
||||
- Mobile large: 430×932
|
||||
- Foldable / small tablet: 600×960
|
||||
- Tablet portrait: 820×1180
|
||||
- Tablet landscape: 1024×768
|
||||
- Laptop: 1366×768
|
||||
- Desktop: 1440×900
|
||||
- Wide desktop: 1920×1080
|
||||
|
||||
For responsive web exports, treat these as a modern breakpoint system for one adaptive web experience, not three fixed screenshots. Do not split responsive web into unrelated native app screens unless the project explicitly includes native targets. Use semantic layout thresholds, fluid \`clamp()\` type/spacing, and container queries where component width matters more than viewport width. ${hasResponsiveClues ? 'Preserve any CSS media queries, container queries, fluid \`clamp()\` scales, and layout changes already present in the exported files.' : 'If responsive rules are not present in the export, add them in the target implementation before shipping.'}
|
||||
|
||||
## Design fidelity contract
|
||||
- Extract reusable tokens before writing components: background, surface, foreground, muted text, border, accent, radius, shadow, spacing, type scale, and motion duration/easing.
|
||||
- Map product screens, in-app modules/components, optional landing page, and optional OS widget surfaces before coding. Keep these surfaces separate in the target architecture.
|
||||
- Match layout geometry: max-widths, gutters, grid columns, card proportions, sticky/fixed elements, and viewport-specific navigation.
|
||||
- Preserve real copy, labels, and data shown in the export. Do not replace specific text with generic marketing filler.
|
||||
- Preserve interactive affordances: hover, focus, pressed, disabled, loading, validation, copy/share, tab/accordion, modal/sheet, and keyboard states where present.
|
||||
- Preserve accessibility semantics when converting: headings stay hierarchical, controls remain buttons/links/inputs, focus states stay visible.
|
||||
- Do not keep prototype-only annotations, frame labels, or Open Design chrome in the production UI.
|
||||
|
||||
## CJX-ready UX contract
|
||||
- Use \`${DESIGN_MANIFEST_FILENAME}\` as the machine-readable map for screens, app modules, OS widgets, landing pages, tokens, interactions, and viewport checks.
|
||||
- Screen-file-first: when multiple user-facing surfaces exist, implement each HTML screen as its own route/file. Treat \`index.html\` as a launcher/overview when the manifest marks it that way, not as a combined final UI.
|
||||
- If \`landing.html\`, app screens, platform screens, or OS widget files exist, preserve those boundaries in the target app instead of merging them into one page.
|
||||
- A single self-contained \`${entryFile}\` is acceptable only when the export truly contains one user-facing screen and its CSS/JS are structured enough to extract tokens, components, states, and behavior.
|
||||
- If separate \`css/\` or \`js/\` files exist, treat them as source of truth for token/component/interactions before porting to React, Vue, SwiftUI, Compose, or another target stack.
|
||||
- In-app modules/components are product UI blocks inside the app. OS widgets are home-screen/lock-screen/quick-access surfaces outside the app. Do not merge those concepts.
|
||||
|
||||
## Color and brand contract
|
||||
- Use the exported design tokens and product/domain context as the color source of truth.
|
||||
- Do not introduce warm beige / cream / peach / pink / orange-brown background washes unless they are already explicit brand/reference colors in the export.
|
||||
- ${accentLikelyBrandLed ? 'A stylesheet or design/token file was detected; inspect it for canonical color variables before choosing framework theme tokens.' : 'No obvious token stylesheet was detected; sample colors from the entry file and convert them into named tokens before coding.'}
|
||||
|
||||
## Implementation sequence for AI coding tools
|
||||
1. Open \`${entryFile}\` and \`${DESIGN_MANIFEST_FILENAME}\`; identify every screen file, launcher/overview file, app module, and interaction before coding.
|
||||
2. If multiple HTML screens exist, map them to separate routes/surfaces first; do not merge \`landing.html\`, product app screens, platform screens, or OS widgets into one route.
|
||||
3. Extract a token table from CSS/root styles and inline styles before building framework components.
|
||||
4. Build product screens and domain-specific in-app modules from largest layout regions down to controls; avoid starting with isolated atoms that lose spatial intent.
|
||||
5. Port responsive behavior across the modern viewport matrix and test each semantic breakpoint before cleanup.
|
||||
6. Port interactions and states, then replace static placeholders only with real app data or functional equivalents.
|
||||
7. Keep optional landing page and OS widget surfaces as separate surfaces if present.
|
||||
8. Compare final screenshots against the export at 360×800, 390×844, 430×932, 820×1180, 1024×768, 1366×768, 1440×900, and 1920×1080 before declaring done.
|
||||
|
||||
## Entry points
|
||||
${list(htmlFiles.length > 0 ? htmlFiles : [entryFile])}
|
||||
|
||||
## Styles
|
||||
${list(cssFiles)}
|
||||
|
||||
## Scripts/components
|
||||
${list(jsFiles)}
|
||||
|
||||
## Assets and supporting files
|
||||
${list(assetFiles)}
|
||||
|
||||
## Coding checklist for AI tools
|
||||
1. Inspect \`${entryFile}\` and \`${DESIGN_MANIFEST_FILENAME}\` first and identify reusable components before coding.
|
||||
2. Implement each user-facing screen file as its own route/surface; keep launcher, landing, app, platform, and OS widget files separate.
|
||||
3. Extract design tokens into the target stack: colors, type scale, spacing, radius, shadows, and motion.
|
||||
4. Implement layout with real 2025–2026 responsive breakpoints, fluid type/spacing, and container-query-aware component behavior; test with no horizontal overflow.
|
||||
5. Preserve interactive controls, hover/focus/pressed states, form behavior, validation, and copy actions where present.
|
||||
6. Implement domain-specific in-app modules with real states; do not flatten them into generic cards.
|
||||
7. Keep landing page, product screens, and OS widget/quick-access surfaces separate when present.
|
||||
8. Confirm the production result visually matches the exported design before refactoring internals.
|
||||
9. Reject implementation shortcuts that flatten the design into generic cards, generic gradients, placeholder stats, or framework-default typography.
|
||||
10. If a detail is ambiguous, keep the exported HTML/CSS/JS behavior rather than inventing a new pattern.
|
||||
`;
|
||||
}
|
||||
|
||||
export function exportAsZip(html: string, title: string): void {
|
||||
const doc = buildSrcdoc(html);
|
||||
const slug = safeFilename(title, 'artifact');
|
||||
const blob = buildZip([
|
||||
{ path: `${slug}/index.html`, content: doc },
|
||||
{
|
||||
path: `${slug}/README.md`,
|
||||
content: `# ${title || slug}\n\nGenerated by Open Design.\nOpen index.html in a browser to view.\n`,
|
||||
path: `${slug}/${DESIGN_HANDOFF_FILENAME}`,
|
||||
content: buildDesignHandoffContent({
|
||||
title: title || slug,
|
||||
entryFile: 'index.html',
|
||||
files: ['index.html'],
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${slug}/${DESIGN_MANIFEST_FILENAME}`,
|
||||
content: buildDesignManifestContent({
|
||||
title: title || slug,
|
||||
entryFile: 'index.html',
|
||||
files: ['index.html'],
|
||||
}),
|
||||
},
|
||||
]);
|
||||
triggerDownload(blob, `${slug}.zip`);
|
||||
|
|
@ -119,11 +370,26 @@ export function exportReactComponentAsZip(
|
|||
extension: ReactSourceExtension = '.jsx',
|
||||
): void {
|
||||
const slug = safeFilename(title, 'component');
|
||||
const componentFile = `${slug}${extension}`;
|
||||
const blob = buildZip([
|
||||
{ path: `${slug}/${slug}${extension}`, content: source },
|
||||
{ path: `${slug}/${componentFile}`, content: source },
|
||||
{
|
||||
path: `${slug}/README.md`,
|
||||
content: `# ${title || slug}\n\nGenerated by Open Design.\nOpen the JSX file in a React project or export the standalone HTML preview from Open Design.\n`,
|
||||
path: `${slug}/${DESIGN_HANDOFF_FILENAME}`,
|
||||
content: buildDesignHandoffContent({
|
||||
title: title || slug,
|
||||
entryFile: componentFile,
|
||||
files: [componentFile],
|
||||
kind: 'react',
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${slug}/${DESIGN_MANIFEST_FILENAME}`,
|
||||
content: buildDesignManifestContent({
|
||||
title: title || slug,
|
||||
entryFile: componentFile,
|
||||
files: [componentFile],
|
||||
kind: 'react',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
triggerDownload(blob, `${slug}.zip`);
|
||||
|
|
|
|||
|
|
@ -557,8 +557,22 @@ const DAEMON_OWNED_KEYS = new Set<keyof AppConfig>([
|
|||
'privacyDecisionAt',
|
||||
]);
|
||||
|
||||
const AGENT_CLI_SECRET_ENV_KEYS = new Set(['ANTHROPIC_API_KEY', 'OPENAI_API_KEY']);
|
||||
|
||||
function sanitizeAgentCliEnv(agentCliEnv: AppConfig['agentCliEnv']): AppConfig['agentCliEnv'] {
|
||||
if (!agentCliEnv) return agentCliEnv;
|
||||
const sanitized: NonNullable<AppConfig['agentCliEnv']> = {};
|
||||
for (const [agentId, env] of Object.entries(agentCliEnv)) {
|
||||
const safeEnv = Object.fromEntries(
|
||||
Object.entries(env ?? {}).filter(([key]) => !AGENT_CLI_SECRET_ENV_KEYS.has(key)),
|
||||
);
|
||||
sanitized[agentId] = safeEnv;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function saveConfig(config: AppConfig): void {
|
||||
const sanitized: AppConfig = { ...config };
|
||||
const sanitized: AppConfig = { ...config, agentCliEnv: sanitizeAgentCliEnv(config.agentCliEnv) };
|
||||
for (const key of DAEMON_OWNED_KEYS) {
|
||||
delete (sanitized as unknown as Record<string, unknown>)[key];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import type {
|
|||
ProviderModelsRequest,
|
||||
ProviderModelsResponse,
|
||||
Project,
|
||||
ProjectPlatform,
|
||||
PreviewCommentMember,
|
||||
PreviewCommentSelectionKind,
|
||||
PreviewComment,
|
||||
|
|
@ -424,6 +425,7 @@ export type {
|
|||
MediaAspect,
|
||||
ProjectDeploymentsResponse,
|
||||
Project,
|
||||
ProjectPlatform,
|
||||
PreviewComment,
|
||||
PreviewCommentStatus,
|
||||
PreviewCommentTarget,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
LiveArtifactRefreshHistoryPanel,
|
||||
SvgViewer,
|
||||
applyInspectOverridesToSource,
|
||||
effectivePreviewScale,
|
||||
parseInspectOverridesFromSource,
|
||||
serializeInspectOverrides,
|
||||
updateInspectOverride,
|
||||
|
|
@ -60,6 +61,17 @@ function deferredResponse() {
|
|||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe('FileViewer preview scale', () => {
|
||||
it('uses the requested zoom for desktop preview overlays', () => {
|
||||
expect(effectivePreviewScale('desktop', 1.5, { width: 320, height: 480 })).toBe(1.5);
|
||||
});
|
||||
|
||||
it('clamps mobile and tablet overlay scale to the iframe auto-fit scale', () => {
|
||||
expect(effectivePreviewScale('mobile', 1, { width: 390, height: 844 })).toBeLessThan(1);
|
||||
expect(effectivePreviewScale('tablet', 1.25, { width: 820, height: 700 })).toBeLessThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FileViewer JSON artifacts', () => {
|
||||
it('pretty-prints valid JSON in the text viewer', async () => {
|
||||
const file = baseFile({
|
||||
|
|
@ -1608,7 +1620,7 @@ describe('LiveArtifactViewer', () => {
|
|||
});
|
||||
|
||||
const requestFullscreen = vi.fn(() => Promise.reject(new Error('denied')));
|
||||
const previewHost = container.querySelector('.live-artifact-preview-frame-host');
|
||||
const previewHost = container.querySelector('.viewer-body');
|
||||
expect(previewHost).toBeTruthy();
|
||||
Object.defineProperty(previewHost!, 'requestFullscreen', {
|
||||
configurable: true,
|
||||
|
|
@ -1650,7 +1662,7 @@ describe('LiveArtifactViewer', () => {
|
|||
});
|
||||
|
||||
const requestFullscreen = vi.fn(() => Promise.resolve());
|
||||
const previewHost = container.querySelector('.live-artifact-preview-frame-host');
|
||||
const previewHost = container.querySelector('.viewer-body');
|
||||
expect(previewHost).toBeTruthy();
|
||||
Object.defineProperty(previewHost!, 'requestFullscreen', {
|
||||
configurable: true,
|
||||
|
|
|
|||
|
|
@ -278,6 +278,9 @@ describe('NewProjectPanel design system defaults', () => {
|
|||
}),
|
||||
}),
|
||||
);
|
||||
const payload = onCreate.mock.calls[0]?.[0];
|
||||
expect(payload.metadata).not.toHaveProperty('platform');
|
||||
expect(payload.metadata).not.toHaveProperty('platformTargets');
|
||||
});
|
||||
|
||||
it('prevents template creation when there are no saved templates and enables creation once one exists', () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
import {
|
||||
archiveFilenameFrom,
|
||||
archiveRootFromFilePath,
|
||||
buildDesignHandoffContent,
|
||||
buildDesignManifestContent,
|
||||
buildSandboxedPreviewDocument,
|
||||
exportAsMd,
|
||||
exportAsPdf,
|
||||
|
|
@ -73,6 +75,118 @@ describe('archiveFilenameFrom', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('buildDesignHandoffContent', () => {
|
||||
it('documents coding handoff and responsive verification expectations', () => {
|
||||
const content = buildDesignHandoffContent({
|
||||
title: 'Checkout Design',
|
||||
entryFile: 'index.html',
|
||||
files: ['index.html', 'src/app.css', 'src/app.js'],
|
||||
});
|
||||
|
||||
expect(content).toContain('Checkout Design implementation handoff');
|
||||
expect(content).toContain('Mobile compact: 360×800');
|
||||
expect(content).toContain('Tablet portrait: 820×1180');
|
||||
expect(content).toContain('Wide desktop: 1920×1080');
|
||||
expect(content).toContain('`src/app.css`');
|
||||
expect(content).toContain('visual system');
|
||||
expect(content).toContain('Design fidelity contract');
|
||||
expect(content).toContain('CJX-ready UX contract');
|
||||
expect(content).toContain('DESIGN-MANIFEST.json');
|
||||
expect(content).toContain('in-app modules/components');
|
||||
expect(content).toContain('OS widgets are home-screen/lock-screen/quick-access surfaces');
|
||||
expect(content).toContain('Color and brand contract');
|
||||
expect(content).toContain('Do not introduce warm beige / cream / peach / pink / orange-brown background washes');
|
||||
expect(content).toContain('Build product screens and domain-specific in-app modules');
|
||||
});
|
||||
|
||||
it('builds a machine-readable design manifest for coding tools', () => {
|
||||
const manifest = JSON.parse(buildDesignManifestContent({
|
||||
title: 'Checkout Design',
|
||||
entryFile: 'index.html',
|
||||
files: ['index.html', 'src/app.css', 'src/app.js'],
|
||||
}));
|
||||
|
||||
expect(manifest.schema).toBe('open-design.design-manifest.v1');
|
||||
expect(manifest.entryFile).toBe('index.html');
|
||||
expect(manifest.sourceFiles.css).toEqual(['src/app.css']);
|
||||
expect(manifest.sourceFiles.scriptsAndComponents).toEqual(['src/app.js']);
|
||||
expect(manifest.appModules.join(' ')).toContain('domain-specific in-app modules');
|
||||
expect(manifest.osWidgets.join(' ')).toContain('home-screen');
|
||||
expect(manifest.responsiveViewports).toContainEqual({
|
||||
name: 'tablet-portrait',
|
||||
width: 820,
|
||||
height: 1180,
|
||||
category: 'tablet',
|
||||
mustAvoidHorizontalScroll: true,
|
||||
});
|
||||
expect(manifest.implementationChecklist.join(' ')).toContain('landing pages, in-app modules, and OS widgets');
|
||||
});
|
||||
|
||||
it('does not classify plain home.html as a landing page in the manifest', () => {
|
||||
const manifest = JSON.parse(buildDesignManifestContent({
|
||||
title: 'Product App',
|
||||
entryFile: 'home.html',
|
||||
files: ['home.html', 'dashboard.html', 'marketing.html'],
|
||||
}));
|
||||
|
||||
const screens = new Map(manifest.screens.map((screen: { file: string; role: string }) => [screen.file, screen.role]));
|
||||
expect(screens.get('home.html')).not.toBe('landing-page');
|
||||
expect(screens.get('marketing.html')).toBe('landing-page');
|
||||
expect(screens.get('dashboard.html')).toBe('product-screen');
|
||||
});
|
||||
|
||||
it('keeps frame wrapper HTML out of client export manifest screens', () => {
|
||||
const manifest = JSON.parse(buildDesignManifestContent({
|
||||
title: 'Framed App',
|
||||
entryFile: 'index.html',
|
||||
files: ['index.html', 'frames/iphone-15-pro.html', 'browser-chrome.html', 'src/app.css'],
|
||||
}));
|
||||
|
||||
expect(manifest.sourceFiles.html).toEqual(['browser-chrome.html', 'frames/iphone-15-pro.html', 'index.html']);
|
||||
expect(manifest.screens.map((screen: { file: string }) => screen.file)).toEqual(['index.html']);
|
||||
});
|
||||
|
||||
it('normalizes a frame-wrapper active file to the implementable screen entry in manifest and handoff', () => {
|
||||
const manifest = JSON.parse(buildDesignManifestContent({
|
||||
title: 'Framed App',
|
||||
entryFile: 'frames/iphone-15-pro.html',
|
||||
files: ['index.html', 'landing.html', 'frames/iphone-15-pro.html', 'src/app.css'],
|
||||
}));
|
||||
const handoff = buildDesignHandoffContent({
|
||||
title: 'Framed App',
|
||||
entryFile: 'frames/iphone-15-pro.html',
|
||||
files: ['index.html', 'landing.html', 'frames/iphone-15-pro.html', 'src/app.css'],
|
||||
});
|
||||
|
||||
expect(manifest.entryFile).toBe('index.html');
|
||||
expect(manifest.screens.map((screen: { file: string }) => screen.file)).toEqual(['index.html', 'landing.html']);
|
||||
expect(handoff).toContain('Primary entry: `index.html`');
|
||||
expect(handoff).toContain('Open `index.html` and `DESIGN-MANIFEST.json`');
|
||||
expect(handoff).not.toContain('Primary entry: `frames/iphone-15-pro.html`');
|
||||
});
|
||||
|
||||
it('keeps phone.html and iphone-upgrade.html as real screens when outside frames/ directory', () => {
|
||||
// phone.html as a carrier storefront screen, iphone-upgrade.html as a
|
||||
// product surface — neither should be silently dropped from screens just
|
||||
// because the filename resembles a device name.
|
||||
const manifest = JSON.parse(buildDesignManifestContent({
|
||||
title: 'Carrier Storefront',
|
||||
entryFile: 'phone.html',
|
||||
files: ['phone.html', 'iphone-upgrade.html', 'frames/browser-shell.html', 'src/app.css'],
|
||||
}));
|
||||
|
||||
const screenFiles = manifest.screens.map((screen: { file: string }) => screen.file);
|
||||
expect(screenFiles).toContain('phone.html');
|
||||
expect(screenFiles).toContain('iphone-upgrade.html');
|
||||
// frame wrapper inside frames/ is still excluded
|
||||
expect(screenFiles).not.toContain('frames/browser-shell.html');
|
||||
// both real screens appear in sourceFiles.html
|
||||
expect(manifest.sourceFiles.html).toContain('phone.html');
|
||||
expect(manifest.sourceFiles.html).toContain('iphone-upgrade.html');
|
||||
expect(manifest.sourceFiles.html).toContain('frames/browser-shell.html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportProjectAsPdf', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
|
|
|
|||
|
|
@ -104,6 +104,27 @@ describe('syncConfigToDaemon', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('syncs proxy API key env values to daemon app config while localStorage strips them', async () => {
|
||||
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await syncConfigToDaemon({
|
||||
...DEFAULT_CONFIG,
|
||||
agentCliEnv: {
|
||||
claude: { ANTHROPIC_API_KEY: 'sk-anthropic', ANTHROPIC_BASE_URL: 'https://proxy.example/anthropic' },
|
||||
codex: { OPENAI_API_KEY: 'sk-openai', OPENAI_BASE_URL: 'https://proxy.example/openai' },
|
||||
},
|
||||
});
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
||||
expect(JSON.parse(String(init.body))).toMatchObject({
|
||||
agentCliEnv: {
|
||||
claude: { ANTHROPIC_API_KEY: 'sk-anthropic', ANTHROPIC_BASE_URL: 'https://proxy.example/anthropic' },
|
||||
codex: { OPENAI_API_KEY: 'sk-openai', OPENAI_BASE_URL: 'https://proxy.example/openai' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('syncs daemon-owned privacy decision fields', async () => {
|
||||
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
|
@ -782,4 +803,32 @@ describe('saveConfig', () => {
|
|||
expect(saved.privacyDecisionAt).toBeUndefined();
|
||||
expect(saved.telemetry).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps proxy API key env values out of localStorage while preserving non-secret env', () => {
|
||||
saveConfig({
|
||||
...DEFAULT_CONFIG,
|
||||
agentCliEnv: {
|
||||
claude: {
|
||||
ANTHROPIC_API_KEY: 'sk-anthropic',
|
||||
ANTHROPIC_BASE_URL: 'https://proxy.example/anthropic',
|
||||
CLAUDE_CONFIG_DIR: '~/.claude-2',
|
||||
},
|
||||
codex: {
|
||||
OPENAI_API_KEY: 'sk-openai',
|
||||
OPENAI_BASE_URL: 'https://proxy.example/openai',
|
||||
CODEX_HOME: '~/.codex-alt',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const saved = JSON.parse(store.get('open-design:config') ?? '{}');
|
||||
expect(saved.agentCliEnv.claude).toEqual({
|
||||
ANTHROPIC_BASE_URL: 'https://proxy.example/anthropic',
|
||||
CLAUDE_CONFIG_DIR: '~/.claude-2',
|
||||
});
|
||||
expect(saved.agentCliEnv.codex).toEqual({
|
||||
OPENAI_BASE_URL: 'https://proxy.example/openai',
|
||||
CODEX_HOME: '~/.codex-alt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ export type ProjectKind =
|
|||
|
||||
export type MediaAspect = '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
|
||||
|
||||
export type ProjectPlatform =
|
||||
| 'auto'
|
||||
| 'responsive'
|
||||
| 'web-desktop'
|
||||
| 'mobile-ios'
|
||||
| 'mobile-android'
|
||||
| 'tablet'
|
||||
| 'desktop-app';
|
||||
|
||||
export type AudioKind = 'music' | 'speech' | 'sfx';
|
||||
|
||||
export type ProjectDisplayStatus =
|
||||
|
|
@ -59,8 +68,14 @@ export interface ProjectMetadata {
|
|||
fidelity?: 'wireframe' | 'high-fidelity';
|
||||
speakerNotes?: boolean;
|
||||
animations?: boolean;
|
||||
includeLandingPage?: boolean;
|
||||
includeOsWidgets?: boolean;
|
||||
templateId?: string;
|
||||
templateLabel?: string;
|
||||
/** Primary target surface selected at project creation. */
|
||||
platform?: ProjectPlatform;
|
||||
/** Concrete delivery surfaces the artifact must account for. `responsive` is a web breakpoint target, not a native app expansion. */
|
||||
platformTargets?: ProjectPlatform[];
|
||||
inspirationDesignSystemIds?: string[];
|
||||
importedFrom?: 'claude-design' | 'folder' | string;
|
||||
entryFile?: string;
|
||||
|
|
|
|||
|
|
@ -55,31 +55,31 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
|
|||
id: 'editorial-monocle',
|
||||
label: 'Editorial — Monocle / FT magazine',
|
||||
mood:
|
||||
'Print-magazine feel. Generous whitespace, large serif headlines, restrained palette of off-white paper + ink + a single warm accent. Confident, quietly intelligent.',
|
||||
'Print-magazine feel for explicitly editorial or publishing briefs. Generous whitespace, large serif headlines, restrained palette of neutral paper + ink + a single brand-justified accent. Do not use this as the default for commerce, SaaS, dashboards, or product utilities.',
|
||||
references: ['Monocle', 'The Financial Times Weekend', 'NYT Magazine', 'It\'s Nice That'],
|
||||
displayFont: "'Iowan Old Style', 'Charter', Georgia, serif",
|
||||
bodyFont:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
palette: {
|
||||
bg: 'oklch(97% 0.012 80)', // off-white paper
|
||||
surface: 'oklch(99% 0.005 80)',
|
||||
fg: 'oklch(20% 0.02 60)', // ink
|
||||
muted: 'oklch(48% 0.015 60)',
|
||||
border: 'oklch(89% 0.012 80)',
|
||||
accent: 'oklch(58% 0.16 35)', // warm rust / clay
|
||||
bg: 'oklch(98% 0.004 95)', // neutral paper, not beige wash
|
||||
surface: 'oklch(100% 0.002 95)',
|
||||
fg: 'oklch(20% 0.018 70)', // ink
|
||||
muted: 'oklch(48% 0.012 70)',
|
||||
border: 'oklch(90% 0.006 95)',
|
||||
accent: 'oklch(52% 0.10 28)', // restrained editorial red; override from brand when available
|
||||
},
|
||||
posture: [
|
||||
'serif display, sans body, mono for metadata only',
|
||||
'no shadows, no rounded cards — borders + whitespace do the work',
|
||||
'one decisive image, cropped only at the bottom',
|
||||
'kicker / eyebrow in mono uppercase, one accent color, used at most twice',
|
||||
'kicker / eyebrow in mono uppercase, one accent color, used at most twice; never create peach/pink/orange-beige page washes unless the brand/reference requires them',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'modern-minimal',
|
||||
label: 'Modern minimal — Linear / Vercel',
|
||||
mood:
|
||||
'Quiet, precise, software-native. System fonts, near-greyscale palette, a single saturated accent. The chrome disappears so content is the only thing that registers.',
|
||||
'Quiet, precise, software-native. System fonts, crisp neutral foundations, and a small but visible product palette (primary + secondary + status/accent) so the interface feels shipped rather than greyscale. The chrome stays restrained while interaction states, illustrations, charts, and product moments carry color.',
|
||||
references: ['Linear', 'Vercel', 'Notion 2024', 'Stripe docs'],
|
||||
displayFont:
|
||||
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif",
|
||||
|
|
@ -97,34 +97,34 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
|
|||
'tight letter-spacing on display sizes (-0.02em)',
|
||||
'hairline borders only, no shadows except dropdowns/modals',
|
||||
'mono numerics with `font-variant-numeric: tabular-nums`',
|
||||
'sticky frosted nav, content-led layouts (no hero illustrations)',
|
||||
'one accent: links + primary CTA, nothing else',
|
||||
'sticky frosted nav, content-led layouts with one product illustration, device mockup, or data visualization when it clarifies the product',
|
||||
'controlled color system: primary action color + one secondary signal + status colors; avoid monochrome/unstyled outputs, but never flood every card with gradients',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'warm-soft',
|
||||
label: 'Warm & soft — Stripe pre-2020 / Headspace',
|
||||
id: 'human-approachable',
|
||||
label: 'Human / approachable — Airbnb / Duolingo systems',
|
||||
mood:
|
||||
'Cream backgrounds, soft accent, gentle radii. Reads like a thoughtful product magazine — friendly without being cute. Good for fintech, wellness, indie SaaS.',
|
||||
references: ['Stripe pre-2020', 'Headspace', 'Substack', 'Mercury'],
|
||||
'Friendly and tactile without the generic cozy canvas. Uses a clean neutral background, product-led color system, generous radii, and clear hierarchy. Good for consumer tools, marketplaces, wellness, education, translation, AI assistants, and indie SaaS when the brand has not supplied a palette.',
|
||||
references: ['Airbnb', 'Duolingo product surfaces', 'Miro', 'Mercury'],
|
||||
displayFont:
|
||||
"'Tiempos Headline', 'Newsreader', 'Iowan Old Style', Georgia, serif",
|
||||
"'Söhne', 'Avenir Next', -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
||||
bodyFont:
|
||||
"'Söhne', -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
||||
"-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
|
||||
palette: {
|
||||
bg: 'oklch(97% 0.018 70)', // warm cream
|
||||
surface: 'oklch(99% 0.008 70)',
|
||||
fg: 'oklch(22% 0.02 50)',
|
||||
muted: 'oklch(50% 0.018 50)',
|
||||
border: 'oklch(90% 0.014 70)',
|
||||
accent: 'oklch(64% 0.13 28)', // terracotta
|
||||
bg: 'oklch(98% 0.004 240)',
|
||||
surface: 'oklch(100% 0 0)',
|
||||
fg: 'oklch(20% 0.02 240)',
|
||||
muted: 'oklch(50% 0.018 240)',
|
||||
border: 'oklch(90% 0.006 240)',
|
||||
accent: 'oklch(56% 0.12 170)', // brand-safe teal
|
||||
},
|
||||
posture: [
|
||||
'serif display, soft sans body',
|
||||
'gentle radii (12–16px), no hard 0px corners on content cards',
|
||||
'single accent used for primary CTA + one editorial flourish (a quote mark, a stat)',
|
||||
'soft inner glow on hero cards rather than drop shadows',
|
||||
'avoid icons; use real screenshots / photographs / illustrations',
|
||||
'sans display with strong weight contrast, system body for readability',
|
||||
'comfortable radii (12–18px) paired with crisp grid alignment',
|
||||
'primary action color plus a secondary/domain accent and clear status colors; use color to separate panels, states, and product moments',
|
||||
'subtle elevation only on interactive cards; tasteful gradients/glows are allowed for hero/device/product moments, never as a full-page beige/pastel wash',
|
||||
'avoid generic pastel/beige gradients; use real product screenshots, data, or labelled placeholders',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -165,7 +165,7 @@ export const DESIGN_DIRECTIONS: DesignDirection[] = [
|
|||
bodyFont:
|
||||
"ui-monospace, 'IBM Plex Mono', 'JetBrains Mono', Menlo, monospace",
|
||||
palette: {
|
||||
bg: 'oklch(96% 0.004 100)', // off-white printer paper
|
||||
bg: 'oklch(98% 0.004 240)', // neutral printer paper
|
||||
surface: 'oklch(100% 0 0)',
|
||||
fg: 'oklch(15% 0.02 100)',
|
||||
muted: 'oklch(40% 0.02 100)',
|
||||
|
|
|
|||
|
|
@ -42,12 +42,12 @@ When the user opens a new project or sends a fresh design brief, your **very fir
|
|||
"questions": [
|
||||
{ "id": "output", "label": "What are we making?", "type": "radio", "required": true,
|
||||
"options": ["Slide deck / pitch", "Single web prototype / landing", "Multi-screen app prototype", "Dashboard / tool UI", "Editorial / marketing page", "Other — I'll describe"] },
|
||||
{ "id": "platform", "label": "Primary surface", "type": "radio",
|
||||
"options": ["Mobile (iOS/Android)", "Desktop web", "Tablet", "Responsive — all sizes", "Fixed canvas (1920×1080)"] },
|
||||
{ "id": "platform", "label": "Target platform", "type": "checkbox", "maxSelections": 4,
|
||||
"options": ["Responsive web", "Desktop web", "iOS app", "Android app", "Tablet", "Desktop app", "Fixed canvas (1920×1080)"] },
|
||||
{ "id": "audience", "label": "Who is this for?", "type": "text",
|
||||
"placeholder": "e.g. early-stage investors, dev-tools buyers, internal exec review" },
|
||||
{ "id": "tone", "label": "Visual tone", "type": "checkbox", "maxSelections": 2,
|
||||
"options": ["Editorial / magazine", "Modern minimal", "Playful / illustrative", "Tech / utility", "Luxury / refined", "Brutalist / experimental", "Soft / warm"] },
|
||||
"options": ["Editorial / magazine", "Modern minimal", "Playful / illustrative", "Tech / utility", "Luxury / refined", "Brutalist / experimental", "Human / approachable"] },
|
||||
{ "id": "brand", "label": "Brand context", "type": "radio",
|
||||
"options": ["Pick a direction for me", "I have a brand spec — I'll share it", "Match a reference site / screenshot — I'll attach it"] },
|
||||
{ "id": "scale", "label": "Roughly how much?", "type": "text",
|
||||
|
|
@ -64,7 +64,7 @@ Form authoring rules:
|
|||
- \`type\` is one of: \`radio\`, \`checkbox\`, \`select\`, \`text\`, \`textarea\`.
|
||||
- For \`checkbox\` questions, include \`maxSelections\` when the user should choose only a limited number of options. Do not encode limits only in the label text.
|
||||
- Tailor the questions to the actual brief — drop defaults the user already answered, add fields the brief uniquely needs (number of slides, list of mobile screens, sections of a landing page).
|
||||
- **Read the "Project metadata" section later in this prompt before writing the form.** That block lists what the user already chose at create time (kind, fidelity, speakerNotes, animations, template). Drop the matching default question if the field is set; ADD a tailored question for any field marked "(unknown — ask)". For example, on a deck with \`speakerNotes: (unknown — ask…)\`, include a yes/no on speaker notes; on a template project where animations is unknown, include a motion radio. Don't re-ask the kind itself if metadata.kind is set — the user already told you.
|
||||
- **Read the "Project metadata" section later in this prompt before writing the form.** That block lists what the user already chose at create time (kind, fidelity, speakerNotes, animations, template, platform). Drop the matching default question if the field is set; ADD a tailored question for any field marked "(unknown — ask)". For example, on a deck with \`speakerNotes: (unknown — ask…)\`, include a yes/no on speaker notes; on a template project where animations is unknown, include a motion radio; on a cross-platform project, ask which screens need native variants instead of re-asking platform. Don't re-ask the kind itself if metadata.kind is set — the user already told you.
|
||||
- Keep it under ~7 questions. Second batch in a follow-up form if needed.
|
||||
- Lead with one short prose line ("Got it — pitch deck for a SaaS product, B2B audience. Tell me the rest:") then the form. Do **not** write a long pre-amble.
|
||||
- After \`</question-form>\`, **stop your turn**. Do not write code. Do not start tools. Do not narrate "I'll wait."
|
||||
|
|
@ -113,7 +113,7 @@ Run brand-spec extraction *before* TodoWrite — five steps, each in its own \`B
|
|||
- Six color tokens (\`--bg\`, \`--surface\`, \`--fg\`, \`--muted\`, \`--border\`, \`--accent\`) in OKLch
|
||||
- Display + body + mono font stacks
|
||||
- 3–5 layout posture rules you observed (radii, border weight, accent budget)
|
||||
5. **Vocalise.** State the system you'll use in one sentence ("warm cream background, single rust accent at oklch(58% 0.15 35), Newsreader display + system body") so the user can redirect cheaply.
|
||||
5. **Vocalise.** State the system you'll use in one sentence ("deep navy product canvas, single electric-cyan accent at oklch(68% 0.16 220), geometric display + system body") so the user can redirect cheaply.
|
||||
|
||||
Then proceed to RULE 3.
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ The standard plan template (adapt the middle steps to the brief):
|
|||
- 2. (if branch B) Confirm brand-spec.md + bind to :root
|
||||
(if branch A) Bind chosen direction's palette to :root
|
||||
(else) Pick a direction matching the tone, bind to :root
|
||||
- 3. Plan section/slide/screen list with rhythm (state list aloud before writing)
|
||||
- 3. Plan section/slide/screen list with platform variants and rhythm (state list aloud before writing)
|
||||
- 4. Copy the seed template to project root
|
||||
- 5. Paste & fill the planned layouts/screens/slides
|
||||
- 6. Replace [REPLACE] placeholders with real, specific copy from the brief
|
||||
|
|
@ -175,6 +175,7 @@ ${renderDirectionSpecBlock()}
|
|||
|
||||
### A. Embody the specialist
|
||||
Pick the persona before writing CSS:
|
||||
- **Responsive / cross-platform prototype** → product systems designer. Define shared information architecture first, then explicit modern breakpoint variants: mobile compact (360px), mobile standard/large (390–430px), foldable/small tablet (600–744px), tablet portrait (768–834px), tablet landscape/large tablet (1024–1180px), laptop (1280–1366px), desktop (1440–1536px), and wide (1920px). Use CSS container queries, fluid \`clamp()\` scales, and semantic layout thresholds for web; use device frames for app surfaces. Never merely shrink desktop cards into a phone viewport. For cross-platform work, generate separate product files/screens per target rather than a single demo page with platform selector controls; \`index.html\` should only be an overview/launcher when multiple files exist.
|
||||
- **Slide deck** → slide designer. Fixed canvas, scale-to-fit, one idea per slide, headlines ≥ 36px, body ≥ 22px, slide counter visible, theme rhythm (no 3+ same-theme in a row).
|
||||
- **Mobile app prototype** → interaction designer. Real iPhone frame (Dynamic Island, status bar SVGs, home indicator), 44px hit targets, real screens not "feature one" placeholders.
|
||||
- **Landing / marketing** → brand designer. One hero, 3–6 sections, real copy, *one* decisive flourish.
|
||||
|
|
@ -198,6 +199,8 @@ Every prototype / mobile / deck skill ships:
|
|||
- ❌ Filler copy — "Feature One / Feature Two", lorem ipsum
|
||||
- ❌ An icon next to every heading
|
||||
- ❌ A gradient on every background
|
||||
- ❌ Warm beige / cream / peach / pink / orange-brown page backgrounds unless the user's brand, screenshots, or selected direction explicitly require them
|
||||
- ❌ Product artifacts that expose designer settings, viewport selectors, platform toggles, target-count badges, "demo controls", or generated-design metadata as if they were app UI
|
||||
|
||||
When you don't have a real value, leave a short honest placeholder (\`—\`, a grey block, a labelled stub) instead of inventing one. An honest placeholder beats a fake stat.
|
||||
|
||||
|
|
@ -208,13 +211,24 @@ Default to 2–3 differentiated directions on the same brief — different colou
|
|||
Show something visible early, even if it is a wireframe with grey blocks and labelled placeholders. The user redirects cheaply at this stage. Wrap the first pass in a visible artifact and *say* it is a wireframe.
|
||||
|
||||
### F. Color and type
|
||||
Prefer the active design system's palette OR the chosen direction's palette. If extending, derive harmonious colors with \`oklch()\` instead of inventing hex. Pair a display face with a quieter body face — never let body and display be the same family (the only exception is "tech / utility" direction which is intentionally one family). One accent colour, used at most twice per screen.
|
||||
Prefer the active design system's palette OR the chosen direction's palette. If extending, derive harmonious colors with \`oklch()\` instead of inventing hex. The background must be selected from the user's product domain, brand assets, screenshots, or chosen direction — never from generic app chrome or a default cozy canvas. For product utilities, marketplaces, dashboards, and SaaS, start from neutral or brand-colored foundations; do not fall back to warm beige / peach / pink / orange-brown Claude-style canvases just because no brand was provided. Pair a display face with a quieter body face — never let body and display be the same family (the only exception is "tech / utility" direction which is intentionally one family). One accent colour, used at most twice per screen.
|
||||
|
||||
### G. Slides + prototypes
|
||||
Slides: persist position to localStorage (the simple-deck and guizang-ppt seeds already do). Tag slides with \`data-screen-label="01 Title"\`. Slide numbers are 1-indexed. Theme rhythm: no 3+ same-theme in a row.
|
||||
Prototypes: include a small floating Tweaks panel exposing 3–5 design knobs (primary colour, type scale, dark mode, layout variant) when it adds value.
|
||||
Product prototypes: do **not** include floating Tweaks panels, platform/settings choosers, theme knobs, viewport toggles, or other designer/demo controls in the artifact. If variation controls are useful for internal iteration, keep them out of final product files unless the user explicitly asks for a design-system/spec dashboard.
|
||||
|
||||
### H. Cross-platform + multi-device layouts — use platform contracts and shared frames
|
||||
When the user selects multiple platform targets or metadata says \`platform: responsive\`, design the same product across surfaces instead of one web-only page. Apply these contracts:
|
||||
|
||||
- **Responsive web**: include desktop, tablet, and mobile states for the same web product. Use semantic layout regions, fluid type with \`clamp()\`, breakpoint/container-query adaptations, and verify no horizontal scroll at 360px / 390px / 430px / 600px / 820px / 1024px / 1366px / 1440px / 1920px. The mobile layout must be redesigned for small screens with usable spacing, prioritised content, and real product navigation — not a squeezed desktop or tiny centered poster.
|
||||
- **iOS app**: create a dedicated iOS product file/screen (for example \`mobile-ios.html\`) with an iPhone frame, Dynamic Island/status/home indicators, 44px minimum hit targets, iOS-safe bottom navigation or sheet patterns, and no Android-only Material navigation.
|
||||
- **Android app**: create a dedicated Android product file/screen (for example \`mobile-android.html\`) with a Pixel frame, status bar + nav bar, 48dp hit targets, Material navigation patterns, and no iOS-only chrome.
|
||||
- **Tablet**: create a dedicated tablet product file/screen (for example \`tablet.html\`) with split panes, sidebars, inspectors, and larger touch targets; do not simply scale the phone UI up or let tablet layouts overflow horizontally.
|
||||
- **Desktop app**: include desktop chrome/sidebar density, keyboard-friendly states, resizable panes, and hover/focus states.
|
||||
- **App-specific modules/components**: every product/app prototype must include domain-specific in-app modules by default (not optional): player controls for media, streak/check-in modules for habits, cart/order/coupon modules for commerce, balance/transaction/budget modules for finance, etc. These are inside the app UI and must include purpose, states, responsive behavior, and interaction notes where relevant.
|
||||
- **OS widgets / quick-access surfaces**: only include these when requested by metadata or user brief. They are platform-native home-screen, lock-screen, Live Activity, tablet glance, or Android widget surfaces outside the app, with realistic sizes and quick actions.
|
||||
- **CJX-ready UX**: artifacts must be implementation-ready. Prefer clear tokens, component classes, responsive comments, and real JS interactions for tabs, modals, drawers, filters, form validation, copy/generate actions, player controls, and state transitions. A self-contained \`index.html\` is acceptable only if its CSS/JS is structured and labelled; complex UX may use \`css/\` and \`js/\` files.
|
||||
|
||||
### H. Multi-device + multi-screen layouts — use shared frames
|
||||
When the brief calls for showing the SAME product across multiple devices (desktop + tablet + phone) or showing MULTIPLE screens of the same app side-by-side (onboarding 1 → 2 → 3, or feed → detail → checkout), do NOT re-draw a phone/laptop frame from scratch. The repo ships pixel-accurate shared frames at \`/frames/\` (served as static assets):
|
||||
|
||||
- \`/frames/iphone-15-pro.html\` — 390 × 844, Dynamic Island
|
||||
|
|
@ -245,7 +259,7 @@ Then in \`index.html\` use:
|
|||
width="390" height="844" loading="lazy"></iframe>
|
||||
\`\`\`
|
||||
|
||||
The single-screen \`mobile-app\` skill already inlines the iPhone frame in its seed; you only need the shared frames for the multi-device / multi-screen case. Don't re-draw — use these.
|
||||
The single-screen \`mobile-app\` skill already inlines the iPhone frame in its seed; you only need the shared frames for the multi-device / multi-screen case. Don't re-draw — use these. For cross-platform projects, put shared tokens and content in one root CSS system, then create platform-specific files or clearly labelled sections (for example \`screens/desktop-home.html\`, \`screens/ios-home.html\`, \`screens/android-home.html\`) so reviewers can compare native adaptations side by side.
|
||||
|
||||
### I. Restraint over ornament
|
||||
"One thousand no's for every yes." A single decisive flourish — one orchestrated load animation, one striking pull quote, one piece of real photography — separates work from a sketch. Three competing flourishes turn it back into noise.
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ PDFs, PPTX, DOCX: you can extract them via Bash (\`unzip\`, \`pdftotext\`, etc.)
|
|||
- Keep individual files under ~1000 lines. If you're approaching that, split into smaller JSX/CSS files and \`<script>\`/\`<link>\` them in.
|
||||
- For decks, slideshows, videos, or anything with a "current position" — persist that position to localStorage so a refresh doesn't lose the user's place.
|
||||
- Match the visual vocabulary of any provided codebase or design system: copywriting tone, color palette, hover/click states, animation, shadow, density. Think out loud about what you observe before you start writing.
|
||||
- **Color usage**: prefer the active design system's palette. If you must extend it, define harmonious colors with \`oklch()\` rather than inventing hex from scratch.
|
||||
- **Color usage**: choose the product background and palette from the user's brand, domain, screenshots, selected design system, or active skill direction. Do not inherit Open Design app chrome colors. Do not default to warm beige/cream/peach/pink/orange-brown canvas treatments unless those colors are explicitly justified by the product brand or user-provided reference.
|
||||
- Don't use \`scrollIntoView\` — it can break the embedded preview. Use other DOM scroll methods.
|
||||
|
||||
## Content guidelines
|
||||
|
|
@ -64,7 +64,7 @@ PDFs, PPTX, DOCX: you can extract them via Bash (\`unzip\`, \`pdftotext\`, etc.)
|
|||
- **Ask before adding material.** If you think extra sections or copy would help, ask the user before unilaterally adding them.
|
||||
- **Vocalize the system up front.** After exploring resources, state the system you'll use (background colors, type scale, layout patterns) before you start building. This gives the user a chance to redirect cheaply.
|
||||
- **Use appropriate scales.** 1920×1080 slide text is never smaller than 24px. Mobile hit targets are at least 44px. 12pt minimum for print.
|
||||
- **Avoid AI slop tropes:** aggressive gradient backgrounds, gratuitous emoji, rounded boxes with a left-border accent, SVG-as-illustration when a placeholder would do, overused fonts (Inter, Roboto, Arial, Fraunces).
|
||||
- **Avoid AI slop tropes:** aggressive gradient backgrounds; gratuitous emoji; rounded boxes with a left-border accent; SVG-as-illustration when a placeholder would do; overused fonts (Inter, Roboto, Arial, Fraunces); and the generic warm beige/peach/pink/orange-brown “AI canvas” look when it is not brand-led.
|
||||
- **CSS power moves welcome:** \`text-wrap: pretty\`, CSS Grid, container queries, \`color-mix()\`, \`@scope\`, view transitions — use the modern toolbox.
|
||||
|
||||
## React + Babel (inline JSX)
|
||||
|
|
|
|||
|
|
@ -218,6 +218,54 @@ function renderMetadataBlock(
|
|||
);
|
||||
lines.push('');
|
||||
lines.push(`- **kind**: ${metadata.kind}`);
|
||||
if (metadata.platform) {
|
||||
lines.push(`- **platform**: ${metadata.platform}`);
|
||||
} else if (metadata.kind === 'prototype' || metadata.kind === 'template' || metadata.kind === 'other') {
|
||||
lines.push('- **platform**: (unknown — ask: responsive web, desktop web, iOS app, Android app, tablet app, or desktop app?)');
|
||||
}
|
||||
if (metadata.platformTargets && metadata.platformTargets.length > 0) {
|
||||
lines.push(`- **platformTargets**: ${metadata.platformTargets.join(', ')}`);
|
||||
}
|
||||
if (metadata.platform === 'responsive' || metadata.platformTargets?.includes('responsive')) {
|
||||
lines.push(
|
||||
'- **responsive web contract**: `responsive` means one web product experience that adapts across modern browser/device ranges, not only legacy desktop/tablet/mobile buckets. It is not an iOS app, Android app, or native tablet app target. Show responsive behavior through real product layout changes; do not render viewport labels as user-facing product content. Cover 2025–2026 breakpoints: mobile compact 360px, mobile standard 390–430px, foldable/small tablet 600–744px, tablet portrait 768–834px, tablet landscape/large tablet 1024–1180px, laptop 1280–1366px, desktop 1440–1536px, and wide 1920px. Use fluid `clamp()` scales, container queries where useful, and explicit layout changes at semantic thresholds. Verify no horizontal scroll at 360px, 390px, 430px, 768px, 820px, 1024px, 1366px, 1440px, and 1920px unless the brief explicitly asks for a pan/board canvas.',
|
||||
);
|
||||
}
|
||||
if ((metadata.platformTargets?.length ?? 0) > 1) {
|
||||
lines.push(
|
||||
'- **cross-platform deliverable rule**: each selected target keeps the same product goal but MUST be delivered as its own product screen/file when more than one concrete target is selected. Use clear files such as `landing.html` (if enabled), `mobile-ios.html`, `mobile-android.html`, `tablet.html`, `desktop.html`, plus shared `css/` and `js/` when useful. `index.html` may be a launcher/overview that links to these files, but it must not be the only place where mobile/tablet/desktop designs live. Do not collapse cross-platform work into a single tabbed demo, selector UI, comparison board, platform map, or labelled documentation section inside one mock product page.',
|
||||
);
|
||||
}
|
||||
if (metadata.kind === 'prototype' || metadata.kind === 'template' || metadata.kind === 'other') {
|
||||
lines.push(
|
||||
'- **screen-file-first rule**: each distinct user-facing screen or surface MUST be delivered as its own HTML file unless the user explicitly asks for a single-page scroll or single-file artifact. Do not combine landing pages, product app screens, dashboards, history, pricing, settings, mobile app, tablet app, desktop app, or OS widget surfaces into one long page. Use `index.html` as a launcher/overview that links to screen files when more than one screen exists; it may summarize the product and show screen cards, but it must not contain the full design for every screen.',
|
||||
);
|
||||
lines.push(
|
||||
'- **product-realism rule**: final artifacts must look like real end-user product UI. Do not render project metadata, screen counts, target counts, state counts, "demo only" labels, "settings" panels for choosing platforms, "full design target" badges, viewport/device selector controls, theme/style knobs, platform output maps, behavior-spec sections, or design-process cards inside the product unless the user explicitly asks for a design spec/dashboard. Any navigation/tabs inside the artifact must be real product navigation, not designer controls for switching generated mockups.',
|
||||
);
|
||||
lines.push(
|
||||
'- **visual-system rule**: when the user does not specify colors, layout, or visual direction, you must still make an intentional product-appropriate visual system. Infer a palette from the product category and audience with at least: neutral surface tokens, a primary action color, a secondary/domain accent, and status colors. Avoid plain monochrome/unstyled greyscale outputs. Use tasteful gradients, illustrations, iconography, device/product mockups, and colored state moments where they clarify the product, while still avoiding generic beige/peach/pink/brown AI washes.',
|
||||
);
|
||||
lines.push(
|
||||
'- **app-specific modules rule**: include domain-specific in-app modules/components by default (cards, panels, controls, charts, lists, quick actions, status modules, mini players, checkout/cart summaries, etc. as appropriate). These are product UI modules, not OS home-screen widgets. Give each major module a clear purpose, states, and responsive behavior instead of generic card grids.',
|
||||
);
|
||||
lines.push(
|
||||
'- **CJX-ready UX rule**: the artifact must be implementation-ready, not a static screenshot. Structure CSS tokens/components/responsive sections clearly; include real JavaScript behavior for meaningful UX such as tabs, dialogs, drawers, filters, generation/copy actions, validation, playback controls, or state transitions. If keeping a self-contained `index.html`, put the CSS/JS in clearly labelled blocks; for complex UX, generate `css/` and `js/` files when useful.',
|
||||
);
|
||||
lines.push(
|
||||
'- **interaction-fidelity rule**: when the requested screen includes user input, generation, copying, validation, login, checkout, filtering, or any action verb, build real interactive controls for that screen. Do not substitute static text rows, prefilled-only mockups, screenshot-like device frames, or decorative state cards for editable inputs and working actions.',
|
||||
);
|
||||
}
|
||||
if (metadata.includeLandingPage) {
|
||||
lines.push(
|
||||
'- **includeLandingPage**: true — create `landing.html` as a separate responsive marketing companion surface in addition to the selected product/app screens. Do not implement the landing page only as a section inside `index.html`, even for responsive-web-only projects. If there is a working product/app screen, create it as a separate file such as `app.html`, `dashboard.html`, or a domain-specific screen name. `index.html` should be a lightweight launcher/overview when multiple files exist. Include hero, value props, product screenshots/device mockups, proof/features, and an appropriate CTA such as waitlist, download, or contact sales.',
|
||||
);
|
||||
}
|
||||
if (metadata.includeOsWidgets) {
|
||||
lines.push(
|
||||
'- **includeOsWidgets**: true — add platform-native OS home-screen / lock-screen / quick-access widget surfaces where relevant. These are outside-the-app widgets (for example iOS WidgetKit, Android home screen widget, Live Activity/lock screen, tablet glance panel), not in-app cards. Include realistic widget sizes and direct quick actions for the domain.',
|
||||
);
|
||||
}
|
||||
if (metadata.intent === 'live-artifact') {
|
||||
lines.push(
|
||||
'- **intent**: live-artifact — the user chose New live artifact. The first output should be a live artifact/dashboard/report, not a one-off static mockup. Prefer the `live-artifact` skill workflow when available, keep source data compact, and register through the daemon live-artifact tool path once that wrapper/tooling is available.',
|
||||
|
|
|
|||
Loading…
Reference in a new issue