opencode-design-system 1.0.1 → 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
 
@@ -268,23 +268,59 @@ function validateTokens(value) {
268
268
  return errors;
269
269
  }
270
270
 
271
- // src/preview.ts
272
- function createPreviewHtml(input) {
273
- const { manifest, tokens, components, patterns } = input;
274
- const themes = tokens.themes ?? {};
275
- const firstTheme = Object.keys(themes)[0] ?? "light";
276
- const themePayload = safeJson(themes);
277
- 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) => `
278
319
  <article class="spec-card">
279
- <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>
280
- <p>${escapeHtml(component.purpose)}</p>
281
- <div class="showcase">
282
- ${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>`}
283
- </div>
284
- ${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>
285
323
  </article>`).join("\n");
286
- const patternCards = patterns.map((pattern) => `
287
- <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");
288
324
  const componentIndex = manifest.components.map((item) => `<li><a href="../${escapeHtml(item.file)}">${escapeHtml(item.name)}</a></li>`).join("");
289
325
  const patternIndex = manifest.patterns.map((item) => `<li><a href="../${escapeHtml(item.file)}">${escapeHtml(item.name)}</a></li>`).join("");
290
326
  return `<!doctype html>
@@ -295,14 +331,17 @@ function createPreviewHtml(input) {
295
331
  <meta name="description" content="Generated interactive preview for ${escapeHtml(manifest.name)}." />
296
332
  <title>${escapeHtml(manifest.name)} \u2014 Design System</title>
297
333
  <style>
298
- :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}
299
- *{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)}
300
- .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}
301
- 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}
302
- 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}
303
- .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)}
304
- @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}}
305
- @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}}
306
345
  </style>
307
346
  </head>
308
347
  <body>
@@ -314,12 +353,13 @@ function createPreviewHtml(input) {
314
353
  ${manifest.patterns.length ? `<div class="nav-label">Patterns</div><ul class="component-nav">${patternIndex}</ul>` : ""}
315
354
  </aside>
316
355
  <main>
317
- <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>
318
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>
319
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>
320
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>
321
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>
322
- <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>
323
363
  <div class="grid">
324
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>
325
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>
@@ -327,53 +367,127 @@ function createPreviewHtml(input) {
327
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>
328
368
  </div>
329
369
  </section>
330
- <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>
331
371
  </main>
332
372
  </div>
333
- <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>
334
374
  <div class="toast" id="toast" role="status" aria-live="polite">Changes saved</div>
335
- <script type="application/json" id="theme-data">${themePayload}</script>
375
+ <script type="application/json" id="theme-data">${safeJson(themeData)}</script>
336
376
  <script>
337
377
  const themes=JSON.parse(document.getElementById('theme-data').textContent||'{}');
338
- 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}
339
- 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)}
340
- 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)}}
341
- 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])});
342
- 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}});
343
- 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()})}
344
384
  document.querySelector('[role=switch]').addEventListener('click',event=>{const control=event.currentTarget;control.setAttribute('aria-checked',String(control.getAttribute('aria-checked')!=='true'))});
345
- 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()});
346
- 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)});
347
392
  setTheme(${safeJson(firstTheme)});
348
393
  </script>
349
394
  </body>
350
395
  </html>
351
396
  `;
352
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
+ }
353
454
  function escapeHtml(value) {
354
- return value.replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
455
+ return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
355
456
  }
