@columenlabs/lumen-ui 0.1.86 → 0.1.88

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.
@@ -302,7 +302,7 @@ function Draft({ draft, onSettle, busy, row, }) {
302
302
  const [open, setOpen] = useState(!row && draft.status === 'open');
303
303
  const lines = argLines(draft.args);
304
304
  const actions = draft.status === 'open' && (_jsxs("div", { className: "flex shrink-0 justify-end gap-1", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: !!pending || busy, onClick: () => void go(draft, 'dismiss'), children: "Dismiss" }), _jsxs(Button, { size: "sm", disabled: !!pending || busy, onClick: () => void go(draft, 'save'), children: [pending === 'save' ? _jsx(Spinner, {}) : _jsx(CheckIcon, {}), "Save"] })] }));
305
- return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "assistant-draft", className: cn('flex flex-col text-sm', row ? 'border-b px-1 py-1.5 last:border-b-0' : 'gap-2 rounded-lg border bg-card p-3'), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": `${open ? 'Hide' : 'Show'} ${draft.title}`, className: "flex min-w-0 flex-1 items-center gap-1.5 text-left", children: [_jsx(ChevronRightIcon, { className: cn('size-3.5 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90') }), _jsx("span", { className: "shrink-0 font-medium", children: draft.title }), !open && (_jsx("span", { className: "min-w-0 truncate text-muted-foreground text-xs", children: argSummary(draft.args) }))] }) }), _jsx(DraftStatus, { draft: draft }), row && draft.status === 'open' && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: XIcon, label: `Dismiss ${draft.title}`, size: "icon-xs", disabled: !!pending || busy, onClick: () => void go(draft, 'dismiss') }), _jsx(IconButton, { icon: pending === 'save' ? Spinner : CheckIcon, label: `Save ${draft.title}`, size: "icon-xs", variant: "outline", disabled: !!pending || busy, onClick: () => void go(draft, 'save') })] }))] }), _jsxs(CollapsibleContent, { className: cn('flex flex-col gap-2', row && 'pt-1.5 pl-5'), children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs", children: lines.slice(0, 12).map(([k, v]) => (_jsxs("div", { className: "contents", children: [_jsx("dt", { className: "text-muted-foreground", children: k }), _jsx("dd", { className: "font-code break-words", children: v })] }, k))) }), !row && actions] }), draft.error && _jsx("p", { className: "text-loss text-xs", children: draft.error })] }));
305
+ return (_jsxs(Collapsible, { open: open, onOpenChange: setOpen, "data-slot": "assistant-draft", className: cn('flex flex-col text-sm', row ? 'border-b px-1 py-1.5 last:border-b-0' : 'gap-2 rounded-lg border bg-card p-3'), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": `${open ? 'Hide' : 'Show'} ${draft.title}`, className: "flex min-w-0 flex-1 items-center gap-1.5 text-left", children: [_jsx(ChevronRightIcon, { className: cn('size-3.5 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90') }), _jsx("span", { className: "shrink-0 font-medium", children: draft.title }), !open && (_jsx("span", { className: "min-w-0 truncate text-muted-foreground text-xs", children: argSummary(draft.args) }))] }) }), _jsx(DraftStatus, { draft: draft }), row && draft.status === 'open' && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: XIcon, label: `Dismiss ${draft.title}`, size: "icon-xs", disabled: !!pending || busy, onClick: () => void go(draft, 'dismiss') }), _jsx(IconButton, { icon: pending === 'save' ? Spinner : CheckIcon, label: `Save ${draft.title}`, size: "icon-xs", variant: "outline", disabled: !!pending || busy, onClick: () => void go(draft, 'save') })] }))] }), _jsxs(CollapsibleContent, { className: cn('flex flex-col gap-2', row && 'pt-1.5 pl-5'), children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs", children: lines.slice(0, 12).map(([k, v]) => (_jsxs("div", { className: "contents", children: [_jsx("dt", { className: "whitespace-nowrap text-muted-foreground", children: k }), _jsx("dd", { className: "font-code break-words", children: v })] }, k))) }), !row && actions] }), draft.error && _jsx("p", { className: "text-loss text-xs", children: draft.error })] }));
306
306
  }
