@columenlabs/lumen-ui 0.1.188 → 0.1.189

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.
@@ -101,10 +101,11 @@ export type ConnectionStatus = 'connected' | 'untested' | 'expired' | 'failed' |
101
101
  export declare function connectionStatus(way: ConnectionWay | undefined): ConnectionStatus;
102
102
  /** The way in use: the picked one when it is set up, else the first set up. */
103
103
  export declare function activeWay(ways: ConnectionWay[], use?: string): ConnectionWay | undefined;
104
- /** The row's pill for a status. */
105
- export declare function ConnectionBadge({ status, signin, }: {
104
+ /** The row's pill for a status: Works, Failed, Not tested or Off; hover says to sign in again. */
105
+ export declare function ConnectionBadge({ status, signin, title, }: {
106
106
  status: ConnectionStatus;
107
107
  signin?: boolean;
108
+ title?: string;
108
109
  }): import("react").JSX.Element;
109
110
  /** A connection's mark: an icon or a letter on a tile. */
110
111
  export declare function ConnectionLogo({ icon: Icon, text, }: {
@@ -113,5 +114,10 @@ export declare function ConnectionLogo({ icon: Icon, text, }: {
113
114
  }>;
114
115
  text?: string;
115
116
  }): import("react").JSX.Element;
116
- /** One connection: a row with its status, the ways to connect inside. */
117
+ /**
118
+ * One connection on the settings slots: the header carries its name, the way in use and one
119
+ * status pill; each way is a row (its label beside its value, Replace or Sign in again inside);
120
+ * with one way the action bar holds its last test, Test and Remove, with several each row ends in
121
+ * its own. `lines` are rows of their own; `children` the app's settings below them.
122
+ */
117
123
  export declare function LumenConnection({ id, name, meta, logo, ways, use, onUse, lines, defaultOpen, children, }: LumenConnectionProps): import("react").JSX.Element;
@@ -1,22 +1,21 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // One connection, one look on every app: an AI provider, an email relay, a Google account, a
3
- // connector. A row that expands in place (LumenExpandRow) with one status pill; inside, each way to
4
- // connect (a pasted key or token, a sign-in that ends on an address the person pastes back, or the
5
- // app's own form). Several ways show radios and an In use tag, so a person who has both an API key and
6
- // a plan sign-in picks which one runs. A way that is set up shows who or what it is and its last test,
7
- // then Test, Remove (asks first) and Replace or Sign in again, all on the right. `lines` and
8
- // `children` hold the app's own settings below the ways (a server, a sender, folders).
9
- import { AlertTriangleIcon, CheckIcon, ExternalLinkIcon, KeyRoundIcon, LogInIcon, PlusIcon, RefreshCwIcon, Trash2Icon, XIcon, } from 'lucide-react';
3
+ // connector. Built on the settings slots (docs/parts/settings.md): a LumenExpandRow whose header has
4
+ // one status pill; inside, each way to connect is a row (a pasted key or token, a sign-in that ends on
5
+ // an address the person pastes back, or the app's own form) with Replace or Sign in again inside its
6
+ // value. Several ways add a Use row, so a person who has both an API key and a plan sign-in picks which
7
+ // one runs, and each way's row ends in its own status, Test and Remove; one way puts its last test,
8
+ // Test and Remove in the action bar. `lines` and `children` are the app's own rows below the ways.
9
+ import { ExternalLinkIcon, LogInIcon, PlusIcon, RefreshCwIcon, Trash2Icon } from 'lucide-react';
10
10
  import { useId, useState } from 'react';
11
11
  import { LumenConfirm } from './confirm.js';
12
12
  import { LumenExpandRow } from './expand-list.js';
13
13
  import { SaveButton } from './form-issues.js';
14
14
  import { IconButton } from './icon-button.js';
15
- import { InfoLabel } from './info-tip.js';
16
- import { ErrorLine, errorText, useClock } from './integrations.js';
15
+ import { ErrorLine, errorText } from './integrations.js';
17
16
  import { cn } from './lib/utils.js';
18
17
  import { lumenToast } from './lumen-toast.js';
19
- import { Badge } from './ui/badge.js';
18
+ import { SettingActions, SettingField, SettingStatus, SettingValue, } from './setting-parts.js';
20
19
  import { Button } from './ui/button.js';
21
20
  import { Input } from './ui/input.js';
22
21
  import { Label } from './ui/label.js';
@@ -35,44 +34,54 @@ export function connectionStatus(way) {
35
34
  export function activeWay(ways, use) {
36
35
  return ways.find((w) => w.id === use && w.set) ?? ways.find((w) => w.set);
37
36
  }
38
- /** The row's pill for a status. */
39
- export function ConnectionBadge({ status, signin, }) {
40
- switch (status) {
41
- case 'connected':
42
- return (_jsxs(Badge, { variant: "outline", className: "text-gain", children: [_jsx(CheckIcon, {}), "Connected"] }));
43
- case 'untested':
44
- return _jsx(Badge, { variant: "outline", children: "Not tested" });
45
- case 'expired':
46
- return (_jsxs(Badge, { variant: "outline", className: "text-warning", children: [_jsx(AlertTriangleIcon, {}), signin ? 'Sign in again' : 'Key expired'] }));
47
- case 'failed':
48
- return (_jsxs(Badge, { variant: "outline", className: "text-loss", children: [_jsx(AlertTriangleIcon, {}), "Failed"] }));
49
- default:
50
- return (_jsx(Badge, { variant: "outline", className: "border-dashed text-muted-foreground", children: "Not connected" }));
51
- }
37
+ /** The row's pill for a status: Works, Failed, Not tested or Off; hover says to sign in again. */
38
+ export function ConnectionBadge({ status, signin, title, }) {
39
+ const health = status === 'connected'
40
+ ? 'works'
41
+ : status === 'untested'
42
+ ? 'untested'
43
+ : status === 'off'
44
+ ? 'off'
45
+ : 'failed';
46
+ return (_jsx(SettingStatus, { health: health, title: title ?? (status === 'expired' ? (signin ? 'Sign in again' : 'Key expired') : undefined) }));
52
47
  }
53
48
  /** A connection's mark: an icon or a letter on a tile. */
54
49
  export function ConnectionLogo({ icon: Icon, text, }) {
55
50
  return (_jsx("span", { "aria-hidden": true, className: "flex size-8 shrink-0 items-center justify-center rounded-md border bg-muted font-semibold text-muted-foreground text-sm", children: Icon ? _jsx(Icon, { className: "size-4" }) : text }));
56
51
  }
57
- /** `just now`, `5 min ago`, `3 h ago`, `2 d ago`. */
58
- function ago(ms) {
59
- const s = Math.max(0, (Date.now() - ms) / 1000);
60
- if (s < 60)
61
- return 'just now';
62
- if (s < 3600)
63
- return `${Math.floor(s / 60)} min ago`;
64
- if (s < 86400)
65
- return `${Math.floor(s / 3600)} h ago`;
66
- return `${Math.floor(s / 86400)} d ago`;
67
- }
68
52
  /** `Service token` → `service token`; `API key` stays. */
69
53
  const lower = (label) => /^[A-Z][a-z]/.test(label) ? label.charAt(0).toLowerCase() + label.slice(1) : label;
70
- const toMs = (at) => (typeof at === 'number' ? at : Date.parse(at));
71
- /** One connection: a row with its status, the ways to connect inside. */
54
+ const toIso = (at) => new Date(typeof at === 'number' ? at : Date.parse(at)).toISOString();
55
+ /** A way's last test as the action bar reads it. */
56
+ const lastTest = (t) => t
57
+ ? {
58
+ at: toIso(t.at),
59
+ ok: t.ok,
60
+ detail: t.detail,
61
+ error: t.ok ? t.error : (t.error ?? (t.expired ? 'Expired' : null)),
62
+ }
63
+ : null;
64
+ /** The confirm that removes a way. */
65
+ const removeOf = (way) => !way.readOnly && !way.note && way.onRemove
66
+ ? {
67
+ label: way.removeLabel ?? `Remove ${way.label}`,
68
+ title: way.removeTitle ?? `Remove ${lower(way.label)}?`,
69
+ description: way.removeText ?? 'You can add it again any time.',
70
+ action: way.removeLabel ?? 'Remove',
71
+ run: () => way.onRemove?.(),
72
+ }
73
+ : undefined;
74
+ /**
75
+ * One connection on the settings slots: the header carries its name, the way in use and one
76
+ * status pill; each way is a row (its label beside its value, Replace or Sign in again inside);
77
+ * with one way the action bar holds its last test, Test and Remove, with several each row ends in
78
+ * its own. `lines` are rows of their own; `children` the app's settings below them.
79
+ */
72
80
  export function LumenConnection({ id, name, meta, logo, ways, use, onUse, lines, defaultOpen, children, }) {
73
81
  const active = activeWay(ways, use);
74
82
  const status = connectionStatus(active);
75
83
  const pick = ways.length > 1 && !!onUse;
84
+ const solo = ways.length === 1 ? ways[0] : null;
76
85
  const [picking, setPicking] = useState(false);
77
86
  const choose = async (way) => {
78
87
  if (!onUse || way === active?.id)
@@ -90,49 +99,52 @@ export function LumenConnection({ id, name, meta, logo, ways, use, onUse, lines,
90
99
  };
91
100
  const line = meta ??
92
101
  (active ? (_jsxs(_Fragment, { children: [ways.length > 1 && active.label, ways.length > 1 && active.ident ? ' · ' : '', active.ident] })) : ('Not set up'));
93
- const list = ways.map((w) => (_jsx(WayRow, { way: w, pick: pick, inUse: w.id === active?.id && ways.length > 1 }, w.id)));
94
- return (_jsxs(LumenExpandRow, { id: id, name: name, meta: line, icon: logo, defaultOpen: defaultOpen, badge: _jsx(ConnectionBadge, { status: status, signin: !!active?.signin }), children: [_jsx("div", { className: "grid grid-cols-1 gap-2", children: pick ? (_jsx(RadioGroup, { value: active?.id ?? '', onValueChange: (v) => void choose(v), disabled: picking, "aria-label": `Which way ${typeof name === 'string' ? name : ''} connects`, className: "gap-2", children: list })) : (list) }), lines?.length ? (_jsx("dl", { className: "grid grid-cols-1 text-sm", children: lines.map((l) => (_jsxs("div", { className: "flex items-center gap-3 border-b py-2 last:border-b-0", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: _jsx(InfoLabel, { info: l.info, children: l.label }) }), _jsx("dd", { className: cn('min-w-0 flex-1 truncate text-right', l.mono && 'font-code text-xs'), title: typeof l.value === 'string' ? l.value : undefined, children: l.value })] }, l.label))) })) : null, children] }));
102
+ const t = active?.test;
103
+ return (_jsxs(LumenExpandRow, { id: id, name: name, meta: line, icon: logo, defaultOpen: defaultOpen, status: _jsx(ConnectionBadge, { status: status, signin: !!active?.signin, title: t ? (t.ok ? (t.detail ?? undefined) : (t.error ?? undefined)) : undefined }), actions: solo?.set && !solo.note && (solo.onTest || removeOf(solo)) ? (_jsx(SettingActions, { test: lastTest(solo.test), onTest: solo.onTest, remove: removeOf(solo) })) : undefined, children: [pick && (_jsx(SettingField, { label: "Use", children: _jsx(RadioGroup, { value: active?.id ?? '', onValueChange: (v) => void choose(v), disabled: picking, "aria-label": `Which way ${typeof name === 'string' ? name : ''} connects`, className: "flex flex-wrap gap-x-4 gap-y-1", children: ways.map((w) => (_jsx(WayChoice, { way: w }, w.id))) }) })), ways.map((w) => (_jsx(WayField, { way: w, solo: !!solo }, w.id))), lines?.map((l) => (_jsx(SettingField, { label: l.label, info: l.info, children: _jsx("span", { className: cn('min-w-0 truncate text-sm', l.mono && 'font-code text-xs'), title: typeof l.value === 'string' ? l.value : undefined, children: l.value }) }, l.label))), children] }));
95
104
  }
96
- /** One way: its name and state, its actions on the right, its form when open. */
97
- function WayRow({ way, pick, inUse }) {
98
- const radioId = useId();
99
- const clock = useClock();
105
+ /** One way in the Use picker. */
106
+ function WayChoice({ way }) {
107
+ const rid = useId();
108
+ return (_jsxs("span", { className: "flex min-h-8 items-center gap-2", children: [_jsx(RadioGroupItem, { id: rid, value: way.id, disabled: !way.set, "aria-label": `Use ${way.label}` }), _jsx(Label, { htmlFor: rid, className: cn('font-normal', !way.set && 'text-muted-foreground'), children: way.label })] }));
109
+ }
110
+ /** One way as a row: its label, then its value with Replace inside, or Add; its form when open. */
111
+ function WayField({ way, solo }) {
100
112
  const [editing, setEditing] = useState(false);
101
- const [testing, setTesting] = useState(false);
102
- const [error, setError] = useState(null);
103
- const Icon = way.icon;
104
- const status = connectionStatus(way);
105
- const failing = status === 'failed' || status === 'expired';
113
+ const close = () => setEditing(false);
106
114
  const editable = !way.readOnly && !way.note;
107
115
  const hasForm = !!(way.form || way.fields?.length || way.signin);
116
+ if (editing)
117
+ return way.form ? (_jsx("div", { className: "py-2", children: way.form(close) })) : way.signin ? (_jsx(SigninForm, { way: way, close: close })) : (_jsx(FieldsForm, { way: way, close: close }));
118
+ const Icon = way.signin ? LogInIcon : PlusIcon;
119
+ const addLabel = way.addLabel ??
120
+ (way.signin
121
+ ? way.signin.provider
122
+ ? `Sign in with ${way.signin.provider}`
123
+ : 'Sign in'
124
+ : `Add ${lower(way.label)}`);
125
+ const control = way.note ? (_jsx("span", { className: "min-w-0 truncate text-muted-foreground text-sm", children: way.note })) : way.set ? ((typeof way.ident === 'string' || way.ident == null) && editable && hasForm ? (_jsx(SettingValue, { value: way.ident ?? 'Set', label: way.label, mono: true, onReplace: () => setEditing(true), replaceLabel: way.signin ? 'Sign in again' : 'Replace', replaceIcon: way.signin ? LogInIcon : undefined })) : (_jsx("span", { className: cn('min-w-0 truncate text-sm', typeof way.ident === 'string' && 'font-code text-xs'), title: typeof way.ident === 'string' ? way.ident : undefined, children: way.ident ?? 'Set' }))) : editable && hasForm ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: [_jsx(Icon, {}), addLabel] })) : (_jsx("span", { className: "text-muted-foreground text-sm", children: "Not set up" }));
126
+ return (_jsx(SettingField, { label: way.label, info: way.info, stack: !solo, end: !solo && way.set && !way.note ? _jsx(WayEnd, { way: way }) : undefined, children: control }));
127
+ }
128
+ /** With several ways, each set-up way's row ends in its status, Test and Remove. */
129
+ function WayEnd({ way }) {
130
+ const [testing, setTesting] = useState(false);
131
+ const remove = removeOf(way);
132
+ const t = way.test;
108
133
  const test = async () => {
109
134
  if (!way.onTest)
110
135
  return;
111
136
  setTesting(true);
112
- setError(null);
113
137
  try {
114
138
  await way.onTest();
115
139
  }
116
140
  catch (e) {
117
- setError(e);
141
+ lumenToast.error(errorText(e));
118
142
  }
119
143
  finally {
120
144
  setTesting(false);
121
145
  }
122
146
  };
123
- const close = () => setEditing(false);
124
- const fixLabel = way.signin ? 'Sign in again' : 'Replace';
125
- const addLabel = way.addLabel ??
126
- (way.signin
127
- ? way.signin.provider
128
- ? `Sign in with ${way.signin.provider}`
129
- : 'Sign in'
130
- : `Add ${lower(way.label)}`);
131
- const t = way.test;
132
- return (_jsxs("div", { className: cn('grid grid-cols-1 gap-2 rounded-md border px-3 py-2.5', inUse && 'border-primary/60 bg-primary/5'), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [pick && (_jsx(RadioGroupItem, { id: radioId, value: way.id, disabled: !way.set, "aria-label": `Use ${way.label}` })), Icon && _jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "min-w-0 flex-1 basis-40", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [pick ? (_jsx(Label, { htmlFor: radioId, className: "whitespace-nowrap font-medium", children: _jsx(InfoLabel, { info: way.info, children: way.label }) })) : (_jsx(InfoLabel, { className: "whitespace-nowrap font-medium text-sm", info: way.info, children: way.label })), inUse && (_jsx(Badge, { variant: "outline", className: "h-5 shrink-0 border-primary/40 text-primary", children: "In use" }))] }), _jsx("span", { className: "flex min-w-0 items-center gap-1 text-muted-foreground text-xs", children: way.set ? (_jsxs(_Fragment, { children: [way.ident && (_jsx("span", { className: "truncate", title: typeof way.ident === 'string' ? way.ident : undefined, children: way.ident })), way.ident && _jsx("span", { "aria-hidden": true, children: "\u00B7" }), t ? (_jsxs("span", { className: cn('flex shrink-0 items-center gap-1', t.ok ? 'text-gain' : 'text-loss'), title: `Tested ${clock(new Date(toMs(t.at)).toISOString())}${t.ok && t.detail ? ` · ${t.detail}` : ''}`, children: [t.ok ? (_jsx(CheckIcon, { className: "size-3" })) : (_jsx(AlertTriangleIcon, { className: "size-3" })), t.ok ? 'Works' : t.expired ? 'Expired' : 'Failed', " ", ago(toMs(t.at))] })) : (_jsx("span", { className: "shrink-0", children: "Not tested" }))] })) : (_jsx("span", { children: "Not set up" })) })] }), way.note ? (_jsx("span", { className: "ml-auto shrink-0 text-muted-foreground text-xs", children: way.note })) : way.set ? (_jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: [way.onTest && (_jsx(IconButton, { icon: RefreshCwIcon, label: testing ? 'Testing…' : `Test ${way.label}`, busy: testing, disabled: testing, onClick: () => void test() })), editable && way.onRemove && (_jsx(LumenConfirm, { title: way.removeTitle ?? `Remove ${lower(way.label)}?`, description: way.removeText ?? 'You can add it again any time.', action: way.removeLabel ?? 'Remove', onConfirm: () => way.onRemove?.(), problem: errorText, children: _jsx(IconButton, { icon: Trash2Icon, label: way.removeLabel ?? `Remove ${way.label}` }) })), editable && hasForm && !editing && (_jsxs(Button, { size: "sm", variant: failing ? 'default' : 'outline', onClick: () => setEditing(true), children: [way.signin ? _jsx(LogInIcon, {}) : _jsx(KeyRoundIcon, {}), fixLabel] }))] })) : (editable &&
133
- hasForm &&
134
- !editing && (_jsxs(Button, { size: "sm", className: "ml-auto shrink-0", onClick: () => setEditing(true), children: [way.signin ? _jsx(LogInIcon, {}) : _jsx(PlusIcon, {}), addLabel] })))] }), t && !t.ok && t.error && (_jsx("p", { role: "alert", className: "text-xs text-loss", children: t.error })), _jsx(ErrorLine, { error: error }), editing &&
135
- (way.form ? (way.form(close)) : way.signin ? (_jsx(SigninForm, { way: way, close: close })) : (_jsx(FieldsForm, { way: way, close: close })))] }));
147
+ return (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(ConnectionBadge, { status: connectionStatus(way), signin: !!way.signin, title: t ? (t.ok ? (t.detail ?? undefined) : (t.error ?? undefined)) : undefined }), way.onTest && (_jsx(IconButton, { icon: RefreshCwIcon, label: testing ? 'Testing…' : `Test ${way.label}`, busy: testing, disabled: testing, onClick: () => void test() })), remove && (_jsx(LumenConfirm, { title: remove.title, description: remove.description, action: remove.action, onConfirm: remove.run, problem: errorText, children: _jsx(IconButton, { icon: Trash2Icon, label: remove.label, className: "text-loss" }) }))] }));
136
148
  }
