@hanzo/build 0.2.80 → 0.2.82
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 +13 -0
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +1 -20
- package/lib/api/agents.js +16 -8
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/links.d.ts +1 -1
- package/lib/api/links.js +1 -1
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +11 -10
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.js +78 -31
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +43 -10
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/merge.d.ts +2 -2
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +7 -4
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +20 -17
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/shelf.js
CHANGED
|
@@ -3,12 +3,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* Two shelves: the sites this org has deployed (Sites, drawn at the foot of the
|
|
4
4
|
* Library — library.tsx), and the public starters (Templates).
|
|
5
5
|
*
|
|
6
|
-
* A project is a real row — `/v1/
|
|
6
|
+
* A project is a real row — `/v1/project`, the same one a deployed site is
|
|
7
7
|
* served from — so opening one opens its workspace at its own address. A
|
|
8
|
-
* starter is taken as a COPY of its own repository (`POST /v1/
|
|
8
|
+
* starter is taken as a COPY of its own repository (`POST /v1/project/fork`),
|
|
9
9
|
* so what opens is that template, not a model's imitation of it.
|
|
10
10
|
*/
|
|
11
11
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
12
13
|
import { Boxes, ExternalLink, Globe, Lock, MoreHorizontal } from '@hanzogui/lucide-icons-2';
|
|
13
14
|
import { Button, DropdownMenu } from '@hanzo/ui';
|
|
14
15
|
import { useState } from 'react';
|
|
@@ -53,21 +54,21 @@ function useFork() {
|
|
|
53
54
|
/** When a project was last edited, or made if it never was; Unix seconds. */
|
|
54
55
|
const when = (p) => p.updated || p.created;
|
|
55
56
|
/** The org's projects under the month each was last edited, newest first. */
|
|
56
|
-
function months(list) {
|
|
57
|
+
function months(list, say) {
|
|
57
58
|
const out = new Map();
|
|
58
59
|
for (const p of [...list].sort((a, b) => when(b) - when(a))) {
|
|
59
60
|
const s = when(p);
|
|
60
|
-
const label = s ? new Date(s * 1000).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) : 'Earlier';
|
|
61
|
+
const label = s ? new Date(s * 1000).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) : say('Earlier');
|
|
61
62
|
out.set(label, [...(out.get(label) ?? []), p]);
|
|
62
63
|
}
|
|
63
64
|
return [...out].map(([label, rows]) => ({ label, rows }));
|
|
64
65
|
}
|
|
65
|
-
function edited(s) {
|
|
66
|
+
function edited(s, say) {
|
|
66
67
|
if (!s)
|
|
67
68
|
return '';
|
|
68
69
|
const d = new Date(s * 1000);
|
|
69
70
|
const year = d.getFullYear() === new Date().getFullYear() ? undefined : 'numeric';
|
|
70
|
-
return
|
|
71
|
+
return say('Edited {date}', { date: d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year }) });
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* What this org has deployed: a few starters to make something new from, then
|
|
@@ -77,6 +78,7 @@ function edited(s) {
|
|
|
77
78
|
* is why it is a section of its own under the Library's items, which are yours.
|
|
78
79
|
*/
|
|
79
80
|
export function Sites() {
|
|
81
|
+
const { t: say } = useI18n();
|
|
80
82
|
const t = useTarget();
|
|
81
83
|
const host = useHost();
|
|
82
84
|
const list = useProjects(t, Boolean(host.person));
|
|
@@ -90,7 +92,7 @@ export function Sites() {
|
|
|
90
92
|
setNote('');
|
|
91
93
|
try {
|
|
92
94
|
await change(t, p.slug, { visibility });
|
|
93
|
-
setNote(
|
|
95
|
+
setNote(visibility === 'public' ? say('{name} is public now', { name: p.name }) : say('{name} is private now', { name: p.name }));
|
|
94
96
|
list.reload();
|
|
95
97
|
}
|
|
96
98
|
catch (e) {
|
|
@@ -98,20 +100,20 @@ export function Sites() {
|
|
|
98
100
|
}
|
|
99
101
|
};
|
|
100
102
|
const said = failed || note;
|
|
101
|
-
return (_jsxs(YStack, { width: "100%", role: "region", "aria-label":
|
|
103
|
+
return (_jsxs(YStack, { width: "100%", role: "region", "aria-label": say('Sites'), children: [_jsxs(YStack, { width: "100%", children: [_jsxs(YStack, { gap: "$1", pb: "$4", children: [_jsx(SizableText, { render: "h2", size: "$5", color: "$ink", children: say('Sites') }), _jsx(SizableText, { size: "$2", color: "$soft", children: `${say('What this organization has built.')} ${say('Open one to keep building it.')}` })] }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: say('Make something new') }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: say('Start from {template}', { template: x.title }), onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsx(YStack, { px: "$3", py: "$2.5", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: busy === x.slug ? say('Copying…') : x.title }) })] }, x.slug))) })] })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: say(list.error.message) })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: say('Sign in to see what your organization has built.') })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: say('Reading your projects…') })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${say('Nothing built yet.')} ${say('Describe something on the New page and it lands here.')}` }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: say('New run') })] })) : (_jsx(YStack, { gap: "$5", children: months(list.value, say).map((g) => (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: g.label }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: g.rows.map((p, i) => (_jsxs(XStack, { items: "center", pr: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { flex: 1, minW: 0, render: "button", "aria-label": say('Open {name}', { name: p.name }), onPress: () => host.go(p.slug), items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [p.visibility === 'private' ? _jsx(Lock, { size: 16, opacity: 0.7 }) : _jsx(Globe, { size: 16, opacity: 0.7 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [p.visibility === 'private' ? say('Private') : say('Public'), edited(when(p), say), p.live ? new URL(p.live).host : say('Not deployed yet')]
|
|
102
104
|
.filter(Boolean)
|
|
103
|
-
.join(' · ') })] })] }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label":
|
|
104
|
-
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(p) },
|
|
105
|
-
{ key: 'visibility', label: p.visibility === 'private' ? 'Make public' : 'Make private', onSelect: () => void flip(p) },
|
|
105
|
+
.join(' · ') })] })] }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": say('Actions for {name}', { name: p.name }), px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$raised' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
106
|
+
{ key: 'rename', label: say('Rename'), onSelect: () => setRenaming(p) },
|
|
107
|
+
{ key: 'visibility', label: p.visibility === 'private' ? say('Make public') : say('Make private'), onSelect: () => void flip(p) },
|
|
106
108
|
{ type: 'separator' },
|
|
107
|
-
{ key: 'delete', label: 'Delete', destructive: true, onSelect: () => setDeleting(p) },
|
|
108
|
-
] })] }, p.slug))) })] }, g.label))) }))] }), renaming ? (_jsx(Rename, { open: true, onOpenChange: (o) => !o && setRenaming(null), title:
|
|
109
|
+
{ key: 'delete', label: say('Delete'), destructive: true, onSelect: () => setDeleting(p) },
|
|
110
|
+
] })] }, p.slug))) })] }, g.label))) }))] }), renaming ? (_jsx(Rename, { open: true, onOpenChange: (o) => !o && setRenaming(null), title: say('Rename'), name: renaming.name, run: async (name) => {
|
|
109
111
|
await change(t, renaming.slug, { name });
|
|
110
|
-
setNote(
|
|
112
|
+
setNote(say('Renamed to {name}', { name }));
|
|
111
113
|
list.reload();
|
|
112
|
-
} })) : null, deleting ? (_jsx(Confirm, { open: true, onOpenChange: (o) => !o && setDeleting(null), title:
|
|
114
|
+
} })) : null, deleting ? (_jsx(Confirm, { open: true, onOpenChange: (o) => !o && setDeleting(null), title: say('Delete {name}?', { name: deleting.name }), says: `${say('Its site stops answering, its releases are dropped, and its address is released.')} ${say('This cannot be undone.')}`, act: say('Delete'), run: async () => {
|
|
113
115
|
await remove(t, deleting.slug);
|
|
114
|
-
setNote(
|
|
116
|
+
setNote(say('{name} is deleted', { name: deleting.name }));
|
|
115
117
|
list.reload();
|
|
116
118
|
} })) : null] }));
|
|
117
119
|
}
|
|
@@ -125,8 +127,9 @@ function Shot({ slug }) {
|
|
|
125
127
|
return (_jsxs(YStack, { width: "100%", aspectRatio: 16 / 10, bg: "$hover", items: "center", justify: "center", position: "relative", overflow: "hidden", children: [_jsxs(YStack, { items: "center", gap: "$2", opacity: 0.6, children: [_jsx(Boxes, { size: 24 }), _jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: slug })] }), gone ? null : (_jsx("img", { src: `${SHOTS}/${encodeURIComponent(slug)}.webp`, alt: "", loading: "lazy", onError: () => setGone(true), style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top' } }))] }));
|
|
126
128
|
}
|
|
127
129
|
export function Templates() {
|
|
130
|
+
const { t: say } = useI18n();
|
|
128
131
|
const t = useTarget();
|
|
129
132
|
const list = useRead(() => templates(t), [], [t.api]);
|
|
130
133
|
const { busy, failed, take } = useFork();
|
|
131
|
-
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title:
|
|
134
|
+
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: say('Templates'), says: `${say('Start from a working app.')} ${say('Picking one copies its repository into a project of yours.')}` }), failed ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: failed })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: say(list.error.message) })) : list.loading && !list.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: say('Reading templates…') })) : !list.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: `${say('No templates here right now.')} ${say('The whole catalog has more.')}` })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((x) => (_jsxs(Card, { label: say('Start from {template}', { template: x.title }), onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsxs(YStack, { p: "$3", gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: busy === x.slug ? say('Copying…') : x.title }), x.framework ? (_jsx(SizableText, { size: "$1", color: "$soft", children: x.framework })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })] })] }, x.slug))) })), _jsx(YStack, { pt: "$5", children: _jsxs(Out, { href: "https://gallery.hanzo.ai", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: say('The whole catalog') }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })] }) }));
|
|
132
135
|
}
|
package/lib/side.d.ts
CHANGED
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The side panel's column beside a conversation or a run, once for Chat and
|
|
3
|
-
* Dev: how wide it is, the middle column it can take the room from, and full
|
|
4
|
-
* screen. The host draws the two columns and spreads what `useSplit` hands it
|
|
5
|
-
* on them and on the kit's `Grip`; the `Panel` inside reads `useSide` for its
|
|
6
|
-
* full-screen button and its way back to the middle column.
|
|
7
|
-
*
|
|
8
|
-
* width the person's, kept in their preferences (`side.width`, /v1/pref),
|
|
9
|
-
* so it follows them to every device and to both modes. Where no
|
|
10
|
-
* PrefsProvider holds them (hanzo.team's rooms) it lasts the page.
|
|
11
|
-
* Saved once a drag lets go, after a burst of keys has stopped.
|
|
12
|
-
* main the middle column keeps MAIN px. Pulled under half of that it is
|
|
13
|
-
* hidden, never unmounted, so the transcript and the composer keep
|
|
14
|
-
* their state, and the panel takes the room. Pulled back out, or the
|
|
15
|
-
* strip's Show button, brings it back at the width kept.
|
|
16
|
-
* full the panel over the whole window. Escape or the button returns.
|
|
17
|
-
*
|
|
18
|
-
* A DRAG NEVER DRAWS REACT. Each frame writes the panel's width and the middle
|
|
19
|
-
* column's display on the two elements (`paint`), the same properties React
|
|
20
|
-
* writes from the settled state (`look`), so the two never disagree; React
|
|
21
|
-
* learns the width once, when the drag lets go. The room the two share is
|
|
22
|
-
* measured by a ResizeObserver, after layout, so a frame never reads layout
|
|
23
|
-
* it has just dirtied. A window narrowed under the panel is painted the same
|
|
24
|
-
* way: the panel gives way, and the person's width comes back with the room.
|
|
25
|
-
*/
|
|
26
1
|
import { type CSSProperties } from 'react';
|
|
27
2
|
/** What the panel's strip reads of the column it stands in. */
|
|
28
3
|
export interface Side {
|
|
@@ -50,7 +25,7 @@ export interface Split {
|
|
|
50
25
|
asideStyle: CSSProperties;
|
|
51
26
|
/** The kit `Grip`'s props, but its label and its place; none while the panel is full screen. */
|
|
52
27
|
grip: {
|
|
53
|
-
side: 'right';
|
|
28
|
+
side: 'left' | 'right';
|
|
54
29
|
span: number;
|
|
55
30
|
floor: number;
|
|
56
31
|
ceil: number;
|
package/lib/side.js
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
* measured by a ResizeObserver, after layout, so a frame never reads layout
|
|
23
23
|
* it has just dirtied. A window narrowed under the panel is painted the same
|
|
24
24
|
* way: the panel gives way, and the person's width comes back with the room.
|
|
25
|
+
*
|
|
26
|
+
* The panel stands at the line's end and the gutter is its inline-start margin,
|
|
27
|
+
* so a right-to-left page puts it on the left and its edge on its right
|
|
28
|
+
* (@hanzo/i18n `dir`), and the grip grows it rightwards.
|
|
25
29
|
*/
|
|
30
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
26
31
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
27
32
|
import { usePrefs } from './prefs.js';
|
|
28
33
|
/** The panel's least width, px, and the width it opens at before the person has chosen one. */
|
|
@@ -40,8 +45,8 @@ export const useSide = () => useContext(Context);
|
|
|
40
45
|
/** The panel's own properties, at a width or with the room. Every key every time, so React and a drag write one set. */
|
|
41
46
|
function look(width, hidden) {
|
|
42
47
|
return hidden
|
|
43
|
-
? { width: 'auto', flexGrow: '1', flexShrink: '1', minWidth: '0px', maxWidth: 'none',
|
|
44
|
-
: { width: `${width}px`, flexGrow: '', flexShrink: '0', minWidth: `${FLOOR}px`, maxWidth: `calc(100vw - ${MAIN}px)`,
|
|
48
|
+
? { width: 'auto', flexGrow: '1', flexShrink: '1', minWidth: '0px', maxWidth: 'none', marginInlineStart: '0px', gridColumnStart: 'pane' }
|
|
49
|
+
: { width: `${width}px`, flexGrow: '', flexShrink: '0', minWidth: `${FLOOR}px`, maxWidth: `calc(100vw - ${MAIN}px)`, marginInlineStart: '', gridColumnStart: '' };
|
|
45
50
|
}
|
|
46
51
|
/** Over the whole window, opaque, above the frame and under the kit's menus. */
|
|
47
52
|
const FULL = {
|
|
@@ -51,7 +56,7 @@ const FULL = {
|
|
|
51
56
|
width: 'auto',
|
|
52
57
|
maxWidth: 'none',
|
|
53
58
|
minWidth: '0px',
|
|
54
|
-
|
|
59
|
+
marginInlineStart: '0px',
|
|
55
60
|
borderRadius: '0px',
|
|
56
61
|
backgroundColor: 'var(--background)',
|
|
57
62
|
backdropFilter: 'none',
|
|
@@ -66,6 +71,7 @@ const write = (el, style) => {
|
|
|
66
71
|
* the panel away (its edge pulled past its floor).
|
|
67
72
|
*/
|
|
68
73
|
export function useSplit(open, main, onShut) {
|
|
74
|
+
const { dir } = useI18n();
|
|
69
75
|
const prefs = usePrefs(false);
|
|
70
76
|
const kept = prefs?.prefs.side?.width;
|
|
71
77
|
const [width, setWidth] = useState(() => Math.max(FLOOR, kept ?? WIDTH));
|
|
@@ -107,7 +113,7 @@ export function useSplit(open, main, onShut) {
|
|
|
107
113
|
return;
|
|
108
114
|
const shown = main.style.display !== 'none';
|
|
109
115
|
if (shown)
|
|
110
|
-
now.current.gap = parseFloat(getComputedStyle(aside).
|
|
116
|
+
now.current.gap = parseFloat(getComputedStyle(aside).marginInlineStart) || 0;
|
|
111
117
|
const total = Math.round((shown ? main.getBoundingClientRect().width + now.current.gap : 0) + aside.getBoundingClientRect().width);
|
|
112
118
|
// A drag moves width between the two and leaves their sum; only a new window is news.
|
|
113
119
|
if (Math.abs(total - now.current.room) < 2)
|
|
@@ -190,6 +196,6 @@ export function useSplit(open, main, onShut) {
|
|
|
190
196
|
asideStyle: (side.full ? { ...look(across, side.hidden), ...FULL } : look(across, side.hidden)),
|
|
191
197
|
grip: side.full
|
|
192
198
|
? null
|
|
193
|
-
: { side: 'right', span: side.hidden ? room : across, floor: FLOOR, ceil: Math.max(FLOOR, room), reset: WIDTH, onSpan: (n) => void paint(n), onKeep: keep, onShut },
|
|
199
|
+
: { side: dir === 'rtl' ? 'left' : 'right', span: side.hidden ? room : across, floor: FLOOR, ceil: Math.max(FLOOR, room), reset: WIDTH, onSpan: (n) => void paint(n), onKeep: keep, onShut },
|
|
194
200
|
};
|
|
195
201
|
}
|
package/lib/standing.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Where an account stands: which plan it is on, which meter it reads, and the
|
|
3
|
-
* reads behind it — the tier for the plan's name, the limits for its windows,
|
|
4
|
-
* the free allowance, and the prepaid balance. One read for every surface that
|
|
5
|
-
* shows a plan (the account menus, Usage, Billing), so they cannot disagree.
|
|
6
|
-
*
|
|
7
|
-
* Each read stands alone: one that fails leaves its part empty and the rest
|
|
8
|
-
* drawn. Only when none answers is the whole read an error.
|
|
9
|
-
*/
|
|
1
|
+
import type { Say } from '@hanzo/i18n/react';
|
|
10
2
|
import { type Allowance } from './api/billing.ts';
|
|
11
3
|
import type { Target } from './api/call.ts';
|
|
12
4
|
import { type Limits } from './api/limits.ts';
|
|
@@ -39,6 +31,6 @@ export interface Row {
|
|
|
39
31
|
* the billing period of whichever class of model has used the most of it,
|
|
40
32
|
* because that is the one that stops a request first.
|
|
41
33
|
*/
|
|
42
|
-
export declare function rows(l: Limits | null, now?: number): Row[];
|
|
34
|
+
export declare function rows(l: Limits | null, now?: number, t?: Say): Row[];
|
|
43
35
|
/** The share of the free allowance left in its window, 0 to 100, or null where none bounds the account. */
|
|
44
36
|
export declare function left(a: Allowance | null): number | null;
|
package/lib/standing.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* Each read stands alone: one that fails leaves its part empty and the rest
|
|
8
8
|
* drawn. Only when none answers is the whole read an error.
|
|
9
9
|
*/
|
|
10
|
+
import { phrase } from '@hanzo/i18n';
|
|
10
11
|
import { allowance, balance, tier } from './api/billing.js';
|
|
11
12
|
import { limits } from './api/limits.js';
|
|
12
13
|
import { useRead } from './data.js';
|
|
13
14
|
import { kind, label, when } from './plan.js';
|
|
15
|
+
import { english } from './say.js';
|
|
14
16
|
const got = (r) => (r.status === 'fulfilled' ? r.value : null);
|
|
15
17
|
/** Every read at once. Rejects only when all of them were refused, with the first refusal. */
|
|
16
18
|
export async function stand(t) {
|
|
@@ -24,7 +26,7 @@ export async function stand(t) {
|
|
|
24
26
|
const k = kind(slug, money, billed?.tier);
|
|
25
27
|
return {
|
|
26
28
|
kind: k,
|
|
27
|
-
label: k === 'plan' ? label(slug, billed?.tier) : k === 'free' ? { name: 'Free', tag: '' } : { name: 'Pay as you go', tag: '' },
|
|
29
|
+
label: k === 'plan' ? label(slug, billed?.tier) : k === 'free' ? { name: phrase('Free'), tag: '' } : { name: phrase('Pay as you go'), tag: '' },
|
|
28
30
|
limits: used,
|
|
29
31
|
allowance: free,
|
|
30
32
|
balance: money,
|
|
@@ -40,7 +42,7 @@ const RANK = { ok: 0, near: 1, limited: 2 };
|
|
|
40
42
|
* the billing period of whichever class of model has used the most of it,
|
|
41
43
|
* because that is the one that stops a request first.
|
|
42
44
|
*/
|
|
43
|
-
export function rows(l, now = Date.now()) {
|
|
45
|
+
export function rows(l, now = Date.now(), t = english) {
|
|
44
46
|
if (!l)
|
|
45
47
|
return [];
|
|
46
48
|
const month = Object.values(l.classes).reduce((a, c) => (!c ? a : !a || c.percent > a.percent || (c.percent === a.percent && RANK[c.state] > RANK[a.state]) ? c : a), null);
|
|
@@ -49,7 +51,7 @@ export function rows(l, now = Date.now()) {
|
|
|
49
51
|
if (!w)
|
|
50
52
|
return;
|
|
51
53
|
const at = when(w.resets, now);
|
|
52
|
-
out.push({ name, percent: w.percent, state: w.state, resets: at ?
|
|
54
|
+
out.push({ name, percent: w.percent, state: w.state, resets: at ? t('Resets {time}', { time: at }) : '' });
|
|
53
55
|
};
|
|
54
56
|
add('Session', l.session);
|
|
55
57
|
add('Today', l.day);
|
package/lib/suggest.d.ts
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Three suggestions under the composer, each one person's own and each a thing
|
|
3
|
-
* done at once when pressed (`Act`): open what it names (a conversation, a run,
|
|
4
|
-
* a Studio creation, an item, an issue), send a new chat, start a Dev run, or
|
|
5
|
-
* schedule a task. None only fills the composer.
|
|
6
|
-
*
|
|
7
|
-
* Nothing here is written by hand. The rows are generated by Hanzo's own model
|
|
8
|
-
* (`MODEL`, POST /v1/chat/completions with the person's bearer, so the call is
|
|
9
|
-
* theirs like any other) from that person's activity alone: their recent
|
|
10
|
-
* conversations, their Dev runs, the org's issues and pull requests, their
|
|
11
|
-
* automations and their files. An account with no activity yet is given rows
|
|
12
|
-
* from what signup learned (its role, its use, what it said about its work),
|
|
13
|
-
* until there is activity to read. No input and no answer is no row: the model
|
|
14
|
-
* may answer none, and nothing pads it.
|
|
15
|
-
*
|
|
16
|
-
* Kept in the account (`suggest` in /v1/pref), so the rows follow the person:
|
|
17
|
-
*
|
|
18
|
-
* {at, sig, rows, dismissed} when they were made, the fingerprint of what
|
|
19
|
-
* they were made from, the rows, and the rows
|
|
20
|
-
* the person dismissed (by id)
|
|
21
|
-
*
|
|
22
|
-
* They are made again a day later, or once the activity's fingerprint has moved
|
|
23
|
-
* and the rows are at least `QUIET` old, so a busy hour asks the model once.
|
|
24
|
-
*
|
|
25
|
-
* This file is the pure half: inputs to a request, an answer to validated rows,
|
|
26
|
-
* the fingerprint and the policy. suggestions.tsx reads the sources and draws them.
|
|
27
|
-
*/
|
|
28
1
|
import type { Automation, Schedule } from './api/auto.ts';
|
|
29
2
|
import type { Conversation } from './api/conversations.ts';
|
|
30
3
|
import type { Item } from './api/library.ts';
|
|
@@ -231,8 +204,15 @@ export declare function answer(raw: unknown, inputs: Input[]): Suggestion[];
|
|
|
231
204
|
export declare function kept(raw: unknown): Kept | undefined;
|
|
232
205
|
/** The rows to draw: those not dismissed. */
|
|
233
206
|
export declare const shown: (k: Kept | null | undefined) => Suggestion[];
|
|
234
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* What Hanzo does, three ways in, each done at once: what fills the rows a
|
|
209
|
+
* person's activity and signup do not. The one copy here that is authored, so
|
|
210
|
+
* its words are marked (`phrase`) and drawn in the reader's language, the
|
|
211
|
+
* prompt too: it is sent as their message and shown as theirs.
|
|
212
|
+
*/
|
|
235
213
|
export declare const STARTERS: readonly Suggestion[];
|
|
214
|
+
/** Whether a row is a starter: authored, so drawn through `t`; every other row is the person's own words. */
|
|
215
|
+
export declare const starter: (r: Suggestion) => boolean;
|
|
236
216
|
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
237
217
|
export declare const fill: (k: Kept | null | undefined) => Suggestion[];
|
|
238
218
|
/** `k` with `id` dismissed: remembered first, the oldest forgotten past `REMEMBER`. */
|
package/lib/suggest.js
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three suggestions under the composer, each one person's own and each a thing
|
|
3
|
+
* done at once when pressed (`Act`): open what it names (a conversation, a run,
|
|
4
|
+
* a Studio creation, an item, an issue), send a new chat, start a Dev run, or
|
|
5
|
+
* schedule a task. None only fills the composer.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here is written by hand. The rows are generated by Hanzo's own model
|
|
8
|
+
* (`MODEL`, POST /v1/chat/completions with the person's bearer, so the call is
|
|
9
|
+
* theirs like any other) from that person's activity alone: their recent
|
|
10
|
+
* conversations, their Dev runs, the org's issues and pull requests, their
|
|
11
|
+
* automations and their files. An account with no activity yet is given rows
|
|
12
|
+
* from what signup learned (its role, its use, what it said about its work),
|
|
13
|
+
* until there is activity to read. No input and no answer is no row: the model
|
|
14
|
+
* may answer none, and nothing pads it.
|
|
15
|
+
*
|
|
16
|
+
* Kept in the account (`suggest` in /v1/pref), so the rows follow the person:
|
|
17
|
+
*
|
|
18
|
+
* {at, sig, rows, dismissed} when they were made, the fingerprint of what
|
|
19
|
+
* they were made from, the rows, and the rows
|
|
20
|
+
* the person dismissed (by id)
|
|
21
|
+
*
|
|
22
|
+
* They are made again a day later, or once the activity's fingerprint has moved
|
|
23
|
+
* and the rows are at least `QUIET` old, so a busy hour asks the model once.
|
|
24
|
+
*
|
|
25
|
+
* This file is the pure half: inputs to a request, an answer to validated rows,
|
|
26
|
+
* the fingerprint and the policy. suggestions.tsx reads the sources and draws them.
|
|
27
|
+
*/
|
|
28
|
+
import { phrase } from '@hanzo/i18n';
|
|
1
29
|
export const ICONS = ['github', 'agent', 'chat', 'file', 'task'];
|
|
2
30
|
/** Hanzo's model the rows are asked of: it decodes under a strict JSON schema at api.hanzo.ai. */
|
|
3
31
|
export const MODEL = 'zen5-flash';
|
|
@@ -363,12 +391,19 @@ export function kept(raw) {
|
|
|
363
391
|
}
|
|
364
392
|
/** The rows to draw: those not dismissed. */
|
|
365
393
|
export const shown = (k) => (k ? k.rows.filter((r) => !k.dismissed.includes(r.id)).slice(0, MOST) : []);
|
|
366
|
-
/**
|
|
394
|
+
/**
|
|
395
|
+
* What Hanzo does, three ways in, each done at once: what fills the rows a
|
|
396
|
+
* person's activity and signup do not. The one copy here that is authored, so
|
|
397
|
+
* its words are marked (`phrase`) and drawn in the reader's language, the
|
|
398
|
+
* prompt too: it is sent as their message and shown as theirs.
|
|
399
|
+
*/
|
|
367
400
|
export const STARTERS = [
|
|
368
|
-
{ id: 'start:chat', icon: 'chat', title: 'Ask what Hanzo can do for you', summary: '', act: { kind: 'send', prompt: 'What can you do for me? Give me three concrete things to try today.' } },
|
|
369
|
-
{ id: 'start:dev', icon: 'agent', title: 'Build a landing page with Dev', summary: '', act: { kind: 'task', prompt: 'Build a one-page landing site for my project and give me the preview link.' } },
|
|
370
|
-
{ id: 'start:research', icon: 'file', title: 'Get a cited brief on AI agents', summary: '', act: { kind: 'send', prompt: 'Research the state of AI coding agents this month and give me a short brief with sources.' } },
|
|
401
|
+
{ id: 'start:chat', icon: 'chat', title: phrase('Ask what Hanzo can do for you'), summary: '', act: { kind: 'send', prompt: phrase('What can you do for me? Give me three concrete things to try today.') } },
|
|
402
|
+
{ id: 'start:dev', icon: 'agent', title: phrase('Build a landing page with Dev'), summary: '', act: { kind: 'task', prompt: phrase('Build a one-page landing site for my project and give me the preview link.') } },
|
|
403
|
+
{ id: 'start:research', icon: 'file', title: phrase('Get a cited brief on AI agents'), summary: '', act: { kind: 'send', prompt: phrase('Research the state of AI coding agents this month and give me a short brief with sources.') } },
|
|
371
404
|
];
|
|
405
|
+
/** Whether a row is a starter: authored, so drawn through `t`; every other row is the person's own words. */
|
|
406
|
+
export const starter = (r) => STARTERS.some((s) => s.id === r.id);
|
|
372
407
|
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
373
408
|
export const fill = (k) => {
|
|
374
409
|
if (!k)
|
package/lib/suggestions.js
CHANGED
|
@@ -6,6 +6,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* a new chat, starts its run, or makes its scheduled task. Nothing only fills
|
|
7
7
|
* the composer. Its × dismisses it, and the account remembers.
|
|
8
8
|
*
|
|
9
|
+
* A row's words are the person's own, made from their activity, and drawn as
|
|
10
|
+
* given; a starter's are authored (suggest.ts `STARTERS`), so drawn and sent in
|
|
11
|
+
* the reader's language (@hanzo/i18n `t`).
|
|
12
|
+
*
|
|
9
13
|
* `useSuggestions` reads the kept rows from the account (`suggest` in /v1/pref)
|
|
10
14
|
* and, when they are due (suggest.ts `due`), reads the person's activity and
|
|
11
15
|
* asks Hanzo's model for new ones. One document per page, read once per person
|
|
@@ -24,6 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
28
|
*/
|
|
25
29
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
26
30
|
import { Bot, CalendarClock, FileText, Github, MessageSquare, X } from '@hanzogui/lucide-icons-2';
|
|
31
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
27
32
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@hanzo/ui';
|
|
28
33
|
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
29
34
|
import { automations, create } from './api/auto.js';
|
|
@@ -34,7 +39,7 @@ import { read, save } from './api/pref.js';
|
|
|
34
39
|
import { list } from './api/sessions.js';
|
|
35
40
|
import { everything } from './api/work.js';
|
|
36
41
|
import { claims } from './claims.js';
|
|
37
|
-
import { answer, dismissed, due, fill, inputs, request, seed, sig, skip, stale, TAKE } from './suggest.js';
|
|
42
|
+
import { answer, dismissed, due, fill, inputs, request, seed, sig, skip, stale, starter, TAKE } from './suggest.js';
|
|
38
43
|
/** A source that refuses this person (a 4xx other than an expired sign-in or a rate) has nothing for them; any other failure leaves it unknown. */
|
|
39
44
|
function soft(p, none) {
|
|
40
45
|
return p.catch((e) => {
|
|
@@ -187,16 +192,20 @@ const MARKS = {
|
|
|
187
192
|
const PITCH = 44;
|
|
188
193
|
/** The rows, or nothing when there are none. */
|
|
189
194
|
export function Suggestions({ rows, onAct, onDismiss }) {
|
|
195
|
+
const { t } = useI18n();
|
|
190
196
|
// A phone has no hover: its × stays in sight.
|
|
191
197
|
const touch = !useMedia().sm;
|
|
192
198
|
if (!rows.length)
|
|
193
199
|
return null;
|
|
194
|
-
return (_jsx(YStack, { role: "list", "aria-label":
|
|
200
|
+
return (_jsx(YStack, { role: "list", "aria-label": t('Suggestions'), "data-slot": "suggestions", children: rows.map((r) => (_jsx(Row, { row: r, touch: touch, onAct: onAct, onDismiss: onDismiss }, r.id))) }));
|
|
195
201
|
}
|
|
196
202
|
/** One row: the whole of it one button that does the act, and its × beside it. Two stops for the keyboard, neither inside the other. */
|
|
197
|
-
function Row({ row, touch, onAct, onDismiss }) {
|
|
203
|
+
function Row({ row: kept, touch, onAct, onDismiss }) {
|
|
204
|
+
const { t } = useI18n();
|
|
198
205
|
const [near, setNear] = useState(false);
|
|
206
|
+
// A starter in the reader's words, its prompt too: what is sent is what they pressed.
|
|
207
|
+
const row = starter(kept) ? { ...kept, title: t(kept.title), act: 'prompt' in kept.act ? { ...kept.act, prompt: t(kept.act.prompt) } : kept.act } : kept;
|
|
199
208
|
const Mark = MARKS[row.icon];
|
|
200
|
-
const button = (_jsxs(XStack, { render: "button", "data-act": row.act.kind, "aria-label": row.title, onPress: () => onAct(row.act, row), flex: 1, minW: 0, self: "stretch", items: "center", gap: "$3",
|
|
201
|
-
return (_jsxs(XStack, { role: "listitem", "data-slot": "suggestion", items: "center", gap: "$1", minH: PITCH,
|
|
209
|
+
const button = (_jsxs(XStack, { render: "button", "data-act": row.act.kind, "aria-label": row.title, onPress: () => onAct(row.act, row), flex: 1, minW: 0, self: "stretch", items: "center", gap: "$3", paddingInlineStart: "$3", rounded: "$4", cursor: "pointer", focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(Mark, { size: 18, color: "$soft" }), _jsx(SizableText, { fontSize: 15, color: "$soft", numberOfLines: 1, shrink: 1, minW: 0, children: row.title })] }));
|
|
210
|
+
return (_jsxs(XStack, { role: "listitem", "data-slot": "suggestion", items: "center", gap: "$1", minH: PITCH, paddingInlineEnd: "$2", rounded: "$4", hoverStyle: { bg: '$hover' }, onPointerEnter: () => setNear(true), onPointerLeave: () => setNear(false), children: [row.summary ? (_jsxs(HoverCard, { openDelay: 300, closeDelay: 150, children: [_jsx(HoverCardTrigger, { flex: 1, minW: 0, self: "stretch", flexDirection: "row", children: button }), _jsx(HoverCardContent, { side: "top", align: "start", sideOffset: 8, width: 470, maxW: "calc(100vw - 32px)", rounded: "$6", p: "$4", children: _jsx(SizableText, { fontSize: 16, lineHeight: 26, color: "$ink", children: row.summary }) })] })) : (button), _jsx(XStack, { render: "button", "aria-label": t('Dismiss {suggestion}', { suggestion: row.title }), onPress: () => onDismiss(row.id), p: "$1.5", rounded: "$2", cursor: "pointer", opacity: touch || near ? 1 : 0, hoverStyle: { bg: '$hover' }, focusVisibleStyle: { opacity: 1, outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(X, { size: 16, color: "$soft" }) })] }));
|
|
202
211
|
}
|
package/lib/summary.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface Section {
|
|
|
39
39
|
* The summary, from the outline mark in a header. `onOpen` runs after a row is
|
|
40
40
|
* pressed, for the host to show the panel the row opened a tab in.
|
|
41
41
|
*/
|
|
42
|
-
export declare function Summary({ sections, onOpen, label }: {
|
|
42
|
+
export declare function Summary({ sections, onOpen, label: named }: {
|
|
43
43
|
sections: readonly Section[];
|
|
44
44
|
onOpen?: () => void;
|
|
45
45
|
label?: string;
|
package/lib/summary.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
*/
|
|
13
13
|
import { useMemo, useState } from 'react';
|
|
14
14
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
15
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
15
16
|
import { FilePlus2, ListTodo } from '@hanzogui/lucide-icons-2';
|
|
16
17
|
import { Popover, PopoverContent, PopoverTrigger } from '@hanzo/ui';
|
|
17
18
|
/** A row's pitch and inset, px: the kit menu's, so this panel reads as every other one. */
|
|
@@ -40,7 +41,9 @@ function Rows({ section, done, details = false }) {
|
|
|
40
41
|
* The summary, from the outline mark in a header. `onOpen` runs after a row is
|
|
41
42
|
* pressed, for the host to show the panel the row opened a tab in.
|
|
42
43
|
*/
|
|
43
|
-
export function Summary({ sections, onOpen, label
|
|
44
|
+
export function Summary({ sections, onOpen, label: named }) {
|
|
45
|
+
const { t } = useI18n();
|
|
46
|
+
const label = named ?? t('Summary');
|
|
44
47
|
const [open, setOpen] = useState(false);
|
|
45
48
|
const done = () => {
|
|
46
49
|
setOpen(false);
|