@hanzo/rooms 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Me.js CHANGED
@@ -24,7 +24,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
24
24
  // `POST /v1/account/orgs` the no-organization gate uses (components/workspace/
25
25
  // Orgs.tsx); the next load mints a token that carries it.
26
26
  import { useEffect, useMemo, useRef, useState } from 'react';
27
- import { Popover, SizableText, View, XStack, YStack } from '@hanzo/gui';
27
+ import { Popover, SizableText, usePopperContextSlow, View, XStack, YStack } from '@hanzo/gui';
28
28
  import { Building2, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
29
29
  import { label, Meter, path, said as spoken, useStanding, useWho } from '@hanzo/build';
30
30
  import { useOrganizations } from '@hanzo/iam/react';
@@ -132,6 +132,24 @@ function rove(e, close) {
132
132
  e.preventDefault();
133
133
  rows[to]?.focus();
134
134
  }
135
+ /**
136
+ * The menu grows once the plan is read (`Meter`), after it was placed, and the
137
+ * popover follows its anchor, not its own size. So it is placed again whenever
138
+ * its content changes size, and never hangs over the row it opens from.
139
+ */
140
+ function Refit({ children }) {
141
+ const { update } = usePopperContextSlow();
142
+ const box = useRef(null);
143
+ useEffect(() => {
144
+ const node = box.current;
145
+ if (!node || typeof ResizeObserver === 'undefined')
146
+ return;
147
+ const watch = new ResizeObserver(() => update());
148
+ watch.observe(node);
149
+ return () => watch.disconnect();
150
+ }, [update]);
151
+ return (_jsx(View, { render: _jsx("div", { ref: box }), self: "stretch", minW: 0, children: children }));
152
+ }
135
153
  export function Me({ host, narrow = false, onOrg, }) {
136
154
  const menu = useWho();
137
155
  const [creating, setCreating] = useState(false);
@@ -168,11 +186,11 @@ export function Me({ host, narrow = false, onOrg, }) {
168
186
  return null;
169
187
  return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: menu.onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: "100%", children: _jsxs(XStack, { ref: (el) => {
170
188
  row.current = el instanceof HTMLElement ? el : null;
171
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 30 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), standing ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: standing })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
172
- menu.onOpenChange(false);
173
- if (o.name !== current)
174
- onOrg(o.name);
175
- } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(site(ORG_SETTINGS))) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
189
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 30 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), standing ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: standing })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(Refit, { children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
190
+ menu.onOpenChange(false);
191
+ if (o.name !== current)
192
+ onOrg(o.name);
193
+ } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(site(ORG_SETTINGS))) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
176
194
  }
177
195
  /**
178
196
  * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
package/dist/Orgs.js CHANGED
@@ -175,12 +175,13 @@ export function Orgs({ children, front = false, forward, signup = false, }) {
175
175
  if (!hydrated)
176
176
  return front && Landing ? _jsx(Landing, {}) : _jsx(Veil, {});
177
177
  // Past hydration the room keeps its place in the tree in every branch that
178
- // draws it — the veil goes AFTER it — so a token refresh mid-session never
178
+ // draws it — `Held`, the veil AFTER it — so a token refresh mid-session never
179
179
  // remounts what the reader had open. The veil is unconditional now: it was
180
180
  // hung only when a token was already in hand, which left a visitor arriving
181
- // without one looking at the room while the SDK decided.
181
+ // without one looking at the room while the SDK decided. Under it the room is
182
+ // inert: it takes no key or press until the gate has decided.
182
183
  if (isLoading)
183
- return front && Landing && !hasSession() && !arriving ? _jsx(Landing, {}) : _jsxs(_Fragment, { children: [children, _jsx(Veil, {})] });
184
+ return front && Landing && !hasSession() && !arriving ? _jsx(Landing, {}) : _jsxs(_Fragment, { children: [_jsx(Held, { veiled: true, children: children }), _jsx(Veil, {})] });
184
185
  if (arriving && !isAuthenticated)
185
186
  return _jsx(SignIn, { to: LOGIN });
186
187
  if (!isAuthenticated)
@@ -202,11 +203,11 @@ export function Orgs({ children, front = false, forward, signup = false, }) {
202
203
  // exception rather than the fallback. The one exception to the plan is a
203
204
  // personal organization on Free, off hanzo.team, and its kind is read first.
204
205
  if (!tier && !wrong)
205
- return _jsxs(_Fragment, { children: [children, _jsx(Veil, {})] });
206
+ return _jsxs(_Fragment, { children: [_jsx(Held, { veiled: true, children: children }), _jsx(Veil, {})] });
206
207
  if (free && personal === null)
207
- return _jsxs(_Fragment, { children: [children, _jsx(Veil, {})] });
208
+ return _jsxs(_Fragment, { children: [_jsx(Held, { veiled: true, children: children }), _jsx(Veil, {})] });
208
209
  if (open)
209
- return forward ? _jsx(Forward, { to: forward }) : _jsx(_Fragment, { children: children });
210
+ return forward ? _jsx(Forward, { to: forward }) : _jsx(_Fragment, { children: _jsx(Held, { veiled: false, children: children }) });
210
211
  const swap = () => setPicked(false);
211
212
  if (door) {
212
213
  // A billing OUTAGE is ours; the plan is still the condition.
@@ -293,6 +294,16 @@ function Heading({ title, lede, lead }) {
293
294
  function Veil() {
294
295
  return _jsx(View, { "aria-hidden": true, position: "fixed", inset: 0, z: SHEET, bg: "var(--background)" });
295
296
  }
297
+ /**
298
+ * The room, in the one place every branch that draws it puts it. Under the veil
299
+ * it is inert and hidden from assistive technology, so what the veil covers
300
+ * takes no focus, key or press: Dev's composer focuses itself as it mounts, and
301
+ * a team on Free could otherwise type and send before its ask was drawn. It
302
+ * lays out as its children do.
303
+ */
304
+ function Held({ veiled, children }) {
305
+ return (_jsx(View, { render: _jsx("div", { "data-slot": "held", inert: veiled, "aria-hidden": veiled || undefined }), display: "contents", children: children }));
306
+ }
296
307
  /** An open organization, sent on. */
