@columenlabs/lumen-ui 0.1.236 → 0.1.238

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.
@@ -9,6 +9,7 @@ import { InfoLabel } from '../info-tip.js';
9
9
  import { errorText } from '../integrations.js';
10
10
  import { LumenSelector } from '../lumen-selector.js';
11
11
  import { lumenToast } from '../lumen-toast.js';
12
+ import { SettingField } from '../setting-parts.js';
12
13
  import { ShareMeter } from '../share-meter.js';
13
14
  import { Badge } from '../ui/badge.js';
14
15
  import { Button } from '../ui/button.js';
@@ -67,26 +68,23 @@ function WithStatus({ client, children, }) {
67
68
  return _jsx(_Fragment, { children: children(q.data) });
68
69
  }
69
70
  /** One setting: its label (with an (i) when `info` says more) and its control. */
70
- function Row({ label, info, children }) {
71
- return (_jsxs("div", { "data-slot": "setting-row", className: "flex flex-wrap items-center gap-x-4 gap-y-2 border-b py-3 last:border-b-0", children: [_jsx(Label, { className: "min-w-0 flex-1", children: _jsx(InfoLabel, { info: info, children: label }) }), _jsx("div", { className: "shrink-0", children: children })] }));
72
- }
73
71
  // ---------------------------------------------------------------- the assistant
74
72
  function General({ client }) {
75
73
  const { apply } = useApply(client);
76
74
  const set = (patch) => apply('settings', () => client.settings(patch));
77
75
  return (_jsx(WithStatus, { client: client, children: (s) => {
78
76
  const above = s.tiers.slice(s.tiers.findIndex((t) => t.id === s.tier) + 1);
79
- return (_jsxs("div", { className: "flex flex-col", children: [_jsx(Row, { label: "Tier", children: _jsx(LumenSelector, { label: "Tier", size: "sm", className: "w-44", value: s.tier, options: s.tiers.map((t) => ({
77
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingField, { label: "Tier", children: _jsx(LumenSelector, { label: "Tier", size: "sm", className: "w-44", value: s.tier, options: s.tiers.map((t) => ({
80
78
  value: t.id,
81
79
  label: t.label,
82
80
  detail: t.model_label,
83
- })), onChange: (tier) => void set({ tier }) }) }), s.tiers.length > 1 && (_jsx(Row, { label: "Hand off the work", info: "On its own, a higher tier takes one task in its own section of the chat and sends back a memo. It costs more.", children: _jsx(LumenSelector, { label: "Hand off the work", size: "sm", className: "w-44", value: s.hand_off, options: HAND_OFF, onChange: (hand_off) => void set({ hand_off }) }) })), s.tiers.length > 1 && (_jsx(Row, { label: "Ask for advice", info: "A bigger tier reads the chat and sends a plan before hard questions. It runs no tools, at most twice an answer.", children: _jsx(LumenSelector, { label: "Ask for advice", size: "sm", className: "w-44", value: s.advice, options: [
81
+ })), onChange: (tier) => void set({ tier }) }) }), s.tiers.length > 1 && (_jsx(SettingField, { label: "Hand off the work", info: "On its own, a higher tier takes one task in its own section of the chat and sends back a memo. It costs more.", children: _jsx(LumenSelector, { label: "Hand off the work", size: "sm", className: "w-44", value: s.hand_off, options: HAND_OFF, onChange: (hand_off) => void set({ hand_off }) }) })), s.tiers.length > 1 && (_jsx(SettingField, { label: "Ask for advice", info: "A bigger tier reads the chat and sends a plan before hard questions. It runs no tools, at most twice an answer.", children: _jsx(LumenSelector, { label: "Ask for advice", size: "sm", className: "w-44", value: s.advice, options: [
84
82
  { value: 'off', label: 'Off' },
85
83
  ...(above.length ? above : s.tiers).map((t) => ({
86
84
  value: t.id,
87
85
  label: t.label,
88
86
  })),
89
- ], onChange: (advice) => void set({ advice }) }) })), _jsx(Row, { label: "Screen", info: "Takes you to a page, a Settings row, a chart or a card when you ask. Each move has an Undo; nothing is saved.", children: _jsx(LumenSelector, { label: "Screen", size: "sm", className: "w-44", value: s.screen ?? 'move', options: [
87
+ ], onChange: (advice) => void set({ advice }) }) })), _jsx(SettingField, { label: "Screen", info: "Takes you to a page, a Settings row, a chart or a card when you ask. Each move has an Undo; nothing is saved.", children: _jsx(LumenSelector, { label: "Screen", size: "sm", className: "w-44", value: s.screen ?? 'move', options: [
90
88
  { value: 'move', label: 'Move' },
91
89
  { value: 'off', label: 'Off' },
92
90
  ], onChange: (screen) => void set({ screen: screen }) }) })] }));
@@ -173,10 +171,10 @@ function Providers({ client }) {
173
171
  // is exactly what the server may pay with.
174
172
  const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0);
175
173
  const ownSet = rows.some(ownUsable);
176
- return (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: rows.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), ownSet && (_jsx(Row, { label: "Answers run on", info: "Your own key or plan first, with the shared credits behind it; or the shared credits first.", children: _jsx(LumenSelector, { label: "Answers run on", size: "sm", className: "w-44", value: s.pay, options: [
174
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: rows.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), ownSet && (_jsx(SettingField, { label: "Answers run on", info: "Your own key or plan first, with the shared credits behind it; or the shared credits first.", children: _jsx(LumenSelector, { label: "Answers run on", size: "sm", className: "w-44", value: s.pay, options: [
177
175
  { value: 'own', label: 'Your own first' },
178
176
  { value: 'app', label: 'Shared credits first' },
179
- ], onChange: (pay) => void apply('pay', () => client.settings({ pay: pay })) }) })), ownSet && (_jsx(Row, { label: "Your keys may spend", info: "On your keys, every provider together.", children: _jsx(CapInput, { label: "Your monthly cap", value: s.caps.own, onSave: (n) => void apply('own', () => client.ownCap(n)) }) })), _jsx(OwnKeyUsage, { client: client, always: ownSet })] }));
177
+ ], onChange: (pay) => void apply('pay', () => client.settings({ pay: pay })) }) })), ownSet && (_jsx(SettingField, { label: "Your keys may spend", info: "On your keys, every provider together.", children: _jsx(CapInput, { label: "Your monthly cap", value: s.caps.own, onSave: (n) => void apply('own', () => client.ownCap(n)) }) })), _jsx(OwnKeyUsage, { client: client, always: ownSet })] }));
180
178
  } }));
