@hanzo/build 0.2.68 → 0.2.69

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.
@@ -30,7 +30,34 @@ export interface Start {
30
30
  input: Record<string, unknown>;
31
31
  title?: string;
32
32
  parent?: string;
33
+ /** A brand (design system) the run reads as brand.* — its palette, type and voice. */
34
+ brand?: string;
33
35
  }
36
+ /**
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.
40
+ *
41
+ * GET /v1/studio/brands {data: [brand]} the ones you may read
42
+ * GET /v1/studio/brands/{id} one
43
+ * POST /v1/studio/brands {name, tokens, shared?} → brand
44
+ */
45
+ export interface Brand {
46
+ id: string;
47
+ name: string;
48
+ tokens: Record<string, unknown>;
49
+ }
50
+ export declare function brandOf(raw: unknown): Brand;
51
+ export declare function brands(t: Target, init?: {
52
+ signal?: AbortSignal;
53
+ }): Promise<Brand[]>;
54
+ export declare function brand(t: Target, id: string, init?: {
55
+ signal?: AbortSignal;
56
+ }): Promise<Brand>;
57
+ export declare function keepBrand(t: Target, b: {
58
+ name: string;
59
+ tokens: Record<string, unknown>;
60
+ }, signal?: AbortSignal): Promise<Brand>;
34
61
  export declare function run(raw: unknown): Run;
35
62
  /** A run that has ended, one way or the other. */
36
63
  export declare const ended: (r: Run) => boolean;
package/lib/api/studio.js CHANGED
@@ -14,6 +14,20 @@
14
14
  */
15
15
  import { call, Refusal, seg } from './call.js';
16
16
  import { item } from './library.js';
17
+ export function brandOf(raw) {
18
+ const o = obj(raw);
19
+ return { id: str(o.id), name: str(o.name), tokens: obj(o.tokens) };
20
+ }
21
+ export async function brands(t, init) {
22
+ const raw = obj(await call(t, 'GET', '/v1/studio/brands', undefined, init));
23
+ return (Array.isArray(raw.data) ? raw.data : []).map(brandOf).filter((b) => b.id);
24
+ }
25
+ export async function brand(t, id, init) {
26
+ return brandOf(await call(t, 'GET', `/v1/studio/brands/${seg(id)}`, undefined, init));
27
+ }
28
+ export async function keepBrand(t, b, signal) {
29
+ return brandOf(await call(t, 'POST', '/v1/studio/brands', { name: b.name, tokens: b.tokens }, { signal }));
30
+ }
17
31
  const str = (v) => (typeof v === 'string' ? v : '');
18
32
  const obj = (v) => (v && typeof v === 'object' ? v : {});
