@hanzo/rooms 0.1.0

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 (235) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +23 -0
  4. package/dist/Account.d.ts +8 -0
  5. package/dist/Account.js +58 -0
  6. package/dist/Beluga.d.ts +12 -0
  7. package/dist/Beluga.js +152 -0
  8. package/dist/Billing.d.ts +1 -0
  9. package/dist/Billing.js +31 -0
  10. package/dist/Board.d.ts +9 -0
  11. package/dist/Board.js +447 -0
  12. package/dist/Boxes.d.ts +1 -0
  13. package/dist/Boxes.js +173 -0
  14. package/dist/CalScheduler.d.ts +15 -0
  15. package/dist/CalScheduler.js +196 -0
  16. package/dist/Chat.d.ts +35 -0
  17. package/dist/Chat.js +1049 -0
  18. package/dist/Column.d.ts +15 -0
  19. package/dist/Column.js +88 -0
  20. package/dist/Contacts.d.ts +1 -0
  21. package/dist/Contacts.js +301 -0
  22. package/dist/Directory.d.ts +24 -0
  23. package/dist/Directory.js +356 -0
  24. package/dist/Drive.d.ts +2 -0
  25. package/dist/Drive.js +466 -0
  26. package/dist/Find.d.ts +7 -0
  27. package/dist/Find.js +82 -0
  28. package/dist/Guide.d.ts +6 -0
  29. package/dist/Guide.js +272 -0
  30. package/dist/Home.d.ts +39 -0
  31. package/dist/Home.js +306 -0
  32. package/dist/Inbox.d.ts +9 -0
  33. package/dist/Inbox.js +193 -0
  34. package/dist/Market.d.ts +3 -0
  35. package/dist/Market.js +149 -0
  36. package/dist/Me.d.ts +27 -0
  37. package/dist/Me.js +251 -0
  38. package/dist/Meet.d.ts +9 -0
  39. package/dist/Meet.js +370 -0
  40. package/dist/ModeMark.d.ts +8 -0
  41. package/dist/ModeMark.js +18 -0
  42. package/dist/OrgSwitcher.d.ts +31 -0
  43. package/dist/OrgSwitcher.js +120 -0
  44. package/dist/Orgs.d.ts +10 -0
  45. package/dist/Orgs.js +582 -0
  46. package/dist/Profile.d.ts +4 -0
  47. package/dist/Profile.js +67 -0
  48. package/dist/Prose.d.ts +5 -0
  49. package/dist/Prose.js +111 -0
  50. package/dist/ProviderMark.d.ts +17 -0
  51. package/dist/ProviderMark.js +277 -0
  52. package/dist/Recents.d.ts +10 -0
  53. package/dist/Recents.js +91 -0
  54. package/dist/RightPane.d.ts +13 -0
  55. package/dist/RightPane.js +426 -0
  56. package/dist/Room.d.ts +8 -0
  57. package/dist/Room.js +50 -0
  58. package/dist/Scene.d.ts +12 -0
  59. package/dist/Scene.js +105 -0
  60. package/dist/Settings.d.ts +46 -0
  61. package/dist/Settings.js +321 -0
  62. package/dist/Setup.d.ts +1 -0
  63. package/dist/Setup.js +36 -0
  64. package/dist/Share.d.ts +3 -0
  65. package/dist/Share.js +130 -0
  66. package/dist/Shared.d.ts +1 -0
  67. package/dist/Shared.js +113 -0
  68. package/dist/Shell.d.ts +63 -0
  69. package/dist/Shell.js +1225 -0
  70. package/dist/Start.d.ts +1 -0
  71. package/dist/Start.js +96 -0
  72. package/dist/Support.d.ts +14 -0
  73. package/dist/Support.js +122 -0
  74. package/dist/Talk.d.ts +4 -0
  75. package/dist/Talk.js +107 -0
  76. package/dist/Upgrade.d.ts +11 -0
  77. package/dist/Upgrade.js +75 -0
  78. package/dist/Usage.d.ts +1 -0
  79. package/dist/Usage.js +21 -0
  80. package/dist/Vibe.d.ts +2 -0
  81. package/dist/Vibe.js +188 -0
  82. package/dist/Work.d.ts +12 -0
  83. package/dist/Work.js +256 -0
  84. package/dist/bots/Bots.d.ts +2 -0
  85. package/dist/bots/Bots.js +456 -0
  86. package/dist/bots/Lights.d.ts +8 -0
  87. package/dist/bots/Lights.js +13 -0
  88. package/dist/bots/api.d.ts +120 -0
  89. package/dist/bots/api.js +75 -0
  90. package/dist/bots/face.d.ts +5 -0
  91. package/dist/bots/face.js +13 -0
  92. package/dist/bots/ink.d.ts +7 -0
  93. package/dist/bots/ink.js +29 -0
  94. package/dist/bots/kit.d.ts +113 -0
  95. package/dist/bots/kit.js +43 -0
  96. package/dist/bots/runtime.d.ts +4 -0
  97. package/dist/bots/runtime.js +326 -0
  98. package/dist/card.d.ts +52 -0
  99. package/dist/card.js +52 -0
  100. package/dist/cast.d.ts +143 -0
  101. package/dist/cast.js +251 -0
  102. package/dist/connect.d.ts +29 -0
  103. package/dist/connect.js +64 -0
  104. package/dist/contacts-store.d.ts +111 -0
  105. package/dist/contacts-store.js +276 -0
  106. package/dist/conversations.d.ts +54 -0
  107. package/dist/conversations.js +98 -0
  108. package/dist/copy.d.ts +23 -0
  109. package/dist/copy.js +39 -0
  110. package/dist/crew.d.ts +36 -0
  111. package/dist/crew.js +304 -0
  112. package/dist/dev.d.ts +3 -0
  113. package/dist/dev.js +102 -0
  114. package/dist/enso.d.ts +70 -0
  115. package/dist/enso.js +112 -0
  116. package/dist/failure.d.ts +57 -0
  117. package/dist/failure.js +143 -0
  118. package/dist/ground.d.ts +32 -0
  119. package/dist/ground.js +32 -0
  120. package/dist/host.d.ts +76 -0
  121. package/dist/host.js +76 -0
  122. package/dist/index.d.ts +14 -0
  123. package/dist/index.js +12 -0
  124. package/dist/kept.d.ts +5 -0
  125. package/dist/kept.js +27 -0
  126. package/dist/lib/account.d.ts +49 -0
  127. package/dist/lib/account.js +229 -0
  128. package/dist/lib/ai.d.ts +46 -0
  129. package/dist/lib/ai.js +140 -0
  130. package/dist/lib/api.d.ts +43 -0
  131. package/dist/lib/api.js +62 -0
  132. package/dist/lib/bare.d.ts +9 -0
  133. package/dist/lib/bare.js +7 -0
  134. package/dist/lib/coding.d.ts +105 -0
  135. package/dist/lib/coding.js +87 -0
  136. package/dist/lib/destination.d.ts +102 -0
  137. package/dist/lib/destination.js +260 -0
  138. package/dist/lib/durable.d.ts +66 -0
  139. package/dist/lib/durable.js +107 -0
  140. package/dist/lib/first.d.ts +11 -0
  141. package/dist/lib/first.js +23 -0
  142. package/dist/lib/home.d.ts +61 -0
  143. package/dist/lib/home.js +54 -0
  144. package/dist/lib/host.d.ts +104 -0
  145. package/dist/lib/host.js +132 -0
  146. package/dist/lib/hostname.d.ts +1 -0
  147. package/dist/lib/hostname.js +14 -0
  148. package/dist/lib/hydrated.d.ts +1 -0
  149. package/dist/lib/hydrated.js +20 -0
  150. package/dist/lib/integrations.d.ts +21 -0
  151. package/dist/lib/integrations.js +1 -0
  152. package/dist/lib/intent.d.ts +6 -0
  153. package/dist/lib/intent.js +71 -0
  154. package/dist/lib/invite.d.ts +155 -0
  155. package/dist/lib/invite.js +245 -0
  156. package/dist/lib/limits.d.ts +55 -0
  157. package/dist/lib/limits.js +119 -0
  158. package/dist/lib/list.d.ts +9 -0
  159. package/dist/lib/list.js +11 -0
  160. package/dist/lib/mark.d.ts +13 -0
  161. package/dist/lib/mark.js +85 -0
  162. package/dist/lib/mix.d.ts +22 -0
  163. package/dist/lib/mix.js +22 -0
  164. package/dist/lib/models.d.ts +27 -0
  165. package/dist/lib/models.js +106 -0
  166. package/dist/lib/org.d.ts +109 -0
  167. package/dist/lib/org.js +140 -0
  168. package/dist/lib/path.d.ts +9 -0
  169. package/dist/lib/path.js +29 -0
  170. package/dist/lib/plans.d.ts +193 -0
  171. package/dist/lib/plans.js +310 -0
  172. package/dist/lib/prose.d.ts +13 -0
  173. package/dist/lib/prose.js +62 -0
  174. package/dist/lib/reach.d.ts +17 -0
  175. package/dist/lib/reach.js +70 -0
  176. package/dist/lib/rooms.json +23 -0
  177. package/dist/lib/served.d.ts +20 -0
  178. package/dist/lib/served.js +92 -0
  179. package/dist/lib/session.d.ts +211 -0
  180. package/dist/lib/session.js +493 -0
  181. package/dist/lib/share.d.ts +131 -0
  182. package/dist/lib/share.js +216 -0
  183. package/dist/lib/signout.d.ts +14 -0
  184. package/dist/lib/signout.js +40 -0
  185. package/dist/lib/tags.d.ts +27 -0
  186. package/dist/lib/tags.js +46 -0
  187. package/dist/lib/tap.d.ts +17 -0
  188. package/dist/lib/tap.js +9 -0
  189. package/dist/lib/tier.d.ts +115 -0
  190. package/dist/lib/tier.js +207 -0
  191. package/dist/lib/tiers.d.ts +9 -0
  192. package/dist/lib/tiers.js +141 -0
  193. package/dist/lib/todo.d.ts +125 -0
  194. package/dist/lib/todo.js +379 -0
  195. package/dist/look.d.ts +1 -0
  196. package/dist/look.js +21 -0
  197. package/dist/meters.d.ts +8 -0
  198. package/dist/meters.js +31 -0
  199. package/dist/model.d.ts +4 -0
  200. package/dist/model.js +44 -0
  201. package/dist/network.d.ts +64 -0
  202. package/dist/network.js +216 -0
  203. package/dist/open.d.ts +81 -0
  204. package/dist/open.js +149 -0
  205. package/dist/pane.d.ts +92 -0
  206. package/dist/pane.js +231 -0
  207. package/dist/progress.d.ts +46 -0
  208. package/dist/progress.js +51 -0
  209. package/dist/recent.d.ts +67 -0
  210. package/dist/recent.js +161 -0
  211. package/dist/rooms.d.ts +131 -0
  212. package/dist/rooms.js +195 -0
  213. package/dist/roster.d.ts +28 -0
  214. package/dist/roster.js +70 -0
  215. package/dist/span.d.ts +14 -0
  216. package/dist/span.js +50 -0
  217. package/dist/speech.d.ts +44 -0
  218. package/dist/speech.js +229 -0
  219. package/dist/stars.d.ts +16 -0
  220. package/dist/stars.js +75 -0
  221. package/dist/tabs.d.ts +26 -0
  222. package/dist/tabs.js +25 -0
  223. package/dist/team.d.ts +11 -0
  224. package/dist/team.gen.d.ts +10 -0
  225. package/dist/team.gen.js +11 -0
  226. package/dist/team.js +30 -0
  227. package/dist/tools.d.ts +35 -0
  228. package/dist/tools.js +143 -0
  229. package/dist/tours.d.ts +24 -0
  230. package/dist/tours.js +311 -0
  231. package/dist/waiting.d.ts +15 -0
  232. package/dist/waiting.js +17 -0
  233. package/dist/where.d.ts +54 -0
  234. package/dist/where.js +14 -0
  235. package/package.json +108 -0