297
308
  function Forward({ to }) {
298
309
  const { router } = useRooms();
package/dist/lib/api.d.ts CHANGED
@@ -19,6 +19,15 @@
19
19
  * production address was frozen into the bundle before a browser could say
20
20
  * where it was. Called at request time it answers for the page that is open.
21
21
  */
22
+ /**
23
+ * The devnet's gateway and identity. A page on a devnet host (`devnet()` in
24
+ * host.ts) talks to these and to nothing of production's: the export is the
25
+ * same bytes on hanzo.ai and on app-dev.hanzo.ai, so the host it is open on is
26
+ * the only thing that tells the two apart, and it outranks any address the
27
+ * build stated with `configure()`.
28
+ */
29
+ export declare const API_DEV = "https://api-dev.hanzo.ai";
30
+ export declare const IAM_DEV = "https://id-dev.hanzo.ai";
22
31
  /** The gateway's origin, with no trailing slash: callers write `${api()}/v1/…`. */
23
32
  export declare function api(): string;
24
33
  /**
package/dist/lib/api.js CHANGED
@@ -19,10 +19,23 @@
19
19
  * production address was frozen into the bundle before a browser could say
20
20
  * where it was. Called at request time it answers for the page that is open.
21
21
  */
22
- import { guest, team } from './host.js';
22
+ import { devnet, guest, team } from './host.js';
23
23
  import { where } from '../where.js';
24
+ /**
25
+ * The devnet's gateway and identity. A page on a devnet host (`devnet()` in
26
+ * host.ts) talks to these and to nothing of production's: the export is the
27
+ * same bytes on hanzo.ai and on app-dev.hanzo.ai, so the host it is open on is
28
+ * the only thing that tells the two apart, and it outranks any address the
29
+ * build stated with `configure()`.
30
+ */
31
+ export const API_DEV = 'https://api-dev.hanzo.ai';
32
+ export const IAM_DEV = 'https://id-dev.hanzo.ai';
33
+ /** Whether the page in front of us is on a devnet host. */
34
+ const onDevnet = () => typeof window !== 'undefined' && devnet(window.location.hostname);
24
35
  /** The gateway's origin, with no trailing slash: callers write `${api()}/v1/…`. */
25
36
  export function api() {
37
+ if (onDevnet())
38
+ return API_DEV;
26
39
  const set = where().api;
27
40
  if (set)
28
41
  return set.replace(/\/+$/, '');
@@ -52,6 +65,8 @@ export function api() {
52
65
  * the day it is changed.
53
66
  */
54
67
  export function iam() {
68
+ if (onDevnet())
69
+ return IAM_DEV;
55
70
  return (where().iam || 'https://hanzo.id').replace(/\/+$/, '');
56
71
  }
57
72
  /**
@@ -128,3 +128,10 @@ export declare const WEB = "https://hanzo.ai/chat";
128
128
  * on hanzo.ai.
129
129
  */
130
130
  export declare const guest: (hostname: string) => boolean;
131
+ /**
132
+ * Whether a host is the devnet's: its production twin with `-dev` on the one
133
+ * label under hanzo.ai (app-dev.hanzo.ai, api-dev.hanzo.ai, id-dev.hanzo.ai).
134
+ * A deeper name (a.b-dev.hanzo.ai) is not one, and neither is a name that only
135
+ * contains the suffix (app-dev.hanzo.ai.example.com).
136
+ */
137
+ export declare function devnet(hostname: string): boolean;
package/dist/lib/host.js CHANGED
@@ -197,3 +197,17 @@ export const WEB = `${SIGNIN}${CHAT}`;
197
197
  * on hanzo.ai.
198
198
  */
199
199
  export const guest = (hostname) => hostname === 'hanzo.app' || hostname.endsWith('.hanzo.app') || hostname === 'hanzo.chat';
200
+ /** The devnet's hosts are one label under hanzo.ai ending in `-dev`: app-dev, api-dev, id-dev. */
201
+ const DEVNET = '-dev.hanzo.ai';
202
+ /**
203
+ * Whether a host is the devnet's: its production twin with `-dev` on the one
204
+ * label under hanzo.ai (app-dev.hanzo.ai, api-dev.hanzo.ai, id-dev.hanzo.ai).
205
+ * A deeper name (a.b-dev.hanzo.ai) is not one, and neither is a name that only
206
+ * contains the suffix (app-dev.hanzo.ai.example.com).
207
+ */
208
+ export function devnet(hostname) {
209
+ if (!hostname.endsWith(DEVNET))
210
+ return false;
211
+ const label = hostname.slice(0, -'.hanzo.ai'.length);
212
+ return label.length > '-dev'.length && !label.includes('.');
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",