307
307
  /** Two or more cards in a row: one stack with a count, Save all and Dismiss all, a line each. */
308
308
  function DraftStack({ drafts, onSettle, busy, }) {
@@ -29,5 +29,5 @@ export function LumenExpandRow({ id, name, meta, badge, open: openProp, onOpenCh
29
29
  else
30
30
  setOwn(o);
31
31
  };
32
- return (_jsxs(Collapsible, { open: open, onOpenChange: change, className: "border-b last:border-b-0", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex w-full items-center gap-3 px-1 py-3 text-left hover:bg-muted/50", children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block font-medium", children: name }), meta && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), badge, _jsx(ChevronRightIcon, { className: cn('size-4 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90') })] }) }), _jsx(CollapsibleContent, { className: "grid grid-cols-1 gap-5 px-1 pt-1 pb-5", children: children })] }));
32
+ return (_jsxs(Collapsible, { open: open, onOpenChange: change, className: "border-b last:border-b-0", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex w-full items-center gap-3 px-1 py-3 text-left hover:bg-muted/50", children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate font-medium", title: typeof name === 'string' ? name : undefined, children: name }), meta && (_jsx("span", { className: "block truncate text-xs text-muted-foreground", title: typeof meta === 'string' ? meta : undefined, children: meta }))] }), badge, _jsx(ChevronRightIcon, { className: cn('size-4 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90') })] }) }), _jsx(CollapsibleContent, { className: "grid grid-cols-1 gap-5 px-1 pt-1 pb-5", children: children })] }));
33
33
  }
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface Fact {
3
+ label: string;
4
+ value: ReactNode;
5
+ /** The full value on hover, when the value is not plain text. */
6
+ title?: string;
7
+ /** A sentence that may take more lines ("What it inspects"); short values stay on one. */
8
+ wrap?: boolean;
9
+ }
10
+ export interface FactListProps {
11
+ facts: readonly (Fact | false | null | undefined)[];
12
+ className?: string;
13
+ }
14
+ /**
15
+ * Label and value pairs in a drawer or card ("Last seen today 07:18 PM"). The labels take the
16
+ * width of the longest one and the values the rest. Each sits on one line: a value that runs out
17
+ * of room ends in an ellipsis, the whole of it on hover, so a time or an email never breaks.
18
+ */
19
+ export declare function FactList({ facts, className }: FactListProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ import { cn } from './lib/utils.js';
4
+ /**
5
+ * Label and value pairs in a drawer or card ("Last seen today 07:18 PM"). The labels take the
6
+ * width of the longest one and the values the rest. Each sits on one line: a value that runs out
7
+ * of room ends in an ellipsis, the whole of it on hover, so a time or an email never breaks.
8
+ */
9
+ export function FactList({ facts, className }) {
10
+ return (_jsx("dl", { className: cn('grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-sm', className), children: facts.map((f) => f && (_jsxs(Fragment, { children: [_jsx("dt", { className: "whitespace-nowrap text-muted-foreground", children: f.label }), _jsx("dd", { className: cn('min-w-0', f.wrap ? 'break-words' : 'truncate'), title: f.wrap
11
+ ? undefined
12
+ : (f.title ?? (typeof f.value === 'string' ? f.value : undefined)), children: f.value })] }, f.label))) }));
13
+ }
package/dist/index.d.ts CHANGED
@@ -51,6 +51,7 @@ export { KIT, type KitConfig, configureLumen } from './kit-config.js';
51
51
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, type KeyTestResult, TestBadge, errorText, useClock, } from './integrations.js';
52
52
  export { LumenChatgptSignin, type LumenChatgptSigninProps } from './chatgpt-signin.js';
53
53
  export { LumenExpandList, LumenExpandRow } from './expand-list.js';
54
+ export { type Fact, FactList, type FactListProps } from './fact-list.js';
54
55
  export { LumenPager } from './pager.js';
