@columenlabs/lumen-ui 0.1.235 → 0.1.237

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 }) }) })] }));
@@ -117,12 +115,20 @@ const wayTest = (k, plan = false) => k?.test
117
115
  : null;
118
116
  /** A provider's mark: its initial on a tile. */
119
117
  const mark = (label) => _jsx(ConnectionLogo, { text: label.charAt(0) });
118
+ /**
119
+ * Whether the app lets this person use `w` of their own on `p` (the app's `ownWays`, the same
120
+ * rule the server pays by). A way it does not allow never shows, even when one is still stored:
121
+ * the server would not run on it, so Settings never says it works.
122
+ */
123
+ const mayOwn = (p, w) => !p.own_ways || p.own_ways.includes(w);
124
+ /** Whether the person has a way of their own on `p` that the app lets them use. */
125
+ const ownUsable = (p) => (mayOwn(p, 'key') && p.own.set) || (mayOwn(p, 'plan') && !!p.plan?.set);
120
126
  /** One provider, collapsed to a row: the person's own key and, where it has one, their plan sign-in. */
121
127
  function OwnProviderKey({ client, p }) {
122
128
  const { apply, refresh } = useApply(client);
123
129
  const ways = [];
124
- const may = (w) => !p.own_ways || p.own_ways.includes(w);
125
- if (p.signin === 'chatgpt' && (may('plan') || p.plan?.set))
130
+ const may = (w) => mayOwn(p, w);
131
+ if (p.signin === 'chatgpt' && may('plan'))
126
132
  ways.push({
127
133
  id: 'plan',
128
134
  label: 'ChatGPT plan',
@@ -139,7 +145,7 @@ function OwnProviderKey({ client, p }) {
139
145
  removeText: 'Your API key, if you set one, runs from then on.',
140
146
  removeLabel: 'Sign out',
141
147
  });
142
- if (may('key') || p.own.set)
148
+ if (may('key'))
143
149
  ways.push({
144
150
  id: 'key',
145
151
  label: 'API key',
@@ -161,14 +167,14 @@ function OwnProviderKey({ client, p }) {
161
167
  function Providers({ client }) {
162
168
  const { apply } = useApply(client);
163
169
  return (_jsx(WithStatus, { client: client, children: (s) => {
164
- // A provider the app gives this person no way of their own on shows no row, unless one is
165
- // still set (so it can be removed).
166
- const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0 || p.own.set || p.plan?.set);
167
- const ownSet = rows.some((p) => p.own.set || p.plan?.set);
168
- 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: [
170
+ // A provider the app gives this person no way of their own on shows no row: what shows here
171
+ // is exactly what the server may pay with.
172
+ const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0);
173
+ const ownSet = rows.some(ownUsable);
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: [
169
175
  { value: 'own', label: 'Your own first' },
170
176
  { value: 'app', label: 'Shared credits first' },
171
- ], 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 })] }));
172
178
  } }));
173
179
  }
174
180
  /** One provider's shared key, which an admin sets for everyone. */
@@ -204,7 +210,7 @@ function SharedCap({ client, s }) {
204
210
  const { apply } = useApply(client);
205
211
  if (s.allowance?.period === 'once')
206
212
  return null;
207
- 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)) }) }));
208
214
  }
209
215
  /** Admins only: the app's shared keys and what each person may spend on them. */
210
216
  function SharedKeys({ client }) {
@@ -235,15 +241,15 @@ function Models({ client }) {
235
241
  return (_jsxs("div", { className: "flex flex-col", children: [s.tiers.map((t) => {
236
242
  const p = s.providers.find((x) => x.id === t.provider);
237
243
  const keyless = p && !p.app.set && !p.app.from_env && !p.own.set && !p.plan?.set;
238
- 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));
239
245
  }), s.tasks.length > 1 &&
240
- 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)))] }));
241
247
  } }));
242
248
  }
243
249
  // ---------------------------------------------------------------- tools
244
250
  function Tools({ client }) {
245
251
  const { apply } = useApply(client);
246
- 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 &&
247
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." })) }));
248
254
  }
249
255
  // ---------------------------------------------------------------- web
@@ -294,7 +300,7 @@ function ScrapeqlBlock({ client, s }) {
294
300
  }
295
301
  function Web({ client }) {
296
302
  const { apply } = useApply(client);
297
- 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 })] })) }));
298
304
  }
299
305
  // ---------------------------------------------------------------- spend
300
306
  function Spend({ client }) {
@@ -338,7 +344,7 @@ function Allowance({ client }) {
338
344
  : [{ where: 'What for', message: 'Say what it is for', target: whyId }]),
339
345
  ];
340
346
  const ask = () => void apply('request', () => requestMore(Number(pick), why.trim()), 'Request sent').then((ok) => ok && setWhy(''));
341
- 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 })] })] }))] }));
342
348
  } }));
343
349
  }
344
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`;
@@ -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.235",
3
+ "version": "0.1.237",
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.235",
52
+ "@columenlabs/lumen-ui-types": "0.1.237",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",