@hanzo/build 0.2.75 → 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.
@@ -13,8 +13,13 @@
13
13
  * signed and stop working at `expires`: read the item again for fresh ones.
14
14
  */
15
15
  import { type Target } from './call.ts';
16
- export declare const KINDS: readonly ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
17
- export type Kind = (typeof KINDS)[number];
16
+ export declare const KIND_IDS: readonly ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
17
+ export type Kind = (typeof KIND_IDS)[number];
18
+ /** The kinds a reader narrows to, in the order the Library lists them, '' being all. */
19
+ export declare const KINDS: readonly {
20
+ id: Kind | '';
21
+ label: string;
22
+ }[];
18
23
  export declare const SOURCES: readonly ['chat', 'dev', 'studio', 'workflow'];
19
24
  export type Source = (typeof SOURCES)[number];
20
25
  export type Scope = 'all' | 'mine' | 'shared';
@@ -13,7 +13,19 @@
13
13
  * signed and stop working at `expires`: read the item again for fresh ones.
14
14
  */
15
15
  import { call, query, seg } from './call.js';
16
- export const KINDS = ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
16
+ export const KIND_IDS = ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
17
+ /** The kinds a reader narrows to, in the order the Library lists them, '' being all. */
18
+ export const KINDS = [
19
+ { id: '', label: 'All' },
20
+ { id: 'image', label: 'Images' },
21
+ { id: 'video', label: 'Videos' },
22
+ { id: 'audio', label: 'Audio' },
23
+ { id: 'document', label: 'Documents' },
24
+ { id: 'slides', label: 'Slides' },
25
+ { id: 'model', label: '3D' },
26
+ { id: 'code', label: 'Code' },
27
+ { id: 'file', label: 'Files' },
28
+ ];
17
29
  export const SOURCES = ['chat', 'dev', 'studio', 'workflow'];
18
30
  /** A product, as a person names it. */
19
31
  export const SOURCE = { chat: 'Chat', dev: 'Dev', studio: 'Studio', workflow: 'Workflow' };
