@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.
Files changed (168) hide show
  1. package/README.md +13 -0
  2. package/lib/activity.d.ts +3 -7
  3. package/lib/activity.js +18 -5
  4. package/lib/api/agents.d.ts +1 -20
  5. package/lib/api/agents.js +16 -8
  6. package/lib/api/auto.d.ts +3 -23
  7. package/lib/api/auto.js +39 -22
  8. package/lib/api/billing.d.ts +0 -35
  9. package/lib/api/billing.js +6 -5
  10. package/lib/api/call.d.ts +16 -8
  11. package/lib/api/call.js +32 -1
  12. package/lib/api/capabilities.d.ts +0 -11
  13. package/lib/api/capabilities.js +2 -1
  14. package/lib/api/codebases.d.ts +0 -10
  15. package/lib/api/codebases.js +3 -2
  16. package/lib/api/coding.d.ts +3 -32
  17. package/lib/api/coding.js +13 -7
  18. package/lib/api/connectors.d.ts +1 -17
  19. package/lib/api/connectors.js +5 -4
  20. package/lib/api/environment.d.ts +6 -20
  21. package/lib/api/environment.js +22 -19
  22. package/lib/api/github.d.ts +0 -22
  23. package/lib/api/github.js +3 -2
  24. package/lib/api/keys.d.ts +0 -14
  25. package/lib/api/keys.js +4 -3
  26. package/lib/api/library.d.ts +3 -17
  27. package/lib/api/library.js +23 -22
  28. package/lib/api/links.d.ts +1 -1
  29. package/lib/api/links.js +1 -1
  30. package/lib/api/machines.d.ts +0 -15
  31. package/lib/api/machines.js +6 -5
  32. package/lib/api/mcp.d.ts +0 -9
  33. package/lib/api/mcp.js +3 -2
  34. package/lib/api/members.d.ts +0 -15
  35. package/lib/api/members.js +4 -3
  36. package/lib/api/memory.d.ts +0 -11
  37. package/lib/api/memory.js +2 -1
  38. package/lib/api/places.d.ts +1 -9
  39. package/lib/api/places.js +12 -1
  40. package/lib/api/plugins.d.ts +1 -16
  41. package/lib/api/plugins.js +4 -3
  42. package/lib/api/profile.d.ts +0 -10
  43. package/lib/api/profile.js +5 -4
  44. package/lib/api/projects.d.ts +0 -14
  45. package/lib/api/projects.js +11 -10
  46. package/lib/api/provider.d.ts +2 -14
  47. package/lib/api/provider.js +12 -10
  48. package/lib/api/sandbox.d.ts +0 -17
  49. package/lib/api/sandbox.js +5 -4
  50. package/lib/api/sessions.d.ts +0 -17
  51. package/lib/api/sessions.js +5 -4
  52. package/lib/api/skills.d.ts +1 -15
  53. package/lib/api/skills.js +5 -4
  54. package/lib/api/studio.d.ts +0 -14
  55. package/lib/api/studio.js +6 -5
  56. package/lib/api/turn.d.ts +6 -28
  57. package/lib/api/turn.js +105 -49
  58. package/lib/api/verdict.d.ts +0 -9
  59. package/lib/api/verdict.js +2 -1
  60. package/lib/api/webhooks.d.ts +1 -14
  61. package/lib/api/webhooks.js +5 -4
  62. package/lib/ask.js +7 -4
  63. package/lib/automations.d.ts +5 -4
  64. package/lib/automations.js +95 -74
  65. package/lib/back.js +10 -4
  66. package/lib/builder.js +7 -5
  67. package/lib/carry.js +20 -16
  68. package/lib/customize/agents.js +78 -31
  69. package/lib/customize/connectors.js +62 -48
  70. package/lib/customize/index.js +43 -10
  71. package/lib/customize/plugins.js +34 -22
  72. package/lib/customize/say.d.ts +13 -1
  73. package/lib/customize/say.js +15 -21
  74. package/lib/customize/skills.js +31 -22
  75. package/lib/customize/ui.d.ts +5 -9
  76. package/lib/customize/ui.js +26 -16
  77. package/lib/data.d.ts +1 -1
  78. package/lib/data.js +3 -2
  79. package/lib/desk.js +45 -33
  80. package/lib/door.js +35 -10
  81. package/lib/environment.js +25 -19
  82. package/lib/files.js +43 -34
  83. package/lib/find.js +12 -9
  84. package/lib/foot.js +7 -4
  85. package/lib/forge.js +61 -40
  86. package/lib/gear.d.ts +2 -1
  87. package/lib/gear.js +6 -4
  88. package/lib/git.js +29 -19
  89. package/lib/landing.d.ts +5 -5
  90. package/lib/landing.js +52 -44
  91. package/lib/library.js +73 -51
  92. package/lib/merge.d.ts +2 -2
  93. package/lib/meter.d.ts +1 -1
  94. package/lib/meter.js +14 -5
  95. package/lib/mic.js +12 -5
  96. package/lib/mind.d.ts +1 -0
  97. package/lib/mind.js +5 -3
  98. package/lib/nav.d.ts +6 -5
  99. package/lib/nav.js +37 -30
  100. package/lib/panel.d.ts +2 -1
  101. package/lib/panel.js +52 -36
  102. package/lib/plan.d.ts +4 -20
  103. package/lib/plan.js +26 -10
  104. package/lib/plans.js +49 -30
  105. package/lib/plugins/design.js +3 -2
  106. package/lib/plugins/index.d.ts +23 -4
  107. package/lib/plugins/index.js +11 -26
  108. package/lib/plugins/pdf.js +2 -1
  109. package/lib/plugins/project.js +7 -4
  110. package/lib/plugins/research.js +3 -2
  111. package/lib/plugins/sketch.js +6 -3
  112. package/lib/plugins/slides.js +3 -2
  113. package/lib/plugins/template.js +6 -3
  114. package/lib/plus.js +25 -13
  115. package/lib/prefs.d.ts +0 -14
  116. package/lib/prefs.js +2 -1
  117. package/lib/project.js +69 -62
  118. package/lib/prompt.d.ts +12 -11
  119. package/lib/prompt.js +38 -20
  120. package/lib/publish.js +17 -12
  121. package/lib/reply.js +6 -3
  122. package/lib/run.js +72 -54
  123. package/lib/say.d.ts +2 -0
  124. package/lib/say.js +7 -0
  125. package/lib/scheduled.js +54 -43
  126. package/lib/section.d.ts +1 -0
  127. package/lib/section.js +8 -2
  128. package/lib/settings/account.d.ts +1 -0
  129. package/lib/settings/account.js +31 -21
  130. package/lib/settings/billing.js +55 -28
  131. package/lib/settings/capabilities.js +26 -15
  132. package/lib/settings/card.js +19 -12
  133. package/lib/settings/code.js +22 -11
  134. package/lib/settings/credits.d.ts +4 -4
  135. package/lib/settings/credits.js +34 -21
  136. package/lib/settings/environments.js +21 -14
  137. package/lib/settings/general.js +26 -14
  138. package/lib/settings/index.js +6 -3
  139. package/lib/settings/integrations.js +42 -28
  140. package/lib/settings/keys.js +37 -19
  141. package/lib/settings/list.d.ts +0 -5
  142. package/lib/settings/list.js +24 -15
  143. package/lib/settings/machines.d.ts +3 -0
  144. package/lib/settings/machines.js +63 -38
  145. package/lib/settings/members.js +23 -14
  146. package/lib/settings/memory.js +12 -6
  147. package/lib/settings/notifications.js +32 -19
  148. package/lib/settings/plan.d.ts +1 -1
  149. package/lib/settings/plan.js +40 -17
  150. package/lib/settings/privacy.js +26 -13
  151. package/lib/settings/ui.d.ts +3 -2
  152. package/lib/settings/ui.js +9 -3
  153. package/lib/settings/usage.js +7 -3
  154. package/lib/shelf.js +20 -17
  155. package/lib/side.d.ts +1 -26
  156. package/lib/side.js +11 -5
  157. package/lib/standing.d.ts +2 -10
  158. package/lib/standing.js +5 -3
  159. package/lib/suggest.d.ts +8 -28
  160. package/lib/suggest.js +39 -4
  161. package/lib/suggestions.js +14 -5
  162. package/lib/summary.d.ts +1 -1
  163. package/lib/summary.js +4 -1
  164. package/lib/sync.js +22 -12
  165. package/lib/tools.d.ts +4 -3
  166. package/lib/tools.js +10 -7
  167. package/lib/transcript.js +10 -8
  168. 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": "Show tabs", "aria-expanded": false, "aria-keyshortcuts": SHORTCUTS, ...{ title: `Show tabs (${KEYS})` }, onPress: onToggle, width: 32, height: 32, ...quiet(), children: _jsx(SquarePlus, { size: 16 }) }));
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
- return (_jsxs(YStack, { "data-slot": "side-suggested", px: "$4", pt: "$4", gap: "$3", children: [_jsx(SizableText, { fontSize: 13, color: "$soft", children: "Suggested" }), suggested.length ? (_jsx(XStack, { role: "list", "aria-label": "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: "No suggestions." }))] }));
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
- 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: "Open in a new window" })] })] }));
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": "Zoom", items: "center", gap: "$2", px: 8, height: 38, opacity: disabled ? 0.5 : 1, children: [_jsx(SizableText, { fontSize: 13, color: "$ink", flex: 1, children: "Zoom" }), _jsxs(XStack, { items: "center", gap: 2, borderWidth: 1, borderColor: "$borderColor", rounded: 8, px: 2, height: 28, children: [box('Zoom out', step(-1), _jsx(Minus, { size: 12 })), _jsx(SizableText, { fontSize: 12, color: "$ink", width: 40, text: "center", "aria-live": "polite", children: `${level}%` }), box('Zoom in', step(1), _jsx(Plus, { size: 12 }))] }), box('Reset zoom', () => onLevel(100), _jsx(RotateCcw, { size: 13 }))] }));
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: "Skills" }) }), open ? (_jsx(DropdownMenuSubContent, { minW: 200, children: skills.map((k) => (_jsx(Row, { label: k.name, onSelect: () => here && k.run(here) }, k.id))) })) : null] }));
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: "Ask about this page", disabled: none, onSelect: () => void ask() }, "ask") : null,
199
- acts.onDev && shown && hosted(shown.url) ? _jsx(Row, { label: "Open in Dev", onSelect: () => acts.onDev?.({ url: shown.url, title: shown.title }) }, "dev") : null,
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: "Open in a new window", disabled: none, onSelect: () => popOut(tab) }, "out"),
202
- _jsx(Row, { label: "Copy link", disabled: none || !tab.url, onSelect: () => tab.url && void navigator.clipboard?.writeText(tab.url) }, "copy"),
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: "Find in page", disabled: none, onSelect: () => shown && browser.find?.(shown) }, "find") : null,
207
- print ? _jsx(Row, { label: "Print", disabled: none, onSelect: () => shown && print(shown) }, "print") : null,
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: "Show device toolbar", disabled: none, onSelect: () => shown && browser.devices?.(shown) }, "devices") : null,
210
- browser?.capture && acts.onCapture ? _jsx(Row, { label: "Take a screenshot", disabled: none, onSelect: () => void capture() }, "capture") : null,
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: "Passwords and autofill", onSelect: browser.passwords, end: _jsx(ChevronRight, { size: 14, opacity: 0.6 }) }, "passwords") : null,
214
- browser?.downloads ? _jsx(Row, { label: "Downloads", onSelect: browser.downloads }, "downloads") : null,
215
- browser?.history ? _jsx(Row, { label: "History", onSelect: browser.history }, "history") : null,
216
- browser?.clear ? _jsx(Row, { label: "Clear browsing data", onSelect: browser.clear }, "clear") : null,
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: "Browser settings", onSelect: browser.settings }, "settings")] : [];
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": "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)) })] }));
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": "Back", "aria-disabled": !back || undefined, onPress: back ? onBack : undefined, width: 28, height: 28, ...quiet(false, !back), rounded: 999, children: _jsx(ArrowLeft, { size: 15 }) }), _jsx(XStack, { render: "button", role: "button", "aria-label": "Forward", "aria-disabled": !ahead || undefined, onPress: ahead ? onForward : undefined, width: 28, height: 28, ...quiet(false, !ahead), rounded: 999, children: _jsx(ArrowRight, { size: 15 }) }), _jsx(XStack, { render: "button", role: "button", "aria-label": "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,
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: "Search or enter a URL", "aria-label": "Address", spellCheck: false, autoFocus: !own && !tab.url, onFocus: (e) => {
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 left.
294
- textAlign: typing || typed ? 'left' : 'center',
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: `${(ext || type || 'This').toUpperCase()} previews aren’t supported yet.` }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Open it in a new window to view it." })] }))] }));
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
- 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": `${label} tab`, children: [_jsx(ContextMenuItem, { onSelect: () => (tab.pinned ? side.unpin(tab.id) : side.pin(tab.id)), children: tab.pinned ? 'Unpin tab' : 'Pin tab' }), _jsx(ContextMenuSeparator, {}), _jsx(ContextMenuItem, { onSelect: () => side.shut(tab.id), children: "Close tab" })] })] }));
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": `Unpin ${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": `Close ${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 },
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 = 'Side panel', suggested, ...acts }) {
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
- const title = (t) => (t.kind === PAGE ? t.title || BLANK : (kinds.find((k) => k.id === t.kind)?.label ?? t.title));
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": "Open a tab", width: 28, height: 28, ...quiet(), onPress: closed.length ? undefined : blank, children: _jsx(Plus, { size: 16 }) }));
379
- const body = (_jsxs(YStack, { role: "complementary", "aria-label": label, "data-slot": "side", flex: 1, minW: 0, minH: 0, overflow: "hidden", style: { borderRadius: '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: `Show ${column.main}` })] })) : null, _jsx(XStack, { role: "tablist", "aria-label": "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: [
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": "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 ? 'Exit full screen' : 'Full screen', "aria-pressed": column.full, "aria-keyshortcuts": column.full ? 'Escape' : undefined, ...{ title: column.full ? 'Exit full screen (Esc)' : '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": "Hide tabs", "aria-expanded": true, "aria-keyshortcuts": SHORTCUTS, ...{ title: `Hide tabs (${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,
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: "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: "This tab is not offered here." })) })] }));
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.length ? `${names.join(' and ')} model usage` : 'usage';
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
- return `Your ${l ? `${said(l)} plan’s` : 'plan’s'} included ${what} is used${at ? ` until ${at}` : ''}.`;
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 t = useTarget();
32
+ const target = useTarget();
33
+ const { t } = useI18n();
29
34
  const signed = Boolean(host.person);
30
- const list = useRead(() => plans(t), [], [t]);
31
- const subs = useRead(signed ? () => subscriptions(t) : null, [], [t, signed]);
32
- const cards = useRead(signed ? () => methods(t) : null, [], [t, signed]);
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 who = host.org ?? 'This organization';
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: "Current plan" }));
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: "Priced with our team for your organization." });
74
+ return _jsx(Line, { children: t('Priced with our team for your organization.') });
70
75
  if (!signed)