181
179
  }
182
180
  /** One provider's shared key, which an admin sets for everyone. */
@@ -212,7 +210,7 @@ function SharedCap({ client, s }) {
212
210
  const { apply } = useApply(client);
213
211
  if (s.allowance?.period === 'once')
214
212
  return null;
215
- return (_jsx(Row, { label: "Each person may spend", info: "A month on the shared keys, every provider together. Past it, a person's own key pays.", children: _jsx(CapInput, { label: "Allowance per person", value: s.caps.app, onSave: (n) => void apply('cap', () => client.appCap(n)) }) }));
213
+ return (_jsx(SettingField, { label: "Each person may spend", info: "A month on the shared keys, every provider together. Past it, a person's own key pays.", children: _jsx(CapInput, { label: "Allowance per person", value: s.caps.app, onSave: (n) => void apply('cap', () => client.appCap(n)) }) }));
216
214
  }
217
215
  /** Admins only: the app's shared keys and what each person may spend on them. */
218
216
  function SharedKeys({ client }) {
@@ -243,15 +241,15 @@ function Models({ client }) {
243
241
  return (_jsxs("div", { className: "flex flex-col", children: [s.tiers.map((t) => {
244
242
  const p = s.providers.find((x) => x.id === t.provider);
245
243
  const keyless = p && !p.app.set && !p.app.from_env && !p.own.set && !p.plan?.set;
246
- return (_jsx(Row, { label: t.label, info: t.about, children: _jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [keyless && _jsxs(Badge, { variant: "outline", children: ["No ", p.label, " key"] }), _jsx(LumenSelector, { label: `${t.label} model`, size: "sm", className: "w-36 sm:w-44", value: t.model, options: models, onChange: (model) => set({ tiers: { [t.id]: { model } } }) }), _jsx(LumenSelector, { label: `${t.label} effort`, size: "sm", className: "w-24 sm:w-32", value: t.effort, options: EFFORTS, onChange: (effort) => set({ tiers: { [t.id]: { effort } } }) })] }) }, t.id));
244
+ return (_jsx(SettingField, { label: t.label, info: t.about, children: _jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [keyless && _jsxs(Badge, { variant: "outline", children: ["No ", p.label, " key"] }), _jsx(LumenSelector, { label: `${t.label} model`, size: "sm", className: "w-36 sm:w-44", value: t.model, options: models, onChange: (model) => set({ tiers: { [t.id]: { model } } }) }), _jsx(LumenSelector, { label: `${t.label} effort`, size: "sm", className: "w-24 sm:w-32", value: t.effort, options: EFFORTS, onChange: (effort) => set({ tiers: { [t.id]: { effort } } }) })] }) }, t.id));
247
245
  }), s.tasks.length > 1 &&
248
- s.tasks.map((task) => (_jsx(Row, { label: `${task.label} starts on`, children: _jsx(LumenSelector, { label: `${task.label} starts on`, size: "sm", className: "w-44", value: task.tier, options: tiers, onChange: (tier) => set({ tasks: { [task.id]: tier } }) }) }, task.id)))] }));
246
+ s.tasks.map((task) => (_jsx(SettingField, { label: `${task.label} starts on`, children: _jsx(LumenSelector, { label: `${task.label} starts on`, size: "sm", className: "w-44", value: task.tier, options: tiers, onChange: (tier) => set({ tasks: { [task.id]: tier } }) }) }, task.id)))] }));
249
247
  } }));
