@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/Orgs.js ADDED
@@ -0,0 +1,582 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The organization comes first.
4
+ //
5
+ // An organization owns every agent, credit and member. A signed-in reader who
6
+ // has none is shown one thing: make one. A reader with several picks the one
7
+ // they are working in when they arrive. And an organization whose plan is not
8
+ // live is asked for it before any room is drawn. Only past all three does the
9
+ // workspace render, and every request under it then carries that organization
10
+ // (lib/auth/session.ts `scope`, hooks/useAi.ts).
11
+ //
12
+ // FREE IS FOR ONE PERSON. A personal organization on the Free tier opens /chat
13
+ // and /dev on every host but hanzo.team; the workspace's own host asks every
14
+ // organization for a paid plan. A personal organization starts on Free and may
15
+ // upgrade at creation; a team organization is sold the per-seat Team plan, and
16
+ // that is the plan the subscribe ask offers. Plans are the catalog's, read live
17
+ // and seeded from @hanzo/plans for first paint. Checkout is hanzo.ai/pay in
18
+ // this tab; it returns here, and the tier is read again on return.
19
+ //
20
+ // THE FRONT DOOR. A workspace rooted at `/` (hanzo.team) draws its host's
21
+ // Landing for a stranger at Home (`front`) and its host's Signup at /start
22
+ // (`signup`). Behind either, a reader with no team names one, invites people
23
+ // and takes the per-seat plan (`Start`, `Plan`) instead of the create dialog
24
+ // and the subscribe ask; a personal organization on Free is offered a team,
25
+ // and an open one is sent on (`forward`). Without those props nothing here
26
+ // changes.
27
+ import { dev } from './lib/host.js';
28
+ import { useCallback, useEffect, useMemo, useState } from 'react';
29
+ import { useIam } from '@hanzo/iam/react';
30
+ import { Text, View, XStack, YStack } from '@hanzo/gui';
31
+ import { Button } from '@hanzo/ui';
32
+ import { Minus, Plus, X } from 'lucide-react';
33
+ import { Waiting } from './waiting.js';
34
+ import { LOGIN, enter } from './lib/destination.js';
35
+ import { useAi } from './lib/ai.js';
36
+ import { useHost } from './lib/hostname.js';
37
+ import { useHydrated } from './lib/hydrated.js';
38
+ import { api } from './lib/api.js';
39
+ import { usePersonal, useTier } from './lib/tier.js';
40
+ import { ORG, PICKED, assumed, hasSession, org, orgs as claimed, pick, renew, scope } from './lib/session.js';
41
+ import { team as workspace } from './lib/host.js';
42
+ import { cartUrl, charge, checkoutUrl, money, priced, usePlan, usePlans } from './lib/plans.js';
43
+ import { createInvitation, inviteLink } from './lib/invite.js';
44
+ import { countSignup } from './lib/intent.js';
45
+ import { BAD, mix } from './lib/mix.js';
46
+ import { useRooms } from './host.js';
47
+ import { site, where } from './where.js';
48
+ /** A template to set up once the organization is live. */
49
+ const SETUP = 'hanzo:org:template';
50
+ /** Checkout for a plan, coming back to this room. */
51
+ function checkout(plan) {
52
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'https://hanzo.ai';
53
+ const path = typeof window !== 'undefined' ? window.location.pathname : '/chat';
54
+ return checkoutUrl(plan.id, `${origin}${path}`);
55
+ }
56
+ export function Orgs({ children, front = false, forward, signup = false, }) {
57
+ const hydrated = useHydrated();
58
+ const { isAuthenticated, isLoading, accessToken } = useIam();
59
+ const { Landing, Signup } = where();
60
+ // The front door's own flow: naming a team, inviting it, the per-seat plan.
61
+ const door = front || signup;
62
+ const arriving = useArrival(door, hydrated, isLoading, isAuthenticated);
63
+ const members = useMemo(() => claimed(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
64
+ const [current, setCurrent] = useState(() => org());
65
+ useEffect(() => {
66
+ setCurrent(org());
67
+ const on = () => setCurrent(org());
68
+ window.addEventListener(ORG, on);
69
+ window.addEventListener('storage', on);
70
+ return () => {
71
+ window.removeEventListener(ORG, on);
72
+ window.removeEventListener('storage', on);
73
+ };
74
+ }, [accessToken]);
75
+ const [picked, setPicked] = useState(false);
76
+ // The team being set up in this page. Once its name is taken the setup owns
77
+ // the screen until checkout, whatever `Orgs` reads of the new organization.
78
+ const [founding, setFounding] = useState(null);
79
+ const { tier, wrong, reload } = useTier(Boolean(isAuthenticated && current), current);
80
+ // A plan is live when billing READ one for this organization and it is not the free rung.
81
+ const live = Boolean(tier?.tier && tier.tier.name !== FREE_TIER);
82
+ // A Free organization off the workspace's host is asked what kind it is, and a
83
+ // personal one opens: Free is the plan a person uses the app on alone. Behind
84
+ // the front door every organization without a live plan is read for its kind:
85
+ // a team is offered the plan, a personal one is offered a team.
86
+ const onTeam = workspace(useHost());
87
+ const free = door ? Boolean(tier) && !live : !live && !onTeam && tier?.tier?.name === FREE_TIER;
88
+ const personal = usePersonal(current, free);
89
+ // A SuperAdmin IAM stepped into this organization sees its rooms whatever its
90
+ // plan: support is there to see what the organization has, and what it spends
91
+ // is billed to the operator's own books, never the organization's.
92
+ const supporting = Boolean(current) && assumed() === current;
93
+ const open = live || (!door && free && personal === true) || supporting;
94
+ const { client } = useAi();
95
+ useEffect(() => {
96
+ try {
97
+ setPicked(Boolean(localStorage.getItem(PICKED)));
98
+ }
99
+ catch {
100
+ setPicked(true);
101
+ }
102
+ }, [accessToken]);
103
+ // Back from checkout: the plan may be live now.
104
+ useEffect(() => {
105
+ const back = () => {
106
+ if (document.visibilityState === 'visible')
107
+ reload();
108
+ };
109
+ document.addEventListener('visibilitychange', back);
110
+ window.addEventListener('focus', back);
111
+ return () => {
112
+ document.removeEventListener('visibilitychange', back);
113
+ window.removeEventListener('focus', back);
114
+ };
115
+ }, [reload]);
116
+ const choose = useCallback((o) => {
117
+ pick(o);
118
+ setPicked(true);
119
+ }, []);
120
+ // The organization a link arrives for. hanzo.ai opens the workspace at
121
+ // `?signin&org=<the org it is working in>` (Me.tsx); once signed in, an org
122
+ // the token's `orgs` names is chosen and the parameter leaves the address, the
123
+ // way `?signin` does. An org the token does not name is ignored.
124
+ useEffect(() => {
125
+ if (!door || !hydrated || isLoading || !isAuthenticated)
126
+ return;
127
+ const url = new URL(window.location.href);
128
+ const asked = url.searchParams.get('org');
129
+ if (asked === null)
130
+ return;
131
+ url.searchParams.delete('org');
132
+ window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
133
+ if (members.includes(asked))
134
+ choose(asked);
135
+ }, [door, hydrated, isLoading, isAuthenticated, members, choose]);
136
+ // One organization is not a choice.
137
+ useEffect(() => {
138
+ if (members.length === 1 && !picked)
139
+ choose(members[0]);
140
+ }, [members, picked, choose]);
141
+ // The template chosen at creation, set up once the organization opens.
142
+ useEffect(() => {
143
+ if (!open || !client)
144
+ return;
145
+ let slug;
146
+ try {
147
+ slug = localStorage.getItem(SETUP);
148
+ if (slug)
149
+ localStorage.removeItem(SETUP);
150
+ }
151
+ catch {
152
+ return;
153
+ }
154
+ if (!slug)
155
+ return;
156
+ client.http
157
+ .json({ method: 'POST', path: '/v1/projects/fork', body: { slug } })
158
+ .then((made) => {
159
+ if (made?.id)
160
+ window.location.assign(dev());
161
+ })
162
+ .catch(() => { });
163
+ }, [open, client]);
164
+ // NOTHING PRODUCT-SHAPED IS IN THE EXPORTED MARKUP. This branch is what the
165
+ // static file holds and what hydration's first render must agree with, so
166
+ // whatever it returns is what a reader sees before any answer exists. It used
167
+ // to return the room, which put the whole paid interface on disk: the browser
168
+ // painted it from the file, the SDK settled, billing answered, and only then
169
+ // did the ask appear over the top of a product the reader had already been
170
+ // shown. The veil is the same element in both renders, so there is no
171
+ // mismatch and there is nothing to see. Home at `/` ships the host's Landing
172
+ // instead, which is what a stranger there is shown and what a crawler reads.
173
+ if (!hydrated)
174
+ return front && Landing ? _jsx(Landing, {}) : _jsx(Veil, {});
175
+ // Past hydration the room keeps its place in the tree in every branch that
176
+ // draws it — the veil goes AFTER it — so a token refresh mid-session never
177
+ // remounts what the reader had open. The veil is unconditional now: it was
178
+ // hung only when a token was already in hand, which left a visitor arriving
179
+ // without one looking at the room while the SDK decided.
180
+ if (isLoading)
181
+ return front && Landing && !hasSession() && !arriving ? _jsx(Landing, {}) : _jsxs(_Fragment, { children: [children, _jsx(Veil, {})] });
182
+ if (arriving && !isAuthenticated)
183
+ return _jsx(SignIn, { to: LOGIN });
184
+ if (!isAuthenticated)
185
+ return front && Landing ? _jsx(Landing, {}) : signup && Signup ? _jsx(Signup, {}) : _jsx(SignIn, {});
186
+ if (founding)
187
+ return _jsx(Start, { founded: founding, onFounded: setFounding, first: members.length === 0 });
188
+ if (members.length === 0) {
189
+ if (front && Landing)
190
+ return _jsx(Landing, { member: true });
191
+ return door ? _jsx(Start, { onFounded: setFounding, first: true }) : _jsx(Create, {});
192
+ }
193
+ if (!current || (members.length > 1 && !picked))
194
+ return _jsx(Pick, { members: members, onPick: choose });
195
+ // NOTHING OPENS WITHOUT A LIVE PLAN. Every branch here used to hand the room
196
+ // over when the answer was anything but a plain "free": an error of any kind,
197
+ // a read still in flight, a document with no plan in it. Each was a way in
198
+ // without paying — a refusal, a slow network, or devtools blocking one request
199
+ // all read as permission. The default is now the ask, and the room is the
200
+ // exception rather than the fallback. The one exception to the plan is a
201
+ // personal organization on Free, off hanzo.team, and its kind is read first.
202
+ if (!tier && !wrong)
203
+ return _jsxs(_Fragment, { children: [children, _jsx(Veil, {})] });
204
+ if (free && personal === null)
205
+ return _jsxs(_Fragment, { children: [children, _jsx(Veil, {})] });
206
+ if (open)
207
+ return forward ? _jsx(Forward, { to: forward }) : _jsx(_Fragment, { children: children });
208
+ const swap = () => setPicked(false);
209
+ if (door) {
210
+ // A billing OUTAGE is ours; the plan is still the condition.
211
+ if (!tier)
212
+ return _jsx(Plan, { org: current, members: members, onSwitch: swap, outage: outage(wrong), onRetry: reload });
213
+ if (personal) {
214
+ if (front && Landing)
215
+ return _jsx(Landing, { member: true });
216
+ return _jsx(Start, { onFounded: setFounding, first: false, members: members, onSwitch: swap });
217
+ }
218
+ return _jsx(Plan, { org: current, members: members, onSwitch: swap });
219
+ }
220
+ // A billing OUTAGE is not a bill: it is ours, and saying "subscribe" to
221
+ // someone who already has would be a lie. It still does not open the room.
222
+ return _jsx(Subscribe, { org: current, members: members, onSwitch: swap, outage: outage(wrong), onRetry: reload });
223
+ }
224
+ /**
225
+ * A link that asks to arrive signed in.
226
+ *
227
+ * A workspace on its own origin keeps its own session, so a person signed in on
228
+ * hanzo.ai arrives there signed out and would meet the landing. hanzo.ai's
229
+ * Workspace row sends `?signin` (lib/host `ENTRY`), and behind the front door
230
+ * that draws the sign-in in place of the landing; the sign-in returns to this
231
+ * page, which opens the workspace. The flag leaves the address at once, so the
232
+ * page the sign-in returns to does not ask again.
233
+ */
234
+ function useArrival(door, hydrated, isLoading, isAuthenticated) {
235
+ const [arriving, setArriving] = useState(false);
236
+ useEffect(() => {
237
+ if (!door || !hydrated)
238
+ return;
239
+ const url = new URL(window.location.href);
240
+ if (!url.searchParams.has('signin'))
241
+ return;
242
+ url.searchParams.delete('signin');
243
+ window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
244
+ setArriving(true);
245
+ }, [door, hydrated]);
246
+ useEffect(() => {
247
+ if (arriving && !isLoading && isAuthenticated)
248
+ setArriving(false);
249
+ }, [arriving, isLoading, isAuthenticated]);
250
+ return arriving;
251
+ }
252
+ // ── the screens ────────────────────────────────────────────────────────────────
253
+ /** Every screen here is one sheet over the room, ten above its modals, and the create dialog ten above that.
254
+ * gui carries a `calc()` to the page unchanged; its types only name custom properties. */
255
+ const SHEET = 'calc(var(--z-modal) + 10)';
256
+ const DIALOG = 'calc(var(--z-modal) + 20)';
257
+ /** The sheet's top inset: a share of the window's height, held between two lengths. */
258
+ const TOP = 'clamp(32px, 12vh, 120px)';
259
+ const muted = 'var(--muted-foreground)';
260
+ /** The black that veils and shadows, at a share of itself. */
261
+ const shade = (share) => mix('var(--pure-black)', share, 'srgb');
262
+ /** A field in the create dialog. */
263
+ const FIELD = {
264
+ width: '100%',
265
+ fontSize: '$6',
266
+ px: 16,
267
+ py: 14,
268
+ rounded: 12,
269
+ borderWidth: 1,
270
+ borderColor: 'var(--border)',
271
+ bg: 'var(--background)',
272
+ color: 'var(--foreground)',
273
+ };
274
+ /** The sheet every screen stands on: the whole viewport, scrolling, its column centred. */
275
+ function Page({ label, children }) {
276
+ return (_jsx(YStack, { position: "fixed", inset: 0, z: SHEET, overflowY: "auto", bg: "var(--background)", pt: TOP, px: 20, pb: 48, role: "region", "aria-label": label, children: children }));
277
+ }
278
+ /** The column a screen's words sit in. */
279
+ function Column({ children }) {
280
+ return (_jsx(YStack, { width: "100%", maxW: 640, mx: "auto", children: children }));
281
+ }
282
+ /** A hairline between the parts of a screen. */
283
+ function Rule({ mt = 20, mb = 20 }) {
284
+ return _jsx(View, { height: 1, bg: "var(--border)", mt: mt, mb: mb });
285
+ }
286
+ /** A screen's heading and the line under it; `lead` is the line's leading where it states one. */
287
+ function Heading({ title, lede, lead }) {
288
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { render: "h1", fontSize: "$10", fontWeight: "500", letterSpacing: -0.32, color: "var(--text-primary)", children: title }), _jsx(Text, { render: "p", mt: 6, fontSize: "$4", lineHeight: lead, color: muted, children: lede })] }));
289
+ }
290
+ /** The room, held back until the organization is known. */
291
+ function Veil() {
292
+ return _jsx(View, { "aria-hidden": true, position: "fixed", inset: 0, z: SHEET, bg: "var(--background)" });
293
+ }
294
+ /** An open organization, sent on. */
295
+ function Forward({ to }) {
296
+ const { router } = useRooms();
297
+ useEffect(() => router.replace(to), [router, to]);
298
+ return _jsx(Veil, {});
299
+ }
300
+ /** Which step of the setup this is, of how many. */
301
+ function Step({ at, of }) {
302
+ return (_jsxs(Text, { render: "p", mb: 10, fontSize: "$2", fontFamily: "$mono", color: "$soft", children: ["Step ", at, " of ", of] }));
303
+ }
304
+ function Brand() {
305
+ return (_jsx(Text, { fontSize: "$6", fontWeight: "500", mb: 28, color: "var(--foreground)", children: "Hanzo" }));
306
+ }
307
+ /** The free rung's name in the billing catalog. */
308
+ const FREE_TIER = 'free';
309
+ /**
310
+ * Whether a failed tier read is OUR fault. A 5xx or a network error is an
311
+ * outage; a 401, 403 or 404 is an answer, and an answer that is not a paid plan
312
+ * is not a reason to open the room.
313
+ */
314
+ function outage(wrong) {
315
+ const status = wrong?.status;
316
+ return status == null || status >= 500;
317
+ }
318
+ /**
319
+ * SIGNED OUT IS A TRIP TO SIGN IN, and nothing else.
320
+ *
321
+ * IAM owns every credential, so this room draws no form of its own: it sends
322
+ * the reader to this site's sign-in (destination.ts `enter`), which asks
323
+ * hanzo.id for a session first and draws IAM's form only when there is none.
324
+ * `Room` wrote the address down on mount, so the sign-in returns to this room,
325
+ * `?q=` included.
326
+ */
327
+ function SignIn({ to }) {
328
+ useEffect(() => enter(to), [to]);
329
+ return (_jsx(Page, { label: "Sign in", children: _jsx(Waiting, { title: "Signing you in", lede: "One moment." }) }));
330
+ }
331
+ /** No organization yet: the one thing to do. */
332
+ function Create() {
333
+ const [open, setOpen] = useState(false);
334
+ const [details, setDetails] = useState(false);
335
+ return (_jsxs(Page, { label: "Organizations", children: [_jsxs(Column, { children: [_jsx(Brand, {}), _jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 16, flexWrap: "wrap", children: [_jsxs(YStack, { children: [_jsx(Text, { render: "h1", fontSize: "$10", fontWeight: "500", letterSpacing: -0.32, color: "var(--text-primary)", children: "Organizations" }), _jsx(Text, { render: "p", mt: 6, fontSize: "$4", color: muted, children: "You do not belong to one yet." })] }), _jsxs(Button, { variant: "primary", size: "lg", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 16, "aria-hidden": true }), "New organization"] })] }), _jsx(Rule, {}), _jsxs(YStack, { items: "center", py: 28, children: [_jsx(Text, { render: "p", fontSize: "$6", fontWeight: "500", color: "var(--foreground)", text: "center", children: "No organizations yet" }), _jsx(Button, { variant: "linkMuted", size: "sm", mt: 8, onClick: () => setDetails((d) => !d), children: details ? '- Details' : '+ Details' }), details ? (_jsx(Text, { render: "p", mt: 12, maxW: 440, fontSize: "$4", color: muted, lineHeight: 23.25, text: "center", children: "An organization owns every agent, credit and member. Create one and you are its owner." })) : null] }), _jsx(Rule, {})] }), open ? _jsx(CreateDialog, { onClose: () => setOpen(false) }) : null] }));
336
+ }
337
+ /** Name it, pick what it runs and its plan: a paid plan goes to checkout, Free goes straight in. */
338
+ function CreateDialog({ onClose }) {
339
+ const team = usePlan('team');
340
+ // The personal ladder, Free first. hanzo.team offers no personal organization:
341
+ // every room there runs on a paid plan.
342
+ const ladder = usePlans('personal');
343
+ const alone = !workspace(useHost());
344
+ const [rung, setRung] = useState(null);
345
+ const chosen = ladder.find((p) => p.id === rung) ?? ladder.find((p) => !p.priceMonthly) ?? ladder[0] ?? null;
346
+ const personal = priced(chosen);
347
+ const [name, setName] = useState('');
348
+ const [kind, setKind] = useState('org');
349
+ // A team organization runs on the per-seat Team plan; a personal one on the
350
+ // rung chosen below, Free unless another is. Checkout sets the seats and the term.
351
+ const { plan, price } = kind === 'org' ? team : personal;
352
+ const seat = plan?.pricePerUser ? ' / seat' : '';
353
+ const paid = (plan?.priceMonthly ?? 0) > 0;
354
+ // A row on the landing carries its template in the address, across sign-in.
355
+ const [template, setTemplate] = useState(() => {
356
+ if (typeof window === 'undefined')
357
+ return '';
358
+ return new URLSearchParams(window.location.search).get('template') ?? '';
359
+ });
360
+ const [templates, setTemplates] = useState([]);
361
+ const [busy, setBusy] = useState(false);
362
+ const [refused, setRefused] = useState(null);
363
+ useEffect(() => {
364
+ fetch(`${api()}/v1/templates`)
365
+ .then((r) => (r.ok ? r.json() : null))
366
+ .then((d) => {
367
+ if (Array.isArray(d?.data))
368
+ setTemplates(d.data.filter((t) => t?.slug));
369
+ })
370
+ .catch(() => { });
371
+ }, []);
372
+ useEffect(() => {
373
+ const key = (e) => {
374
+ if (e.key === 'Escape')
375
+ onClose();
376
+ };
377
+ document.addEventListener('keydown', key);
378
+ return () => document.removeEventListener('keydown', key);
379
+ }, [onClose]);
380
+ const submit = async (e) => {
381
+ e.preventDefault();
382
+ if (!plan || busy)
383
+ return;
384
+ if (kind === 'org' && !name.trim()) {
385
+ setRefused('Give it a name.');
386
+ return;
387
+ }
388
+ setBusy(true);
389
+ setRefused(null);
390
+ try {
391
+ const r = await fetch(`${api()}/v1/account/orgs`, {
392
+ method: 'POST',
393
+ headers: { 'Content-Type': 'application/json', ...scope() },
394
+ body: JSON.stringify(kind === 'org' ? { name: name.trim() } : { personal: true }),
395
+ });
396
+ if (!r.ok) {
397
+ let why = `Could not create it (${r.status}).`;
398
+ try {
399
+ const body = (await r.json());
400
+ why = body.detail || body.error || body.message || why;
401
+ }
402
+ catch {
403
+ /* no body worth showing */
404
+ }
405
+ setRefused(why);
406
+ setBusy(false);
407
+ return;
408
+ }
409
+ const made = (await r.json());
410
+ try {
411
+ if (template)
412
+ localStorage.setItem(SETUP, template);
413
+ localStorage.setItem(PICKED, '1');
414
+ }
415
+ catch {
416
+ /* the next load re-derives both */
417
+ }
418
+ if (made?.org)
419
+ pick(made.org);
420
+ // Only a reader with no organization is offered this form, so its first
421
+ // success is the account becoming a customer: the ONE sign-up conversion,
422
+ // Free included — our signup_completed, GA4's sign_up, Meta's
423
+ // CompleteRegistration (lib/analytics/tags.ts).
424
+ countSignup();
425
+ // The token in hand predates the organization; the next load mints one
426
+ // that carries it. A paid plan is bought first, and checkout charges the
427
+ // organization it names; Free goes straight in.
428
+ renew();
429
+ if (paid)
430
+ window.location.assign(checkout(plan));
431
+ else
432
+ window.location.reload();
433
+ }
434
+ catch (err) {
435
+ setRefused(err instanceof Error ? err.message : 'Could not create it.');
436
+ setBusy(false);
437
+ }
438
+ };
439
+ return (_jsx(YStack, { role: "dialog", "aria-modal": true, "aria-label": "Create an organization", position: "fixed", inset: 0, z: DIALOG, bg: shade(28), overflowY: "auto", py: "6vh", px: 16, onClick: onClose, children: _jsxs(YStack, { render: "form", onSubmit: submit, onClick: (e) => e.stopPropagation(), width: "100%", maxW: 640, mx: "auto", bg: "var(--background)", rounded: 16, boxShadow: `0 24px 64px ${shade(28)}`, pt: 28, px: 28, pb: 20, children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsx(Text, { render: "h2", fontSize: "$8", fontWeight: "500", color: "var(--text-primary)", children: "Create an organization" }), _jsx(Text, { render: "p", mt: 8, fontSize: "$4", color: muted, lineHeight: 22.5, children: "You are its owner. Agents, credits and members all hang off it; you can switch between organizations from the rail." })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(View, { render: "fieldset", display: "block", borderWidth: 0, p: 0, m: 0, mt: 22, children: [_jsx(Text, { render: "legend", display: "block", fontSize: "$4", fontWeight: "500", mb: 10, color: "var(--foreground)", children: "Plan" }), _jsx(View, { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 10, children: [
440
+ ['org', 'Organization', 'A team. Invite members, share agents and credit.', team],
441
+ ['personal', 'Personal', 'Just you. Named after your account.', personal],
442
+ ].filter(([k]) => alone || k === 'org').map(([k, label, hint, offer]) => (_jsxs(XStack, { render: "label", gap: 10, items: "flex-start", px: 14, py: 12, rounded: 12, borderWidth: 1, borderColor: kind === k ? 'var(--foreground)' : 'var(--border)', cursor: "pointer", children: [_jsx("input", { type: "radio", name: "kind", value: k, checked: kind === k, onChange: () => setKind(k) }), _jsxs(YStack, { shrink: 1, children: [_jsxs(Text, { fontSize: "$4", fontWeight: "500", color: "var(--foreground)", children: [label, offer.plan ? (_jsxs(Text, { color: muted, fontWeight: "400", children: [' ', "\u2014 ", offer.plan.priceMonthly ? `${offer.price}${offer.plan.pricePerUser ? ' / seat' : ''} / month` : offer.plan.name] })) : null] }), _jsx(Text, { fontSize: "$3", color: muted, mt: 2, children: hint })] })] }, k))) })] }), kind === 'personal' && ladder.length > 1 ? (_jsxs(View, { render: "fieldset", display: "block", borderWidth: 0, p: 0, m: 0, mt: 20, children: [_jsx(Text, { render: "legend", display: "block", fontSize: "$4", fontWeight: "500", mb: 10, color: "var(--foreground)", children: "Personal plan" }), _jsx(View, { display: "grid", gap: 8, children: ladder.map((p) => (_jsxs(XStack, { render: "label", gap: 10, items: "center", px: 14, py: 10, rounded: 12, borderWidth: 1, borderColor: chosen?.id === p.id ? 'var(--foreground)' : 'var(--border)', cursor: "pointer", children: [_jsx("input", { type: "radio", name: "rung", value: p.id, checked: chosen?.id === p.id, onChange: () => setRung(p.id) }), _jsx(Text, { fontSize: "$4", fontWeight: "500", color: "var(--foreground)", children: p.name }), p.priceMonthly ? (_jsxs(Text, { ml: "auto", fontSize: "$3", color: muted, children: [priced(p).price, " / month"] })) : null] }, p.id))) })] })) : null, kind === 'org' ? (_jsxs(View, { render: "label", display: "block", mt: 20, children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "var(--foreground)", children: "Name" }), _jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), placeholder: "Acme", autoFocus: true }), ...FIELD }), _jsx(Text, { display: "block", fontSize: "$3", color: muted, mt: 8, children: "What the rail and every invoice call it." })] })) : null, templates.length > 0 ? (_jsxs(View, { render: "label", display: "block", mt: 20, children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "var(--foreground)", children: "Template" }), _jsxs(Text, { render: _jsx("select", { value: template, onChange: (e) => setTemplate(e.target.value) }), ...FIELD, children: [_jsx("option", { value: "", children: "None \u2014 start empty" }), templates.map((t) => (_jsxs("option", { value: t.slug, children: [t.title || t.name || t.slug, t.category ? ` · ${t.category}` : ''] }, t.slug)))] }), _jsx(Text, { display: "block", fontSize: "$3", color: muted, mt: 8, children: "What it runs, set up as soon as the organization opens. Choose none to start empty." })] })) : null, plan && !paid ? (_jsxs(Text, { render: "p", mt: 22, fontSize: "$4", color: muted, lineHeight: 23.25, children: ["A personal organization starts on ", plan.name, " and opens as soon as it is made. Choose a paid plan above to go to checkout instead."] })) : plan ? (_jsxs(Text, { render: "p", mt: 22, fontSize: "$4", color: muted, lineHeight: 23.25, children: [plan.pricePerUser
443
+ ? `A team organization runs on ${plan.name} at ${price} per seat a month, at least ${Number(plan.limits?.minSeats ?? 1)} seats.`
444
+ : `A personal organization runs on ${plan.name} at ${price} a month.`, ' ', "Nothing in it works until that plan is live, so creating one takes you straight to checkout, where you choose the seats and the term.", ' ', "Each plan includes session and daily usage limits; usage never draws your credit balance.", ' ', "Payment is on the provider's hosted page."] })) : null, refused ? (_jsx(Text, { render: "p", role: "alert", mt: 14, fontSize: "$3", color: "var(--foreground)", children: refused })) : null, _jsxs(XStack, { justify: "flex-end", gap: 10, mt: 24, flexWrap: "wrap", children: [_jsx(Button, { type: "button", size: "lg", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", size: "lg", shrink: 1, py: "$2", disabled: !plan || busy, children: _jsx(Text, { fontFamily: "$body", fontSize: "$3", color: "inherit", text: "center", children: busy ? 'Creating…' : paid ? `Create and subscribe — ${price}${seat} / month` : 'Create' }) })] }), _jsx(Rule, { mt: 18, mb: 8 }), _jsx(Text, { render: "p", text: "right", fontSize: "$2", color: muted, fontFamily: "$mono", children: "Esc to close" })] }) }));
445
+ }
446
+ /** Several organizations: which one, this time. */
447
+ function Pick({ members, onPick }) {
448
+ return (_jsx(Page, { label: "Choose an organization", children: _jsxs(Column, { children: [_jsx(Brand, {}), _jsx(Heading, { title: "Choose an organization", lede: _jsx(_Fragment, { children: "Everything you open \u2014 chats, agents, credit \u2014 is this one's." }) }), _jsx(Rule, {}), _jsx(View, { display: "grid", gap: 10, children: members.map((o) => (_jsx(View, { render: "button", onClick: () => onPick(o), items: "flex-start", px: 16, py: 14, rounded: 12, borderWidth: 1, borderColor: "var(--border)", bg: "transparent", cursor: "pointer", children: _jsx(Text, { fontSize: "$4", color: "var(--foreground)", text: "left", children: o }) }, o))) })] }) }));
449
+ }
450
+ /** The organization exists and its plan is not live: it is offered the per-seat Team plan. */
451
+ function Subscribe({ org, members, onSwitch, outage, onRetry, }) {
452
+ const { plan, price } = usePlan('team');
453
+ const seat = plan?.pricePerUser ? ' / seat' : '';
454
+ return (_jsx(Page, { label: "Subscribe", children: _jsxs(Column, { children: [_jsx(Brand, {}), _jsx(Heading, { title: org, lead: 22.5, lede: plan ? `This organization runs on the ${plan.name} plan at ${price}${seat} / month, and nothing in it works until that plan is live.` : 'Its plan is not live yet.' }), _jsx(Rule, {}), _jsxs(XStack, { gap: 10, flexWrap: "wrap", children: [plan ? (_jsx(Button, { asChild: true, variant: "primary", size: "lg", children: _jsx("a", { href: checkout(plan), children: `Subscribe — ${price}${seat} / month` }) })) : null, members.length > 1 ? (_jsx(Button, { size: "lg", onClick: onSwitch, children: "Switch organization" })) : null] })] }) }));
455
+ }
456
+ // ── the front door's setup ─────────────────────────────────────────────────────
457
+ /**
458
+ * CREATE YOUR TEAM: its name, then who else is in it, then its plan.
459
+ *
460
+ * The name makes the organization (`POST /v1/account/orgs`). The access token
461
+ * in hand predates it, so it is expired and refreshed at once, which mints one
462
+ * that carries the new organization, and the organization is selected: the
463
+ * invitations that follow are filed against it. Invitations are optional, one
464
+ * single-use link per address. The plan step is `Plan`, the same one a team on
465
+ * Free is shown.
466
+ */
467
+ function Start({ founded = null, onFounded, first, members = [], onSwitch, }) {
468
+ const { sdk } = useIam();
469
+ const [step, setStep] = useState(founded ? 'invite' : 'name');
470
+ const [name, setName] = useState('');
471
+ const [busy, setBusy] = useState(false);
472
+ const [refused, setRefused] = useState(null);
473
+ const [emails, setEmails] = useState('');
474
+ const [sent, setSent] = useState([]);
475
+ const create = async (e) => {
476
+ e.preventDefault();
477
+ if (busy)
478
+ return;
479
+ if (!name.trim()) {
480
+ setRefused('Give it a name.');
481
+ return;
482
+ }
483
+ setBusy(true);
484
+ setRefused(null);
485
+ try {
486
+ const r = await fetch(`${api()}/v1/account/orgs`, {
487
+ method: 'POST',
488
+ headers: { 'Content-Type': 'application/json', ...scope() },
489
+ body: JSON.stringify({ name: name.trim() }),
490
+ });
491
+ if (!r.ok) {
492
+ let why = `Could not create it (${r.status}).`;
493
+ try {
494
+ const body = (await r.json());
495
+ why = body.detail || body.error || body.message || why;
496
+ }
497
+ catch {
498
+ /* no body worth showing */
499
+ }
500
+ setRefused(why);
501
+ setBusy(false);
502
+ return;
503
+ }
504
+ const made = (await r.json());
505
+ if (!made?.org) {
506
+ setRefused('The organization was made but its name did not come back. Reload to find it.');
507
+ setBusy(false);
508
+ return;
509
+ }
510
+ // The first organization is the account becoming a customer: the ONE
511
+ // sign-up conversion, as the create dialog counts it.
512
+ if (first)
513
+ countSignup();
514
+ renew();
515
+ await sdk.refreshAccessToken().catch(() => null);
516
+ pick(made.org);
517
+ onFounded(made.org);
518
+ setBusy(false);
519
+ setStep('invite');
520
+ }
521
+ catch (err) {
522
+ setRefused(err instanceof Error ? err.message : 'Could not create it.');
523
+ setBusy(false);
524
+ }
525
+ };
526
+ const invite = async (e) => {
527
+ e.preventDefault();
528
+ const list = [...new Set(emails.split(/[\s,;]+/).map((s) => s.trim()).filter(Boolean))];
529
+ if (list.length === 0 || !founded) {
530
+ setStep('plan');
531
+ return;
532
+ }
533
+ setBusy(true);
534
+ const out = await Promise.all(list.map(async (email) => {
535
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
536
+ return { email, refused: 'Not an email address.' };
537
+ try {
538
+ const made = await createInvitation(founded, email);
539
+ return { email, link: inviteLink(made.owner || founded, made.code) };
540
+ }
541
+ catch (err) {
542
+ return { email, refused: err instanceof Error ? err.message : 'Could not invite.' };
543
+ }
544
+ }));
545
+ setSent(out);
546
+ setBusy(false);
547
+ };
548
+ if (step === 'plan' && founded)
549
+ return _jsx(Plan, { org: founded, members: [founded], seats: 1 + sent.filter((s) => s.link).length, setup: true });
550
+ return (_jsx(Page, { label: "Create your team", children: _jsxs(Column, { children: [_jsx(Brand, {}), step === 'name' ? (_jsxs(YStack, { render: "form", onSubmit: create, children: [_jsx(Step, { at: 1, of: 3 }), _jsx(Heading, { title: "Create your team", lede: "A team owns its agents, credit and members. You are its owner." }), _jsx(Rule, {}), _jsxs(View, { render: "label", display: "block", children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "$ink", children: "Team name" }), _jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), placeholder: "Acme", autoFocus: true }), ...FIELD }), _jsx(Text, { display: "block", fontSize: "$3", color: "$soft", mt: 8, children: "What the rail and every invoice call it." })] }), refused ? (_jsx(Text, { render: "p", role: "alert", mt: 14, fontSize: "$3", color: BAD, children: refused })) : null, _jsxs(XStack, { gap: 10, flexWrap: "wrap", mt: 24, children: [_jsx(Button, { type: "submit", variant: "primary", size: "lg", disabled: busy, children: busy ? 'Creating…' : 'Continue' }), members.length > 1 && onSwitch ? (_jsx(Button, { type: "button", size: "lg", onClick: onSwitch, children: "Switch organization" })) : null] }), _jsx(Rule, {}), _jsxs(Text, { render: "p", fontSize: "$3", color: "$soft", children: ["Just you? Hanzo Chat is free for personal use at", ' ', _jsx(Text, { render: _jsx("a", { href: site('/chat') }), color: "$ink", textDecorationLine: "underline", children: "hanzo.ai/chat" }), "."] })] })) : (_jsxs(YStack, { render: "form", onSubmit: invite, children: [_jsx(Step, { at: 2, of: 3 }), _jsx(Heading, { title: "Invite your teammates", lede: "Each address gets its own single-use link to join. You can skip this and invite people later." }), _jsx(Rule, {}), _jsxs(View, { render: "label", display: "block", children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "$ink", children: "Email addresses" }), _jsx(Text, { render: _jsx("textarea", { value: emails, onChange: (e) => setEmails(e.target.value), placeholder: "ada@acme.com, alan@acme.com", rows: 3 }), ...FIELD }), _jsx(Text, { display: "block", fontSize: "$3", color: "$soft", mt: 8, children: "Separate them with commas, spaces or new lines." })] }), sent.length ? (_jsx(YStack, { render: "ul", gap: 8, m: 0, p: 0, mt: 18, "aria-label": "Invitations", children: sent.map((s) => (_jsx(YStack, { render: "li", children: _jsxs(Text, { fontSize: "$3", color: "$soft", wordWrap: "break-word", children: [_jsx(Text, { fontWeight: "500", color: "$ink", children: s.email }), ' ', s.link ? (_jsxs(_Fragment, { children: ["invited. Their link:", ' ', _jsx(Text, { render: _jsx("a", { href: s.link }), color: "$ink", textDecorationLine: "underline", children: s.link })] })) : (_jsx(Text, { role: "alert", color: BAD, children: s.refused }))] }) }, s.email))) })) : null, _jsx(XStack, { gap: 10, flexWrap: "wrap", mt: 24, children: sent.length ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: () => setStep('plan'), children: "Continue" })) : (_jsxs(_Fragment, { children: [_jsx(Button, { type: "submit", variant: "primary", size: "lg", disabled: busy, children: busy ? 'Inviting…' : emails.trim() ? 'Invite and continue' : 'Continue' }), _jsx(Button, { type: "button", size: "lg", onClick: () => setStep('plan'), children: "Skip for now" })] })) })] }))] }) }));
551
+ }
552
+ /** Where a checkout from the setup returns: the rooms' Home, on this origin. */
553
+ const home = () => `${window.location.origin}${where().home}`;
554
+ /**
555
+ * THE PLAN: how many seats, billed monthly or annually, then checkout.
556
+ *
557
+ * The prices are the catalog's Team row, never typed: its monthly price per
558
+ * seat, and a year as the catalog states it (`charge`). The seat floor is the
559
+ * row's `minSeats`. Checkout is hanzo.ai/pay's cart (`cartUrl`), charged to this
560
+ * organization and returning Home.
561
+ */
562
+ function Plan({ org, members, onSwitch, outage, onRetry, seats: wanted = 1, setup = false, }) {
563
+ const { plan } = usePlan('team');
564
+ const floor = Math.max(1, Number(plan?.limits?.minSeats ?? 1));
565
+ // The count asked for, held above the floor: the floor arrives with the live
566
+ // catalog row and can rise after the first paint.
567
+ const [asked, setSeats] = useState(wanted);
568
+ const seats = Math.max(floor, asked);
569
+ const [term, setTerm] = useState('month');
570
+ const monthly = (plan && charge(plan, 'month')) ?? 0;
571
+ const yearly = plan ? charge(plan, 'year') : null;
572
+ const each = term === 'year' && yearly ? yearly : monthly;
573
+ const terms = [
574
+ ['month', 'Monthly', `${money(monthly)} / seat / month`],
575
+ ...(yearly ? [['year', 'Annual', `${money(yearly)} / seat, billed once a year`]] : []),
576
+ ];
577
+ return (_jsx(Page, { label: "Choose your plan", children: _jsxs(Column, { children: [_jsx(Brand, {}), setup ? _jsx(Step, { at: 3, of: 3 }) : null, _jsx(Heading, { title: org, lead: 22.5, lede: outage
578
+ ? 'Billing could not be read just now, so this team cannot open yet. That is ours to fix.'
579
+ : plan
580
+ ? `A team runs on the ${plan.name} plan, and nothing in it works until that plan is live.`
581
+ : 'Its plan is not live yet.' }), outage && onRetry ? (_jsx(XStack, { mt: 16, children: _jsx(Button, { size: "lg", onClick: onRetry, children: "Try again" }) })) : null, _jsx(Rule, {}), plan ? (_jsxs(_Fragment, { children: [_jsxs(View, { render: "fieldset", display: "block", borderWidth: 0, p: 0, m: 0, children: [_jsx(Text, { render: "legend", display: "block", p: 0, fontSize: "$4", fontWeight: "500", mb: 10, color: "$ink", children: "Seats" }), _jsxs(XStack, { items: "center", gap: 10, children: [_jsx(Button, { type: "button", size: "icon", "aria-label": "One seat fewer", disabled: seats <= floor, onClick: () => setSeats(Math.max(floor, seats - 1)), children: _jsx(Minus, { size: 16, "aria-hidden": true }) }), _jsx(Text, { render: _jsx("input", { "aria-label": "Seats", type: "number", inputMode: "numeric", min: floor, value: seats, onChange: (e) => setSeats(Math.max(floor, Math.floor(Number(e.target.value) || floor))) }), ...FIELD, width: 80, height: 36, py: 0, fontSize: "$4", rounded: "$3", text: "center" }), _jsx(Button, { type: "button", size: "icon", "aria-label": "One seat more", onClick: () => setSeats(seats + 1), children: _jsx(Plus, { size: 16, "aria-hidden": true }) })] }), _jsxs(Text, { display: "block", fontSize: "$3", color: "$soft", mt: 8, children: ["At least ", floor, " ", floor === 1 ? 'seat' : 'seats', ". Each person in the team takes one."] })] }), _jsxs(View, { render: "fieldset", display: "block", borderWidth: 0, p: 0, m: 0, mt: 24, children: [_jsx(Text, { render: "legend", display: "block", p: 0, fontSize: "$4", fontWeight: "500", mb: 10, color: "$ink", children: "Billing" }), _jsx(View, { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 10, children: terms.map(([value, label, price]) => (_jsxs(XStack, { render: "label", gap: 10, items: "flex-start", px: 14, py: 12, rounded: "$3", borderWidth: 1, borderColor: term === value ? '$ink' : '$edge', cursor: "pointer", children: [_jsx("input", { type: "radio", name: "term", value: value, checked: term === value, onChange: () => setTerm(value) }), _jsxs(YStack, { shrink: 1, children: [_jsx(Text, { fontSize: "$4", fontWeight: "500", color: "$ink", children: label }), _jsx(Text, { fontSize: "$3", color: "$soft", mt: 2, children: price })] })] }, value))) })] }), _jsxs(Text, { render: "p", mt: 22, fontSize: "$4", color: "$soft", lineHeight: 23.25, children: [seats, " ", seats === 1 ? 'seat' : 'seats', " at ", money(each), " a ", term, " is", ' ', _jsxs(Text, { fontWeight: "500", color: "$ink", children: [money(seats * each), " a ", term] }), ". Payment is on the provider's hosted page."] })] })) : null, _jsxs(XStack, { gap: 10, flexWrap: "wrap", mt: 24, children: [plan ? (_jsx(Button, { asChild: true, variant: "primary", size: "lg", children: _jsx("a", { href: cartUrl(plan.id, seats, term, org, home()), children: "Continue to checkout" }) })) : null, members.length > 1 && onSwitch ? (_jsx(Button, { size: "lg", onClick: onSwitch, children: "Switch organization" })) : null] })] }) }));
582
+ }
@@ -0,0 +1,4 @@
1
+ export declare function Profile({ open, onClose }: {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ }): import("react").JSX.Element;