@hanzo/rooms 0.1.21 → 0.1.23

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/lib/tier.js CHANGED
@@ -26,7 +26,11 @@ import { api } from './api.js';
26
26
  import { bearer, org, orgs, scope } from './session.js';
27
27
  import { list } from './list.js';
28
28
  import { money } from './pay.js';
29
- import { date } from './limits.js';
29
+ /** A period's end as a sentence states it — "Oct 30, 2026" — or '' for none. Periods end on UTC days. */
30
+ function date(iso) {
31
+ const t = iso ? Date.parse(iso) : NaN;
32
+ return Number.isNaN(t) ? '' : new Date(t).toLocaleDateString('en-US', { timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric' });
33
+ }
30
34
  /** An error that names the status, for `say()` to turn into a sentence. */
31
35
  const refusal = (status, message) => Object.assign(new Error(message), { status });
32
36
  /** GETs `path` as whoever holds the bearer, in this browser's organization. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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",
@@ -56,8 +56,8 @@
56
56
  "@hanzo/iam": ">=0.21.28",
57
57
  "@hanzo/logo": ">=1.0.26",
58
58
  "@hanzo/plans": ">=1.8.9",
59
- "@hanzo/ui": ">=8.27.51",
60
- "@hanzo/usage": ">=0.1.17",
59
+ "@hanzo/ui": ">=8.27.57",
60
+ "@hanzo/usage": ">=0.1.18",
61
61
  "@hanzo/voice": ">=0.1.12",
62
62
  "@hanzogui/shell": "^8.3.20",
63
63
  "lucide-react": ">=1.42.0",
@@ -75,8 +75,8 @@
75
75
  "@hanzo/iam": "0.21.28",
76
76
  "@hanzo/logo": "1.0.26",
77
77
  "@hanzo/plans": "1.8.9",
78
- "@hanzo/ui": "8.27.51",
79
- "@hanzo/usage": "0.1.17",
78
+ "@hanzo/ui": "8.27.57",
79
+ "@hanzo/usage": "0.1.18",
80
80
  "@hanzo/voice": "0.1.12",
81
81
  "@hanzogui/config": "8.3.5",
82
82
  "@hanzogui/core": "8.3.5",
package/dist/Guide.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type Tour } from './tours.js';
2
- export declare function Guide({ room }: {
3
- room?: Tour;
4
- }): import("react").ReactPortal;
5
- /** Let a room's guide be asked for again — a menu row can call this and reload. */
6
- export declare function replayGuide(room?: Tour): void;
package/dist/Guide.js DELETED
@@ -1,273 +0,0 @@
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 { brand } from './where.js';
33
- import { useIam } from '@hanzo/iam/react';
34
- import { useAi } from './lib/ai.js';
35
- import { Next, Scene } from './Scene.js';
36
- import { TOURS } from './tours.js';
37
- /** The ring on the spot: design's focus ring and its halo, one above the dim. gui hands a `calc()` to the page
38
- * unchanged; its types only name custom properties. */
39
- const RING = '0 0 0 1px var(--ring), 0 0 26px var(--ring-halo)';
40
- const ABOVE = 'calc(var(--z-overlay) + 1)';
41
- /** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
42
- const FLOOR = 10;
43
- /** The page's own leading (design's `--leading-base`, 1.35rem), which a face's scope would otherwise replace with its
44
- * 20px; gui's line height takes a rung or a number. */
45
- const LEAD = 21.6;
46
- /** Where the card's pointer sits, and which two edges it draws, for each side the card can take. */
47
- const POINTER = {
48
- bottom: { t: -6.5, l: '50%', ml: -5.5, borderLeftWidth: 1, borderTopWidth: 1 },
49
- top: { b: -6.5, l: '50%', ml: -5.5, borderRightWidth: 1, borderBottomWidth: 1 },
50
- right: { l: -6.5, t: '50%', mt: -5.5, borderLeftWidth: 1, borderBottomWidth: 1 },
51
- left: { r: -6.5, t: '50%', mt: -5.5, borderRightWidth: 1, borderTopWidth: 1 },
52
- };
53
- /** Where a room's tour is marked seen, in this browser and in the person's preferences. */
54
- const keyOf = (room) => (room === 'chat' ? 'hanzo:chat:guide' : `hanzo:${room}:guide`);
55
- const prefOf = (room) => `${room}Guide`;
56
- const PAD = 8;
57
- const GAP = 14;
58
- const CARD_W = 352;
59
- export function Guide({ room = 'chat' }) {
60
- const KEY = keyOf(room);
61
- const PREF = prefOf(room);
62
- const STOPS = TOURS[room];
63
- const { client } = useAi();
64
- // ONLY FOR SOMEONE WHO IS SIGNED IN. `useAi` hands back a client either way —
65
- // the anonymous one the free composer runs on — so a client is not proof of a
66
- // reader. Opening for a visitor put a dimmed backdrop over the page before
67
- // they had an account, and its panels sat on top of the Sign in button: the
68
- // guide blocked the one action the page was asking for. It also has nothing
69
- // true to say to a stranger, since half of what it points at is theirs only
70
- // once they have a workspace.
71
- const { isAuthenticated, isLoading } = useIam();
72
- const [live, setLive] = useState(false);
73
- const [i, setI] = useState(0);
74
- const [spot, setSpot] = useState(null);
75
- const [side, setSide] = useState('bottom');
76
- const [card, setCard] = useState(null);
77
- const cardRef = useRef(null);
78
- // The stops whose anchors exist right now. A destination the org does not
79
- // carry has no element, so it is not a step — the tour describes THIS
80
- // workspace rather than a catalogue.
81
- const stops = useMemo(() => (live ? STOPS.filter((s) => s.find() !== null) : STOPS), [live, STOPS]);
82
- const stop = stops[i];
83
- const done = useCallback((how) => {
84
- setLive(false);
85
- const mark = JSON.stringify({ at: new Date().toISOString(), how });
86
- try {
87
- localStorage.setItem(KEY, mark);
88
- }
89
- catch {
90
- /* a browser with storage off still gets the guide once per visit */
91
- }
92
- // Follows the person, not the browser. Failure is silent on purpose: the
93
- // local mark already settled it, and a guide is not worth an error.
94
- client?.http
95
- .json({ method: 'PATCH', path: '/v1/pref', body: { prefs: { [PREF]: { at: new Date().toISOString(), how } } } })
96
- .catch(() => { });
97
- }, [client, KEY, PREF]);
98
- // Should it open at all? Local first — it answers instantly and it answers
99
- // for a reader who never signs in.
100
- useEffect(() => {
101
- if (isLoading || !isAuthenticated)
102
- return;
103
- let held = false;
104
- try {
105
- held = Boolean(localStorage.getItem(KEY));
106
- }
107
- catch {
108
- /* treat unreadable storage as "not yet shown" */
109
- }
110
- if (held)
111
- return;
112
- let alive = true;
113
- const open = () => {
114
- if (!alive)
115
- return;
116
- // Wait for the room to actually render before pointing at it.
117
- const t = window.setTimeout(() => alive && setLive(true), 900);
118
- return () => window.clearTimeout(t);
119
- };
120
- if (!client)
121
- return open();
122
- client.http
123
- .json({ path: '/v1/pref' })
124
- .then((doc) => {
125
- if (!alive)
126
- return;
127
- if (doc?.prefs && doc.prefs[PREF]) {
128
- try {
129
- localStorage.setItem(KEY, JSON.stringify(doc.prefs[PREF]));
130
- }
131
- catch {
132
- /* nothing to do */
133
- }
134
- return;
135
- }
136
- // THE TOUR IS FOR A NEW ACCOUNT. One that has already had a conversation
137
- // here is not new: it may never have met this tour, and it does not
138
- // need to. It is marked as such, so the question is not asked again.
139
- client.threads
140
- .list()
141
- .then((had) => {
142
- if (!alive)
143
- return;
144
- if (Array.isArray(had) && had.length > 0)
145
- done('unneeded');
146
- else
147
- open();
148
- })
149
- .catch(() => open());
150
- })
151
- .catch(() => open());
152
- return () => {
153
- alive = false;
154
- };
155
- }, [client, isAuthenticated, isLoading, KEY, PREF, done]);
156
- // Measure the anchor, and re-measure when the page moves under it.
157
- //
158
- // NOT on every frame. A rAF loop that calls setSpot each tick re-renders the
159
- // card sixty times a second, which makes it a target nothing can click: the
160
- // element under the pointer is replaced between the hit test and the press.
161
- // Measure on the events that actually move things, and write state only when
162
- // the rectangle really changed.
163
- useLayoutEffect(() => {
164
- if (!live || !stop)
165
- return;
166
- let last = '';
167
- const measure = () => {
168
- const el = stop.find();
169
- if (!el) {
170
- setSpot(null);
171
- return;
172
- }
173
- const r = el.getBoundingClientRect();
174
- const next = { top: r.top - PAD, left: r.left - PAD, width: r.width + PAD * 2, height: r.height + PAD * 2 };
175
- const key = `${Math.round(next.top)}:${Math.round(next.left)}:${Math.round(next.width)}:${Math.round(next.height)}`;
176
- if (key === last)
177
- return;
178
- last = key;
179
- setSpot(next);
180
- };
181
- // Bring it into view once per step, then measure after the scroll settles.
182
- stop.find()?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
183
- measure();
184
- const settle = window.setTimeout(measure, 260);
185
- const ro = new ResizeObserver(measure);
186
- ro.observe(document.body);
187
- const el = stop.find();
188
- if (el)
189
- ro.observe(el);
190
- window.addEventListener('scroll', measure, true);
191
- window.addEventListener('resize', measure);
192
- return () => {
193
- window.clearTimeout(settle);
194
- ro.disconnect();
195
- window.removeEventListener('scroll', measure, true);
196
- window.removeEventListener('resize', measure);
197
- };
198
- }, [live, stop, i]);
199
- // Place the card against the measured spot, flipping rather than overflowing.
200
- useLayoutEffect(() => {
201
- if (!spot) {
202
- setCard(null);
203
- return;
204
- }
205
- const h = cardRef.current?.offsetHeight ?? 168;
206
- const vw = window.innerWidth;
207
- const vh = window.innerHeight;
208
- let want = stop?.side ?? 'bottom';
209
- if (want === 'bottom' && spot.top + spot.height + GAP + h > vh - 12)
210
- want = 'top';
211
- if (want === 'top' && spot.top - GAP - h < 12)
212
- want = 'bottom';
213
- if (want === 'right' && spot.left + spot.width + GAP + CARD_W > vw - 12)
214
- want = 'left';
215
- if (want === 'left' && spot.left - GAP - CARD_W < 12)
216
- want = 'right';
217
- let top;
218
- let left;
219
- if (want === 'bottom' || want === 'top') {
220
- top = want === 'bottom' ? spot.top + spot.height + GAP : spot.top - GAP - h;
221
- left = spot.left + spot.width / 2 - CARD_W / 2;
222
- }
223
- else {
224
- left = want === 'right' ? spot.left + spot.width + GAP : spot.left - GAP - CARD_W;
225
- top = spot.top + spot.height / 2 - h / 2;
226
- }
227
- setSide(want);
228
- setCard({ top: Math.max(12, Math.min(top, vh - h - 12)), left: Math.max(12, Math.min(left, vw - CARD_W - 12)) });
229
- }, [spot, stop]);
230
- const next = useCallback(() => {
231
- if (i + 1 >= stops.length)
232
- done('finished');
233
- else
234
- setI(i + 1);
235
- }, [i, stops.length, done]);
236
- const back = useCallback(() => setI((n) => Math.max(0, n - 1)), []);
237
- useEffect(() => {
238
- if (!live)
239
- return;
240
- const onKey = (e) => {
241
- if (e.key === 'Escape') {
242
- e.preventDefault();
243
- done('skipped');
244
- }
245
- else if (e.key === 'ArrowRight' || e.key === 'Enter') {
246
- e.preventDefault();
247
- next();
248
- }
249
- else if (e.key === 'ArrowLeft') {
250
- e.preventDefault();
251
- back();
252
- }
253
- };
254
- document.addEventListener('keydown', onKey, true);
255
- return () => document.removeEventListener('keydown', onKey, true);
256
- }, [live, next, back, done]);
257
- if (!live || !stop || !spot || !card || typeof document === 'undefined')
258
- return null;
259
- const last = i + 1 >= stops.length;
260
- // The dim is four panels around the spot rather than one box with a hole:
261
- // a hole wants a mask, and a mask over a blurred backdrop reads as a seam.
262
- const panel = { position: 'fixed', bg: 'var(--surface-scrim)', z: 'var(--z-overlay)', onClick: () => done('skipped') };
263
- return createPortal(_jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": `Getting started with ${brand()}`, 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);
264
- }
265
- /** Let a room's guide be asked for again — a menu row can call this and reload. */
266
- export function replayGuide(room = 'chat') {
267
- try {
268
- localStorage.removeItem(keyOf(room));
269
- }
270
- catch {
271
- /* nothing to clear */
272
- }
273
- }
package/dist/Scene.d.ts DELETED
@@ -1,12 +0,0 @@
1
- export declare function Scene({ step, onNext, onSkip }: {
2
- step: string;
3
- onNext: () => void;
4
- onSkip: () => void;
5
- }): import("react").JSX.Element;
6
- /** A stop's one primary control, the scene's and the card's. It takes focus when the stop opens, which is what
7
- * `autoFocus` did on the element it replaced; a stop that only relabels it (Next to Done) keeps the focus it has.
8
- */
9
- export declare function Next({ onPress, label }: {
10
- onPress: () => void;
11
- label?: string;
12
- }): import("react").JSX.Element;
package/dist/Scene.js DELETED
@@ -1,105 +0,0 @@
1
- 'use client';
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // The scene: one chat, the team brought in, and the call it turns into — played
4
- // rather than read. It is drawn on a stage of its own, so nothing typed here
5
- // reaches the composer, and nothing said here is sent or saved.
6
- import { useEffect, useRef, useState } from 'react';
7
- import { Text, View, XStack, YStack } from '@hanzo/gui';
8
- import { Button } from '@hanzo/ui';
9
- import { CAST as CREW } from './cast.js';
10
- const CAST = {
11
- you: { name: 'You', part: 'Owner', initial: 'Y', ground: 'var(--neutral-700)' },
12
- des: { name: 'Des', part: 'Designer', face: '/agents/des.png', initial: 'D', ground: CREW.des.ground },
13
- vi: { name: 'Vi', part: 'Visionary', face: '/agents/vi.png', initial: 'V', ground: CREW.vi.ground },
14
- alex: { name: 'Alex', part: 'Teammate', initial: 'A', ground: 'var(--neutral-600)' },
15
- };
16
- /** The tour's own prompt: typed for show, never sent. */
17
- const SAMPLE = 'Help me plan the next version of our app';
18
- const ADDED = ['des', 'vi', 'alex'];
19
- const CHAT = [
20
- { who: 'you', text: 'We need to create a more immersive experience.' },
21
- { who: 'vi', text: 'Should we create UI for wearable glasses?' },
22
- { who: 'des', text: 'I love this idea. We could explore the best settings in Roblox and other top games and create some of the best ideas!' },
23
- { who: 'alex', text: "What a great idea, let's jump on a call now so we can plan and create deliverables and due dates." },
24
- ];
25
- const CALL = [
26
- { who: 'alex', text: "Let's plan it. Vi, you own the concept. Des, the first mockups. I'll set the deliverables and due dates." },
27
- { who: 'vi', text: 'First deliverable: a glasses-ready layout, due Friday.' },
28
- { who: 'des', text: "I'll pull references from the top Roblox worlds and draft three directions, due Wednesday." },
29
- { who: 'you', text: 'Perfect. Save the transcript and put the deliverables on the Board.' },
30
- ];
31
- // The beats, in order: the prompt, three people added, four lines of chat, the
32
- // move to a call, four lines said on it, and the record of it.
33
- const PROMPT = 0;
34
- const ADD = 1; // .. 3
35
- const TALK = 4; // .. 7
36
- const RING = 8;
37
- const SAY = 9; // .. 12
38
- const SAVED = 13;
39
- const MS = 26;
40
- /** Text revealed a character at a time; says when it is all there. */
41
- function Typed({ text, go, onDone }) {
42
- const [n, setN] = useState(go ? 0 : text.length);
43
- useEffect(() => {
44
- if (!go)
45
- return;
46
- if (n >= text.length) {
47
- const t = window.setTimeout(onDone, 520);
48
- return () => window.clearTimeout(t);
49
- }
50
- const t = window.setTimeout(() => setN((k) => k + 1), MS);
51
- return () => window.clearTimeout(t);
52
- }, [go, n, text, onDone]);
53
- return _jsx(_Fragment, { children: text.slice(0, n) });
54
- }
55
- function Face({ who, size = 28 }) {
56
- const c = CAST[who];
57
- return (_jsx(View, { render: "span", "aria-hidden": true, width: size, height: size, rounded: 9999, bg: c.ground, display: "inline-flex", items: "center", justify: "center", overflow: "hidden", shrink: 0, children: c.face ? (_jsx("img", { src: c.face, alt: "", width: size, height: size })) : (_jsx(Text, { fontFamily: "$body", fontSize: size * 0.42, fontWeight: "600", color: "var(--pure-white)", children: c.initial })) }));
58
- }
59
- const ink = '$ink';
60
- const dim = '$soft';
61
- const line = '$borderColor';
62
- // The stage is portalled to <body> by the Guide, outside every font scope gui
63
- // writes, so a `$N` size or leading resolves only on a Text that names `$body`
64
- // itself — which is why every Text below does.
65
- /** A line of the scene's own type: the base rung on its own leading. */
66
- const SAID = { fontSize: '$3', lineHeight: '$3' };
67
- export function Scene({ step, onNext, onSkip }) {
68
- const [beat, setBeat] = useState(PROMPT);
69
- const [typed, setTyped] = useState(0);
70
- const advance = () => setBeat((b) => Math.min(b + 1, SAVED));
71
- const settle = () => setTyped((k) => k + 1);
72
- // Beats that type advance themselves when the typing is done; the ones that
73
- // only appear advance on a short pause.
74
- useEffect(() => {
75
- const typing = beat === PROMPT || (beat >= TALK && beat < RING) || (beat >= SAY && beat < SAVED);
76
- if (typing)
77
- return;
78
- if (beat === SAVED)
79
- return;
80
- const t = window.setTimeout(advance, beat === RING ? 1400 : 900);
81
- return () => window.clearTimeout(t);
82
- }, [beat]);
83
- useEffect(() => {
84
- if (typed === 0)
85
- return;
86
- advance();
87
- }, [typed]);
88
- const added = ADDED.slice(0, Math.max(0, Math.min(ADDED.length, beat - ADD + 1)));
89
- const said = CHAT.slice(0, Math.max(0, Math.min(CHAT.length, beat - TALK + 1)));
90
- const onCall = beat >= RING;
91
- const spoken = CALL.slice(0, Math.max(0, Math.min(CALL.length, beat - SAY + 1)));
92
- const speaker = onCall && beat >= SAY && beat < SAVED ? CALL[beat - SAY].who : null;
93
- return (_jsxs(YStack, { role: "group", "aria-label": "A chat that becomes a call", position: "fixed", l: "50%", t: "50%", x: "-50%", y: "-50%", width: "min(720px, calc(100vw - 32px))", maxH: "calc(100vh - 32px)", overflowY: "auto", z: "var(--z-modal)", bg: "var(--popover)", borderWidth: 1, borderColor: "var(--border-control)", rounded: 14, boxShadow: "var(--shadow-floating)", p: 18, children: [_jsxs(XStack, { items: "center", gap: 10, mb: 12, children: [_jsx(Text, { fontFamily: "$body", fontSize: 10, letterSpacing: 1.1, textTransform: "uppercase", color: "$faint", children: step }), _jsx(View, { flex: 1, height: 1, bg: line }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: dim, children: onCall ? 'On a call' : 'In chat' })] }), _jsx(Text, { fontFamily: "$body", render: "h2", mb: 4, fontSize: "$6", fontWeight: "600", lineHeight: "$4", color: ink, children: "More than chat" }), _jsx(Text, { fontFamily: "$body", render: "p", mb: 14, ...SAID, color: "$quiet", children: "Start with a bot, bring in the team, and take it to a call. Nothing here is sent or saved." }), !onCall ? (_jsxs(YStack, { borderWidth: 1, borderColor: line, rounded: 12, overflow: "hidden", children: [_jsxs(XStack, { items: "center", gap: 10, px: 12, py: 10, borderBottomWidth: 1, borderColor: line, children: [_jsx(Face, { who: "des" }), _jsxs(YStack, { children: [_jsx(Text, { fontFamily: "$body", fontSize: "$3", fontWeight: "600", lineHeight: "$1", color: ink, children: "des" }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", lineHeight: "$1", color: dim, children: "Designer \u00B7 bot" })] }), _jsx(View, { flex: 1 }), _jsxs(XStack, { gap: 6, flexWrap: "wrap", justify: "flex-end", children: [added.map((w) => (_jsxs(XStack, { items: "center", gap: 6, pl: 4, pr: 8, py: 3, rounded: 999, borderWidth: 1, borderColor: line, children: [_jsx(Face, { who: w, size: 18 }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: ink, children: CAST[w].name }), _jsxs(Text, { fontFamily: "$body", fontSize: "$2", color: dim, children: ["\u00B7 ", CAST[w].part] })] }, w))), beat >= ADD && beat < TALK ? (_jsx(Text, { fontFamily: "$body", px: 8, py: 3, rounded: 999, borderWidth: 1, borderStyle: "dashed", borderColor: line, fontSize: "$2", color: dim, children: "+ Add to chat" })) : null] })] }), _jsxs(View, { display: "grid", p: 12, gap: 10, minH: 168, children: [said.map((m, k) => (_jsxs(XStack, { gap: 10, items: "flex-start", flexDirection: m.who === 'you' ? 'row-reverse' : 'row', children: [_jsx(Face, { who: m.who }), _jsxs(YStack, { maxW: "78%", px: 12, py: 8, rounded: 12, bg: m.who === 'you' ? 'var(--foreground)' : '$hover', children: [m.who !== 'you' ? (_jsx(Text, { fontFamily: "$body", fontSize: "$1", color: dim, mb: 2, children: CAST[m.who].name })) : null, _jsx(Text, { fontFamily: "$body", ...SAID, color: m.who === 'you' ? 'var(--background)' : ink, children: _jsx(Typed, { text: m.text, go: k === said.length - 1 && beat < RING, onDone: settle }) })] })] }, k))), said.length === 0 ? (_jsx(Text, { fontFamily: "$body", fontSize: "$2", color: dim, text: "center", pt: 48, children: beat < ADD ? 'A chat with des, your designer bot.' : 'Bringing the team in…' })) : null] }), _jsxs(Text, { fontFamily: "$body", m: 12, px: 12, py: 10, rounded: 10, borderWidth: 1, borderColor: line, ...SAID, color: beat === PROMPT ? ink : dim, minH: 40, children: [beat === PROMPT ? _jsx(Typed, { text: SAMPLE, go: true, onDone: settle }) : beat < TALK ? SAMPLE : 'Ask anything', _jsx(View, { render: "span", "aria-hidden": true, display: beat === PROMPT ? 'inline-flex' : 'none', width: 1, height: 14, bg: ink, ml: 1, y: 2 })] })] })) : (_jsxs(YStack, { borderWidth: 1, borderColor: line, rounded: 12, overflow: "hidden", children: [_jsx(View, { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, p: 12, borderBottomWidth: 1, borderColor: line, children: ['you', 'des', 'vi', 'alex'].map((w) => (_jsxs(YStack, { items: "center", gap: 6, px: 6, py: 12, rounded: 10, bg: "$hover", outlineWidth: 2, outlineStyle: "solid", outlineColor: speaker === w ? ink : 'transparent', transition: "200ms", children: [_jsx(Face, { who: w, size: 40 }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: ink, children: CAST[w].name })] }, w))) }), _jsxs(YStack, { p: 12, minH: 168, children: [_jsx(Text, { fontFamily: "$body", fontSize: "$1", letterSpacing: 1.1, textTransform: "uppercase", color: dim, mb: 8, children: beat === RING ? 'Calling…' : 'Live transcript' }), _jsx(View, { display: "grid", gap: 8, children: spoken.map((m, k) => (_jsxs(XStack, { gap: 8, children: [_jsx(Text, { fontFamily: "$body", ...SAID, color: dim, shrink: 0, width: 44, children: CAST[m.who].name }), _jsx(Text, { fontFamily: "$body", ...SAID, color: ink, children: _jsx(Typed, { text: m.text, go: k === spoken.length - 1 && beat < SAVED, onDone: settle }) })] }, k))) }), beat === SAVED ? (_jsx(Text, { fontFamily: "$body", mt: 14, px: 10, py: 8, rounded: 8, borderWidth: 1, borderColor: line, fontSize: "$2", color: ink, children: "\u2713 Transcript saved \u00B7 Deliverables and due dates on the Board" })) : null] })] })), _jsxs(XStack, { items: "center", gap: 8, mt: 14, children: [_jsx(Button, { variant: "linkMuted", size: "sm", px: 2, onClick: onSkip, children: "Skip" }), _jsx(View, { flex: 1 }), beat === SAVED ? (_jsx(Button, { size: "sm", onClick: () => {
94
- setTyped(0);
95
- setBeat(PROMPT);
96
- }, children: "Replay" })) : null, _jsx(Next, { onPress: onNext })] })] }));
97
- }
98
- /** A stop's one primary control, the scene's and the card's. It takes focus when the stop opens, which is what
99
- * `autoFocus` did on the element it replaced; a stop that only relabels it (Next to Done) keeps the focus it has.
100
- */
101
- export function Next({ onPress, label = 'Next' }) {
102
- const ref = useRef(null);
103
- useEffect(() => ref.current?.focus(), []);
104
- return (_jsx(Button, { ref: ref, variant: "primary", size: "sm", onClick: onPress, children: label }));
105
- }
package/dist/Usage.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function Usage(): import("react").JSX.Element;
package/dist/Usage.js DELETED
@@ -1,21 +0,0 @@
1
- 'use client';
2
- import { jsxs as _jsxs } from "react/jsx-runtime";
3
- // Free's usage, said where it is spent. Free is limited, and what it spends
4
- // comes from one pool every free user shares, so the line says both, and what
5
- // is left of today's when billing states a day window for the plan (the tier's
6
- // `windows`, lib/hanzo/tier.ts). It renders nothing for any other plan, or
7
- // while the plan is unread.
8
- import { useIam } from '@hanzo/iam/react';
9
- import { Text } from '@hanzo/ui';
10
- import { org } from './lib/session.js';
11
- import { useTier } from './lib/tier.js';
12
- /** The free rung's name in the billing catalog. */
13
- const FREE = 'free';
14
- export function Usage() {
15
- const { isAuthenticated } = useIam();
16
- const { tier } = useTier(isAuthenticated, org());
17
- if (tier?.tier?.name !== FREE)
18
- return null;
19
- const today = tier.windows?.find((w) => /day/i.test(w.span));
20
- return (_jsxs(Text, { role: "status", fontSize: "$1", color: "$soft", text: "center", children: ["Free \u00B7 limited, shared pool", today ? ` · ${today.remaining} of ${today.limit} left today` : ''] }));
21
- }
package/dist/meters.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { type Limits } from './lib/limits.js';
2
- /** What a spent month says. */
3
- export declare const SPENT = "Monthly included usage is used; Enso and Zen keep answering.";
4
- /** The plan holder's windows, or nothing where the limits name none. */
5
- export declare function Meters({ limits, large }: {
6
- limits: Limits;
7
- large?: boolean;
8
- }): import("react").JSX.Element;
package/dist/meters.js DELETED
@@ -1,31 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // A plan's windows as bars of what is LEFT: the five-hour session, the day and
4
- // the billing month, each with when it starts over. Shares only — a plan is a
5
- // subscription with limits, never a balance, so no count and no amount is
6
- // drawn. The month spent refuses nothing (Enso and Zen keep answering), which
7
- // is why it is a quiet line under the bars rather than a warning.
8
- //
9
- // One component for the composer's foot and for the usage page: `large` is the
10
- // page's size. Each bar is drawn above its words so the three bars stay on one
11
- // line however the words under them wrap on a phone.
12
- import { Progress, Text, XStack, YStack } from '@hanzo/ui';
13
- import { clock, day, left } from './lib/limits.js';
14
- /** What a spent month says. */
15
- export const SPENT = 'Monthly included usage is used; Enso and Zen keep answering.';
16
- const WINDOWS = [
17
- ['session', 'Session', (s) => (s.resets_at ? `resets ${clock(s.resets_at)}` : '')],
18
- ['day', 'Today', (s) => (s.resets_at ? `resets ${clock(s.resets_at)}` : '')],
19
- ['month', 'This month', (s) => (s.resets_at ? `renews ${day(s.resets_at)}` : '')],
20
- ];
21
- /** The plan holder's windows, or nothing where the limits name none. */
22
- export function Meters({ limits, large = false }) {
23
- const shown = WINDOWS.flatMap(([key, name, when]) => {
24
- const span = limits[key];
25
- return span ? [{ name, span, when: when(span) }] : [];
26
- });
27
- if (!shown.length)
28
- return null;
29
- const size = large ? '$3' : '$1';
30
- return (_jsxs(YStack, { gap: large ? '$4' : '$1.5', width: "100%", minW: 0, children: [_jsx(XStack, { gap: large ? '$6' : '$3', rowGap: "$4", width: "100%", flexWrap: large ? 'wrap' : 'nowrap', children: shown.map(({ name, span, when }) => (_jsxs(YStack, { flex: 1, minW: large ? 200 : 0, gap: large ? '$2' : '$1', children: [_jsx(Progress, { value: left(span), height: large ? 8 : 4, minW: 0, "aria-label": name }), _jsxs(XStack, { flexWrap: "wrap", columnGap: "$1.5", items: "baseline", minW: 0, children: [_jsx(Text, { fontSize: size, color: large ? '$ink' : '$soft', numberOfLines: 1, children: name }), _jsxs(XStack, { children: [_jsxs(Text, { fontSize: size, color: "$ink", fontWeight: "500", fontVariant: ['tabular-nums'], children: [left(span), "%"] }), _jsx(Text, { fontSize: size, color: "$ink", display: large ? 'flex' : 'none', $sm: { display: 'flex' }, children: "\u00A0left" })] }), when ? (_jsx(Text, { fontSize: large ? '$2' : '$1', color: "$soft", numberOfLines: 1, children: when })) : null] })] }, name))) }), limits.month && limits.month.percent >= 100 ? (_jsx(Text, { fontSize: large ? '$2' : '$1', color: "$soft", children: SPENT })) : null] }));
31
- }
package/dist/tours.d.ts DELETED
@@ -1,24 +0,0 @@
1
- /** One stop: what to point at, and what to say about it. */
2
- export interface Stop {
3
- id: string;
4
- title: string;
5
- body: string;
6
- /** Where the card sits relative to the anchor; it flips if it would leave the viewport. */
7
- side?: 'top' | 'bottom' | 'left' | 'right';
8
- /** Resolve the live element. Returning null skips the stop. */
9
- find: () => Element | null;
10
- /** Played rather than read: the scene, drawn on a stage instead of a card. */
11
- scene?: boolean;
12
- }
13
- export type Tour = 'chat' | 'meet' | 'calendar' | 'contacts' | 'inbox' | 'drive' | 'board';
14
- export declare const seen: (el: Element | null) => Element | null;
15
- export declare const byLabel: (...labels: string[]) => Element | null;
16
- export declare const byPlaceholder: (...values: string[]) => Element;
17
- export declare const byTitle: (...values: string[]) => Element;
18
- /** The smallest element whose own text is exactly this word. */
19
- export declare const byText: (text: string, within?: Element | null) => Element | null;
20
- /** The composer is the one visible textarea in the room. */
21
- export declare const composer: () => Element | null;
22
- /** The Enso control on the composer: effort, and the model override inside it. */
23
- export declare const modelChip: () => Element | null;
24
- export declare const TOURS: Record<Tour, Stop[]>;