@hanzo/rooms 0.1.22 → 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/Column.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The app's one column, the same in both modes: the Chat / Dev switch, a filter,
4
4
  // New, Dev's places, the Recents of the mode it is in, and at the foot the
5
- // Slack card and the account (components/workspace/Me.tsx).
5
+ // account (components/workspace/Me.tsx).
6
6
  //
7
7
  // It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
8
8
  // gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  // choice outlives the move.
12
12
  import { Text, XStack, YStack } from '@hanzo/gui';
13
13
  import { ChevronDown, ChevronUp, PanelLeft, Plus, Search } from 'lucide-react';
14
- import { Grip, nav, path, route, Slack } from '@hanzo/build';
14
+ import { Grip, nav, route } from '@hanzo/build';
15
15
  import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
16
16
  import { HanzoMark } from '@hanzogui/shell';
17
17
  import { useState } from 'react';
@@ -54,7 +54,6 @@ const ROW = { rounded: '$4', minH: 34 };
54
54
  export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
55
55
  const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
56
56
  const collapsed = kept && !drawer;
57
- const [slack, setSlack] = useKept('hanzo.build.slack', false);
58
57
  const [unfolded, setUnfolded] = useState(false);
59
58
  const [words, setWords] = useState('');
60
59
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
@@ -83,7 +82,7 @@ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false,
83
82
  setWords('');
84
83
  }, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
85
84
  ? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
86
- : null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {}), host.person && !slack ? (_jsx(YStack, { "data-slot": "app-slack", children: _jsx(Slack, { onOpen: () => dev(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) }) })) : null] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
85
+ : null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
87
86
  // In the gutter between the column and the pane, not over the column's own rows.
88
87
  r: GUTTER_END, width: "var(--pane-gap)" }))] }));
89
88
  }
package/dist/Profile.js CHANGED
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The reader's own profile, in the column beside the room.
4
4
  //
5
5
  // A WORKSPACE OPENS A PROFILE IN A PANEL, not a page. The page exists — it is
6
- // /account, and it is where the whole record is edited — but reaching it from a
6
+ // Settings (/settings), where the whole record is edited — but reaching it from a
7
7
  // conversation costs the conversation: the room, the sidebar and the rail all
8
8
  // go, and coming back is a navigation rather than a close. Slack, Teams and
9
9
  // every app shaped like them put this in the right column for that reason, and
@@ -26,7 +26,7 @@ import { mark } from './lib/mark.js';
26
26
  import { BAD } from './lib/mix.js';
27
27
  /** Slack's is 380 and the thread panel that shares this slot is the same. */
28
28
  const WIDTH = 380;
