@hanzo/rooms 0.1.91 → 0.1.93

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 CHANGED
@@ -119,7 +119,7 @@ function Window({ bar }) {
119
119
  const shut = useCallback(() => setDrawer(false), []);
120
120
  // The sidebar shut, kept per browser under the key the builder's rail kept it under.
121
121
  const [closed, setClosed] = useKept('hanzo.build.rail', false);
122
- // The palette is open (⌘K, the sidebar's search).
122
+ // The palette is open (the sidebar's search; ⌘K toggles it).
123
123
  const [finding, setFinding] = useState(false);
124
124
  const found = useCallback(() => setFinding(false), []);
125
125
  const dev = useDevHost();
@@ -217,7 +217,7 @@ function Window({ bar }) {
217
217
  const key = (e) => {
218
218
  if (summons(e)) {
219
219
  e.preventDefault();
220
- setFinding(true);
220
+ setFinding((open) => !open);
221
221
  return;
222
222
  }
223
223
  if (typing(e))
@@ -0,0 +1,21 @@
1
+ /** The apps in beta as this reader stands: which open, which they asked for, and the ask. */
2
+ export declare function useBetas(): {
3
+ /** The one predicate: whether this reader may open the app `id`. */
4
+ open: (id: string) => boolean;
5
+ /** Whether they asked for it. */
6
+ asked: (id: string) => boolean;
7
+ /** Ask for these apps. */
8
+ request: (ids: string[]) => Promise<void>;
9
+ /** The cloud has answered for this reader. */
10
+ ready: boolean;
11
+ };
12
+ /** Every app in beta, by group, each with its action. */
13
+ export declare function BetasList({ go }: {
14
+ go?: (href: string) => void;
15
+ }): import("react").JSX.Element;
16
+ /** The Betas list in a dialog, from the rail's More. */
17
+ export declare function BetasDialog({ open, onOpenChange, go }: {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ go?: (href: string) => void;
21
+ }): import("react").JSX.Element;
package/dist/Betas.js ADDED
@@ -0,0 +1,127 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The apps in beta: which this reader may open, and the list they ask from.
4
+ //
5
+ // WHO MAY OPEN ONE is the cloud's answer, never this page's. Each app in beta is a
6
+ // principal switch, `beta.<id>`, decided per caller from the validated org and
7
+ // person (cloud apps/flags), and GET /v1/flag/platform answers every one for the
8
+ // signed-in caller. A SuperAdmin grants one to a person from their CRM record.
9
+ // `opens` (lib/betas) reads that answer, the one predicate every caller asks.
10
+ //
11
+ // WHAT THEY ASKED FOR is their CRM record's, apps/marketing's beta record:
12
+ // GET /v1/marketing/beta reads it and POST /v1/marketing/beta/request adds to it,
13
+ // stamped with the time and stated on the event plane under the person and the
14
+ // org, so the request reaches the team where it works the person.
15
+ //
16
+ // Signed out, still loading, or on any error nothing is open: a flags outage
17
+ // cannot leak an unfinished app.
18
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
19
+ import { useIam } from '@hanzo/iam/react';
20
+ import { Button, Dialog, DialogContent, DialogTitle, Text, XStack, YStack } from '@hanzo/ui';
21
+ import { api } from './lib/api.js';
22
+ import { BETAS, GROUPS, opens } from './lib/betas.js';
23
+ import { bearer, scope, superAdmin } from './lib/session.js';
24
+ const NONE = { flags: {}, asked: [] };
25
+ /** One read per token, shared by every caller on the page. */
26
+ let held = null;
27
+ let reading = null;
28
+ const listeners = new Set();
29
+ const tell = () => listeners.forEach((f) => f());
30
+ /** A switch read this long ago is read again on the next mount. */
31
+ const FRESH = 30_000;
32
+ async function get(path) {
33
+ const r = await fetch(`${api()}${path}`, { headers: scope() });
34
+ return r.ok ? (await r.json()) : null;
35
+ }
36
+ function read(token) {
37
+ if (reading)
38
+ return reading;
39
+ reading = Promise.all([
40
+ get('/v1/flag/platform').catch(() => null),
41
+ get('/v1/marketing/beta').catch(() => null),
42
+ ])
43
+ .then(([f, b]) => {
44
+ held = { token, said: { flags: f?.flags ?? {}, asked: b?.betas ?? [] }, at: Date.now() };
45
+ })
46
+ .finally(() => {
47
+ reading = null;
48
+ tell();
49
+ });
50
+ return reading;
51
+ }
52
+ const subscribe = (f) => {
53
+ listeners.add(f);
54
+ return () => listeners.delete(f);
55
+ };
56
+ /** The apps in beta as this reader stands: which open, which they asked for, and the ask. */
57
+ export function useBetas() {
58
+ const { accessToken } = useIam();
59
+ const [sudo, setSudo] = useState(false);
60
+ const [local, setLocal] = useState(false);
61
+ // Facts only the browser holds arrive after mount, so the first render
62
+ // matches the HTML it hydrates.
63
+ useEffect(() => setLocal(here()), []);
64
+ useEffect(() => setSudo(superAdmin()), [accessToken]);
65
+ const token = accessToken ? bearer() ?? '' : '';
66
+ const now = useSyncExternalStore(subscribe, () => held, () => null);
67
+ useEffect(() => {
68
+ if (token && (!held || held.token !== token || Date.now() - held.at > FRESH))
69
+ void read(token);
70
+ }, [token]);
71
+ const said = token && now?.token === token ? now.said : NONE;
72
+ const open = useCallback((id) => opens(id, said.flags, sudo || local), [said, sudo, local]);
73
+ const asked = useCallback((id) => said.asked.includes(id), [said]);
74
+ const request = useCallback(async (ids) => {
75
+ const r = await fetch(`${api()}/v1/marketing/beta/request`, {
76
+ method: 'POST',
77
+ headers: { 'content-type': 'application/json', ...scope() },
78
+ body: JSON.stringify({ betas: ids }),
79
+ });
80
+ if (!r.ok)
81
+ throw new Error(r.status === 401 ? 'Sign in to ask for access.' : `The request was not taken (${r.status}).`);
82
+ const record = (await r.json());
83
+ if (held && held.token === token) {
84
+ held = { ...held, said: { ...held.said, asked: record.betas ?? [...held.said.asked, ...ids] } };
85
+ tell();
86
+ }
87
+ }, [token]);
88
+ return { open, asked, request, ready: Boolean(token) && now?.token === token };
89
+ }
90
+ /**
91
+ * Whether this page is served from a developer's own machine, where every app
92
+ * opens: a room nobody can open is a room nobody can work on. The ORIGIN, which
93
+ * no deployment can acquire by configuration.
94
+ */
95
+ function here() {
96
+ if (typeof window === 'undefined')
97
+ return false;
98
+ const host = window.location.hostname;
99
+ return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host.endsWith('.local') || /^10\./.test(host) || /^192\.168\./.test(host);
100
+ }
101
+ /** One app's action: Open where it may, Requested once asked, else Request access. */
102
+ function Ask({ beta, go }) {
103
+ const { open, asked, request } = useBetas();
104
+ const [busy, setBusy] = useState(false);
105
+ const [wrong, setWrong] = useState(null);
106
+ if (open(beta.id)) {
107
+ return beta.href && go ? (_jsx(Button, { size: "sm", rounded: 999, onPress: () => go(beta.href), "data-slot": "beta-open", children: "Open" })) : (_jsx(Text, { fontSize: "$2", color: "$color11", "data-slot": "beta-on", children: "You\u2019re in" }));
108
+ }
109
+ if (asked(beta.id)) {
110
+ return (_jsx(Text, { fontSize: "$2", color: "$color11", "data-slot": "beta-requested", children: "Requested" }));
111
+ }
112
+ return (_jsxs(YStack, { items: "flex-end", gap: "$1", children: [_jsx(Button, { size: "sm", rounded: 999, variant: "outline", disabled: busy, "data-slot": "beta-request", onPress: () => {
113
+ setBusy(true);
114
+ setWrong(null);
115
+ request([beta.id])
116
+ .catch((e) => setWrong(e instanceof Error ? e.message : String(e)))
117
+ .finally(() => setBusy(false));
118
+ }, children: busy ? 'Requesting…' : 'Request access' }), wrong ? (_jsx(Text, { fontSize: "$1", color: "$red10", children: wrong })) : null] }));
119
+ }
120
+ /** Every app in beta, by group, each with its action. */
121
+ export function BetasList({ go }) {
122
+ return (_jsx(YStack, { gap: "$4", "data-slot": "betas", children: GROUPS.map((group) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$color10", textTransform: "uppercase", letterSpacing: 0.6, children: group }), BETAS.filter((b) => b.group === group).map((b) => (_jsxs(XStack, { items: "center", gap: "$3", "data-beta": b.id, children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", children: b.name }), _jsx(Text, { fontSize: "$2", color: "$color11", children: b.line })] }), _jsx(Ask, { beta: b, go: go })] }, b.id)))] }, group))) }));
123
+ }
124
+ /** The Betas list in a dialog, from the rail's More. */
125
+ export function BetasDialog({ open, onOpenChange, go }) {
126
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Betas" }), _jsx(Text, { fontSize: "$2", color: "$color11", children: "Apps we have not opened to everyone yet. Ask for the ones you want and we will let you in." }), _jsx(YStack, { maxH: "60vh", overflowY: "auto", overflowX: "hidden", pr: "$2", children: _jsx(BetasList, { go: go }) })] }) }));
127
+ }
package/dist/Chat.d.ts CHANGED
@@ -2,10 +2,9 @@ export declare function Chat(): import("react").JSX.Element;
2
2
  /**
3
3
  * The same conversation, embedded in a page that is not /chat.
4
4
  *
5
- * R8: there were TWO live chat surfaces with different consent semantics — this
6
- * one asks at SEND and fails closed, the other gated on ARRIVAL through a
7
- * separate copy of the consent code. Two answers to one question is how they
8
- * drift, so /models mounts this and the second surface is gone. It carries no
5
+ * R8: there were TWO live chat surfaces, so /models mounts this one and the
6
+ * second is gone. The data-share choice is made once at onboarding; this room
7
+ * does not ask again when a message is sent. It carries no
9
8
  * heading of its own: the host page already has one, and a second `<h1>` is the
10
9
  * defect that took the landings off /chat and /dev.
11
10
  *
@@ -32,4 +31,3 @@ export declare function refusalOf(error: Error): {
32
31
  visitor: boolean;
33
32
  provider: boolean;
34
33
  };
35
- /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
package/dist/Chat.js CHANGED
@@ -17,9 +17,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
17
17
  // the free pool, so what changes at sign-in is the menu, not the room.
18
18
  //
19
19
  // The FREE lane is about PRICE, not identity: a free turn spends no balance and
20
- // is data-shared in exchange, which is what the consent block asks about, at
21
- // SEND rather than on arrival — consent is owed when someone sends, not when
22
- // they walk in. Signing in is what buys the paid models and a history that
20
+ // is data-shared in exchange. That choice is made once, at onboarding, where
21
+ // the terms are linked. The line under the composer says so for as long as a
22
+ // free model answers. Signing in buys the paid models and a history that
23
23
  // follows you; it is never the price of asking a question.
24
24
  //
25
25
  // This file used to branch on `isAuthenticated` and hand a visitor a composer
@@ -31,7 +31,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "r
31
31
  import { billed, detail, familyOf, planRequired, plainly, refused } from "./failure.js";
32
32
  import { Prose } from "./Prose.js";
33
33
  import { useAgent, useAi, useChat, } from "@hanzo/ai/react";
34
- import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
34
+ import { isFree } from "@hanzo/ai";
35
35
  import { ASK, Chat as ChatSurface, Failure, Parts, words } from "@hanzo/ui/chat";
36
36
  import { carriedIn, carrying, compose, partsOf, said as told, weigh } from "./lib/attach.js";
37
37
  import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
@@ -42,7 +42,7 @@ import { AgentMaker, HOUSE, useAgentShelf, useCrew } from "./crew.js";
42
42
  import { called } from "./team.js";
43
43
  import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
44
44
  import { useMedia } from "@hanzo/gui";
45
- import { tidy } from "./lib/title.js";
45
+ import { given, tidy } from "./lib/title.js";
46
46
  import { useIam, useIamToken } from "@hanzo/iam/react";
47
47
  import { bearer, hasSession, named, org } from "./lib/session.js";
48
48
  import { api } from "./lib/api.js";
@@ -64,15 +64,13 @@ import { Share } from "./Share.js";
64
64
  import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
65
65
  import { Schedule } from "./Schedule.js";
66
66
  import { keepFile, useAttach, useKeep } from "./keep.js";
67
- import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
67
+ import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, book, hand, taken, want, path as devPath, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
68
68
  import { checkoutUrl } from './lib/pay.js';
69
69
  import { Upgrade, useFree } from './Upgrade.js';
70
70
  import { enter, LOGIN } from './lib/destination.js';
71
71
  import { first } from './lib/first.js';
72
- import { BAD, mix } from "./lib/mix.js";
72
+ import { BAD } from "./lib/mix.js";
73
73
  import { brand, site } from './where.js';
74
- /** The veil under the free lane's consent sheet. */
75
- const SCRIM = mix("var(--pure-black)", 72, "srgb");
76
74
  // NO REASONING PANEL HERE. Every assistant turn opened a "Thinking" disclosure
