diff --git a/scripts/design-sync/README.md b/scripts/design-sync/README.md new file mode 100644 index 00000000..a0827511 --- /dev/null +++ b/scripts/design-sync/README.md @@ -0,0 +1,93 @@ +# Design-sync bundle generator + +Generates a [Claude Design](https://claude.ai/design) **design-system** bundle from +ScriptHammer's real DaisyUI components and brand themes, then you push it with Claude +Code's `/design-sync`. + +## What this is (and isn't) + +`/design-sync` does **not** publish an npm package or upload your `.tsx` source. It +uploads **self-contained preview HTML cards** into a claude.ai design-system project. +Each card's first line is a marker comment — `` — and +the Design System pane compiles those markers into its card index automatically. + +ScriptHammer's atomic/presentational components style purely from DaisyUI classes + +the two brand themes (`scripthammer-dark`, `scripthammer-light`). So each card just +needs the brand CSS attached and the real DaisyUI class markup — no hand-written CSS, +full fidelity. + +## Files + +| File | Role | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `manifest.js` | Curated component list. Each entry's class strings are copied **verbatim** from the component source (e.g. `Button.tsx`'s `variantClasses`/`sizeClasses`) so cards stay faithful. Add a component here. | +| `gen-theme.js` | Compiles `theme.css` — `@tailwindcss/postcss` + DaisyUI scoped to the two brand themes only (the 32 stock themes are omitted). Reads `classes.txt` as the safelist. | +| `gen-cards.js` | Emits the `@dsCard` HTML. `--classes` writes the safelist; no arg emits cards with `theme.css` inlined, rendered **side-by-side dark + light**. | +| `build.js` | One-command orchestrator: classes → theme → cards into an output dir. | + +## Build (Docker-first — required) + +`@tailwindcss/postcss` + DaisyUI live in the container, so the build runs there: + +```bash +docker compose exec -w /app scripthammer node scripts/design-sync/build.js +``` + +Output defaults to `/tmp/ds-bundle` **inside the container** (it's a build artifact, +not source — nothing is written into the repo). To get it onto the host, either copy +it out: + +```bash +docker compose cp scripthammer:/tmp/ds-bundle ./ds-bundle +``` + +…or point `DS_OUT` at a path under the repo (which is bind-mounted at `/app`) — but +remember to keep it out of git: + +```bash +DS_OUT=/app/.ds-bundle docker compose exec -w /app scripthammer \ + node scripts/design-sync/build.js +``` + +## Push to claude.ai + +From Claude Code, in this repo: + +``` +/design-sync +``` + +It will `list_projects` (first run grants design scope), `create_project "ScriptHammer"` +if none exists, `finalize_plan`, then `write_files` from the bundle dir. Re-running with +an existing project updates it incrementally — one component at a time is fine. + +Plan paths to finalize: `theme.css`, `tokens/**`, `components/**/*.html`. + +## Adding a component + +1. Open the component's `.tsx`, copy its variant/size class maps verbatim. +2. Add a `render()` function + a manifest entry in `manifest.js` (pick a `group`). +3. Rebuild and eyeball before pushing: + +```bash +docker compose exec -w /app scripthammer node scripts/design-sync/build.js +# screenshot a card to confirm it renders (OKLCH resolves, dark ≠ light): +docker compose exec scripthammer chromium --headless --no-sandbox \ + --screenshot=/tmp/shot.png "file:///tmp/ds-bundle/components//index.html" +``` + +## Scope + +Synced: design tokens (colors, shape, typography) + the portable atomic/presentational +tier. **Excluded:** components whose visual output is state/data-driven (auth, payment, +admin, map, game, and anything importing `@/contexts`, `@/services`, fetching hooks, or +canvas/3D) — they have no honest static preview. + +## Note on DaisyUI v5 drift + +DaisyUI 5 (beta) renamed/removed several v4 classes the app still uses: +`card-bordered`→`card-border`, `card-compact`→`card-sm`, and `input-bordered` / +`form-control` / `label-text` were removed (the base `.input` is bordered; v5 uses +`.fieldset` + `.label`). The manifest uses the **v5** names so cards render correctly. +The app components still carry the v4 names (dead classes — harmless, but worth a future +cleanup pass). diff --git a/scripts/design-sync/build.js b/scripts/design-sync/build.js new file mode 100644 index 00000000..f2e424cd --- /dev/null +++ b/scripts/design-sync/build.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/** + * build.js — one-command builder for the ScriptHammer design-sync bundle. + * + * Runs the full pipeline into an output dir: + * 1. gen-cards --classes → classes.txt (safelist) + * 2. gen-theme → theme.css (scoped DaisyUI + both brand themes) + * 3. gen-cards → token + component preview cards (@dsCard HTML) + * + * MUST run inside the scripthammer container (needs @tailwindcss/postcss + DaisyUI): + * + * docker compose exec -w /app scripthammer node scripts/design-sync/build.js + * + * Output defaults to /tmp/ds-bundle inside the container (NOT the repo — the + * cards + theme.css are build artifacts, not source). Override with DS_OUT: + * + * DS_OUT=/tmp/my-bundle docker compose exec -w /app scripthammer \ + * node scripts/design-sync/build.js + * + * Then, from the host, push to claude.ai with Claude Code's /design-sync + * (copy the bundle out of the container first, or point DS_OUT at a bind mount). + * See scripts/design-sync/README.md. + */ +const { execFileSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const OUT = process.env.DS_OUT || '/tmp/ds-bundle'; +const SCRIPTS = __dirname; + +fs.mkdirSync(OUT, { recursive: true }); + +function run(label, args) { + process.stdout.write(`\n▶ ${label}\n`); + execFileSync('node', args, { + stdio: 'inherit', + env: { ...process.env, DS_OUT: OUT }, + }); +} + +run('1/3 extract class safelist', [ + path.join(SCRIPTS, 'gen-cards.js'), + '--classes', +]); +run('2/3 compile theme.css', [path.join(SCRIPTS, 'gen-theme.js')]); +run('3/3 emit preview cards', [path.join(SCRIPTS, 'gen-cards.js')]); + +const cards = []; +for (const sub of ['tokens', 'components']) { + const dir = path.join(OUT, sub); + if (!fs.existsSync(dir)) continue; + const walk = (d) => { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, e.name); + if (e.isDirectory()) walk(p); + else if (e.name.endsWith('.html')) cards.push(path.relative(OUT, p)); + } + }; + walk(dir); +} + +process.stdout.write( + `\n✓ bundle ready in ${OUT}\n theme.css + ${cards.length} cards:\n` + + cards + .sort() + .map((c) => ` ${c}`) + .join('\n') + + '\n' +); diff --git a/scripts/design-sync/gen-cards.js b/scripts/design-sync/gen-cards.js new file mode 100644 index 00000000..e5b542f5 --- /dev/null +++ b/scripts/design-sync/gen-cards.js @@ -0,0 +1,161 @@ +#!/usr/bin/env node +/** + * gen-cards.js — emit the design-sync preview cards. + * + * Node stdlib only. Driven by manifest.js. Two modes: + * + * node gen-cards.js --classes -> write classes.txt (every class used by the + * cards) so gen-theme.js can safelist them. + * Run this FIRST, then gen-theme.js. + * node gen-cards.js -> emit token and component card HTML files + * with theme.css inlined and the @dsCard marker. + * Run this AFTER theme.css exists. + * + * Each card: + * - line 1 is (compiled into _ds_manifest.json) + * - inlines theme.css in a + + +
+
+
+
scripthammer-dark
+ ${innerDark} +
+
+
+
+
scripthammer-light
+ ${innerLight} +
+
+
+ + +`; +} + +function emitCards() { + const themeCssPath = path.join(DIR, 'theme.css'); + if (!fs.existsSync(themeCssPath)) { + console.error('theme.css not found — run gen-theme.js first.'); + process.exit(1); + } + const themeCss = fs.readFileSync(themeCssPath, 'utf8'); + const fakeH = { esc: (s) => s }; + + let count = 0; + for (const t of M.tokens) { + const inner = t.render(fakeH); + const html = pageWrap(t.group, t.title, themeCss, inner, inner); + const dest = path.join(DIR, 'tokens', `${t.slug}.html`); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, html); + count++; + } + for (const c of M.components) { + const inner = c.render(fakeH); + const html = pageWrap(c.group, c.title, themeCss, inner, inner); + const dest = path.join(DIR, 'components', c.slug, 'index.html'); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, html); + count++; + } + console.log( + `emitted ${count} cards (inlined theme.css ${(Buffer.byteLength(themeCss) / 1024).toFixed(1)} KB each)` + ); +} + +const mode = process.argv[2]; +if (mode === '--classes') { + const classes = extractClasses(); + fs.writeFileSync(path.join(DIR, 'classes.txt'), classes.join('\n') + '\n'); + console.log(`classes.txt written: ${classes.length} classes`); +} else { + emitCards(); +} diff --git a/scripts/design-sync/gen-theme.js b/scripts/design-sync/gen-theme.js new file mode 100644 index 00000000..f03a07d3 --- /dev/null +++ b/scripts/design-sync/gen-theme.js @@ -0,0 +1,171 @@ +#!/usr/bin/env node +/** + * gen-theme.js — build a scoped DaisyUI theme.css for the design-sync bundle. + * + * Produces theme.css containing ONLY the two brand themes + * (scripthammer-dark + scripthammer-light) plus the brand-polish CSS, + * compiled through the project's own @tailwindcss/postcss + DaisyUI v5. + * + * Run inside the scripthammer container so node_modules is available: + * docker compose exec -T scripthammer node /app//gen-theme.js + * + * Inputs (same dir): + * - classes.txt (safelist of every class the cards use; written by gen-cards.js) + * Output (same dir): + * - theme.css + * + * The entry CSS @imports tailwindcss, declares the two brand themes via + * @plugin "daisyui/theme", and uses @source inline(...) so Tailwind keeps + * every utility class our static HTML references (there is no JS/TSX content + * to scan in the bundle). + */ +const fs = require('fs'); +const path = require('path'); + +// pnpm strict layout: `postcss` is not top-level in /app/node_modules. Resolve +// `@tailwindcss/postcss` first (it IS top-level), then resolve `postcss` from +// within that package's own resolution scope. +const tailwindEntry = require.resolve('@tailwindcss/postcss', { + paths: [process.cwd(), '/app'], +}); +const tailwind = require(tailwindEntry); +const postcss = require( + require.resolve('postcss', { paths: [path.dirname(tailwindEntry)] }) +); + +const DIR = process.env.DS_OUT || __dirname; +const classesPath = path.join(DIR, 'classes.txt'); +const safelist = fs.existsSync(classesPath) + ? fs + .readFileSync(classesPath, 'utf8') + .split('\n') + .map((s) => s.trim()) + .filter(Boolean) + : []; + +// The two brand theme blocks, copied verbatim from src/app/globals.css +// (scripthammer-dark lines 55-94, scripthammer-light lines 97-137) plus the +// brand-polish block (lines 139-180). DaisyUI is enabled WITHOUT the 32 stock +// themes so the bundle stays brand-scoped and small. +const ENTRY = ` +@import 'tailwindcss'; + +/* Keep every class our static preview cards reference. */ +@source inline("${safelist.join(' ')}"); + +@plugin "daisyui" { + themes: scripthammer-dark --default, scripthammer-light; +} + +@plugin "daisyui/theme" { + name: 'scripthammer-dark'; + default: true; + color-scheme: dark; + --color-base-100: oklch(22.84% 0.038 282.93); + --color-base-200: oklch(21.13% 0.039 282.53); + --color-base-300: oklch(28.51% 0.067 281.32); + --color-base-content: oklch(92.88% 0.013 255.51); + --color-primary: oklch(76.05% 0.024 258.37); + --color-primary-content: oklch(22.84% 0.038 282.93); + --color-secondary: oklch(87.91% 0.043 76.31); + --color-secondary-content: oklch(22.84% 0.038 282.93); + --color-accent: oklch(75.35% 0.139 232.66); + --color-accent-content: oklch(22.84% 0.038 282.93); + --color-neutral: oklch(31.14% 0.052 282.99); + --color-neutral-content: oklch(87.17% 0.009 258.34); + --color-info: oklch(73.08% 0.13 260.06); + --color-info-content: oklch(22.84% 0.038 282.93); + --color-success: oklch(72.27% 0.192 149.58); + --color-success-content: oklch(22.84% 0.038 282.93); + --color-warning: oklch(90.52% 0.166 98.11); + --color-warning-content: oklch(22.84% 0.038 282.93); + --color-error: oklch(74.7% 0.132 20.69); + --color-error-content: oklch(22.84% 0.038 282.93); + --radius-selector: 0.75rem; + --radius-field: 0.5rem; + --radius-box: 1.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 1; +} + +@plugin "daisyui/theme" { + name: 'scripthammer-light'; + default: false; + prefersdark: false; + color-scheme: light; + --color-base-100: oklch(95.76% 0.009 67.72); + --color-base-200: oklch(92.44% 0.013 75.36); + --color-base-300: oklch(87.66% 0.016 73.66); + --color-base-content: oklch(27.81% 0.03 256.85); + --color-primary: oklch(42.79% 0.03 257.68); + --color-primary-content: oklch(100% 0 0); + --color-secondary: oklch(44.28% 0.116 46.14); + --color-secondary-content: oklch(100% 0 0); + --color-accent: oklch(42.86% 0.098 239.94); + --color-accent-content: oklch(100% 0 0); + --color-neutral: oklch(37.29% 0.031 259.73); + --color-neutral-content: oklch(98.46% 0.002 247.84); + --color-info: oklch(43.86% 0.167 262.77); + --color-info-content: oklch(100% 0 0); + --color-success: oklch(42.03% 0.11 149.9); + --color-success-content: oklch(100% 0 0); + --color-warning: oklch(43.35% 0.09 76.98); + --color-warning-content: oklch(100% 0 0); + --color-error: oklch(44.94% 0.164 26.98); + --color-error-content: oklch(100% 0 0); + --radius-selector: 0.75rem; + --radius-field: 0.5rem; + --radius-box: 1.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 1; +} + +/* Brand polish — scoped to the custom themes only (globals.css 139-180). */ +[data-theme='scripthammer-dark'] .card, +[data-theme='scripthammer-light'] .card { + box-shadow: + 0 4px 6px -1px rgb(0 0 0 / 0.25), + 0 10px 15px -3px rgb(0 0 0 / 0.2), + 0 20px 25px -5px rgb(0 0 0 / 0.15); +} +[data-theme='scripthammer-dark'] .btn, +[data-theme='scripthammer-light'] .btn { + box-shadow: + 0 2px 4px 0 rgb(0 0 0 / 0.2), + 0 1px 2px -1px rgb(0 0 0 / 0.15); +} +[data-theme='scripthammer-dark'] .btn:hover, +[data-theme='scripthammer-light'] .btn:hover { + box-shadow: + 0 4px 8px -1px rgb(0 0 0 / 0.3), + 0 2px 4px -2px rgb(0 0 0 / 0.2); +} +`; + +(async () => { + // from: must point inside the project so DaisyUI's plugin resolves under node_modules. + const result = await postcss([tailwind()]).process(ENTRY, { + from: path.join(process.cwd(), 'ds-entry.css'), + }); + const out = path.join(DIR, 'theme.css'); + fs.writeFileSync(out, result.css); + const kb = (Buffer.byteLength(result.css) / 1024).toFixed(1); + const oklch = (result.css.match(/oklch\(/g) || []).length; + const themes = (result.css.match(/\[data-theme=/g) || []).length; + console.log( + `theme.css written: ${kb} KB, ${oklch} oklch() refs, ${themes} data-theme selectors` + ); + if (oklch === 0) { + console.error('WARNING: no oklch() in output — theme tokens missing!'); + process.exit(1); + } +})().catch((e) => { + console.error('gen-theme failed:', e.message); + process.exit(1); +}); diff --git a/scripts/design-sync/manifest.js b/scripts/design-sync/manifest.js new file mode 100644 index 00000000..52610a7d --- /dev/null +++ b/scripts/design-sync/manifest.js @@ -0,0 +1,585 @@ +/** + * manifest.js — the curated component manifest for the ScriptHammer design-sync bundle. + * + * Each entry's class strings are copied verbatim from the component source so the + * preview cards stay faithful to the real components: + * Button -> src/components/atomic/Button/Button.tsx (variantClasses/sizeClasses) + * Card -> src/components/atomic/Card/Card.tsx + * TagBadge -> src/components/atomic/TagBadge/TagBadge.tsx + * Tooltip -> src/components/atomic/Tooltip/Tooltip.tsx + * Text -> src/components/subatomic/Text/Text.tsx (variantStyles) + * NetworkStatus / TypingIndicator -> atomic status atoms + * + * `render(h)` returns the inner markup for ONE theme wrapper. `h` is a tiny + * helper { esc } passed by the generator. Markup uses real DaisyUI classes. + */ + +// ---- Button (9 variants x 4 sizes + modifiers) ---- +const BTN_VARIANTS = [ + 'primary', + 'secondary', + 'accent', + 'ghost', + 'link', + 'info', + 'success', + 'warning', + 'error', +]; +const BTN_SIZES = [ + ['xs', 'btn-xs min-w-11 min-h-11'], + ['sm', 'btn-sm min-w-11 min-h-11'], + ['md', 'min-w-11 min-h-11'], + ['lg', 'btn-lg'], +]; + +function buttonCard() { + let out = ''; + // variant rows + out += '
'; + for (const v of BTN_VARIANTS) { + out += '
'; + out += `${v}`; + for (const [, sizeCls] of BTN_SIZES) { + out += ``; + } + out += '
'; + } + // modifiers + out += '
'; + out += + 'modifiers'; + out += + ''; + out += + ''; + out += + ''; + out += + ''; + out += + ''; + out += '
'; + out += '
'; + return out; +} + +// ---- Card ---- +function cardCard() { + return ` +
+
+
+