137
149
  /** Runs a save; false or a throw keeps the form open (a throw says why). */
138
150
  async function keep(run, setError) {
@@ -145,7 +157,11 @@ async function keep(run, setError) {
145
157
  return false;
146
158
  }
147
159
  }
148
- /** The way's fields, checked before Test and save. */
160
+ /** A form's footer: a link on the left, then Cancel and its Save. */
161
+ function FormFooter({ left, children }) {
162
+ return (_jsxs("div", { className: "mt-1 flex min-h-10 min-w-0 items-center gap-2 border-t pt-2 text-sm", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: left }), children] }));
163
+ }
164
+ /** The way's fields, a row each, checked before Test and save. */
149
165
  function FieldsForm({ way, close }) {
150
166
  const ids = useId();
151
167
  const fields = way.fields ?? [];
@@ -173,7 +189,7 @@ function FieldsForm({ way, close }) {
173
189
  close();
174
190
  setBusy(false);
175
191
  };
176
- return (_jsxs("div", { className: "grid grid-cols-1 gap-2 border-t pt-2", children: [fields.map((f) => (_jsxs("div", { className: "grid grid-cols-1 gap-1", children: [fields.length > 1 && (_jsx(Label, { htmlFor: `${ids}-${f.id}`, className: "text-xs", children: _jsx(InfoLabel, { info: f.info, children: f.label }) })), _jsx(Input, { id: `${ids}-${f.id}`, "aria-label": f.label, type: f.secret ? 'password' : 'text', autoComplete: "off", placeholder: f.placeholder ?? (f.prefix ? `${f.prefix}…` : f.label), className: "font-code text-xs", value: values[f.id] ?? '', onChange: (e) => setValues((s) => ({ ...s, [f.id]: e.target.value })), onKeyDown: (e) => e.key === 'Enter' && !empty && !issues.length && void save() })] }, f.id))), _jsx(ErrorLine, { error: error }), _jsxs("div", { className: "flex items-center justify-end gap-1", children: [way.getUrl && (_jsx(IconButton, { icon: ExternalLinkIcon, label: `Get ${/^[AEIOU]/i.test(way.label) ? 'an' : 'a'} ${lower(way.label)}`, href: way.getUrl })), _jsx(IconButton, { icon: XIcon, label: "Cancel", onClick: close }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: empty, label: "Test and save", onSave: () => void save() })] })] }));
192
+ return (_jsxs("div", { className: "flex flex-col", children: [fields.map((f) => (_jsx(SettingField, { label: fields.length > 1 ? f.label : way.label, info: fields.length > 1 ? f.info : (f.info ?? way.info), htmlFor: `${ids}-${f.id}`, children: _jsx(Input, { id: `${ids}-${f.id}`, type: f.secret ? 'password' : 'text', autoComplete: "off", placeholder: f.placeholder ?? (f.prefix ? `${f.prefix}…` : f.label), className: "h-8 font-code text-xs", value: values[f.id] ?? '', onChange: (e) => setValues((s) => ({ ...s, [f.id]: e.target.value })), onKeyDown: (e) => e.key === 'Enter' && !empty && !issues.length && void save() }) }, f.id))), _jsx(ErrorLine, { error: error }), _jsxs(FormFooter, { left: way.getUrl && (_jsxs("a", { className: "underline", href: way.getUrl, target: "_blank", rel: "noreferrer", children: ["Get ", /^[AEIOU]/i.test(way.label) ? 'an' : 'a', " ", lower(way.label), " \u2197"] })), children: [_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: close, children: "Cancel" }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: empty, label: "Test and save", onSave: () => void save() })] })] }));
177
193
  }