77
75
  // listing seven steps — "Planning CSS extraction for chat styles", "Designing
78
76
  // responsive top bar grid layout" — typed into this file as a literal array.
@@ -89,24 +87,6 @@ const SCRIM = mix("var(--pure-black)", 72, "srgb");
89
87
  * room has always read at, so an untouched setting changes nothing.
90
88
  */
91
89
  const MEASURE = "var(--container-prose, 48rem)";
92
- /**
93
- * The store, or null when the browser will not give us one.
94
- *
95
- * Reading `window.localStorage` THROWS `SecurityError` where site data is
96
- * blocked — third-party contexts, Safari's Block All Cookies, a hardened
97
- * profile — and this is called from inside a send handler, so an unguarded
98
- * access takes the send down rather than the storage.
99
- */
100
- const store = () => {
101
- if (typeof window === "undefined")
102
- return null;
103
- try {
104
- return window.localStorage;
105
- }
106
- catch {
107
- return null;
108
- }
109
- };
110
90
  /**
111
91
  * The text this screen arrived with, TYPED INTO THE COMPOSER — never sent.
112
92
  *
@@ -182,8 +162,8 @@ export function Chat() {
182
162
  // mounted under the provider the shell already put above it.
183
163
  //
184
164
  // ONE room, for everyone. `anonymous` changes which models are on offer and
185
- // nothing else — the composer, the consent and the transcript are the same
186
- // components either way, because a visitor is having the same conversation.
165
+ // nothing else — the composer and the transcript are the same components
166
+ // either way, because a visitor is having the same conversation.
187
167
  //
188
168
  // The conversations are NOT drawn here. `Threads` is the list and the shell
189
169
  // owns the column it sits in — one sidebar for six rooms, already built, and a
@@ -194,10 +174,9 @@ export function Chat() {
194
174
  /**
195
175
  * The same conversation, embedded in a page that is not /chat.
196
176
  *
197
- * R8: there were TWO live chat surfaces with different consent semantics — this
198
- * one asks at SEND and fails closed, the other gated on ARRIVAL through a
199
- * separate copy of the consent code. Two answers to one question is how they
200
- * drift, so /models mounts this and the second surface is gone. It carries no
177
+ * R8: there were TWO live chat surfaces, so /models mounts this one and the
178
+ * second is gone. The data-share choice is made once at onboarding; this room
179
+ * does not ask again when a message is sent. It carries no
201
180
  * heading of its own: the host page already has one, and a second `<h1>` is the
202
181
  * defect that took the landings off /chat and /dev.
203
182
  *
@@ -476,9 +455,9 @@ function Plan({ onPress }) {
476
455
  *
477
456
  * `anonymous` opens on the free pool and offers only what the free pool can
478
457
  * answer, because listing a paid model to someone with no balance is an offer
479
- * that fails at send. Everything else — the transcript, the composer, the
480
- * consent — is identical, which is the point: signing in later changes the
481
- * models on the menu, not the room.
458
+ * that fails at send. Everything else — the transcript, the composer — is
459
+ * identical, which is the point: signing in later changes the models on the
460
+ * menu, not the room.
482
461
  */