Card title

Supporting subtitle

+

A standard card with a body, title and actions.

+
+
+
+
+
+

Bordered

+

A bordered card variant.

+
+
+
+
+

Compact

+

A compact card with tighter padding.

+
+
+
`; +} + +// ---- TagBadge (3 sizes x 4 variants) ---- +const TAG_SIZES = [ + ['sm', 'badge-sm text-xs'], + ['md', 'badge-md text-sm'], + ['lg', 'badge-lg text-base'], +]; +const TAG_VARIANTS = [ + ['default', 'badge-outline'], + ['primary', 'badge-primary'], + ['secondary', 'badge-secondary'], + ['accent', 'badge-accent'], +]; +function tagBadgeCard() { + let out = '
'; + for (const [vname, vcls] of TAG_VARIANTS) { + out += '
'; + out += `${vname}`; + for (const [, scls] of TAG_SIZES) { + out += `tag`; + } + out += '
'; + } + out += + '
with count'; + out += + 'react (12)'; + out += + 'active
'; + out += '
'; + return out; +} + +// ---- Tooltip (data-tip; force-open via tooltip-open so it shows in a static card) ---- +function tooltipCard() { + return ` +
+
+
+
+
+
`; +} + +// ---- Text (variant scale; verbatim from variantStyles) ---- +const TEXT_VARIANTS = [ + ['h1', 'text-5xl font-bold text-base-content', 'Heading 1'], + ['h2', 'text-4xl font-bold text-base-content', 'Heading 2'], + ['h3', 'text-3xl font-semibold text-base-content', 'Heading 3'], + ['h4', 'text-2xl font-semibold text-base-content', 'Heading 4'], + ['h5', 'text-xl font-medium text-base-content', 'Heading 5'], + ['h6', 'text-lg font-medium text-base-content', 'Heading 6'], + ['lead', 'text-xl text-base-content/85', 'Lead paragraph text'], + [ + 'body', + 'text-base text-base-content', + 'Body text — the default paragraph style.', + ], + ['small', 'text-sm text-base-content/85', 'Small text'], + ['caption', 'text-xs text-base-content', 'Caption text'], + ['emphasis', 'text-base italic text-base-content', 'Emphasised text'], + [ + 'code', + 'font-mono text-sm bg-base-200 px-1 py-0.5 rounded', + 'const code = true', + ], +]; +function textCard() { + let out = '
'; + for (const [name, cls, sample] of TEXT_VARIANTS) { + out += `
${name}${sample}
`; + } + out += '
'; + return out; +} + +// ---- NetworkStatus + TypingIndicator (small status atoms) ---- +function statusCard() { + return ` +
+
online +
Online
+
offline +
Offline
+
typing +
+
+ Alex is typing...
+
`; +} + +// ---- Token cards ---- +// OKLCH values per theme, copied verbatim from globals.css. +const SEMANTIC_COLORS = [ + 'primary', + 'secondary', + 'accent', + 'neutral', + 'info', + 'success', + 'warning', + 'error', +]; +const BASES = ['base-100', 'base-200', 'base-300', 'base-content']; + +function colorsCard() { + let out = '
'; + out += '
'; + for (const c of SEMANTIC_COLORS) { + out += `
+
${c}
+
${c} / ${c}-content
+
`; + } + out += '
'; + out += '
'; + for (const c of BASES) { + out += `
+
${c}
+
`; + } + out += '
'; + return out; +} + +function shapeCard() { + const radii = [ + ['radius-selector', '0.75rem', 'rounded-selector'], + ['radius-field', '0.5rem', 'rounded-field'], + ['radius-box', '1.5rem', 'rounded-box'], + ]; + let out = '
'; + out += '
'; + for (const [name, val, cls] of radii) { + out += `
+
+ ${name}${val} +
`; + } + out += '
'; + out += '
'; + out += + '
--border 1px
'; + out += + '
--depth shadow
'; + out += '
'; + return out; +} + +function typographyTokenCard() { + // reuse the Text scale but framed as type tokens + return textCard(); +} + +// ---- AvatarDisplay (4 sizes; image + initials fallback) ---- +// sizeClasses/ringClasses copied verbatim from AvatarDisplay.tsx +const AVATAR_SIZES = [ + [ + 'sm', + 'w-8 h-8 text-sm', + 'ring-1 ring-base-content/20 ring-offset-1 ring-offset-base-100', + ], + [ + 'md', + 'w-12 h-12 text-base', + 'ring-2 ring-base-content/25 ring-offset-2 ring-offset-base-100', + ], + [ + 'lg', + 'w-16 h-16 text-lg', + 'ring-2 ring-base-content/25 ring-offset-2 ring-offset-base-100', + ], + [ + 'xl', + 'w-24 h-24 text-2xl', + 'ring-2 ring-base-content/25 ring-offset-2 ring-offset-base-100', + ], +]; +function avatarCard() { + let out = '
'; + out += + '
initials'; + for (const [, size, ring] of AVATAR_SIZES) { + out += `
JD
`; + } + out += '
'; + out += + '
accent'; + for (const [, size, ring] of AVATAR_SIZES) { + out += `
SH
`; + } + out += '
'; + return out; +} + +// ---- PasswordStrengthIndicator (3 strengths) ---- +// strengthConfig copied verbatim from PasswordStrengthIndicator.tsx +const PW_STRENGTHS = [ + [ + 'Weak', + 'bg-error', + 'text-error', + '33%', + 'Add more characters, uppercase, numbers, and symbols', + ], + [ + 'Medium', + 'bg-warning', + 'text-warning', + '66%', + 'Good! Consider adding more variety', + ], + ['Strong', 'bg-success', 'text-success', '100%', 'Excellent password!'], +]; +function passwordStrengthCard() { + let out = '
'; + for (const [label, color, textColor, width, desc] of PW_STRENGTHS) { + out += `
+
+
${label}${desc}
+
`; + } + out += '
'; + return out; +} + +// ---- ReadReceipt (sent / delivered / read) ---- +function check(cls) { + return ``; +} +function readReceiptCard() { + return ` +
+
sent
${check('text-base-content')}
+
delivered
${check('text-base-content absolute left-0')}${check('text-base-content absolute left-1')}
+
read
${check('text-primary absolute left-0')}${check('text-primary absolute left-1')}
+
`; +} + +// ---- Pagination (join pattern, verbatim classes) ---- +function paginationCard() { + return ` +
+ + +
`; +} + +// ---- Form inputs (ValidatedInput states + FormField wrapper) ---- +const INPUT_SIZES = [ + ['xs', 'input-xs min-h-11'], + ['sm', 'input-sm min-h-11'], + ['md', 'input-md min-h-11'], + ['lg', 'input-lg'], +]; +function errIcon() { + return ''; +} +function okIcon() { + return ''; +} +function inputsCard() { + let out = '
'; + // sizes + out += '
'; + for (const [name, cls] of INPUT_SIZES) { + out += `
`; + } + out += '
'; + // states via FormField wrapper (DaisyUI v5: no form-control/label-text; .label + text utilities) + out += `
+
${okIcon()}
+
Looks good.
`; + out += `
+
${errIcon()}
+
Must be at least 8 characters.
`; + out += `
+
`; + out += '
'; + return out; +} + +// ---- SocialIcon (configured platforms) ---- +const SOCIAL = { + github: + '', + twitter: + '', + linkedin: + '', + twitch: + '', +}; +// ---- Sparkline (SVG, theme-reactive via var() tokens; verbatim geometry) ---- +const VB_W = 100, + VB_H = 24, + PAD_Y = 1, + PLOT_H = VB_H - PAD_Y * 2; +const SPARK_TOKENS = { + primary: 'var(--color-primary)', + success: 'var(--color-success)', + error: 'var(--color-error)', + info: 'var(--color-info)', +}; +function sparklineSvg(data, tone) { + const maxY = Math.max(1, ...data); + const xStep = VB_W / (data.length - 1); + const yOf = (v) => PAD_Y + PLOT_H - (v / maxY) * PLOT_H; + const points = data + .map((v, i) => `${(i * xStep).toFixed(1)},${yOf(v).toFixed(1)}`) + .join(' '); + const areaPath = `M 0,${VB_H} L ${points.replace(/ /g, ' L ')} L ${VB_W},${VB_H} Z`; + const stroke = SPARK_TOKENS[tone]; + return ` + + + `; +} +function sparklineCard() { + const series = { + primary: [3, 5, 4, 8, 6, 9, 7, 11, 10, 14], + success: [2, 3, 5, 4, 6, 7, 6, 9, 11, 13], + error: [12, 10, 11, 8, 9, 6, 7, 4, 5, 2], + info: [5, 6, 5, 7, 6, 8, 7, 8, 9, 8], + }; + let out = '
'; + for (const [tone, data] of Object.entries(series)) { + out += `
${tone}${sparklineSvg(data, tone)}
`; + } + out += '
'; + return out; +} + +// ---- MessageBubble (chat-start/chat-end, markdown; verbatim chat-* classes) ---- +function messageBubbleCard() { + return ` +
+
+
Alex
+