250
248
  }
251
249
  // ---------------------------------------------------------------- tools
252
250
  function Tools({ client }) {
253
251
  const { apply } = useApply(client);
254
- return (_jsx(WithStatus, { client: client, children: (s) => s.groups.length ? (_jsx("div", { className: "flex flex-col", children: s.groups.map((g) => (_jsx(Row, { label: g.label, info: g.about, children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: g.access, "aria-label": g.label, onValueChange: (v) => v &&
252
+ return (_jsx(WithStatus, { client: client, children: (s) => s.groups.length ? (_jsx("div", { className: "flex flex-col", children: s.groups.map((g) => (_jsx(SettingField, { label: g.label, info: g.about, children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: g.access, "aria-label": g.label, onValueChange: (v) => v &&
255
253
  void apply('access', () => client.settings({ access: { [g.id]: v } })), children: ACCESS.filter((a) => (a.value !== 'draft' || g.drafts) && (a.value !== 'write' || g.writes)).map((a) => (_jsx(ToggleGroupItem, { value: a.value, children: a.label }, a.value))) }) }, g.id))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "This app gives the assistant no tools yet." })) }));
256
254
  }
257
255
  // ---------------------------------------------------------------- web
@@ -302,7 +300,7 @@ function ScrapeqlBlock({ client, s }) {
302
300
  }
303
301
  function Web({ client }) {
304
302
  const { apply } = useApply(client);
305
- return (_jsx(WithStatus, { client: client, children: (s) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(Row, { label: "Read the web", info: "A plain fetch of public pages and APIs from the server, never the app's own pages.", children: _jsx(Switch, { "aria-label": "Read the web", checked: s.web.fetch, onCheckedChange: (fetch) => void apply('web', () => client.settings({ web: { fetch } })) }) }), _jsx(ScrapeqlBlock, { client: client, s: s })] })) }));
303
+ return (_jsx(WithStatus, { client: client, children: (s) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingField, { label: "Read the web", info: "A plain fetch of public pages and APIs from the server, never the app's own pages.", children: _jsx(Switch, { "aria-label": "Read the web", checked: s.web.fetch, onCheckedChange: (fetch) => void apply('web', () => client.settings({ web: { fetch } })) }) }), _jsx(ScrapeqlBlock, { client: client, s: s })] })) }));
306
304
  }
307
305
  // ---------------------------------------------------------------- spend
308
306
  function Spend({ client }) {
@@ -346,7 +344,7 @@ function Allowance({ client }) {
346
344
  : [{ where: 'What for', message: 'Say what it is for', target: whyId }]),
347
345
  ];
348
346
  const ask = () => void apply('request', () => requestMore(Number(pick), why.trim()), 'Request sent').then((ok) => ok && setWhy(''));
