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/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 and applies even when the Design System plugin is unavailable.
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
- // src/preview.ts
322
- function createPreviewHtml(input) {
323
- const { manifest, tokens, components, patterns } = input;
324
- const themes = tokens.themes ?? {};
325
- const firstTheme = Object.keys(themes)[0] ?? "light";
326
- const themePayload = safeJson(themes);
327
- const componentCards = components.map((component) => `
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
- <div class="spec-heading"><div><p class="eyebrow">Component</p><h3>${escapeHtml(component.name)}</h3></div><span class="tag">${escapeHtml(component.variants?.[0] ?? "base")}</span></div>
330
- <p>${escapeHtml(component.purpose)}</p>
331
- <div class="showcase">
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{font-family:var(--typography-font-family-sans,Inter,ui-sans-serif,system-ui,sans-serif);color:var(--color-text-primary,#17211f);background:var(--color-surface-base,#f6f8f7);font-synthesis:none;font-optical-sizing:auto;line-height:1.5}
349
- *{box-sizing:border-box}body{margin:0;background:var(--color-surface-base,#f6f8f7);color:var(--color-text-primary,#17211f)}button,input,select{font:inherit}button{cursor:pointer}a{color:var(--color-accent-primary,#236b55)}
350
- .shell{min-height:100vh;display:grid;grid-template-columns:250px minmax(0,1fr)}.sidebar{padding:28px 20px;border-right:1px solid var(--color-border-subtle,#dbe2de);background:var(--color-surface-raised,#fff)}.brand{font-size:1.05rem;font-weight:750;margin-bottom:4px}.side-note,.muted{color:var(--color-text-secondary,#65726d);font-size:.88rem}.nav{display:grid;gap:6px;margin:28px 0}.nav a{padding:8px 10px;text-decoration:none;border-radius:var(--radius-control,6px);color:var(--color-text-secondary,#65726d)}.nav a:hover{background:var(--color-surface-base,#f6f8f7);color:var(--color-text-primary,#17211f)}.nav-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--color-text-secondary,#65726d);margin:20px 8px 8px}
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(--color-text-secondary,#65726d);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(--color-text-secondary,#65726d)}.status{display:inline-flex;border:1px solid var(--color-border-subtle,#dbe2de);border-radius:999px;padding:3px 10px;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}
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(--color-border-subtle,#dbe2de);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(--color-text-secondary,#65726d);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(--color-border-subtle,#dbe2de);border-radius:var(--radius-card,10px);background:var(--color-surface-raised,#fff);padding:18px}.spec-card h3{margin:0 0 8px;font-size:1.05rem}.spec-card p{color:var(--color-text-secondary,#65726d);margin:8px 0}.spec-card small{display:block;margin-top:12px;color:var(--color-text-secondary,#65726d)}.spec-heading{display:flex;justify-content:space-between;align-items:center;gap:12px}.tag{border-radius:var(--radius-tag,4px);background:var(--color-accent-subtle,#e6f0eb);color:var(--color-accent-primary,#236b55);padding:3px 8px;font-size:.75rem}.showcase{display:flex;align-items:end;gap:10px;flex-wrap:wrap;margin:16px 0 4px}.button{border:1px solid var(--color-accent-primary,#236b55);border-radius:var(--radius-control,6px);background:var(--color-accent-primary,#236b55);color:var(--color-on-accent,#fff);padding:9px 14px;min-height:40px;font-weight:650;transition:background 140ms ease,border-color 140ms ease,transform 140ms ease}.button:hover{filter:brightness(.94)}.button:active{transform:translateY(1px)}.button:focus-visible,input:focus-visible,select:focus-visible,.tab:focus-visible{outline:3px solid var(--color-focus-ring,#79b8a0);outline-offset:2px}.button.secondary{background:var(--color-surface-raised,#fff);color:var(--color-text-primary,#17211f);border-color:var(--color-border-strong,#9aa9a1)}.button:disabled{opacity:.5;cursor:not-allowed}.field-label{display:grid;gap:5px;font-size:.82rem;color:var(--color-text-secondary,#65726d)}input,select{min-height:40px;border:1px solid var(--color-border-strong,#9aa9a1);border-radius:var(--radius-control,6px);padding:8px 10px;background:var(--color-surface-base,#f6f8f7);color:var(--color-text-primary,#17211f)}.field-error{border-color:var(--color-status-danger,#b83d48)}code{font-size:.78rem;background:var(--color-surface-base,#f6f8f7);padding:2px 5px;border-radius:3px}.swatches{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}.swatch{min-width:0;border:1px solid var(--color-border-subtle,#dbe2de);border-radius:var(--radius-control,6px);overflow:hidden;background:var(--color-surface-raised,#fff)}.swatch-color{height:68px;background:var(--swatch,#ddd);border-bottom:1px solid var(--color-border-subtle,#dbe2de)}.swatch-label{padding:8px 10px;font-size:.78rem}.swatch-label code{display:block;overflow-wrap:anywhere;background:none;padding:2px 0;color:var(--color-text-secondary,#65726d)}.tabs{display:flex;gap:4px;border-bottom:1px solid var(--color-border-subtle,#dbe2de);margin-bottom:14px}.tab{border:0;border-bottom:2px solid transparent;background:transparent;color:var(--color-text-secondary,#65726d);padding:9px 12px}.tab[aria-selected=true]{border-bottom-color:var(--color-accent-primary,#236b55);color:var(--color-accent-primary,#236b55);font-weight:700}.tab-panel{padding:12px 0}.table-wrap{overflow:auto;border:1px solid var(--color-border-subtle,#dbe2de);border-radius:var(--radius-card,10px)}table{width:100%;border-collapse:collapse;background:var(--color-surface-raised,#fff)}th,td{text-align:left;padding:11px 14px;border-bottom:1px solid var(--color-border-subtle,#dbe2de);white-space:nowrap}th{color:var(--color-text-secondary,#65726d);font-size:.78rem}.switch-row{display:flex;align-items:center;gap:12px}.switch{width:42px;height:24px;border:0;border-radius:999px;background:var(--color-border-strong,#9aa9a1);padding:3px}.switch:before{content:"";display:block;width:18px;height:18px;border-radius:50%;background:white;transition:transform .16s}.switch[aria-checked=true]{background:var(--color-accent-primary,#236b55)}.switch[aria-checked=true]:before{transform:translateX(18px)}.overlay{position:fixed;inset:0;display:none;place-items:center;background:rgba(12,22,18,.48);padding:20px;z-index:5}.overlay.open{display:grid}.dialog{width:min(100%,480px);padding:24px;background:var(--color-surface-raised,#fff);color:var(--color-text-primary,#17211f);border:1px solid var(--color-border-subtle,#dbe2de);border-radius:var(--radius-dialog,12px);box-shadow:var(--elevation-dialog,0 18px 60px rgba(0,0,0,.2))}.dialog h3{margin-top:0}.toast{position:fixed;right:24px;bottom:24px;display:none;padding:12px 16px;border-radius:var(--radius-control,6px);background:var(--color-text-primary,#17211f);color:var(--color-surface-raised,#fff);box-shadow:var(--elevation-popover,0 8px 24px rgba(0,0,0,.18));z-index:7}.toast.show{display:block}.token-table{display:grid;gap:16px}.token-group h3{font-size:1rem;margin-bottom:8px}.footer{margin-top:56px;padding-top:16px;border-top:1px solid var(--color-border-subtle,#dbe2de);color:var(--color-text-secondary,#65726d);font-size:.83rem}
353
- .token-demo{height:68px;display:flex;align-items:center;justify-content:center;background:var(--color-surface-base,#f6f8f7);border-bottom:1px solid var(--color-border-subtle,#dbe2de);overflow:hidden}.token-demo-sample{display:block;background:var(--color-accent-primary,#236b55);min-width:12px;min-height:8px}.token-demo[data-group="typography"] .token-demo-sample{min-width:0;min-height:0;background:transparent;color:var(--color-text-primary,#17211f)}
354
- @media(max-width:760px){.shell{grid-template-columns:1fr}.sidebar{border-right:0;border-bottom:1px solid var(--color-border-subtle,#dbe2de);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}}
355
- @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}
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><div><button class="button secondary" id="theme-toggle" type="button">Toggle theme</button></div></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>Keyboard-accessible examples</p></div>
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="footer">Generated from manifest.json, tokens.json, component specifications, and patterns. Edit the structured sources and regenerate this preview; do not edit generated markup as system truth.</footer>
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">${themePayload}</script>
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
- function flatten(value,prefix='',out={}){for(const [key,item] of Object.entries(value||{})){const name=prefix?prefix+'-'+key:key;if(item&&typeof item==='object'&&!Array.isArray(item))flatten(item,name,out);else if(['string','number'].includes(typeof item))out[name]=String(item)}return out}
389
- function setTheme(name){const theme=themes[name];if(!theme)return;document.documentElement.dataset.theme=name;for(const [key,value] of Object.entries(flatten(theme)))document.documentElement.style.setProperty('--'+key,value);document.getElementById('theme-toggle').hidden=Object.keys(themes).length<2;renderTokens(theme)}
390
- function renderTokens(theme){const holder=document.getElementById('token-groups');holder.replaceChildren();for(const [group,values] of Object.entries(theme)){if(!values||typeof values!=='object')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 [name,value] of Object.entries(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.background=String(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=String(value);if(group==='radius'){shape.style.width='42px';shape.style.height='28px';shape.style.borderRadius=String(value)}if(group==='typography'&&/family/i.test(name))shape.style.fontFamily=String(value);if(group==='typography'&&/size/i.test(name))shape.style.fontSize=String(value);if(group==='elevation')shape.style.boxShadow=String(value);sample.append(shape)}const label=document.createElement('div');label.className='swatch-label';label.textContent=name;const code=document.createElement('code');code.textContent=String(value);label.append(code);card.append(sample,label);swatches.append(card)}section.append(swatches);holder.append(section)}}
391
- document.getElementById('theme-toggle').addEventListener('click',()=>{const names=Object.keys(themes);const index=names.indexOf(document.documentElement.dataset.theme);setTheme(names[(index+1)%names.length])});
392
- 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}});
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(tab)+(event.key==='ArrowRight'?1:tabs.length-1))%tabs.length;tabs[next].focus();tabs[next].click()});
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
- 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()});
396
- 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)});
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
455
+ return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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 existingEntries = [];
472
+ let existingDirectories = [];
473
+ let targetExists = false;
417
474
  try {
418
- existingEntries = await readdir(target);
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 !== "ENOENT") throw error;
421
- }
422
- if (existingEntries.length > 0) {
423
- if (await fileExists(root, `${DESIGN_SYSTEM_DIR}/manifest.json`)) {
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/index.html", createPreviewHtml({ manifest, tokens, components, patterns })]
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 (existingEntries.length === 0) await rm2(target, { recursive: true, force: true });
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
- const supportFiles = [
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 = createPreviewHtml({ manifest, tokens, components, patterns });
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) and local OpenCode Skill/agents/commands preserve usage guidance when the plugin is not installed.
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}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, "mi"));
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 the project's design-system-designer. Collaborate naturally and gather only the identity decisions that are genuinely unclear. Explicit preferences always win. 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 them 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 any source evidence. Keep status draft until reviewed. Do not modify application files.
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: `Read the Design System first using design_system_read. When the design-system-designer subagent is available, delegate the design decision to it; otherwise adopt its collaborative role. Interpret the user's 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 that preference. 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 any unresolved references. Do not use blind text replacement and do not modify app UI code.
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: `Use the screen-designer subagent when available; otherwise adopt its system prompt. First call design_system_read with the user's task to load only the relevant tokens, components, patterns, preferences, and system guidelines. Design and document hierarchy, content, layout, 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.
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 portable design-system/manifest.json. For UI tasks, load the design-system skill and read only the relevant token/component/pattern files. Honor AI-GUIDELINES.md, preferences.json, and DECISIONS.md. The HTML preview is generated output, not the source of truth."
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);