@hanzo/build 0.2.17 → 0.2.19

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.
@@ -50,7 +50,7 @@ export function Machines() {
50
50
  list.reload();
51
51
  }
52
52
  catch (e) {
53
- setNote(e instanceof Error ? e.message : 'That did not work');
53
+ setNote(e.message);
54
54
  }
55
55
  };
56
56
  return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsx(Group, { title: "Linked", children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading machines\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { status: m.status }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
@@ -64,21 +64,17 @@ export function Machines() {
64
64
  ] }) }, m.id))) })) }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
65
65
  setNote(`${m.label} is registered`);
66
66
  list.reload();
67
- } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '', run: async (label) => {
68
- if (!renaming)
69
- return;
67
+ } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
68
+ // Each dialog acts only while it is open, which is while its machine is chosen.
69
+ run: async (label) => {
70
70
  await change(t, renaming.id, { label });
71
71
  setNote(`Renamed to ${label}`);
72
72
  list.reload();
73
73
  } }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: `Remove ${removing?.label ?? 'this machine'}?`, says: "Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.", act: "Remove", run: async () => {
74
- if (!removing)
75
- return;
76
74
  await remove(t, removing.id);
77
75
  setNote(`${removing.label} is removed`);
78
76
  list.reload();
79
77
  } }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
80
- if (!keying)
81
- return;
82
78
  const k = await key(t, keying.id);
83
79
  setMinted({ machine: keying, key: k });
84
80
  } }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
@@ -110,7 +106,7 @@ function Adding({ open, onOpenChange, onAdded }) {
110
106
  close();
111
107
  }
112
108
  catch (e) {
113
- setNote(e instanceof Error ? e.message : 'That did not work');
109
+ setNote(e.message);
114
110
  setWorking(false);
115
111
  }
116
112
  };
@@ -38,7 +38,7 @@ export function Members() {
38
38
  setNote(done);
39
39
  }
40
40
  catch (e) {
41
- setNote(e instanceof Error ? e.message : 'That did not work');
41
+ setNote(e.message);
42
42
  }