349
- return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex flex-col gap-2 border-b pb-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "font-semibold text-2xl tabular-nums", children: n(a.left) }), _jsxs("span", { className: "text-muted-foreground text-sm", children: ["left of ", n(a.total), a.period === 'month' ? ' this month' : ''] })] }), _jsx(ShareMeter, { value: a.total > 0 ? a.used / a.total : 1, wide: true })] }), r && (_jsx(Row, { label: `Asked for ${n(r.amount)} · ${day(r.at)}`, info: r.why || undefined, children: _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [r.status === 'granted' && r.granted !== undefined && (_jsxs("span", { className: "tabular-nums", children: ["+", n(r.granted)] })), _jsx(RequestBadge, { r: r })] }) })), r?.status === 'open' ? (_jsx("p", { className: "pt-3 text-muted-foreground text-sm", children: "You can ask again once this one is decided." })) : (_jsxs("div", { className: "flex flex-col gap-2 pt-3", children: [_jsx(Input, { id: whyId, "aria-label": "What for", placeholder: "What it is for", className: "h-8 w-full", maxLength: 500, value: why, onChange: (e) => setWhy(e.target.value) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(LumenSelector, { label: "Amount", size: "sm", className: "w-40", value: pick, options: a.amounts.map((x) => ({ value: String(x), label: n(x) })), onChange: (v) => setAmount(v) }), _jsx(SaveButton, { issues: issues, label: "Request more", icon: SendIcon, busy: busy === 'request', onSave: ask })] })] }))] }));
347
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex flex-col gap-2 border-b pb-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "font-semibold text-2xl tabular-nums", children: n(a.left) }), _jsxs("span", { className: "text-muted-foreground text-sm", children: ["left of ", n(a.total), a.period === 'month' ? ' this month' : ''] })] }), _jsx(ShareMeter, { value: a.total > 0 ? a.used / a.total : 1, wide: true })] }), r && (_jsx(SettingField, { label: `Asked for ${n(r.amount)} · ${day(r.at)}`, info: r.why || undefined, children: _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [r.status === 'granted' && r.granted !== undefined && (_jsxs("span", { className: "tabular-nums", children: ["+", n(r.granted)] })), _jsx(RequestBadge, { r: r })] }) })), r?.status === 'open' ? (_jsx("p", { className: "pt-3 text-muted-foreground text-sm", children: "You can ask again once this one is decided." })) : (_jsxs("div", { className: "flex flex-col gap-2 pt-3", children: [_jsx(Input, { id: whyId, "aria-label": "What for", placeholder: "What it is for", className: "h-8 w-full", maxLength: 500, value: why, onChange: (e) => setWhy(e.target.value) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(LumenSelector, { label: "Amount", size: "sm", className: "w-40", value: pick, options: a.amounts.map((x) => ({ value: String(x), label: n(x) })), onChange: (v) => setAmount(v) }), _jsx(SaveButton, { issues: issues, label: "Request more", icon: SendIcon, busy: busy === 'request', onSave: ask })] })] }))] }));
350
348
  } }));
351
349
  }
352
350
  /** One request, opened in place to grant (any amount) or decline. */
@@ -6,6 +6,7 @@ import { DetailsTable } from '../details-table.js';
6
6
  import { IconButton } from '../icon-button.js';
7
7
  import { InfoLabel } from '../info-tip.js';
8
8
  import { cn } from '../lib/utils.js';
9
+ import { SettingField } from '../setting-parts.js';
9
10
  import { Label } from '../ui/label.js';
10
11
  import { Spinner } from '../ui/spinner.js';
11
12
  import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