55
56
  export { createDeskQueryClient } from './query-client.js';
56
57
  export { type ClientPoint, type ClientPointKind, type LumenTelemetryOptions, apiRoute, reportClient, reportClientError, reportFailedFetch, stackFingerprint, useLumenTelemetry, } from './telemetry.js';
package/dist/index.js CHANGED
@@ -58,6 +58,7 @@ export { KIT, configureLumen } from './kit-config.js';
58
58
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, TestBadge, errorText, useClock, } from './integrations.js';
59
59
  export { LumenChatgptSignin } from './chatgpt-signin.js';
60
60
  export { LumenExpandList, LumenExpandRow } from './expand-list.js';
61
+ export { FactList } from './fact-list.js';
61
62
  export { LumenPager } from './pager.js';
62
63
  export { createDeskQueryClient } from './query-client.js';
63
64
  export { apiRoute, reportClient, reportClientError, reportFailedFetch, stackFingerprint, useLumenTelemetry, } from './telemetry.js';
@@ -16,7 +16,7 @@ import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTi
16
16
  */
17
17
  export function LumenDrawer({ open, onClose, title, description, children, footer, footerStart, defaultSize = '32%', minSize = '320px', maxSize = '60%', }) {
18
18
  const isMobile = useIsMobile();
19
- const head = (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx(SheetTitle, { className: "truncate", children: title }), _jsx(SheetDescription, { className: description ? 'truncate text-muted-foreground text-xs' : 'sr-only', children: description ?? 'Details' })] }));
19
+ const head = (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx(SheetTitle, { className: "truncate", title: typeof title === 'string' ? title : undefined, children: title }), _jsx(SheetDescription, { className: description ? 'truncate text-muted-foreground text-xs' : 'sr-only', title: typeof description === 'string' ? description : undefined, children: description ?? 'Details' })] }));
20
20
  const body = _jsx("div", { className: "flex flex-col px-4 pb-4", children: children });
21
21
  // Every drawer ends on a footer, styled like a dialog's: the body scrolls above it and always
22
22
  // ends on a clear edge. Without actions of its own it offers Close, except on a phone, where the