43
43
  finally {
44
44
  setWorking(false);
@@ -46,8 +46,9 @@ export function Members() {
46
46
  };
47
47
  const send = () => {
48
48
  const address = email.trim();
49
+ // Invite is offered only once something is typed.
49
50
  if (!EMAIL.test(address)) {
50
- setNote(`${address || 'That'} is not an email address`);
51
+ setNote(`${address} is not an email address`);
51
52
  return;
52
53
  }
53
54
  void act(() => invite(t, org, address), `${address} is invited. IAM sends no email: share the code with them.`, () => {
@@ -39,7 +39,7 @@ export function Memory() {
39
39
  return true;
40
40
  }
41
41
  catch (e) {
42
- setNote(e instanceof Error ? e.message : 'That did not work');
42
+ setNote(e.message);
43
43
  return false;
44
44
  }
45
45
  finally {
@@ -37,7 +37,7 @@ export function Notifications() {
37
37
  setNote(await what());
38
38
  }
39
39
  catch (e) {
40
- setNote(e instanceof Error ? e.message : 'That did not work');
40
+ setNote(e.message);
41
41
  }
42
42
  finally {
43
43
  setWorking(false);
@@ -33,7 +33,7 @@ export function Privacy() {
33
33
  setShown(await setConsent(t, change));
34
34
  }
35
35
  catch (e) {
36
- setNote(e instanceof Error ? e.message : 'That was not saved');
36
+ setNote(e.message);
37
37
  }
38
38
  finally {
39
39
  setBusy('');
@@ -48,7 +48,7 @@ export function Privacy() {
48
48
  setNote(`${saved.name} is ${saved.visibility || 'saved'}`);
49
49
  }
50
50
  catch (e) {
51
- setNote(e instanceof Error ? e.message : 'That was not saved');
51
+ setNote(e.message);
52
52
  }
53
53
  finally {
54
54
  setBusy('');
@@ -14,9 +14,9 @@ import { useHost, useTarget } from '../host.js';
14
14
  import { path } from '../route.js';
15
15
  import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
16
16
  const SPAN = { hour: 'This hour', day: 'Today', week: 'This week', month: 'This month' };
17
- /** A thin bar: how much of a whole is used. */
17
+ /** A thin bar: how much of a whole is used. Every whole drawn is above zero: a limit of none is not drawn. */
18
18
  function Bar({ part, whole, limit }) {
19
- const pct = whole > 0 ? Math.min(100, Math.max(0, (part / whole) * 100)) : 0;
19
+ const pct = Math.min(100, Math.max(0, (part / whole) * 100));
20
20
  return (_jsx(YStack, { height: 6, rounded: 999, bg: "$hover", overflow: "hidden", role: "progressbar", "aria-valuenow": Math.round(pct), "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx(YStack, { height: 6, width: `${pct}%`, rounded: 999, bg: limit && pct >= 100 ? '$red10' : '$ink' }) }));
21
21
  }
22
22
  /**
@@ -67,7 +67,7 @@ export function Usage() {
67
67
  setNote(done);
68
68
  }
69
69
  catch (e) {
70
- setNote(e instanceof Error ? e.message : 'That did not work');
70
+ setNote(e.message);
71
71
  }
72
72
  finally {
73
73
  setWorking(false);
@@ -80,15 +80,14 @@ export function Usage() {
80
80
  const others = ceilings.value.filter((c) => c !== cap);
81
81
  const s = spent.value;
82
82
  const top = Math.max(0, ...(s?.categories ?? []).map((c) => c.cents));
83
+ const adding = cents(amount);
84
+ // Offered only with a card to charge.
83
85
  const buy = () => {
84
- const c = cents(amount);
85
- if (!c) {
86
+ if (!adding) {
86
87
  setNote('Enter an amount in dollars');
87
88
  return;
88
89
  }
89
- if (!card)
90
- return;
91
- void act(() => topup(t, c, card.id), `${money(c)} is added to the balance`, () => {
90
+ void act(() => topup(t, adding, card.id), `${money(adding)} is added to the balance`, () => {
92
91
  setBuying(false);
93
92
  left.reload();
94
93
  });
@@ -104,7 +103,7 @@ export function Usage() {
104
103
  ceilings.reload();
105
104
  });
106
105
  };
107
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: cents(amount) ? `Add ${money(cents(amount) ?? 0)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
106
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
108
107
  .filter(Boolean)
109
108
  .join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
110
109
  }
package/lib/shelf.js CHANGED
@@ -42,7 +42,7 @@ function useFork() {
42
42
  host.go(made.slug || slug);
43
43
  }
44
44
  catch (e) {
45
- setFailed(e instanceof Error ? e.message : 'Could not copy this template');
45
+ setFailed(e.message);
46
46
  }
47
47
  finally {
48
48
  setBusy('');
@@ -93,7 +93,7 @@ export function Artifacts() {
93
93
  list.reload();
94
94
  }
95
95
  catch (e) {
96
- setNote(e instanceof Error ? e.message : 'That did not work');
96
+ setNote(e.message);
97
97
  }
98
98
  };
99
99
  const said = failed || note;
@@ -104,19 +104,15 @@ export function Artifacts() {
104
104
  { key: 'visibility', label: p.visibility === 'private' ? 'Make public' : 'Make private', onSelect: () => void flip(p) },
105
105
  { type: 'separator' },
106
106
  { key: 'delete', label: 'Delete', destructive: true, onSelect: () => setDeleting(p) },
107
- ] })] }, p.slug))) })] }, g.label))) }))] }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename", name: renaming?.name ?? '', run: async (name) => {
108
- if (!renaming)
109
- return;
107
+ ] })] }, p.slug))) })] }, g.label))) }))] }), renaming ? (_jsx(Rename, { open: true, onOpenChange: (o) => !o && setRenaming(null), title: "Rename", name: renaming.name, run: async (name) => {
110
108
  await change(t, renaming.slug, { name });
111
109
  setNote(`Renamed to ${name}`);
112
110
  list.reload();
113
- } }), _jsx(Confirm, { open: deleting !== null, onOpenChange: (o) => !o && setDeleting(null), title: `Delete ${deleting?.name ?? 'this project'}?`, says: "Its site stops answering, its releases are dropped, and its address is released. This cannot be undone.", act: "Delete", run: async () => {
114
- if (!deleting)
115
- return;
111
+ } })) : null, deleting ? (_jsx(Confirm, { open: true, onOpenChange: (o) => !o && setDeleting(null), title: `Delete ${deleting.name}?`, says: "Its site stops answering, its releases are dropped, and its address is released. This cannot be undone.", act: "Delete", run: async () => {
116
112
  await remove(t, deleting.slug);
117
113
  setNote(`${deleting.name} is deleted`);
118
114
  list.reload();
119
- } })] }));
115
+ } })) : null] }));
120
116
  }
121
117
  function Shot({ slug, height = 140 }) {
122
118
  const [gone, setGone] = useState(false);
package/lib/sync.js CHANGED
@@ -17,9 +17,6 @@ import { useRead } from './data.js';
17
17
  import { useHost, useTarget } from './host.js';
18
18
  import { path } from './route.js';
19
19
  const EMPTY = { repos: [], unread: [] };
20
- function noteOf(e, fallback) {
21
- return e instanceof Error ? e.message : fallback;
22
- }
23
20
  export function Sync() {
24
21
  const host = useHost();
25
22
  const t = useTarget();
@@ -57,7 +54,7 @@ export function Sync() {
57
54
  host.open(url);
58
55
  }
59
56
  catch (e) {
60
- setError(noteOf(e, 'Could not open the GitHub connection'));
57
+ setError(e.message);
61
58
  }
62
59
  };
63
60
  const send = async () => {
@@ -73,7 +70,7 @@ export function Sync() {
73
70
  setSyncing(true);
74
71
  }
75
72
  catch (e) {
76
- setError(noteOf(e, 'Could not queue those repositories'));
73
+ setError(e.message);
77
74
  }
78
75
  finally {
79
76
  setBusy(false);
package/lib/tools.js CHANGED
@@ -45,8 +45,7 @@ export function Attach({ files, onFiles, onNote }) {
45
45
  // `hidden` alone loses to a reset that sets `display` on inputs.
46
46
  style: { display: 'none' }, tabIndex: -1, "aria-hidden": true, onChange: (e) => {
47
47
  const input = e.currentTarget;
48
- if (!input.files)
49
- return;
48
+ // A file input always holds a list, empty or not.
50
49
  void read(files, input.files).then(({ files: next, note }) => {
51
50
  onFiles(next);
52
51
  onNote(note);
@@ -60,5 +59,5 @@ export function Files({ files, onFiles }) {
60
59
  }
61
60
  export function Dictate({ onText, onNote }) {
62
61
  const voice = useDictation(useTarget(), onText, onNote);
63
- return (_jsxs(XStack, { items: "center", children: [_jsx(ComposerTool, { label: !voice.able ? 'Dictation needs a microphone this page can record' : voice.on ? 'Stop and transcribe' : voice.busy ? 'Transcribing…' : 'Dictate', icon: voice.on ? _jsx(MicOff, { size: 14 }) : _jsx(Mic, { size: 14 }), onPress: () => void voice.toggle(), disabled: !voice.able || voice.busy, "aria-pressed": voice.on }), _jsx(ChipSelect, { quiet: true, name: "Dictation language", icon: _jsx(ChevronDown, { size: 12 }), label: "", chosen: voice.languages.find((l) => l.id === voice.language) ?? null, items: voice.languages, onChange: (l) => voice.setLanguage(l.id), placeholder: "Search languages\u2026", disabled: !voice.able })] }));
62
+ return (_jsxs(XStack, { items: "center", children: [_jsx(ComposerTool, { label: !voice.able ? 'Dictation needs a microphone this page can record' : voice.on ? 'Stop and transcribe' : voice.busy ? 'Transcribing…' : 'Dictate', icon: voice.on ? _jsx(MicOff, { size: 14 }) : _jsx(Mic, { size: 14 }), onPress: () => void voice.toggle(), disabled: !voice.able || voice.busy, "aria-pressed": voice.on }), _jsx(ChipSelect, { quiet: true, name: "Dictation language", icon: _jsx(ChevronDown, { size: 12 }), label: "", chosen: voice.languages.find((l) => l.id === voice.language), items: voice.languages, onChange: (l) => voice.setLanguage(l.id), placeholder: "Search languages\u2026", disabled: !voice.able })] }));
64
63
  }
package/lib/visit.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const PLACES: readonly [{
3
+ readonly label: 'Download';
4
+ readonly href: '/#features';
5
+ }, {
6
+ readonly label: 'Enterprise';
7
+ readonly href: "https://hanzo.ai/enterprise";
8
+ }, {
9
+ readonly label: 'Pricing';
10
+ readonly href: "https://hanzo.ai/pricing";
11
+ }];
12
+ export declare const RESOURCES: readonly [{
13
+ readonly label: 'Docs';
14
+ readonly href: 'https://docs.hanzo.ai';
15
+ }, {
16
+ readonly label: 'Blog';
17
+ readonly href: "https://hanzo.ai/blog";
18
+ }, {
19
+ readonly label: 'Customers';
20
+ readonly href: "https://hanzo.ai/customers";
21
+ }, {
22
+ readonly label: 'Learn';
23
+ readonly href: "https://hanzo.ai/learn";
24
+ }, {
25
+ readonly label: 'Support';
26
+ readonly href: "https://hanzo.ai/support";
27
+ }];
28
+ /** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
29
+ export declare function Bar({ name }: {
30
+ name: ReactNode;
31
+ }): import("react").JSX.Element;
package/lib/visit.js ADDED
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The page to a visitor: the product's header over New.
4
+ *
5
+ * Signed out there are no runs to list, so the rail gives way to a header —
6
+ * Download (Hanzo Dev, under New, pitch.tsx), Resources, Enterprise, Pricing — and the two
7
+ * doors in, which are one door: IAM's page offers both. New stays the page, and
8
+ * sending from it asks the visitor to sign in with the sentence kept
9
+ * (landing.tsx). Below md the four places fold into one menu beside Log in.
10
+ */
11
+ import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
12
+ import { ChevronDown, Menu } from '@hanzogui/lucide-icons-2';
13
+ import { Button } from '@hanzo/ui';
14
+ import { MenuLabel, MenuRow, MenuRule } from '@hanzo/ui/product';
15
+ import { useState } from 'react';
16
+ import { useHost } from './host.js';
17
+ const SITE = 'https://hanzo.ai';
18
+ export const PLACES = [
19
+ { label: 'Download', href: '/#features' },
20
+ { label: 'Enterprise', href: `${SITE}/enterprise` },
21
+ { label: 'Pricing', href: `${SITE}/pricing` },
22
+ ];
23
+ export const RESOURCES = [
24
+ { label: 'Docs', href: 'https://docs.hanzo.ai' },
25
+ { label: 'Blog', href: `${SITE}/blog` },
26
+ { label: 'Customers', href: `${SITE}/customers` },
27
+ { label: 'Learn', href: `${SITE}/learn` },
28
+ { label: 'Support', href: `${SITE}/support` },
29
+ ];
30
+ const leave = (href) => window.location.assign(href);
31
+ /** One place in the header: an address, drawn as text. */
32
+ function Place({ href, children }) {
33
+ return (_jsx("a", { href: href, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: children }) }) }));
34
+ }
35
+ function Resources() {
36
+ const [open, setOpen] = useState(false);
37
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-start", offset: 6, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs(XStack, { render: "button", "aria-label": "Resources", "aria-expanded": open, items: "center", gap: "$1", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Resources" }), _jsx(ChevronDown, { size: 14, color: "$soft" })] }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Resources", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 220, bg: "$panel", borderColor: "$borderColor", children: _jsx(YStack, { gap: "$1", children: RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))) }) })] }));
38
+ }
39
+ /** Below md: every place, the resources and the door, in one menu. */
40
+ function Fold({ onEnter }) {
41
+ const [open, setOpen] = useState(false);
42
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-end", offset: 6, stayInFrame: true, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Menu", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Menu", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [PLACES.map((p) => (_jsx(MenuRow, { label: p.label, onPress: () => leave(p.href) }, p.label))), _jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Resources" }), RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Sign up", onPress: () => {
43
+ setOpen(false);
44
+ onEnter();
45
+ } })] }) })] }));
46
+ }
47
+ /** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
48
+ export function Bar({ name }) {
49
+ const host = useHost();
50
+ const enter = () => host.signIn?.();
51
+ const [features, enterprise, pricing] = PLACES;
52
+ return (_jsxs(XStack, { role: "banner", height: 60, px: "$4", gap: "$2", items: "center", shrink: 0, $md: { px: '$6' }, children: [name, _jsxs(XStack, { role: "navigation", "aria-label": "Site", items: "center", gap: "$1", ml: "$4", "$max-md": { display: 'none' }, children: [_jsx(Place, { href: features.href, children: features.label }), _jsx(Resources, {}), _jsx(Place, { href: enterprise.href, children: enterprise.label }), _jsx(Place, { href: pricing.href, children: pricing.label })] }), _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "sm", onPress: enter, children: "Log in" }), _jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Button, { size: "sm", variant: "primary", onPress: enter, children: "Sign up" }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Fold, { onEnter: enter }) })] }));
53
+ }
package/lib/voice.js CHANGED
@@ -24,10 +24,8 @@ export function spoken() {
24
24
  const nav = typeof navigator !== 'undefined' ? navigator.language.split('-')[0] : 'en';
25
25
  return LANGUAGES.some((l) => l.id === nav) ? nav : 'en';
26
26
  }
27
- /** Whether this browser can record the microphone at all. */
27
+ /** Whether this browser can record the microphone at all. A page that is not a secure context has no media devices. */
28
28
  function recordable() {
29
- if (typeof window === 'undefined' || typeof navigator === 'undefined')
30
- return false;
31
29
  return typeof window.MediaRecorder === 'function' && Boolean(navigator.mediaDevices?.getUserMedia);
32
30
  }
