opencode-design-system 0.1.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +244 -0
- package/README.md +120 -127
- package/README.pt-BR.md +244 -0
- package/dist/index.js +212 -141
- package/package.json +3 -1
- package/templates/generate-preview.mjs +38 -68
- package/templates/preview-renderer.d.mts +33 -0
- package/templates/preview-renderer.mjs +199 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import path5 from "path";
|
|
|
4
4
|
import { Plugin } from "@opencode/plugin";
|
|
5
5
|
|
|
6
6
|
// src/generator.ts
|
|
7
|
-
import { mkdir as mkdir2, readFile as readFile2, readdir, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
7
|
+
import { lstat, mkdir as mkdir2, readFile as readFile2, readdir, rename as rename2, rm as rm2, rmdir, writeFile as writeFile2 } from "fs/promises";
|
|
8
8
|
import path3 from "path";
|
|
9
9
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
10
10
|
|
|
@@ -118,48 +118,9 @@ function projectAgentsBlock(systemName) {
|
|
|
118
118
|
|
|
119
119
|
This repository has a framework-neutral Design System at [design-system/manifest.json](design-system/manifest.json). Before creating or changing UI, read [design-system/AI-GUIDELINES.md](design-system/AI-GUIDELINES.md) and discover the relevant tokens, components, and patterns through the manifest. Load only task-relevant documents; the structured Markdown and JSON files are authoritative, and [design-system/preview/index.html](design-system/preview/index.html) is generated visualization only.
|
|
120
120
|
|
|
121
|
-
Honor explicit decisions in [design-system/preferences.json](design-system/preferences.json) and [design-system/DECISIONS.md](design-system/DECISIONS.md). Do not add arbitrary visual values or redesign existing identity during analysis. A screen brief is a design artifact in [design-system/screens/](design-system/screens/); implementation is a separate step. This guidance is intentionally tool- and framework-independent
|
|
121
|
+
Honor explicit decisions in [design-system/preferences.json](design-system/preferences.json) and [design-system/DECISIONS.md](design-system/DECISIONS.md). Do not add arbitrary visual values or redesign existing identity during analysis. A screen brief is a design artifact in [design-system/screens/](design-system/screens/); implementation is a separate step. This guidance is intentionally tool- and framework-independent: any coding agent can use the Design System without this plugin or project-local copies of plugin agents, commands, or skills.
|
|
122
122
|
${AGENTS_END}`;
|
|
123
123
|
}
|
|
124
|
-
var PORTABLE_SKILL = `---
|
|
125
|
-
name: Design System
|
|
126
|
-
description: Apply the project's design-system tokens, components, patterns, and accessibility guidance to UI design and implementation.
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
Before any UI task:
|
|
130
|
-
|
|
131
|
-
1. Check whether design-system/manifest.json exists. If it does not, continue normally without inventing a system.
|
|
132
|
-
2. Read the manifest, AI-GUIDELINES.md, and preferences.json. Use manifest paths to discover relevant documents.
|
|
133
|
-
3. Load only tokens and component/pattern documents needed by this task. For a form, for example, load input/select/button plus the form pattern; do not read every component.
|
|
134
|
-
4. Treat explicit preferences and DECISIONS.md as user-owned constraints. Ask before changing a design identity decision; do not silently override it.
|
|
135
|
-
5. Use documented semantic tokens and components; preserve responsive, state, keyboard, focus, and accessibility requirements.
|
|
136
|
-
6. For design-only requests, write a screen brief to design-system/screens/<screen-name>.md and do not implement application code unless requested.
|
|
137
|
-
7. The preview is generated from structured tokens and specifications. Never use it as the only source of truth.
|
|
138
|
-
|
|
139
|
-
For larger systems, read only the relevant paths discovered from the manifest and keep unrelated documentation out of context.
|
|
140
|
-
`;
|
|
141
|
-
var DESIGNER_AGENT = `---
|
|
142
|
-
description: Collaborates with users to create and evolve original, accessible UI design systems.
|
|
143
|
-
mode: subagent
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
You are a senior UI/UX designer, accessibility specialist, and design-system architect. Collaborate in natural language. Start by understanding the product, audience, desired mood, references, explicit avoidances, platforms, and relevant accessibility needs. Ask only a few high-value questions when identity choices are unclear; offer concrete alternatives in everyday language. Do not turn the process into a long questionnaire, and do not decide identity questions on the user's behalf.
|
|
147
|
-
|
|
148
|
-
Explicit preferences have priority. Record them structurally, repeat them into AI-GUIDELINES.md, and preserve them in every update. You may explain contrast or usability tradeoffs, but ask before departing from an explicit request. Build an original visual language rather than copying a known design system.
|
|
149
|
-
|
|
150
|
-
For an existing application, use the read-only project analysis tool when available; otherwise inspect likely UI/style files without changing them. Preserve its recognizable identity by default. Distinguish probable accidents from intentional variants, explain evidence and uncertainty, and ask the user before normalizing ambiguous inconsistencies. Analysis never authorizes changing application files.
|
|
151
|
-
|
|
152
|
-
When enough direction is known, summarize the proposed direction and ask for confirmation before committing a substantial initial system. Use Design System tools when available; otherwise create the documented Markdown/JSON files directly and use the included preview generator. Keep the system's status as draft/review until the user accepts it. Explain what changed and any unresolved choices.
|
|
153
|
-
`;
|
|
154
|
-
var SCREEN_AGENT = `---
|
|
155
|
-
description: Produces implementation-ready screen specifications using the project's Design System.
|
|
156
|
-
mode: subagent
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
You are a UI/UX screen designer. Before designing, check design-system/manifest.json and follow AI-GUIDELINES.md. Load only the tokens, components, and patterns relevant to the requested screen. Understand the user's task, hierarchy, content, states, interactions, responsive behavior, and accessibility. Reuse the system; flag a missing reusable component rather than silently inventing a design language.
|
|
160
|
-
|
|
161
|
-
Design is separate from code implementation. Produce a concise, implementation-ready Markdown specification with purpose, layout, hierarchy, components and token references, data/content, interactions and states, responsive behavior, and accessibility. Save it under design-system/screens/<kebab-case-name>.md using the screen-spec tool when available; otherwise write the Markdown file directly. Do not write React/Vue/etc. unless the user separately requests implementation. If no system exists, state that and create a coherent brief without claiming it follows a nonexistent system.
|
|
162
|
-
`;
|
|
163
124
|
function list(items) {
|
|
164
125
|
return items?.length ? items.map((item) => `- ${item}`).join("\n") : "- None specified.";
|
|
165
126
|
}
|
|
@@ -202,17 +163,6 @@ async function atomicWrite(root, relativePath, content) {
|
|
|
202
163
|
throw error;
|
|
203
164
|
}
|
|
204
165
|
}
|
|
205
|
-
async function writeIfAbsent(root, relativePath, content) {
|
|
206
|
-
const destination = resolveInside(root, relativePath);
|
|
207
|
-
await mkdir(path2.dirname(destination), { recursive: true });
|
|
208
|
-
try {
|
|
209
|
-
await writeFile(destination, content, { encoding: "utf8", flag: "wx" });
|
|
210
|
-
return true;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
if (error.code === "EEXIST") return false;
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
166
|
async function updateManagedBlock(root, relativePath, startMarker, endMarker, block) {
|
|
217
167
|
let current = "";
|
|
218
168
|
try {
|
|
@@ -318,23 +268,59 @@ function validateTokens(value) {
|
|
|
318
268
|
return errors;
|
|
319
269
|
}
|
|
320
270
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
271
|
+
// templates/preview-renderer.mjs
|
|
272
|
+
var TOKEN_ROLE_PATHS = {
|
|
273
|
+
canvas: ["color.canvas", "color.surface.base", "surface.canvas"],
|
|
274
|
+
surface: ["color.surface", "color.surface.raised", "surface.raised"],
|
|
275
|
+
text: ["color.text", "color.text.primary", "text.primary"],
|
|
276
|
+
muted: ["color.muted", "color.text.secondary", "text.secondary"],
|
|
277
|
+
brand: ["color.brand", "color.accent.primary", "color.primary", "brand"],
|
|
278
|
+
brandHover: ["color.brandHover", "color.accent.hover", "color.accent.primary.hover", "color.brand.hover"],
|
|
279
|
+
brandSubtle: ["color.brandSubtle", "color.accent.subtle", "color.brand.subtle"],
|
|
280
|
+
onBrand: ["color.onBrand", "color.onAccent", "color.on.accent", "color.text.onBrand"],
|
|
281
|
+
border: ["color.border", "color.border.subtle", "border.subtle"],
|
|
282
|
+
borderStrong: ["color.border.strong", "border.strong", "color.border"],
|
|
283
|
+
focus: ["color.focus", "color.focus.ring", "color.focusRing", "focus.ring"],
|
|
284
|
+
success: ["color.success", "color.status.success", "color.status.positive", "status.success"],
|
|
285
|
+
warning: ["color.warning", "color.status.warning", "status.warning"],
|
|
286
|
+
danger: ["color.danger", "color.status.danger", "status.danger"],
|
|
287
|
+
controlRadius: ["radius.control", "borderRadius.control"],
|
|
288
|
+
cardRadius: ["radius.card", "borderRadius.card"],
|
|
289
|
+
tagRadius: ["radius.tag", "radius.pill", "borderRadius.tag"],
|
|
290
|
+
bodyFont: ["typography.body", "typography.fontFamily.sans", "typography.font-family-sans"]
|
|
291
|
+
};
|
|
292
|
+
var DEFAULT_ROLES = {
|
|
293
|
+
canvas: "#f5f3f0",
|
|
294
|
+
surface: "#ffffff",
|
|
295
|
+
text: "#292724",
|
|
296
|
+
muted: "#6b6862",
|
|
297
|
+
brand: "#5b524b",
|
|
298
|
+
brandHover: "#4a433d",
|
|
299
|
+
brandSubtle: "#e9e4df",
|
|
300
|
+
onBrand: "#ffffff",
|
|
301
|
+
border: "#d9d4ce",
|
|
302
|
+
borderStrong: "#b9b1a8",
|
|
303
|
+
focus: "#8a6d52",
|
|
304
|
+
success: "#43735a",
|
|
305
|
+
warning: "#946b2e",
|
|
306
|
+
danger: "#9b4a47",
|
|
307
|
+
controlRadius: "0.5rem",
|
|
308
|
+
cardRadius: "0.75rem",
|
|
309
|
+
tagRadius: "9999px",
|
|
310
|
+
bodyFont: "Inter, ui-sans-serif, system-ui, sans-serif"
|
|
311
|
+
};
|
|
312
|
+
function createPreviewHtml({ manifest, tokens, components, patterns }) {
|
|
313
|
+
const themes = tokens?.themes && typeof tokens.themes === "object" ? tokens.themes : {};
|
|
314
|
+
const themeNames = Object.keys(themes);
|
|
315
|
+
const firstTheme = themeNames[0] ?? "light";
|
|
316
|
+
const themeData = Object.fromEntries(themeNames.map((name) => [name, normalizeTheme(themes[name])]));
|
|
317
|
+
const componentCards = components.map((component, index) => componentCard(component, index)).join("\n");
|
|
318
|
+
const patternCards = patterns.map((pattern) => `
|
|
328
319
|
<article class="spec-card">
|
|
329
|
-
<
|
|
330
|
-
<p>${escapeHtml(
|
|
331
|
-
<
|
|
332
|
-
${component.name.toLowerCase().includes("button") ? `<button class="button" type="button">${escapeHtml(component.name)}</button><button class="button secondary" type="button">Secondary</button><button class="button" type="button" disabled>Disabled</button>` : component.name.toLowerCase().includes("input") || component.name.toLowerCase().includes("search") ? `<label class="field-label">${escapeHtml(component.name)}<input type="text" placeholder="Enter a value" /></label><label class="field-label">Error state<input class="field-error" aria-invalid="true" value="Check this value" /></label>` : `<button class="button secondary" type="button">${escapeHtml(component.name)} example</button>`}
|
|
333
|
-
</div>
|
|
334
|
-
${component.tokens?.length ? `<small>Tokens: ${component.tokens.map((token) => `<code>${escapeHtml(token)}</code>`).join(" ")}</small>` : ""}
|
|
320
|
+
<p class="eyebrow">Pattern</p><h3>${escapeHtml(pattern.name)}</h3>
|
|
321
|
+
<p>${escapeHtml(pattern.purpose)}</p>
|
|
322
|
+
<p class="muted">${escapeHtml(pattern.guidance && pattern.guidance !== pattern.purpose ? pattern.guidance : pattern.composition?.join(" \xB7 ") || pattern.guidance || "")}</p>
|
|
335
323
|
</article>`).join("\n");
|
|
336
|
-
const patternCards = patterns.map((pattern) => `
|
|
337
|
-
<article class="spec-card"><p class="eyebrow">Pattern</p><h3>${escapeHtml(pattern.name)}</h3><p>${escapeHtml(pattern.purpose)}</p><p class="muted">${escapeHtml(pattern.guidance ?? pattern.composition?.join(" \xB7 ") ?? "")}</p></article>`).join("\n");
|
|
338
324
|
const componentIndex = manifest.components.map((item) => `<li><a href="../${escapeHtml(item.file)}">${escapeHtml(item.name)}</a></li>`).join("");
|
|
339
325
|
const patternIndex = manifest.patterns.map((item) => `<li><a href="../${escapeHtml(item.file)}">${escapeHtml(item.name)}</a></li>`).join("");
|
|
340
326
|
return `<!doctype html>
|
|
@@ -345,14 +331,17 @@ function createPreviewHtml(input) {
|
|
|
345
331
|
<meta name="description" content="Generated interactive preview for ${escapeHtml(manifest.name)}." />
|
|
346
332
|
<title>${escapeHtml(manifest.name)} \u2014 Design System</title>
|
|
347
333
|
<style>
|
|
348
|
-
:root{
|
|
349
|
-
*{box-sizing:border-box}body{margin:0;background:var(--
|
|
350
|
-
.shell{min-height:100vh;display:grid;grid-template-columns:250px minmax(0,1fr)}.sidebar{padding:28px 20px;border-right:1px solid var(--
|
|
351
|
-
main{min-width:0;padding:34px clamp(20px,5vw,72px) 72px;max-width:1440px}.topbar{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:34px}.eyebrow{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;font-weight:700;color:var(--
|
|
352
|
-
section{margin-top:54px;scroll-margin-top:20px}.section-heading{display:flex;justify-content:space-between;align-items:end;gap:16px;border-bottom:1px solid var(--
|
|
353
|
-
.
|
|
354
|
-
|
|
355
|
-
|
|
334
|
+
:root{color-scheme:light;--preview-canvas:#f5f3f0;--preview-surface:#fff;--preview-text:#292724;--preview-muted:#6b6862;--preview-brand:#5b524b;--preview-brand-hover:#4a433d;--preview-brand-subtle:#e9e4df;--preview-on-brand:#fff;--preview-border:#d9d4ce;--preview-border-strong:#b9b1a8;--preview-focus:#8a6d52;--preview-success:#43735a;--preview-warning:#946b2e;--preview-danger:#9b4a47;--preview-control-radius:.5rem;--preview-card-radius:.75rem;--preview-tag-radius:9999px;--preview-body-font:Inter,ui-sans-serif,system-ui,sans-serif;font-family:var(--preview-body-font);color:var(--preview-text);background:var(--preview-canvas);font-synthesis:none;line-height:1.5}
|
|
335
|
+
*{box-sizing:border-box}body{margin:0;background:var(--preview-canvas);color:var(--preview-text)}button,input,select{font:inherit}button{cursor:pointer}a{color:var(--preview-brand)}
|
|
336
|
+
.shell{min-height:100vh;display:grid;grid-template-columns:250px minmax(0,1fr)}.sidebar{padding:28px 20px;border-right:1px solid var(--preview-border);background:var(--preview-surface)}.brand{font-size:1.05rem;font-weight:750;margin-bottom:4px}.side-note,.muted{color:var(--preview-muted);font-size:.88rem}.nav{display:grid;gap:6px;margin:28px 0}.nav a{padding:8px 10px;text-decoration:none;border-radius:var(--preview-control-radius);color:var(--preview-muted)}.nav a:hover{background:var(--preview-canvas);color:var(--preview-text)}.nav-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--preview-muted);margin:20px 8px 8px}
|
|
337
|
+
main{min-width:0;padding:34px clamp(20px,5vw,72px) 72px;max-width:1440px}.topbar{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:34px}.eyebrow{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;font-weight:700;color:var(--preview-muted);margin:0 0 5px}.hero h1{font-size:clamp(2rem,4vw,3.4rem);letter-spacing:-.045em;line-height:1.08;margin:0}.hero>p{max-width:720px;color:var(--preview-muted)}.status{display:inline-flex;border:1px solid var(--preview-border);border-radius:var(--preview-tag-radius);padding:3px 10px;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}
|
|
338
|
+
section{margin-top:54px;scroll-margin-top:20px}.section-heading{display:flex;justify-content:space-between;align-items:end;gap:16px;border-bottom:1px solid var(--preview-border);padding-bottom:12px;margin-bottom:18px}.section-heading h2{margin:0;font-size:1.45rem;letter-spacing:-.025em}.section-heading p{margin:0;color:var(--preview-muted);font-size:.9rem}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr));gap:14px}.spec-card,.surface{border:1px solid var(--preview-border);border-radius:var(--preview-card-radius);background:var(--preview-surface);padding:18px}.spec-card h3{margin:0 0 8px;font-size:1.05rem}.spec-card p{color:var(--preview-muted);margin:8px 0}.spec-card small{display:block;margin-top:12px;color:var(--preview-muted)}.spec-heading{display:flex;justify-content:space-between;align-items:center;gap:12px}.tag{border-radius:var(--preview-tag-radius);background:var(--preview-brand-subtle);color:var(--preview-brand);padding:3px 8px;font-size:.75rem}.showcase{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:16px 0 4px}.button{border:1px solid var(--preview-brand);border-radius:var(--preview-control-radius);background:var(--preview-brand);color:var(--preview-on-brand);padding:9px 14px;min-height:40px;font-weight:650;transition:background 140ms ease,border-color 140ms ease,transform 140ms ease,box-shadow 140ms ease}.button:not(.secondary):not(:disabled):hover{background:var(--preview-brand-hover);border-color:var(--preview-brand-hover)}.button.secondary:not(:disabled):hover{background:var(--preview-canvas)}.button:active{transform:translateY(1px)}.button:focus-visible,input:focus-visible,select:focus-visible,.tab:focus-visible,.switch:focus-visible{outline:3px solid var(--preview-focus);outline-offset:2px}.button.secondary{background:var(--preview-surface);color:var(--preview-text);border-color:var(--preview-border-strong)}.button:disabled{opacity:.5;cursor:not-allowed}.button.magnetic{transform:perspective(var(--token-depth-buttonPerspective,600px)) translate3d(var(--magnet-x,0px),var(--magnet-y,0px),0) rotateX(var(--magnet-rx,0deg)) rotateY(var(--magnet-ry,0deg));transform-style:preserve-3d;will-change:transform}.button.magnetic[data-moving="true"]{transition:background 140ms ease,border-color 140ms ease,box-shadow 140ms ease}.button.magnetic:focus-visible{transform:none;will-change:auto}
|
|
339
|
+
.field-label{display:grid;gap:5px;font-size:.82rem;color:var(--preview-text)}.field-label input{min-height:40px;border:1px solid var(--preview-border-strong);border-radius:var(--preview-control-radius);padding:8px 10px;background:var(--preview-canvas);color:var(--preview-text)}.field-label input[aria-invalid="true"]{border-color:var(--preview-danger)}.feedback{min-height:1.5em;font-size:.82rem;color:var(--preview-muted)}.feedback[data-state="error"]{color:var(--preview-danger)}.feedback[data-state="success"]{color:var(--preview-success)}.demo-surface{min-height:72px;padding:14px;border:1px solid var(--preview-border);border-radius:var(--preview-control-radius);background:var(--preview-canvas)}.demo-surface strong{display:block}.badge{display:inline-flex;align-items:center;gap:7px;border-radius:var(--preview-tag-radius);padding:5px 10px;background:var(--preview-brand-subtle);color:var(--preview-brand);font-size:.82rem}.badge::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.badge.success{background:color-mix(in srgb,var(--preview-success) 14%,var(--preview-surface));color:var(--preview-success)}.badge.warning{background:color-mix(in srgb,var(--preview-warning) 14%,var(--preview-surface));color:var(--preview-warning)}.badge.danger{background:color-mix(in srgb,var(--preview-danger) 14%,var(--preview-surface));color:var(--preview-danger)}
|
|
340
|
+
.token-table{display:grid;gap:18px}.token-group h3{font-size:.95rem;margin:0 0 10px;text-transform:capitalize}.swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}.swatch{overflow:hidden;border:1px solid var(--preview-border);border-radius:var(--preview-control-radius);background:var(--preview-surface)}.swatch-color,.token-demo{height:58px;display:grid;place-items:center;border-bottom:1px solid var(--preview-border);background:var(--preview-canvas);overflow:hidden}.swatch-color{background:var(--swatch-color)}.token-demo-sample{display:block;min-width:12px;min-height:8px;background:var(--preview-brand)}.token-demo[data-group="spacing"] .token-demo-sample{height:8px}.token-demo[data-group="typography"] .token-demo-sample{min-width:0;min-height:0;background:transparent;color:var(--preview-text)}.token-label{padding:8px 10px;font-size:.75rem}.token-label code{display:block;overflow-wrap:anywhere;color:var(--preview-muted);font-size:.68rem}
|
|
341
|
+
.magnetic-note{font-size:.75rem;color:var(--preview-muted)}.identity-scene{perspective:var(--token-depth-cardPerspective,var(--token-depth-identityPerspective,1200px));padding:10px 6px 18px;max-width:340px}.identity-card{min-height:190px;padding:18px;border:1px solid var(--preview-border);border-radius:var(--preview-card-radius);background:var(--preview-surface);box-shadow:0 14px 34px color-mix(in srgb,var(--preview-text) 14%,transparent);transform:rotateX(var(--card-rx,0deg)) rotateY(var(--card-ry,0deg)) translateZ(var(--card-lift,0px));transform-style:preserve-3d;transition:transform 240ms ease,box-shadow 240ms ease;will-change:transform}.identity-card[data-moving="true"]{transition:none}.identity-mark{color:var(--preview-brand);font-weight:750;letter-spacing:.04em}.identity-divider{height:1px;margin:12px 0;background:var(--preview-border)}.identity-name{font-size:1.1rem;font-weight:700}.identity-meta{font-size:.78rem;color:var(--preview-muted)}
|
|
342
|
+
.component-nav{padding-left:18px}.component-nav a{color:var(--preview-muted);text-decoration:none}.component-nav a:hover{color:var(--preview-brand)}.warning-note{padding:10px 12px;border:1px solid var(--preview-warning);border-radius:var(--preview-control-radius);color:var(--preview-text);font-size:.85rem}.warning-note[hidden]{display:none}.tabs{display:flex;gap:6px}.tab,.switch{border:1px solid var(--preview-border-strong);border-radius:var(--preview-control-radius);background:var(--preview-surface);color:var(--preview-text);padding:7px 10px}.tab[aria-selected="true"]{border-color:var(--preview-brand);color:var(--preview-brand)}.switch{width:46px;height:26px;padding:2px;border-radius:999px;background:var(--preview-border);position:relative}.switch::after{content:"";display:block;width:20px;height:20px;border-radius:50%;background:var(--preview-surface);transition:transform 140ms ease}.switch[aria-checked="true"]{background:var(--preview-brand)}.switch[aria-checked="true"]::after{transform:translateX(20px)}.switch-row{display:flex;align-items:center;gap:10px}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse;font-size:.85rem}th,td{text-align:left;padding:9px;border-bottom:1px solid var(--preview-border)}.overlay{position:fixed;inset:0;display:none;place-items:center;padding:20px;background:rgb(0 0 0 / .45);z-index:5}.overlay.open{display:grid}.dialog{width:min(100%,440px);padding:22px;border-radius:var(--preview-card-radius);background:var(--preview-surface);box-shadow:0 20px 60px rgb(0 0 0 / .25)}.toast{position:fixed;right:20px;bottom:20px;z-index:8;padding:12px 16px;border-radius:var(--preview-control-radius);background:var(--preview-text);color:var(--preview-canvas);opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity 160ms ease,transform 160ms ease}.toast.show{opacity:1;transform:translateY(0)}
|
|
343
|
+
@media(max-width:760px){.shell{grid-template-columns:1fr}.sidebar{border-right:0;border-bottom:1px solid var(--preview-border);padding:14px 18px}.nav{display:flex;overflow:auto;margin:12px 0 0}.nav a{white-space:nowrap}.sidebar .nav-label,.sidebar .side-note,.sidebar ul{display:none}main{padding:24px 18px 54px}.topbar{align-items:flex-start}.component-nav{display:none}.section-heading{align-items:flex-start;flex-direction:column}}
|
|
344
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}.button.magnetic,.identity-card{transform:none!important;will-change:auto!important}}
|
|
356
345
|
</style>
|
|
357
346
|
</head>
|
|
358
347
|
<body>
|
|
@@ -364,12 +353,13 @@ function createPreviewHtml(input) {
|
|
|
364
353
|
${manifest.patterns.length ? `<div class="nav-label">Patterns</div><ul class="component-nav">${patternIndex}</ul>` : ""}
|
|
365
354
|
</aside>
|
|
366
355
|
<main>
|
|
367
|
-
<div class="topbar"><div class="status">${escapeHtml(manifest.status)}</div><
|
|
356
|
+
<div class="topbar"><div class="status">${escapeHtml(manifest.status)}</div><button class="button secondary" id="theme-toggle" type="button" aria-label="Toggle color theme">Toggle theme</button></div>
|
|
368
357
|
<header id="overview" class="hero"><p class="eyebrow">Framework-neutral design language</p><h1>${escapeHtml(manifest.name)}</h1><p>${escapeHtml(manifest.description)}</p></header>
|
|
358
|
+
<p id="token-warning" class="warning-note" role="status" hidden></p>
|
|
369
359
|
<section id="colors"><div class="section-heading"><div><p class="eyebrow">Foundations</p><h2>Semantic tokens</h2></div><p>Values generated from tokens.json</p></div><div id="token-groups" class="token-table"></div></section>
|
|
370
360
|
<section id="components"><div class="section-heading"><div><p class="eyebrow">Building blocks</p><h2>Components</h2></div><p>${components.length} documented</p></div><div class="grid">${componentCards || `<p class="muted">No components documented yet.</p>`}</div></section>
|
|
371
361
|
<section id="patterns"><div class="section-heading"><div><p class="eyebrow">Compositions</p><h2>Patterns</h2></div><p>${patterns.length} documented</p></div><div class="grid">${patternCards || `<p class="muted">No patterns documented yet.</p>`}</div></section>
|
|
372
|
-
<section id="interactions"><div class="section-heading"><div><p class="eyebrow">Try it</p><h2>Interactive states</h2></div><p>
|
|
362
|
+
<section id="interactions"><div class="section-heading"><div><p class="eyebrow">Try it</p><h2>Interactive states</h2></div><p>Local examples \xB7 no application services</p></div>
|
|
373
363
|
<div class="grid">
|
|
374
364
|
<article class="spec-card"><h3>Tabs</h3><div class="tabs" role="tablist" aria-label="Preview tabs"><button class="tab" role="tab" aria-selected="true" aria-controls="tab-a" id="tab-button-a">General</button><button class="tab" role="tab" aria-selected="false" aria-controls="tab-b" id="tab-button-b" tabindex="-1">Advanced</button></div><div id="tab-a" class="tab-panel" role="tabpanel" aria-labelledby="tab-button-a">General settings are visible.</div><div id="tab-b" class="tab-panel" role="tabpanel" aria-labelledby="tab-button-b" hidden>Advanced options are available here.</div></article>
|
|
375
365
|
<article class="spec-card"><h3>Switch</h3><div class="switch-row"><button class="switch" type="button" role="switch" aria-checked="false" aria-label="Enable notifications"></button><span>Enable notifications</span></div></article>
|
|
@@ -377,53 +367,127 @@ function createPreviewHtml(input) {
|
|
|
377
367
|
<article class="spec-card"><h3>Data table</h3><div class="table-wrap"><table><thead><tr><th>Name</th><th>Status</th><th>Role</th></tr></thead><tbody><tr><td>Jordan Lee</td><td><span class="tag">Active</span></td><td>Editor</td></tr><tr><td>Sam Rivera</td><td>Invited</td><td>Admin</td></tr></tbody></table></div></article>
|
|
378
368
|
</div>
|
|
379
369
|
</section>
|
|
380
|
-
<footer class="
|
|
370
|
+
<footer class="spec-card"><p>Generated from manifest.json, tokens.json, component specifications, and patterns. Edit the structured sources and regenerate this preview.</p></footer>
|
|
381
371
|
</main>
|
|
382
372
|
</div>
|
|
383
|
-
<div class="overlay" id="dialog-overlay" aria-hidden="true"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><h3 id="dialog-title">Confirm an action</h3><p>This dialog demonstrates the documented surface and focus treatment.</p><div class="showcase"><button class="button" type="button" id="close-dialog">Continue</button><button class="button secondary" type="button" id="cancel-dialog">Cancel</button></div></section></div>
|
|
373
|
+
<div class="overlay" id="dialog-overlay" aria-hidden="true"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><h3 id="dialog-title">Confirm an action</h3><p>This local dialog demonstrates the documented surface and focus treatment.</p><div class="showcase"><button class="button" type="button" id="close-dialog">Continue</button><button class="button secondary" type="button" id="cancel-dialog">Cancel</button></div></section></div>
|
|
384
374
|
<div class="toast" id="toast" role="status" aria-live="polite">Changes saved</div>
|
|
385
|
-
<script type="application/json" id="theme-data">${
|
|
375
|
+
<script type="application/json" id="theme-data">${safeJson(themeData)}</script>
|
|
386
376
|
<script>
|
|
387
377
|
const themes=JSON.parse(document.getElementById('theme-data').textContent||'{}');
|
|
388
|
-
|
|
389
|
-
function
|
|
390
|
-
function
|
|
391
|
-
document.getElementById('theme-
|
|
392
|
-
|
|
393
|
-
for(const tab of document.querySelectorAll('[role=tab]'))tab.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight'].includes(event.key))return;event.preventDefault();const tabs=[...document.querySelectorAll('[role=tab]')];const next=(tabs.indexOf(
|
|
378
|
+
const themeNames=Object.keys(themes);const themeToggle=document.getElementById('theme-toggle');
|
|
379
|
+
function flatten(value,prefix='',out=[]){if(value&&typeof value==='object'&&!Array.isArray(value)){for(const [key,item] of Object.entries(value))flatten(item,prefix?prefix+'.'+key:key,out)}else if(['string','number','boolean'].includes(typeof value))out.push({path:prefix,value:String(value)});return out}
|
|
380
|
+
function setTheme(name){const theme=themes[name];if(!theme)return;document.documentElement.dataset.theme=name;document.documentElement.style.colorScheme=/dark/i.test(name)?'dark':'light';for(const [key,value] of Object.entries(theme.roles||{}))document.documentElement.style.setProperty('--preview-'+key.replace(/[A-Z]/g,letter=>'-'+letter.toLowerCase()),value);for(const property of [...document.documentElement.style])if(property.startsWith('--token-'))document.documentElement.style.removeProperty(property);for(const token of flatten(theme.tokens)){if(/^[w-]+(?:.[w-]+)*$/.test(token.path))document.documentElement.style.setProperty('--token-'+token.path.replaceAll('.','-'),token.value)}themeToggle.hidden=themeNames.length<2;renderTokens(theme.tokens);const missing=theme.missing||[];const warning=document.getElementById('token-warning');warning.hidden=missing.length===0;warning.textContent=missing.length?'Some preview roles use neutral defaults because matching semantic tokens were not found: '+missing.join(', ')+'.':''}
|
|
381
|
+
function renderTokens(theme){const holder=document.getElementById('token-groups');holder.replaceChildren();for(const [group,values] of Object.entries(theme||{})){if(!values||typeof values!=='object'||Array.isArray(values))continue;const section=document.createElement('div');section.className='token-group';const heading=document.createElement('h3');heading.textContent=group;section.append(heading);const swatches=document.createElement('div');swatches.className='swatches';for(const token of flatten(values,group)){const card=document.createElement('div');card.className='swatch';const sample=document.createElement('div');if(group==='color'){sample.className='swatch-color';sample.style.setProperty('--swatch-color',token.value)}else{sample.className='token-demo';sample.dataset.group=group;const shape=document.createElement('span');shape.className='token-demo-sample';shape.textContent=group==='typography'?'Aa':'';if(group==='spacing')shape.style.width=token.value;if(group==='radius'){shape.style.width='42px';shape.style.height='28px';shape.style.borderRadius=token.value}if(group==='typography'&&/family|body/i.test(token.path))shape.style.fontFamily=token.value;if(group==='typography'&&/size/i.test(token.path))shape.style.fontSize=token.value;if(group==='elevation')shape.style.boxShadow=token.value;sample.append(shape)}const label=document.createElement('div');label.className='token-label';const name=document.createElement('strong');name.textContent=token.path;const code=document.createElement('code');code.textContent=token.value;label.append(name,code);card.append(sample,label);swatches.append(card)}section.append(swatches);holder.append(section)}}
|
|
382
|
+
themeToggle.addEventListener('click',()=>{const index=themeNames.indexOf(document.documentElement.dataset.theme);setTheme(themeNames[(index+1)%themeNames.length])});
|
|
383
|
+
for(const button of document.querySelectorAll('[role=tab]')){button.addEventListener('click',()=>{for(const tab of document.querySelectorAll('[role=tab]')){const selected=tab===button;tab.setAttribute('aria-selected',String(selected));tab.tabIndex=selected?0:-1;document.getElementById(tab.getAttribute('aria-controls')).hidden=!selected}});button.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight'].includes(event.key))return;event.preventDefault();const tabs=[...document.querySelectorAll('[role=tab]')];const next=(tabs.indexOf(button)+(event.key==='ArrowRight'?1:tabs.length-1))%tabs.length;tabs[next].focus();tabs[next].click()})}
|
|
394
384
|
document.querySelector('[role=switch]').addEventListener('click',event=>{const control=event.currentTarget;control.setAttribute('aria-checked',String(control.getAttribute('aria-checked')!=='true'))});
|
|
395
|
-
|
|
396
|
-
|
|
385
|
+
for(const button of document.querySelectorAll('[data-preview-action]'))button.addEventListener('click',()=>{const output=document.getElementById(button.dataset.feedbackTarget);if(output){output.textContent=button.dataset.previewAction+' activated.';output.dataset.state='success'}});
|
|
386
|
+
for(const form of document.querySelectorAll('[data-preview-form]'))form.addEventListener('submit',event=>{event.preventDefault();const field=form.querySelector('input');const output=form.querySelector('.feedback');const valid=field.checkValidity();field.setAttribute('aria-invalid',String(!valid));output.dataset.state=valid?'success':'error';output.textContent=valid?'Example value is valid; no data was sent.':'Enter a valid value to see the success state.';if(!valid)field.focus()});
|
|
387
|
+
const motion=window.matchMedia('(prefers-reduced-motion: reduce)');const finePointer=window.matchMedia('(hover: hover) and (pointer: fine)');
|
|
388
|
+
function resetMagnet(element){element.dataset.moving='false';for(const key of ['--magnet-x','--magnet-y','--magnet-rx','--magnet-ry'])element.style.removeProperty(key)}
|
|
389
|
+
for(const element of document.querySelectorAll('.magnetic')){let frame=0;element.addEventListener('pointermove',event=>{if(motion.matches||!finePointer.matches||event.pointerType==='touch')return;cancelAnimationFrame(frame);frame=requestAnimationFrame(()=>{const rect=element.getBoundingClientRect();const x=(event.clientX-rect.left-rect.width/2)/rect.width;const y=(event.clientY-rect.top-rect.height/2)/rect.height;const tilt=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--token-depth-buttonTiltMax'))||10;const strength=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--token-depth-buttonTranslation'))||.2;element.dataset.moving='true';element.style.setProperty('--magnet-x',(x*rect.width*strength)+'px');element.style.setProperty('--magnet-y',(y*rect.height*strength)+'px');element.style.setProperty('--magnet-rx',(y*tilt)+'deg');element.style.setProperty('--magnet-ry',(-x*tilt)+'deg')})});for(const type of ['pointerleave','pointercancel','blur'])element.addEventListener(type,()=>{cancelAnimationFrame(frame);resetMagnet(element)});motion.addEventListener('change',()=>resetMagnet(element))}
|
|
390
|
+
for(const card of document.querySelectorAll('[data-tilt-card]')){let frame=0;function reset(){card.dataset.moving='false';for(const key of ['--card-rx','--card-ry','--card-lift'])card.style.removeProperty(key)}card.addEventListener('pointermove',event=>{if(motion.matches||!finePointer.matches||event.pointerType==='touch')return;cancelAnimationFrame(frame);frame=requestAnimationFrame(()=>{const rect=card.getBoundingClientRect();const x=(event.clientX-rect.left-rect.width/2)/rect.width;const y=(event.clientY-rect.top-rect.height/2)/rect.height;const tilt=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--token-depth-cardTiltMax')||getComputedStyle(document.documentElement).getPropertyValue('--token-depth-identityTiltMax'))||12;const lift=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--token-depth-cardLift')||getComputedStyle(document.documentElement).getPropertyValue('--token-depth-identityLiftMax'))||10;card.dataset.moving='true';card.style.setProperty('--card-rx',(y*tilt)+'deg');card.style.setProperty('--card-ry',(-x*tilt)+'deg');card.style.setProperty('--card-lift',lift+'px')})});for(const type of ['pointerleave','pointercancel'])card.addEventListener(type,()=>{cancelAnimationFrame(frame);reset()});motion.addEventListener('change',reset)}
|
|
391
|
+
const overlay=document.getElementById('dialog-overlay');const open=document.getElementById('open-dialog');function closeDialog(){overlay.classList.remove('open');overlay.setAttribute('aria-hidden','true');open.focus()}open.addEventListener('click',()=>{overlay.classList.add('open');overlay.setAttribute('aria-hidden','false');document.getElementById('close-dialog').focus()});document.getElementById('close-dialog').addEventListener('click',closeDialog);document.getElementById('cancel-dialog').addEventListener('click',closeDialog);overlay.addEventListener('click',event=>{if(event.target===overlay)closeDialog()});document.addEventListener('keydown',event=>{if(event.key==='Escape'&&overlay.classList.contains('open'))closeDialog()});let toastTimeout;document.getElementById('show-toast').addEventListener('click',()=>{const toast=document.getElementById('toast');toast.classList.add('show');clearTimeout(toastTimeout);toastTimeout=setTimeout(()=>toast.classList.remove('show'),2200)});
|
|
397
392
|
setTheme(${safeJson(firstTheme)});
|
|
398
393
|
</script>
|
|
399
394
|
</body>
|
|
400
395
|
</html>
|
|
401
396
|
`;
|
|
402
397
|
}
|
|
398
|
+
function normalizeTheme(theme) {
|
|
399
|
+
const roles = {};
|
|
400
|
+
const missing = [];
|
|
401
|
+
for (const [name, paths] of Object.entries(TOKEN_ROLE_PATHS)) {
|
|
402
|
+
const value = firstTokenValue(theme, paths);
|
|
403
|
+
if (value === void 0) {
|
|
404
|
+
roles[name] = DEFAULT_ROLES[name];
|
|
405
|
+
if (["canvas", "surface", "text", "brand", "border"].includes(name)) missing.push(name);
|
|
406
|
+
} else roles[name] = value;
|
|
407
|
+
}
|
|
408
|
+
return { roles, missing, tokens: theme };
|
|
409
|
+
}
|
|
410
|
+
function firstTokenValue(theme, paths) {
|
|
411
|
+
for (const path6 of paths) {
|
|
412
|
+
let value = theme;
|
|
413
|
+
for (const segment of path6.split(".")) value = value && typeof value === "object" ? value[segment] : void 0;
|
|
414
|
+
if ((typeof value === "string" || typeof value === "number") && String(value).trim()) return String(value);
|
|
415
|
+
}
|
|
416
|
+
return void 0;
|
|
417
|
+
}
|
|
418
|
+
function componentCard(component, index) {
|
|
419
|
+
const name = String(component.name ?? "Component");
|
|
420
|
+
const tokens = component.tokens ?? [];
|
|
421
|
+
const evidence = [name, component.behavior, ...component.variants ?? [], ...component.states ?? [], ...tokens].filter(Boolean).join(" ");
|
|
422
|
+
const isButton = /button|action/i.test(name);
|
|
423
|
+
const isField = /input|field|search/i.test(name);
|
|
424
|
+
const isStatus = /status|indicator|badge/i.test(name);
|
|
425
|
+
const isIdentityCard = /profile|identity|credential/i.test(name) && /card|identity|credential/i.test(name) || tokens.some((token) => /depth\.(card|identity)(Perspective|TiltMax|Lift)/i.test(token));
|
|
426
|
+
const magnetic = isButton && (/magnet/i.test(evidence) || tokens.some((token) => /depth\.(button|magnetic)/i.test(token)));
|
|
427
|
+
let demo;
|
|
428
|
+
if (isIdentityCard) demo = identityCardDemo(name);
|
|
429
|
+
else if (isButton) demo = buttonDemo(name, index, magnetic);
|
|
430
|
+
else if (isField) demo = fieldDemo(name, index);
|
|
431
|
+
else if (isStatus) demo = `<div class="showcase"><span class="badge">In progress</span><span class="badge success">Complete</span><span class="badge warning">Needs review</span><span class="badge danger">Blocked</span></div>`;
|
|
432
|
+
else demo = `<div class="demo-surface"><strong>${escapeHtml(name)} preview</strong><span class="muted">Static sample of the documented component.</span></div>`;
|
|
433
|
+
const tokensHtml = tokens.length ? `<small>Tokens: ${tokens.map((token) => `<code>${escapeHtml(token)}</code>`).join(" ")}</small>` : "";
|
|
434
|
+
return `
|
|
435
|
+
<article class="spec-card">
|
|
436
|
+
<div class="spec-heading"><div><p class="eyebrow">Component</p><h3>${escapeHtml(name)}</h3></div><span class="tag">${escapeHtml(component.variants?.[0] ?? "base")}</span></div>
|
|
437
|
+
<p>${escapeHtml(component.purpose ?? "")}</p>
|
|
438
|
+
<div class="showcase">${demo}</div>
|
|
439
|
+
${tokensHtml}
|
|
440
|
+
</article>`;
|
|
441
|
+
}
|
|
442
|
+
function buttonDemo(name, index, magnetic) {
|
|
443
|
+
const className = magnetic ? "button magnetic" : "button";
|
|
444
|
+
const hint = magnetic ? `<span class="magnetic-note">Move the pointer to try the magnetic response.</span>` : "";
|
|
445
|
+
return `<div class="showcase"><button class="${className}" type="button" data-preview-action="${escapeHtml(name)}" data-feedback-target="component-feedback-${index}">${escapeHtml(name)} action</button><button class="button secondary" type="button" data-preview-action="Secondary" data-feedback-target="component-feedback-${index}">Secondary</button><button class="button" type="button" disabled>Disabled</button></div><p class="feedback" id="component-feedback-${index}" role="status" aria-live="polite"></p>${hint}`;
|
|
446
|
+
}
|
|
447
|
+
function fieldDemo(name, index) {
|
|
448
|
+
const id = `preview-field-${index}`;
|
|
449
|
+
return `<form data-preview-form><label class="field-label" for="${id}">${escapeHtml(name)}<input id="${id}" type="email" autocomplete="off" placeholder="name@example.com" required aria-describedby="field-feedback-${index}" /></label><div class="showcase"><button class="button" type="submit">Validate example</button></div><p class="feedback" id="field-feedback-${index}" role="status" aria-live="polite"></p></form>`;
|
|
450
|
+
}
|
|
451
|
+
function identityCardDemo(name) {
|
|
452
|
+
return `<div class="identity-scene"><article class="identity-card" data-tilt-card><div class="identity-mark">${escapeHtml(name)}</div><div class="identity-divider"></div><div class="identity-name">Alex Martin</div><div class="identity-meta">Workshop coordinator \xB7 sample identity</div></article></div><span class="magnetic-note">Move the pointer to explore the 3D identity card.</span>`;
|
|
453
|
+
}
|
|
403
454
|
function escapeHtml(value) {
|
|
404
|
-
return value.replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
455
|
+
return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
405
456
|
}
|
|
406
457
|
function safeJson(value) {
|
|
407
458
|
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => ({ "<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "\u2028": "\\u2028", "\u2029": "\\u2029" })[character]);
|
|
408
459
|
}
|
|
409
460
|
|
|
461
|
+
// src/preview.ts
|
|
462
|
+
function createPreviewHtml2(input) {
|
|
463
|
+
return createPreviewHtml(input);
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
// src/generator.ts
|
|
411
467
|
var SCHEMA_VERSION = "1.0.0";
|
|
412
468
|
var INITIAL_VERSION = "0.1.0";
|
|
413
469
|
async function createDesignSystem(root, input) {
|
|
414
470
|
validateCreateInput(input);
|
|
415
471
|
const target = resolveInside(root, DESIGN_SYSTEM_DIR);
|
|
416
|
-
let
|
|
472
|
+
let existingDirectories = [];
|
|
473
|
+
let targetExists = false;
|
|
417
474
|
try {
|
|
418
|
-
|
|
475
|
+
targetExists = true;
|
|
476
|
+
const inspection = await inspectExistingDirectoryTree(target);
|
|
477
|
+
if (inspection.userOwnedPaths.length > 0) {
|
|
478
|
+
if (await fileExists(root, `${DESIGN_SYSTEM_DIR}/manifest.json`)) {
|
|
479
|
+
throw new Error("A Design System already exists. Use design_system_read, then /design-system:update.");
|
|
480
|
+
}
|
|
481
|
+
const paths = inspection.userOwnedPaths.map((item) => path3.relative(root, item).split(path3.sep).join("/"));
|
|
482
|
+
throw new Error(`design-system/ contains existing user-owned files or links: ${paths.join(", ")}. Review them before creating a system there.`);
|
|
483
|
+
}
|
|
484
|
+
existingDirectories = inspection.directories;
|
|
419
485
|
} catch (error) {
|
|
420
|
-
if (error.code
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
throw new Error("A Design System already exists. Use design_system_read, then /design-system:update.");
|
|
486
|
+
if (error.code === "ENOENT") {
|
|
487
|
+
targetExists = false;
|
|
488
|
+
} else {
|
|
489
|
+
throw error;
|
|
425
490
|
}
|
|
426
|
-
throw new Error("design-system/ already contains user files. Move or review them before creating a system there.");
|
|
427
491
|
}
|
|
428
492
|
const preferences = input.preferences ?? [];
|
|
429
493
|
const tokens = { ...input.tokens, schemaVersion: typeof input.tokens.schemaVersion === "string" ? input.tokens.schemaVersion : SCHEMA_VERSION };
|
|
@@ -483,10 +547,17 @@ async function createDesignSystem(root, input) {
|
|
|
483
547
|
["schema/tokens.schema.json", pretty(tokensSchema)],
|
|
484
548
|
["README.md", systemReadme(manifest)],
|
|
485
549
|
["tools/generate-preview.mjs", await readFile2(new URL("../templates/generate-preview.mjs", import.meta.url), "utf8")],
|
|
486
|
-
["preview
|
|
550
|
+
["tools/preview-renderer.mjs", await readFile2(new URL("../templates/preview-renderer.mjs", import.meta.url), "utf8")],
|
|
551
|
+
["preview/index.html", createPreviewHtml2({ manifest, tokens, components, patterns })]
|
|
487
552
|
]);
|
|
488
553
|
for (const [index, component] of components.entries()) files.set(manifest.components[index].file, componentMarkdown(component));
|
|
489
554
|
for (const [index, pattern] of patterns.entries()) files.set(manifest.patterns[index].file, patternMarkdown(pattern));
|
|
555
|
+
const generatedFilePaths = new Set(files.keys());
|
|
556
|
+
const directoryConflicts = existingDirectories.map((directory) => ({ directory, relative: path3.relative(target, directory).split(path3.sep).join("/") })).filter(({ relative }) => generatedFilePaths.has(relative));
|
|
557
|
+
if (directoryConflicts.length > 0) {
|
|
558
|
+
const paths = directoryConflicts.map(({ relative }) => `${DESIGN_SYSTEM_DIR}/${relative}`);
|
|
559
|
+
throw new Error(`design-system/ has empty directories where generated files would be written: ${paths.join(", ")}. Review them before creating a system there.`);
|
|
560
|
+
}
|
|
490
561
|
await mkdir2(path3.dirname(target), { recursive: true });
|
|
491
562
|
const staging = path3.join(path3.dirname(target), `.design-system-${randomUUID2()}`);
|
|
492
563
|
try {
|
|
@@ -495,40 +566,31 @@ async function createDesignSystem(root, input) {
|
|
|
495
566
|
await mkdir2(path3.dirname(destination), { recursive: true });
|
|
496
567
|
await writeFile2(destination, content, "utf8");
|
|
497
568
|
}
|
|
498
|
-
if (
|
|
569
|
+
if (targetExists) {
|
|
570
|
+
for (const directory of [...existingDirectories].sort((left, right) => right.length - left.length)) {
|
|
571
|
+
await rmdir(directory);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
499
574
|
await rename2(staging, target);
|
|
575
|
+
if (targetExists) await restoreEmptyDirectories(existingDirectories);
|
|
500
576
|
} catch (error) {
|
|
501
577
|
await rm2(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
578
|
+
if (existingDirectories.length > 0) await restoreEmptyDirectories(existingDirectories);
|
|
579
|
+
const code = error.code;
|
|
580
|
+
if (targetExists && (code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM")) {
|
|
581
|
+
throw new Error("design-system/ changed during creation or contains user-owned content. Existing files were preserved; review the folder and retry.");
|
|
582
|
+
}
|
|
502
583
|
throw error;
|
|
503
584
|
}
|
|
504
|
-
const conflicts = [];
|
|
505
585
|
await updateManagedBlock(root, "AGENTS.md", "<!-- opencode-design-system:start -->", "<!-- opencode-design-system:end -->", projectAgentsBlock(manifest.name));
|
|
506
|
-
|
|
507
|
-
[".opencode/skills/design-system/SKILL.md", PORTABLE_SKILL],
|
|
508
|
-
[".opencode/agents/design-system-designer.md", DESIGNER_AGENT],
|
|
509
|
-
[".opencode/agents/screen-designer.md", SCREEN_AGENT],
|
|
510
|
-
[".opencode/commands/design-system.md", `Create or continue the project's Design System. Treat the user as a collaborator: ask only relevant design identity questions, preserve explicit preferences, inspect existing UI read-only when appropriate, then create the framework-neutral files in design-system/. Read AGENTS.md and design-system/AI-GUIDELINES.md. If the OpenCode Design System tools are available, use them to validate and regenerate the preview. Do not edit application UI during analysis. User request: $ARGUMENTS
|
|
511
|
-
`],
|
|
512
|
-
[".opencode/commands/design-system/update.md", `Update the existing Design System in response to: $ARGUMENTS. Read its manifest, preferences, relevant tokens and component/pattern documents first. Explain affected parts and ask only if an identity choice is ambiguous. Keep the user's explicit preferences. Apply semantic updates, record the decision, update the version/changelog, run consistency checks, and regenerate the preview from tokens/specifications. If plugin tools are not available, edit the structured Markdown/JSON deliberately and run node design-system/tools/generate-preview.mjs. Do not use blind search/replace and do not modify application components unless explicitly asked.
|
|
513
|
-
`],
|
|
514
|
-
[".opencode/commands/design-system/preview.md", `Regenerate design-system/preview/index.html from manifest.json, tokens.json, component documents, and patterns. Treat structured Markdown/JSON as the source of truth. If the Design System plugin is available, use its preview tool; otherwise run node design-system/tools/generate-preview.mjs from the project root.
|
|
515
|
-
`],
|
|
516
|
-
[".opencode/commands/design-system/check.md", `Check project UI code against design-system/manifest.json and its semantic tokens. Report unknown values, component/state/layout deviations, and likely inconsistencies with file paths. Do not edit application code. If available, use the Design System check tool.
|
|
517
|
-
`],
|
|
518
|
-
[".opencode/commands/design-screen.md", `Design (do not implement) the requested screen using the Design System. Read AGENTS.md and design-system/AI-GUIDELINES.md, then load only the matching manifest entries, tokens, components, and patterns. Write an implementation-ready brief to design-system/screens/<kebab-case-name>.md with purpose, layout, hierarchy, components/token references, content/data, interactions/states, responsive behavior, and accessibility. If the Design System is absent, say so and write a portable brief without claiming system conformance. Request: $ARGUMENTS
|
|
519
|
-
`]
|
|
520
|
-
];
|
|
521
|
-
for (const [relative, content] of supportFiles) {
|
|
522
|
-
if (!await writeIfAbsent(root, relative, content)) conflicts.push(relative);
|
|
523
|
-
}
|
|
524
|
-
return { success: true, manifest, files: [...files.keys()].map((file) => `${DESIGN_SYSTEM_DIR}/${file}`), conflicts };
|
|
586
|
+
return { success: true, manifest, files: [...files.keys()].map((file) => `${DESIGN_SYSTEM_DIR}/${file}`) };
|
|
525
587
|
}
|
|
526
588
|
async function regeneratePreview(root) {
|
|
527
589
|
const manifest = await readManifest(root);
|
|
528
590
|
const tokens = await readJson(root, `${DESIGN_SYSTEM_DIR}/${manifest.tokens}`);
|
|
529
591
|
const components = await Promise.all(manifest.components.map(async (item) => parseComponent(item.name, await readText2(root, `${DESIGN_SYSTEM_DIR}/${item.file}`), item.tokens)));
|
|
530
592
|
const patterns = await Promise.all(manifest.patterns.map(async (item) => parsePattern(item.name, await readText2(root, `${DESIGN_SYSTEM_DIR}/${item.file}`), item.tokens)));
|
|
531
|
-
const html =
|
|
593
|
+
const html = createPreviewHtml2({ manifest, tokens, components, patterns });
|
|
532
594
|
await atomicWrite(root, `${DESIGN_SYSTEM_DIR}/${manifest.preview}`, html);
|
|
533
595
|
return { preview: `${DESIGN_SYSTEM_DIR}/${manifest.preview}`, componentCount: components.length, patternCount: patterns.length };
|
|
534
596
|
}
|
|
@@ -536,6 +598,30 @@ async function readManifest(root) {
|
|
|
536
598
|
return readJson(root, `${DESIGN_SYSTEM_DIR}/manifest.json`);
|
|
537
599
|
}
|
|
538
600
|
var readText2 = readText;
|
|
601
|
+
async function inspectExistingDirectoryTree(target) {
|
|
602
|
+
const directories = [];
|
|
603
|
+
const userOwnedPaths = [];
|
|
604
|
+
async function visit(directory) {
|
|
605
|
+
const info = await lstat(directory);
|
|
606
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
607
|
+
userOwnedPaths.push(directory);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
directories.push(directory);
|
|
611
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
612
|
+
const child = path3.join(directory, entry.name);
|
|
613
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(child);
|
|
614
|
+
else userOwnedPaths.push(child);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
await visit(target);
|
|
618
|
+
return { directories, userOwnedPaths };
|
|
619
|
+
}
|
|
620
|
+
async function restoreEmptyDirectories(directories) {
|
|
621
|
+
for (const directory of [...directories].sort((left, right) => left.length - right.length)) {
|
|
622
|
+
await mkdir2(directory, { recursive: true }).catch(() => void 0);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
539
625
|
function validateCreateInput(input) {
|
|
540
626
|
if (!input.name?.trim()) throw new Error("name is required");
|
|
541
627
|
if (!input.description?.trim()) throw new Error("description is required");
|
|
@@ -615,7 +701,7 @@ Read the manifest and AI guidelines first. Load only task-relevant component and
|
|
|
615
701
|
|
|
616
702
|
## Plugin-independent maintenance
|
|
617
703
|
|
|
618
|
-
This project includes [tools/generate-preview.mjs](tools/generate-preview.mjs), a dependency-free Node.js renderer. After editing structured tokens/specifications without the plugin, run node design-system/tools/generate-preview.mjs from the project root. The project [AGENTS.md](../AGENTS.md)
|
|
704
|
+
This project includes [tools/generate-preview.mjs](tools/generate-preview.mjs), a dependency-free Node.js renderer. After editing structured tokens/specifications without the plugin, run node design-system/tools/generate-preview.mjs from the project root. The project's [AGENTS.md](../AGENTS.md) block points any coding agent to the portable system; no project-local plugin agents, commands, or skills are required.
|
|
619
705
|
`;
|
|
620
706
|
}
|
|
621
707
|
function pretty(value) {
|
|
@@ -651,7 +737,7 @@ function parsePattern(name, markdown, tokens) {
|
|
|
651
737
|
};
|
|
652
738
|
}
|
|
653
739
|
function section(markdown, heading) {
|
|
654
|
-
const match = markdown.match(new RegExp(`^## ${heading}\\
|
|
740
|
+
const match = markdown.match(new RegExp(`^## ${heading}[ \\t]*\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |(?![\\s\\S]))`, "mi"));
|
|
655
741
|
return match?.[1]?.replace(/^- None specified\.$/m, "").trim() ?? "";
|
|
656
742
|
}
|
|
657
743
|
function bullets(value) {
|
|
@@ -1211,14 +1297,14 @@ var commandPrompts = [
|
|
|
1211
1297
|
{
|
|
1212
1298
|
name: "design-system",
|
|
1213
1299
|
description: "Create a Design System collaboratively, from scratch or from an existing UI",
|
|
1214
|
-
instruction: `Act as
|
|
1300
|
+
instruction: `Act as a collaborative design-system designer. Gather only identity decisions that are genuinely unclear; honor explicit preferences. If a Design System already exists, read it and offer an update/continue path rather than overwriting it. If the repository has UI and the user has not said whether to formalize that UI or start fresh, call the read-only analysis tool and ask which path they prefer; do not assume. Distinguish evidence from inference and ask about important inconsistencies before normalization. For a new system, confirm a concise visual direction before writing files; then call design_system_create with neutral tokens, foundations, explicit preferences, a few useful components and patterns, and source evidence. Keep status draft until reviewed. Do not modify application files.
|
|
1215
1301
|
|
|
1216
1302
|
User request:`
|
|
1217
1303
|
},
|
|
1218
1304
|
{
|
|
1219
1305
|
name: "design-system/update",
|
|
1220
1306
|
description: "Make a coherent, versioned change to the existing Design System",
|
|
1221
|
-
instruction: `
|
|
1307
|
+
instruction: `Work collaboratively as a design-system architect. Read the Design System first using design_system_read. Interpret the request semantically, identify impacted token paths and dependent components/patterns, and honor recorded decisions. If the request conflicts with an explicit preference, ask before changing it. For a clear requested change, apply it with design_system_update, explain the dependency impact, provide revised full componentUpdates/patternUpdates where documented behavior or guidance needs a semantic change, add tokens only when existing semantic paths do not fit and then provide a value for every theme, add reusable components/patterns when composition is insufficient, update preferences/decisions/foundations where appropriate, choose patch/minor/major impact (expansion requires at least minor), and report unresolved references. Do not use blind text replacement and do not modify app UI code.
|
|
1222
1308
|
|
|
1223
1309
|
User request:`
|
|
1224
1310
|
},
|
|
@@ -1239,7 +1325,7 @@ User request:`
|
|
|
1239
1325
|
{
|
|
1240
1326
|
name: "design-screen",
|
|
1241
1327
|
description: "Design a screen specification using relevant Design System documentation",
|
|
1242
|
-
instruction: `
|
|
1328
|
+
instruction: `Act as a UI/UX screen designer. First call design_system_read with the user's task to load only the relevant tokens, components, patterns, preferences, and guidelines. Clarify the screen's purpose and key content when needed, then define hierarchy, layout, data, states, interactions, responsive behavior, and accessibility. Keep design separate from implementation. Call design_system_screen_spec to save an implementation-ready Markdown specification under design-system/screens/. Do not write UI code unless asked separately.
|
|
1243
1329
|
|
|
1244
1330
|
User request:`
|
|
1245
1331
|
}
|
|
@@ -1249,22 +1335,11 @@ var index_default = Plugin.define({
|
|
|
1249
1335
|
async setup(ctx) {
|
|
1250
1336
|
const projectRoot = path5.resolve(ctx.location.project.canonical || ctx.location.directory);
|
|
1251
1337
|
const designSystemPath = path5.join(projectRoot, DESIGN_SYSTEM_DIR, "manifest.json");
|
|
1252
|
-
const skillPath = path5.join(projectRoot, ".opencode", "skills", "design-system", "SKILL.md");
|
|
1253
|
-
await ctx.skill.transform((editor) => {
|
|
1254
|
-
editor.add({
|
|
1255
|
-
id: "design-system",
|
|
1256
|
-
name: "Design System",
|
|
1257
|
-
description: "Apply this project's semantic design tokens, documented components, patterns, preferences, and accessibility guidance to UI work with progressive loading.",
|
|
1258
|
-
path: skillPath,
|
|
1259
|
-
content: skillBody(),
|
|
1260
|
-
autoinvoke: true
|
|
1261
|
-
});
|
|
1262
|
-
});
|
|
1263
1338
|
await ctx.session.hook("context", (event) => {
|
|
1264
1339
|
if (!existsSync(designSystemPath)) return;
|
|
1265
1340
|
event.system.push({
|
|
1266
1341
|
type: "text",
|
|
1267
|
-
text: "This project has a
|
|
1342
|
+
text: "This project has a framework-neutral Design System at design-system/manifest.json. Follow the Design System guidance in AGENTS.md and design-system/AI-GUIDELINES.md; read only the relevant tokens, components, and patterns, and treat the HTML preview as generated output rather than the source of truth."
|
|
1268
1343
|
});
|
|
1269
1344
|
});
|
|
1270
1345
|
await ctx.command.transform((editor) => {
|
|
@@ -1314,7 +1389,6 @@ ${prompt.text.trim()}` : "";
|
|
|
1314
1389
|
},
|
|
1315
1390
|
execute: async (raw) => {
|
|
1316
1391
|
const result = await createDesignSystem(projectRoot, raw);
|
|
1317
|
-
await ctx.skill.reload();
|
|
1318
1392
|
return { content: JSON.stringify(result, null, 2) };
|
|
1319
1393
|
}
|
|
1320
1394
|
});
|
|
@@ -1443,9 +1517,6 @@ var patternSchema = {
|
|
|
1443
1517
|
function stringArray() {
|
|
1444
1518
|
return { type: "array", items: { type: "string" } };
|
|
1445
1519
|
}
|
|
1446
|
-
function skillBody() {
|
|
1447
|
-
return PORTABLE_SKILL.replace(/^---\n[\s\S]*?\n---\n\n/, "");
|
|
1448
|
-
}
|
|
1449
1520
|
async function readRelevantSystem(root, task) {
|
|
1450
1521
|
try {
|
|
1451
1522
|
const manifest = await readManifest(root);
|