@hanzo/build 0.2.9 → 0.2.11
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.
- package/README.md +126 -23
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +42 -4
- package/lib/api/coding.js +62 -3
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +39 -4
- package/lib/api/sessions.js +58 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +224 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +197 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +50 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +125 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +8 -1
- package/lib/desk.js +29 -35
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +8 -4
- package/lib/git.d.ts +9 -0
- package/lib/git.js +74 -0
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +47 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +207 -28
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/e2e/site.spec.d.ts +0 -1
- package/lib/e2e/site.spec.js +0 -125
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
package/lib/landing.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* New: the empty state. A heading at the top of the column, and at the foot of
|
|
4
|
-
* the pane the composer — where the run goes (the sandbox
|
|
5
|
-
*
|
|
4
|
+
* the pane the composer — where the run goes (Cloud: the sandbox with the
|
|
5
|
+
* codebase's environment; or one of the org's machines, under remote control),
|
|
6
|
+
* which repository and branch it starts from, and the ask itself.
|
|
6
7
|
* Under it: attach, dictate, the mode, and the model and effort on the right.
|
|
7
8
|
*
|
|
8
9
|
* Sending starts a coding run and opens it. The choices a person made here are
|
|
@@ -10,17 +11,21 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
11
|
*/
|
|
11
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
13
|
import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
|
|
14
|
+
import { Button } from '@hanzo/ui';
|
|
13
15
|
import { ModeSelect } from '@hanzo/ui/agents';
|
|
14
16
|
import { Composer, EmptyPrompt } from '@hanzo/ui/chat';
|
|
15
|
-
import { BranchSelect, ChipSelect, HanzoMark, RepoSelect } from '@hanzo/ui/product';
|
|
17
|
+
import { BranchSelect, ChipSelect, FooterLink, HanzoMark, RepoSelect } from '@hanzo/ui/product';
|
|
16
18
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
17
19
|
import { start, unhonoured } from './api/coding.js';
|
|
18
20
|
import { asRepo, chosen, codebases, one } from './api/codebases.js';
|
|
21
|
+
import { read, SETUP } from './api/environment.js';
|
|
22
|
+
import { SetupDialog } from './environment.js';
|
|
19
23
|
import { ENSO, models } from './api/models.js';
|
|
20
24
|
import { ready, SANDBOX } from './api/places.js';
|
|
21
25
|
import { isForge } from './choice.js';
|
|
22
26
|
import { useKept, usePlaces, useRead } from './data.js';
|
|
23
27
|
import { useHost, useTarget } from './host.js';
|
|
28
|
+
import { usePrefs } from './prefs.js';
|
|
24
29
|
import { Publish } from './publish.js';
|
|
25
30
|
import { path } from './route.js';
|
|
26
31
|
import { Attach, compose, Dictate, Files } from './tools.js';
|
|
@@ -29,7 +34,7 @@ export const MODES = [
|
|
|
29
34
|
{ id: 'build', label: 'Build', hint: 'Edits, commits and pushes a branch' },
|
|
30
35
|
{ id: 'plan', label: 'Plan', hint: 'Plans the change and writes nothing' },
|
|
31
36
|
];
|
|
32
|
-
const EFFORTS = [
|
|
37
|
+
export const EFFORTS = [
|
|
33
38
|
{ id: 'low', label: 'Low' },
|
|
34
39
|
{ id: 'medium', label: 'Medium' },
|
|
35
40
|
{ id: 'high', label: 'High' },
|
|
@@ -40,13 +45,20 @@ export function Landing({ onStarted }) {
|
|
|
40
45
|
const host = useHost();
|
|
41
46
|
const t = useTarget();
|
|
42
47
|
const signed = Boolean(host.person);
|
|
43
|
-
const
|
|
48
|
+
const { prefs } = usePrefs();
|
|
49
|
+
const [stored, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
|
|
50
|
+
// Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
|
|
51
|
+
const kept = stored ?? { ...FIRST, ...prefs.code };
|
|
44
52
|
const set = (patch) => keep({ ...kept, ...patch });
|
|
45
53
|
const [draft, setDraft] = useState(kept.ask || '');
|
|
46
54
|
const [files, setFiles] = useState([]);
|
|
47
55
|
const [busy, setBusy] = useState(false);
|
|
48
56
|
const [note, setNote] = useState('');
|
|
49
57
|
const [adding, setAdding] = useState(null);
|
|
58
|
+
const [offering, setOffering] = useState(false);
|
|
59
|
+
// The repository list is read again each time it opens; Refresh reads it again while open.
|
|
60
|
+
const [picking, setPicking] = useState(false);
|
|
61
|
+
const [round, setRound] = useState(0);
|
|
50
62
|
// A codebase or an issue hands its words over once. Left in the kept choice,
|
|
51
63
|
// every later visit to New would put them back.
|
|
52
64
|
useEffect(() => {
|
|
@@ -57,6 +69,9 @@ export function Landing({ onStarted }) {
|
|
|
57
69
|
}, []);
|
|
58
70
|
const places = usePlaces(t, signed);
|
|
59
71
|
const catalog = useRead(signed ? () => models(t) : null, [], [t, signed]);
|
|
72
|
+
// The chosen codebase's environment, so New can say when it has none yet.
|
|
73
|
+
const codebase = isForge(kept.repo) ? kept.repo.name : '';
|
|
74
|
+
const env = useRead(signed && codebase ? () => read(t, codebase) : null, null, [t, signed, codebase]);
|
|
60
75
|
const place = places.value.find((p) => p.id === kept.place) ?? SANDBOX;
|
|
61
76
|
const known = useRef(new Map());
|
|
62
77
|
const loadRepos = useCallback(async (q) => {
|
|
@@ -110,21 +125,34 @@ export function Landing({ onStarted }) {
|
|
|
110
125
|
setBusy(false);
|
|
111
126
|
}
|
|
112
127
|
};
|
|
128
|
+
/** Start the agent that finds this codebase's environment, and open it. A refusal is the dialog's to say. */
|
|
129
|
+
const setup = async () => {
|
|
130
|
+
const run = await start(t, { prompt: SETUP, repo: codebase, mode: 'setup' });
|
|
131
|
+
onStarted(run.session);
|
|
132
|
+
};
|
|
133
|
+
// Cloud is the sandbox, set up by the chosen codebase's environment.
|
|
134
|
+
const setUp = codebase && env.value
|
|
135
|
+
? env.value.state === 'ready'
|
|
136
|
+
? `${codebase} environment`
|
|
137
|
+
: env.value.state === 'proposed'
|
|
138
|
+
? `${codebase} environment proposed`
|
|
139
|
+
: `no ${codebase} environment yet`
|
|
140
|
+
: '';
|
|
113
141
|
const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
|
|
114
142
|
id: p.id || 'sandbox',
|
|
115
|
-
label: p.label,
|
|
116
|
-
hint: p.id ? [p.status, p.capacity].filter(Boolean).join(' · ') : '
|
|
143
|
+
label: p.id ? p.label : 'Cloud',
|
|
144
|
+
hint: p.id ? ['Remote control', p.status, p.capacity].filter(Boolean).join(' · ') : ['Hanzo sandbox', setUp].filter(Boolean).join(' · '),
|
|
117
145
|
disabled: !ready(p),
|
|
118
146
|
place: p,
|
|
119
|
-
})), [places.value]);
|
|
147
|
+
})), [places.value, setUp]);
|
|
120
148
|
const branch = kept.branch || kept.repo?.default_branch || 'main';
|
|
121
|
-
const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : '
|
|
149
|
+
const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id) ?? null, items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
|
|
122
150
|
const row = known.current.get(r.name) ?? chosen(r);
|
|
123
151
|
set({ repo: row, branch: row.default_branch });
|
|
124
152
|
}, load: loadRepos, troubleshoot: {
|
|
125
|
-
label: '
|
|
126
|
-
onPress: () => host.go(path({ kind: 'screen', screen: '
|
|
127
|
-
}, note: "Repositories on the forge. Type to search.", action: {
|
|
153
|
+
label: 'Missing a GitHub repository? Bring it onto the forge',
|
|
154
|
+
onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })),
|
|
155
|
+
}, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label: "Refresh list", onPress: () => setRound((n) => n + 1) }) }), note: "Repositories on the forge. Type to search.", action: {
|
|
128
156
|
label: 'Add to project',
|
|
129
157
|
onPress: (r) => {
|
|
130
158
|
const row = known.current.get(r.name);
|
|
@@ -135,7 +163,12 @@ export function Landing({ onStarted }) {
|
|
|
135
163
|
name: r.name,
|
|
136
164
|
});
|
|
137
165
|
},
|
|
138
|
-
}, placeholder: "Codebase", disabled: !signed }), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
|
|
166
|
+
}, placeholder: "Codebase", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
|
|
139
167
|
const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
|
|
140
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title:
|
|
168
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?', mark: _jsx(HanzoMark, { size: 18 }), column: COLUMN }), _jsx(YStack, { flex: 1 }), _jsxs(YStack, { pb: "$2", gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
|
|
169
|
+
? `A proposed environment for ${codebase} is waiting for review.`
|
|
170
|
+
: `${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(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
|
|
171
|
+
setOffering(false);
|
|
172
|
+
env.reload();
|
|
173
|
+
} })) : null] }));
|
|
141
174
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown an agent writes, read into blocks and inline pieces: paragraphs,
|
|
3
|
+
* headings, lists, quotes, rules, tables and fenced code; inline code, bold,
|
|
4
|
+
* italics and links. What prose.tsx draws, as plain values.
|
|
5
|
+
*
|
|
6
|
+
* A link keeps an address only when it is http(s); anything else, and every
|
|
7
|
+
* image, is its words.
|
|
8
|
+
*/
|
|
9
|
+
export type Block = {
|
|
10
|
+
kind: 'paragraph';
|
|
11
|
+
text: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'heading';
|
|
14
|
+
level: number;
|
|
15
|
+
text: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'code';
|
|
18
|
+
lang: string;
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'list';
|
|
22
|
+
ordered: boolean;
|
|
23
|
+
start: number;
|
|
24
|
+
items: {
|
|
25
|
+
text: string;
|
|
26
|
+
depth: number;
|
|
27
|
+
}[];
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'quote';
|
|
30
|
+
text: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'rule';
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'table';
|
|
35
|
+
head: string[];
|
|
36
|
+
rows: string[][];
|
|
37
|
+
};
|
|
38
|
+
/** The blocks of a markdown text, in order. */
|
|
39
|
+
export declare function blocks(md: string): Block[];
|
|
40
|
+
export type Span = {
|
|
41
|
+
kind: 'text' | 'code' | 'strong' | 'em';
|
|
42
|
+
text: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'link';
|
|
45
|
+
text: string;
|
|
46
|
+
href: string;
|
|
47
|
+
};
|
|
48
|
+
/** A line's inline pieces. A link to anything but http(s) is its words. */
|
|
49
|
+
export declare function spans(text: string): Span[];
|
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const FENCE = /^\s{0,3}(```+|~~~+)\s*([\w+#.-]*)\s*$/;
|
|
2
|
+
const HEADING = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
3
|
+
const ITEM = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
|
|
4
|
+
const RULE = /^\s{0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
5
|
+
const QUOTE = /^\s{0,3}>\s?(.*)$/;
|
|
6
|
+
const ROW = /^\s*\|(.*)\|\s*$/;
|
|
7
|
+
const DIVIDER = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
8
|
+
const cells = (line) => (ROW.exec(line)?.[1] ?? line).split('|').map((c) => c.trim());
|
|
9
|
+
/** The blocks of a markdown text, in order. */
|
|
10
|
+
export function blocks(md) {
|
|
11
|
+
const lines = md.replace(/\r\n?/g, '\n').split('\n');
|
|
12
|
+
const out = [];
|
|
13
|
+
let para = [];
|
|
14
|
+
const flush = () => {
|
|
15
|
+
if (para.length)
|
|
16
|
+
out.push({ kind: 'paragraph', text: para.join(' ').trim() });
|
|
17
|
+
para = [];
|
|
18
|
+
};
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
const fence = FENCE.exec(line);
|
|
22
|
+
if (fence) {
|
|
23
|
+
flush();
|
|
24
|
+
const body = [];
|
|
25
|
+
i++;
|
|
26
|
+
while (i < lines.length && !lines[i].trim().startsWith(fence[1]))
|
|
27
|
+
body.push(lines[i++]);
|
|
28
|
+
out.push({ kind: 'code', lang: fence[2] ?? '', text: body.join('\n') });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!line.trim()) {
|
|
32
|
+
flush();
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const heading = HEADING.exec(line);
|
|
36
|
+
if (heading) {
|
|
37
|
+
flush();
|
|
38
|
+
out.push({ kind: 'heading', level: heading[1].length, text: heading[2] });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (RULE.test(line)) {
|
|
42
|
+
flush();
|
|
43
|
+
out.push({ kind: 'rule' });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ROW.test(line) && DIVIDER.test(lines[i + 1] ?? '')) {
|
|
47
|
+
flush();
|
|
48
|
+
const head = cells(line);
|
|
49
|
+
const rows = [];
|
|
50
|
+
i += 2;
|
|
51
|
+
while (i < lines.length && ROW.test(lines[i]))
|
|
52
|
+
rows.push(cells(lines[i++]));
|
|
53
|
+
i--;
|
|
54
|
+
out.push({ kind: 'table', head, rows });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const quote = QUOTE.exec(line);
|
|
58
|
+
if (quote) {
|
|
59
|
+
flush();
|
|
60
|
+
const body = [quote[1]];
|
|
61
|
+
while (i + 1 < lines.length && QUOTE.test(lines[i + 1]))
|
|
62
|
+
body.push(QUOTE.exec(lines[++i])[1]);
|
|
63
|
+
out.push({ kind: 'quote', text: body.join(' ').trim() });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const item = ITEM.exec(line);
|
|
67
|
+
if (item) {
|
|
68
|
+
flush();
|
|
69
|
+
const ordered = /\d/.test(item[2]);
|
|
70
|
+
const items = [];
|
|
71
|
+
const start = ordered ? parseInt(item[2], 10) : 1;
|
|
72
|
+
let j = i;
|
|
73
|
+
while (j < lines.length) {
|
|
74
|
+
const m = ITEM.exec(lines[j]);
|
|
75
|
+
if (m) {
|
|
76
|
+
items.push({ text: m[3], depth: Math.min(3, Math.floor(m[1].replace(/\t/g, ' ').length / 2)) });
|
|
77
|
+
j++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// A line indented under an item continues it.
|
|
81
|
+
if (lines[j].trim() && /^\s{2,}/.test(lines[j]) && items.length) {
|
|
82
|
+
items[items.length - 1].text += ` ${lines[j].trim()}`;
|
|
83
|
+
j++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
out.push({ kind: 'list', ordered, start, items });
|
|
89
|
+
i = j - 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
para.push(line.trim());
|
|
93
|
+
}
|
|
94
|
+
flush();
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
const INLINE = /(`+)([^`]+?)\1|!?\[([^\]]+)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?\)|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s](?:[^*]*[^*\s])?)\*|(?<![\w])_([^_\s](?:[^_]*[^_\s])?)_(?![\w])|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
|
|
98
|
+
const safe = (href) => (/^https?:\/\/[^\s]+$/i.test(href) ? href : '');
|
|
99
|
+
/** A line's inline pieces. A link to anything but http(s) is its words. */
|
|
100
|
+
export function spans(text) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let at = 0;
|
|
103
|
+
for (const m of text.matchAll(INLINE)) {
|
|
104
|
+
if (m.index > at)
|
|
105
|
+
out.push({ kind: 'text', text: text.slice(at, m.index) });
|
|
106
|
+
if (m[2] !== undefined)
|
|
107
|
+
out.push({ kind: 'code', text: m[2] });
|
|
108
|
+
else if (m[3] !== undefined) {
|
|
109
|
+
const href = safe(m[4]);
|
|
110
|
+
out.push(href && !m[0].startsWith('!') ? { kind: 'link', text: m[3], href } : { kind: 'text', text: m[3] });
|
|
111
|
+
}
|
|
112
|
+
else if (m[5] !== undefined || m[6] !== undefined)
|
|
113
|
+
out.push({ kind: 'strong', text: (m[5] ?? m[6]) });
|
|
114
|
+
else if (m[7] !== undefined || m[8] !== undefined)
|
|
115
|
+
out.push({ kind: 'em', text: (m[7] ?? m[8]) });
|
|
116
|
+
else if (m[9] !== undefined)
|
|
117
|
+
out.push({ kind: 'link', text: m[9], href: m[9] });
|
|
118
|
+
at = m.index + m[0].length;
|
|
119
|
+
}
|
|
120
|
+
if (at < text.length)
|
|
121
|
+
out.push({ kind: 'text', text: text.slice(at) });
|
|
122
|
+
return out;
|
|
123
|
+
}
|
package/lib/plans.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Plans(): import("react").JSX.Element;
|
package/lib/plans.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Plans: every plan this brand sells, priced as the checkout will charge, with
|
|
4
|
+
* the one the organization is on marked, and the move to another.
|
|
5
|
+
*
|
|
6
|
+
* Buying charges a saved card for the plan's first period at the catalog's
|
|
7
|
+
* price; the platform sells one paid plan at a time, so a move between paid
|
|
8
|
+
* plans is the current one ending first. Going back to Free is ending the paid
|
|
9
|
+
* plan at the close of the period already paid for.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { Check, ChevronLeft } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { cancel, chosen, current, label, methods, money, plans, subscribe, subscriptions } from './api/billing.js';
|
|
16
|
+
import { useRead } from './data.js';
|
|
17
|
+
import { useHost, useTarget } from './host.js';
|
|
18
|
+
import { path } from './route.js';
|
|
19
|
+
import { AddCard } from './settings/card.js';
|
|
20
|
+
import { day } from './settings/ui.js';
|
|
21
|
+
/** What a plan costs a month on a term, and what the term charges at once. */
|
|
22
|
+
function price(p, term) {
|
|
23
|
+
const month = term === 'year' && p.yearly ? p.yearly : p.monthly;
|
|
24
|
+
return { month, now: term === 'year' && p.yearly ? p.yearly * 12 : p.monthly };
|
|
25
|
+
}
|
|
26
|
+
export function Plans() {
|
|
27
|
+
const host = useHost();
|
|
28
|
+
const t = useTarget();
|
|
29
|
+
const signed = Boolean(host.person);
|
|
30
|
+
const list = useRead(() => plans(t), [], [t]);
|
|
31
|
+
const subs = useRead(signed ? () => subscriptions(t) : null, [], [t, signed]);
|
|
32
|
+
const cards = useRead(signed ? () => methods(t) : null, [], [t, signed]);
|
|
33
|
+
const [term, setTerm] = useState('month');
|
|
34
|
+
const [buying, setBuying] = useState(null);
|
|
35
|
+
const [leaving, setLeaving] = useState(false);
|
|
36
|
+
const [adding, setAdding] = useState(false);
|
|
37
|
+
const [working, setWorking] = useState(false);
|
|
38
|
+
const [note, setNote] = useState('');
|
|
39
|
+
const on = current(subs.value);
|
|
40
|
+
const card = chosen(cards.value);
|
|
41
|
+
const yearly = list.value.some((p) => p.yearly > 0);
|
|
42
|
+
const free = (p) => p.monthly === 0 && !p.sales;
|
|
43
|
+
const mine = (p) => signed && (on ? p.id === on.plan : free(p));
|
|
44
|
+
// Until the plan and the cards are read, no move is offered: it would be a guess.
|
|
45
|
+
const settled = signed && !(subs.loading && !subs.value.length) && !(cards.loading && !cards.value.length);
|
|
46
|
+
const act = async (what) => {
|
|
47
|
+
setWorking(true);
|
|
48
|
+
setNote('');
|
|
49
|
+
try {
|
|
50
|
+
setNote(await what());
|
|
51
|
+
subs.reload();
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setWorking(false);
|
|
58
|
+
setBuying(null);
|
|
59
|
+
setLeaving(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** The one thing a card can do, or a line saying why it does nothing. */
|
|
63
|
+
const move = (p) => {
|
|
64
|
+
if (mine(p)) {
|
|
65
|
+
return (_jsx(Button, { size: "sm", variant: "outline", disabled: true, children: "Current plan" }));
|
|
66
|
+
}
|
|
67
|
+
if (p.sales)
|
|
68
|
+
return _jsx(Line, { children: "Priced with our team for your organization." });
|
|
69
|
+
if (!signed)
|
|
70
|
+
return _jsx(Line, { children: "Sign in to choose a plan." });
|
|
71
|
+
if (!host.admin)
|
|
72
|
+
return _jsx(Line, { children: "An org admin changes the plan." });
|
|
73
|
+
if (!settled)
|
|
74
|
+
return null;
|
|
75
|
+
if (free(p)) {
|
|
76
|
+
if (!on)
|
|
77
|
+
return null;
|
|
78
|
+
if (on.ending)
|
|
79
|
+
return _jsx(Line, { children: `You move to ${p.name} when ${on.name} ends on ${day(on.ends)}.` });
|
|
80
|
+
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: `Switch to ${p.name}` }));
|
|
81
|
+
}
|
|
82
|
+
if (on && on.price > 0)
|
|
83
|
+
return _jsx(Line, { children: `One paid plan at a time: ${on.name} ends before ${p.name} begins.` });
|
|
84
|
+
if (!card) {
|
|
85
|
+
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: "Add a card to upgrade" }));
|
|
86
|
+
}
|
|
87
|
+
return (_jsx(Button, { size: "sm", variant: p.popular ? 'primary' : 'outline', onPress: () => setBuying(p), children: `Upgrade to ${p.name}` }));
|
|
88
|
+
};
|
|
89
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 1100, mx: "auto", px: "$5", py: "$5", gap: "$5", children: [_jsxs(XStack, { render: "button", "aria-label": "Back to billing", items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Billing" })] }), _jsxs(XStack, { items: "flex-end", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 240, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", children: "Plans that grow with you" }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
|
|
90
|
+
? 'Sign in to see the plan you are on.'
|
|
91
|
+
: on
|
|
92
|
+
? `${host.org ?? 'This organization'} is on ${on.name}.`
|
|
93
|
+
: `${host.org ?? 'This organization'} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
|
|
94
|
+
const cost = price(p, term);
|
|
95
|
+
return (_jsxs(YStack, { "aria-label": `${p.name} plan`, flex: 1, minW: 240, maxW: 360, "$max-sm": { maxW: '100%' }, gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: mine(p) || p.popular ? '$ink' : '$borderColor', bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$5", color: "$ink", children: p.name }), mine(p) ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Current" })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Popular" })) : null] }), p.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: p.description })) : null, _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$7", color: "$ink", children: p.sales ? 'Custom' : cost.month === 0 ? 'Free' : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
|
|
96
|
+
? 'Talk to us'
|
|
97
|
+
: cost.month === 0
|
|
98
|
+
? 'Forever'
|
|
99
|
+
: `${p.perSeat ? 'per seat, ' : ''}a month${term === 'year' && p.yearly ? `, ${money(cost.now, p.currency)} billed yearly` : ''}` })] }), _jsx(XStack, { children: move(p) }), p.features.length ? (_jsx(YStack, { gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: p.features.map((f) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(YStack, { pt: 2, children: _jsx(Check, { size: 14 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$soft", children: f })] }, f))) })) : null] }, p.id));
|
|
100
|
+
}) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => (o ? undefined : setBuying(null)), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
|
|
101
|
+
await subscribe(t, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
|
|
102
|
+
return `${host.org ?? 'This organization'} is on ${buying.name}`;
|
|
103
|
+
}), children: "Upgrade" })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Switch to the free plan?" }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${on.name} stays on until ${day(on.ends) || 'the end of this period'}, and nothing is charged after that.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: `Keep ${on.name}` }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
|
|
104
|
+
await cancel(t, on.id);
|
|
105
|
+
return `${on.name} ends on ${day(on.ends)}`;
|
|
106
|
+
}), children: "Switch" })] })] })) : null] }) }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
|
|
107
|
+
cards.reload();
|
|
108
|
+
setNote(`${label(m)} is saved`);
|
|
109
|
+
} })] }));
|
|
110
|
+
}
|
|
111
|
+
function Line({ children }) {
|
|
112
|
+
return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
|
|
113
|
+
}
|
package/lib/prefs.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person's own settings, read once for the whole builder and shared by
|
|
3
|
+
* everything that follows them: Settings writes them, New starts from the
|
|
4
|
+
* coding defaults and says the name, dictation listens in the chosen language,
|
|
5
|
+
* and the page takes the theme, the text size and the motion.
|
|
6
|
+
*
|
|
7
|
+
* The last document read is kept in this browser, per org, so the page is
|
|
8
|
+
* drawn the person's way before the platform answers. A save shows at once and
|
|
9
|
+
* is taken back, key by key, if the platform refuses it.
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactNode } from 'react';
|
|
12
|
+
import { type Patch, type Prefs } from './api/pref.ts';
|
|
13
|
+
export interface PrefState {
|
|
14
|
+
prefs: Prefs;
|
|
15
|
+
/** The first read is still in flight. */
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
/** Merge a change in and keep it. Throws the platform's refusal. */
|
|
19
|
+
save: (patch: Patch) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function PrefsProvider({ children }: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
export declare function usePrefs(): PrefState;
|
|
25
|
+
/** The type scale each text size is drawn at. Medium is the design system's own. */
|
|
26
|
+
export declare const SCALE: {
|
|
27
|
+
readonly small: 0.9;
|
|
28
|
+
readonly medium: 1;
|
|
29
|
+
readonly large: 1.15;
|
|
30
|
+
};
|
package/lib/prefs.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The person's own settings, read once for the whole builder and shared by
|
|
4
|
+
* everything that follows them: Settings writes them, New starts from the
|
|
5
|
+
* coding defaults and says the name, dictation listens in the chosen language,
|
|
6
|
+
* and the page takes the theme, the text size and the motion.
|
|
7
|
+
*
|
|
8
|
+
* The last document read is kept in this browser, per org, so the page is
|
|
9
|
+
* drawn the person's way before the platform answers. A save shows at once and
|
|
10
|
+
* is taken back, key by key, if the platform refuses it.
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
import { merge, read, save } from './api/pref.js';
|
|
14
|
+
import { useKept, useRead } from './data.js';
|
|
15
|
+
import { useHost, useTarget } from './host.js';
|
|
16
|
+
const Context = createContext(null);
|
|
17
|
+
export function PrefsProvider({ children }) {
|
|
18
|
+
const host = useHost();
|
|
19
|
+
const t = useTarget();
|
|
20
|
+
const signed = Boolean(host.person);
|
|
21
|
+
const [kept, keep] = useKept(`hanzo.build.pref.${host.org ?? 'none'}`, {});
|
|
22
|
+
const now = useRef(kept);
|
|
23
|
+
const doc = useRead(signed ? () => read(t) : null, null, [t, signed]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!doc.value)
|
|
26
|
+
return;
|
|
27
|
+
now.current = doc.value;
|
|
28
|
+
keep(doc.value);
|
|
29
|
+
}, [doc.value, keep]);
|
|
30
|
+
const write = useCallback(async (patch) => {
|
|
31
|
+
if (!signed)
|
|
32
|
+
throw new Error('Sign in to save your settings.');
|
|
33
|
+
const before = now.current;
|
|
34
|
+
now.current = merge(before, patch);
|
|
35
|
+
keep(now.current);
|
|
36
|
+
try {
|
|
37
|
+
now.current = await save(t, patch);
|
|
38
|
+
keep(now.current);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// Only the keys this change named go back; another change since stands.
|
|
42
|
+
const undo = {};
|
|
43
|
+
for (const k of Object.keys(patch))
|
|
44
|
+
undo[k] = before[k] ?? null;
|
|
45
|
+
now.current = merge(now.current, undo);
|
|
46
|
+
keep(now.current);
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}, [signed, t, keep]);
|
|
50
|
+
const value = useMemo(() => ({ prefs: kept, loading: doc.loading, error: doc.error, save: write }), [kept, doc.loading, doc.error, write]);
|
|
51
|
+
return (_jsxs(Context.Provider, { value: value, children: [_jsx(Look, { prefs: kept }), children] }));
|
|
52
|
+
}
|
|
53
|
+
export function usePrefs() {
|
|
54
|
+
const p = useContext(Context);
|
|
55
|
+
if (!p)
|
|
56
|
+
throw new Error('The builder was rendered without its settings');
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
/** The type scale each text size is drawn at. Medium is the design system's own. */
|
|
60
|
+
export const SCALE = { small: 0.9, medium: 1, large: 1.15 };
|
|
61
|
+
/** Every animation and transition finished at once, the way the system setting does it. */
|
|
62
|
+
const STILL = '*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}';
|
|
63
|
+
/**
|
|
64
|
+
* The saved theme handed to the host, and the text size and motion put on the
|
|
65
|
+
* page. A stylesheet rather than a property on <html>: the design system's own
|
|
66
|
+
* root re-applies its appearance on mount, and a rule marked important outlasts
|
|
67
|
+
* that, while the builder is on the page and no longer.
|
|
68
|
+
*/
|
|
69
|
+
function Look({ prefs }) {
|
|
70
|
+
const host = useHost();
|
|
71
|
+
const { theme, text, motion } = prefs;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (theme && theme !== host.theme)
|
|
74
|
+
host.chooseTheme?.(theme);
|
|
75
|
+
// The host's theme is followed, not watched: a choice made elsewhere stands until this one changes.
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [theme]);
|
|
78
|
+
const rules = [
|
|
79
|
+
text && text !== 'medium' ? `html:root{--type-scale:${SCALE[text]}!important}` : '',
|
|
80
|
+
motion === 'reduced' ? STILL : '',
|
|
81
|
+
].join('');
|
|
82
|
+
return rules ? _jsx("style", { "data-hanzo-build": "look", children: rules }) : null;
|
|
83
|
+
}
|
package/lib/prose.d.ts
ADDED
package/lib/prose.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Markdown an agent writes, drawn as text (markdown.ts reads it).
|
|
4
|
+
*
|
|
5
|
+
* @hanzo/ui leaves the markdown pipeline to the surface, and the builder needs
|
|
6
|
+
* only the part agents use. Nothing here produces HTML from the text: every
|
|
7
|
+
* piece is a React element holding a string, and a link is drawn only for an
|
|
8
|
+
* http(s) address, opened in a new tab.
|
|
9
|
+
*/
|
|
10
|
+
import { ScrollView, SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
+
import { Code } from '@hanzo/ui/chat';
|
|
12
|
+
import { blocks, spans } from './markdown.js';
|
|
13
|
+
import { Out } from './out.js';
|
|
14
|
+
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
15
|
+
function Inline({ text }) {
|
|
16
|
+
return (_jsx(_Fragment, { children: spans(text).map((s, i) => {
|
|
17
|
+
switch (s.kind) {
|
|
18
|
+
case 'code':
|
|
19
|
+
return (_jsx(SizableText, { size: "$2", color: "$ink", bg: "$hover", px: "$1", rounded: "$1", style: mono, children: s.text }, i));
|
|
20
|
+
case 'strong':
|
|
21
|
+
return (_jsx(SizableText, { size: "$3", color: "$ink", fontWeight: "600", children: s.text }, i));
|
|
22
|
+
case 'em':
|
|
23
|
+
return (_jsx(SizableText, { size: "$3", color: "$ink", fontStyle: "italic", children: s.text }, i));
|
|
24
|
+
case 'link':
|
|
25
|
+
return (_jsx(Out, { href: s.href, children: _jsx(SizableText, { size: "$3", color: "$ink", textDecorationLine: "underline", children: s.text }) }, i));
|
|
26
|
+
default:
|
|
27
|
+
return s.text;
|
|
28
|
+
}
|
|
29
|
+
}) }));
|
|
30
|
+
}
|
|
31
|
+
const SIZES = ['$6', '$5', '$4', '$4', '$3', '$3'];
|
|
32
|
+
function Draw({ block }) {
|
|
33
|
+
switch (block.kind) {
|
|
34
|
+
case 'heading':
|
|
35
|
+
return (_jsx(SizableText, { size: SIZES[block.level - 1] ?? '$3', color: "$ink", fontWeight: "600", pt: "$1", children: _jsx(Inline, { text: block.text }) }));
|
|
36
|
+
case 'code':
|
|
37
|
+
return (_jsx(Code, { language: block.lang || 'text', value: block.text, children: block.text }));
|
|
38
|
+
case 'rule':
|
|
39
|
+
return _jsx(YStack, { height: 1, bg: "$borderColor", my: "$1" });
|
|
40
|
+
case 'quote':
|
|
41
|
+
return (_jsx(YStack, { borderLeftWidth: 2, borderColor: "$borderColor", pl: "$3", children: _jsx(SizableText, { size: "$3", color: "$soft", children: _jsx(Inline, { text: block.text }) }) }));
|
|
42
|
+
case 'list':
|
|
43
|
+
return (_jsx(YStack, { gap: "$1", role: "list", children: block.items.map((item, i) => (_jsxs(XStack, { gap: "$2", pl: item.depth * 16, role: "listitem", children: [_jsx(SizableText, { size: "$3", color: "$soft", minW: 16, style: { textAlign: 'right' }, children: block.ordered ? `${block.start + i}.` : '•' }), _jsx(SizableText, { flex: 1, minW: 0, size: "$3", color: "$ink", children: _jsx(Inline, { text: item.text }) })] }, i))) }));
|
|
44
|
+
case 'table':
|
|
45
|
+
return (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, children: _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", minW: "100%", children: [block.head, ...block.rows].map((row, r) => (_jsx(XStack, { borderTopWidth: r ? 1 : 0, borderColor: "$borderColor", bg: r ? 'transparent' : '$hover', children: block.head.map((_, c) => (_jsx(SizableText, { flex: 1, minW: 96, px: "$2", py: "$1.5", size: "$2", color: "$ink", fontWeight: r ? '400' : '600', children: _jsx(Inline, { text: row[c] ?? '' }) }, c))) }, r))) }) }));
|
|
46
|
+
default:
|
|
47
|
+
return (_jsx(SizableText, { size: "$3", color: "$ink", children: _jsx(Inline, { text: block.text }) }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** A markdown text, drawn. */
|
|
51
|
+
export function Prose({ text }) {
|
|
52
|
+
return (_jsx(YStack, { gap: "$2.5", minW: 0, children: blocks(text).map((b, i) => (_jsx(Draw, { block: b }, i))) }));
|
|
53
|
+
}
|