33
31
  export function useDictation(t, onText, onNote) {
@@ -42,7 +40,7 @@ export function useDictation(t, onText, onNote) {
42
40
  // The person's saved language (Settings → General), else this browser's, else English.
43
41
  const kept = usePrefs();
44
42
  const language = LANGUAGES.some((l) => l.id === kept.prefs.language) ? kept.prefs.language : spoken();
45
- const setLanguage = useCallback((id) => kept.save({ language: id }).catch((e) => note.current(e instanceof Error ? e.message : 'The language was not saved.')), [kept]);
43
+ const setLanguage = useCallback((id) => kept.save({ language: id }).catch((e) => note.current(e.message)), [kept]);
46
44
  const ear = useMemo(() => speech({ baseUrl: t.api, token: t.token, ear: 'whisper' }), [t]);
47
45
  useEffect(() => setAble(recordable()), []);
48
46
  const toggle = useCallback(async () => {
@@ -80,7 +78,7 @@ export function useDictation(t, onText, onNote) {
80
78
  if (words)
81
79
  said.current(words);
82
80
  })
83
- .catch((e) => note.current(e instanceof Error ? e.message : 'The platform could not transcribe that.'))
81
+ .catch((e) => note.current(e.message))
84
82
  .finally(() => setBusy(false));
85
83
  };
86
84
  rec.current = r;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
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 page (hanzo.build) and as a component (hanzo.ai/dev).",