483
462
  function Thread({ placeholder = message(), initial, anonymous = false, heading, }) {
484
463
  /**
@@ -487,16 +466,14 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
487
466
  * `useState(anonymous ? FREE : ENSO)` read the flag ONCE, on the first render
488
467
  * — which is the render before hydration settles, where `anonymous` is always
489
468
  * false. So every visitor opened on ENSO while the composer beside them said
490
- * "Enso Free", `isFree(model)` was false, and the two things that hang off it
491
- * both went quiet: the data-sharing notice never appeared, and the consent
492
- * block never asked. A visitor was sending on the paid pool having agreed to
493
- * nothing, because a state initialiser cannot see a value that arrives later.
469
+ * "Enso Free", and `isFree(model)` was false, so the data-sharing notice
470
+ * never appeared. A visitor was sending on the paid pool, because a state
471
+ * initialiser cannot see a value that arrives later.
494
472
  *
495
473
  * A CHOICE, or the identity. `null` is "nobody has chosen", which is a
496
474
  * different fact from either model and the only one that can defer to a flag
497
475
  * that settles after mount. Picking from the menu still wins, permanently.
498
476
  */
499
- const [asking, setAsking] = useState(null);
500
477
  // THE MODEL AND THE EFFORT are the one choice Chat and Dev share
501
478
  // (@hanzo/build `useMind`): the catalog from `GET /v1/models` and what the
502
479
  // reader kept, changed from the composer's chip or from Settings.
@@ -527,8 +504,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
527
504
  go: (href) => (/^https?:/.test(href) ? void window.open(href, "_blank", "noopener,noreferrer") : go(href)),
528
505
  }, CHAT_MODES);