@@ -119,7 +120,7 @@ export function OwnKeyUsage({ client, always }) {
119
120
  .filter((e) => e.at >= since && !seen.has(e.id) && seen.add(e.id))
120
121
  .sort((a, b) => b.at - a.at);
121
122
  const total = sessions.reduce((a, e) => a + (e.own_usd ?? 0), 0);
122
- return (_jsxs("div", { "data-slot": "setting-row", className: "flex flex-col gap-2 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Label, { className: "min-w-0 flex-1", children: _jsx(InfoLabel, { info: "Each chat and run your own key or plan paid for, by its last use.", children: "Spent on your keys" }) }), _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: range, "aria-label": "Range", onValueChange: (v) => v && setRange(v), children: OWN_RANGES.map((r) => (_jsx(ToggleGroupItem, { value: r.id, "aria-label": `Last ${r.id}`, children: r.id }, r.id))) })] }), sessions.length ? (_jsx(DetailsTable, { head: [
123
+ return (_jsxs("div", { className: "flex flex-col gap-2 border-t border-border/60 pb-3 first:border-t-0", children: [_jsx(SettingField, { label: "Spent on your keys", info: "Each chat and run your own key or plan paid for, by its last use.", className: "border-t-0", end: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: range, "aria-label": "Range", onValueChange: (v) => v && setRange(v), children: OWN_RANGES.map((r) => (_jsx(ToggleGroupItem, { value: r.id, "aria-label": `Last ${r.id}`, children: r.id }, r.id))) }) }), sessions.length ? (_jsx(DetailsTable, { head: [
123
124
  { label: 'Session', grow: true },
124
125
  { label: 'Date', hideOnPhone: true },
125
126
  { label: 'Amount', right: true },
@@ -162,7 +162,7 @@ function ScreenProvider({ children }) {
162
162
  return null;
163
163
  const want = m.row.toLowerCase();
164
164
  const row = await waitFor(() => [
165
- ...document.querySelectorAll('[data-slot="settings"] [data-slot="setting-row"]'),
165
+ ...document.querySelectorAll('[data-slot="settings"] [data-slot="setting-field"]'),
166
166
  ].find((el) => el.querySelector('label')?.textContent?.trim().toLowerCase().includes(want)));
167
167
  if (!row)
168
168
  return `no row "${m.row}" in that section`;
@@ -19,7 +19,7 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTi
19
19
  import { InviteDrawer } from './invite-drawer.js';
20
20
  import { PersonDrawer } from './person-drawer.js';
21
21
  import { AuditSection, InvitesSection, PermissionsSection } from './sections.js';
22
- import { LevelPicker, STATUS_LABEL, fromNow, peopleView, scopeKey, statusTone, topScopes, usePeopleAction, usePeopleQuery, useVisitView, when, withGrant, } from './shared.js';
22
+ import { LevelPicker, STATUS_LABEL, fromNow, peopleView, scopeKey, statusTone, syncText, topScopes, usePeopleAction, usePeopleQuery, useVisitView, when, withGrant, } from './shared.js';
23
23
  export function LumenPeople({ adapter, crumbs = [], section: given, onSection, onSetUpMail, fields = [], }) {
24
24
  const [own, setOwn] = useState('people');
25
25
  const section = given ?? own;
@@ -167,8 +167,13 @@ function PeopleList({ frame, fields, onInvite, }) {
167
167
  const invited = people.filter((p) => p.status === 'invited').length;
168
168
  const active = people.filter((p) => p.status === 'active').length;
169
169
  const disabled = people.filter((p) => p.status === 'disabled').length;
170
- const onlyMe = people.length <= 1;
171
- return (_jsxs(_Fragment, { children: [_jsx(LumenLayout, { crumbs: frame.crumbs, actions: frame.invite, intro: _jsxs("div", { className: "flex flex-col gap-3", children: [frame.tabs, _jsxs(TileRow, { fill: true, "aria-label": "People at a glance", children: [_jsx(Tile, { label: "People", value: people.length, caption: `${active} active${disabled ? ` · ${disabled} disabled` : ''}` }), _jsx(Tile, { label: "Admins", value: admins.length, caption: "Can\u2019t remove the last admin" }), _jsx(Tile, { label: "Invites pending", value: invited, caption: expired ? `${expired} expired` : 'none expired' }), view.sync ? (_jsx(Tile, { label: "Sign-in sync", value: view.sync.ok ? 'Synced' : 'Out of step', caption: view.sync.at ? when(view.sync.at) : view.sync.detail, valueClassName: view.sync.ok ? undefined : 'text-loss' })) : (_jsx(Tile, { label: "Email", value: view.mail.configured ? 'Working' : 'Not set up', caption: view.mail.from ?? 'links only' }))] }), frame.mailNote, picked.size > 0 && (_jsx(BulkBar, { frame: frame, ids: [...picked], onClear: () => setPicked(new Set()) }))] }), domain: domain, ...layout, rows: people, load: load, state: onlyMe ? _jsx(FirstRun, { mail: view.mail.configured, onInvite: onInvite }) : undefined, selection: { keys: picked, onChange: setPicked }, rowTitle: (p) => p.name || p.email, onOpen: (p) => setOpenId(p.id) }), _jsx(LumenBoundary, { where: "people-person", what: "panel", children: _jsx(PersonDrawer, { adapter: adapter, view: view, person: open, onClose: () => setOpenId(null) }) })] }));
170
+ // The first-run note stands in for the table only when it has no row; a lone admin is a row.
171
+ const none = people.length === 0;
172
+ return (_jsxs(_Fragment, { children: [_jsx(LumenLayout, { crumbs: frame.crumbs, actions: frame.invite, intro: _jsxs("div", { className: "flex flex-col gap-3", children: [frame.tabs, _jsxs(TileRow, { fill: true, "aria-label": "People at a glance", children: [_jsx(Tile, { label: "People", value: people.length, caption: `${active} active${disabled ? ` · ${disabled} disabled` : ''}` }), _jsx(Tile, { label: "Admins", value: admins.length, caption: "Can\u2019t remove the last admin" }), _jsx(Tile, { label: "Invites pending", value: invited, caption: expired ? `${expired} expired` : 'none expired' }), view.sync ? (_jsx(Tile, { label: "Sign-in sync", value: syncText(view.sync), caption: !view.sync.configured
173
+ ? 'Invites don’t reach sign-in'
174
+ : view.sync.at
175
+ ? when(view.sync.at)
176
+ : view.sync.detail, valueClassName: !view.sync.configured || view.sync.ok ? undefined : 'text-loss' })) : (_jsx(Tile, { label: "Email", value: view.mail.configured ? 'Working' : 'Not set up', caption: view.mail.from ?? 'links only' }))] }), frame.mailNote, picked.size > 0 && (_jsx(BulkBar, { frame: frame, ids: [...picked], onClear: () => setPicked(new Set()) }))] }), domain: domain, ...layout, rows: people, load: load, state: none ? _jsx(FirstRun, { mail: view.mail.configured, onInvite: onInvite }) : undefined, selection: { keys: picked, onChange: setPicked }, rowTitle: (p) => p.name || p.email, onOpen: (p) => setOpenId(p.id) }), _jsx(LumenBoundary, { where: "people-person", what: "panel", children: _jsx(PersonDrawer, { adapter: adapter, view: view, person: open, onClose: () => setOpenId(null) }) })] }));
172
177
  }
173
178
  /** One change over the ticked people. Apply clears the selection; Undo puts the change back. */
174
179
  function BulkBar({ frame, ids, onClear, }) {
@@ -14,7 +14,7 @@ import { LumenSelector } from '../lumen-selector.js';
14
14
  import { NoteLine } from '../note-line.js';
15
15
  import { Badge } from '../ui/badge.js';
16
16
  import { Button } from '../ui/button.js';
17
- import { LevelPicker, STATUS_LABEL, adminGuard, fromNow, topScopes, usePeopleAction, when, withGrant, } from './shared.js';
17
+ import { LevelPicker, STATUS_LABEL, adminGuard, fromNow, syncText, topScopes, usePeopleAction, when, withGrant, } from './shared.js';
18
18
  const ROLES = [
19
19
  { value: 'member', label: 'Member', description: 'Only the levels set per scope' },
20
20
  { value: 'admin', label: 'Admin', description: 'Top level everywhere, and manages People' },
@@ -47,7 +47,9 @@ export function PersonDrawer({ adapter, view, person, onClose, }) {
47
47
  },
48
48
  view.sync && {
49
49
  label: 'Sign-in provider',
50
- value: `${view.sync.ok ? 'Synced' : 'Out of step'} · ${when(view.sync.at)}`,
50
+ value: view.sync.at
51
+ ? `${syncText(view.sync)} · ${when(view.sync.at)}`
52
+ : syncText(view.sync),
51
53
  },
52
54
  ] }) }), adapter.audit && _jsx(ActivitySection, { adapter: adapter, person: p }), _jsx(DangerSection, { adapter: adapter, person: p, guard: guard, onGone: onClose })] })) }));
53
55
  }
@@ -1,4 +1,4 @@
1
- import { type Grant, type PeopleView, type Person, type PersonStatus, type Scope } from '@columenlabs/lumen-ui-types/people';
1
+ import { type Grant, type PeopleView, type Person, type PersonStatus, type Scope, type SyncStatus } from '@columenlabs/lumen-ui-types/people';
2
2
  import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
3
3
  import { type ReactNode } from 'react';
4
4
  import type { Crumb } from '../page.js';
@@ -12,6 +12,8 @@ export declare const statusTone: (p: Pick<Person, 'status'>) => ViewChipTone | u
12
12
  */
13
13
  export declare function when(ts: number | null | undefined, now?: number): string;
14
14
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
15
+ /** A sign-in sync's one-word state: never set up is not a failed sync. */
16
+ export declare const syncText: (s: Pick<SyncStatus, 'configured' | 'ok'>) => "Not set up" | "Out of step" | "Synced";
15
17
  export declare function fromNow(ts: number | null | undefined, now?: number): string;
16
18
  /** The top scopes (apps), in order; constructs inside one have a `parent`. */
17
19
  export declare const topScopes: (scopes: readonly Scope[]) => Scope[];
@@ -53,6 +53,8 @@ export function when(ts, now = Date.now()) {
53
53
  }));