71
- return _jsx(Line, { children: "Sign in to choose a plan." });
76
+ return _jsx(Line, { children: t('Sign in to choose a plan.') });
72
77
  if (!host.admin)
73
- return _jsx(Line, { children: "An org admin changes the plan." });
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: `You move to ${p.name} when ${paid.name} ends on ${day(paid.ends)}.` });
81
- return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: `Switch to ${p.name}` }));
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: `One paid plan at a time: ${on.name} ends before ${p.name} begins.` });
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: "Add a card to upgrade" }));
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: `Upgrade to ${p.name}` }));
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": "Back to billing", items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Billing" })] }), _jsxs(XStack, { items: "flex-end", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 240, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", children: "Plans that grow with you" }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
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
- ? `${who} is on ${on.name}.`
94
- : `${who} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
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": `${p.name} plan`, flex: 1, minW: 240, maxW: 360, "$max-sm": { maxW: '100%' }, gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: mine(p) || p.popular ? '$ink' : '$borderColor', bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$5", color: "$ink", children: p.name }), mine(p) ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Current" })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Popular" })) : null] }), p.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: p.description })) : null, _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$7", color: "$ink", children: p.sales ? 'Custom' : cost.month === 0 ? 'Free' : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
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
- : `${p.perSeat ? 'per seat, ' : ''}a month${term === 'year' && p.yearly ? `, ${money(cost.now, p.currency)} billed yearly` : ''}` })] }), _jsx(XStack, { children: move(p) }), p.features.length ? (_jsx(YStack, { gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: p.features.map((f) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(YStack, { pt: 2, children: _jsx(Check, { size: 14 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$soft", children: f })] }, f))) })) : null] }, p.id));
101
- }) })), 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 ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
102
- await subscribe(t, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
103
- return `${who} is on ${buying.name}`;
104
- }), children: "Upgrade" })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Switch to the free plan?" }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${on.name} stays on until ${day(on.ends) || 'the end of this period'}, and nothing is charged after that.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: `Keep ${on.name}` }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
105
- await cancel(t, on.id);
106
- return `${on.name} ends on ${day(on.ends)}`;
107
- }), children: "Switch" })] })] })) : null] }) }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
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(`${label(m)} is saved`);
128
+ setNote(t('{card} is saved', { card: label(m) }));
110
129
  } })] }));
111
130
  }
112
131
  function Line({ children }) {
@@ -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
  };
@@ -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/project)
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>;