529
506
  const agentShelf = useAgentShelf();
530
- // The opening greets the person by their first name, as their account spells it.
531
- const firstName = useMemo(() => (anonymous ? "" : ((named()?.name ?? "").trim().split(/\s+/)[0] ?? "")), [anonymous]);
507
+ // The opening greets the person by the first word of their display name, capitalised ("What’s up, Z?").
508
+ // A username is a handle, not a name: without a display name the greeting has none.
509
+ const firstName = useMemo(() => (anonymous ? "" : given(named()?.display ?? "")), [anonymous]);
532
510
  // Up to three things worth doing next, from the person's own activity (@hanzo/build).
533
511
  const hints = useSuggestions(target);
534
512
  // THE HEADER'S ⋯ IS THE ROW'S MENU (thread.tsx): rename, pin, share, its
@@ -1137,17 +1115,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1137
1115
  * no longer allowed to draw. `:focus-within` would say this in CSS; the
1138
1116
  * stylesheet that would hold it cannot reach past gui's own rule, so the
1139
1117
  * state is read where the event is. */
1140
- const guarded = useCallback((text) => {
1141
- const s = store();
1142
- // FAILS CLOSED. This read `s && !hasConsent(s)`, so a browser that gives
1143
- // us no store answered "no consent needed" and sent a data-shared turn
1144
- // having asked nobody. No store means no record, and no record means ask.
1145
- if (isFree(model) && (!s || !hasConsent(s))) {
1146
- setAsking(text);
1147
- return;
1148
- }
1149
- speak(text);
1150
- }, [model, speak]);
1118
+ // A free send goes. The data-share choice was made at onboarding, once,
1119
+ // and the line under the composer still says what a free model does.
1120
+ const guarded = speak;
1151
1121
  /**
1152
1122
  * The draft and the files, sent as one turn.
1153
1123
  *
@@ -1179,8 +1149,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1179
1149
  .then((id) => (!open && id ? reopen(id) : undefined))
1180
1150
  .catch(() => { });
1181
1151
  };
1182
- const submit = () => {
1183
- const text = draft.trim();
1152
+ const submit = (ask = draft) => {
1153
+ const text = ask.trim();
1184
1154
  if (preparing || (!text && !files.length))
1185
1155
  return;
1186
1156
  if (!carry.preparing)
@@ -1316,25 +1286,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1316
1286
  enter(LOGIN);
1317
1287
  }, children: "Sign in" })) : lift ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => setAsk({ ...lift, model: nameOf(model, listed) }), children: "Upgrade" })) : how?.visitor ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => enter(), children: "Sign in to keep going" })) : (_jsx(Anchor, { href: checkoutUrl(), children: _jsx(Button, { size: "sm", variant: "outline", children: "Add credit" }) })) })) : null] }) }));
1318
1288
  };
1319
- // The agreement is asked OVER the room, not instead of it. Returning the
1320
- // consent on its own replaced the whole pane: the question the reader had
1321
- // just typed left the screen, and the first thing a stranger saw of the
1322
- // product was a page about training data. The room stays; the sheet sits on
1323
- // top of it, and the question is still there behind it.
1324
- const consent = asking === null ? null : (_jsx(Consent, { question: told(asking).text, onAgree: () => {
1325
- const s = store();
1326
- // Nothing to persist into is not a reason to refuse the turn the
1327
- // reader just agreed to; it means the question is asked again next
1328
- // time, which is the honest cost of a browser that stores nothing.
1329
- if (s)
1330
- grantConsent(s);
1331
- const held = asking;
1332
- setAsking(null);
1333
- if (held)
1334
- speak(held);
1335
- }, onCancel: () => {
1336
- setAsking(null);
1337
- } }));
1338
1289
  // Dev, from the Summary's Create file or site: a move inside the app where the
1339
1290
  // host's app reads the path (hanzo.ai), else Dev on hanzo.ai, as the shell's
1340
1291
  // Dev place goes from a host without it (hanzo.team).
@@ -1356,10 +1307,46 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1356
1307
  };
1357
1308
  // THE COMPOSER'S OPTIONS, held apart so its keys (the `/` and `@` lists' arrows,
1358
1309
  // Ctrl+U) hook in through `useMention` before the frame is drawn.
1310
+ // What pressing a suggestion does, at once: open what it names, send its prompt
1311
+ // here (the room is a new chat while one shows), hand its run to Dev, or make
1312
+ // its scheduled task and show it in Scheduled tasks.
1313
+ const hint = (a) => {
1314
+ switch (a.kind) {
1315
+ case "open":
1316
+ if (a.item)
1317
+ want(a.item);
1318
+ if (/^https:\/\//.test(a.to))
1319
+ window.open(a.to, "_blank", "noopener,noreferrer");
1320
+ else
1321
+ go(a.to.startsWith("/") ? a.to : app(a.to));
1322
+ return;
1323
+ case "send":
1324
+ submit(a.prompt);
1325
+ return;
1326
+ case "task":
1327
+ hand("task", a.prompt);
1328
+ go(app(""));
1329
+ return;
1330
+ case "schedule":
1331
+ void book(target, a, "chat").then((made) => go(app(devPath({ kind: "automation", id: made.id }))), (e) => setPrepWrong(e instanceof Error && e.message ? e.message : "The scheduled task could not be made."));
1332
+ return;
1333
+ }
1334
+ };
1335
+ // A chat a suggestion in Dev left for this room is sent once the room is a new
1336
+ // chat: the address's own conversation let go of, nothing said yet.
1337
+ const [handed] = useState(() => ({ prompt: anonymous ? "" : taken("send") }));
1338
+ useEffect(() => {
1339
+ if (!handed.prompt || open || said.length)
1340
+ return;
1341
+ const p = handed.prompt;
1342
+ handed.prompt = "";
1343
+ submit(p);
1344
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1345
+ }, [open, said.length]);
1359
1346
  const composing = {
1360
1347
  value: draft,
1361
1348
  onChange: setDraft,
1362
- onSend: submit,
1349
+ onSend: () => submit(),
1363
1350
  onStop: chat.stop,
1364
1351
  busy: chat.streaming,
1365
1352
  // Files alone are a message: what is held goes with an empty draft too.
@@ -1424,7 +1411,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1424
1411
  onLanguage: speakIn,
1425
1412
  talk,
1426
1413
  // While nothing has been said: what is worth doing next.
1427
- below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onPick: setDraft, onDismiss: hints.dismiss }) : undefined,
1414
+ below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onAct: hint, onDismiss: hints.dismiss }) : undefined,
1428
1415
  };
1429
1416
  const mention = useMention(composing);
1430
1417
  return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
@@ -1536,9 +1523,5 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1536
1523
  empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", pt: "$6", pb: "$2", ...(media.sm ? null : { flex: 1, justify: "flex-end" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { ml: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room) || heading || (firstName ? `What’s up, ${firstName}?` : "What’s up?") }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
1537
1524
  // The transcript reads at the same measure. `Thread` defaults to 768
1538
1525
  // itself; handing it the column here is what lets Width reach it.
1539
- column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: isFree(model) ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models.", " ", _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] })) : (_jsxs(_Fragment, { children: [_jsx(Anchor, { href: site("/legal/terms"), fontSize: "$1", color: "$soft", children: "Terms" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "\u00B7" }), _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$soft", children: "Privacy" })] })) }), consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1540
- }
1541
- /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1542
- function Consent({ question, onAgree, onCancel, }) {
1543
- return (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", p: "$4", z: 40, bg: SCRIM, children: _jsxs(YStack, { width: "100%", maxW: 512, px: "$5", py: "$5", gap: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { render: "h2", fontSize: "$6", fontWeight: "500", color: "$ink", children: freeCopy.consentTitle }), question ? (_jsxs(Text, { fontSize: "$3", color: "$soft", numberOfLines: 2, children: ["Your question is waiting: \u201C", question, "\u201D"] })) : null, _jsx(Text, { fontSize: "$4", color: "$soft", children: freeCopy.consentBody }), _jsx(YStack, { gap: "$2", children: freeCopy.consentPoints.map((point) => (_jsx(Text, { fontSize: "$3", color: "$soft", children: point }, point))) }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsx(Button, { onClick: onAgree, children: freeCopy.consentAgreeCta }), _jsx(Button, { variant: "outline", onClick: onCancel, children: freeCopy.consentCancelCta })] }), _jsxs(Text, { fontSize: "$2", color: "$soft", children: [freeCopy.termsText, " ", _jsx(Anchor, { href: site("/legal/terms"), children: "Terms" }), " · ", _jsx(Anchor, { href: site("/legal/privacy"), children: "Privacy" })] })] }) }));
1526
+ column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: isFree(model) ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models.", " ", _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] })) : (_jsxs(_Fragment, { children: [_jsx(Anchor, { href: site("/legal/terms"), fontSize: "$1", color: "$soft", children: "Terms" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "\u00B7" }), _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$soft", children: "Privacy" })] })) }), _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1544
1527
  }
package/dist/Me.js CHANGED
@@ -5,7 +5,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  // other app (Shell.tsx).
6
6
  //
7
7
  // Top to bottom: who, with their plan, a row whose › opens the account itself;
8
- // then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,); then
8
+ // then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,); Beta,
9
+ // the person's place on the beta waitlist (the site's /beta/status); then
9
10
  // Help ›, the site's own pages and the keyboard shortcuts (⌘/ / Ctrl+/ from
10
11
  // anywhere); and Log out. Every row opens a real destination: Settings'
11
12
  // sections through `showSettings` (open.ts), which the app answers with the
@@ -29,7 +30,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
29
30
  // mints a token that carries it.
30
31
  import { useEffect, useMemo, useRef, useState } from 'react';
31
32
  import { SizableText, XStack, YStack } from '@hanzo/gui';
32
- import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
33
+ 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';
33
34
  import { label, Meter, said as spoken, useStanding } from '@hanzo/build';
34
35
  import { useOrganizations } from '@hanzo/iam/react';
35
36
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Kbd, } from '@hanzo/ui';
@@ -196,7 +197,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
196
197
  // Clear of the rail's mark (or the row) it opens from, and flush with its foot.
197
198
  offset: 8, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
198
199
  row.current = el instanceof HTMLElement ? el : null;
199
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 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": "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: "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": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "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: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "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": "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: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "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: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "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: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "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: "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: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
200
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 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": "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: "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": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "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: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "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": "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: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "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: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "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: "Settings" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: "Beta" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "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: "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: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
200
201
  }
201
202
  /** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
202
203
  function Shortcuts({ open, onOpenChange, apple }) {
package/dist/Palette.d.ts CHANGED
@@ -3,22 +3,36 @@ import { type Host } from '@hanzo/build';
3
3
  import { type ShortcutKeys } from '@hanzo/ui';
4
4
  import { type Place } from './lib/host.js';
5
5
  import { type Hit } from './seek.js';
6
- /** A row: what it is called, its glyph, and what pressing it does. */
6
+ /** A row: what it is called, its glyph, where it lives, and what pressing it does. */
7
7
  export interface Action extends Hit {
8
8
  icon?: ReactNode;
9
9
  shortcut?: ShortcutKeys;
10
+ /** Where it lives, dimmed after its name: Settings, Customize, Help. */
11
+ where?: string;
10
12
  run: () => void;
11
13
  /** Pressing it keeps the palette open. */
12
14
  stay?: boolean;
13
15
  }