@@ -6,6 +6,7 @@ import { AppWindowIcon, BanIcon, HistoryIcon, KeyRoundIcon, MailIcon, ShieldChec
6
6
  import { useState } from 'react';
7
7
  import { LumenConfirm } from '../confirm.js';
8
8
  import { DrawerSection } from '../drawer-section.js';
9
+ import { FactList } from '../fact-list.js';
9
10
  import { InfoLabel, InfoTip } from '../info-tip.js';
10
11
  import { errorText } from '../integrations.js';
11
12
  import { LumenDrawer } from '../lumen-drawer.js';
@@ -35,9 +36,20 @@ export function PersonDrawer({ adapter, view, person, onClose, }) {
35
36
  void act(() => adapter.setAccess(p.id, next), `${scope.name}: ${levelLabel(scope, level)} for ${who}`, () => adapter.setAccess(p.id, before));
36
37
  };
37
38
  return (_jsx(LumenDrawer, { open: !!p, onClose: onClose, title: who, description: p ? `${p.email} · ${STATUS_LABEL[p.status]} · invited ${when(p.invitedAt)}` : undefined, footer: p && pending ? (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => act(() => adapter.revoke(p.id), `Revoked the invite for ${p.email}`).then((r) => r && onClose()), children: [_jsx(XIcon, {}), " Revoke"] }), _jsxs(Button, { size: "sm", onClick: () => act(() => adapter.resend(p.id), `New invite sent to ${p.email}`), children: [_jsx(MailIcon, {}), " Resend invite"] })] })) : undefined, children: p && (_jsxs(_Fragment, { children: [_jsx(AccessSection, { adapter: adapter, view: view, person: p, onLevel: setLevel }), _jsxs(DrawerSection, { icon: ShieldCheckIcon, title: "Role", defaultOpen: true, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 py-1 text-sm", children: [_jsx(InfoLabel, { info: "Admins hold the top level on every scope and manage People. One active admin is always kept.", children: "Role" }), _jsx(LumenSelector, { trigger: "chip", size: "sm", label: `Role of ${who}`, disabled: !!guard && p.role === 'admin', value: p.role, options: ROLES, onChange: (role) => role !== p.role &&
38
- void act(() => adapter.update(p.id, { role }), `${who} is now ${role === 'admin' ? 'an admin' : 'a member'}`, () => adapter.update(p.id, { role: p.role })) })] }), guard && p.role === 'admin' && _jsx(NoteLine, { icon: ShieldCheckIcon, title: guard })] }), _jsx(DrawerSection, { icon: KeyRoundIcon, title: "Sign-in", defaultOpen: true, children: _jsxs("dl", { className: "grid grid-cols-2 gap-x-3 gap-y-1.5 text-sm", children: [_jsx("dt", { className: "text-muted-foreground", children: "Last seen" }), _jsx("dd", { children: when(p.lastSeen) }), _jsx("dt", { className: "text-muted-foreground", children: "Invited by" }), _jsx("dd", { className: "truncate", children: p.invitedBy ?? '—' }), pending && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-muted-foreground", children: "Invite" }), _jsx("dd", { children: p.status === 'expired'
39
- ? `expired ${when(p.expiresAt)}`
40
- : `expires ${fromNow(p.expiresAt)}` })] })), view.sync && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-muted-foreground", children: "Sign-in provider" }), _jsxs("dd", { children: [view.sync.ok ? 'Synced' : 'Out of step', " \u00B7 ", when(view.sync.at)] })] }))] }) }), adapter.audit && _jsx(ActivitySection, { adapter: adapter, person: p }), _jsx(DangerSection, { adapter: adapter, person: p, guard: guard, onGone: onClose })] })) }));
39
+ void act(() => adapter.update(p.id, { role }), `${who} is now ${role === 'admin' ? 'an admin' : 'a member'}`, () => adapter.update(p.id, { role: p.role })) })] }), guard && p.role === 'admin' && _jsx(NoteLine, { icon: ShieldCheckIcon, title: guard })] }), _jsx(DrawerSection, { icon: KeyRoundIcon, title: "Sign-in", defaultOpen: true, children: _jsx(FactList, { facts: [
40
+ { label: 'Last seen', value: when(p.lastSeen) },
41
+ { label: 'Invited by', value: p.invitedBy ?? '—' },
42
+ pending && {
43
+ label: 'Invite',
44
+ value: p.status === 'expired'
45
+ ? `expired ${when(p.expiresAt)}`
46
+ : `expires ${fromNow(p.expiresAt)}`,
47
+ },
48
+ view.sync && {
49
+ label: 'Sign-in provider',
50
+ value: `${view.sync.ok ? 'Synced' : 'Out of step'} · ${when(view.sync.at)}`,
51
+ },
52
+ ] }) }), adapter.audit && _jsx(ActivitySection, { adapter: adapter, person: p }), _jsx(DangerSection, { adapter: adapter, person: p, guard: guard, onGone: onClose })] })) }));
41
53
  }
42
54
  /** Each app's level, then the person's grants on constructs inside it. */
43
55
  function AccessSection({ adapter, view, person: p, onLevel, }) {
@@ -73,7 +85,7 @@ function ActivitySection({ adapter, person }) {
73
85
  queryFn: () => adapter.audit?.({ target: person.id, limit: 5 }) ?? Promise.resolve(null),
74
86
  });
75
87
  const events = q.data?.events ?? [];