Hey! Did you see the new design system?

+
+
+
You
+

Yes — it has both themes and tokens.

+ +
+
`; +} + +// ---- QueuedMessageBubble (pending + failed; verbatim classes) ---- +function queuedMessageBubbleCard() { + return ` +
+
+

Sending this one now…

+ +
+
+

This one failed to send.

+ +
+
`; +} + +function socialIconCard() { + let out = '
'; + const rows = [ + ['base-content', 'text-base-content'], + ['primary', 'text-primary'], + ['accent', 'text-accent'], + ]; + for (const [label, color] of rows) { + out += `
${label}`; + for (const [, path] of Object.entries(SOCIAL)) { + out += ``; + } + out += '
'; + } + out += '
'; + return out; +} + +module.exports = { + // group names map to Design System pane sections + tokens: [ + { slug: 'colors', group: 'Tokens', title: 'Colors', render: colorsCard }, + { + slug: 'shape', + group: 'Tokens', + title: 'Shape & radius', + render: shapeCard, + }, + { + slug: 'typography', + group: 'Tokens', + title: 'Typography scale', + render: typographyTokenCard, + }, + ], + components: [ + { slug: 'button', group: 'Buttons', title: 'Button', render: buttonCard }, + { slug: 'card', group: 'Cards', title: 'Card', render: cardCard }, + { + slug: 'tag-badge', + group: 'Badges', + title: 'TagBadge', + render: tagBadgeCard, + }, + { + slug: 'tooltip', + group: 'Overlays', + title: 'Tooltip', + render: tooltipCard, + }, + { slug: 'text', group: 'Typography', title: 'Text', render: textCard }, + { + slug: 'status', + group: 'Status', + title: 'Network & typing status', + render: statusCard, + }, + { + slug: 'avatar', + group: 'Media', + title: 'AvatarDisplay', + render: avatarCard, + }, + { + slug: 'inputs', + group: 'Forms', + title: 'Form inputs', + render: inputsCard, + }, + { + slug: 'password-strength', + group: 'Forms', + title: 'PasswordStrengthIndicator', + render: passwordStrengthCard, + }, + { + slug: 'pagination', + group: 'Navigation', + title: 'Pagination', + render: paginationCard, + }, + { + slug: 'read-receipt', + group: 'Status', + title: 'ReadReceipt', + render: readReceiptCard, + }, + { + slug: 'social-icon', + group: 'Media', + title: 'SocialIcon', + render: socialIconCard, + }, + { + slug: 'sparkline', + group: 'Data', + title: 'Sparkline', + render: sparklineCard, + }, + { + slug: 'message-bubble', + group: 'Messaging', + title: 'MessageBubble', + render: messageBubbleCard, + }, + { + slug: 'queued-message-bubble', + group: 'Messaging', + title: 'QueuedMessageBubble', + render: queuedMessageBubbleCard, + }, + ], +};