29
- const ACCOUNT = '/account';
29
+ const ACCOUNT = '/settings';
30
30
  export function Profile({ open, onClose }) {
31
31
  const { Link } = useRooms();
32
32
  const { user, updateUserProfile } = useAccount();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.22",
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",
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/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[]>;
package/dist/tours.js DELETED
@@ -1,319 +0,0 @@
1
- 'use client';
2
- // The tours, one per room, and the finders they point with.
3
- //
4
- // A stop RESOLVES A LIVE ELEMENT by what it already carries — its label, its
5
- // role, its text — never a marker sprinkled for the tour. A stop whose anchor is
6
- // absent is skipped, so each tour describes the room as it is that day.
7
- import { brand } from './where.js';
8
- export const seen = (el) => {
9
- if (!el)
10
- return null;
11
- const r = el.getBoundingClientRect();
12
- if (r.width < 6 || r.height < 6)
13
- return null;
14
- const s = getComputedStyle(el);
15
- if (s.visibility === 'hidden' || s.display === 'none' || Number(s.opacity) === 0)
16
- return null;
17
- return el;
18
- };
19
- export const byLabel = (...labels) => {
20
- for (const l of labels) {
21
- const el = seen(document.querySelector(`[aria-label="${l}"]`));
22
- if (el)
23
- return el;
24
- }
25
- return null;
26
- };
27
- const byAttr = (attr, ...values) => {
28
- for (const v of values) {
29
- const el = seen(document.querySelector(`[${attr}="${v}"]`));
30
- if (el)
31
- return el;
32
- }
33
- return null;
34
- };
35
- export const byPlaceholder = (...values) => byAttr('placeholder', ...values);
36
- export const byTitle = (...values) => byAttr('title', ...values);
37
- /** The smallest element whose own text is exactly this word. */
38
- export const byText = (text, within) => {
39
- const root = within ?? document.body;
40
- let best = null;
41
- for (const el of root.querySelectorAll('button,a,[role="button"]')) {
42
- if ((el.textContent || '').trim() !== text)
43
- continue;
44
- const ok = seen(el);
45
- if (!ok)
46
- continue;
47
- if (!best || el.getBoundingClientRect().width < best.getBoundingClientRect().width)
48
- best = el;
49
- }
50
- return best;
51
- };
52
- /** A field whose placeholder starts with these words. */
53
- const byPlaceholderStart = (start) => {
54
- for (const el of document.querySelectorAll('input,textarea')) {
55
- if ((el.getAttribute('placeholder') || '').startsWith(start) && seen(el))
56
- return el;
57
- }
58
- return null;
59
- };
60
- /** The composer is the one visible textarea in the room. */
61
- export const composer = () => {
62
- const all = [...document.querySelectorAll('textarea')].map(seen).filter(Boolean);
63
- return all.sort((a, b) => b.getBoundingClientRect().width - a.getBoundingClientRect().width)[0] ?? null;
64
- };
65
- /** The Enso control on the composer: effort, and the model override inside it. */
66
- export const modelChip = () => seen(document.querySelector('[aria-label^="Enso"]'));
67
- /** A place on the rail, by the word under its icon. */
68
- const rail = (label) => {
69
- const b = byText(label);
70
- return b?.parentElement ?? b;
71
- };
72
- const CHAT = [
73
- {
74
- id: 'composer',
75
- get title() {
76
- return `What are you interested in? How can ${brand()} help you?`;
77
- },
78
- body: 'Type what you want in plain words. Paste an error, describe a feature, or drop in a file.',
79
- side: 'top',
80
- find: composer,
81
- },
82
- {
83
- id: 'model',
84
- title: 'Perfectly tuned with 500+ of the leading models',
85
- body: 'Enso reads your question and picks the best model for it. Tap here to choose one yourself.',
86
- side: 'top',
87
- find: modelChip,
88
- },
89
- {
90
- id: 'work',
91
- title: 'Audit the work-flow',
92
- body: "Show work opens the AI's steps and the tools it used. Turn it on when an answer surprises you.",
93
- side: 'top',
94
- find: () => byText('Show work') || byText('Hide work'),
95
- },
96
- {
97
- id: 'scene',
98
- title: 'More than chat',
99
- body: 'Chat with a bot, bring in the team, and take it to a call. Watch one happen.',
100
- side: 'right',
101
- scene: true,
102
- find: () => rail('Chat'),
103
- },
104
- {
105
- id: 'roster',
106
- title: 'People and agents, one list',
107
- body: 'Your team and your AI agents sit together. Click any of them to start talking. An agent answers like a person.',
108
- side: 'right',
109
- find: () => {
110
- const az = byLabel('A-Z', 'Sort A-Z');
111
- if (az?.parentElement)
112
- return az.parentElement;
113
- const dir = byText('Directory');
114
- return dir?.parentElement ?? dir;
115
- },
116
- },
117
- {
118
- id: 'find',
119
- title: 'Find anything',
120
- body: 'Press ⌘K to search chats, people, agents and every section at once.',
121
- side: 'bottom',
122
- find: () => {
123
- for (const b of document.querySelectorAll('button,[role="button"]')) {
124
- if (/^Search\b/.test((b.textContent || '').trim()) && seen(b))
125
- return b;
126
- }
127
- return null;
128
- },
129
- },
130
- {
131
- id: 'aside',
132
- title: 'Results stay beside you',
133
- body: 'Files, sources and previews collect in the side panel, so the chat stays easy to read.',
134
- side: 'bottom',
135
- find: () => byLabel('Open side panel', 'Close side panel'),
136
- },
137
- {
138
- id: 'orgs',
139
- title: 'Switch workspaces',
140
- body: 'Each icon on the far left is a workspace, with its own chats, agents, files and billing.',
141
- side: 'right',
142
- find: () => {
143
- // The rail is a column of small square marks pinned to the far edge —
144
- // that shape is what identifies it, not a label, because each mark is
145
- // labelled with its own workspace ("LUX", "ZOO", an initial) and none of
146
- // them names the rail. Take their common parent.
147
- const marks = [...document.querySelectorAll('button,[role="button"]')].filter((el) => {
148
- const r = el.getBoundingClientRect();
149
- return r.left < 64 && r.width >= 26 && r.width <= 60 && Math.abs(r.width - r.height) < 10 && seen(el);
150
- });
151
- if (marks.length < 2)
152
- return byLabel('Switch organization');
153
- let node = marks[0].parentElement;
154
- while (node && !marks.every((m) => node.contains(m)))
155
- node = node.parentElement;
156
- return seen(node) ?? marks[0];
157
- },
158
- },
159
- {
160
- id: 'account',
161
- title: 'Your plan',
162
- body: 'See your credit, manage payment and change settings. Free uses the open models. Paid unlocks the rest.',
163
- side: 'top',
164
- find: () => byLabel('Account, plan and workspace'),
165
- },
166
- ];
167
- const MEET = [
168
- {
169
- id: 'start',
170
- title: 'Start a meeting',
171
- body: 'One click opens a room. Share the link and anyone can join from a browser.',
172
- side: 'bottom',
173
- find: () => byText('Start Instant Meeting'),
174
- },
175
- {
176
- id: 'join',
177
- title: 'Join with a code',
178
- body: 'Paste a meeting code or link here to join a call someone else started.',
179
- side: 'bottom',
180
- find: () => byPlaceholder('Enter meeting code or link'),
181
- },
182
- {
183
- id: 'transcript',
184
- title: 'Every call is written down',
185
- body: 'A Hanzo bot sits in and transcribes. What everyone said is saved with the meeting.',
186
- side: 'left',
187
- find: () => {
188
- for (const el of document.querySelectorAll('div,p,section')) {
189
- if (el.childElementCount === 0 && /transcription/i.test(el.textContent || '') && seen(el))
190
- return el.parentElement ?? el;
191
- }
192
- return null;
193
- },
194
- },
195
- ];
196
- const CALENDAR = [
197
- {
198
- id: 'schedule',
199
- title: 'Add an event or a routine',
200
- body: 'A meeting, a reminder, or a job an agent runs on a schedule. Name it and pick the time.',
201
- side: 'bottom',
202
- find: () => byText('Schedule Event / Routine'),
203
- },
204
- {
205
- id: 'crons',
206
- title: 'Bots on a timer',
207
- body: 'Bot Crons lists what your bots run and when. Change a time here and the bot follows it.',
208
- side: 'bottom',
209
- find: () => byText('Bot Crons'),
210
- },
211
- {
212
- id: 'booker',
213
- title: 'Let people book you',
214
- body: 'A public page where anyone can pick a free time on your calendar.',
215
- side: 'bottom',
216
- find: () => byText('Public Booker'),
217
- },
218
- ];
219
- const CONTACTS = [
220
- {
221
- id: 'search',
222
- title: 'Everyone in one place',
223
- body: 'People and agents, side by side. Search by name to find anyone fast.',
224
- side: 'bottom',
225
- find: () => byLabel('Search contacts') || byPlaceholder('Search'),
226
- },
227
- {
228
- id: 'invite',
229
- title: 'Invite your team',
230
- body: 'Send an invite by email, or share a link anyone can use to join your organization.',
231
- side: 'bottom',
232
- find: () => byLabel('Invite') || byText('Invite'),
233
- },
234
- {
235
- id: 'agent',
236
- title: 'Make an agent',
237
- body: 'Give it a name and a job. It joins the roster next to your people and answers like one.',
238
- side: 'bottom',
239
- find: () => byText('New agent'),
240
- },
241
- ];
242
- const INBOX = [
243
- {
244
- id: 'connect',
245
- title: 'Every channel, one inbox',
246
- body: 'Connect Slack, email, Discord and the rest. Their messages land here together.',
247
- side: 'bottom',
248
- find: () => byText('Connect an app'),
249
- },
250
- {
251
- id: 'channels',
252
- title: 'Your channels',
253
- body: 'Each connected app is a channel on the left. Nothing gets lost between them.',
254
- side: 'right',
255
- find: () => byLabel('Resize inbox channel list')?.parentElement ?? null,
256
- },
257
- {
258
- id: 'reply',
259
- title: 'Reply where it came from',
260
- body: 'Answer here and it goes back on the same channel the message arrived on.',
261
- side: 'top',
262
- find: () => byPlaceholderStart('Reply on'),
263
- },
264
- ];
265
- const DRIVE = [
266
- {
267
- id: 'search',
268
- title: 'Search everything',
269
- body: 'Every file your team and your agents made, found by name or by what is inside it.',
270
- side: 'bottom',
271
- find: () => byLabel('Search Drive'),
272
- },
273
- {
274
- id: 'upload',
275
- title: 'Bring files in',
276
- body: 'Upload anything. Agents can read it, and it shows up in chat when you ask.',
277
- side: 'bottom',
278
- find: () => byTitle('Upload'),
279
- },
280
- {
281
- id: 'folder',
282
- title: 'Keep it tidy',
283
- body: 'Make folders for projects, clients or agents. Drag files between them.',
284
- side: 'bottom',
285
- find: () => byTitle('Add Folder'),
286
- },
287
- ];
288
- const BOARD = [
289
- {
290
- id: 'add',
291
- title: 'Add a card',
292
- body: 'One card per task. Give it a date, a priority, and a person or an agent to do it.',
293
- side: 'bottom',
294
- find: () => byText('Add a card') || byPlaceholder('Add an item'),
295
- },
296
- {
297
- id: 'planner',
298
- title: 'Plan by day',
299
- body: 'Planner lays the cards out by date, so today is one column and next week is a glance away.',
300
- side: 'bottom',
301
- find: () => byText('Planner'),
302
- },
303
- {
304
- id: 'filter',
305
- title: 'See only what matters now',
306
- body: 'Filter by who, by when, or by what is finished.',
307
- side: 'bottom',
308
- find: () => byText('Filter'),
309
- },
310
- ];
311
- export const TOURS = {
312
- chat: CHAT,
313
- meet: MEET,
314
- calendar: CALENDAR,
315
- contacts: CONTACTS,
316
- inbox: INBOX,
317
- drive: DRIVE,
318
- board: BOARD,
319
- };