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/README.es.md +244 -0
- package/README.md +125 -99
- package/README.pt-BR.md +244 -0
- package/dist/index.js +205 -50
- package/package.json +3 -1
- package/templates/generate-preview.mjs +38 -68
- package/templates/preview-renderer.d.mts +33 -0
- package/templates/preview-renderer.mjs +199 -0
|
@@ -1,101 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile, writeFile, mkdir } from
|
|
3
|
-
import path from
|
|
4
|
-
import { fileURLToPath } from
|
|
2
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { createPreviewHtml } from "./preview-renderer.mjs"
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
7
|
+
const templateRoot = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const systemRoot = path.resolve(templateRoot, "..")
|
|
9
|
+
const projectRoot = path.resolve(systemRoot, "..")
|
|
8
10
|
|
|
9
11
|
function insideSystem(relative) {
|
|
10
12
|
const target = path.resolve(systemRoot, relative)
|
|
11
13
|
const prefix = systemRoot.endsWith(path.sep) ? systemRoot : systemRoot + path.sep
|
|
12
|
-
if (target !== systemRoot && !target.startsWith(prefix)) throw new Error(
|
|
14
|
+
if (target !== systemRoot && !target.startsWith(prefix)) throw new Error("Design System path escapes its root: " + relative)
|
|
13
15
|
return target
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
async function json(relative) {
|
|
17
|
-
return JSON.parse(await readFile(insideSystem(relative),
|
|
19
|
+
return JSON.parse(await readFile(insideSystem(relative), "utf8"))
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
async function text(relative) {
|
|
21
|
-
return readFile(insideSystem(relative),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function escape(value) {
|
|
25
|
-
return String(value ?? '').replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character])
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function safeJson(value) {
|
|
29
|
-
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => ({ '<': '\\u003c', '>': '\\u003e', '&': '\\u0026', '\u2028': '\\u2028', '\u2029': '\\u2029' })[character])
|
|
23
|
+
return readFile(insideSystem(relative), "utf8")
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
function section(markdown, heading) {
|
|
33
|
-
const expression = new RegExp(
|
|
34
|
-
return (markdown.match(expression)?.[1] ??
|
|
27
|
+
const expression = new RegExp("^## " + heading + "[ \\t]*\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |(?![\\s\\S]))", "mi")
|
|
28
|
+
return (markdown.match(expression)?.[1] ?? "").replace(/^- None specified\.$/m, "").trim()
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
function bullets(markdown, heading) {
|
|
38
|
-
return section(markdown, heading).split(
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function flatten(value, prefix = '', out = []) {
|
|
42
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
43
|
-
for (const [key, item] of Object.entries(value)) flatten(item, prefix ? prefix + '.' + key : key, out)
|
|
44
|
-
} else if (typeof value === 'string' || typeof value === 'number') out.push({ path: prefix, value: String(value) })
|
|
45
|
-
return out
|
|
32
|
+
return section(markdown, heading).split("\n").map((line) => line.match(/^\s*-\s+(.*)$/)?.[1]?.trim()).filter(Boolean)
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
async function main() {
|
|
49
|
-
const manifest = await json(
|
|
50
|
-
const
|
|
51
|
-
const tokenDocument = await json(tokenFile)
|
|
52
|
-
const themes = tokenDocument.themes || {}
|
|
53
|
-
const initialTheme = Object.keys(themes)[0] || 'light'
|
|
36
|
+
const manifest = await json("manifest.json")
|
|
37
|
+
const tokenDocument = await json(manifest.tokens || "tokens.json")
|
|
54
38
|
const components = await Promise.all((manifest.components || []).map(async (item) => {
|
|
55
39
|
const markdown = await text(item.file)
|
|
56
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
name: item.name,
|
|
42
|
+
file: item.file,
|
|
43
|
+
purpose: section(markdown, "Purpose"),
|
|
44
|
+
variants: bullets(markdown, "Variants"),
|
|
45
|
+
states: bullets(markdown, "States"),
|
|
46
|
+
behavior: section(markdown, "Behavior"),
|
|
47
|
+
tokens: item.tokens || bullets(markdown, "Tokens").map((token) => token.replaceAll("`", "")),
|
|
48
|
+
}
|
|
57
49
|
}))
|
|
58
50
|
const patterns = await Promise.all((manifest.patterns || []).map(async (item) => {
|
|
59
51
|
const markdown = await text(item.file)
|
|
60
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
name: item.name,
|
|
54
|
+
file: item.file,
|
|
55
|
+
purpose: section(markdown, "Purpose"),
|
|
56
|
+
guidance: section(markdown, "Guidance"),
|
|
57
|
+
composition: bullets(markdown, "Composition"),
|
|
58
|
+
tokens: item.tokens || [],
|
|
59
|
+
}
|
|
61
60
|
}))
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (group === 'typography' && /family/i.test(item.path)) sampleStyle = 'font-family:' + escape(item.value)
|
|
68
|
-
if (group === 'typography' && /size/i.test(item.path)) sampleStyle = 'font-size:' + escape(item.value)
|
|
69
|
-
if (group === 'elevation') sampleStyle = 'box-shadow:' + escape(item.value)
|
|
70
|
-
const visual = group === 'color'
|
|
71
|
-
? '<div class="sample-color" style="background:' + escape(item.value) + '"></div>'
|
|
72
|
-
: '<div class="sample-value" data-group="' + escape(group) + '"><span style="' + sampleStyle + '">' + (group === 'typography' ? 'Aa' : '') + '</span></div>'
|
|
73
|
-
return '<article class="token"><div class="token-sample">' + visual + '</div><div><code>' + escape(item.path) + '</code><br><small>' + escape(item.value) + '</small></div></article>'
|
|
74
|
-
}).join('')
|
|
75
|
-
return '<div class="token-group"><h3>' + escape(group) + '</h3><div class="token-grid">' + entries + '</div></div>'
|
|
76
|
-
}).join('')
|
|
77
|
-
const componentCards = components.map((item) => '<article class="card"><p class="eyebrow">Component</p><h3>' + escape(item.name) + '</h3><p>' + escape(item.purpose) + '</p><div class="examples">' + (item.name.toLowerCase().includes('button') ? '<button class="button">Primary action</button><button class="button secondary">Secondary</button><button class="button" disabled>Disabled</button>' : item.name.toLowerCase().includes('input') || item.name.toLowerCase().includes('search') ? '<label>Default<input placeholder="Enter a value"></label><label>Error<input aria-invalid="true" value="Check this value"></label>' : '<button class="button secondary">' + escape(item.name) + ' example</button>') + '</div><p class="token-refs">' + item.tokens.map((token) => '<code>' + escape(token) + '</code>').join(' ') + '</p></article>').join('')
|
|
78
|
-
const patternCards = patterns.map((item) => '<article class="card"><p class="eyebrow">Pattern</p><h3>' + escape(item.name) + '</h3><p>' + escape(item.purpose) + '</p><p class="muted">' + escape(item.guidance) + '</p></article>').join('')
|
|
79
|
-
const componentNav = components.map((item) => '<li><a href="../' + escape(item.file) + '">' + escape(item.name) + '</a></li>').join('')
|
|
80
|
-
const patternNav = patterns.map((item) => '<li><a href="../' + escape(item.file) + '">' + escape(item.name) + '</a></li>').join('')
|
|
81
|
-
const html = `<!doctype html>
|
|
82
|
-
<html lang="en" data-theme="${escape(initialTheme)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(manifest.name)} — Design System</title>
|
|
83
|
-
<style>
|
|
84
|
-
.sample-value{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}.sample-value[data-group=spacing]{justify-content:flex-start}.sample-value[data-group=typography]>span{background:transparent!important}
|
|
85
|
-
:root{font-family:var(--typography-font-family-sans,Inter,system-ui,sans-serif);color:var(--color-text-primary,#17211f);background:var(--color-surface-base,#f6f8f7);line-height:1.5}*{box-sizing:border-box}body{margin:0;background:var(--color-surface-base,#f6f8f7);color:var(--color-text-primary,#17211f)}button,input{font:inherit}.layout{display:grid;grid-template-columns:235px 1fr;min-height:100vh}.side{padding:24px 18px;background:var(--color-surface-raised,#fff);border-right:1px solid var(--color-border-subtle,#dbe2de)}.brand{font-weight:750}.nav{display:grid;gap:8px;margin:25px 0}.nav a{color:var(--color-text-secondary,#65726d);text-decoration:none}.side li{font-size:.85rem;margin:4px 0}main{max-width:1400px;padding:32px clamp(18px,5vw,64px)}.top{display:flex;justify-content:space-between;align-items:center}.eyebrow{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--color-text-secondary,#65726d)}h1{font-size:clamp(2rem,4vw,3.1rem);line-height:1.1;letter-spacing:-.04em}.muted,p{color:var(--color-text-secondary,#65726d)}section{margin-top:50px;scroll-margin-top:18px}.heading{border-bottom:1px solid var(--color-border-subtle,#dbe2de);margin-bottom:16px}.grid,.token-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,270px),1fr));gap:14px}.card{padding:18px;border:1px solid var(--color-border-subtle,#dbe2de);border-radius:var(--radius-card,10px);background:var(--color-surface-raised,#fff)}.card h3{margin:4px 0}.button{background:var(--color-accent-primary,#276f55);color:var(--color-on-accent,#fff);border:1px solid var(--color-accent-primary,#276f55);border-radius:var(--radius-control,6px);padding:9px 14px;min-height:40px;cursor:pointer}.button:hover{filter:brightness(.93)}.button:focus-visible,input:focus-visible,.tab:focus-visible,.switch: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}.examples{display:flex;flex-wrap:wrap;align-items:end;gap:8px}label{display:grid;gap:4px;font-size:.85rem;color:var(--color-text-secondary,#65726d)}input{min-height:40px;border:1px solid var(--color-border-strong,#9aa9a1);border-radius:var(--radius-control,6px);padding:8px;background:var(--color-surface-base,#f6f8f7);color:var(--color-text-primary,#17211f)}input[aria-invalid=true]{border-color:var(--color-status-danger,#b83d48)}.token{display:flex;align-items:center;gap:10px;padding:10px;border:1px solid var(--color-border-subtle,#dbe2de);border-radius:var(--radius-control,6px);background:var(--color-surface-raised,#fff);overflow-wrap:anywhere}.sample-color{width:42px;height:36px;border:1px solid var(--color-border-subtle,#dbe2de);border-radius:4px}.sample-value{min-width:42px;height:36px;display:grid;place-items:center;background:var(--color-surface-base,#f6f8f7);border:1px solid var(--color-border-subtle,#dbe2de);font-size:12px}.sample-value[data-group=spacing]{width:var(--sample-width,42px)}.token code,.token small{font-size:.75rem}.tabs{display:flex;border-bottom:1px solid var(--color-border-subtle,#dbe2de)}.tab{background:transparent;border:0;padding:9px 12px;color:var(--color-text-secondary,#65726d);cursor:pointer}.tab[aria-selected=true]{border-bottom:2px solid var(--color-accent-primary,#276f55);color:var(--color-accent-primary,#276f55)}.switch{width:44px;height:25px;border:0;border-radius:99px;background:var(--color-border-strong,#9aa9a1);padding:3px}.switch:before{content:"";display:block;width:19px;height:19px;border-radius:50%;background:white;transition:transform .15s}.switch[aria-checked=true]{background:var(--color-accent-primary,#276f55)}.switch[aria-checked=true]:before{transform:translateX(19px)}.overlay{display:none;position:fixed;inset:0;place-items:center;background:#0007;padding:20px}.overlay.open{display:grid}.dialog{max-width:460px;background:var(--color-surface-raised,#fff);padding:22px;border-radius:var(--radius-dialog,12px)}.toast{display:none;position:fixed;right:20px;bottom:20px;padding:12px 16px;background:var(--color-text-primary,#17211f);color:var(--color-surface-raised,#fff);border-radius:var(--radius-control,6px)}.toast.show{display:block}table{width:100%;border-collapse:collapse;background:var(--color-surface-raised,#fff)}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--color-border-subtle,#dbe2de)}@media(max-width:720px){.layout{grid-template-columns:1fr}.side{border-right:0;border-bottom:1px solid var(--color-border-subtle,#dbe2de)}.side ul{display:none}.nav{display:flex;overflow:auto;margin:10px 0 0}.nav a{white-space:nowrap}main{padding:24px 16px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition-duration:.01ms!important;scroll-behavior:auto!important}}
|
|
86
|
-
</style></head><body><div class="layout"><aside class="side"><div class="brand">${escape(manifest.name)}</div><small>Design system · v${escape(manifest.designSystemVersion)}</small><nav class="nav"><a href="#tokens">Tokens</a><a href="#components">Components</a><a href="#patterns">Patterns</a><a href="#interactions">Interactions</a></nav>${componentNav ? '<small>COMPONENTS</small><ul>' + componentNav + '</ul>' : ''}${patternNav ? '<small>PATTERNS</small><ul>' + patternNav + '</ul>' : ''}</aside><main><div class="top"><span>${escape(manifest.status)}</span><button class="button secondary" id="theme-toggle">Toggle theme</button></div><header><p class="eyebrow">Framework-neutral design language</p><h1>${escape(manifest.name)}</h1><p>${escape(manifest.description)}</p></header>
|
|
87
|
-
<section id="tokens"><div class="heading"><p class="eyebrow">Foundations</p><h2>Semantic tokens</h2></div><div id="token-content">${tokens}</div></section><section id="components"><div class="heading"><p class="eyebrow">Building blocks</p><h2>Components</h2></div><div class="grid">${componentCards || '<p>No components documented.</p>'}</div></section><section id="patterns"><div class="heading"><p class="eyebrow">Compositions</p><h2>Patterns</h2></div><div class="grid">${patternCards || '<p>No patterns documented.</p>'}</div></section>
|
|
88
|
-
<section id="interactions"><div class="heading"><p class="eyebrow">Try it</p><h2>Interactive states</h2></div><div class="grid"><article class="card"><h3>Tabs</h3><div class="tabs" role="tablist"><button class="tab" role="tab" aria-selected="true" aria-controls="panel-a">General</button><button class="tab" role="tab" aria-selected="false" aria-controls="panel-b">Advanced</button></div><p id="panel-a" role="tabpanel">General settings are visible.</p><p id="panel-b" role="tabpanel" hidden>Advanced options are available here.</p></article><article class="card"><h3>Switch</h3><button class="switch" type="button" role="switch" aria-checked="false" aria-label="Enable notifications"></button> Enable notifications</article><article class="card"><h3>Dialog and toast</h3><div class="examples"><button class="button" id="open-dialog">Open dialog</button><button class="button secondary" id="show-toast">Show toast</button></div></article><article class="card"><h3>Table</h3><table><thead><tr><th>Name</th><th>Status</th></tr></thead><tbody><tr><td>Jordan Lee</td><td>Active</td></tr><tr><td>Sam Rivera</td><td>Invited</td></tr></tbody></table></article></div></section><footer><hr><p>Generated from manifest, tokens, component specifications, and patterns. Edit structured files and regenerate this preview; the HTML is not design-system source.</p></footer></main></div><div class="overlay" id="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>Review the action before continuing.</p><button class="button" id="close-dialog">Continue</button><button class="button secondary" id="cancel-dialog">Cancel</button></section></div><div class="toast" id="toast" role="status" aria-live="polite">Changes saved</div><script type="application/json" id="theme-data">${safeJson(themes)}</script>
|
|
89
|
-
<script>
|
|
90
|
-
const themes=JSON.parse(document.getElementById('theme-data').textContent||'{}');function flatten(v,p='',o={}){for(const [k,x] of Object.entries(v||{})){const n=p?p+'-'+k:k;if(x&&typeof x==='object'&&!Array.isArray(x))flatten(x,n,o);else if(['string','number'].includes(typeof x))o[n]=String(x)}return o}function setTheme(n){const t=themes[n];if(!t)return;document.documentElement.dataset.theme=n;for(const [k,v] of Object.entries(flatten(t)))document.documentElement.style.setProperty('--'+k,v);document.getElementById('theme-toggle').hidden=Object.keys(themes).length<2}document.getElementById('theme-toggle').addEventListener('click',()=>{const names=Object.keys(themes),i=names.indexOf(document.documentElement.dataset.theme);setTheme(names[(i+1)%names.length])});for(const tab of document.querySelectorAll('[role=tab]'))tab.addEventListener('click',()=>{for(const item of document.querySelectorAll('[role=tab]')){const active=item===tab;item.setAttribute('aria-selected',String(active));document.getElementById(item.getAttribute('aria-controls')).hidden=!active}});document.querySelector('[role=switch]').addEventListener('click',e=>e.currentTarget.setAttribute('aria-checked',String(e.currentTarget.getAttribute('aria-checked')!=='true')));const overlay=document.getElementById('overlay'),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);document.addEventListener('keydown',e=>{if(e.key==='Escape'&&overlay.classList.contains('open'))closeDialog()});let timeout;document.getElementById('show-toast').addEventListener('click',()=>{const toast=document.getElementById('toast');toast.classList.add('show');clearTimeout(timeout);timeout=setTimeout(()=>toast.classList.remove('show'),2200)});setTheme(${safeJson(initialTheme)});
|
|
91
|
-
</script></body></html>`
|
|
92
|
-
const previewPath = insideSystem(manifest.preview || 'preview/index.html')
|
|
93
|
-
await mkdir(path.dirname(previewPath), { recursive: true })
|
|
94
|
-
await writeFile(previewPath, html, 'utf8')
|
|
95
|
-
console.log('Generated ' + path.relative(projectRoot, previewPath).split(path.sep).join('/'))
|
|
61
|
+
const html = createPreviewHtml({ manifest, tokens: tokenDocument, components, patterns })
|
|
62
|
+
const destination = insideSystem(manifest.preview || "preview/index.html")
|
|
63
|
+
await mkdir(path.dirname(destination), { recursive: true })
|
|
64
|
+
await writeFile(destination, html, "utf8")
|
|
65
|
+
console.log("Generated " + path.relative(projectRoot, destination).split(path.sep).join("/"))
|
|
96
66
|
}
|
|
97
67
|
|
|
98
68
|
main().catch((error) => {
|
|
99
|
-
console.error(
|
|
69
|
+
console.error("Could not generate Design System preview: " + (error instanceof Error ? error.message : String(error)))
|
|
100
70
|
process.exitCode = 1
|
|
101
71
|
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type PreviewComponent = {
|
|
2
|
+
name: string
|
|
3
|
+
purpose: string
|
|
4
|
+
file?: string
|
|
5
|
+
variants?: string[]
|
|
6
|
+
sizes?: string[]
|
|
7
|
+
tokens?: string[]
|
|
8
|
+
states?: string[]
|
|
9
|
+
behavior?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PreviewPattern = {
|
|
13
|
+
name: string
|
|
14
|
+
purpose: string
|
|
15
|
+
file?: string
|
|
16
|
+
guidance?: string
|
|
17
|
+
composition?: string[]
|
|
18
|
+
tokens?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createPreviewHtml(input: {
|
|
22
|
+
manifest: {
|
|
23
|
+
name: string
|
|
24
|
+
designSystemVersion: string
|
|
25
|
+
status: string
|
|
26
|
+
description: string
|
|
27
|
+
components: Array<{ name: string; file: string }>
|
|
28
|
+
patterns: Array<{ name: string; file: string }>
|
|
29
|
+
}
|
|
30
|
+
tokens: Record<string, unknown>
|
|
31
|
+
components: PreviewComponent[]
|
|
32
|
+
patterns: PreviewPattern[]
|
|
33
|
+
}): string
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const TOKEN_ROLE_PATHS = {
|
|
2
|
+
canvas: ["color.canvas", "color.surface.base", "surface.canvas"],
|
|
3
|
+
surface: ["color.surface", "color.surface.raised", "surface.raised"],
|
|
4
|
+
text: ["color.text", "color.text.primary", "text.primary"],
|
|
5
|
+
muted: ["color.muted", "color.text.secondary", "text.secondary"],
|
|
6
|
+
brand: ["color.brand", "color.accent.primary", "color.primary", "brand"],
|
|
7
|
+
brandHover: ["color.brandHover", "color.accent.hover", "color.accent.primary.hover", "color.brand.hover"],
|
|
8
|
+
brandSubtle: ["color.brandSubtle", "color.accent.subtle", "color.brand.subtle"],
|
|
9
|
+
onBrand: ["color.onBrand", "color.onAccent", "color.on.accent", "color.text.onBrand"],
|
|
10
|
+
border: ["color.border", "color.border.subtle", "border.subtle"],
|
|
11
|
+
borderStrong: ["color.border.strong", "border.strong", "color.border"],
|
|
12
|
+
focus: ["color.focus", "color.focus.ring", "color.focusRing", "focus.ring"],
|
|
13
|
+
success: ["color.success", "color.status.success", "color.status.positive", "status.success"],
|
|
14
|
+
warning: ["color.warning", "color.status.warning", "status.warning"],
|
|
15
|
+
danger: ["color.danger", "color.status.danger", "status.danger"],
|
|
16
|
+
controlRadius: ["radius.control", "borderRadius.control"],
|
|
17
|
+
cardRadius: ["radius.card", "borderRadius.card"],
|
|
18
|
+
tagRadius: ["radius.tag", "radius.pill", "borderRadius.tag"],
|
|
19
|
+
bodyFont: ["typography.body", "typography.fontFamily.sans", "typography.font-family-sans"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_ROLES = {
|
|
23
|
+
canvas: "#f5f3f0",
|
|
24
|
+
surface: "#ffffff",
|
|
25
|
+
text: "#292724",
|
|
26
|
+
muted: "#6b6862",
|
|
27
|
+
brand: "#5b524b",
|
|
28
|
+
brandHover: "#4a433d",
|
|
29
|
+
brandSubtle: "#e9e4df",
|
|
30
|
+
onBrand: "#ffffff",
|
|
31
|
+
border: "#d9d4ce",
|
|
32
|
+
borderStrong: "#b9b1a8",
|
|
33
|
+
focus: "#8a6d52",
|
|
34
|
+
success: "#43735a",
|
|
35
|
+
warning: "#946b2e",
|
|
36
|
+
danger: "#9b4a47",
|
|
37
|
+
controlRadius: "0.5rem",
|
|
38
|
+
cardRadius: "0.75rem",
|
|
39
|
+
tagRadius: "9999px",
|
|
40
|
+
bodyFont: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createPreviewHtml({ manifest, tokens, components, patterns }) {
|
|
44
|
+
const themes = tokens?.themes && typeof tokens.themes === "object" ? tokens.themes : {}
|
|
45
|
+
const themeNames = Object.keys(themes)
|
|
46
|
+
const firstTheme = themeNames[0] ?? "light"
|
|
47
|
+
const themeData = Object.fromEntries(themeNames.map((name) => [name, normalizeTheme(themes[name])]))
|
|
48
|
+
const componentCards = components.map((component, index) => componentCard(component, index)).join("\n")
|
|
49
|
+
const patternCards = patterns.map((pattern) => `
|
|
50
|
+
<article class="spec-card">
|
|
51
|
+
<p class="eyebrow">Pattern</p><h3>${escapeHtml(pattern.name)}</h3>
|
|
52
|
+
<p>${escapeHtml(pattern.purpose)}</p>
|
|
53
|
+
<p class="muted">${escapeHtml(pattern.guidance && pattern.guidance !== pattern.purpose ? pattern.guidance : pattern.composition?.join(" · ") || pattern.guidance || "")}</p>
|
|
54
|
+
</article>`).join("\n")
|
|
55
|
+
const componentIndex = manifest.components.map((item) => `<li><a href="../${escapeHtml(item.file)}">${escapeHtml(item.name)}</a></li>`).join("")
|
|
56
|
+
const patternIndex = manifest.patterns.map((item) => `<li><a href="../${escapeHtml(item.file)}">${escapeHtml(item.name)}</a></li>`).join("")
|
|
57
|
+
|
|
58
|
+
return `<!doctype html>
|
|
59
|
+
<html lang="en" data-theme="${escapeHtml(firstTheme)}">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="utf-8" />
|
|
62
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
63
|
+
<meta name="description" content="Generated interactive preview for ${escapeHtml(manifest.name)}." />
|
|
64
|
+
<title>${escapeHtml(manifest.name)} — Design System</title>
|
|
65
|
+
<style>
|
|
66
|
+
: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}
|
|
67
|
+
*{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)}
|
|
68
|
+
.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}
|
|
69
|
+
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}
|
|
70
|
+
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}
|
|
71
|
+
.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)}
|
|
72
|
+
.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}
|
|
73
|
+
.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)}
|
|
74
|
+
.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)}
|
|
75
|
+
@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}}
|
|
76
|
+
@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}}
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<div class="shell">
|
|
81
|
+
<aside class="sidebar" aria-label="Design system navigation">
|
|
82
|
+
<div class="brand">${escapeHtml(manifest.name)}</div><div class="side-note">Design system · v${escapeHtml(manifest.designSystemVersion)}</div>
|
|
83
|
+
<nav class="nav"><a href="#overview">Overview</a><a href="#colors">Tokens</a><a href="#components">Components</a><a href="#patterns">Patterns</a><a href="#interactions">Interactions</a></nav>
|
|
84
|
+
${manifest.components.length ? `<div class="nav-label">Components</div><ul class="component-nav">${componentIndex}</ul>` : ""}
|
|
85
|
+
${manifest.patterns.length ? `<div class="nav-label">Patterns</div><ul class="component-nav">${patternIndex}</ul>` : ""}
|
|
86
|
+
</aside>
|
|
87
|
+
<main>
|
|
88
|
+
<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>
|
|
89
|
+
<header id="overview" class="hero"><p class="eyebrow">Framework-neutral design language</p><h1>${escapeHtml(manifest.name)}</h1><p>${escapeHtml(manifest.description)}</p></header>
|
|
90
|
+
<p id="token-warning" class="warning-note" role="status" hidden></p>
|
|
91
|
+
<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>
|
|
92
|
+
<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>
|
|
93
|
+
<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>
|
|
94
|
+
<section id="interactions"><div class="section-heading"><div><p class="eyebrow">Try it</p><h2>Interactive states</h2></div><p>Local examples · no application services</p></div>
|
|
95
|
+
<div class="grid">
|
|
96
|
+
<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>
|
|
97
|
+
<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>
|
|
98
|
+
<article class="spec-card"><h3>Dialog and toast</h3><div class="showcase"><button class="button" type="button" id="open-dialog">Open dialog</button><button class="button secondary" type="button" id="show-toast">Show toast</button></div></article>
|
|
99
|
+
<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>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
<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>
|
|
103
|
+
</main>
|
|
104
|
+
</div>
|
|
105
|
+
<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>
|
|
106
|
+
<div class="toast" id="toast" role="status" aria-live="polite">Changes saved</div>
|
|
107
|
+
<script type="application/json" id="theme-data">${safeJson(themeData)}</script>
|
|
108
|
+
<script>
|
|
109
|
+
const themes=JSON.parse(document.getElementById('theme-data').textContent||'{}');
|
|
110
|
+
const themeNames=Object.keys(themes);const themeToggle=document.getElementById('theme-toggle');
|
|
111
|
+
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}
|
|
112
|
+
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(', ')+'.':''}
|
|
113
|
+
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)}}
|
|
114
|
+
themeToggle.addEventListener('click',()=>{const index=themeNames.indexOf(document.documentElement.dataset.theme);setTheme(themeNames[(index+1)%themeNames.length])});
|
|
115
|
+
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()})}
|
|
116
|
+
document.querySelector('[role=switch]').addEventListener('click',event=>{const control=event.currentTarget;control.setAttribute('aria-checked',String(control.getAttribute('aria-checked')!=='true'))});
|
|
117
|
+
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'}});
|
|
118
|
+
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()});
|
|
119
|
+
const motion=window.matchMedia('(prefers-reduced-motion: reduce)');const finePointer=window.matchMedia('(hover: hover) and (pointer: fine)');
|
|
120
|
+
function resetMagnet(element){element.dataset.moving='false';for(const key of ['--magnet-x','--magnet-y','--magnet-rx','--magnet-ry'])element.style.removeProperty(key)}
|
|
121
|
+
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))}
|
|
122
|
+
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)}
|
|
123
|
+
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)});
|
|
124
|
+
setTheme(${safeJson(firstTheme)});
|
|
125
|
+
</script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
128
|
+
`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeTheme(theme) {
|
|
132
|
+
const roles = {}
|
|
133
|
+
const missing = []
|
|
134
|
+
for (const [name, paths] of Object.entries(TOKEN_ROLE_PATHS)) {
|
|
135
|
+
const value = firstTokenValue(theme, paths)
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
roles[name] = DEFAULT_ROLES[name]
|
|
138
|
+
if (["canvas", "surface", "text", "brand", "border"].includes(name)) missing.push(name)
|
|
139
|
+
} else roles[name] = value
|
|
140
|
+
}
|
|
141
|
+
return { roles, missing, tokens: theme }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function firstTokenValue(theme, paths) {
|
|
145
|
+
for (const path of paths) {
|
|
146
|
+
let value = theme
|
|
147
|
+
for (const segment of path.split(".")) value = value && typeof value === "object" ? value[segment] : undefined
|
|
148
|
+
if ((typeof value === "string" || typeof value === "number") && String(value).trim()) return String(value)
|
|
149
|
+
}
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function componentCard(component, index) {
|
|
154
|
+
const name = String(component.name ?? "Component")
|
|
155
|
+
const tokens = component.tokens ?? []
|
|
156
|
+
const evidence = [name, component.behavior, ...(component.variants ?? []), ...(component.states ?? []), ...tokens].filter(Boolean).join(" ")
|
|
157
|
+
const isButton = /button|action/i.test(name)
|
|
158
|
+
const isField = /input|field|search/i.test(name)
|
|
159
|
+
const isStatus = /status|indicator|badge/i.test(name)
|
|
160
|
+
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))
|
|
161
|
+
const magnetic = isButton && (/magnet/i.test(evidence) || tokens.some((token) => /depth\.(button|magnetic)/i.test(token)))
|
|
162
|
+
let demo
|
|
163
|
+
if (isIdentityCard) demo = identityCardDemo(name)
|
|
164
|
+
else if (isButton) demo = buttonDemo(name, index, magnetic)
|
|
165
|
+
else if (isField) demo = fieldDemo(name, index)
|
|
166
|
+
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>`
|
|
167
|
+
else demo = `<div class="demo-surface"><strong>${escapeHtml(name)} preview</strong><span class="muted">Static sample of the documented component.</span></div>`
|
|
168
|
+
const tokensHtml = tokens.length ? `<small>Tokens: ${tokens.map((token) => `<code>${escapeHtml(token)}</code>`).join(" ")}</small>` : ""
|
|
169
|
+
return `
|
|
170
|
+
<article class="spec-card">
|
|
171
|
+
<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>
|
|
172
|
+
<p>${escapeHtml(component.purpose ?? "")}</p>
|
|
173
|
+
<div class="showcase">${demo}</div>
|
|
174
|
+
${tokensHtml}
|
|
175
|
+
</article>`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buttonDemo(name, index, magnetic) {
|
|
179
|
+
const className = magnetic ? "button magnetic" : "button"
|
|
180
|
+
const hint = magnetic ? `<span class="magnetic-note">Move the pointer to try the magnetic response.</span>` : ""
|
|
181
|
+
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}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function fieldDemo(name, index) {
|
|
185
|
+
const id = `preview-field-${index}`
|
|
186
|
+
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>`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function identityCardDemo(name) {
|
|
190
|
+
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 · sample identity</div></article></div><span class="magnetic-note">Move the pointer to explore the 3D identity card.</span>`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function escapeHtml(value) {
|
|
194
|
+
return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character])
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function safeJson(value) {
|
|
198
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => ({ "<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "\u2028": "\\u2028", "\u2029": "\\u2029" })[character])
|
|
199
|
+
}
|