178
194
  /** Open the sign-in in a new tab, then paste back the address it ended on. */
179
195
  function SigninForm({ way, close }) {
@@ -222,8 +238,5 @@ function SigninForm({ way, close }) {
222
238
  close();
223
239
  setBusy(false);
224
240
  };
225
- return (_jsxs("ol", { className: "grid grid-cols-1 gap-3 border-t pt-2 text-sm", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx(Step, { n: 1 }), _jsx("span", { className: "flex-1", children: "Sign in in a new tab" }), _jsxs(Button, { size: "sm", variant: redirect === null ? 'default' : 'outline', onClick: () => void open(), children: [_jsx(ExternalLinkIcon, {}), redirect === null ? 'Open sign-in' : 'Open again'] })] }), _jsxs("li", { className: "grid grid-cols-1 gap-2", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(Step, { n: 2 }), _jsx(Label, { htmlFor: id, className: cn(redirect === null && 'text-muted-foreground'), children: _jsx(InfoLabel, { info: `The sign-in ends on ${redirect || signin.redirect || 'an address'} that does not load. That is expected: copy the whole address from that tab's address bar.`, children: "Paste the address it ended on" }) })] }), _jsx("span", { className: "flex gap-2 pl-7", children: _jsx(Input, { id: id, autoComplete: "off", disabled: redirect === null, placeholder: `${redirect || signin.redirect || 'http://localhost'}…`, className: "font-code text-xs", value: pasted, onChange: (e) => setPasted(e.target.value) }) })] }), _jsx(ErrorLine, { error: error }), _jsxs("li", { className: "flex items-center justify-end gap-1", children: [_jsx(IconButton, { icon: XIcon, label: "Cancel", onClick: close }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: !p, label: "Finish", onSave: () => void finish() })] })] }));
226
- }
227
- function Step({ n }) {
228
- return (_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center rounded-full bg-muted font-medium text-xs tabular-nums", children: n }));
241
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingField, { label: way.label, info: way.info, children: _jsxs(Button, { size: "sm", variant: redirect === null ? 'default' : 'outline', onClick: () => void open(), children: [_jsx(ExternalLinkIcon, {}), redirect === null ? 'Open sign-in' : 'Open again'] }) }), _jsx(SettingField, { label: "Address", htmlFor: id, stack: true, info: `The sign-in ends on ${redirect || signin.redirect || 'an address'} that does not load. That is expected: copy the whole address from that tab's address bar and paste it here.`, children: _jsx(Input, { id: id, autoComplete: "off", disabled: redirect === null, placeholder: `${redirect || signin.redirect || 'http://localhost'}…`, className: "h-8 font-code text-xs", value: pasted, onChange: (e) => setPasted(e.target.value) }) }), _jsx(ErrorLine, { error: error }), _jsxs(FormFooter, { children: [_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: close, children: "Cancel" }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: !p, label: "Finish", onSave: () => void finish() })] })] }));
229
242
  }