14
- export declare function Palette({ onClose, go, actions, mode, host, }: {
16
+ /**
17
+ * Every place the reader can be taken: `top` is what an empty field lists, the
18
+ * products and the places of the app; `deep` is what only a query reaches.
19
+ * Without a host (hanzo.team's frame) the builder's registries are not there,
20
+ * and the products and the site's pages are.
21
+ */
22
+ export declare function places(host: Host | undefined, go: (address: string) => void): {
23
+ top: Action[];
24
+ deep: Action[];
25
+ };
26
+ /** The palette's actions; a host that cannot do one (no theme to change, no org to pick) is not offered it. */
27
+ export declare function actions(host: Host | undefined, go: (address: string) => void, signOut: () => void, keys: () => void): Action[];
28
+ export declare function Palette({ onClose, go, actions: frame, mode, host, }: {
15
29
  onClose: () => void;
16
- /** Moves the app to one of its addresses (lib/host `talk`, `app`). */
30
+ /** Moves the app to one of its addresses (lib/host `talk`, `app`, `studio`). */
17
31
  go: (address: string) => void;
18
- /** The frame's own actions, after the palette's; one with a palette action's id stands in for it. */
32
+ /** The frame's own places; one with a palette row's id stands in for it, the rest follow under Go to. */
19
33
  actions?: Action[];
20
- /** The product the reader is in: what the empty field lists and what a query ranks first. */
34
+ /** The product the reader is in: its own things lead a tie. */
21
35
  mode?: Place['mode'];
22
- /** Dev's host, whose runs the palette lists in Dev. */
36
+ /** The builder's host: its registries, its runs, its theme and organization. */
23
37
  host?: Host;
24
38
  }): import("react").JSX.Element;