76
- return (_jsx(DrawerSection, { icon: HistoryIcon, title: "Recent activity", count: events.length || null, children: q.isError ? (_jsx("p", { className: "text-destructive text-sm", children: errorText(q.error) })) : events.length ? (_jsx("ul", { className: "flex flex-col gap-1.5 text-sm", children: events.map((e) => (_jsxs("li", { className: "flex gap-3", children: [_jsx("span", { className: "num w-24 shrink-0 text-muted-foreground", children: when(e.at) }), _jsxs("span", { className: "min-w-0 flex-1 truncate", title: e.detail, children: [e.actor, " \u00B7 ", e.detail] }), e.refused && _jsx(Badge, { variant: "destructive", children: "Refused" })] }, e.id))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: q.isPending ? 'Loading…' : 'Nothing yet.' })) }));
88
+ return (_jsx(DrawerSection, { icon: HistoryIcon, title: "Recent activity", count: events.length || null, children: q.isError ? (_jsx("p", { className: "text-destructive text-sm", children: errorText(q.error) })) : events.length ? (_jsx("ul", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1.5 text-sm", children: events.map((e) => (_jsxs("li", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [_jsx("time", { dateTime: new Date(e.at).toISOString(), className: "num text-muted-foreground", children: when(e.at) }), _jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsxs("span", { className: "truncate", title: `${e.actor} · ${e.detail}`, children: [e.actor, " \u00B7 ", e.detail] }), e.refused && _jsx(Badge, { variant: "destructive", children: "Refused" })] })] }, e.id))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: q.isPending ? 'Loading…' : 'Nothing yet.' })) }));
77
89
  }
78
90
  /** Disable or Enable, and Remove with what each part of the app does with their data first. */
79
91
  function DangerSection({ adapter, person: p, guard, onGone, }) {
@@ -212,7 +212,7 @@ export function PermissionsSection({ frame }) {
212
212
  title: `Levels for ${scope.name}`,
213
213
  icon: ListOrderedIcon,
214
214
  link: false,
215
- children: (_jsxs("ol", { className: "flex flex-col gap-2 text-sm", children: [levels.map((l, i) => (_jsxs("li", { className: "flex gap-3", children: [_jsx("span", { className: "num w-4 shrink-0 text-muted-foreground", children: i + 1 }), _jsx("span", { className: "w-20 shrink-0 font-medium", children: l.label }), _jsx("span", { className: "min-w-0 flex-1 text-muted-foreground", children: l.description ?? '' })] }, l.id))), parent && (_jsxs("li", { className: "pt-1 text-muted-foreground text-xs", children: ["No grant of its own: the level on ", parent.name, ", by rank."] }))] })),
215
+ children: (_jsxs("ol", { className: "grid grid-cols-[auto_auto_minmax(0,1fr)] gap-x-3 gap-y-2 text-sm", children: [levels.map((l, i) => (_jsxs("li", { className: "col-span-3 grid grid-cols-subgrid", children: [_jsx("span", { className: "num text-muted-foreground", children: i + 1 }), _jsx("span", { className: "whitespace-nowrap font-medium", children: l.label }), _jsx("span", { className: "text-muted-foreground", children: l.description ?? '' })] }, l.id))), parent && (_jsxs("li", { className: "col-span-3 pt-1 text-muted-foreground text-xs", children: ["No grant of its own: the level on ", parent.name, ", by rank."] }))] })),
216
216
  },
217
217
  {
218
218
  id: 'roles',
@@ -220,7 +220,7 @@ export function PermissionsSection({ frame }) {
220
220
  title: 'Roles',
221
221
  icon: ShieldCheckIcon,
222
222
  link: false,
223
- children: (_jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm", children: [_jsx("dt", { className: "font-medium", children: "Admin" }), _jsx("dd", { className: "text-muted-foreground", children: "The top level on every scope; manages People. One is always kept." }), _jsx("dt", { className: "font-medium", children: "Member" }), _jsx("dd", { className: "text-muted-foreground", children: "Only the levels set per scope. The default for invites." })] })),
223
+ children: (_jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm", children: [_jsx("dt", { className: "whitespace-nowrap font-medium", children: "Admin" }), _jsx("dd", { className: "text-muted-foreground", children: "The top level on every scope; manages People. One is always kept." }), _jsx("dt", { className: "whitespace-nowrap font-medium", children: "Member" }), _jsx("dd", { className: "text-muted-foreground", children: "Only the levels set per scope. The default for invites." })] })),
224
224
  },
225
225
  ] }));
226
226
  }
@@ -22,6 +22,8 @@ export const statusTone = (p) => p.status === 'active'
22
22
  ? 'amber'
23
23
  : 'muted';
24
24
  const DAY = 86_400_000;
25
+ /** Non-breaking spaces, so `today 07:18 PM` and `in 7 d` never split across lines in any text. */
26
+ const glue = (s) => s.replace(/ /g, '\u00a0');
25
27
  const sameDay = (a, b) => a.toDateString() === b.toDateString();
26
28
  /** `today 09:12`, `yesterday 18:42`, `24 Oct`, `24 Oct 2025`; a dash for never. */
27
29
  export function when(ts, now = Date.now()) {
@@ -31,14 +33,14 @@ export function when(ts, now = Date.now()) {
31
33
  const n = new Date(now);
32
34
  const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
33
35
  if (sameDay(d, n))
34
- return `today ${time}`;
36
+ return glue(`today ${time}`);
35
37
  if (sameDay(d, new Date(now - DAY)))
36
- return `yesterday ${time}`;
37
- return d.toLocaleDateString(undefined, {
38
+ return glue(`yesterday ${time}`);
39
+ return glue(d.toLocaleDateString(undefined, {
38
40
  day: 'numeric',
39
41
  month: 'short',
40
42
  ...(d.getFullYear() === n.getFullYear() ? {} : { year: 'numeric' }),
41
- });
43
+ }));
42
44
  }
43
45
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
44
46
  export function fromNow(ts, now = Date.now()) {
@@ -51,7 +53,7 @@ export function fromNow(ts, now = Date.now()) {
51
53
  : abs >= 3_600_000
52
54
  ? `${Math.round(abs / 3_600_000)} h`
53
55
  : `${Math.max(1, Math.round(abs / 60_000))} min`;
54
- return ms >= 0 ? `in ${n}` : `${n} ago`;
56
+ return glue(ms >= 0 ? `in ${n}` : `${n} ago`);
55
57
  }
56
58
  /** The top scopes (apps), in order; constructs inside one have a `parent`. */
57
59
  export const topScopes = (scopes) => scopes.filter((s) => !s.parent);
package/dist/schedules.js CHANGED
@@ -149,7 +149,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
149
149
  ['Cost', s.about.cost],
150
150
  ]
151
151
  .filter(([, v]) => v)
152
- .map(([k, v]) => (_jsxs(Fragment, { children: [_jsx("dt", { className: "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: () => {
152
+ .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: () => {
153
153
  assistant.loadChat(last.chat);
154
154
  assistant.setOpen(true);
155
155
  onClose();
package/dist/theme.css CHANGED
@@ -457,6 +457,11 @@
457
457
  body {
458
458
  @apply bg-background text-foreground antialiased;
459
459
  }
460
+ /* A fieldset keeps its content's min-content width by default, so a form in a drawer or
461
+ dialog pushed its fields past the edge; it fits its box like any block. */
462
+ fieldset {
463
+ min-inline-size: 0;
464
+ }
460
465
  html {
461
466
  @apply font-sans;
462
467
  }
@@ -465,6 +470,16 @@
465
470
  font-family: var(--num-font);
466
471
  font-variant-numeric: tabular-nums;
467
472
  }
473
+
474
+ /* Short values never wrap: a number, a time, a key. `07:18 PM` stays on one line; where room
475
+ runs out the holder truncates it (`truncate` and a `title`), never breaks it. A `.num` that
476
+ clamps on purpose (`line-clamp-*`) keeps its lines. */
477
+ span.num:not([class*="line-clamp"]),
478
+ dd.num,
479
+ time,
480
+ kbd {
481
+ white-space: nowrap;
482
+ }
468
483
  }
469
484
 
470
485
  /* Font and size: the size is the root every rem scales from. Unlayered, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
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.86",
52
+ "@columenlabs/lumen-ui-types": "0.1.88",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",