@@ -7,7 +7,8 @@ import { useState } from 'react';
7
7
  // common `mcp-remote` bridge on the person's own computer, whose sign-in returns to localhost.
8
8
  import { LumenSelector } from './lumen-selector.js';
9
9
  import { IconButton } from './page.js';
10
- import { SettingRow } from './settings.js';
10
+ import { SettingField, SettingValue } from './setting-parts.js';
11
+ import { SettingsFooter } from './settings.js';
11
12
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
12
13
  import { Tabs, TabsList, TabsTrigger } from './ui/tabs.js';
13
14
  const CLIENTS = {
@@ -52,14 +53,10 @@ function ClientSteps({ client, server }) {
52
53
  }
53
54
  }
54
55
  export function McpSettings({ servers, current }) {
55
- const { copied, copy } = useCopy();
56
56
  const [serverId, setServerId] = useState(current ?? servers[0]?.id);
57
57
  const [client, setClient] = useState('claude');
58
58
  const server = servers.find((d) => d.id === serverId) ?? servers[0];
59
59
  if (!server)
60
60
  return null;
61
- return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col", children: [_jsx("h4", { className: "text-xs font-medium text-muted-foreground", children: "Servers" }), servers.map((d) => {
62
- const url = d.url;
63
- return (_jsx(SettingRow, { label: d.label, description: "MCP address", children: _jsxs(InputGroup, { className: "w-56 sm:w-80", children: [_jsx(InputGroupInput, { readOnly: true, value: url, "aria-label": `${d.label} MCP address`, className: "font-mono text-xs", onFocus: (e) => e.currentTarget.select() }), _jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": `Copy the ${d.label} address`, onClick: () => copy(url), children: copied === url ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) }) })] }) }, d.id));
64
- })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h4", { className: "text-xs font-medium text-muted-foreground", children: "Connect" }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [servers.length > 1 && (_jsx(LumenSelector, { label: "Server", size: "sm", className: "w-40", options: servers.map((d) => ({ value: d.id, label: d.label })), value: server.id, onChange: setServerId })), _jsx(LumenSelector, { label: "App", size: "sm", className: "w-36 md:hidden", options: Object.entries(CLIENTS).map(([k, label]) => ({ value: k, label })), value: client, onChange: (v) => setClient(v) })] }), _jsx(Tabs, { value: client, onValueChange: (v) => setClient(v), className: "max-md:hidden", children: _jsx(TabsList, { children: Object.entries(CLIENTS).map(([k, label]) => (_jsx(TabsTrigger, { value: k, children: label }, k))) }) }), _jsx(ClientSteps, { client: client, server: server })] }), _jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [_jsx(ShieldIcon, { className: "mt-0.5 size-3.5 shrink-0" }), "Every client signs in as you through your browser; there are no API keys. Sign-ins return only to known apps and to http://localhost or http://127.0.0.1 on your own computer."] })] }));
61
+ return (_jsxs("div", { className: "flex flex-col", children: [servers.map((d) => (_jsx(SettingField, { label: d.label, stack: true, children: _jsx(SettingValue, { value: d.url, label: `${d.label} address`, mono: true, copy: true }) }, d.id))), servers.length > 1 && (_jsx(SettingField, { label: "Connect", children: _jsx(LumenSelector, { label: "Server", size: "sm", className: "w-40", options: servers.map((d) => ({ value: d.id, label: d.label })), value: server.id, onChange: setServerId }) })), _jsxs(SettingField, { label: "From", children: [_jsx(LumenSelector, { label: "App", size: "sm", className: "w-36 md:hidden", options: Object.entries(CLIENTS).map(([k, label]) => ({ value: k, label })), value: client, onChange: (v) => setClient(v) }), _jsx(Tabs, { value: client, onValueChange: (v) => setClient(v), className: "max-md:hidden", children: _jsx(TabsList, { children: Object.entries(CLIENTS).map(([k, label]) => (_jsx(TabsTrigger, { value: k, children: label }, k))) }) })] }), _jsx("div", { className: "pt-3", children: _jsx(ClientSteps, { client: client, server: server }) }), _jsx(SettingsFooter, { children: _jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [_jsx(ShieldIcon, { className: "mt-0.5 size-3.5 shrink-0" }), "Every client signs in as you through your browser; there are no API keys. Sign-ins return only to known apps and to http://localhost or http://127.0.0.1 on your own computer."] }) })] }));
65
62
  }
package/dist/schedules.js CHANGED
@@ -12,6 +12,7 @@ import { useIsMobile } from './hooks/use-mobile.js';
12
12
  import { LumenSelector } from './lumen-selector.js';
13
13
  import { IconButton } from './page.js';
14
14
  import { usePrefs, zoneAbbr } from './prefs.js';
15
+ import { SettingField } from './setting-parts.js';
15
16
  import { useShell } from './shell.js';
16
17
  import { Badge } from './ui/badge.js';
17
18
  import { Button } from './ui/button.js';
@@ -118,13 +119,14 @@ function OnSwitch({ s, onSave }) {
118
119
  void onSave(s.id, { on }).finally(() => setPending(false));
119
120
  } }));