@@ -33,7 +45,7 @@ export function item(raw) {
33
45
  const id = str(o.id);
34
46
  if (!id)
35
47
  return null;
36
- const kind = KINDS.includes(str(o.kind)) ? o.kind : 'file';
48
+ const kind = KIND_IDS.includes(str(o.kind)) ? o.kind : 'file';
37
49
  const source = SOURCES.includes(str(o.source)) ? o.source : 'dev';
38
50
  return {
39
51
  id,
@@ -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/lib/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export { Library, Outputs, Preview, Thumb, want } from './library.tsx';
25
25
  export { SOURCE, type Item, type Kind as ItemKind, type Source } from './api/library.ts';
26
26
  export { Scheduled, schedule } from './scheduled.tsx';
27
27
  export { Editor } from './automations.tsx';
28
- export { nav, NEW, own, sections, tabs, type Group } from './nav.tsx';
28
+ export { library, nav, NEW, own, sections, tabs, useScheduled, type Group } from './nav.tsx';
29
29
  export { DevSection, DOTS, useSessions } from './section.tsx';
30
30
  export { HostProvider } from './host.tsx';
31
31
  export { Slack, useWho, Who } from './foot.tsx';
package/lib/index.js CHANGED
@@ -25,7 +25,7 @@ export { Library, Outputs, Preview, Thumb, want } from './library.js';
25
25
  export { SOURCE } from './api/library.js';
26
26
  export { Scheduled, schedule } from './scheduled.js';
27
27
  export { Editor } from './automations.js';
28
- export { nav, NEW, own, sections, tabs } from './nav.js';
28
+ export { library, nav, NEW, own, sections, tabs, useScheduled } from './nav.js';
29
29
  export { DevSection, DOTS, useSessions } from './section.js';
30
30
  export { HostProvider } from './host.js';
31
31
  export { Slack, useWho, Who } from './foot.js';
package/lib/library.js CHANGED
@@ -18,26 +18,16 @@ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from
18
18
  import { ChipSelect } from '@hanzo/ui/product';
19
19
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
20
20
  import { ago } from './ago.js';
21
- import { change, days, get, items as list, remove, size, SOURCE } from './api/library.js';
21
+ import { change, days, get, items as list, KINDS, remove, size, SOURCE } from './api/library.js';
22
22
  import { Confirm, Rename } from './ask.js';
23
23
  import { useKept, useProjects, useRead } from './data.js';
24
24
  import { useHost, useTarget } from './host.js';
25
+ import { KEPT } from './nav.js';
26
+ import { path, route } from './route.js';
25
27
  import { Panel } from './panel.js';
26
28
  import { gap, pane } from './pane.js';
27
29
  import { Sites } from './shelf.js';
28
30
  import { useDeck } from './tabs.js';
29
- /** The kinds a reader narrows to, in the order the chips draw them. */
30
- const KINDS = [
31
- { id: '', label: 'All' },
32
- { id: 'image', label: 'Images' },
33
- { id: 'video', label: 'Videos' },
34
- { id: 'audio', label: 'Audio' },
35
- { id: 'document', label: 'Documents' },
36
- { id: 'slides', label: 'Slides' },
37
- { id: 'model', label: '3D' },
38
- { id: 'code', label: 'Code' },
39
- { id: 'file', label: 'Files' },
40
- ];
41
31
  const FROM = [
42
32
  { id: '', label: 'Anywhere' },
43
33
  { id: 'chat', label: 'Chat' },
@@ -351,7 +341,10 @@ export function Library() {
351
341
  const t = useTarget();
352
342
  const signed = Boolean(host.person);
353
343
  const wide = useMedia().md;
354
- const [kind, setKind] = useState('');
344
+ // The kind is the address's (-/artifacts/<kind>), so the sidebar, a link and Back all name it.
345
+ const at = route(host.path);
346
+ const kind = at.kind === 'screen' && at.of ? at.of : '';
347
+ const setKind = (k) => host.go(path({ kind: 'screen', screen: 'artifacts', ...(k ? { of: k } : {}) }));
355
348
  const [source, setSource] = useState('');
356
349
  const [scope, setScope] = useState('all');
357
350
  const [typed, setTyped] = useState('');
@@ -368,6 +361,11 @@ export function Library() {
368
361
  const load = useMemo(() => (signed ? (cursor, signal) => list(t, { kind, source, scope, q, limit: PAGE, cursor }, { signal }) : null), [signed, t, kind, source, scope, q]);
369
362
  const pages = usePages(load, `${kind}|${source}|${scope}|${q}|${t.org}`);
370
363
  const end = useEnd(pages.more);
364
+ const reload = pages.reload;
365
+ useEffect(() => {
366
+ window.addEventListener(KEPT, reload);
367
+ return () => window.removeEventListener(KEPT, reload);
368
+ }, [reload]);
371
369
  const look = useCallback((x) => {
372
370
  side.open({ id: `${TAB}${x.id}`, kind: `${TAB}${x.id}`, title: x.name });
373
371
  setShown(true);
@@ -397,7 +395,7 @@ export function Library() {
397
395
  const open = shown && side.tabs.length > 0;
398
396
  const groups = days(pages.rows);
399
397
  const narrowed = Boolean(kind || source || q || scope !== 'all');
400
- return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsx(YStack, { flex: 1, minH: 0, minW: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { render: "h1", size: "$7", fontWeight: "600", color: "$ink", flex: 1, children: "Library" }), _jsxs(XStack, { role: "radiogroup", "aria-label": "View", gap: "$1", children: [_jsx(ViewMark, { label: "Grid", on: view === 'grid', onPress: () => setView('grid'), children: _jsx(LayoutGrid, { size: 16 }) }), _jsx(ViewMark, { label: "List", on: view === 'list', onPress: () => setView('list'), children: _jsx(List, { size: 16 }) })] })] }), _jsxs(XStack, { items: "center", gap: "$2", maxW: 480, children: [_jsx(Search, { size: 16, opacity: 0.6 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(Input, { value: typed, onChangeText: setTyped, placeholder: "Search your library", "aria-label": "Search your library", disabled: !signed }) })] }), _jsxs(YStack, { gap: "$2", children: [_jsx(XStack, { role: "radiogroup", "aria-label": "Kind", gap: "$2", overflowX: "auto", pb: "$1", "$platform-web": { scrollbarWidth: 'none' }, children: KINDS.map((k) => (_jsx(Chip, { label: k.label, on: kind === k.id, onPress: () => setKind(k.id) }, k.id || 'all'))) }), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [_jsx(ChipSelect, { name: "From", label: FROM.find((f) => f.id === source).label, chosen: FROM.find((f) => f.id === source), items: FROM, onChange: (f) => setSource(f.id), placement: "bottom-start", width: 200 }), _jsx(ChipSelect, { name: "Whose", label: WHOSE.find((w) => w.id === scope).label, chosen: WHOSE.find((w) => w.id === scope), items: WHOSE, onChange: (w) => setScope(w.id), placement: "bottom-start", width: 220 })] })] }), acts.note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: acts.note })) : null, !signed ? (_jsx(Said, { says: "Sign in to see your library.", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) })) : pages.error && !pages.rows.length ? (_jsx(Said, { says: pages.error.message, children: _jsx(Button, { size: "sm", variant: "outline", onPress: pages.reload, children: "Try again" }) })) : pages.loading && !pages.rows.length ? (_jsx(Said, { says: "Reading\u2026" })) : !pages.rows.length ? (_jsx(Said, { says: narrowed ? 'Nothing matches.' : 'Nothing here yet. What you make in Chat, Dev and Studio lands here.' })) : (_jsxs(YStack, { gap: "$5", children: [groups.map((g) => (_jsxs(YStack, { gap: "$3", role: "group", "aria-label": g.label, children: [_jsx(SizableText, { size: "$2", color: "$soft", fontWeight: "500", children: g.label }), view === 'grid' ? (_jsx("div", { role: "list", "aria-label": `Made ${g.label.toLowerCase()}`, style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(46%, 168px), 1fr))', gap: 16 }, children: g.rows.map((x) => (_jsx(Tile, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) })) : (_jsx(YStack, { role: "list", "aria-label": `Made ${g.label.toLowerCase()}`, gap: "$0.5", children: g.rows.map((x) => (_jsx(Row, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) }))] }, g.label))), _jsx(YStack, { ref: end, height: 1 }), pages.loading ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Reading more\u2026" })) : pages.error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: pages.error.message })) : null] })), signed && !q && !kind && (source === '' || source === 'dev') && scope !== 'shared' ? (_jsx(YStack, { pt: "$6", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Sites, {}) })) : null] }) }), open && !wide ? panel(true) : null, open && wide ? (_jsx(YStack, { "data-slot": "library-side", ...pane, ml: gap, shrink: 0, minH: 0, width: 440, maxW: "45%", children: panel(false) })) : null, acts.dialogs] }));
398
+ return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsx(YStack, { flex: 1, minH: 0, minW: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { render: "h1", size: "$7", fontWeight: "600", color: "$ink", flex: 1, children: KINDS.find((k) => k.id === kind)?.id ? KINDS.find((k) => k.id === kind).label : 'Library' }), _jsxs(XStack, { role: "radiogroup", "aria-label": "View", gap: "$1", children: [_jsx(ViewMark, { label: "Grid", on: view === 'grid', onPress: () => setView('grid'), children: _jsx(LayoutGrid, { size: 16 }) }), _jsx(ViewMark, { label: "List", on: view === 'list', onPress: () => setView('list'), children: _jsx(List, { size: 16 }) })] })] }), _jsxs(XStack, { items: "center", gap: "$2", maxW: 480, children: [_jsx(Search, { size: 16, opacity: 0.6 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(Input, { value: typed, onChangeText: setTyped, placeholder: "Search your library", "aria-label": "Search your library", disabled: !signed }) })] }), _jsxs(YStack, { gap: "$2", children: [host.sections ? null : (_jsx(XStack, { role: "radiogroup", "aria-label": "Kind", gap: "$2", overflowX: "auto", pb: "$1", "$platform-web": { scrollbarWidth: 'none' }, children: KINDS.map((k) => (_jsx(Chip, { label: k.label, on: kind === k.id, onPress: () => setKind(k.id) }, k.id || 'all'))) })), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [_jsx(ChipSelect, { name: "From", label: FROM.find((f) => f.id === source).label, chosen: FROM.find((f) => f.id === source), items: FROM, onChange: (f) => setSource(f.id), placement: "bottom-start", width: 200 }), _jsx(ChipSelect, { name: "Whose", label: WHOSE.find((w) => w.id === scope).label, chosen: WHOSE.find((w) => w.id === scope), items: WHOSE, onChange: (w) => setScope(w.id), placement: "bottom-start", width: 220 })] })] }), acts.note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: acts.note })) : null, !signed ? (_jsx(Said, { says: "Sign in to see your library.", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) })) : pages.error && !pages.rows.length ? (_jsx(Said, { says: pages.error.message, children: _jsx(Button, { size: "sm", variant: "outline", onPress: pages.reload, children: "Try again" }) })) : pages.loading && !pages.rows.length ? (_jsx(Said, { says: "Reading\u2026" })) : !pages.rows.length ? (_jsx(Said, { says: narrowed ? 'Nothing matches.' : 'Nothing here yet. What you make in Chat, Dev and Studio lands here.' })) : (_jsxs(YStack, { gap: "$5", children: [groups.map((g) => (_jsxs(YStack, { gap: "$3", role: "group", "aria-label": g.label, children: [_jsx(SizableText, { size: "$2", color: "$soft", fontWeight: "500", children: g.label }), view === 'grid' ? (_jsx("div", { role: "list", "aria-label": `Made ${g.label.toLowerCase()}`, style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(46%, 168px), 1fr))', gap: 16 }, children: g.rows.map((x) => (_jsx(Tile, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) })) : (_jsx(YStack, { role: "list", "aria-label": `Made ${g.label.toLowerCase()}`, gap: "$0.5", children: g.rows.map((x) => (_jsx(Row, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) }))] }, g.label))), _jsx(YStack, { ref: end, height: 1 }), pages.loading ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Reading more\u2026" })) : pages.error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: pages.error.message })) : null] })), signed && !q && !kind && (source === '' || source === 'dev') && scope !== 'shared' ? (_jsx(YStack, { pt: "$6", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Sites, {}) })) : null] }) }), open && !wide ? panel(true) : null, open && wide ? (_jsx(YStack, { "data-slot": "library-side", ...pane, ml: gap, shrink: 0, minH: 0, width: 440, maxW: "45%", children: panel(false) })) : null, acts.dialogs] }));
401
399
  }
402
400
  function ViewMark({ label, on, onPress, children }) {
403
401
  return (_jsx(XStack, { render: "button", role: "radio", "aria-checked": on, "aria-label": label, onPress: onPress, width: 32, height: 32, items: "center", justify: "center", rounded: "$3", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", opacity: on ? 1 : 0.6, children: children }));
package/lib/nav.d.ts CHANGED
@@ -19,3 +19,9 @@ export declare const own: (host: Host, go: (path: string) => void) => Group[];
19
19
  export declare function sections(host: Host, go: (path: string) => void): Group[];
20
20
  /** Customize's tabs as a host's sidebar lists them, Agents first; an agent's profile lights Agents. */
21
21
  export declare function tabs(host: Host, go: (path: string) => void): Group[];
22
+ /** Fired on `window` once the sidebar's Upload has kept its files, for the Library to read again. */
23
+ export declare const KEPT = "hanzo:library:kept";
24
+ /** The Library's sidebar: Upload, then every kind it narrows to, the one in the address lit. */
25
+ export declare function library(host: Host, go: (path: string) => void): Group[];
26
+ /** Scheduled tasks' sidebar: New task, then each of the person's tasks, the one open lit. Read only while `on`. */
27
+ export declare function useScheduled(host: Host, go: (path: string) => void, on: boolean): Group[];
package/lib/nav.js CHANGED
@@ -9,8 +9,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  * Every row moves the builder through `go` with the address `path` writes, so
10
10
  * where a place lives is said here and in route.ts and nowhere else.
11
11
  */
12
- import { Bell, BookOpen, Bot, Brain, CircleDot, Clock, Code, Container, Cpu, CreditCard, FolderGit2, Gauge, Key, LayoutTemplate, Library, Plug, Puzzle, Settings, Shield, SlidersHorizontal, Sparkles, SquarePen, User, Users } from '@hanzogui/lucide-icons-2';
12
+ import { AudioLines, Bell, BookOpen, Bot, Box, Brain, CircleDot, Clock, Code, Container, Cpu, CreditCard, File, FileText, FolderGit2, Gauge, Image, Key, LayoutTemplate, Library, Plug, Plus, Presentation, Puzzle, Settings, Shield, SlidersHorizontal, Sparkles, SquarePen, Upload, User, Users, Video } from '@hanzogui/lucide-icons-2';
13
+ import { useMemo } from 'react';
14
+ import { automations } from './api/auto.js';
15
+ import { KINDS, upload } from './api/library.js';
13
16
  import { pinBoard } from './choice.js';
17
+ import { useRead } from './data.js';
14
18
  import { DOCS } from './foot.js';
15
19
  import { path, route, TABS } from './route.js';
16
20
  import { GROUPS, heading, LIST } from './settings/list.js';
@@ -117,3 +121,73 @@ const SECTION_ICON = {
117
121
  integrations: _jsx(Plug, { size: 16 }),
118
122
  notifications: _jsx(Bell, { size: 16 }),
119
123
  };
124
+ const KIND_ICON = {
125
+ '': _jsx(Library, { size: 16 }),
126
+ image: _jsx(Image, { size: 16 }),
127
+ video: _jsx(Video, { size: 16 }),
128
+ audio: _jsx(AudioLines, { size: 16 }),
129
+ document: _jsx(FileText, { size: 16 }),
130
+ slides: _jsx(Presentation, { size: 16 }),
131
+ model: _jsx(Box, { size: 16 }),
132
+ code: _jsx(Code, { size: 16 }),
133
+ file: _jsx(File, { size: 16 }),
134
+ };
135
+ /** Fired on `window` once the sidebar's Upload has kept its files, for the Library to read again. */
136
+ export const KEPT = 'hanzo:library:kept';
137
+ /** Asks for files and keeps each in the Library; a refusal is said in the page's own words. */
138
+ function keep(host) {
139
+ const input = document.createElement('input');
140
+ input.type = 'file';
141
+ input.multiple = true;
142
+ input.onchange = async () => {
143
+ const t = { api: host.api, token: host.token, org: host.org };
144
+ for (const f of Array.from(input.files ?? [])) {
145
+ try {
146
+ await upload(t, { name: f.name, type: f.type, bytes: new Uint8Array(await f.arrayBuffer()) }, { source: 'chat' });
147
+ }
148
+ catch (e) {
149
+ window.alert(e instanceof Error ? e.message : `${f.name} was not kept`);
150
+ }
151
+ }
152
+ window.dispatchEvent(new Event(KEPT));
153
+ };
154
+ input.click();
155
+ }
156
+ /** The Library's sidebar: Upload, then every kind it narrows to, the one in the address lit. */
157
+ export function library(host, go) {
158
+ const r = route(host.path);
159
+ const at = r.kind === 'screen' && r.screen === 'artifacts' ? (r.of ?? '') : null;
160
+ return [
161
+ { id: 'library-do', label: '', links: [{ id: 'upload', label: 'Upload', icon: _jsx(Upload, { size: 16 }), onPress: () => keep(host) }] },
162
+ {
163
+ id: 'library-kinds',
164
+ label: '',
165
+ links: KINDS.map((k) => ({
166
+ id: `kind-${k.id || 'all'}`,
167
+ label: k.label,
168
+ icon: KIND_ICON[k.id],
169
+ onPress: () => go(path({ kind: 'screen', screen: 'artifacts', ...(k.id ? { of: k.id } : {}) })),
170
+ active: at === k.id,
171
+ })),
172
+ },
173
+ ];
174
+ }
175
+ /** Scheduled tasks' sidebar: New task, then each of the person's tasks, the one open lit. Read only while `on`. */
176
+ export function useScheduled(host, go, on) {
177
+ const t = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
178
+ const list = useRead(on && host.person ? () => automations(t) : null, [], [t, on, host.path]);
179
+ const r = route(host.path);
180
+ const at = r.kind === 'automation' ? r.id : '';
181
+ return [
182
+ {
183
+ id: 'scheduled-do',
184
+ label: '',
185
+ links: [{ id: 'new-task', label: 'New task', icon: _jsx(Plus, { size: 16 }), onPress: () => go(path({ kind: 'automation', id: 'new' })), active: at === 'new' }],
186
+ },
187
+ {
188
+ id: 'scheduled-tasks',
189
+ label: list.value.length ? 'Tasks' : '',
190
+ links: list.value.map((a) => ({ id: a.id, label: a.name || 'Untitled task', icon: _jsx(Clock, { size: 16 }), onPress: () => go(path({ kind: 'automation', id: a.id })), active: at === a.id })),
191
+ },
192
+ ];
193
+ }
package/lib/route.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * '' the empty state: a new run
5
5
  * sess_<32 hex> one run
6
6
  * -/<screen> a builder screen
7
+ * -/artifacts/<kind> the Library narrowed to one kind (image, video, document, …)
7
8
  * -/automations/<id> one automation's editor and runs; `new` writes one
8
9
  * -/settings[/<section>] a section of Settings
9
10
  * -/customize[/<tab>] agents, skills, connectors and plugins: what the agent brings to a run
@@ -42,6 +43,7 @@ export type Route = {
42
43
  } | {
43
44
  kind: 'screen';
44
45
  screen: Screen;
46
+ of?: Of;
45
47
  } | {
46
48
  kind: 'settings';
47
49
  section: Section;
@@ -62,6 +64,9 @@ export type Route = {
62
64
  kind: 'automation';
63
65
  id: string;
64
66
  };
67
+ /** The kinds the Library narrows to by address (api/library.ts KIND_IDS). */
68
+ export declare const OF: readonly ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
69
+ export type Of = (typeof OF)[number];
65
70
  export declare const SESSION: RegExp;
66
71
  export declare const SLUG: RegExp;
67
72
  /** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
package/lib/route.js CHANGED
@@ -17,6 +17,8 @@ export const SECTIONS = [
17
17
  ];
18
18
  /** Customize, one tab each: what the agent brings to a run, yours and to discover. Agents first. */
19
19
  export const TABS = ['agents', 'skills', 'connectors', 'plugins'];
20
+ /** The kinds the Library narrows to by address (api/library.ts KIND_IDS). */
21
+ export const OF = ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
20
22
  export const SESSION = /^sess_[0-9a-f]{32}$/;
21
23
  export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
22
24
  /** An organization or a repository name on the forge (api/codebases.ts `NAME`). */
@@ -44,6 +46,10 @@ export function route(path) {
44
46
  const id = p.slice('-/automations/'.length);
45
47
  return AUTOMATION.test(id) ? { kind: 'automation', id } : { kind: 'new' };
46
48
  }
49
+ if (p.startsWith('-/artifacts/')) {
50
+ const of = p.slice('-/artifacts/'.length);
51
+ return OF.includes(of) ? { kind: 'screen', screen: 'artifacts', of } : { kind: 'new' };
52
+ }
47
53
  if (p.startsWith('-/')) {
48
54
  const s = p.slice(2);
49
55
  return SCREENS.includes(s) ? { kind: 'screen', screen: s } : { kind: 'new' };
@@ -103,7 +109,7 @@ export function path(r) {
103
109
  case 'run':
104
110
  return r.id;
105
111
  case 'screen':
106
- return `-/${r.screen}`;
112
+ return r.of ? `-/${r.screen}/${r.of}` : `-/${r.screen}`;
107
113
  case 'settings':
108
114
  return r.section === 'general' ? '-/settings' : `-/settings/${r.section}`;
109
115
  case 'customize':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.75",
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.",