package/dist/Guide.js ADDED
@@ -0,0 +1,272 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The first-run guide: what this place can do, said once, standing next to the
4
+ // thing that does it.
5
+ //
6
+ // WHY COACHMARKS AND NOT A TOUR PAGE. A page of screenshots is read somewhere
7
+ // else and remembered nowhere; a card pinned to the actual control is read
8
+ // while looking at it, and the next time the reader sees that control they have
9
+ // already been introduced. So every step here RESOLVES A LIVE ELEMENT and
10
+ // positions against its measured rectangle. Nothing is drawn at a coordinate
11
+ // somebody typed.
12
+ //
13
+ // A STEP WHOSE ANCHOR IS ABSENT IS SKIPPED, not faked. Destinations come and go
14
+ // with the org's tier and with what the reader has hidden, so the tour is a
15
+ // list of candidates and the ones present at that moment are the tour. That is
16
+ // also why anchors are found by what the elements ALREADY carry — their aria
17
+ // labels, their roles, their text — rather than by `data-tour` attributes
18
+ // sprinkled through six other components: a marker added for a tour is a marker
19
+ // somebody deletes while tidying, and the tour breaks silently three months
20
+ // later. An accessible name is load-bearing for other reasons and survives.
21
+ //
22
+ // IT RUNS ONCE, and "once" belongs to the person rather than to the browser.
23
+ // Completion is written to the caller's own preference document (`/v1/pref`,
24
+ // whose `prefs` is an opaque object each surface keys as it likes), so it
25
+ // follows them to a second machine. localStorage is written FIRST and read
26
+ // first: it settles instantly, it answers for a reader who never signs in, and
27
+ // it keeps the guide from re-opening in the seconds a PATCH is in flight.
28
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
29
+ import { createPortal } from 'react-dom';
30
+ import { Text, View, XStack, YStack } from '@hanzo/gui';
31
+ import { Button } from '@hanzo/ui';
32
+ import { useIam } from '@hanzo/iam/react';
33
+ import { useAi } from './lib/ai.js';
34
+ import { Next, Scene } from './Scene.js';
35
+ import { TOURS } from './tours.js';
36
+ /** The ring on the spot: design's focus ring and its halo, one above the dim. gui hands a `calc()` to the page
37
+ * unchanged; its types only name custom properties. */
38
+ const RING = '0 0 0 1px var(--ring), 0 0 26px var(--ring-halo)';
39
+ const ABOVE = 'calc(var(--z-overlay) + 1)';
40
+ /** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
41
+ const FLOOR = 10;
42
+ /** The page's own leading (design's `--leading-base`, 1.35rem), which a face's scope would otherwise replace with its
43
+ * 20px; gui's line height takes a rung or a number. */
44
+ const LEAD = 21.6;
45
+ /** Where the card's pointer sits, and which two edges it draws, for each side the card can take. */
46
+ const POINTER = {
47
+ bottom: { t: -6.5, l: '50%', ml: -5.5, borderLeftWidth: 1, borderTopWidth: 1 },
48
+ top: { b: -6.5, l: '50%', ml: -5.5, borderRightWidth: 1, borderBottomWidth: 1 },
49
+ right: { l: -6.5, t: '50%', mt: -5.5, borderLeftWidth: 1, borderBottomWidth: 1 },
50
+ left: { r: -6.5, t: '50%', mt: -5.5, borderRightWidth: 1, borderTopWidth: 1 },
51
+ };
52
+ /** Where a room's tour is marked seen, in this browser and in the person's preferences. */
53
+ const keyOf = (room) => (room === 'chat' ? 'hanzo:chat:guide' : `hanzo:${room}:guide`);
54
+ const prefOf = (room) => `${room}Guide`;
55
+ const PAD = 8;
56
+ const GAP = 14;
57
+ const CARD_W = 352;
58
+ export function Guide({ room = 'chat' }) {
59
+ const KEY = keyOf(room);
60
+ const PREF = prefOf(room);
61
+ const STOPS = TOURS[room];
62
+ const { client } = useAi();
63
+ // ONLY FOR SOMEONE WHO IS SIGNED IN. `useAi` hands back a client either way —
64
+ // the anonymous one the free composer runs on — so a client is not proof of a
65
+ // reader. Opening for a visitor put a dimmed backdrop over the page before
66
+ // they had an account, and its panels sat on top of the Sign in button: the
67
+ // guide blocked the one action the page was asking for. It also has nothing
68
+ // true to say to a stranger, since half of what it points at is theirs only
69
+ // once they have a workspace.
70
+ const { isAuthenticated, isLoading } = useIam();
71
+ const [live, setLive] = useState(false);
72
+ const [i, setI] = useState(0);
73
+ const [spot, setSpot] = useState(null);
74
+ const [side, setSide] = useState('bottom');
75
+ const [card, setCard] = useState(null);
76
+ const cardRef = useRef(null);
77
+ // The stops whose anchors exist right now. A destination the org does not
78
+ // carry has no element, so it is not a step — the tour describes THIS
79
+ // workspace rather than a catalogue.
80
+ const stops = useMemo(() => (live ? STOPS.filter((s) => s.find() !== null) : STOPS), [live, STOPS]);
81
+ const stop = stops[i];
82
+ const done = useCallback((how) => {
83
+ setLive(false);
84
+ const mark = JSON.stringify({ at: new Date().toISOString(), how });
85
+ try {
86
+ localStorage.setItem(KEY, mark);
87
+ }
88
+ catch {
89
+ /* a browser with storage off still gets the guide once per visit */
90
+ }
91
+ // Follows the person, not the browser. Failure is silent on purpose: the
92
+ // local mark already settled it, and a guide is not worth an error.
93
+ client?.http
94
+ .json({ method: 'PATCH', path: '/v1/pref', body: { prefs: { [PREF]: { at: new Date().toISOString(), how } } } })
95
+ .catch(() => { });
96
+ }, [client, KEY, PREF]);
97
+ // Should it open at all? Local first — it answers instantly and it answers
98
+ // for a reader who never signs in.
99
+ useEffect(() => {
100
+ if (isLoading || !isAuthenticated)
101
+ return;
102
+ let held = false;
103
+ try {
104
+ held = Boolean(localStorage.getItem(KEY));
105
+ }
106
+ catch {
107
+ /* treat unreadable storage as "not yet shown" */
108
+ }
109
+ if (held)
110
+ return;
111
+ let alive = true;
112
+ const open = () => {
113
+ if (!alive)
114
+ return;
115
+ // Wait for the room to actually render before pointing at it.
116
+ const t = window.setTimeout(() => alive && setLive(true), 900);
117
+ return () => window.clearTimeout(t);
118
+ };
119
+ if (!client)
120
+ return open();
121
+ client.http
122
+ .json({ path: '/v1/pref' })
123
+ .then((doc) => {
124
+ if (!alive)
125
+ return;
126
+ if (doc?.prefs && doc.prefs[PREF]) {
127
+ try {
128
+ localStorage.setItem(KEY, JSON.stringify(doc.prefs[PREF]));
129
+ }
130
+ catch {
131
+ /* nothing to do */
132
+ }
133
+ return;
134
+ }
135
+ // THE TOUR IS FOR A NEW ACCOUNT. One that has already had a conversation
136
+ // here is not new: it may never have met this tour, and it does not
137
+ // need to. It is marked as such, so the question is not asked again.
138
+ client.threads
139
+ .list()
140
+ .then((had) => {
141
+ if (!alive)
142
+ return;
143
+ if (Array.isArray(had) && had.length > 0)
144
+ done('unneeded');
145
+ else
146
+ open();
147
+ })
148
+ .catch(() => open());
149
+ })
150
+ .catch(() => open());
151
+ return () => {
152
+ alive = false;
153
+ };
154
+ }, [client, isAuthenticated, isLoading, KEY, PREF, done]);
155
+ // Measure the anchor, and re-measure when the page moves under it.
156
+ //
157
+ // NOT on every frame. A rAF loop that calls setSpot each tick re-renders the
158
+ // card sixty times a second, which makes it a target nothing can click: the
159
+ // element under the pointer is replaced between the hit test and the press.
160
+ // Measure on the events that actually move things, and write state only when
161
+ // the rectangle really changed.
162
+ useLayoutEffect(() => {
163
+ if (!live || !stop)
164
+ return;
165
+ let last = '';
166
+ const measure = () => {
167
+ const el = stop.find();
168
+ if (!el) {
169
+ setSpot(null);
170
+ return;
171
+ }
172
+ const r = el.getBoundingClientRect();
173
+ const next = { top: r.top - PAD, left: r.left - PAD, width: r.width + PAD * 2, height: r.height + PAD * 2 };
174
+ const key = `${Math.round(next.top)}:${Math.round(next.left)}:${Math.round(next.width)}:${Math.round(next.height)}`;
175
+ if (key === last)
176
+ return;
177
+ last = key;
178
+ setSpot(next);
179
+ };
180
+ // Bring it into view once per step, then measure after the scroll settles.
181
+ stop.find()?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
182
+ measure();
183
+ const settle = window.setTimeout(measure, 260);
184
+ const ro = new ResizeObserver(measure);
185
+ ro.observe(document.body);
186
+ const el = stop.find();
187
+ if (el)
188
+ ro.observe(el);
189
+ window.addEventListener('scroll', measure, true);
190
+ window.addEventListener('resize', measure);
191
+ return () => {
192
+ window.clearTimeout(settle);
193
+ ro.disconnect();
194
+ window.removeEventListener('scroll', measure, true);
195
+ window.removeEventListener('resize', measure);
196
+ };
197
+ }, [live, stop, i]);
198
+ // Place the card against the measured spot, flipping rather than overflowing.
199
+ useLayoutEffect(() => {
200
+ if (!spot) {
201
+ setCard(null);
202
+ return;
203
+ }
204
+ const h = cardRef.current?.offsetHeight ?? 168;
205
+ const vw = window.innerWidth;
206
+ const vh = window.innerHeight;
207
+ let want = stop?.side ?? 'bottom';
208
+ if (want === 'bottom' && spot.top + spot.height + GAP + h > vh - 12)
209
+ want = 'top';
210
+ if (want === 'top' && spot.top - GAP - h < 12)
211
+ want = 'bottom';
212
+ if (want === 'right' && spot.left + spot.width + GAP + CARD_W > vw - 12)
213
+ want = 'left';
214
+ if (want === 'left' && spot.left - GAP - CARD_W < 12)
215
+ want = 'right';
216
+ let top;
217
+ let left;
218
+ if (want === 'bottom' || want === 'top') {
219
+ top = want === 'bottom' ? spot.top + spot.height + GAP : spot.top - GAP - h;
220
+ left = spot.left + spot.width / 2 - CARD_W / 2;
221
+ }
222
+ else {
223
+ left = want === 'right' ? spot.left + spot.width + GAP : spot.left - GAP - CARD_W;
224
+ top = spot.top + spot.height / 2 - h / 2;
225
+ }
226
+ setSide(want);
227
+ setCard({ top: Math.max(12, Math.min(top, vh - h - 12)), left: Math.max(12, Math.min(left, vw - CARD_W - 12)) });
228
+ }, [spot, stop]);
229
+ const next = useCallback(() => {
230
+ if (i + 1 >= stops.length)
231
+ done('finished');
232
+ else
233
+ setI(i + 1);
234
+ }, [i, stops.length, done]);
235
+ const back = useCallback(() => setI((n) => Math.max(0, n - 1)), []);
236
+ useEffect(() => {
237
+ if (!live)
238
+ return;
239
+ const onKey = (e) => {
240
+ if (e.key === 'Escape') {
241
+ e.preventDefault();
242
+ done('skipped');
243
+ }
244
+ else if (e.key === 'ArrowRight' || e.key === 'Enter') {
245
+ e.preventDefault();
246
+ next();
247
+ }
248
+ else if (e.key === 'ArrowLeft') {
249
+ e.preventDefault();
250
+ back();
251
+ }
252
+ };
253
+ document.addEventListener('keydown', onKey, true);
254
+ return () => document.removeEventListener('keydown', onKey, true);
255
+ }, [live, next, back, done]);
256
+ if (!live || !stop || !spot || !card || typeof document === 'undefined')
257
+ return null;
258
+ const last = i + 1 >= stops.length;
259
+ // The dim is four panels around the spot rather than one box with a hole:
260
+ // a hole wants a mask, and a mask over a blurred backdrop reads as a seam.
261
+ const panel = { position: 'fixed', bg: 'var(--surface-scrim)', z: 'var(--z-overlay)', onClick: () => done('skipped') };
262
+ return createPortal(_jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": "Getting started with Hanzo", children: [_jsx(View, { ...panel, t: 0, l: 0, r: 0, height: Math.max(0, spot.top) }), _jsx(View, { ...panel, t: spot.top + spot.height, l: 0, r: 0, b: 0 }), _jsx(View, { ...panel, t: spot.top, l: 0, width: Math.max(0, spot.left), height: spot.height }), _jsx(View, { ...panel, t: spot.top, l: spot.left + spot.width, r: 0, height: spot.height }), _jsx(View, { position: "fixed", t: spot.top, l: spot.left, width: spot.width, height: spot.height, rounded: 10, borderWidth: 1, borderColor: "var(--ring)", boxShadow: RING, pointerEvents: "none", z: ABOVE }), stop.scene ? (_jsx(Scene, { step: `${i + 1} / ${stops.length}`, onNext: next, onSkip: () => done('skipped') })) : (_jsxs(YStack, { ref: cardRef, position: "fixed", t: card.top, l: card.left, width: CARD_W, z: "var(--z-modal)", bg: "var(--popover)", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, boxShadow: "var(--shadow-floating)", pt: 18, px: 18, pb: 15, children: [_jsx(View, { "aria-hidden": true, position: "absolute", width: 11, height: 11, bg: "var(--popover)", rotate: "45deg", borderColor: "var(--border-control)", ...POINTER[side] }), _jsxs(XStack, { items: "center", gap: 8, mb: 9, children: [_jsxs(Text, { fontFamily: "$body", lineHeight: LEAD, fontSize: FLOOR, letterSpacing: 1.1, textTransform: "uppercase", color: "$faint", fontVariant: ['tabular-nums'], children: [i + 1, " / ", stops.length] }), _jsx(View, { flex: 1, height: 1, bg: "$borderColor" }), _jsx(XStack, { gap: 3, children: stops.map((s, n) => (_jsx(View, { width: n === i ? 14 : 5, height: 5, rounded: 3, bg: n <= i ? '$ink' : '$rim', transition: "200ms" }, s.id))) })] }), _jsx(Text, { render: "h2", fontFamily: "$body", mb: 7, fontSize: "$6", fontWeight: "600", lineHeight: 22.1, letterSpacing: -0.17, color: "$ink", children: stop.title }), _jsx(Text, { render: "p", fontFamily: "$body", mb: 16, fontSize: "$3", lineHeight: 22.4, color: "$quiet", children: stop.body }), _jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Button, { variant: "linkMuted", size: "sm", px: 2, onClick: () => done('skipped'), children: "Skip" }), _jsx(View, { flex: 1 }), i > 0 ? (_jsx(Button, { size: "sm", onClick: back, children: "Back" })) : null, _jsx(Next, { onPress: next, label: last ? 'Done' : 'Next' })] })] }))] }), document.body);
263
+ }
264
+ /** Let a room's guide be asked for again — a menu row can call this and reload. */
265
+ export function replayGuide(room = 'chat') {
266
+ try {
267
+ localStorage.removeItem(keyOf(room));
268
+ }
269
+ catch {
270
+ /* nothing to clear */
271
+ }
272
+ }
package/dist/Home.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /** What a question can start here. Four, in the order they escalate. */
2
+ declare const MODES: readonly [{
3
+ readonly id: 'chat';
4
+ readonly label: 'Chat';
5
+ readonly asks: 'Ask anything';
6
+ }, {
7
+ readonly id: 'dev';
8
+ readonly label: 'Dev';
9
+ readonly asks: 'Describe what to build';
10
+ }, {
11
+ readonly id: 'news';
12
+ readonly label: 'News';
13
+ readonly asks: 'What happened?';
14
+ }, {
15
+ readonly id: 'research';
16
+ readonly label: 'Research';
17
+ readonly asks: 'What should we dig into?';
18
+ }];
19
+ export type Mode = (typeof MODES)[number]['id'];
20
+ /**
21
+ * Ask, however the host answers.
22
+ *
23
+ * The default sends the question to /chat, which is the one room on this site
24
+ * that reads a question off the address (`?q=`, the name every Hanzo surface
25
+ * takes). A host that already HAS the thread — the chat pane, where this home
26
+ * belongs — passes its own `send` instead and nothing navigates.
27
+ */
28
+ export interface HomeProps {
29
+ ask?: (question: string, mode: Mode, sources: string[]) => void;
30
+ }
31
+ /**
32
+ * A ROOM IS READ HERE. Home is the org's room: the rail's channels and direct
33
+ * messages open in this pane (`Talk`), keyed `team:<space>/<id>` in the
34
+ * selection slot and in the address, and the Rooms card below lists them and
35
+ * opens a new one. What other networks carry in stays in the Inbox, under its
36
+ * own key.
37
+ */
38
+ export declare function Home({ ask }: HomeProps): import("react").JSX.Element;
39
+ export {};
package/dist/Home.js ADDED
@@ -0,0 +1,306 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // Home: one question at the top, and what to do next underneath.
4
+ //
5
+ // THE COMPOSER IS THE PAGE. Everything else is a card, and every card is a real
6
+ // affordance reading real cloud data — no placeholder feed, no invented metric,
7
+ // and no list that renders unknown as zero.
8
+ //
9
+ // The field is `@hanzo/ui/chat`'s `Composer`, the same component the browser
10
+ // extension's home mounts and the same one `Chat.tsx` and `Work.tsx` already
11
+ // use. Its `children` is the documented toolbar slot — "where a surface puts
12
+ // its ModelSelector, attachment control or mode chips" — so the source chips go
13
+ // THERE and the package needs no change to carry them. The four options above
14
+ // it are this surface's, for the same reason: a home decides what it starts.
15
+ import { dev } from './lib/host.js';
16
+ import { useEffect, useMemo, useState } from 'react';
17
+ import { AtSign, Blocks, Bot, Code2, FolderGit2, Hash, MessageSquare, Plus, UserPlus } from 'lucide-react';
18
+ import { useAgents, usePeople, useSessions, useThreads } from '@hanzo/ai/react';
19
+ import { Composer } from '@hanzo/ui/chat';
20
+ import { Crew } from './crew.js';
21
+ import { Box, Text, XStack, YStack } from '@hanzo/ui';
22
+ import { tap } from './lib/tap.js';
23
+ import { Grid } from '@hanzo/ui/grid';
24
+ import { useIam, useOrganizations } from '@hanzo/iam/react';
25
+ import { hasSession } from './lib/session.js';
26
+ import { useAi } from './lib/ai.js';
27
+ import { useHydrated } from './lib/hydrated.js';
28
+ import { useOpen } from './open.js';
29
+ import { plainly, say } from './failure.js';
30
+ import { Directory } from './Directory.js';
31
+ import { Bar } from './progress.js';
32
+ import { Card, Go, INK, Quiet, Row, SOFT, CHOSEN } from './card.js';
33
+ import { Talk } from './Talk.js';
34
+ import { useMe } from './roster.js';
35
+ import { roomKey, teamKey, useTeamRooms } from './rooms.js';
36
+ import { invite, apps, projects } from './lib/home.js';
37
+ import { BAD } from './lib/mix.js';
38
+ /** What a question can start here. Four, in the order they escalate. */
39
+ const MODES = [
40
+ { id: 'chat', label: 'Chat', asks: 'Ask anything' },
41
+ { id: 'dev', label: 'Dev', asks: 'Describe what to build' },
42
+ { id: 'news', label: 'News', asks: 'What happened?' },
43
+ { id: 'research', label: 'Research', asks: 'What should we dig into?' },
44
+ ];
45
+ /**
46
+ * Where a grounded answer may look, as the `@source` hints the answer engine
47
+ * already reads (`SearchOptions.sources` in `@hanzo/ai`). Chat and Dev do not
48
+ * search, so the chips are drawn only for the two modes that do — and they live
49
+ * in the composer's footer row, whose height the send control already sets, so
50
+ * switching modes moves nothing.
51
+ */
52
+ const SOURCES = ['web', 'news', 'academic', 'github', 'reddit', 'x'];
53
+ const GROUNDS = (mode) => mode === 'news' || mode === 'research';
54
+ /**
55
+ * A ROOM IS READ HERE. Home is the org's room: the rail's channels and direct
56
+ * messages open in this pane (`Talk`), keyed `team:<space>/<id>` in the
57
+ * selection slot and in the address, and the Rooms card below lists them and
58
+ * opens a new one. What other networks carry in stays in the Inbox, under its
59
+ * own key.
60
+ */
61
+ export function Home({ ask }) {
62
+ const [mode, setMode] = useState('chat');
63
+ const [draft, setDraft] = useState('');
64
+ const [picked, setPicked] = useState([]);
65
+ // Connecting an app happens in the Directory, so the home mounts the
66
+ // Directory rather than linking somewhere that is not it. It is a Dialog —
67
+ // closed, it draws nothing — so a second instance beside the shell's costs a
68
+ // component and no pixels, and there is still ONE directory in the codebase.
69
+ const [directory, setDirectory] = useState(false);
70
+ const { room, openRoom } = useOpen();
71
+ const held = useTeamRooms();
72
+ const here = MODES.find((m) => m.id === mode) ?? MODES[0];
73
+ const grounded = GROUNDS(mode);
74
+ const at = roomKey(room);
75
+ if (at)
76
+ return _jsx(Open, { at: at, rooms: held, back: () => openRoom(null) });
77
+ const send = () => {
78
+ const question = draft.trim();
79
+ if (!question)
80
+ return;
81
+ setDraft('');
82
+ const hints = grounded ? picked : [];
83
+ if (ask) {
84
+ ask(question, mode, hints);
85
+ return;
86
+ }
87
+ // The hints ride IN the question, which is what they are: the engine reads
88
+ // `@news` in the query. Appending them to a chat turn would be noise, so
89
+ // only a grounded mode carries them.
90
+ const q = hints.length ? `${question} ${hints.map((s) => `@${s}`).join(' ')}` : question;
91
+ window.location.href = `/chat?q=${encodeURIComponent(q)}`;
92
+ };
93
+ return (_jsxs(YStack, { "data-home": true, flex: 1, minH: 0, width: "100%", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 720, mx: "auto", px: "$4", pt: "$8", gap: "$4", $sm: { pt: '$10' }, children: [_jsx(Text, { render: "h1", fontSize: "$8", fontWeight: "500", color: INK, text: "center", $sm: { fontSize: '$9' }, children: "What are we building?" }), _jsx(XStack, { justify: "center", gap: "$1", flexWrap: "wrap", children: MODES.map((one) => (_jsx(Box, { "data-tab": one.id, render: "button", onClick: () => setMode(one.id), "aria-current": mode === one.id, px: "$3", py: "$2", rounded: "$10", bg: mode === one.id ? CHOSEN : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$3", fontWeight: "500", color: mode === one.id ? INK : SOFT, children: one.label }) }, one.id))) }), _jsx(Composer, { value: draft, onChange: setDraft, onSend: send, placeholder: here.asks, label: here.asks, rows: 2, maxHeight: 240, children: grounded ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", shrink: 1, minW: 0, children: SOURCES.map((s) => {
94
+ const on = picked.includes(s);
95
+ return (_jsx(Box, { "data-chip": s, render: "button", "aria-pressed": on, onClick: () => setPicked((held) => (on ? held.filter((h) => h !== s) : [...held, s])), px: "$2", py: "$1", rounded: "$10", bg: on ? CHOSEN : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsxs(Text, { fontSize: "$2", color: on ? INK : SOFT, children: ["@", s] }) }, s));
96
+ }) })) : null }), _jsx(Crew, { to: "/chat" })] }), _jsx(YStack, { width: "100%", maxW: 880, mx: "auto", px: "$4", py: "$8", children: _jsxs(Grid, { columns: { min: 260, max: 3 }, gap: 12, children: [_jsx(Rooms, { ...held }), _jsx(Projects, {}), _jsx(Chats, {}), _jsx(Next, { onConnect: () => setDirectory(true) }), _jsx(Apps, { onConnect: () => setDirectory(true) }), _jsx(Team, {}), _jsx(Build, {})] }) }), _jsx(Directory, { open: directory, onClose: () => setDirectory(false) })] }));
97
+ }
98
+ /**
99
+ * The open room, in place of the question and the cards.
100
+ *
101
+ * The key names a space and an id; the org's list answers which room that is.
102
+ * The ladder is the one every card climbs: the session first, because that is
103
+ * the state that never resolves, then the refusal, then the read, then a key no
104
+ * room answers to — a shared link to a room this reader cannot see.
105
+ */
106
+ function Open({ at, rooms, back, }) {
107
+ const { isAuthenticated, isLoading } = useIam();
108
+ const found = rooms.rooms?.find((one) => one.space === at.space && one.id === at.id);
109
+ if (found)
110
+ return _jsx(Talk, { room: found }, teamKey(found));
111
+ const reading = isLoading || (isAuthenticated && rooms.rooms === null && !rooms.wrong);
112
+ const line = reading
113
+ ? 'Reading the room.'
114
+ : !isAuthenticated
115
+ ? 'Sign in to read this room.'
116
+ : rooms.wrong
117
+ ? say(rooms.wrong, 'this room')
118
+ : 'That room is not here.';
119
+ return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$3", children: [_jsx(Text, { fontSize: "$3", color: SOFT, children: line }), reading ? null : (_jsx(Box, { render: "button", onClick: back, ...tap, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Back to home" }) }))] }));
120
+ }
121
+ /* ─────────────────────────────────────────────────────────────────────────────
122
+ The cards. Each one owns its own read, so a card that cannot answer says so
123
+ on its own rather than emptying the grid.
124
+ ────────────────────────────────────────────────────────────────────────── */
125
+ /**
126
+ * The org's rooms, and the way to open one.
127
+ *
128
+ * The list is the read the rail makes (`useTeamRooms`), so the two cannot
129
+ * disagree about what the org has. A new room is posted with the reader as its
130
+ * first member — the platform adds nobody on its own — in the space of the
131
+ * org's first room, which is the space an org with one has; an org with none
132
+ * leaves it unsaid. The row the platform answers with is opened at once.
133
+ */
134
+ function Rooms({ rooms, wrong, open }) {
135
+ const { openRoom } = useOpen();
136
+ const me = useMe();
137
+ const [naming, setNaming] = useState(false);
138
+ const [name, setName] = useState('');
139
+ const [opening, setOpening] = useState(false);
140
+ const [refused, setRefused] = useState(null);
141
+ const make = async () => {
142
+ const wanted = name.trim();
143
+ if (!wanted || opening || !me)
144
+ return;
145
+ setOpening(true);
146
+ setRefused(null);
147
+ try {
148
+ const made = await open({ name: wanted, space: rooms?.[0]?.space, members: [me] });
149
+ setName('');
150
+ setNaming(false);
151
+ openRoom(teamKey(made));
152
+ }
153
+ catch (e) {
154
+ setRefused(plainly(e, 'this room'));
155
+ }
156
+ finally {
157
+ setOpening(false);
158
+ }
159
+ };
160
+ return (_jsxs(Card, { kind: "rooms", icon: _jsx(Hash, { size: 14, "aria-hidden": true }), title: "Rooms", children: [rooms === null ? (_jsx(Waiting, { wrong: wrong, subject: "your rooms" })) : rooms.length === 0 ? (_jsx(Quiet, { children: "No rooms yet. Open one and the conversation has somewhere to live." })) : (_jsx(YStack, { gap: "$1", children: rooms.slice(0, 6).map((one) => (_jsx(Row, { icon: one.direct ? _jsx(AtSign, { size: 12, "aria-hidden": true }) : _jsx(Hash, { size: 12, "aria-hidden": true }), onPress: () => openRoom(teamKey(one)), children: one.name || 'Direct message' }, teamKey(one)))) })), !me ? null : naming ? (_jsxs(YStack, { gap: "$1", children: [_jsxs(XStack, { gap: "$1", items: "center", children: [_jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
161
+ if (e.key === 'Enter')
162
+ void make();
163
+ if (e.key === 'Escape')
164
+ setNaming(false);
165
+ }, placeholder: "Name", "aria-label": "Room name", autoFocus: true }), flex: 1, minW: 0, bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 6, px: 8, py: 6, color: "inherit", fontSize: "$2" }), _jsx(Box, { render: "button", onClick: () => void make(), "aria-disabled": opening, opacity: opening ? 0.5 : 1, px: "$2", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Open it" }) })] }), refused ? (_jsx(Text, { fontSize: "$2", color: BAD, children: refused })) : null] })) : (_jsx(Row, { icon: _jsx(Plus, { size: 12, "aria-hidden": true }), onPress: () => setNaming(true), children: "New room" }))] }));
166
+ }
167
+ /**
168
+ * A read the SDK has no hook for.
169
+ *
170
+ * `null` while the answer is not in — including when the request failed, which
171
+ * is the case a `[]` would misreport as "the org has none".
172
+ *
173
+ * The reader is passed by REFERENCE (`lib/home.ts`'s own functions), not built
174
+ * in the caller, so the effect's dependency is honest and it runs once per
175
+ * client rather than once per render.
176
+ */
177
+ function useRead(read) {
178
+ const { client } = useAi();
179
+ const [held, setHeld] = useState(null);
180
+ // WHY THERE IS NOTHING, kept rather than swallowed. `null` alone says only
181
+ // that no answer is held, which is equally true of a request still in flight
182
+ // and one that came back no — and a card cannot choose its sentence without
183
+ // knowing which.
184
+ const [wrong, setWrong] = useState(null);
185
+ useEffect(() => {
186
+ if (!client)
187
+ return;
188
+ const stop = new AbortController();
189
+ void read(client, stop.signal)
190
+ .then((got) => {
191
+ if (!stop.signal.aborted)
192
+ setHeld(got);
193
+ })
194
+ .catch((e) => {
195
+ if (!stop.signal.aborted)
196
+ setWrong(e);
197
+ });
198
+ return () => stop.abort();
199
+ }, [client, read]);
200
+ return { held, wrong };
201
+ }
202
+ /**
203
+ * WHAT A CARD SAYS BEFORE IT HAS ANYTHING.
204
+ *
205
+ * Three states wore one word. "Reading." is true only while an answer is on its
206
+ * way — a reader with no session will never be sent one, and a store that
207
+ * refused has already answered. Measured on production: Projects, Apps and Team
208
+ * all still said "Reading." twelve seconds after load, to a visitor nobody was
209
+ * ever going to answer.
210
+ *
211
+ * The session is asked FIRST, because that is the state that never resolves.
212
+ */
213
+ function Waiting({ wrong, subject }) {
214
+ const { isAuthenticated } = useIam();
215
+ const signedIn = isAuthenticated || hasSession();
216
+ if (!signedIn)
217
+ return _jsxs(Quiet, { children: ["Sign in to see ", subject, "."] });
218
+ if (wrong)
219
+ return _jsx(Quiet, { children: say(wrong, subject) });
220
+ return _jsx(Quiet, { children: "Reading." });
221
+ }
222
+ const useProjects = () => useRead(projects);
223
+ const useApps = () => useRead(apps);
224
+ function Projects() {
225
+ const { held, wrong } = useProjects();
226
+ return (_jsxs(Card, { kind: "projects", icon: _jsx(FolderGit2, { size: 14, "aria-hidden": true }), title: "Projects", children: [held === null ? (_jsx(Waiting, { wrong: wrong, subject: "your projects" })) : held.length === 0 ? (_jsx(Quiet, { children: "Nothing deployed yet." })) : (_jsx(YStack, { gap: "$1", children: held.slice(0, 3).map((p) => (_jsx(Row, { href: p.liveUrl, children: p.name || p.slug }, p.slug))) })), _jsx(Go, { href: dev(), children: "Ship one" })] }));
227
+ }
228
+ function Chats() {
229
+ const { threads, loading } = useThreads();
230
+ const { open } = useOpen();
231
+ return (_jsx(Card, { kind: "chats", icon: _jsx(MessageSquare, { size: 14, "aria-hidden": true }), title: "Chats", children: loading && threads.length === 0 ? (_jsx(Waiting, { subject: "your chats" })) : threads.length === 0 ? (_jsx(Quiet, { children: "No chats yet." })) : (_jsx(YStack, { gap: "$1", children: threads.slice(0, 3).map((t) => (_jsx(Row, { onPress: () => open(t.id), children: t.title || 'Untitled' }, t.id))) })) }));
232
+ }
233
+ /**
234
+ * What is going, then what to do next.
235
+ *
236
+ * Both halves are DERIVED — every line is a fact this page measured, and it
237
+ * names the fact rather than a recommendation nobody can check. A running run
238
+ * carries its own bar; see `progress.tsx` for why that bar is usually invisible
239
+ * today.
240
+ */
241
+ function Next({ onConnect }) {
242
+ const { sessions } = useSessions({ limit: 100 });
243
+ const { agents } = useAgents();
244
+ const { held } = useProjects();
245
+ const { held: connected } = useApps();
246
+ const safeSessions = Array.isArray(sessions) ? sessions : [];
247
+ const safeAgents = Array.isArray(agents) ? agents : [];
248
+ const running = useMemo(() => safeSessions.filter((s) => s && s.status === 'running'), [safeSessions]);
249
+ const rows = useMemo(() => {
250
+ const out = [];
251
+ const failed = safeSessions.filter((s) => s && s.status === 'error').length;
252
+ if (failed)
253
+ out.push({ key: 'failed', say: `${failed} failed`, href: '/work' });
254
+ if (safeAgents.length === 0)
255
+ out.push({ key: 'agent', say: 'Hire your first agent', href: '/work' });
256
+ if (held?.length === 0)
257
+ out.push({ key: 'ship', say: 'Ship your first project', href: dev() });
258
+ if (connected && Array.isArray(connected) && !connected.some((a) => a?.connected))
259
+ out.push({ key: 'connect', say: 'Connect an app', press: true });
260
+ return out;
261
+ }, [safeSessions, safeAgents, held, connected]);
262
+ return (_jsxs(Card, { kind: "next", icon: _jsx(Bot, { size: 14, "aria-hidden": true }), title: "Next", children: [running.length ? (_jsx(YStack, { gap: "$2", children: running.slice(0, 2).map((s) => (_jsxs(YStack, { gap: "$1", children: [_jsx(Row, { href: "/work", children: s.title || s.agent || 'Run' }), _jsx(Bar, { run: s, label: false })] }, s.id))) })) : null, rows.length === 0 && running.length === 0 ? (_jsx(Quiet, { children: "Nothing needs you." })) : (_jsx(YStack, { gap: "$1", children: rows.slice(0, 3).map((r) => (_jsx(Row, { href: r.href, onPress: r.press ? onConnect : undefined, children: r.say }, r.key))) }))] }));
263
+ }
264
+ /**
265
+ * The apps the org has connected.
266
+ *
267
+ * A connected provider has no page on this site — `/integrations/<slug>` is the
268
+ * SDK guide catalogue (openai-sdk, langchain, cursor …) and shares no ids with
269
+ * the cloud's connectors — so a name here is a name, and the ACTION opens the
270
+ * Directory, which is where connecting actually happens.
271
+ */
272
+ function Apps({ onConnect }) {
273
+ const { held, wrong } = useApps();
274
+ const on = held?.filter((a) => a.connected) ?? [];
275
+ return (_jsxs(Card, { kind: "apps", icon: _jsx(Blocks, { size: 14, "aria-hidden": true }), title: "Apps", children: [held === null ? (_jsx(Waiting, { wrong: wrong, subject: "connected apps" })) : on.length === 0 ? (_jsx(Quiet, { children: "Nothing connected." })) : (_jsx(YStack, { gap: "$1", children: on.slice(0, 3).map((a) => (_jsx(Row, { children: a.name }, a.id))) })), _jsx(Go, { onPress: onConnect, children: "Connect" })] }));
276
+ }
277
+ /**
278
+ * THE ONE CARD A PRERENDER CANNOT DRAW.
279
+ *
280
+ * Every other read here starts empty on the server AND on the first client
281
+ * render, so the two agree and hydration is quiet. `useOrganizations` does not:
282
+ * it resolves the org from the SDK synchronously, so the client's first render
283
+ * already knows the reader and the server's never did — a TEXT mismatch, which
284
+ * React answers by throwing the whole tree away and re-rendering it.
285
+ *
286
+ * `useHydrated` is false on the server and false on that first client render,
287
+ * so both draw the same line and the real answer arrives on the render
288
+ * hydration was going to perform anyway.
289
+ */
290
+ function Team() {
291
+ const hydrated = useHydrated();
292
+ const { currentOrgId } = useOrganizations();
293
+ if (!hydrated || !currentOrgId) {
294
+ return (_jsxs(Card, { kind: "team", icon: _jsx(UserPlus, { size: 14, "aria-hidden": true }), title: "Team", children: [_jsx(Waiting, { subject: "your team" }), _jsx(Go, { href: invite(), children: "Invite" })] }));
295
+ }
296
+ return _jsx(Mates, { owner: currentOrgId });
297
+ }
298
+ /** The org's people. Split out because `usePeople` needs an owner and a hook
299
+ * cannot be called conditionally. */
300
+ function Mates({ owner }) {
301
+ const { people, loading } = usePeople({ owner, service: false });
302
+ return (_jsxs(Card, { kind: "team", icon: _jsx(UserPlus, { size: 14, "aria-hidden": true }), title: "Team", children: [loading && people.length === 0 ? (_jsx(Waiting, { subject: "your team" })) : people.length < 2 ? (_jsx(Quiet, { children: "Just you." })) : (_jsxs(Quiet, { children: [people.length, " people"] })), _jsx(Go, { href: invite(), children: "Invite" })] }));
303
+ }
304
+ function Build() {
305
+ return (_jsxs(Card, { kind: "build", icon: _jsx(Code2, { size: 14, "aria-hidden": true }), title: "Build", children: [_jsx(Quiet, { children: "Hanzo Dev writes it, runs it, ships it." }), _jsx(Go, { href: dev(), children: "Open Dev" })] }));
306
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The pane, and the session its reads need.
3
+ *
4
+ * SPLIT so the hooks below never run for a reader with none: every route this
5
+ * pane reads is org-scoped and answers 403 without one, and a hook cannot be
6
+ * called conditionally — so the condition is a component boundary rather than
7
+ * an `if` the rules forbid.
8
+ */
9
+ export declare function Inbox(): import("react").JSX.Element;