356
457
  function safeJson(value) {
357
458
  return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => ({ "<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "\u2028": "\\u2028", "\u2029": "\\u2029" })[character]);
358
459
  }
359
460
 
461
+ // src/preview.ts
462
+ function createPreviewHtml2(input) {
463
+ return createPreviewHtml(input);
464
+ }
465
+
360
466
  // src/generator.ts
361
467
  var SCHEMA_VERSION = "1.0.0";
362
468
  var INITIAL_VERSION = "0.1.0";
363
469
  async function createDesignSystem(root, input) {
364
470
  validateCreateInput(input);
365
471
  const target = resolveInside(root, DESIGN_SYSTEM_DIR);
366
- let existingEntries = [];
472
+ let existingDirectories = [];
473
+ let targetExists = false;
367
474
  try {
368
- 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;
369
485
  } catch (error) {
370
- if (error.code !== "ENOENT") throw error;
371
- }
372
- if (existingEntries.length > 0) {
373
- if (await fileExists(root, `${DESIGN_SYSTEM_DIR}/manifest.json`)) {
374
- 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;
375
490
  }
376
- throw new Error("design-system/ already contains user files. Move or review them before creating a system there.");
377
491
  }
378
492
  const preferences = input.preferences ?? [];
379
493
  const tokens = { ...input.tokens, schemaVersion: typeof input.tokens.schemaVersion === "string" ? input.tokens.schemaVersion : SCHEMA_VERSION };
@@ -433,10 +547,17 @@ async function createDesignSystem(root, input) {
433
547
  ["schema/tokens.schema.json", pretty(tokensSchema)],
434
548
  ["README.md", systemReadme(manifest)],
435
549
  ["tools/generate-preview.mjs", await readFile2(new URL("../templates/generate-preview.mjs", import.meta.url), "utf8")],
436
- ["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 })]
437
552
  ]);
438
553
  for (const [index, component] of components.entries()) files.set(manifest.components[index].file, componentMarkdown(component));
439
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
+ }
440
561
  await mkdir2(path3.dirname(target), { recursive: true });
441
562
  const staging = path3.join(path3.dirname(target), `.design-system-${randomUUID2()}`);
442
563
  try {
@@ -445,10 +566,20 @@ async function createDesignSystem(root, input) {
445
566
  await mkdir2(path3.dirname(destination), { recursive: true });
446
567
  await writeFile2(destination, content, "utf8");
447
568
  }
448
- 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
+ }
449
574
  await rename2(staging, target);
575
+ if (targetExists) await restoreEmptyDirectories(existingDirectories);
450
576
  } catch (error) {
451
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
+ }
452
583
  throw error;
453
584
  }
454
585
  await updateManagedBlock(root, "AGENTS.md", "<!-- opencode-design-system:start -->", "<!-- opencode-design-system:end -->", projectAgentsBlock(manifest.name));
@@ -459,7 +590,7 @@ async function regeneratePreview(root) {
459
590
  const tokens = await readJson(root, `${DESIGN_SYSTEM_DIR}/${manifest.tokens}`);
460
591
  const components = await Promise.all(manifest.components.map(async (item) => parseComponent(item.name, await readText2(root, `${DESIGN_SYSTEM_DIR}/${item.file}`), item.tokens)));
461
592
  const patterns = await Promise.all(manifest.patterns.map(async (item) => parsePattern(item.name, await readText2(root, `${DESIGN_SYSTEM_DIR}/${item.file}`), item.tokens)));
462
- const html = createPreviewHtml({ manifest, tokens, components, patterns });
593
+ const html = createPreviewHtml2({ manifest, tokens, components, patterns });
463
594
  await atomicWrite(root, `${DESIGN_SYSTEM_DIR}/${manifest.preview}`, html);
464
595
  return { preview: `${DESIGN_SYSTEM_DIR}/${manifest.preview}`, componentCount: components.length, patternCount: patterns.length };
465
596
  }
@@ -467,6 +598,30 @@ async function readManifest(root) {
467
598
  return readJson(root, `${DESIGN_SYSTEM_DIR}/manifest.json`);
468
599
  }
469
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
+ }
470
625
  function validateCreateInput(input) {
471
626
  if (!input.name?.trim()) throw new Error("name is required");
472
627
  if (!input.description?.trim()) throw new Error("description is required");
@@ -582,7 +737,7 @@ function parsePattern(name, markdown, tokens) {
582
737
  };
583
738
  }
584
739
  function section(markdown, heading) {
585
- 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"));
586
741
  return match?.[1]?.replace(/^- None specified\.$/m, "").trim() ?? "";
587
742
  }
588
743
  function bullets(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-design-system",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "An OpenCode v2 plugin for creating, evolving, previewing, and using portable AI-readable design systems.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,6 +32,8 @@
32
32
  "dist",
33
33
  "templates",
34
34
  "README.md",
35
+ "README.es.md",
36
+ "README.pt-BR.md",
35
37
  "LICENSE"
36
38
  ],
37
39
  "scripts": {