54
54
  }
55
55
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
56
+ /** A sign-in sync's one-word state: never set up is not a failed sync. */
57
+ export const syncText = (s) => !s.configured ? 'Not set up' : s.ok ? 'Synced' : 'Out of step';
56
58
  export function fromNow(ts, now = Date.now()) {
57
59
  if (!ts)
58
60
  return '—';
@@ -1,5 +1,8 @@
1
1
  import { type ComponentType, type ReactNode } from 'react';
2
2
  import { type KeyTestResult } from './integrations.js';
3
+ /** What Settings' search box holds: a `SettingField` whose label or info misses it hides. */
4
+ export declare const SettingsSearch: import("react").Context<string>;
5
+ export declare const settingMatches: (query: string, ...texts: ReactNode[]) => boolean;
3
6
  /** One setting inside an item: its label, then its control group, then an optional switch at the end. */
4
7
  export declare function SettingField({ label, info, htmlFor, end, stack, className, children, }: {
5
8
  /** The setting's own name ("Keys", "Webhook"), never the thing's ("Plaid keys"). */
@@ -14,7 +17,7 @@ export declare function SettingField({ label, info, htmlFor, end, stack, classNa
14
17
  className?: string;
15
18
  /** The control: an input, a picker, a `SettingValue`, a ToggleGroup. Its own buttons sit inside it. */
16
19
  children?: ReactNode;
17
- }): import("react").JSX.Element;
20
+ }): import("react").JSX.Element | null;
18
21
  /** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
19
22
  export declare function SettingValue({ value, label, mono, copy, onReplace, replaceLabel, replaceIcon: ReplaceIcon, }: {
20
23
  value: string;
@@ -7,7 +7,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  // A row is never a lone button ("Last test", "Remove keys"), and a label never repeats the thing's name.
8
8
  // Built on shadcn's Field and InputGroup parts.
9
9
  import { AlertTriangleIcon, CheckIcon, CopyIcon, PencilIcon, RefreshCwIcon, Trash2Icon, } from 'lucide-react';
10
- import { useState } from 'react';
10
+ import { createContext, useContext, useState } from 'react';
11
11
  import { LumenConfirm } from './confirm.js';
12
12
  import { IconButton } from './icon-button.js';
13
13
  import { InfoTip } from './info-tip.js';
@@ -16,8 +16,14 @@ import { cn } from './lib/utils.js';
16
16
  import { Badge } from './ui/badge.js';
17
17
  import { InputGroup, InputGroupAddon, InputGroupButton } from './ui/input-group.js';
18
18
  import { Label } from './ui/label.js';
19
+ /** What Settings' search box holds: a `SettingField` whose label or info misses it hides. */
20
+ export const SettingsSearch = createContext('');
21
+ export const settingMatches = (query, ...texts) => texts.some((t) => typeof t === 'string' && t.toLowerCase().includes(query.toLowerCase()));
19
22
  /** One setting inside an item: its label, then its control group, then an optional switch at the end. */
20
23
  export function SettingField({ label, info, htmlFor, end, stack = false, className, children, }) {
24
+ const query = useContext(SettingsSearch);
25
+ if (query && !settingMatches(query, label, info))
26
+ return null;
21
27
  return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-t border-border/60 py-1.5 first:border-t-0', stack && 'max-sm:flex-wrap max-sm:gap-y-1.5', className), children: [_jsxs("div", { className: cn('flex w-32 shrink-0 items-center gap-1.5 max-sm:w-24', stack && 'max-sm:w-auto max-sm:flex-1',
22
28
  // Nothing beside it but `end`: the label takes the row, so a short name never cuts off.
23
29
  children === undefined && 'w-auto min-w-0 flex-1 shrink max-sm:w-auto'), children: [_jsx(Label, { htmlFor: htmlFor, className: "truncate font-normal text-muted-foreground", title: label, children: label }), info && _jsx(InfoTip, { label: `About ${label}`, children: info })] }), children !== undefined && (_jsx("div", { className: cn('flex min-w-0 max-w-md flex-1 items-center gap-2', stack && 'max-sm:order-2 max-sm:max-w-none max-sm:basis-full'), children: children })), end && _jsx("div", { className: cn('ml-auto shrink-0', stack && 'max-sm:order-1'), children: end })] }));