120
121
  }
121
- function Row({ label, children }) {
122
- return (_jsxs("div", { className: "grid grid-cols-1 gap-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), children] }));
122
+ /** One setting in the editor: its label beside its control (`stack` puts it above on a phone). */
123
+ function Row({ label, stack, end, children, }) {
124
+ return (_jsx(SettingField, { label: label, stack: stack, end: end, children: children }));
123
125
  }
124
126
  /** At (a daily schedule), Days and Time zone: the schedule editor's and New brief's. */
125
127
  function TimeFields({ kind, draft, set, }) {
126
128
  const zones = !draft.tz || ZONES.includes(draft.tz) ? ZONES : [draft.tz, ...ZONES];
127
- return (_jsxs(_Fragment, { children: [kind === 'daily' ? (_jsx(Row, { label: "At", children: _jsx(Input, { type: "time", "aria-label": "At", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }) })) : null, _jsx(Row, { label: "Days", children: _jsx(ToggleGroup, { type: "multiple", variant: "outline", size: "sm", spacing: 1, className: "w-full", value: (draft.days ?? []).map(String), onValueChange: (v) => v.length && set({ days: v.map(Number) }), children: [1, 2, 3, 4, 5, 6, 0].map((d) => (_jsx(ToggleGroupItem, { value: String(d), className: "flex-1 px-0", children: DAYS[d] }, d))) }) }), _jsx(Row, { label: "Time zone", children: _jsx(LumenSelector, { label: "Time zone", className: "w-full", options: zones.map((z) => ({ value: z, label: z.replace('_', ' ') })), value: draft.tz, onChange: (tz) => set({ tz }) }) })] }));
129
+ return (_jsxs(_Fragment, { children: [kind === 'daily' ? (_jsx(Row, { label: "At", children: _jsx(Input, { type: "time", "aria-label": "At", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }) })) : null, _jsx(Row, { label: "Days", stack: true, children: _jsx(ToggleGroup, { type: "multiple", variant: "outline", size: "sm", spacing: 1, className: "w-full", value: (draft.days ?? []).map(String), onValueChange: (v) => v.length && set({ days: v.map(Number) }), children: [1, 2, 3, 4, 5, 6, 0].map((d) => (_jsx(ToggleGroupItem, { value: String(d), className: "flex-1 px-0", children: DAYS[d] }, d))) }) }), _jsx(Row, { label: "Time zone", children: _jsx(LumenSelector, { label: "Time zone", className: "w-full", options: zones.map((z) => ({ value: z, label: z.replace('_', ' ') })), value: draft.tz, onChange: (tz) => set({ tz }) }) })] }));
128
130
  }
129
131
  /** One schedule's settings, with Run now. */
130
132
  function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
@@ -182,7 +184,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
182
184
  setBusy(null);
183
185
  }
184
186
  };
185
- return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s, timeZone) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
187
+ return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s, timeZone) })] }), _jsxs("div", { className: "flex flex-col overflow-y-auto px-4", children: [_jsx("dl", { className: "mb-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
186
188
  ['Inspects', s.about.inspects],
187
189
  ['Processes', s.about.processes],
188
190
  ['Enriches', s.about.enriches],
@@ -190,7 +192,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
190
192
  ['Cost', s.about.cost],
191
193
  ]
192
194
  .filter(([, v]) => v)
193
- .map(([k, v]) => (_jsxs(Fragment, { children: [_jsx("dt", { className: "whitespace-nowrap text-muted-foreground", children: k }), _jsx("dd", { children: v })] }, k))) }), s.brief ? (_jsx(Row, { label: "Instructions", children: own ? (_jsx(Textarea, { "aria-label": "Instructions", rows: 5, maxLength: 4000, value: brief.instructions ?? '', onChange: (e) => setBrief((b) => ({ ...b, instructions: e.target.value })) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: s.brief.instructions })) })) : null, _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { htmlFor: "sched-on", children: "On" }), _jsx(Switch, { id: "sched-on", checked: draft.on, onCheckedChange: (on) => set({ on }) })] }), s.choices.length ? (_jsx(Row, { label: "Every", children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", value: String(draft.every), onValueChange: (v) => v && set({ every: Number(v) }), children: s.choices.map((m) => (_jsx(ToggleGroupItem, { value: String(m), className: "num", children: everyText(m) }, m))) }) })) : null, s.kind === 'window' ? (_jsx(_Fragment, { children: _jsx(Row, { label: "Window", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "time", "aria-label": "From", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }), _jsx("span", { className: "text-muted-foreground", children: "to" }), _jsx(Input, { type: "time", "aria-label": "To", className: "num w-32", value: draft.to, onChange: (e) => set({ to: e.target.value }) })] }) }) })) : null, s.kind === 'window' || s.kind === 'daily' ? (_jsx(TimeFields, { kind: s.kind, draft: draft, set: set })) : null, s.brief ? (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Spend cap a run", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "$" }), _jsx(Input, { type: "number", "aria-label": "Spend cap a run, USD", className: "num w-24", min: 0.05, max: 5, step: 0.05, value: brief.cap_usd ?? '', onChange: (e) => setBrief((b) => ({ ...b, cap_usd: Number(e.target.value) })) })] }) }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { htmlFor: "brief-notify", children: "Notify when ready" }), _jsx(Switch, { id: "brief-notify", checked: brief.notify ?? false, onCheckedChange: (notify) => setBrief((b) => ({ ...b, notify })) })] })] })) : null, s.waiting ? (_jsx("p", { className: "text-sm text-warning", children: s.waiting })) : s.next ? (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Next run ", time(s.next)] })) : null, recent && !ran ? (_jsx(Row, { label: "Last runs", children: recent(s.id) })) : (_jsx(Row, { label: "Last run", children: _jsx("span", { className: "text-sm", children: _jsx(Last, { s: ran ? { ...s, last: ran } : s }) }) })), last && assistant ? (_jsxs(Button, { variant: "outline", size: "sm", className: "justify-self-start", onClick: () => {
195
+ .map(([k, v]) => (_jsxs(Fragment, { children: [_jsx("dt", { className: "whitespace-nowrap text-muted-foreground", children: k }), _jsx("dd", { children: v })] }, k))) }), s.brief ? (_jsx(Row, { label: "Instructions", stack: true, children: own ? (_jsx(Textarea, { "aria-label": "Instructions", rows: 5, maxLength: 4000, value: brief.instructions ?? '', onChange: (e) => setBrief((b) => ({ ...b, instructions: e.target.value })) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: s.brief.instructions })) })) : null, _jsx(Row, { label: "On", end: _jsx(Switch, { "aria-label": `${s.name} on`, checked: draft.on, onCheckedChange: (on) => set({ on }) }) }), s.choices.length ? (_jsx(Row, { label: "Every", stack: true, children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", value: String(draft.every), onValueChange: (v) => v && set({ every: Number(v) }), children: s.choices.map((m) => (_jsx(ToggleGroupItem, { value: String(m), className: "num", children: everyText(m) }, m))) }) })) : null, s.kind === 'window' ? (_jsx(_Fragment, { children: _jsx(Row, { label: "Window", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "time", "aria-label": "From", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }), _jsx("span", { className: "text-muted-foreground", children: "to" }), _jsx(Input, { type: "time", "aria-label": "To", className: "num w-32", value: draft.to, onChange: (e) => set({ to: e.target.value }) })] }) }) })) : null, s.kind === 'window' || s.kind === 'daily' ? (_jsx(TimeFields, { kind: s.kind, draft: draft, set: set })) : null, s.brief ? (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Cap a run", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "$" }), _jsx(Input, { type: "number", "aria-label": "Spend cap a run, USD", className: "num w-24", min: 0.05, max: 5, step: 0.05, value: brief.cap_usd ?? '', onChange: (e) => setBrief((b) => ({ ...b, cap_usd: Number(e.target.value) })) })] }) }), _jsx(Row, { label: "Notify", end: _jsx(Switch, { "aria-label": "Notify when ready", checked: brief.notify ?? false, onCheckedChange: (notify) => setBrief((b) => ({ ...b, notify })) }) })] })) : null, s.waiting ? (_jsx("p", { className: "py-2 text-sm text-warning", children: s.waiting })) : s.next ? (_jsx(Row, { label: "Next run", children: _jsx("span", { className: "truncate text-sm", children: time(s.next) }) })) : null, recent && !ran ? (_jsx(Row, { label: "Last runs", stack: true, children: recent(s.id) })) : (_jsx(Row, { label: "Last run", children: _jsx("span", { className: "text-sm", children: _jsx(Last, { s: ran ? { ...s, last: ran } : s }) }) })), last && assistant ? (_jsxs(Button, { variant: "outline", size: "sm", className: "mt-2 self-start", onClick: () => {
194
196
  assistant.loadChat(last.chat);
195
197
  assistant.setOpen(true);
196
198
  onClose();
@@ -258,5 +260,5 @@ function NewBrief({ create, onDone, }) {
258
260
  setBusy(false);
259
261
  }
260
262
  };
261
- return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "New brief" }), _jsxs(SheetDescription, { children: [assistantName, " works it out on a schedule, reads only, and sends it when it is ready."] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx(Row, { label: "Name", children: _jsx(Input, { "aria-label": "Name", maxLength: 60, placeholder: "Options brief", value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(Row, { label: "Instructions", children: _jsx(Textarea, { "aria-label": "Instructions", rows: 6, maxLength: 4000, placeholder: `What ${assistantName} should look at and report, e.g. my short options expiring this week with their delta and P&L.`, value: instructions, onChange: (e) => setInstructions(e.target.value) }) }), _jsx(TimeFields, { kind: "daily", draft: time, set: (c) => setTime((t) => ({ ...t, ...c })) }), error ? (_jsx("p", { role: "alert", className: "text-xs text-loss", children: error })) : null] }), _jsx(SheetFooter, { className: "flex-row justify-end", children: _jsxs(Button, { size: "sm", disabled: busy || !name.trim() || !instructions.trim(), onClick: () => void add(), children: [_jsx(PlusIcon, {}), "Add brief"] }) })] }));
263
+ return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "New brief" }), _jsxs(SheetDescription, { children: [assistantName, " works it out on a schedule, reads only, and sends it when it is ready."] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx(Row, { label: "Name", children: _jsx(Input, { "aria-label": "Name", maxLength: 60, placeholder: "Options brief", value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(Row, { label: "Instructions", stack: true, children: _jsx(Textarea, { "aria-label": "Instructions", rows: 6, maxLength: 4000, placeholder: `What ${assistantName} should look at and report, e.g. my short options expiring this week with their delta and P&L.`, value: instructions, onChange: (e) => setInstructions(e.target.value) }) }), _jsx(TimeFields, { kind: "daily", draft: time, set: (c) => setTime((t) => ({ ...t, ...c })) }), error ? (_jsx("p", { role: "alert", className: "text-xs text-loss", children: error })) : null] }), _jsx(SheetFooter, { className: "flex-row justify-end", children: _jsxs(Button, { size: "sm", disabled: busy || !name.trim() || !instructions.trim(), onClick: () => void add(), children: [_jsx(PlusIcon, {}), "Add brief"] }) })] }));
262
264
  }
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from 'react';
1
+ import { type ComponentType, type ReactNode } from 'react';
2
2
  import { type KeyTestResult } from './integrations.js';
