@hanzo/build 0.2.76 → 0.2.77

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.
@@ -35,8 +35,9 @@ export interface Start {
35
35
  }
36
36
  /**
37
37
  * A design system: one of Studio's brands, the one store for them. Its tokens
38
- * are W3C design tokens (color, font, type, space, radius) and two groups of our
39
- * own, `components` and `voice`, each a line a model follows.
38
+ * are CSS custom properties (--background, --primary, --font-sans, --radius-md,
39
+ * …, the form Studio's renderer reads), with `--component-*` and `--voice-*` as
40
+ * quoted lines a model follows (design.ts `tokensOf`).
40
41
  *
41
42
  * GET /v1/studio/brands {data: [brand]} the ones you may read
42
43
  * GET /v1/studio/brands/{id} one
package/lib/carry.js CHANGED
@@ -110,8 +110,7 @@ export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
110
110
  const named = kits.value.find((b) => b.id === design);
111
111
  const designs = {
112
112
  rows: kits.value.map((b) => {
113
- const c = (b.tokens.color ?? {});
114
- return { id: b.id, label: b.name, note: [c.primary?.$value, c.accent?.$value].filter(Boolean).join(' '), icon: _jsx(Palette, { size: 16 }), on: b.id === design };
113
+ return { id: b.id, label: b.name, note: [b.tokens['--primary'], b.tokens['--accent']].filter((x) => typeof x === 'string').join(' '), icon: _jsx(Palette, { size: 16 }), on: b.id === design };
115
114
  }),
116
115
  pick: (id) => gear.set({ design: design === id ? '' : id }),
117
116
  loading: kits.loading,
package/lib/design.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A design system as the composer handles it: drafted by a model, kept as a
3
- * Studio brand (W3C design tokens plus `components` and `voice`), said to the
3
+ * Studio brand (CSS custom properties, the form Studio's renderer reads), said to the
4
4
  * model on every message of a conversation it is attached to, and drawn as a
5
5
  * page to look at. Pure, so it is tested without a browser.
6
6
  */
@@ -126,8 +126,14 @@ export declare const SCHEMA: {
126
126
  };
127
127
  /** A model's answer as a Draft, or why it is not one. */
128
128
  export declare function drafted(raw: unknown): Draft | string;
129
- /** A Draft as W3C design tokens, with our two groups beside them. */
130
- export declare function tokensOf(d: Draft): Record<string, unknown>;
129
+ /**
130
+ * A Draft as design tokens: CSS custom properties, the names @hanzo/design uses
131
+ * (--background, --foreground, --primary, --font-sans, …). That is the one form
132
+ * Studio's renderer takes (hanzo/render `tokens`: every key `--name`, every value
133
+ * one declaration), so a deck, a document and a page draw from the same map.
134
+ * The components and the voice ride along as quoted strings under their own names.
135
+ */
136
+ export declare function tokensOf(d: Draft): Record<string, string>;
131
137
  /** What a model is told on every message of a conversation the system is attached to. */
132
138
  export declare function said(b: Brand): string;
133
139
  /** The system drawn as a page: its palette, type, a button, a card, an input, its voice. */
package/lib/design.js CHANGED
@@ -54,40 +54,92 @@ export function drafted(raw) {
54
54
  voice: { tone: str(v.tone), do: lines(v.do), dont: lines(v.dont) },
55
55
  };
56
56
  }
