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