3
3
  /** One setting inside an item: its label, then its control group, then an optional switch at the end. */
4
4
  export declare function SettingField({ label, info, htmlFor, end, stack, className, children, }: {
@@ -16,7 +16,7 @@ export declare function SettingField({ label, info, htmlFor, end, stack, classNa
16
16
  children?: ReactNode;
17
17
  }): import("react").JSX.Element;
18
18
  /** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
19
- export declare function SettingValue({ value, label, mono, copy, onReplace, replaceLabel, }: {
19
+ export declare function SettingValue({ value, label, mono, copy, onReplace, replaceLabel, replaceIcon: ReplaceIcon, }: {
20
20
  value: string;
21
21
  /** The value's name for Copy's and Replace's accessible names. */
22
22
  label: string;
@@ -24,6 +24,10 @@ export declare function SettingValue({ value, label, mono, copy, onReplace, repl
24
24
  copy?: boolean;
25
25
  onReplace?: () => void;
26
26
  replaceLabel?: string;
27
+ /** Replace's icon: a pencil, or a sign-in for `Sign in again`. */
28
+ replaceIcon?: ComponentType<{
29
+ className?: string;
30
+ }>;
27
31
  }): import("react").JSX.Element;
28
32
  /** The five words a thing's status takes: Works, Failed, Not tested, Off, or a count. */
29
33
  export type SettingHealth = 'works' | 'failed' | 'untested' | 'off';
@@ -18,10 +18,10 @@ import { InputGroup, InputGroupAddon, InputGroupButton } from './ui/input-group.
18
18
  import { Label } from './ui/label.js';
19
19
  /** One setting inside an item: its label, then its control group, then an optional switch at the end. */
20
20
  export function SettingField({ label, info, htmlFor, end, stack = false, className, children, }) {
21
- 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-b border-border/60 py-1.5 last:border-b-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'), 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 })] }));
21
+ 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'), 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 })] }));
22
22
  }
23
23
  /** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
24
- export function SettingValue({ value, label, mono = false, copy = false, onReplace, replaceLabel = 'Replace', }) {
24
+ export function SettingValue({ value, label, mono = false, copy = false, onReplace, replaceLabel = 'Replace', replaceIcon: ReplaceIcon = PencilIcon, }) {
25
25
  const [copied, setCopied] = useState(false);
26
26
  const doCopy = async () => {
27
27
  try {
@@ -33,7 +33,7 @@ export function SettingValue({ value, label, mono = false, copy = false, onRepla
33
33
  setCopied(false);
34
34
  }
35
35
  };
36
- return (_jsxs(InputGroup, { className: "h-8 min-w-0", children: [_jsx("span", { className: cn('min-w-0 flex-1 truncate px-2.5 text-sm', mono && 'font-code text-xs'), title: value, children: value }), (copy || onReplace) && (_jsxs(InputGroupAddon, { align: "inline-end", children: [copy && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": copied ? 'Copied' : `Copy ${label.toLowerCase()}`, title: copied ? 'Copied' : `Copy ${label.toLowerCase()}`, onClick: () => void doCopy(), children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) })), onReplace && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": `${replaceLabel} ${label.toLowerCase()}`, title: `${replaceLabel} ${label.toLowerCase()}`, onClick: onReplace, children: _jsx(PencilIcon, {}) }))] }))] }));
36
+ return (_jsxs(InputGroup, { className: "h-8 min-w-0", children: [_jsx("span", { className: cn('min-w-0 flex-1 truncate px-2.5 text-sm', mono && 'font-code text-xs'), title: value, children: value }), (copy || onReplace) && (_jsxs(InputGroupAddon, { align: "inline-end", children: [copy && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": copied ? 'Copied' : `Copy ${label.toLowerCase()}`, title: copied ? 'Copied' : `Copy ${label.toLowerCase()}`, onClick: () => void doCopy(), children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) })), onReplace && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": `${replaceLabel} ${label.toLowerCase()}`, title: `${replaceLabel} ${label.toLowerCase()}`, onClick: onReplace, children: _jsx(ReplaceIcon, {}) }))] }))] }));
37
37
  }
38
38
  /** Where a thing stands from its switch and last test. */
39
39
  export function settingHealth(on, test) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.188",
3
+ "version": "0.1.189",
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.188",
52
+ "@columenlabs/lumen-ui-types": "0.1.189",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",