57
- /** A Draft as W3C design tokens, with our two groups beside them. */
57
+ /**
58
+ * A Draft as design tokens: CSS custom properties, the names @hanzo/design uses
59
+ * (--background, --foreground, --primary, --font-sans, …). That is the one form
60
+ * Studio's renderer takes (hanzo/render `tokens`: every key `--name`, every value
61
+ * one declaration), so a deck, a document and a page draw from the same map.
62
+ * The components and the voice ride along as quoted strings under their own names.
63
+ */
58
64
  export function tokensOf(d) {
59
- const group = (rec, type, unit = '') => Object.fromEntries(Object.entries(rec).map(([k, v]) => [k, { $value: unit ? `${v}${unit}` : v, $type: type }]));
60
- return {
61
- color: group(d.colors, 'color'),
62
- font: group({ heading: d.fonts.heading, body: d.fonts.body, mono: d.fonts.mono }, 'fontFamily'),
63
- type: group(Object.fromEntries(d.scale.map((n, i) => [`step${i}`, n])), 'dimension', 'px'),
64
- space: group(Object.fromEntries(d.spacing.map((n, i) => [`s${i}`, n])), 'dimension', 'px'),
65
- radius: group(d.radius, 'dimension', 'px'),
66
- components: Object.fromEntries(Object.entries(d.components).map(([k, v]) => [k, { $value: v, $type: 'string' }])),
67
- voice: { tone: { $value: d.voice.tone, $type: 'string' }, do: { $value: d.voice.do, $type: 'string' }, dont: { $value: d.voice.dont, $type: 'string' } },
65
+ // Quoted, with nothing that would make JSON escape it or end the declaration.
66
+ const q = (v) => JSON.stringify(v.replace(/["\\;{}<>\u0000-\u001f]|\/\*/g, ' ').slice(0, 280));
67
+ const c = d.colors;
68
+ const out = {
69
+ '--background': c.background,
70
+ '--foreground': c.text,
71
+ '--primary': c.primary,
68
72
  };
73
+ const more = { '--secondary': c.secondary, '--accent': c.accent, '--card': c.surface, '--muted-foreground': c.muted, '--border': c.border };
74
+ for (const [k, v] of Object.entries(more))
75
+ if (v)
76
+ out[k] = v;
77
+ out['--font-display'] = d.fonts.heading;
78
+ out['--font-sans'] = d.fonts.body;
79
+ out['--font-mono'] = d.fonts.mono;
80
+ for (const [k, v] of Object.entries(d.radius))
81
+ out[`--radius-${k}`] = `${v}px`;
82
+ d.scale.forEach((n, i) => (out[`--text-${i}`] = `${n}px`));
83
+ d.spacing.forEach((n, i) => (out[`--space-${i}`] = `${n}px`));
84
+ for (const [k, v] of Object.entries(d.components))
85
+ out[`--component-${k.replace(/[^a-z0-9-]/gi, '-').toLowerCase()}`] = q(v);
86
+ if (d.voice.tone)
87
+ out['--voice-tone'] = q(d.voice.tone);
88
+ if (d.voice.do.length)
89
+ out['--voice-do'] = q(d.voice.do.join('; ').replace(/;/g, ','));
90
+ if (d.voice.dont.length)
91
+ out['--voice-dont'] = q(d.voice.dont.join('; ').replace(/;/g, ','));
92
+ return out;
69
93
  }
70
- /** One token group as `name value` pairs. */
71
- function pairs(tokens, group) {
72
- return Object.entries(obj(tokens[group])).map(([k, v]) => {
73
- const val = obj(v).$value;
74
- return [k, Array.isArray(val) ? val.join('; ') : String(val ?? '')];
75
- });
94
+ /** A token's value as written, a quoted string unquoted. */
95
+ const value = (v) => {
96
+ const s = typeof v === 'string' ? v : '';
97
+ try {
98
+ return s.startsWith('"') ? String(JSON.parse(s)) : s;
99
+ }
100
+ catch {
101
+ return s;
102
+ }
103
+ };
104
+ /** The tokens whose names start `prefix`, as `name value` pairs, the prefix dropped. */
105
+ function pairs(tokens, prefix) {
106
+ return Object.entries(tokens)
107
+ .filter(([k]) => k.startsWith(prefix))
108
+ .map(([k, v]) => [k.slice(prefix.length), value(v)]);
76
109
  }
110
+ /** The colours by the names a person reads them. */
111
+ const COLOURS = [
112
+ ['primary', '--primary'],
113
+ ['secondary', '--secondary'],
114
+ ['accent', '--accent'],
115
+ ['background', '--background'],
116
+ ['surface', '--card'],
117
+ ['text', '--foreground'],
118
+ ['muted', '--muted-foreground'],
119
+ ['border', '--border'],
120
+ ];
121
+ const colours = (t) => COLOURS.filter(([, k]) => t[k]).map(([n, k]) => [n, value(t[k])]);
77
122
  /** What a model is told on every message of a conversation the system is attached to. */
78
123
  export function said(b) {
79
124
  const t = b.tokens;
80
- const row = (g) => pairs(t, g).map(([k, v]) => `${k} ${v}`).join(', ');
125
+ const row = (list) => list.map(([k, v]) => `${k} ${v}`).join(', ');
126
+ const fonts = [
127
+ ['heading', value(t['--font-display'])],
128
+ ['body', value(t['--font-sans'])],
129
+ ['mono', value(t['--font-mono'])],
130
+ ].filter(([, v]) => v);
131
+ const comps = pairs(t, '--component-');
132
+ const voice = [value(t['--voice-tone']), value(t['--voice-do']) && `do: ${value(t['--voice-do'])}`, value(t['--voice-dont']) && `don't: ${value(t['--voice-dont'])}`].filter(Boolean);
81
133
  const parts = [
82
134
  `Design system "${b.name}" is attached to this conversation. Everything you make — pages, apps, components, slides, images and copy — follows it exactly; never invent other colours, fonts or radii.`,
83
- row('color') && `Colours: ${row('color')}.`,
84
- row('font') && `Fonts: ${row('font')}.`,
85
- row('type') && `Type scale: ${row('type')}.`,
86
- row('space') && `Spacing: ${row('space')}.`,
87
- row('radius') && `Radius: ${row('radius')}.`,
88
- row('components') && `Components: ${pairs(t, 'components').map(([k, v]) => `${k} — ${v}`).join(' ')}`,
89
- row('voice') && `Voice: ${pairs(t, 'voice').map(([k, v]) => `${k}: ${v}`).join(' | ')}`,
90
- 'In HTML, set these as CSS custom properties on :root and use them; load the fonts from Google Fonts when they are web fonts.',
135
+ colours(t).length && `Colours: ${row(colours(t))}.`,
136
+ fonts.length && `Fonts: ${row(fonts)}.`,
137
+ pairs(t, '--text-').length && `Type scale: ${row(pairs(t, '--text-'))}.`,
138
+ pairs(t, '--space-').length && `Spacing: ${row(pairs(t, '--space-'))}.`,
139
+ pairs(t, '--radius-').length && `Radius: ${row(pairs(t, '--radius-'))}.`,
140
+ comps.length && `Components: ${comps.map(([k, v]) => `${k} — ${v}`).join(' ')}`,
141
+ voice.length && `Voice: ${voice.join(' | ')}`,
142
+ `In HTML, declare these exact custom properties on :root and use them: ${Object.keys(t).filter((k) => !k.startsWith('--component-') && !k.startsWith('--voice-')).join(', ')}. Load the fonts from Google Fonts when they are web fonts.`,
91
143
  ];
92
144
  return parts.filter(Boolean).join('\n');
93
145
  }
@@ -95,29 +147,29 @@ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>'
95
147
  /** The system drawn as a page: its palette, type, a button, a card, an input, its voice. */
96
148
  export function preview(b) {
97
149
  const t = b.tokens;
98
- const c = Object.fromEntries(pairs(t, 'color'));
99
- const f = Object.fromEntries(pairs(t, 'font'));
100
- const r = Object.fromEntries(pairs(t, 'radius'));
101
- const sizes = pairs(t, 'type').map(([, v]) => v);
102
- const voice = Object.fromEntries(pairs(t, 'voice'));
103
- const comps = pairs(t, 'components');
104
- const fonts = [f.heading, f.body].filter((x) => x && !/^ui-|system|sans-serif|serif$/i.test(x));
150
+ const v = (k, d) => value(t[k]) || d;
151
+ const sizes = pairs(t, '--text-').map(([, x]) => x);
152
+ const comps = pairs(t, '--component-');
153
+ const heading = v('--font-display', 'Inter');
154
+ const body = v('--font-sans', 'Inter');
155
+ const fonts = [heading, body].filter((x) => x && !/^ui-|system|sans-serif|serif$/i.test(x));
105
156
  const link = fonts.length ? `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?${fonts.map((x) => `family=${encodeURIComponent(x)}:wght@400;600;700`).join('&')}&display=swap">` : '';
106
- const swatches = Object.entries(c).map(([k, v]) => `<div class="sw"><span style="background:${esc(v)}"></span><b>${esc(k)}</b><code>${esc(v)}</code></div>`).join('');
107
- const scale = sizes.map((s) => `<div style="font-size:${esc(s)}">${esc(b.name)} — ${esc(s)}</div>`).join('');
157
+ const swatches = colours(t).map(([k, c]) => `<div class="sw"><span style="background:${esc(c)}"></span><b>${esc(k)}</b><code>${esc(c)}</code></div>`).join('');
158
+ const scale = sizes.map((x) => `<div style="font-size:${esc(x)}">${esc(b.name)} — ${esc(x)}</div>`).join('');
159
+ const root = Object.entries(t).filter(([k]) => !k.startsWith('--component-') && !k.startsWith('--voice-')).map(([k, x]) => `${k}:${value(x)}`).join(';');
108
160
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(b.name)}</title>${link}
109
- <style>:root{--primary:${c.primary};--secondary:${c.secondary ?? c.primary};--accent:${c.accent ?? c.primary};--bg:${c.background};--surface:${c.surface ?? c.background};--text:${c.text};--muted:${c.muted ?? c.text};--border:${c.border ?? c.muted ?? c.text};--r-sm:${r.sm ?? '4px'};--r-md:${r.md ?? '8px'};--r-lg:${r.lg ?? '16px'}}
110
- body{margin:0;background:var(--bg);color:var(--text);font-family:'${esc(f.body ?? 'Inter')}',system-ui,sans-serif;padding:40px;line-height:1.5}
111
- h1,h2{font-family:'${esc(f.heading ?? 'Inter')}',system-ui,sans-serif;margin:0 0 12px}h2{margin-top:40px;font-size:14px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
112
- .row{display:flex;flex-wrap:wrap;gap:16px}.sw{display:flex;flex-direction:column;gap:4px;width:120px}.sw span{height:64px;border-radius:var(--r-md);border:1px solid var(--border)}.sw code{color:var(--muted);font-size:12px}
113
- .btn{background:var(--primary);color:var(--bg);border:0;border-radius:var(--r-md);padding:12px 20px;font:inherit;font-weight:600}.btn.ghost{background:transparent;color:var(--text);border:1px solid var(--border)}
114
- .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:20px;max-width:320px}input{background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;font:inherit;width:260px}
115
- li{color:var(--muted)}</style></head><body>
116
- <h1 style="font-size:40px">${esc(b.name)}</h1><p style="color:var(--muted)">${esc(voice.tone ?? '')}</p>
161
+ <style>:root{${root}}
162
+ body{margin:0;background:var(--background);color:var(--foreground);font-family:'${esc(body)}',system-ui,sans-serif;padding:40px;line-height:1.5}
163
+ h1,h2{font-family:'${esc(heading)}',system-ui,sans-serif;margin:0 0 12px}h2{margin-top:40px;font-size:14px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-foreground,var(--foreground))}
164
+ .row{display:flex;flex-wrap:wrap;gap:16px}.sw{display:flex;flex-direction:column;gap:4px;width:120px}.sw span{height:64px;border-radius:var(--radius-md,8px);border:1px solid var(--border,var(--foreground))}.sw code{color:var(--muted-foreground);font-size:12px}
165
+ .btn{background:var(--primary);color:var(--background);border:0;border-radius:var(--radius-md,8px);padding:12px 20px;font:inherit;font-weight:600}.btn.ghost{background:transparent;color:var(--foreground);border:1px solid var(--border,var(--foreground))}
166
+ .card{background:var(--card,var(--background));border:1px solid var(--border,var(--foreground));border-radius:var(--radius-lg,16px);padding:20px;max-width:320px}input{background:var(--card,var(--background));color:var(--foreground);border:1px solid var(--border,var(--foreground));border-radius:var(--radius-sm,4px);padding:10px 12px;font:inherit;width:260px}
167
+ li{color:var(--muted-foreground)}</style></head><body>
168
+ <h1 style="font-size:40px">${esc(b.name)}</h1><p style="color:var(--muted-foreground)">${esc(value(t['--voice-tone']))}</p>
117
169
  <h2>Colour</h2><div class="row">${swatches}</div>
118
170
  <h2>Type</h2>${scale}
119
- <h2>Components</h2><div class="row" style="align-items:flex-start"><button class="btn">Primary action</button><button class="btn ghost">Secondary</button><input placeholder="An input"><div class="card"><b>A card</b><p style="color:var(--muted);margin:8px 0 0">${esc(comps.find(([k]) => k === 'card')?.[1] ?? '')}</p></div></div>
120
- <ul>${comps.map(([k, v]) => `<li><b style="color:var(--text)">${esc(k)}</b> — ${esc(v)}</li>`).join('')}</ul>
121
- <h2>Voice</h2><p><b>Do</b>: ${esc(voice.do ?? '')}</p><p><b>Don't</b>: ${esc(voice.dont ?? '')}</p>
171
+ <h2>Components</h2><div class="row" style="align-items:flex-start"><button class="btn">Primary action</button><button class="btn ghost">Secondary</button><input placeholder="An input"><div class="card"><b>A card</b><p style="color:var(--muted-foreground);margin:8px 0 0">${esc(comps.find(([k]) => k === 'card')?.[1] ?? '')}</p></div></div>
172
+ <ul>${comps.map(([k, x]) => `<li><b style="color:var(--foreground)">${esc(k)}</b> — ${esc(x)}</li>`).join('')}</ul>
173
+ <h2>Voice</h2><p><b>Do</b>: ${esc(value(t['--voice-do']))}</p><p><b>Don't</b>: ${esc(value(t['--voice-dont']))}</p>
122
174
  </body></html>`;
123
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",