19
33
  export function run(raw) {
package/lib/carry.d.ts CHANGED
@@ -9,6 +9,8 @@ export type Lent = Omit<Composer, 'target' | 'show'>;
9
9
  export interface Carry {
10
10
  gear: GearState;
11
11
  skills: Shelf;
12
+ /** The design systems (Studio brands): attach one, or make one by chat. */
13
+ design: Shelf;
12
14
  /** The plugins that run for this person (`@` lists the same). */
13
15
  plugins: Shelf;
14
16
  /** A dialog a plugin opened, for the surface to draw; null when none is. */
@@ -16,7 +18,7 @@ export interface Carry {
16
18
  /** Whether a plugin that prepares a message is on. */
17
19
  preparing: boolean;
18
20
  /** What the plugins that are on add to the message about to go. */
19
- prep: (turn: Turn, signal: AbortSignal) => Promise<Prep>;
21
+ prep: (turn: Omit<Turn, 'design'>, signal: AbortSignal) => Promise<Prep>;
20
22
  /** The chips after `+`: the skills on, Research, and Plan. */
21
23
  on: Chip[];
22
24
  /** The words the gear adds to every message, '' when none. */
package/lib/carry.js CHANGED
@@ -15,11 +15,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
15
15
  * A shelf is offered only where its service answers; a surface adds its own
16
16
  * (Chat its agents, Dev its devices) beside these.
17
17
  */
18
- import { BookOpen, ListChecks, ScrollText } from '@hanzogui/lucide-icons-2';
18
+ import { BookOpen, ListChecks, Palette, ScrollText } from '@hanzogui/lucide-icons-2';
19
19
  import { useEffect, useMemo, useRef, useState } from 'react';
20
20
  import { call, query, unwrap } from './api/call.js';
21
21
  import { active, authored, document, nameOf as skillOf } from './api/skills.js';
22
+ import { brand, brands } from './api/studio.js';
22
23
  import { useRead } from './data.js';
24
+ import { said as designSaid } from './design.js';
23
25
  import { useGear } from './gear.js';
24
26
  import { active as lighted, prep, shelf as pluginShelf, usePlugins } from './plugins/index.js';
25
27
  /** The most of each skill a message carries. */
@@ -34,8 +36,13 @@ async function remembered(t) {
34
36
  .filter(Boolean);
35
37
  }
36
38
  /** The words `skills`, `memory` and `mode` add to a message. */
37
- async function said(t, skills, memory, mode) {
39
+ async function said(t, skills, memory, mode, design) {
38
40
  const parts = [];
41
+ if (design) {
42
+ const b = await brand(t, design).catch(() => null);
43
+ if (b?.name)
44
+ parts.push(designSaid(b));
45
+ }
39
46
  if (skills.length) {
40
47
  const own = await authored(t).catch(() => []);
41
48
  for (const n of skills) {
@@ -77,7 +84,7 @@ export function useCarry(t, key, customize, lent) {
77
84
  const signed = Boolean(t.token());
78
85
  const list = useRead(signed ? () => active(t) : null, [], [t, signed]);
79
86
  const names = useMemo(() => list.value.map((s) => skillOf(s.name)).filter(Boolean), [list.value]);
80
- const { skills, research, memory, mode } = gear.gear;
87
+ const { skills, research, memory, mode, design } = gear.gear;
81
88
  const shelf = {
82
89
  rows: names.map((n) => ({ id: n, label: n, icon: _jsx(ScrollText, { size: 16 }), on: skills.includes(n) })),
83
90
  pick: (id) => gear.toggle('skills', id),
@@ -89,11 +96,33 @@ export function useCarry(t, key, customize, lent) {
89
96
  browse: { label: 'Browse skills', onPress: () => customize('skills') },
90
97
  };
91
98
  const plugins = pluginShelf(found, gear, composer);
99
+ // The design systems: Studio's brands, the one store for them.
100
+ const kits = useRead(signed ? () => brands(t) : null, [], [t, signed, design]);
101
+ const named = kits.value.find((b) => b.id === design);
102
+ const designs = {
103
+ rows: kits.value.map((b) => {
104
+ const c = (b.tokens.color ?? {});
105
+ 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 };
106
+ }),
107
+ pick: (id) => gear.set({ design: design === id ? '' : id }),
108
+ loading: kits.loading,
109
+ error: kits.error ? 'The design systems could not be read.' : null,
110
+ empty: 'No design systems yet',
111
+ create: {
112
+ label: 'Create design system',
113
+ onPress: () => {
114
+ if (!gear.gear.plugins.includes('design'))
115
+ gear.toggle('plugins', 'design');
116
+ lentNow.current.setDraft('Make me a design system for ');
117
+ },
118
+ },
119
+ };
92
120
  const lit = lighted(found.plugins, gear.gear);
93
121
  const on = [
94
122
  ...skills.map((n) => ({ id: `skill:${n}`, label: n, icon: _jsx(ScrollText, { size: 14 }), onRemove: () => gear.toggle('skills', n) })),
95
123
  ...(research ? [{ id: 'research', label: 'Research', icon: _jsx(BookOpen, { size: 14 }), onRemove: () => gear.set({ research: false }) }] : []),
96
124
  ...(mode === 'plan' ? [{ id: 'plan', label: 'Plan', icon: _jsx(ListChecks, { size: 14 }), onRemove: () => gear.set({ mode: 'auto' }) }] : []),
125
+ ...(design ? [{ id: 'design', label: named?.name ?? 'Design system', icon: _jsx(Palette, { size: 14 }), onRemove: () => gear.set({ design: '' }) }] : []),
97
126
  ...lit
98
127
  .filter((p) => p.id !== 'research')
99
128
  .map((p) => ({ id: `plugin:${p.id}`, label: p.name, icon: p.icon, onRemove: () => gear.toggle('plugins', p.id) })),
@@ -105,13 +134,13 @@ export function useCarry(t, key, customize, lent) {
105
134
  if (!signedIn)
106
135
  return setText(mode === 'plan' ? 'Plan mode: lay out the plan and stop until the person says go.' : '');
107
136
  let live = true;
108
- void said(t, skills, memory, mode).then((x) => live && setText(x));
137
+ void said(t, skills, memory, mode, design).then((x) => live && setText(x));
109
138
  return () => {
110
139
  live = false;
111
140
  };
112
141
  // `skills` is read through `asked`, its contents.
113
142
  // eslint-disable-next-line react-hooks/exhaustive-deps
114
- }, [t, signedIn, asked, memory, mode]);
143
+ }, [t, signedIn, asked, memory, mode, design]);
115
144
  return {
116
145
  gear,
117
146
  skills: shelf,
@@ -120,6 +149,12 @@ export function useCarry(t, key, customize, lent) {
120
149
  on,
121
150
  text,
122
151
  preparing: lit.length > 0,
123
- prep: (turn, signal) => prep(found.plugins, gear.gear, turn, composer, signal),
152
+ design: designs,
153
+ // A design system a plugin made is attached at once, and the maker turns off: it made one.
154
+ prep: (turn, signal) => prep(found.plugins, gear.gear, { ...turn, design }, composer, signal).then((p) => {
155
+ if (p.design)
156
+ gear.set({ design: p.design, plugins: gear.gear.plugins.filter((x) => x !== 'design') });
157
+ return p;
158
+ }),
124
159
  };
125
160
  }
@@ -0,0 +1,134 @@
1
+ /**
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
4
+ * model on every message of a conversation it is attached to, and drawn as a
5
+ * page to look at. Pure, so it is tested without a browser.
6
+ */
7
+ import type { Brand } from './api/studio.ts';
8
+ /** What a model is asked to answer: the system in plain fields. */
9
+ export interface Draft {
10
+ name: string;
11
+ colors: Record<string, string>;
12
+ fonts: {
13
+ heading: string;
14
+ body: string;
15
+ mono: string;
16
+ };
17
+ /** Type sizes, px, smallest first. */
18
+ scale: number[];
19
+ /** Spacing steps, px. */
20
+ spacing: number[];
21
+ radius: Record<string, number>;
22
+ /** One line each: how a button, card, input, nav… looks and behaves. */
23
+ components: Record<string, string>;
24
+ voice: {
25
+ tone: string;
26
+ do: string[];
27
+ dont: string[];
28
+ };
29
+ }
30
+ /** The schema the drafting call decodes under. */
31
+ export declare const SCHEMA: {
32
+ readonly type: 'object';
33
+ readonly additionalProperties: false;
34
+ readonly required: readonly ['name', 'colors', 'fonts', 'scale', 'spacing', 'radius', 'components', 'voice'];
35
+ readonly properties: {
36
+ readonly name: {
37
+ readonly type: 'string';
38
+ };
39
+ readonly colors: {
40
+ readonly type: 'object';
41
+ readonly additionalProperties: false;
42
+ readonly required: readonly ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'];
43
+ readonly properties: {
44
+ [k: string]: {
45
+ type: string;
46
+ };
47
+ };
48
+ };
49
+ readonly fonts: {
50
+ readonly type: 'object';
51
+ readonly additionalProperties: false;
52
+ readonly required: readonly ['heading', 'body', 'mono'];
53
+ readonly properties: {
54
+ readonly heading: {
55
+ readonly type: 'string';
56
+ };
57
+ readonly body: {
58
+ readonly type: 'string';
59
+ };
60
+ readonly mono: {
61
+ readonly type: 'string';
62
+ };
63
+ };
64
+ };
65
+ readonly scale: {
66
+ readonly type: 'array';
67
+ readonly items: {
68
+ readonly type: 'number';
69
+ };
70
+ };
71
+ readonly spacing: {
72
+ readonly type: 'array';
73
+ readonly items: {
74
+ readonly type: 'number';
75
+ };
76
+ };
77
+ readonly radius: {
78
+ readonly type: 'object';
79
+ readonly additionalProperties: false;
80
+ readonly required: readonly ['sm', 'md', 'lg'];
81
+ readonly properties: {
82
+ readonly sm: {
83
+ readonly type: 'number';
84
+ };
85
+ readonly md: {
86
+ readonly type: 'number';
87
+ };
88
+ readonly lg: {
89
+ readonly type: 'number';
90
+ };
91
+ };
92
+ };
93
+ readonly components: {
94
+ readonly type: 'object';
95
+ readonly additionalProperties: false;
96
+ readonly required: readonly ['button', 'card', 'input', 'nav', 'hero'];
97
+ readonly properties: {
98
+ [k: string]: {
99
+ type: string;
100
+ };
101
+ };
102
+ };
103
+ readonly voice: {
104
+ readonly type: 'object';
105
+ readonly additionalProperties: false;
106
+ readonly required: readonly ['tone', 'do', 'dont'];
107
+ readonly properties: {
108
+ readonly tone: {
109
+ readonly type: 'string';
110
+ };
111
+ readonly do: {
112
+ readonly type: 'array';
113
+ readonly items: {
114
+ readonly type: 'string';
115
+ };
116
+ };
117
+ readonly dont: {
118
+ readonly type: 'array';
119
+ readonly items: {
120
+ readonly type: 'string';
121
+ };
122
+ };
123
+ };
124
+ };
125
+ };
126
+ };
127
+ /** A model's answer as a Draft, or why it is not one. */
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>;
131
+ /** What a model is told on every message of a conversation the system is attached to. */
132
+ export declare function said(b: Brand): string;
133
+ /** The system drawn as a page: its palette, type, a button, a card, an input, its voice. */
134
+ export declare function preview(b: Brand): string;
package/lib/design.js ADDED
@@ -0,0 +1,123 @@
1
+ /** The schema the drafting call decodes under. */
2
+ export const SCHEMA = {
3
+ type: 'object',
4
+ additionalProperties: false,
5
+ required: ['name', 'colors', 'fonts', 'scale', 'spacing', 'radius', 'components', 'voice'],
6
+ properties: {
7
+ name: { type: 'string' },
8
+ colors: {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ required: ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'],
12
+ properties: Object.fromEntries(['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'].map((k) => [k, { type: 'string' }])),
13
+ },
14
+ fonts: { type: 'object', additionalProperties: false, required: ['heading', 'body', 'mono'], properties: { heading: { type: 'string' }, body: { type: 'string' }, mono: { type: 'string' } } },
15
+ scale: { type: 'array', items: { type: 'number' } },
16
+ spacing: { type: 'array', items: { type: 'number' } },
17
+ radius: { type: 'object', additionalProperties: false, required: ['sm', 'md', 'lg'], properties: { sm: { type: 'number' }, md: { type: 'number' }, lg: { type: 'number' } } },
18
+ components: {
19
+ type: 'object',
20
+ additionalProperties: false,
21
+ required: ['button', 'card', 'input', 'nav', 'hero'],
22
+ properties: Object.fromEntries(['button', 'card', 'input', 'nav', 'hero'].map((k) => [k, { type: 'string' }])),
23
+ },
24
+ voice: { type: 'object', additionalProperties: false, required: ['tone', 'do', 'dont'], properties: { tone: { type: 'string' }, do: { type: 'array', items: { type: 'string' } }, dont: { type: 'array', items: { type: 'string' } } } },
25
+ },
26
+ };
27
+ const HEX = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
28
+ const str = (v) => (typeof v === 'string' ? v.trim() : '');
29
+ const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
30
+ const nums = (v, lo, hi) => (Array.isArray(v) ? v.filter((n) => typeof n === 'number' && n >= lo && n <= hi) : []);
31
+ const lines = (v) => (Array.isArray(v) ? v.map(str).filter(Boolean).slice(0, 8) : []);
32
+ /** A model's answer as a Draft, or why it is not one. */
33
+ export function drafted(raw) {
34
+ const o = obj(raw);
35
+ const name = str(o.name).slice(0, 80);
36
+ if (!name)
37
+ return 'The design system has no name.';
38
+ const colors = Object.fromEntries(Object.entries(obj(o.colors)).filter(([, v]) => HEX.test(str(v))).map(([k, v]) => [k, str(v)]));
39
+ if (!colors.primary || !colors.background || !colors.text)
40
+ return 'The design system needs at least a primary, a background and a text colour.';
41
+ const f = obj(o.fonts);
42
+ const fonts = { heading: str(f.heading) || 'Inter', body: str(f.body) || 'Inter', mono: str(f.mono) || 'ui-monospace' };
43
+ const r = obj(o.radius);
44
+ const radius = Object.fromEntries(Object.entries(r).filter(([, v]) => typeof v === 'number' && v >= 0 && v <= 64));
45
+ const v = obj(o.voice);
46
+ return {
47
+ name,
48
+ colors,
49
+ fonts,
50
+ scale: nums(o.scale, 8, 160).slice(0, 10),
51
+ spacing: nums(o.spacing, 0, 256).slice(0, 12),
52
+ radius,
53
+ components: Object.fromEntries(Object.entries(obj(o.components)).map(([k, d]) => [k, str(d)]).filter(([, d]) => d)),
54
+ voice: { tone: str(v.tone), do: lines(v.do), dont: lines(v.dont) },
55
+ };
56
+ }
57
+ /** A Draft as W3C design tokens, with our two groups beside them. */
58
+ 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' } },
68
+ };
69
+ }
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
+ });
76
+ }
77
+ /** What a model is told on every message of a conversation the system is attached to. */
78
+ export function said(b) {
79
+ const t = b.tokens;
80
+ const row = (g) => pairs(t, g).map(([k, v]) => `${k} ${v}`).join(', ');
81
+ const parts = [
82
+ `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.',
91
+ ];
92
+ return parts.filter(Boolean).join('\n');
93
+ }
94
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
95
+ /** The system drawn as a page: its palette, type, a button, a card, an input, its voice. */
96
+ export function preview(b) {
97
+ 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));
105
+ 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('');
108
+ 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>
117
+ <h2>Colour</h2><div class="row">${swatches}</div>
118
+ <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>
122
+ </body></html>`;
123
+ }
package/lib/landing.js CHANGED
@@ -205,7 +205,8 @@ export function Landing({ onStarted }) {
205
205
  const prepped = carry.preparing ? await carry.prep({ text: draft.trim(), files: [], thread: '' }, new AbortController().signal) : {};
206
206
  // An answer of a plugin's own (a deck) is the answer: no run is started for it.
207
207
  if (prepped.reply) {
208
- setNote(prepped.reply);
208
+ // Its words, without the page it drew (Chat shows that in the side panel).
209
+ setNote(prepped.reply.split('```')[0].trim());
209
210
  return;
210
211
  }
211
212
  const run = await start(t, {
@@ -308,6 +309,7 @@ export function Landing({ onStarted }) {
308
309
  : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(mode, place.id) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: "Describe a task or ask a question", head: head, plus: {
309
310
  upload: () => pick(files, setFiles, setNote),
310
311
  skills: carry.skills,
312
+ design: carry.design,
311
313
  plugins: carry.plugins,
312
314
  devices,
313
315
  memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
package/lib/plugin.d.ts CHANGED
@@ -35,6 +35,8 @@ export interface Turn {
35
35
  files: File[];
36
36
  /** The conversation's id, or '' for one not begun. */
37
37
  thread: string;
38
+ /** The design system attached to the conversation (a Studio brand id), or ''. */
39
+ design: string;
38
40
  }
39
41
  /** What a plugin adds to the message. */
40
42
  export interface Prep {
@@ -44,6 +46,8 @@ export interface Prep {
44
46
  files?: File[];
45
47
  /** An answer of the plugin's own, in markdown; the model is not asked. */
46
48
  reply?: string;
49
+ /** A design system the plugin made: attached to the conversation from then on. */
50
+ design?: string;
47
51
  }
48
52
  export interface Plugin {
49
53
  id: string;
@@ -0,0 +1,8 @@
1
+ import { type Target } from '../api/call.ts';
2
+ import type { Plugin, Prep, Turn } from '../plugin.ts';
3
+ /** Hanzo's model that decodes under a strict schema. */
4
+ export declare const DRAFTER = "zen5-flash";
5
+ /** The site a brief points at, if it names one. */
6
+ export declare const siteOf: (text: string) => string;
7
+ export declare function make(turn: Turn, t: Target, signal: AbortSignal): Promise<Prep>;
8
+ export declare const designer: Plugin;
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Design system: while it is on, a message is a brief for one — "make me a
4
+ * design system for X", or a site to take it from — and the answer is the
5
+ * system: drafted by a Hanzo model under a strict schema (palette, type,
6
+ * spacing, radii, components, voice), kept as a Studio brand (the one store for
7
+ * them), drawn as a page in the side panel, and attached to the conversation, so
8
+ * everything made after it follows it. From `+` › Design system it attaches to
9
+ * any later conversation, in Chat or in Dev.
10
+ */
11
+ import { Palette } from '@hanzogui/lucide-icons-2';
12
+ import { call } from '../api/call.js';
13
+ import { keepBrand } from '../api/studio.js';
14
+ import { drafted, preview, SCHEMA, tokensOf } from '../design.js';
15
+ /** Hanzo's model that decodes under a strict schema. */
16
+ export const DRAFTER = 'zen5-flash';
17
+ /** The site a brief points at, if it names one. */
18
+ export const siteOf = (text) => text.match(/https?:\/\/[^\s)]+/)?.[0] ?? '';
19
+ const BRIEF = 'You design design systems. From the brief (and the site text, when given), answer one complete, coherent system: ' +
20
+ 'a name; eight hex colours with enough contrast for text on background and on primary; heading, body and mono fonts (Google Fonts names where you can); ' +
21
+ 'a type scale of 6-8 px sizes; a spacing scale of 6-10 px steps; sm, md and lg radii; one line each for button, card, input, nav and hero; ' +
22
+ 'and a voice: its tone in a sentence, 3-5 do and 3-5 dont lines. Take colours, fonts and tone from the site when one is given.';
23
+ async function site(t, url, signal) {
24
+ if (!url)
25
+ return '';
26
+ const d = await call(t, 'POST', '/v1/crawl', { url }, { signal }).catch(() => null);
27
+ return d?.success ? (d.data?.markdown ?? '').slice(0, 6000) : '';
28
+ }
29
+ export async function make(turn, t, signal) {
30
+ const brief = turn.text.trim();
31
+ if (!brief)
32
+ return {};
33
+ const page = await site(t, siteOf(brief), signal);
34
+ const raw = await call(t, 'POST', '/v1/chat/completions', {
35
+ model: DRAFTER,
36
+ max_tokens: 2000,
37
+ messages: [
38
+ { role: 'system', content: BRIEF },
39
+ { role: 'user', content: page ? `${brief}\n\nThe site, as text:\n${page}` : brief },
40
+ ],
41
+ response_format: { type: 'json_schema', json_schema: { name: 'design_system', strict: true, schema: SCHEMA } },
42
+ }, { signal });
43
+ const text = raw?.choices?.[0]?.message?.content;
44
+ let parsed = null;
45
+ try {
46
+ parsed = JSON.parse(typeof text === 'string' ? text : '');
47
+ }
48
+ catch {
49
+ throw new Error('The model did not answer a design system. Try the brief again.');
50
+ }
51
+ const d = drafted(parsed);
52
+ if (typeof d === 'string')
53
+ throw new Error(d);
54
+ const b = await keepBrand(t, { name: d.name, tokens: tokensOf(d) }, signal);
55
+ const file = `${d.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'design-system'}.html`;
56
+ const reply = [
57
+ `**${d.name}** is saved and attached to this conversation: what you build here follows it, and \`+\` › Design system attaches it anywhere else.`,
58
+ `Colours ${Object.values(d.colors).join(' ')} · ${d.fonts.heading} / ${d.fonts.body} · radius ${Object.values(d.radius).join('/')}px`,
59
+ d.voice.tone ? `Voice: ${d.voice.tone}` : '',
60
+ '```html ' + file + '\n' + preview(b) + '\n```',
61
+ ];
62
+ return { reply: reply.filter(Boolean).join('\n\n'), design: b.id };
63
+ }
64
+ export const designer = {
65
+ id: 'design',
66
+ name: 'Design system',
67
+ note: 'Make one from a brief or a site',
68
+ icon: _jsx(Palette, { size: 16 }),
69
+ prepare: (turn, c, signal) => make(turn, c.target, signal),
70
+ };
@@ -23,6 +23,7 @@ import { say } from '../customize/say.js';
23
23
  import { path } from '../route.js';
24
24
  import { pdf } from './pdf.js';
25
25
  import { project } from './project.js';
26
+ import { designer } from './design.js';
26
27
  import { deep } from './research.js';
27
28
  import { sketch } from './sketch.js';
28
29
  import { slides } from './slides.js';
@@ -46,6 +47,7 @@ export async function probe(t) {
46
47
  sketch,
47
48
  administers(t.token(), t.org) ? template : null,
48
49
  runs.includes('slides') ? slides : null,
50
+ studio.status === 'fulfilled' ? designer : null,
49
51
  pdf(runs.includes('document')),
50
52
  ].filter((p) => p !== null);
51
53
  }
@@ -40,7 +40,7 @@ export async function deck(turn, t, signal) {
40
40
  if (!ask)
41
41
  return {};
42
42
  const last = turn.thread ? decks.get(turn.thread) : undefined;
43
- const r = await make(t, { template: 'slides', title: ask.slice(0, 80), input: { brief: last ? revision(last, ask) : ask, format: 'pptx' } }, signal);
43
+ const r = await make(t, { template: 'slides', title: ask.slice(0, 80), input: { brief: last ? revision(last, ask) : ask, format: 'pptx' }, ...(turn.design ? { brand: turn.design } : {}) }, signal);
44
44
  if (turn.thread)
45
45
  decks.set(turn.thread, r);
46
46
  return { reply: told(r) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.68",
3
+ "version": "0.2.69",
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.",