@hanzo/rooms 0.1.96 → 0.1.98
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/App.js +33 -37
- package/dist/Chrome.js +3 -1
- package/dist/Column.d.ts +5 -3
- package/dist/Column.js +12 -19
- package/dist/Drawer.d.ts +55 -0
- package/dist/Drawer.js +334 -0
- package/dist/Me.d.ts +2 -2
- package/dist/Me.js +12 -5
- package/dist/Orgs.js +1 -1
- package/dist/Palette.js +1 -1
- package/dist/ProviderMark.js +5 -2
- package/dist/Rail.d.ts +3 -3
- package/dist/Rail.js +33 -16
- package/dist/Recents.js +4 -2
- package/dist/Settings.js +3 -1
- package/dist/Top.js +5 -2
- package/dist/dev.js +14 -6
- package/dist/index.js +0 -1
- package/dist/lib/signout.d.ts +5 -0
- package/dist/lib/signout.js +7 -1
- package/dist/slide.d.ts +24 -0
- package/dist/slide.js +48 -0
- package/package.json +5 -2
package/dist/Drawer.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// A phone's menu (App.tsx, under gui's `md`), drawn the way the ChatGPT iOS app
|
|
4
|
+
// draws its own: the menu stands BEHIND, on the window's subdued floor, and the
|
|
5
|
+
// pane slides toward the END side over it, raised — the pane's own brighter
|
|
6
|
+
// material, its start corners rounded, a drop falling on the menu. A strip of
|
|
7
|
+
// the pane stays in sight; a press on it, or a pull back toward the start, slides
|
|
8
|
+
// it home.
|
|
9
|
+
//
|
|
10
|
+
// ┌──────────────────────────┬────╮
|
|
11
|
+
// │ Hanzo AI 🔔 ⌕ │ │ the pane: STRIP px of it, inert but for
|
|
12
|
+
// │ ✎ New chat │ │ the press that closes (`drawer-close`)
|
|
13
|
+
// │ ◇ Studio ⌨ Tabs ⋯ More │ │
|
|
14
|
+
// │ Pinned … Recents … │ │
|
|
15
|
+
// │ (ZK) (✎ Chat) │ │ the foot: the account and the accent New chat
|
|
16
|
+
// └──────────────────────────┴────╯
|
|
17
|
+
//
|
|
18
|
+
// Three states, because the slide home has to be seen: shut (no menu, the pane
|
|
19
|
+
// as it always is), open, and closing (the menu still drawn, the pane sliding
|
|
20
|
+
// home), after which the menu unmounts. The pane's offset is a transform, so the
|
|
21
|
+
// slide is the compositor's; under reduced motion it is not animated at all.
|
|
22
|
+
//
|
|
23
|
+
// THE SLIDE IS LOGICAL but for its sign (slide.ts): the menu stands at the
|
|
24
|
+
// start, the strip at the end, both by logical insets; the transform's sign is
|
|
25
|
+
// read off the pane's own `direction` as the menu opens, so an RTL page (by
|
|
26
|
+
// @hanzo/i18n or the host's own `dir`) slides the pane left, and a drag reads
|
|
27
|
+
// along the line the same way.
|
|
28
|
+
//
|
|
29
|
+
// WHILE IT IS OUT the window behind it is inert (`inert` on every other child of
|
|
30
|
+
// the window) and the menu is a modal dialog: focus moves into it as it opens and
|
|
31
|
+
// back to what opened it as it shuts; Escape closes it, unless a menu or dialog
|
|
32
|
+
// it opened holds the focus; Back closes it, because opening it pushed an entry
|
|
33
|
+
// of the same address that Back pops. A move made from inside it takes that
|
|
34
|
+
// entry's place (`depart`), so the history holds the move and never the menu.
|
|
35
|
+
// Its list's scroll stays its own (Recents' `overscrollBehavior`), and the pane,
|
|
36
|
+
// never unmounted, keeps its own.
|
|
37
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
38
|
+
import { Text, XStack, YStack, useMedia } from '@hanzo/gui';
|
|
39
|
+
import { SquarePen } from 'lucide-react';
|
|
40
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
41
|
+
import { Me } from './Me.js';
|
|
42
|
+
import { axis, OPEN, pull, settle, sign } from './slide.js';
|
|
43
|
+
/**
|
|
44
|
+
* The pane's offset as a transform: `--drawer-way` the end side's sign,
|
|
45
|
+
* `--drawer-at` the distance, both written on the pane by the drawer (a drag
|
|
46
|
+
* rewrites the distance every frame, no React commit). Its start corners round.
|
|
47
|
+
*/
|
|
48
|
+
const SLIDE = {
|
|
49
|
+
transform: 'translateX(calc(var(--drawer-way, 1) * var(--drawer-at, 0px)))',
|
|
50
|
+
borderStartStartRadius: 'var(--radius-composer)',
|
|
51
|
+
borderEndStartRadius: 'var(--radius-composer)',
|
|
52
|
+
};
|
|
53
|
+
/** The slide's motion: the system's base duration on its entrance curve. */
|
|
54
|
+
const MOTION = 'transform var(--duration-base) var(--ease-emphasis)';
|
|
55
|
+
/** The element a gui stack hands its ref, on the web. */
|
|
56
|
+
const element = (node) => (node instanceof HTMLElement ? node : null);
|
|
57
|
+
/** The reader asked for no motion. */
|
|
58
|
+
function useStill() {
|
|
59
|
+
const [still, setStill] = useState(false);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const query = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
62
|
+
if (!query)
|
|
63
|
+
return;
|
|
64
|
+
const read = () => setStill(query.matches);
|
|
65
|
+
read();
|
|
66
|
+
query.addEventListener('change', read);
|
|
67
|
+
return () => query.removeEventListener('change', read);
|
|
68
|
+
}, []);
|
|
69
|
+
return still;
|
|
70
|
+
}
|
|
71
|
+
/** The pane's distance from home, px, measured: where it stands against the layer, which stands where it rests. */
|
|
72
|
+
const travel = (pane, layer) => Math.abs(pane.getBoundingClientRect().left - layer.getBoundingClientRect().left);
|
|
73
|
+
export function useDrawer() {
|
|
74
|
+
const [state, setState] = useState('shut');
|
|
75
|
+
// Which way the end side is; read as the menu opens.
|
|
76
|
+
const [way, setWay] = useState(1);
|
|
77
|
+
const still = useStill();
|
|
78
|
+
const media = useMedia();
|
|
79
|
+
const win = useRef(null);
|
|
80
|
+
const pane = useRef(null);
|
|
81
|
+
const layer = useRef(null);
|
|
82
|
+
const opener = useRef(null);
|
|
83
|
+
// The entry opening pushed is the one the history stands on.
|
|
84
|
+
const marked = useRef(false);
|
|
85
|
+
const home = useCallback((now = false) => setState((s) => (s === 'shut' || now || still ? 'shut' : 'closing')), [still]);
|
|
86
|
+
const open = useCallback(() => {
|
|
87
|
+
const el = pane.current;
|
|
88
|
+
setWay(sign(el ? getComputedStyle(el).direction : 'ltr'));
|
|
89
|
+
const at = document.activeElement;
|
|
90
|
+
if (at instanceof HTMLElement && at !== document.body)
|
|
91
|
+
opener.current = at;
|
|
92
|
+
if (!marked.current) {
|
|
93
|
+
window.history.pushState(window.history.state, '');
|
|
94
|
+
marked.current = true;
|
|
95
|
+
}
|
|
96
|
+
setState('open');
|
|
97
|
+
}, []);
|
|
98
|
+
const close = useCallback(() => {
|
|
99
|
+
if (marked.current) {
|
|
100
|
+
marked.current = false;
|
|
101
|
+
window.history.back();
|
|
102
|
+
}
|
|
103
|
+
home();
|
|
104
|
+
}, [home]);
|
|
105
|
+
const depart = useCallback((move) => {
|
|
106
|
+
const replace = marked.current;
|
|
107
|
+
marked.current = false;
|
|
108
|
+
const before = window.location.href;
|
|
109
|
+
move(replace);
|
|
110
|
+
// Nothing moved (the reader was there already): the menu's entry is popped instead.
|
|
111
|
+
if (replace && window.location.href === before)
|
|
112
|
+
window.history.back();
|
|
113
|
+
home();
|
|
114
|
+
}, [home]);
|
|
115
|
+
const drop = useCallback(() => {
|
|
116
|
+
marked.current = false;
|
|
117
|
+
home();
|
|
118
|
+
}, [home]);
|
|
119
|
+
// Back closes it: the entry it pushed is the one popped.
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (state !== 'open')
|
|
122
|
+
return;
|
|
123
|
+
const back = () => {
|
|
124
|
+
if (!marked.current)
|
|
125
|
+
return;
|
|
126
|
+
marked.current = false;
|
|
127
|
+
home();
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener('popstate', back);
|
|
130
|
+
return () => window.removeEventListener('popstate', back);
|
|
131
|
+
}, [state, home]);
|
|
132
|
+
// Escape closes it, unless a menu or dialog it opened holds the focus: that one closes first.
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (state !== 'open')
|
|
135
|
+
return;
|
|
136
|
+
const key = (e) => {
|
|
137
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
138
|
+
return;
|
|
139
|
+
const at = document.activeElement;
|
|
140
|
+
if (at && at !== document.body && !layer.current?.contains(at))
|
|
141
|
+
return;
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
close();
|
|
144
|
+
};
|
|
145
|
+
document.addEventListener('keydown', key);
|
|
146
|
+
return () => document.removeEventListener('keydown', key);
|
|
147
|
+
}, [state, close]);
|
|
148
|
+
// A window that grows past a phone has no drawer: it goes at once.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!media.md || state === 'shut')
|
|
151
|
+
return;
|
|
152
|
+
if (marked.current) {
|
|
153
|
+
marked.current = false;
|
|
154
|
+
window.history.back();
|
|
155
|
+
}
|
|
156
|
+
home(true);
|
|
157
|
+
}, [media.md, state, home]);
|
|
158
|
+
// Everything in the window but the menu is inert while it is out, and the focus goes in.
|
|
159
|
+
useLayoutEffect(() => {
|
|
160
|
+
const w = win.current;
|
|
161
|
+
const out = state !== 'shut';
|
|
162
|
+
if (w)
|
|
163
|
+
for (const child of Array.from(w.children))
|
|
164
|
+
if (child !== layer.current && child instanceof HTMLElement)
|
|
165
|
+
child.inert = out;
|
|
166
|
+
if (state === 'open')
|
|
167
|
+
layer.current?.focus({ preventScroll: true });
|
|
168
|
+
if (state !== 'shut')
|
|
169
|
+
return;
|
|
170
|
+
// Shut: the focus goes back to what opened it, where nothing else took it.
|
|
171
|
+
const back = opener.current;
|
|
172
|
+
opener.current = null;
|
|
173
|
+
const at = document.activeElement;
|
|
174
|
+
if (back?.isConnected && (!at || at === document.body))
|
|
175
|
+
back.focus({ preventScroll: true });
|
|
176
|
+
}, [state]);
|
|
177
|
+
// The slide, written on the pane: its way, its motion, and how far it stands from home.
|
|
178
|
+
useLayoutEffect(() => {
|
|
179
|
+
const s = pane.current?.style;
|
|
180
|
+
if (!s)
|
|
181
|
+
return;
|
|
182
|
+
if (state === 'shut') {
|
|
183
|
+
for (const p of ['--drawer-at', '--drawer-way', 'transition'])
|
|
184
|
+
s.removeProperty(p);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
s.setProperty('--drawer-way', String(way));
|
|
188
|
+
s.setProperty('transition', still ? 'none' : MOTION);
|
|
189
|
+
s.setProperty('--drawer-at', state === 'open' ? OPEN : '0px');
|
|
190
|
+
}, [state, way, still]);
|
|
191
|
+
// Closing ends when the slide does, or after it would have.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (state !== 'closing')
|
|
194
|
+
return;
|
|
195
|
+
const el = pane.current;
|
|
196
|
+
const done = () => setState((s) => (s === 'closing' ? 'shut' : s));
|
|
197
|
+
const end = (e) => {
|
|
198
|
+
if (e.target === el && e.propertyName === 'transform')
|
|
199
|
+
done();
|
|
200
|
+
};
|
|
201
|
+
el?.addEventListener('transitionend', end);
|
|
202
|
+
const late = window.setTimeout(done, 700);
|
|
203
|
+
return () => {
|
|
204
|
+
el?.removeEventListener('transitionend', end);
|
|
205
|
+
window.clearTimeout(late);
|
|
206
|
+
};
|
|
207
|
+
}, [state]);
|
|
208
|
+
// A finger on the menu or the strip pulls the pane along the line; a scroll stays a scroll.
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const el = pane.current;
|
|
211
|
+
const over = layer.current;
|
|
212
|
+
if (state !== 'open' || !el || !over)
|
|
213
|
+
return;
|
|
214
|
+
let grip = null;
|
|
215
|
+
let pulling = false;
|
|
216
|
+
let at = 0;
|
|
217
|
+
let last = { along: 0, t: 0 };
|
|
218
|
+
let speed = 0;
|
|
219
|
+
const down = (e) => {
|
|
220
|
+
if (e.pointerType === 'mouse' && e.button !== 0)
|
|
221
|
+
return;
|
|
222
|
+
grip = { id: e.pointerId, x: e.clientX, y: e.clientY, reach: travel(el, over) };
|
|
223
|
+
pulling = false;
|
|
224
|
+
at = grip.reach;
|
|
225
|
+
last = { along: 0, t: e.timeStamp };
|
|
226
|
+
speed = 0;
|
|
227
|
+
};
|
|
228
|
+
const move = (e) => {
|
|
229
|
+
if (!grip || e.pointerId !== grip.id)
|
|
230
|
+
return;
|
|
231
|
+
const dx = e.clientX - grip.x;
|
|
232
|
+
if (!pulling) {
|
|
233
|
+
const a = axis(dx, e.clientY - grip.y);
|
|
234
|
+
if (a === null)
|
|
235
|
+
return;
|
|
236
|
+
if (a === 'y') {
|
|
237
|
+
grip = null;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
pulling = true;
|
|
241
|
+
over.setPointerCapture?.(e.pointerId);
|
|
242
|
+
el.style.setProperty('transition', 'none');
|
|
243
|
+
}
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
at = pull(grip.reach, dx, way, grip.reach);
|
|
246
|
+
const along = dx * way;
|
|
247
|
+
const dt = e.timeStamp - last.t;
|
|
248
|
+
if (dt > 0)
|
|
249
|
+
speed = (along - last.along) / dt;
|
|
250
|
+
last = { along, t: e.timeStamp };
|
|
251
|
+
el.style.setProperty('--drawer-at', `${at}px`);
|
|
252
|
+
};
|
|
253
|
+
const up = (e) => {
|
|
254
|
+
if (!grip || e.pointerId !== grip.id)
|
|
255
|
+
return;
|
|
256
|
+
const reach = grip.reach;
|
|
257
|
+
grip = null;
|
|
258
|
+
if (!pulling)
|
|
259
|
+
return;
|
|
260
|
+
pulling = false;
|
|
261
|
+
el.style.setProperty('transition', still ? 'none' : MOTION);
|
|
262
|
+
// The press that ends a pull is not a tap on the strip.
|
|
263
|
+
const swallow = (c) => {
|
|
264
|
+
c.stopPropagation();
|
|
265
|
+
c.preventDefault();
|
|
266
|
+
};
|
|
267
|
+
window.addEventListener('click', swallow, { capture: true, once: true });
|
|
268
|
+
window.setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 0);
|
|
269
|
+
if (e.type === 'pointerup' && !settle(at, reach, speed))
|
|
270
|
+
close();
|
|
271
|
+
else
|
|
272
|
+
el.style.setProperty('--drawer-at', OPEN);
|
|
273
|
+
};
|
|
274
|
+
over.addEventListener('pointerdown', down);
|
|
275
|
+
over.addEventListener('pointermove', move);
|
|
276
|
+
over.addEventListener('pointerup', up);
|
|
277
|
+
over.addEventListener('pointercancel', up);
|
|
278
|
+
return () => {
|
|
279
|
+
over.removeEventListener('pointerdown', down);
|
|
280
|
+
over.removeEventListener('pointermove', move);
|
|
281
|
+
over.removeEventListener('pointerup', up);
|
|
282
|
+
over.removeEventListener('pointercancel', up);
|
|
283
|
+
};
|
|
284
|
+
}, [state, way, still, close]);
|
|
285
|
+
return {
|
|
286
|
+
state,
|
|
287
|
+
open,
|
|
288
|
+
close,
|
|
289
|
+
depart,
|
|
290
|
+
drop,
|
|
291
|
+
window: useCallback((node) => {
|
|
292
|
+
win.current = element(node);
|
|
293
|
+
}, []),
|
|
294
|
+
pane: useCallback((node) => {
|
|
295
|
+
pane.current = element(node);
|
|
296
|
+
}, []),
|
|
297
|
+
layer: useCallback((node) => {
|
|
298
|
+
layer.current = element(node);
|
|
299
|
+
}, []),
|
|
300
|
+
slide: state === 'shut' ? null : SLIDE,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The pane while the menu is out: raised over the floor, its edge drawn and a
|
|
305
|
+
* drop falling on the menu, clipped to its corners. Spread on the pane beside
|
|
306
|
+
* `slide`; `Lift` is its fill.
|
|
307
|
+
*/
|
|
308
|
+
export const RAISED = {
|
|
309
|
+
z: 'var(--z-raised)',
|
|
310
|
+
overflow: 'hidden',
|
|
311
|
+
boxShadow: '0 0 0 1px var(--pane-edge), var(--shadow-floating)',
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* The raised pane's fill: the pane's own material composited opaque, a step
|
|
315
|
+
* brighter than the floor the menu stands on, in both themes. Drawn UNDER the
|
|
316
|
+
* pane's content (`--z-under` inside the pane's own stacking) rather than as the
|
|
317
|
+
* pane's background, which is the frame's and a host may paint it at rest.
|
|
318
|
+
*/
|
|
319
|
+
export function Lift() {
|
|
320
|
+
return _jsx(YStack, { "data-slot": "lift", position: "absolute", inset: 0, bg: "var(--pane-solid)", z: "var(--z-under)", pointerEvents: "none" });
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The menu's layer: the menu at the start on the floor, and over the strip of
|
|
324
|
+
* pane left in sight the press that closes it. Drawn while the drawer is not shut.
|
|
325
|
+
*/
|
|
326
|
+
export function Layer({ drawer, top = 0, children }) {
|
|
327
|
+
const { t } = useI18n();
|
|
328
|
+
return (_jsxs(YStack, { ref: drawer.layer, role: "dialog", "aria-modal": true, "aria-label": t('Menu'), tabIndex: -1, "data-slot": "drawer", "data-state": drawer.state, position: "absolute", t: top, b: 0, insetInlineStart: 0, insetInlineEnd: 0, outlineStyle: "none", $md: { display: 'none' }, children: [_jsx(YStack, { "data-slot": "menu", position: "absolute", t: 0, b: 0, insetInlineStart: 0, width: OPEN, bg: "var(--pane-floor)", pt: "var(--safe-top)", pb: "max(var(--pane-gap), var(--safe-bottom))", ...{ paddingInlineStart: 'max(var(--safe-left), var(--safe-right))', touchAction: 'pan-y' }, children: children }), drawer.state === 'open' ? (_jsx(YStack, { render: "button", "data-slot": "drawer-close", "aria-label": t('Close sidebar'), onPress: drawer.close, position: "absolute", t: 0, b: 0, insetInlineEnd: 0, z: "var(--z-panel)", bg: "transparent", borderWidth: 0, cursor: "pointer", ...{ insetInlineStart: OPEN, touchAction: 'none' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' } })) : null] }));
|
|
329
|
+
}
|
|
330
|
+
/** The menu's foot: the account (its menu opens above it) and the accent New chat. */
|
|
331
|
+
export function Foot({ host, onChat, onOrg }) {
|
|
332
|
+
const { t } = useI18n();
|
|
333
|
+
return (_jsxs(XStack, { "data-slot": "drawer-foot", items: "center", justify: "space-between", gap: "$2", pt: "$2", px: "$1", shrink: 0, children: [_jsx(Me, { host: host, narrow: true, above: true, onOrg: onOrg }), _jsxs(XStack, { render: "button", "data-slot": "drawer-chat", "aria-label": t('New chat'), onPress: onChat, height: 44, px: "$4", gap: "$2", rounded: 9999, items: "center", justify: "center", bg: "var(--accent)", borderWidth: 0, cursor: "pointer", hoverStyle: { bg: 'var(--accent-hover)' }, pressStyle: { bg: 'var(--accent-hover)' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(SquarePen, { size: 18, color: "var(--accent-foreground)", "aria-hidden": true }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "var(--accent-foreground)", children: t('Chat') })] })] }));
|
|
334
|
+
}
|
package/dist/Me.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare function Me({ host, narrow, above, onOrg, }: {
|
|
|
26
26
|
/** The rail's: the face alone, the menu opening beside it. */
|
|
27
27
|
narrow?: boolean;
|
|
28
28
|
/**
|
|
29
|
-
* The menu opens above the face, not beside it: the
|
|
30
|
-
*
|
|
29
|
+
* The menu opens above the face, not beside it: the foot of a phone's menu
|
|
30
|
+
* (Drawer.tsx). That copy leaves ⌘/ to the rail's own, so one press opens one dialog.
|
|
31
31
|
*/
|
|
32
32
|
above?: boolean;
|
|
33
33
|
/** Work in another organization: picked, then the app opened in it. */
|
package/dist/Me.js
CHANGED
|
@@ -28,6 +28,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
28
28
|
// the plan from billing and the limits. An organization is created by the same
|
|
29
29
|
// `POST /v1/account/orgs` the no-organization gate uses (Orgs.tsx); the next load
|
|
30
30
|
// mints a token that carries it.
|
|
31
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
31
32
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
32
33
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
33
34
|
import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, FlaskConical, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
@@ -135,6 +136,7 @@ export const upgrade = (family) => family === 'Enterprise' ? 'Enterprise plan' :
|
|
|
135
136
|
/** The keys a row prints, in this platform's spelling. */
|
|
136
137
|
const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
137
138
|
export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
139
|
+
const { t } = useI18n();
|
|
138
140
|
const [open, setOpen] = useState(false);
|
|
139
141
|
const [account, setAccount] = useState(false);
|
|
140
142
|
const [help, setHelp] = useState(false);
|
|
@@ -197,11 +199,14 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
197
199
|
// Clear of the rail's mark (or the row) it opens from, and flush with its foot.
|
|
198
200
|
offset: 8, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
|
|
199
201
|
row.current = el instanceof HTMLElement ? el : null;
|
|
200
|
-
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48,
|
|
202
|
+
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48,
|
|
203
|
+
// A thumb's 44 on a touch screen.
|
|
204
|
+
$touchable: narrow ? { width: 44, minH: 44 } : undefined, 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 ? 26 : 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 }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": t("Account"), onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: t("Organizations") }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: t("Create organization") }) }), _jsx(DropdownMenuItem, { onSelect: () => out(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: t("Organization settings") }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: t("Open in Hanzo Team") }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: t("Create a team") }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: t("All organizations") }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: t("Get Pro") })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: t("Personalization") }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: t("Profile") }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: t("Settings") }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: t("Beta") }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: t("Help") }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: t("Keyboard shortcuts") }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: t("Log out") }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
201
205
|
}
|
|
202
206
|
/** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
|
|
203
207
|
function Shortcuts({ open, onOpenChange, apple }) {
|
|
204
|
-
|
|
208
|
+
const { t } = useI18n();
|
|
209
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: t("Keyboard shortcuts") }), _jsx(YStack, { gap: "$1", children: KEYS.map((k) => (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", minH: 32, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: k.does }), _jsx(Kbd, { children: apple ? k.mac : k.pc })] }, k.does))) })] }) }));
|
|
205
210
|
}
|
|
206
211
|
/**
|
|
207
212
|
* The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
|
|
@@ -210,6 +215,7 @@ function Shortcuts({ open, onOpenChange, apple }) {
|
|
|
210
215
|
* it yet.
|
|
211
216
|
*/
|
|
212
217
|
function Create({ open, onOpenChange }) {
|
|
218
|
+
const { t } = useI18n();
|
|
213
219
|
const [name, setName] = useState('');
|
|
214
220
|
const [busy, setBusy] = useState(false);
|
|
215
221
|
const [refused, setRefused] = useState('');
|
|
@@ -244,16 +250,17 @@ function Create({ open, onOpenChange }) {
|
|
|
244
250
|
setBusy(false);
|
|
245
251
|
}
|
|
246
252
|
};
|
|
247
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Create organization" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "You own it. Agents, credits and members belong to it, and you switch to it from this menu." }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "Organization name", placeholder: "Acme", maxLength: 80, onKeyDown: (e) => {
|
|
253
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: t("Create organization") }), _jsx(SizableText, { size: "$2", color: "$soft", children: t("You own it. Agents, credits and members belong to it, and you switch to it from this menu.") }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": t("Organization name"), placeholder: t("Acme"), maxLength: 80, onKeyDown: (e) => {
|
|
248
254
|
if (e.key === 'Enter')
|
|
249
255
|
void submit();
|
|
250
|
-
} }), refused ? (_jsx(SizableText, { size: "$1", color: BAD, role: "alert", children: refused })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", rounded: 999, disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] }) }));
|
|
256
|
+
} }), refused ? (_jsx(SizableText, { size: "$1", color: BAD, role: "alert", children: refused })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: t("Cancel") }), _jsx(Button, { size: "sm", rounded: 999, disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] }) }));
|
|
251
257
|
}
|
|
252
258
|
/**
|
|
253
259
|
* What became of an organization this tab created: nothing to say once the token
|
|
254
260
|
* carries it (the app is already in it), and a plain line while it does not.
|
|
255
261
|
*/
|
|
256
262
|
export function Made() {
|
|
263
|
+
const { t } = useI18n();
|
|
257
264
|
const [made, setMade] = useState(null);
|
|
258
265
|
useEffect(() => {
|
|
259
266
|
try {
|
|
@@ -284,5 +291,5 @@ export function Made() {
|
|
|
284
291
|
}
|
|
285
292
|
setMade(null);
|
|
286
293
|
};
|
|
287
|
-
return (_jsxs(XStack, { "data-slot": "made", role: "status", items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$5", borderWidth: 1, borderColor: "$borderColor", bg: "$edge", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$ink", children: `${made.name} was created, and your sign-in does not include it yet, so you are still in this organization. It appears here once IAM adds you to it.` }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: dismiss, p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 13, "aria-hidden": true }) })] }));
|
|
294
|
+
return (_jsxs(XStack, { "data-slot": "made", role: "status", items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$5", borderWidth: 1, borderColor: "$borderColor", bg: "$edge", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$ink", children: `${made.name} was created, and your sign-in does not include it yet, so you are still in this organization. It appears here once IAM adds you to it.` }), _jsx(XStack, { render: "button", "aria-label": t("Dismiss"), onPress: dismiss, p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 13, "aria-hidden": true }) })] }));
|
|
288
295
|
}
|
package/dist/Orgs.js
CHANGED
|
@@ -277,7 +277,7 @@ function useArrival(door, hydrated, isLoading, isAuthenticated) {
|
|
|
277
277
|
const SHEET = 'calc(var(--z-modal) + 10)';
|
|
278
278
|
const DIALOG = 'calc(var(--z-modal) + 20)';
|
|
279
279
|
/** The sheet's top inset: a share of the window's height, held between two lengths. */
|
|
280
|
-
const TOP = 'clamp(
|
|
280
|
+
const TOP = 'clamp(72px, 12vh, 120px)';
|
|
281
281
|
const muted = 'var(--muted-foreground)';
|
|
282
282
|
/** The black that veils and shadows, at a share of itself. */
|
|
283
283
|
const shade = (share) => mix('var(--pure-black)', share, 'srgb');
|
package/dist/Palette.js
CHANGED
|
@@ -79,7 +79,7 @@ export function places(host, go) {
|
|
|
79
79
|
{ id: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { ...ICON }), run: () => go(TERMINALS) },
|
|
80
80
|
...(host ? [{ id: 'team', label: 'Team', icon: _jsx(Users, { ...ICON }), keywords: ['workspace'], run: () => out(WORKSPACE) }] : []),
|
|
81
81
|
...(host ? rows(nav(host, builder).map((g) => ({ links: g.links.filter((l) => l.id !== NEW) })), '', 'dev') : []),
|
|
82
|
-
{ id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go(
|
|
82
|
+
{ id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go('/settings') },
|
|
83
83
|
// The site's own pages, outside the app: the browser loads them.
|
|
84
84
|
{ id: 'referral', label: 'Referral', icon: _jsx(Gift, { ...ICON }), keywords: ['invite', 'share'], run: () => window.location.assign(site('/referral')) },
|
|
85
85
|
{ id: 'beta', label: 'Beta status', icon: _jsx(FlaskConical, { ...ICON }), keywords: ['waitlist'], run: () => window.location.assign(site('/beta/status')) },
|
package/dist/ProviderMark.js
CHANGED
|
@@ -107,7 +107,8 @@ const MARKS = {
|
|
|
107
107
|
// disc (zoo-disc.svg): top green, bottom-left red, bottom-right blue, whose
|
|
108
108
|
// pairwise overlaps are the secondaries the full disc shows. The enclosing
|
|
109
109
|
// ring stays `currentColor` — it is the frame, not one of the three, and a
|
|
110
|
-
// fourth hue there would read as a fourth petal.
|
|
110
|
+
// fourth hue there would read as a fourth petal. Under `ink` the petals'
|
|
111
|
+
// strokes come off (`INKED`) and they take the group's `currentColor`.
|
|
111
112
|
'<defs><clipPath id="zoo-cut"><circle cx="12" cy="12" r="11.5"></circle></clipPath></defs><g clip-path="url(#zoo-cut)" fill="none" stroke="currentColor"><circle cx="12.203" cy="6.27" r="9.509" stroke-width="1.341" stroke="#00A652"></circle><circle cx="6.189" cy="15.454" r="9.509" stroke-width="1.341" stroke="#ED1C24"></circle><circle cx="17.486" cy="15.454" r="9.509" stroke-width="1.341" stroke="#2E3192"></circle><circle cx="12" cy="12" r="10.769" stroke-width="1.463"></circle></g>',
|
|
112
113
|
// THE CHANNELS, keyed by the registry's provider id. These are the brands'
|
|
113
114
|
// own published marks, from simple-icons, so a connect tile is drawn the way
|
|
@@ -169,6 +170,8 @@ const OF = {
|
|
|
169
170
|
'z-ai': 'zai',
|
|
170
171
|
zen: 'zoo',
|
|
171
172
|
};
|
|
173
|
+
/** A body's own stroke hues: under `ink` they come off, so it draws in the page's ink. */
|
|
174
|
+
const INKED = / stroke="#[0-9A-Fa-f]{6}"/g;
|
|
172
175
|
/**
|
|
173
176
|
* The mark's NAME, resolved through the alias table — not its body. The body
|
|
174
177
|
* and the hue are both read under this one key, so a lab cannot end up wearing
|
|
@@ -270,5 +273,5 @@ export function ProviderMark({ org, provider, model, size = 32, ink, }) {
|
|
|
270
273
|
// stroke, so one presentation attribute colours a filled glyph and a
|
|
271
274
|
// drawn one alike. A lab with no hue inherits the page's ink, which is
|
|
272
275
|
// the whole point.
|
|
273
|
-
color: ink ? undefined : HUE[mark], dangerouslySetInnerHTML: { __html: MARKS[mark] } }) }));
|
|
276
|
+
color: ink ? undefined : HUE[mark], dangerouslySetInnerHTML: { __html: ink ? MARKS[mark].replace(INKED, '') : MARKS[mark] } }) }));
|
|
274
277
|
}
|
package/dist/Rail.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function Toggle({ open, onPress, label }: {
|
|
|
31
31
|
onPress: () => void;
|
|
32
32
|
label?: string;
|
|
33
33
|
}): import("react").JSX.Element;
|
|
34
|
-
export declare function Rail({ host, mode, at, open, toggle,
|
|
34
|
+
export declare function Rail({ host, mode, at, open, toggle, rows, onChat, onDev, onStudio, onTabs, go, onOrg, }: {
|
|
35
35
|
host: Host;
|
|
36
36
|
mode: Place['mode'];
|
|
37
37
|
/** Dev's builder address, or null outside Dev. */
|
|
@@ -40,8 +40,8 @@ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev
|
|
|
40
40
|
open: boolean;
|
|
41
41
|
/** Lead with the sidebar's toggle; absent where the host's bar holds it. */
|
|
42
42
|
toggle?: () => void;
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/** Listed as rows in a phone's menu rather than marks down the window's edge. */
|
|
44
|
+
rows?: boolean;
|
|
45
45
|
/** To Chat, Dev and Studio, each where the reader left it. */
|
|
46
46
|
onChat: () => void;
|
|
47
47
|
onDev: () => void;
|
package/dist/Rail.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The app's rail: its products as marks down the window's left edge, in one
|
|
4
4
|
// order — Chat, Dev, Studio, Tabs, Library, Scheduled tasks, Customize — then
|
|
5
5
|
// More (Team, on a host of its own, then the apps in beta this reader may open
|
|
@@ -24,15 +24,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
24
24
|
//
|
|
25
25
|
// The sidebar's toggle leads it (`toggle`) where the host has no bar of its own:
|
|
26
26
|
// a browser tab. A host that is a window draws the toggle in its bar beside Back
|
|
27
|
-
// and Forward (App.tsx `bar`), and its rail starts with Chat.
|
|
28
|
-
// rail
|
|
29
|
-
//
|
|
30
|
-
//
|
|
27
|
+
// and Forward (App.tsx `bar`), and its rail starts with Chat. A phone has no
|
|
28
|
+
// rail: its menu (Drawer.tsx) lists the places as rows (`rows`) under the mode's
|
|
29
|
+
// own — Studio, Tabs, then More, which Library, Scheduled tasks and Customize
|
|
30
|
+
// lead there. Chat and Dev are the phone's bar's (Top.tsx), and the account is
|
|
31
|
+
// the menu's foot.
|
|
32
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
31
33
|
import { Clock, Code2, Ellipsis, FlaskConical, Library, MessageSquare, PanelLeft, Puzzle, Sparkles, SquareTerminal, Users, WandSparkles } from 'lucide-react';
|
|
32
34
|
import { useState } from 'react';
|
|
33
|
-
import { XStack, YStack } from '@hanzo/gui';
|
|
35
|
+
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
34
36
|
import { route } from '@hanzo/build';
|
|
35
37
|
import { DropdownMenu } from '@hanzo/ui';
|
|
38
|
+
import { SidebarItem } from '@hanzo/ui/chat';
|
|
36
39
|
import { BetasDialog, useBetas } from './Betas.js';
|
|
37
40
|
import { out } from './help.js';
|
|
38
41
|
import { BETAS } from './lib/betas.js';
|
|
@@ -61,15 +64,27 @@ export function raised(at) {
|
|
|
61
64
|
}
|
|
62
65
|
/** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
|
|
63
66
|
export function Mark({ label, on = false, onPress, children, ...rest }) {
|
|
64
|
-
|
|
67
|
+
const { t } = useI18n();
|
|
68
|
+
return (_jsx(XStack, { render: "button", "aria-label": t(label), ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
|
|
65
69
|
// A thumb's 44px on a touch screen.
|
|
66
70
|
$touchable: { width: 44, height: 44 }, ...rest, children: children }));
|
|
67
71
|
}
|
|
72
|
+
/** A place as a phone's menu lists it: a row on the sidebar's pitch, a thumb's 44 on a touch screen, lit where the reader is. */
|
|
73
|
+
function Row({ label, on = false, onPress, icon, ...rest }) {
|
|
74
|
+
const { t } = useI18n();
|
|
75
|
+
return (_jsx(SidebarItem, { rounded: "$4", minH: 31, py: 0, px: "$2", $touchable: { minH: 44 }, active: on, icon: icon, onPress: onPress, onKeyDown: (e) => {
|
|
76
|
+
if (!onPress || (e.key !== 'Enter' && e.key !== ' '))
|
|
77
|
+
return;
|
|
78
|
+
e.preventDefault?.();
|
|
79
|
+
onPress();
|
|
80
|
+
}, ...rest, children: _jsx(Text, { fontSize: "$3", color: on ? '$ink' : '$quiet', children: t(label) }) }));
|
|
81
|
+
}
|
|
68
82
|
/** The sidebar's toggle: the same mark wherever a host draws it. */
|
|
69
83
|
export function Toggle({ open, onPress, label }) {
|
|
70
84
|
return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
|
|
71
85
|
}
|
|
72
|
-
export function Rail({ host, mode, at, open, toggle,
|
|
86
|
+
export function Rail({ host, mode, at, open, toggle, rows = false, onChat, onDev, onStudio, onTabs, go, onOrg, }) {
|
|
87
|
+
const { t } = useI18n();
|
|
73
88
|
const here = mode === 'dev' ? spot(at) : null;
|
|
74
89
|
const to = (p) => () => go(app(p));
|
|
75
90
|
// The products on hosts of their own, behind More; on a phone the raised places lead it.
|
|
@@ -80,23 +95,25 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
|
|
|
80
95
|
// The apps in beta this reader may open, each where it lives.
|
|
81
96
|
const granted = BETAS.filter((b) => b.href && opened(b.id));
|
|
82
97
|
const rest = [
|
|
83
|
-
...(
|
|
98
|
+
...(rows
|
|
84
99
|
? [
|
|
85
|
-
{ key: 'library', label: 'Library', icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
|
|
86
|
-
{ key: 'scheduled', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
|
|
87
|
-
{ key: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
|
|
100
|
+
{ key: 'library', label: t('Library'), icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
|
|
101
|
+
{ key: 'scheduled', label: t('Scheduled tasks'), icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
|
|
102
|
+
{ key: 'customize', label: t('Customize'), icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
|
|
88
103
|
...(team ? [{ type: 'separator', key: 'raised' }] : []),
|
|
89
104
|
]
|
|
90
105
|
: []),
|
|
91
106
|
...(team
|
|
92
107
|
? [
|
|
93
|
-
{ key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
|
|
108
|
+
{ key: 'team', label: t('Team'), icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
|
|
94
109
|
...granted.map((b) => ({ key: `beta-${b.id}`, label: b.name, icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), onSelect: () => out(b.href) })),
|
|
95
110
|
{ type: 'separator', key: 'betas-rule' },
|
|
96
|
-
{ key: 'betas', label: 'Betas', icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), onSelect: () => setBetas(true) },
|
|
111
|
+
{ key: 'betas', label: t('Betas'), icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), onSelect: () => setBetas(true) },
|
|
97
112
|
]
|
|
98
113
|
: []),
|
|
99
114
|
];
|
|
100
|
-
const
|
|
101
|
-
|
|
115
|
+
const betasDialog = _jsx(BetasDialog, { open: betas, onOpenChange: setBetas, go: out });
|
|
116
|
+
if (rows)
|
|
117
|
+
return (_jsxs(YStack, { "data-slot": "places", render: "nav", "aria-label": t('Products'), pt: "$2", shrink: 0, children: [_jsxs(YStack, { role: "list", gap: 2, children: [_jsx(YStack, { role: "listitem", children: _jsx(Row, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "place-studio", icon: _jsx(WandSparkles, { size: 16, "aria-hidden": true }) }) }), _jsx(YStack, { role: "listitem", children: _jsx(Row, { label: "Tabs", on: mode === 'tabs', onPress: onTabs, "data-slot": "place-tabs", icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }) }) }), _jsx(YStack, { role: "listitem", children: _jsx(DropdownMenu, { placement: "bottom-start", minWidth: 220, trigger: _jsx(Row, { label: "More", "data-slot": "place-more", "aria-haspopup": "menu", icon: _jsx(Ellipsis, { size: 16, "aria-hidden": true }) }), items: rest }) })] }), betasDialog] }));
|
|
118
|
+
return (_jsxs(YStack, { "data-slot": "rail", render: "nav", "aria-label": t('Products'), width: RAIL, flex: 1, pt: "$2", pb: "$2", gap: 8, shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: t("Chat"), on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Dev"), on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Studio"), on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Tabs"), on: mode === 'tabs', onPress: onTabs, "data-slot": "rail-tabs", children: _jsx(SquareTerminal, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Library"), on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Scheduled tasks"), on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Customize"), on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) }), betasDialog, rest.length ? (_jsx(DropdownMenu, { placement: "right-start", minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": t("More"), ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest })) : null, _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: onOrg })] }));
|
|
102
119
|
}
|