opencode-design-system 0.1.0

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/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "opencode-design-system",
3
+ "version": "0.1.0",
4
+ "description": "An OpenCode v2 plugin for creating, evolving, previewing, and using portable AI-readable design systems.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/BraveOtter/opencode-design-system.git"
8
+ },
9
+ "homepage": "https://github.com/BraveOtter/opencode-design-system#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/BraveOtter/opencode-design-system/issues"
12
+ },
13
+ "keywords": [
14
+ "opencode",
15
+ "opencode-plugin",
16
+ "design-system",
17
+ "design-tokens",
18
+ "ai"
19
+ ],
20
+ "type": "module",
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=22.19.0"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "templates",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup src/index.ts --format esm --dts --clean",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest"
42
+ },
43
+ "dependencies": {
44
+ "@opencode/plugin": "^2.0.15"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.15.0",
48
+ "tsup": "^8.5.0",
49
+ "typescript": "^5.8.3",
50
+ "vitest": "^3.1.2"
51
+ }
52
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const systemRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
7
+ const projectRoot = path.resolve(systemRoot, '..')
8
+
9
+ function insideSystem(relative) {
10
+ const target = path.resolve(systemRoot, relative)
11
+ const prefix = systemRoot.endsWith(path.sep) ? systemRoot : systemRoot + path.sep
12
+ if (target !== systemRoot && !target.startsWith(prefix)) throw new Error('Design System path escapes its root: ' + relative)
13
+ return target
14
+ }
15
+
16
+ async function json(relative) {
17
+ return JSON.parse(await readFile(insideSystem(relative), 'utf8'))
18
+ }
19
+
20
+ async function text(relative) {
21
+ return readFile(insideSystem(relative), 'utf8')
22
+ }
23
+
24
+ function escape(value) {
25
+ return String(value ?? '').replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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])
30
+ }
31
+
32
+ function section(markdown, heading) {
33
+ const expression = new RegExp('^## ' + heading + '\\s*\\n([\\s\\S]*?)(?=\\n## |$)', 'mi')
34
+ return (markdown.match(expression)?.[1] ?? '').replace(/^- None specified\.$/m, '').trim()
35
+ }
36
+
37
+ function bullets(markdown, heading) {
38
+ return section(markdown, heading).split('\n').map((line) => line.match(/^\s*-\s+(.*)$/)?.[1]?.trim()).filter(Boolean)
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
46
+ }
47
+
48
+ async function main() {
49
+ const manifest = await json('manifest.json')
50
+ const tokenFile = manifest.tokens || 'tokens.json'
51
+ const tokenDocument = await json(tokenFile)
52
+ const themes = tokenDocument.themes || {}
53
+ const initialTheme = Object.keys(themes)[0] || 'light'
54
+ const components = await Promise.all((manifest.components || []).map(async (item) => {
55
+ const markdown = await text(item.file)
56
+ return { name: item.name, file: item.file, purpose: section(markdown, 'Purpose'), variants: bullets(markdown, 'Variants'), tokens: item.tokens || bullets(markdown, 'Tokens').map((token) => token.replaceAll('`', '')) }
57
+ }))
58
+ const patterns = await Promise.all((manifest.patterns || []).map(async (item) => {
59
+ const markdown = await text(item.file)
60
+ return { name: item.name, file: item.file, purpose: section(markdown, 'Purpose'), guidance: section(markdown, 'Guidance'), tokens: item.tokens || [] }
61
+ }))
62
+ const tokens = Object.entries(themes[initialTheme] || {}).map(([group, values]) => {
63
+ const entries = flatten(values, group).map((item) => {
64
+ let sampleStyle = ''
65
+ if (group === 'spacing') sampleStyle = 'width:' + escape(item.value) + ';height:8px'
66
+ if (group === 'radius') sampleStyle = 'width:42px;height:28px;border-radius:' + escape(item.value)
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('/'))
96
+ }
97
+
98
+ main().catch((error) => {
99
+ console.error('Could not generate Design System preview: ' + (error instanceof Error ? error.message : String(error)))
100
+ process.exitCode = 1
101
+ })