@@ -31,13 +31,17 @@ export declare function SettingsActions({ children }: {
31
31
  export declare function SettingsFooter({ children }: {
32
32
  children: ReactNode;
33
33
  }): import("react").ReactPortal | null;
34
- /** One setting: title and description on the left, its control on the right. */
34
+ /**
35
+ * One setting: the kit's `SettingField` (docs/parts/settings.md), with `description` behind its (i).
36
+ * Kept so older call sites build; it draws nothing of its own. New code uses `SettingField`.
37
+ * @deprecated Use `SettingField`.
38
+ */
35
39
  export declare function SettingRow({ label, description, htmlFor, children, }: {
36
40
  label: string;
37
41
  description?: ReactNode;
38
42
  htmlFor?: string;
39
43
  children: ReactNode;
40
- }): import("react").JSX.Element | null;
44
+ }): import("react").JSX.Element;
41
45
  /** Light, dark or system: in Settings with labels, in the profile menu as icons with tooltips. */
42
46
  export declare function ThemeToggle({ compact }: {
43
47
  compact?: boolean;
package/dist/settings.js CHANGED
@@ -13,7 +13,7 @@ import { useAssistantMoves } from './assistant-screen.js';
13
13
  import { InfoTip } from './info-tip.js';
14
14
  import { LumenSelector } from './lumen-selector.js';
15
15
  import { FONTS, FONT_SIZES, setPref, usePrefs, } from './prefs.js';
16
- import { SettingStatus } from './setting-parts.js';
16
+ import { SettingField, SettingStatus, SettingsSearch, settingMatches as matches, } from './setting-parts.js';
17
17
  import { TimeZoneSelect } from './status.js';
18
18
  import { Dialog, DialogContent, DialogDescription, DialogTitle } from './ui/dialog.js';
19
19
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
@@ -29,8 +29,6 @@ const THEMES = [
29
29
  { value: 'system', label: 'Follow the device', short: 'System', icon: MonitorIcon },
30
30
  ];
31
31
  /** The Settings search text; rows that do not match it hide. */
32
- const SearchContext = createContext('');
33
- const matches = (query, ...texts) => texts.some((t) => typeof t === 'string' && t.toLowerCase().includes(query.toLowerCase()));
34
32
  /** The open section's header slot for its actions (Add and Resync sit by the
35
33
  * title, not on a sub-header). Each section gets its own, so search results keep theirs apart. */
36
34
  const ActionsSlot = createContext(null);
@@ -51,12 +49,13 @@ function ActionsScope({ children, }) {
51
49
  const [foot, setFoot] = useState(null);
52
50
  return (_jsx(ActionsSlot.Provider, { value: el, children: _jsx(FooterSlot.Provider, { value: foot, children: children(setEl, setFoot) }) }));
53
51
  }
54
- /** One setting: title and description on the left, its control on the right. */
52
+ /**
53
+ * One setting: the kit's `SettingField` (docs/parts/settings.md), with `description` behind its (i).
54
+ * Kept so older call sites build; it draws nothing of its own. New code uses `SettingField`.
55
+ * @deprecated Use `SettingField`.
56
+ */
55
57
  export function SettingRow({ label, description, htmlFor, children, }) {
56
- const query = useContext(SearchContext);
57
- if (query && !matches(query, label, description))
58
- return null;
59
- return (_jsxs("div", { "data-slot": "setting-row", className: "flex items-center gap-4 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [_jsx(Label, { htmlFor: htmlFor, className: "truncate", children: label }), description && _jsx(InfoTip, { label: `About ${label}`, children: description })] }), _jsx("div", { className: "shrink-0", children: children })] }));
58
+ return (_jsx(SettingField, { label: label, info: description, htmlFor: htmlFor, children: children }));
60
59
  }
61
60
  /** Light, dark or system: in Settings with labels, in the profile menu as icons with tooltips. */
62
61
  export function ThemeToggle({ compact = false }) {
@@ -148,7 +147,7 @@ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profil
148
147
  if (!q)
149
148
  return;
150
149
  const ids = [...(pane.current?.querySelectorAll('[data-settings-section]') ?? [])]
151
- .filter((el) => el.dataset.whole === 'true' || el.querySelector('[data-slot=setting-row]'))
150
+ .filter((el) => el.dataset.whole === 'true' || el.querySelector('[data-slot=setting-field]'))
152
151
  .map((el) => el.dataset.settingsSection ?? '');
153
152
  setFound((f) => (f.join() === ids.join() ? f : ids));
154
153
  });
@@ -201,6 +200,6 @@ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profil
201
200
  const whole = matches(q, s.title, s.description);
202
201
  return (_jsx(ActionsScope, { children: (slot) => (_jsxs("div", { "data-settings-section": s.id, "data-whole": whole, "aria-labelledby": `settings-${s.id}-title`, className: whole
203
202
  ? 'flex flex-col gap-3'
204
- : 'hidden flex-col gap-3 has-[[data-slot=setting-row]]:flex', children: [header(s, slot), _jsx(Separator, {}), _jsx(SearchContext.Provider, { value: whole ? '' : q, children: s.content })] })) }, s.id));
203
+ : 'hidden flex-col gap-3 has-[[data-slot=setting-field]]:flex', children: [header(s, slot), _jsx(Separator, {}), _jsx(SettingsSearch.Provider, { value: whole ? '' : q, children: s.content })] })) }, s.id));
205
204
  }), !found.length && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["No settings match \u201C", q, "\u201D."] }))] })) : (_jsx(ActionsScope, { children: (slot, footer) => (_jsxs("section", { "aria-labelledby": `settings-${current.id}-title`, className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-col gap-3 px-4 pt-4 md:px-6 md:pt-6", children: [header(current, slot), _jsx(Separator, {})] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-4 pt-3 pb-4 *:shrink-0 md:px-6 [&:has(+[data-slot=settings-footer]:empty)]:end-fade", children: current.content }), _jsx("div", { ref: footer, "data-slot": "settings-footer", className: "flex min-h-12 shrink-0 items-center justify-end gap-2 border-t px-4 py-2 empty:hidden md:px-6", children: current.footer })] })) }, current.id))] }) }));
206
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.236",
3
+ "version": "0.1.238",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.236",
52
+ "@columenlabs/lumen-ui-types": "0.1.238",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",