@hanzo/build 0.2.80 → 0.2.81
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/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 +2 -1
- 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/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 +6 -3
- 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 +18 -15
- 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/panel.js
CHANGED
|
@@ -23,6 +23,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
* panel is the whole window instead (`sheet`).
|
|
24
24
|
*/
|
|
25
25
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
26
|
+
import { phrase } from '@hanzo/i18n';
|
|
27
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
26
28
|
import { ArrowLeft, ArrowRight, ChevronRight, Columns2, ExternalLink, Globe, Maximize2, Minimize2, Minus, MoreHorizontal, PanelLeftOpen, Pin, Plus, RotateCcw, RotateCw, SquarePlus, X } from '@hanzogui/lucide-icons-2';
|
|
27
29
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@hanzo/ui';
|
|
28
30
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -36,7 +38,9 @@ const KEYS = MAC ? '⌘⇧B' : 'Ctrl+Shift+B';
|
|
|
36
38
|
/** Every key that shows and hides the tabs (tabs.ts `useKey`). */
|
|
37
39
|
const SHORTCUTS = 'Control+Shift+B Meta+Shift+B Control+Period Meta+Period';
|
|
38
40
|
/** A tab's title where it has none of its own. */
|
|
39
|
-
const BLANK = 'New tab';
|
|
41
|
+
const BLANK = phrase('New tab');
|
|
42
|
+
/** On an icon that points along the line (Back, Forward): it turns in a right-to-left page. */
|
|
43
|
+
const MIRROR = { 'data-i18n-mirror': '' };
|
|
40
44
|
/** The row heights, px: the strip (the conversation header's row), and a page's bar. */
|
|
41
45
|
const STRIP = 56;
|
|
42
46
|
const BAR = 48;
|
|
@@ -57,12 +61,14 @@ const quiet = (on = false, disabled = false) => ({
|
|
|
57
61
|
/**
|
|
58
62
|
* The header's control for the tabs, while they are hidden: an icon among the
|
|
59
63
|
* header's icons. Open, the strip's own toggle stands at the panel's end and
|
|
60
|
-
* this one is not drawn, so the page carries one toggle at a time.
|
|
64
|
+
* this one is not drawn, so the page carries one toggle at a time. Its words
|
|
65
|
+
* are the reader's language; the key chord is an argument, as this platform writes it.
|
|
61
66
|
*/
|
|
62
67
|
export function PanelToggle({ open, onToggle }) {
|
|
68
|
+
const { t } = useI18n();
|
|
63
69
|
if (open)
|
|
64
70
|
return null;
|
|
65
|
-
return (_jsx(XStack, { render: "button", role: "button", "data-slot": "side-toggle", "aria-label":
|
|
71
|
+
return (_jsx(XStack, { render: "button", role: "button", "data-slot": "side-toggle", "aria-label": t('Show tabs'), "aria-expanded": false, "aria-keyshortcuts": SHORTCUTS, ...{ title: t('Show tabs ({keys})', { keys: KEYS }) }, onPress: onToggle, width: 32, height: 32, ...quiet(), children: _jsx(SquarePlus, { size: 16 }) }));
|
|
66
72
|
}
|
|
67
73
|
/** Our own hosts: a page of theirs answers a frame on any of our pages. */
|
|
68
74
|
const OURS = ['hanzo.ai', 'hanzo.app', 'hanzo.bot'];
|
|
@@ -123,11 +129,13 @@ function Mark({ icon, title }) {
|
|
|
123
129
|
}
|
|
124
130
|
/** An empty page: the places the surface suggests, as cards in a row that scrolls sideways. */
|
|
125
131
|
function Suggested({ suggested, onGo }) {
|
|
126
|
-
|
|
132
|
+
const { t } = useI18n();
|
|
133
|
+
return (_jsxs(YStack, { "data-slot": "side-suggested", px: "$4", pt: "$4", gap: "$3", children: [_jsx(SizableText, { fontSize: 13, color: "$soft", children: t('Suggested') }), suggested.length ? (_jsx(XStack, { role: "list", "aria-label": t('Suggested'), gap: "$2", overflowX: "auto", overflowY: "hidden", pb: "$2", children: suggested.map((s) => (_jsxs(YStack, { role: "listitem", render: "button", "aria-label": s.title, onPress: () => onGo(s.url), width: 120, shrink: 0, items: "center", gap: "$2", py: "$2.5", px: "$1.5", rounded: 12, cursor: "pointer", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(Mark, { icon: s.icon, title: s.title }), _jsx(SizableText, { fontSize: 13, color: "$ink", numberOfLines: 1, maxW: "100%", children: s.title })] }, s.url))) })) : (_jsx(SizableText, { fontSize: 13, color: "$soft", children: t('No suggestions.') }))] }));
|
|
127
134
|
}
|
|
128
135
|
/** A remote address a browser page may not frame: what it is, and the one way to open it. */
|
|
129
136
|
function Elsewhere({ tab }) {
|
|
130
|
-
|
|
137
|
+
const { t } = useI18n();
|
|
138
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", p: "$5", "data-slot": "side-elsewhere", children: [_jsx(Globe, { size: 20, opacity: 0.6 }), _jsx(SizableText, { fontSize: 13, color: "$ink", numberOfLines: 1, maxW: "100%", children: tab.title || tab.url }), _jsxs(XStack, { render: "button", role: "button", onPress: () => popOut(tab), items: "center", gap: "$1.5", px: "$3", height: 32, rounded: 999, borderWidth: 1, borderColor: "$borderColor", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: [_jsx(ExternalLink, { size: 13 }), _jsx(SizableText, { fontSize: 13, color: "$ink", children: t('Open in a new window') })] })] }));
|
|
131
139
|
}
|
|
132
140
|
/** A row of the page's menu, px: a browser's menu is denser than a form's. */
|
|
133
141
|
const ROW = 29;
|
|
@@ -145,17 +153,19 @@ function Row({ label, onSelect, disabled, end }) {
|
|
|
145
153
|
}
|
|
146
154
|
/** The zoom row: the level between − and +, and the way back to 100. Plain buttons, so a press keeps the menu open. */
|
|
147
155
|
function Zoom({ level, onLevel, disabled }) {
|
|
156
|
+
const { t } = useI18n();
|
|
148
157
|
const step = (way) => () => onLevel(zoomed(level, way));
|
|
149
158
|
const box = (label, onPress, child) => (_jsx(XStack, { render: "button", role: "button", "aria-label": label, "aria-disabled": disabled || undefined, onPress: disabled ? undefined : onPress, width: 24, height: 24, ...quiet(false, disabled), rounded: 6, children: child }));
|
|
150
|
-
return (_jsxs(XStack, { "data-slot": "page-zoom", role: "group", "aria-label":
|
|
159
|
+
return (_jsxs(XStack, { "data-slot": "page-zoom", role: "group", "aria-label": t('Zoom'), items: "center", gap: "$2", px: 8, height: 38, opacity: disabled ? 0.5 : 1, children: [_jsx(SizableText, { fontSize: 13, color: "$ink", flex: 1, children: t('Zoom') }), _jsxs(XStack, { items: "center", gap: 2, borderWidth: 1, borderColor: "$borderColor", rounded: 8, px: 2, height: 28, children: [box(t('Zoom out'), step(-1), _jsx(Minus, { size: 12 })), _jsx(SizableText, { fontSize: 12, color: "$ink", width: 40, text: "center", "aria-live": "polite", children: `${level}%` }), box(t('Zoom in'), step(1), _jsx(Plus, { size: 12 }))] }), box(t('Reset zoom'), () => onLevel(100), _jsx(RotateCcw, { size: 13 }))] }));
|
|
151
160
|
}
|
|
152
161
|
/**
|
|
153
162
|
* Skills, a submenu. Its panel is mounted only while open: a closed one stays
|
|
154
163
|
* laid out over the menu and takes its presses.
|
|
155
164
|
*/
|
|
156
165
|
function Skills({ skills, here }) {
|
|
166
|
+
const { t } = useI18n();
|
|
157
167
|
const [open, setOpen] = useState(false);
|
|
158
|
-
return (_jsxs(DropdownMenuSub, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenuSubTrigger, { disabled: !here, minH: ROW, py: 0, children: _jsx(SizableText, { fontSize: 13, color: "$ink", flex: 1, children:
|
|
168
|
+
return (_jsxs(DropdownMenuSub, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenuSubTrigger, { disabled: !here, minH: ROW, py: 0, children: _jsx(SizableText, { fontSize: 13, color: "$ink", flex: 1, children: t('Skills') }) }), open ? (_jsx(DropdownMenuSubContent, { minW: 200, children: skills.map((k) => (_jsx(Row, { label: k.name, onSelect: () => here && k.run(here) }, k.id))) })) : null] }));
|
|
159
169
|
}
|
|
160
170
|
/**
|
|
161
171
|
* The page's menu, in a browser's order: what we add first (ask about it, open
|
|
@@ -167,6 +177,7 @@ function Skills({ skills, here }) {
|
|
|
167
177
|
* an empty tab.
|
|
168
178
|
*/
|
|
169
179
|
function Actions({ tab, here, frame, framed, same, level, onLevel, acts, browser, onPin, }) {
|
|
180
|
+
const { t } = useI18n();
|
|
170
181
|
const shown = here ? { id: tab.id, ...here } : null;
|
|
171
182
|
const none = !shown;
|
|
172
183
|
const pick = () => {
|
|
@@ -195,29 +206,29 @@ function Actions({ tab, here, frame, framed, same, level, onLevel, acts, browser
|
|
|
195
206
|
const print = browser?.print ?? (same ? () => frame.current?.contentWindow?.print() : undefined);
|
|
196
207
|
const zoom = Boolean(browser?.zoom) || framed;
|
|
197
208
|
const ours = [
|
|
198
|
-
acts.onAsk ? _jsx(Row, { label:
|
|
199
|
-
acts.onDev && shown && hosted(shown.url) ? _jsx(Row, { label:
|
|
209
|
+
acts.onAsk ? _jsx(Row, { label: t('Ask about this page'), disabled: none, onSelect: () => void ask() }, "ask") : null,
|
|
210
|
+
acts.onDev && shown && hosted(shown.url) ? _jsx(Row, { label: t('Open in Dev'), onSelect: () => acts.onDev?.({ url: shown.url, title: shown.title }) }, "dev") : null,
|
|
200
211
|
acts.skills?.length ? _jsx(Skills, { skills: acts.skills, here: here }, "skills") : null,
|
|
201
|
-
_jsx(Row, { label:
|
|
202
|
-
_jsx(Row, { label:
|
|
203
|
-
onPin ? _jsx(Row, { label: tab.pinned ? 'Unpin tab' : 'Pin tab', onSelect: () => onPin(!tab.pinned) }, "pin") : null,
|
|
212
|
+
_jsx(Row, { label: t('Open in a new window'), disabled: none, onSelect: () => popOut(tab) }, "out"),
|
|
213
|
+
_jsx(Row, { label: t('Copy link'), disabled: none || !tab.url, onSelect: () => tab.url && void navigator.clipboard?.writeText(tab.url) }, "copy"),
|
|
214
|
+
onPin ? _jsx(Row, { label: tab.pinned ? t('Unpin tab') : t('Pin tab'), onSelect: () => onPin(!tab.pinned) }, "pin") : null,
|
|
204
215
|
].filter(Boolean);
|
|
205
216
|
const page = [
|
|
206
|
-
browser?.find ? _jsx(Row, { label:
|
|
207
|
-
print ? _jsx(Row, { label:
|
|
217
|
+
browser?.find ? _jsx(Row, { label: t('Find in page'), disabled: none, onSelect: () => shown && browser.find?.(shown) }, "find") : null,
|
|
218
|
+
print ? _jsx(Row, { label: t('Print'), disabled: none, onSelect: () => shown && print(shown) }, "print") : null,
|
|
208
219
|
zoom ? _jsx(Zoom, { level: level, onLevel: onLevel, disabled: none }, "zoom") : null,
|
|
209
|
-
browser?.devices ? _jsx(Row, { label:
|
|
210
|
-
browser?.capture && acts.onCapture ? _jsx(Row, { label:
|
|
220
|
+
browser?.devices ? _jsx(Row, { label: t('Show device toolbar'), disabled: none, onSelect: () => shown && browser.devices?.(shown) }, "devices") : null,
|
|
221
|
+
browser?.capture && acts.onCapture ? _jsx(Row, { label: t('Take a screenshot'), disabled: none, onSelect: () => void capture() }, "capture") : null,
|
|
211
222
|
].filter(Boolean);
|
|
212
223
|
const own = [
|
|
213
|
-
browser?.passwords ? _jsx(Row, { label:
|
|
214
|
-
browser?.downloads ? _jsx(Row, { label:
|
|
215
|
-
browser?.history ? _jsx(Row, { label:
|
|
216
|
-
browser?.clear ? _jsx(Row, { label:
|
|
224
|
+
browser?.passwords ? _jsx(Row, { label: t('Passwords and autofill'), onSelect: browser.passwords, end: _jsx(ChevronRight, { size: 14, opacity: 0.6 }) }, "passwords") : null,
|
|
225
|
+
browser?.downloads ? _jsx(Row, { label: t('Downloads'), onSelect: browser.downloads }, "downloads") : null,
|
|
226
|
+
browser?.history ? _jsx(Row, { label: t('History'), onSelect: browser.history }, "history") : null,
|
|
227
|
+
browser?.clear ? _jsx(Row, { label: t('Clear browsing data'), onSelect: browser.clear }, "clear") : null,
|
|
217
228
|
].filter(Boolean);
|
|
218
|
-
const settings = browser?.settings ? [_jsx(Row, { label:
|
|
229
|
+
const settings = browser?.settings ? [_jsx(Row, { label: t('Browser settings'), onSelect: browser.settings }, "settings")] : [];
|
|
219
230
|
const groups = [ours, page, own, settings].filter((g) => g.length);
|
|
220
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { render: "button", role: "button", "aria-label":
|
|
231
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { render: "button", role: "button", "aria-label": t('Page actions'), width: PILL, height: PILL, ...quiet(), rounded: 999, bg: "$edge", children: _jsx(MoreHorizontal, { size: 16 }) }) }), _jsx(DropdownMenuContent, { align: "end", minW: 240, "data-slot": "page-menu", children: groups.flatMap((g, i) => (i ? [_jsx(DropdownMenuSeparator, {}, `line-${i}`), ...g] : g)) })] }));
|
|
221
232
|
}
|
|
222
233
|
/**
|
|
223
234
|
* A page tab: its bar, then an address, bytes rendered as their type, or the
|
|
@@ -228,6 +239,7 @@ function Actions({ tab, here, frame, framed, same, level, onLevel, acts, browser
|
|
|
228
239
|
* remote page takes its own origin, and withholding same-origin breaks it.
|
|
229
240
|
*/
|
|
230
241
|
export function Page({ tab, onGo, onBack, onForward, onTitle, onPin, suggested = [], ...acts }) {
|
|
242
|
+
const { t } = useI18n();
|
|
231
243
|
const Browse = useBrowse();
|
|
232
244
|
const browser = useBrowser();
|
|
233
245
|
const frame = useRef(null);
|
|
@@ -272,9 +284,9 @@ export function Page({ tab, onGo, onBack, onForward, onTitle, onPin, suggested =
|
|
|
272
284
|
};
|
|
273
285
|
// A frame scaled from its corner, its box grown to fill the panel at the scale.
|
|
274
286
|
const scale = native || level === 100 ? null : { width: `${10000 / level}%`, height: `${10000 / level}%`, transform: `scale(${level / 100})`, transformOrigin: '0 0' };
|
|
275
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, "data-slot": "side-page", children: [_jsxs(XStack, { "data-slot": "side-bar", height: BAR, px: "$2", gap: "$2", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsxs(XStack, { items: "center", height: PILL, px: 4, gap: 2, rounded: 999, bg: "$edge", shrink: 0, children: [_jsx(XStack, { render: "button", role: "button", "aria-label":
|
|
287
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, "data-slot": "side-page", children: [_jsxs(XStack, { "data-slot": "side-bar", height: BAR, px: "$2", gap: "$2", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsxs(XStack, { items: "center", height: PILL, px: 4, gap: 2, rounded: 999, bg: "$edge", shrink: 0, children: [_jsx(XStack, { render: "button", role: "button", "aria-label": t('Back'), "aria-disabled": !back || undefined, onPress: back ? onBack : undefined, width: 28, height: 28, ...quiet(false, !back), rounded: 999, children: _jsx(ArrowLeft, { size: 15, ...MIRROR }) }), _jsx(XStack, { render: "button", role: "button", "aria-label": t('Forward'), "aria-disabled": !ahead || undefined, onPress: ahead ? onForward : undefined, width: 28, height: 28, ...quiet(false, !ahead), rounded: 999, children: _jsx(ArrowRight, { size: 15, ...MIRROR }) }), _jsx(XStack, { render: "button", role: "button", "aria-label": t('Reload this tab'), onPress: () => again((n) => n + 1), width: 28, height: 28, ...quiet(), rounded: 999, children: _jsx(RotateCw, { size: 14 }) })] }), _jsx(XStack, { flex: 1, minW: 0, items: "center", height: PILL, px: "$3.5", rounded: 999, bg: "$edge", borderWidth: 1,
|
|
276
288
|
// Focused, the address is the bar's one ringed thing, as a browser's is.
|
|
277
|
-
borderColor: typing ? '$outlineColor' : 'transparent', "data-slot": "side-address", children: _jsx("input", { value: own ? tab.title : typed, readOnly: own, placeholder:
|
|
289
|
+
borderColor: typing ? '$outlineColor' : 'transparent', "data-slot": "side-address", children: _jsx("input", { value: own ? tab.title : typed, readOnly: own, placeholder: t('Search or enter a URL'), "aria-label": t('Address'), spellCheck: false, autoFocus: !own && !tab.url, onFocus: (e) => {
|
|
278
290
|
setTyping(true);
|
|
279
291
|
e.currentTarget.select();
|
|
280
292
|
}, onBlur: () => setTyping(false), onChange: (e) => setTyped(e.target.value), onKeyDown: (e) => {
|
|
@@ -290,9 +302,9 @@ export function Page({ tab, onGo, onBack, onForward, onTitle, onPin, suggested =
|
|
|
290
302
|
color: 'var(--foreground)',
|
|
291
303
|
font: 'inherit',
|
|
292
304
|
fontSize: 14,
|
|
293
|
-
// The resting bar reads centred, as a browser's new tab does; typed in, it reads from the
|
|
294
|
-
textAlign: typing || typed ? '
|
|
295
|
-
} }, tab.id) }), _jsx(Actions, { tab: tab, here: here, frame: frame, framed: framed, same: same, level: level, onLevel: zoomTo, acts: acts, browser: browser, onPin: onPin })] }), !src ? (_jsx(Suggested, { suggested: suggested, onGo: onGo })) : native && Browse ? (_jsx(YStack, { flex: 1, minH: 0, children: _jsx(Browse, { url: src, title: tab.title, onGo: onGo, onTitle: onTitle }) }, round)) : elsewhere ? (_jsx(Elsewhere, { tab: tab })) : picture && !own ? (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflowY: "auto", overflowX: "hidden", children: _jsx("img", { src: src, alt: tab.title, style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', ...(scale ? { transform: scale.transform } : null) } }, round) })) : shows ? (_jsx(YStack, { flex: 1, minH: 0, overflow: "hidden", position: "relative", children: _jsx("iframe", { ref: frame, src: src, title: tab.title, sandbox: local ? 'allow-scripts allow-popups allow-forms' : 'allow-same-origin allow-scripts allow-popups allow-forms allow-modals', style: { position: 'absolute', top: 0, left: 0, border: 0, width: '100%', height: '100%', display: 'block', background: 'var(--background)', ...scale } }, round) })) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children:
|
|
305
|
+
// The resting bar reads centred, as a browser's new tab does; typed in, it reads from the line's start.
|
|
306
|
+
textAlign: typing || typed ? 'start' : 'center',
|
|
307
|
+
} }, tab.id) }), _jsx(Actions, { tab: tab, here: here, frame: frame, framed: framed, same: same, level: level, onLevel: zoomTo, acts: acts, browser: browser, onPin: onPin })] }), !src ? (_jsx(Suggested, { suggested: suggested, onGo: onGo })) : native && Browse ? (_jsx(YStack, { flex: 1, minH: 0, children: _jsx(Browse, { url: src, title: tab.title, onGo: onGo, onTitle: onTitle }) }, round)) : elsewhere ? (_jsx(Elsewhere, { tab: tab })) : picture && !own ? (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflowY: "auto", overflowX: "hidden", children: _jsx("img", { src: src, alt: tab.title, style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', ...(scale ? { transform: scale.transform } : null) } }, round) })) : shows ? (_jsx(YStack, { flex: 1, minH: 0, overflow: "hidden", position: "relative", children: _jsx("iframe", { ref: frame, src: src, title: tab.title, sandbox: local ? 'allow-scripts allow-popups allow-forms' : 'allow-same-origin allow-scripts allow-popups allow-forms allow-modals', style: { position: 'absolute', top: 0, left: 0, border: 0, width: '100%', height: '100%', display: 'block', background: 'var(--background)', ...scale } }, round) })) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: t('{type} previews aren’t supported yet.', { type: (ext || type || 'This').toUpperCase() }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: t('Open it in a new window to view it.') })] }))] }));
|
|
296
308
|
}
|
|
297
309
|
const DRAG = 'text/x-hanzo-tab';
|
|
298
310
|
/**
|
|
@@ -301,10 +313,12 @@ const DRAG = 'text/x-hanzo-tab';
|
|
|
301
313
|
* lets it go, and closes it, whatever kind it is.
|
|
302
314
|
*/
|
|
303
315
|
function Leaf({ tab, on, side, index, label, icon }) {
|
|
304
|
-
|
|
316
|
+
const { t } = useI18n();
|
|
317
|
+
return (_jsxs(ContextMenu, { children: [_jsx(ContextMenuTrigger, { asChild: true, children: _jsx(Strip, { tab: tab, on: on, side: side, index: index, label: label, icon: icon }) }), _jsxs(ContextMenuContent, { minW: 180, "aria-label": t('{tab} tab', { tab: label }), children: [_jsx(ContextMenuItem, { onSelect: () => (tab.pinned ? side.unpin(tab.id) : side.pin(tab.id)), children: tab.pinned ? t('Unpin tab') : t('Pin tab') }), _jsx(ContextMenuSeparator, {}), _jsx(ContextMenuItem, { onSelect: () => side.shut(tab.id), children: t('Close tab') })] })] }));
|
|
305
318
|
}
|
|
306
319
|
/** The tab itself, as the strip draws it. */
|
|
307
320
|
function Strip({ tab, on, side, index, label, icon, ...rest }) {
|
|
321
|
+
const { t } = useI18n();
|
|
308
322
|
return (_jsxs(XStack, { ...rest, "data-slot": "side-tab", "data-tab": tab.id, items: "center", gap: "$1", pl: "$2.5", pr: "$1", height: 30,
|
|
309
323
|
// As wide as a title wants up to a browser tab's width, sharing the strip
|
|
310
324
|
// as a browser's tabs do when there are many.
|
|
@@ -353,12 +367,13 @@ function Strip({ tab, on, side, index, label, icon, ...rest }) {
|
|
|
353
367
|
e.preventDefault?.();
|
|
354
368
|
side.shut(tab.id);
|
|
355
369
|
}
|
|
356
|
-
}, flex: 1, minW: 0, items: "center", gap: "$2", cursor: "pointer", children: [icon ? (_jsx(XStack, { "aria-hidden": true, shrink: 0, opacity: on ? 1 : 0.7, children: icon })) : null, _jsx(SizableText, { fontSize: 13, color: on ? '$ink' : '$soft', numberOfLines: 1, children: label })] }), tab.pinned ? (_jsx(XStack, { render: "button", role: "button", "aria-label":
|
|
370
|
+
}, flex: 1, minW: 0, items: "center", gap: "$2", cursor: "pointer", children: [icon ? (_jsx(XStack, { "aria-hidden": true, shrink: 0, opacity: on ? 1 : 0.7, children: icon })) : null, _jsx(SizableText, { fontSize: 13, color: on ? '$ink' : '$soft', numberOfLines: 1, children: label })] }), tab.pinned ? (_jsx(XStack, { render: "button", role: "button", "aria-label": t('Unpin {tab}', { tab: label }), tabIndex: -1, onPress: () => side.unpin(tab.id), width: 22, height: 22, items: "center", justify: "center", rounded: 6, cursor: "pointer", opacity: on ? 0.8 : 0.55, hoverStyle: { bg: '$hover', opacity: 1 }, children: _jsx(Pin, { size: 12 }) })) : (_jsx(XStack, { render: "button", role: "button", "aria-label": t('Close {tab}', { tab: label }), tabIndex: -1, onPress: () => side.shut(tab.id), width: 22, height: 22, items: "center", justify: "center", rounded: 6, cursor: "pointer", opacity: on ? 0.8 : 0.45, hoverStyle: { bg: '$hover', opacity: 1 },
|
|
357
371
|
// The chosen tab's cross: the others keep their room for their titles when the strip is shared.
|
|
358
372
|
display: on ? 'flex' : 'none', children: _jsx(X, { size: 12 }) }))] }));
|
|
359
373
|
}
|
|
360
374
|
/** The panel: the strip, and the chosen tab. */
|
|
361
|
-
export function Panel({ side, kinds, onHide, sheet = false, bar, menu, label
|
|
375
|
+
export function Panel({ side, kinds, onHide, sheet = false, bar, menu, label, suggested, ...acts }) {
|
|
376
|
+
const { t: say } = useI18n();
|
|
362
377
|
const tab = side.tabs.find((t) => t.id === side.at) ?? null;
|
|
363
378
|
// The column it stands in; a phone's sheet is the whole window already.
|
|
364
379
|
const stands = useSide();
|
|
@@ -371,17 +386,18 @@ export function Panel({ side, kinds, onHide, sheet = false, bar, menu, label = '
|
|
|
371
386
|
const kind = tab && tab.kind !== PAGE ? kinds.find((k) => k.id === tab.kind) : undefined;
|
|
372
387
|
const bleed = !tab || tab.kind === PAGE || Boolean(kind?.bleed);
|
|
373
388
|
const closed = kinds.filter((k) => !side.tabs.some((t) => t.kind === k.id));
|
|
374
|
-
|
|
389
|
+
// A page's title is the page's own; an empty one is a new tab, in the reader's language.
|
|
390
|
+
const title = (t) => (t.kind === PAGE ? (t.title && t.title !== BLANK ? t.title : say(BLANK)) : (kinds.find((k) => k.id === t.kind)?.label ?? t.title));
|
|
375
391
|
const mark = (t) => (t.kind === PAGE ? _jsx(Globe, { size: 14 }) : (kinds.find((k) => k.id === t.kind)?.icon ?? null));
|
|
376
392
|
const blank = () => side.open({ kind: PAGE, title: BLANK, url: '' });
|
|
377
393
|
const poppable = tab?.kind === PAGE && (tab.body !== undefined || Boolean(tab.url));
|
|
378
|
-
const plus = (_jsx(XStack, { render: "button", role: "button", "aria-label":
|
|
379
|
-
const body = (_jsxs(YStack, { role: "complementary", "aria-label": label, "data-slot": "side", flex: 1, minW: 0, minH: 0, overflow: "hidden",
|
|
394
|
+
const plus = (_jsx(XStack, { render: "button", role: "button", "aria-label": say('Open a tab'), width: 28, height: 28, ...quiet(), onPress: closed.length ? undefined : blank, children: _jsx(Plus, { size: 16 }) }));
|
|
395
|
+
const body = (_jsxs(YStack, { role: "complementary", "aria-label": label ?? say('Side panel'), "data-slot": "side", flex: 1, minW: 0, minH: 0, overflow: "hidden", rounded: "inherit", children: [_jsxs(XStack, { "data-slot": "side-strip", height: STRIP, px: "$2", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", shrink: 0, children: [column?.hidden && !column.full ? (_jsxs(XStack, { render: "button", role: "button", "data-slot": "side-show", onPress: column.show, height: 32, px: "$2", gap: "$1.5", ...quiet(), children: [_jsx(PanelLeftOpen, { size: 16 }), _jsx(SizableText, { fontSize: 13, color: "$ink", children: column.main === 'run' ? say('Show run') : column.main === 'chat' ? say('Show chat') : say('Show {main}', { main: column.main }) })] })) : null, _jsx(XStack, { role: "tablist", "aria-label": say('Tabs'), shrink: 1, minW: 0, gap: "$1", items: "center", overflowX: "auto", overflowY: "hidden", "$platform-web": { scrollbarWidth: 'none' }, children: side.tabs.map((t, i) => (_jsx(Leaf, { tab: t, on: t.id === side.at, side: side, index: i, label: title(t), icon: mark(t) }, t.id))) }), closed.length ? (_jsx(DropdownMenu, { trigger: plus, items: [
|
|
380
396
|
...closed.map((k) => ({ key: k.id, label: k.label, onSelect: () => side.open({ kind: k.id, title: k.label }) })),
|
|
381
|
-
{ key: PAGE, label: 'Page', description: 'An address, framed here', onSelect: blank },
|
|
382
|
-
] })) : (plus), _jsx(XStack, { flex: 1 }), menu, _jsx(XStack, { render: "button", role: "button", "aria-label":
|
|
397
|
+
{ key: PAGE, label: say('Page'), description: say('An address, framed here'), onSelect: blank },
|
|
398
|
+
] })) : (plus), _jsx(XStack, { flex: 1 }), menu, _jsx(XStack, { render: "button", role: "button", "aria-label": say('Open this tab in a new window'), "aria-disabled": !poppable || undefined, onPress: poppable ? () => popOut(tab) : undefined, width: 28, height: 28, ...quiet(false, !poppable), children: _jsx(ExternalLink, { size: 14 }) }), column ? (_jsx(XStack, { render: "button", role: "button", "data-slot": "side-full", "aria-label": column.full ? say('Exit full screen') : say('Full screen'), "aria-pressed": column.full, "aria-keyshortcuts": column.full ? 'Escape' : undefined, ...{ title: column.full ? say('Exit full screen ({key})', { key: 'Esc' }) : say('Full screen') }, onPress: () => column.fill(!column.full), width: 28, height: 28, ...quiet(column.full), children: column.full ? _jsx(Minimize2, { size: 14 }) : _jsx(Maximize2, { size: 14 }) })) : null, _jsx(XStack, { render: "button", role: "button", "data-slot": "side-toggle", "aria-label": say('Hide tabs'), "aria-expanded": true, "aria-keyshortcuts": SHORTCUTS, ...{ title: say('Hide tabs ({keys})', { keys: KEYS }) }, onPress: onHide, width: 32, height: 32, ...quiet(true), children: _jsx(Columns2, { size: 16 }) })] }), bar, _jsx(YStack, { id: "side-body", role: "tabpanel", "aria-labelledby": tab ? `side-tab-${tab.id}` : undefined,
|
|
383
399
|
// What a tab lays over itself (a door's "Connecting…") stays inside it, under the strip.
|
|
384
|
-
position: "relative", flex: 1, minH: 0, overflowY: bleed ? 'hidden' : 'auto', overflowX: "hidden", px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: !tab ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", p: "$5", children: _jsx(SizableText, { fontSize: 13, color: "$soft", children:
|
|
400
|
+
position: "relative", flex: 1, minH: 0, overflowY: bleed ? 'hidden' : 'auto', overflowX: "hidden", px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: !tab ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", p: "$5", children: _jsx(SizableText, { fontSize: 13, color: "$soft", children: say('No tabs open.') }) })) : tab.kind === PAGE ? (_jsx(Page, { tab: tab, onGo: (url) => side.go(tab.id, url), onBack: () => side.back(tab.id), onForward: () => side.forward(tab.id), onTitle: (t) => side.rename(tab.id, t), onPin: (on) => (on ? side.pin(tab.id) : side.unpin(tab.id)), suggested: suggested, ...acts }, tab.id)) : kind ? (kind.render()) : (_jsx(SizableText, { size: "$2", color: "$soft", children: say('This tab is not offered here.') })) })] }));
|
|
385
401
|
if (!sheet || typeof document === 'undefined')
|
|
386
402
|
return body;
|
|
387
403
|
// A phone: the panel over the whole window, clear of the notch and the home
|
package/lib/plan.d.ts
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* A plan as a person reads it, and how much of it is used.
|
|
3
|
-
*
|
|
4
|
-
* NAMED BY FAMILY. A slug is the catalog's rung (`max-20x`); a person reads the
|
|
5
|
-
* family — Free, Go, Pro, Max, Team, Agency, Enterprise — with the rung inside
|
|
6
|
-
* it as a small tag (`20x`). A slug outside the families reads as words
|
|
7
|
-
* (`growth-plus` is Growth Plus); one that is an id is never printed, and reads
|
|
8
|
-
* as the tier it is served under.
|
|
9
|
-
*
|
|
10
|
-
* MEASURED IN SHARES. `GET /v1/ai/limits` (cloud apps/ai/limits_ops.go) answers
|
|
11
|
-
* every window as a percent used, its state and when it starts over — the
|
|
12
|
-
* session, the UTC day, and each class's billing period — never an amount.
|
|
13
|
-
*
|
|
14
|
-
* THREE KINDS OF ACCOUNT, three meters: on a PLAN its windows are the meter, on
|
|
15
|
-
* FREE the free allowance left today is, and with CREDITS and no plan (pay as
|
|
16
|
-
* you go) the balance is. A plan's meter never shows money.
|
|
17
|
-
*/
|
|
1
|
+
import type { Say } from '@hanzo/i18n/react';
|
|
18
2
|
export interface Label {
|
|
19
3
|
/** The family: `Max`. */
|
|
20
4
|
name: string;
|
|
@@ -63,9 +47,9 @@ export declare function when(iso: string, now?: number): string;
|
|
|
63
47
|
/**
|
|
64
48
|
* What a reader whose plan's included usage is used is told: the plan, which
|
|
65
49
|
* models, and when they come back — `Your Max 20x plan’s included premium model
|
|
66
|
-
* usage is used until 5:00 PM.`
|
|
50
|
+
* usage is used until 5:00 PM.` In the language of `t`.
|
|
67
51
|
*/
|
|
68
|
-
export declare function spent(l: Label | null, classes: readonly string[], resets: string, now?: number): string;
|
|
52
|
+
export declare function spent(l: Label | null, classes: readonly string[], resets: string, now?: number, t?: Say): string;
|
|
69
53
|
/**
|
|
70
54
|
* The ways past a spent allowance, in this order and two at most: keep going
|
|
71
55
|
* on credits — or, with none the server would let pay, add some — then the
|
|
@@ -75,4 +59,4 @@ export declare function spent(l: Label | null, classes: readonly string[], reset
|
|
|
75
59
|
export declare function ways<A extends {
|
|
76
60
|
kind: string;
|
|
77
61
|
label: string;
|
|
78
|
-
}>(actions: readonly A[]): A[];
|
|
62
|
+
}>(actions: readonly A[], t?: Say): A[];
|
package/lib/plan.js
CHANGED
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
* THREE KINDS OF ACCOUNT, three meters: on a PLAN its windows are the meter, on
|
|
15
15
|
* FREE the free allowance left today is, and with CREDITS and no plan (pay as
|
|
16
16
|
* you go) the balance is. A plan's meter never shows money.
|
|
17
|
+
*
|
|
18
|
+
* WORDS in English: a family's name is a name, the rest is marked (`phrase`)
|
|
19
|
+
* or takes the caller's @hanzo/i18n `t`, so a surface says it in the reader's
|
|
20
|
+
* language.
|
|
17
21
|
*/
|
|
22
|
+
import { phrase } from '@hanzo/i18n';
|
|
23
|
+
import { english } from './say.js';
|
|
18
24
|
/** The families, by the slug's first word. `dev` is the slug Pro is sold under. */
|
|
19
25
|
const FAMILY = {
|
|
20
|
-
free: 'Free',
|
|
26
|
+
free: phrase('Free'),
|
|
21
27
|
go: 'Go',
|
|
22
28
|
dev: 'Pro',
|
|
23
29
|
pro: 'Pro',
|
|
@@ -50,14 +56,14 @@ export function label(slug, tier) {
|
|
|
50
56
|
return fallback();
|
|
51
57
|
// A rung that is an id still IS a rung: the account is on a plan, so it reads as one.
|
|
52
58
|
if (ID.test(raw))
|
|
53
|
-
return fallback() ?? { name: 'Paid plan', tag: '' };
|
|
59
|
+
return fallback() ?? { name: phrase('Paid plan'), tag: '' };
|
|
54
60
|
const s = raw.replace(/[\s_]+/g, '-');
|
|
55
61
|
const [head, ...rest] = s.split('-').filter(Boolean);
|
|
56
62
|
const family = FAMILY[head];
|
|
57
63
|
if (!family)
|
|
58
64
|
return { name: s.split('-').filter(Boolean).map(word).join(' '), tag: '' };
|
|
59
65
|
const tail = rest.join('-');
|
|
60
|
-
const tag = /^\d+x$/.test(tail) ? tail : /^(annual|yearly|year)$/.test(tail) ? 'Annual' : tail ? rest.map(word).join(' ') : '';
|
|
66
|
+
const tag = /^\d+x$/.test(tail) ? tail : /^(annual|yearly|year)$/.test(tail) ? phrase('Annual') : tail ? rest.map(word).join(' ') : '';
|
|
61
67
|
return { name: family, tag };
|
|
62
68
|
}
|
|
63
69
|
/** A label as a sentence says it: `Max 20x`. */
|
|
@@ -109,16 +115,26 @@ export function when(iso, now = Date.now()) {
|
|
|
109
115
|
}
|
|
110
116
|
/** A class of model as a sentence names it. */
|
|
111
117
|
const CLASS = { premium: 'premium', ours: 'Hanzo' };
|
|
118
|
+
/** What was used, by the classes that used it, as one phrase each. */
|
|
119
|
+
const USED = {
|
|
120
|
+
'': phrase('usage'),
|
|
121
|
+
premium: phrase('premium model usage'),
|
|
122
|
+
Hanzo: phrase('Hanzo model usage'),
|
|
123
|
+
'premium and Hanzo': phrase('premium and Hanzo model usage'),
|
|
124
|
+
};
|
|
112
125
|
/**
|
|
113
126
|
* What a reader whose plan's included usage is used is told: the plan, which
|
|
114
127
|
* models, and when they come back — `Your Max 20x plan’s included premium model
|
|
115
|
-
* usage is used until 5:00 PM.`
|
|
128
|
+
* usage is used until 5:00 PM.` In the language of `t`.
|
|
116
129
|
*/
|
|
117
|
-
export function spent(l, classes, resets, now = Date.now()) {
|
|
118
|
-
const names = classes.map((c) => CLASS[c]).filter(Boolean);
|
|
119
|
-
const what = names
|
|
130
|
+
export function spent(l, classes, resets, now = Date.now(), t = english) {
|
|
131
|
+
const names = classes.map((c) => CLASS[c]).filter(Boolean).join(' and ');
|
|
132
|
+
const what = USED[names] !== undefined ? t(USED[names]) : `${names} model usage`;
|
|
120
133
|
const at = when(resets, now);
|
|
121
|
-
|
|
134
|
+
if (!l)
|
|
135
|
+
return at ? t('Your plan’s included {what} is used until {time}.', { what, time: at }) : t('Your plan’s included {what} is used.', { what });
|
|
136
|
+
const plan = said({ name: t(l.name), tag: l.tag ? t(l.tag) : '' });
|
|
137
|
+
return at ? t('Your {plan} plan’s included {what} is used until {time}.', { plan, what, time: at }) : t('Your {plan} plan’s included {what} is used.', { plan, what });
|
|
122
138
|
}
|
|
123
139
|
/**
|
|
124
140
|
* The ways past a spent allowance, in this order and two at most: keep going
|
|
@@ -126,8 +142,8 @@ export function spent(l, classes, resets, now = Date.now()) {
|
|
|
126
142
|
* upgrade; and a switch to another model where the server offers one. Taken
|
|
127
143
|
* from what the server sent, so nothing is offered that it would refuse.
|
|
128
144
|
*/
|
|
129
|
-
export function ways(actions) {
|
|
145
|
+
export function ways(actions, t = english) {
|
|
130
146
|
const pick = (kind) => actions.find((a) => a.kind === kind);
|
|
131
147
|
const topup = pick('topup');
|
|
132
|
-
return [pick('credits') ?? (topup ? { ...topup, label: 'Add credits' } : undefined), pick('upgrade'), pick('switch')].filter((a) => a !== undefined);
|
|
148
|
+
return [pick('credits') ?? (topup ? { ...topup, label: t('Add credits') } : undefined), pick('upgrade'), pick('switch')].filter((a) => a !== undefined);
|
|
133
149
|
}
|
package/lib/plans.js
CHANGED
|
@@ -7,8 +7,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* price; the platform sells one paid plan at a time, so a move between paid
|
|
8
8
|
* plans is the current one ending first. Going back to Free is ending the paid
|
|
9
9
|
* plan at the close of the period already paid for.
|
|
10
|
+
*
|
|
11
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a plan's name,
|
|
12
|
+
* description, features and price are the catalog's, drawn as given.
|
|
10
13
|
*/
|
|
11
14
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
15
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
12
16
|
import { Check, ChevronLeft } from '@hanzogui/lucide-icons-2';
|
|
13
17
|
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
14
18
|
import { useState } from 'react';
|
|
@@ -25,11 +29,12 @@ function price(p, term) {
|
|
|
25
29
|
}
|
|
26
30
|
export function Plans() {
|
|
27
31
|
const host = useHost();
|
|
28
|
-
const
|
|
32
|
+
const target = useTarget();
|
|
33
|
+
const { t } = useI18n();
|
|
29
34
|
const signed = Boolean(host.person);
|
|
30
|
-
const list = useRead(() => plans(
|
|
31
|
-
const subs = useRead(signed ? () => subscriptions(
|
|
32
|
-
const cards = useRead(signed ? () => methods(
|
|
35
|
+
const list = useRead(() => plans(target), [], [target]);
|
|
36
|
+
const subs = useRead(signed ? () => subscriptions(target) : null, [], [target, signed]);
|
|
37
|
+
const cards = useRead(signed ? () => methods(target) : null, [], [target, signed]);
|
|
33
38
|
const [term, setTerm] = useState('month');
|
|
34
39
|
const [buying, setBuying] = useState(null);
|
|
35
40
|
const [leaving, setLeaving] = useState(false);
|
|
@@ -37,7 +42,7 @@ export function Plans() {
|
|
|
37
42
|
const [working, setWorking] = useState(false);
|
|
38
43
|
const [note, setNote] = useState('');
|
|
39
44
|
const on = current(subs.value);
|
|
40
|
-
const
|
|
45
|
+
const org = host.org;
|
|
41
46
|
const card = chosen(cards.value);
|
|
42
47
|
const yearly = list.value.some((p) => p.yearly > 0);
|
|
43
48
|
const free = (p) => p.monthly === 0 && !p.sales;
|
|
@@ -63,50 +68,64 @@ export function Plans() {
|
|
|
63
68
|
/** The one thing a card can do, or a line saying why it does nothing. */
|
|
64
69
|
const move = (p) => {
|
|
65
70
|
if (mine(p)) {
|
|
66
|
-
return (_jsx(Button, { size: "sm", variant: "outline", disabled: true, children:
|
|
71
|
+
return (_jsx(Button, { size: "sm", variant: "outline", disabled: true, children: t('Current plan') }));
|
|
67
72
|
}
|
|
68
73
|
if (p.sales)
|
|
69
|
-
return _jsx(Line, { children:
|
|
74
|
+
return _jsx(Line, { children: t('Priced with our team for your organization.') });
|
|
70
75
|
if (!signed)
|
|
71
|
-
return _jsx(Line, { children:
|
|
76
|
+
return _jsx(Line, { children: t('Sign in to choose a plan.') });
|
|
72
77
|
if (!host.admin)
|
|
73
|
-
return _jsx(Line, { children:
|
|
78
|
+
return _jsx(Line, { children: t('An org admin changes the plan.') });
|
|
74
79
|
if (!settled)
|
|
75
80
|
return null;
|
|
76
81
|
if (free(p)) {
|
|
77
82
|
// A free plan that is not the org's means a paid one is.
|
|
78
83
|
const paid = on;
|
|
79
84
|
if (paid.ending)
|
|
80
|
-
return _jsx(Line, { children:
|
|
81
|
-
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children:
|
|
85
|
+
return _jsx(Line, { children: t('You move to {plan} when {current} ends on {date}.', { plan: p.name, current: paid.name, date: day(paid.ends) }) });
|
|
86
|
+
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: t('Switch to {plan}', { plan: p.name }) }));
|
|
82
87
|
}
|
|
83
88
|
if (on && on.price > 0)
|
|
84
|
-
return _jsx(Line, { children:
|
|
89
|
+
return _jsx(Line, { children: t('One paid plan at a time: {current} ends before {plan} begins.', { current: on.name, plan: p.name }) });
|
|
85
90
|
if (!card) {
|
|
86
|
-
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children:
|
|
91
|
+
return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: t('Add a card to upgrade') }));
|
|
87
92
|
}
|
|
88
|
-
return (_jsx(Button, { size: "sm", variant: p.popular ? 'primary' : 'outline', onPress: () => setBuying(p), children:
|
|
93
|
+
return (_jsx(Button, { size: "sm", variant: p.popular ? 'primary' : 'outline', onPress: () => setBuying(p), children: t('Upgrade to {plan}', { plan: p.name }) }));
|
|
89
94
|
};
|
|
90
|
-
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":
|
|
91
|
-
? 'Sign in to see the plan you are on.'
|
|
95
|
+
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": t('Back to billing'), items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16, 'data-i18n-mirror': '' }), _jsx(SizableText, { size: "$2", color: "$soft", children: t('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: t('Plans that grow with you') }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
|
|
96
|
+
? t('Sign in to see the plan you are on.')
|
|
92
97
|
: on
|
|
93
|
-
?
|
|
94
|
-
|
|
98
|
+
? org
|
|
99
|
+
? t('{org} is on {plan}.', { org, plan: on.name })
|
|
100
|
+
: t('This organization is on {plan}.', { plan: on.name })
|
|
101
|
+
: org
|
|
102
|
+
? t('{org} is on the free plan.', { org })
|
|
103
|
+
: t('This organization is on the free plan.') })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": t('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' ? t('Monthly') : t('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' ? t('Monthly') : t('Yearly') }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: t('Reading plans…') })) : list.value.length === 0 ? (_jsx(Line, { children: t('This brand sells no plans.') })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
|
|
95
104
|
const cost = price(p, term);
|
|
96
|
-
return (_jsxs(YStack, { "aria-label":
|
|
97
|
-
? 'Talk to us'
|
|
105
|
+
return (_jsxs(YStack, { "aria-label": t('{plan} plan', { plan: p.name }), 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: t('Current') })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('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 ? t('Custom') : cost.month === 0 ? t('Free') : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
|
|
106
|
+
? t('Talk to us')
|
|
98
107
|
: cost.month === 0
|
|
99
|
-
? 'Forever'
|
|
100
|
-
:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
? t('Forever')
|
|
109
|
+
: term === 'year' && p.yearly
|
|
110
|
+
? p.perSeat
|
|
111
|
+
? t('per seat, a month, {price} billed yearly', { price: money(cost.now, p.currency) })
|
|
112
|
+
: t('a month, {price} billed yearly', { price: money(cost.now, p.currency) })
|
|
113
|
+
: p.perSeat
|
|
114
|
+
? t('per seat, a month')
|
|
115
|
+
: t('a month') })] }), _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));
|
|
116
|
+
}) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => !o && setBuying(null), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? t('Upgrade to {plan}', { plan: buying.name }) : t('Upgrade') }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: term === 'year'
|
|
117
|
+
? t('{card} is charged {amount} now, and again every year until you cancel.', { card: label(card), amount: money(price(buying, term).now, buying.currency) })
|
|
118
|
+
: t('{card} is charged {amount} now, and again every month until you cancel.', { card: label(card), amount: money(price(buying, term).now, buying.currency) }) }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: t('Not now') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
|
|
119
|
+
await subscribe(target, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
|
|
120
|
+
return org ? t('{org} is on {plan}', { org, plan: buying.name }) : t('This organization is on {plan}', { plan: buying.name });
|
|
121
|
+
}), children: t('Upgrade') })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Switch to the free plan?') }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: day(on.ends)
|
|
122
|
+
? t('{plan} stays on until {date}, and nothing is charged after that.', { plan: on.name, date: day(on.ends) })
|
|
123
|
+
: t('{plan} stays on until the end of this period, and nothing is charged after that.', { plan: on.name }) }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: t('Keep {plan}', { plan: on.name }) }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
|
|
124
|
+
await cancel(target, on.id);
|
|
125
|
+
return t('{plan} ends on {date}', { plan: on.name, date: day(on.ends) });
|
|
126
|
+
}), children: t('Switch') })] })] })) : null] }) }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
|
|
108
127
|
cards.reload();
|
|
109
|
-
setNote(
|
|
128
|
+
setNote(t('{card} is saved', { card: label(m) }));
|
|
110
129
|
} })] }));
|
|
111
130
|
}
|
|
112
131
|
function Line({ children }) {
|
package/lib/plugins/design.js
CHANGED
|
@@ -8,6 +8,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
* everything made after it follows it. From `+` › Design system it attaches to
|
|
9
9
|
* any later conversation, in Chat or in Dev.
|
|
10
10
|
*/
|
|
11
|
+
import { phrase } from '@hanzo/i18n';
|
|
11
12
|
import { Palette } from '@hanzogui/lucide-icons-2';
|
|
12
13
|
import { call } from '../api/call.js';
|
|
13
14
|
import { keepBrand } from '../api/studio.js';
|
|
@@ -63,8 +64,8 @@ export async function make(turn, t, signal) {
|
|
|
63
64
|
}
|
|
64
65
|
export const designer = {
|
|
65
66
|
id: 'design',
|
|
66
|
-
name: 'Design system',
|
|
67
|
-
note: 'Make one from a brief or a site',
|
|
67
|
+
name: phrase('Design system'),
|
|
68
|
+
note: phrase('Make one from a brief or a site'),
|
|
68
69
|
icon: _jsx(Palette, { size: 16 }),
|
|
69
70
|
prepare: (turn, c, signal) => make(turn, c.target, signal),
|
|
70
71
|
};
|
package/lib/plugins/index.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugins the composer offers, from `+` › Plugins and by typing `@`: only
|
|
3
|
+
* those that run for this person, each on Hanzo's own services.
|
|
4
|
+
*
|
|
5
|
+
* Work in a project the org has a project (GET /v1/projects)
|
|
6
|
+
* Deep research signed in: web search, page reading and a Hanzo model, as the person
|
|
7
|
+
* Sketch always: it draws in the page
|
|
8
|
+
* Template Creator an org admin: only an admin writes the org's skills
|
|
9
|
+
* Presentations Studio runs its `slides` template (GET /v1/studio/templates)
|
|
10
|
+
* PDF signed in: read and verify in the page; create where Studio runs `document`
|
|
11
|
+
*
|
|
12
|
+
* What is there is read once per platform, org and sign-in, and kept for the page.
|
|
13
|
+
*
|
|
14
|
+
* Deep research IS the Research switch: its row turns `Gear.research` on and
|
|
15
|
+
* off, and `prep` asks it when that is on. Every other plugin that prepares is on
|
|
16
|
+
* while its id is in `Gear.plugins`; one that opens something runs when picked.
|
|
17
|
+
*/
|
|
18
|
+
import type { Say } from '@hanzo/i18n/react';
|
|
1
19
|
import type { Target } from '../api/call.ts';
|
|
2
20
|
import type { Gear, GearState } from '../gear.ts';
|
|
3
21
|
import type { Composer, Plugin, Prep, Turn } from '../plugin.ts';
|
|
@@ -13,8 +31,8 @@ export declare const BUILD: string;
|
|
|
13
31
|
/** What runs for this person, in the menu's order. */
|
|
14
32
|
export declare function probe(t: Target): Promise<Plugin[]>;
|
|
15
33
|
export declare function usePlugins(t: Target): Plugins;
|
|
16
|
-
/** The `+` menu's Plugins shelf. */
|
|
17
|
-
export declare function shelf(p: Plugins, g: GearState, c: Composer): Shelf;
|
|
34
|
+
/** The `+` menu's Plugins shelf, in the words of the caller's `t` (a plugin's name and note are marked where it is written). */
|
|
35
|
+
export declare function shelf(p: Plugins, g: GearState, c: Composer, t?: Say): Shelf;
|
|
18
36
|
/** The plugins this message asks, in the menu's order. */
|
|
19
37
|
export declare const active: (plugins: readonly Plugin[], g: Gear) => Plugin[];
|
|
20
38
|
/** Several plugins' preparations as one: system parts and replies in order, files together, the design system one made. */
|
|
@@ -22,6 +40,7 @@ export declare function merge(preps: readonly Prep[]): Prep;
|
|
|
22
40
|
/**
|
|
23
41
|
* Ask every plugin that is on what the message carries. A plugin that fails
|
|
24
42
|
* fails the message, with its name and its reason: a report asked for and not
|
|
25
|
-
* researched must not arrive looking researched.
|
|
43
|
+
* researched must not arrive looking researched. Its name is said in the
|
|
44
|
+
* caller's `t`; the reason is `say`'s.
|
|
26
45
|
*/
|
|
27
|
-
export declare function prep(plugins: readonly Plugin[], g: Gear, turn: Turn, c: Composer, signal: AbortSignal, denied?: readonly string[]): Promise<Prep>;
|
|
46
|
+
export declare function prep(plugins: readonly Plugin[], g: Gear, turn: Turn, c: Composer, signal: AbortSignal, denied?: readonly string[], t?: Say): Promise<Prep>;
|