@columenlabs/lumen-ui 0.1.151 → 0.1.153

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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { HashIcon, TypeIcon } from 'lucide-react';
2
+ import { HashIcon, PlusIcon, TypeIcon } from 'lucide-react';
3
3
  import { useEffect, useState } from 'react';
4
4
  import { useAssistantName } from './assistant-context.js';
5
5
  import { SaveButton } from './form-issues.js';
@@ -26,7 +26,7 @@ export function AddFormulaDialog({ open, onOpenChange, domain, formulas, onAdd,
26
26
  }, [open]);
27
27
  const issue = formulaIssues([...formulas, draft], domain)[formulas.length] ?? null;
28
28
  const readable = formulaOptions(domain, formulas);
29
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex max-h-[calc(100svh-2rem)] flex-col gap-0 [--dialog-pad:0px] sm:max-w-2xl max-md:h-svh max-md:max-h-svh max-md:max-w-full max-md:rounded-none", children: [_jsxs(DialogHeader, { className: "border-b px-4 py-3", children: [_jsx(DialogTitle, { className: "text-sm", children: "Add a formula column" }), _jsx(DialogDescription, { children: "A column computed from the other columns, like weight or upside. Saved with the view." })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(FormulaForm, { formula: draft, issue: issue, fields: readable, onChange: (p) => setDraft({ ...draft, ...p }) }) }), _jsxs(DialogFooter, { className: "border-t px-4 py-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsx(SaveButton, { label: "Add column", icon: null, issues: issue
29
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex max-h-[calc(100svh-2rem)] flex-col gap-0 [--dialog-pad:0px] sm:max-w-2xl max-md:h-svh max-md:max-h-svh max-md:max-w-full max-md:rounded-none", children: [_jsxs(DialogHeader, { className: "border-b px-4 py-3", children: [_jsx(DialogTitle, { className: "text-sm", children: "Add a formula column" }), _jsx(DialogDescription, { children: "A column computed from the other columns, like weight or upside. Saved with the view." })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(FormulaForm, { formula: draft, issue: issue, fields: readable, onChange: (p) => setDraft({ ...draft, ...p }) }) }), _jsxs(DialogFooter, { className: "border-t px-4 py-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsx(SaveButton, { label: "Add column", icon: PlusIcon, issues: issue
30
30
  ? [
31
31
  {
32
32
  where: `Formula ${draft.label || draft.key}`,
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery, useQueryClient } from '@tanstack/react-query';
3
- import { CoinsIcon, GlobeIcon, InboxIcon, KeyRoundIcon, LayersIcon, Link2Icon, LogInIcon, ReceiptIcon, SparklesIcon, WrenchIcon, } from 'lucide-react';
3
+ import { CoinsIcon, GlobeIcon, InboxIcon, KeyRoundIcon, LayersIcon, Link2Icon, LogInIcon, ReceiptIcon, SendIcon, SparklesIcon, WrenchIcon, } from 'lucide-react';
4
4
  import { useId, useState } from 'react';
5
5
  import { ConnectionLogo, LumenConnection, } from '../connection.js';
6
6
  import { LumenExpandList, LumenExpandRow } from '../expand-list.js';
@@ -298,7 +298,7 @@ function Allowance({ client }) {
298
298
  : [{ where: 'What for', message: 'Say what it is for', target: whyId }]),
299
299
  ];
300
300
  const ask = () => void apply('request', () => requestMore(Number(pick), why.trim()), 'Request sent').then((ok) => ok && setWhy(''));
301
- 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: null, busy: busy === 'request', onSave: ask })] })] }))] }));
301
+ 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 })] })] }))] }));
302
302
  } }));
303
303
  }
304
304
  /** One request, opened in place to grant (any amount) or decline. */
@@ -13,12 +13,15 @@ export interface FormIssue {
13
13
  export declare function goToIssue(issue: FormIssue): void;
14
14
  /**
15
15
  * Save, and before it, while anything needs fixing, a chip with the count that lists each issue.
16
- * Save stays tappable then and takes the person to the first issue instead of saving.
16
+ * Save stays tappable then and takes the person to the first issue instead of saving. Save is an
17
+ * icon (DESIGN.md, Actions: icon first): `label` is its tooltip and accessible name, never text.
17
18
  */
18
19
  export declare function SaveButton({ issues, onSave, label, icon: Icon, busy, disabled, type, className, }: {
19
20
  issues: FormIssue[];
20
21
  onSave?: () => void;
22
+ /** The tooltip and accessible name. */
21
23
  label?: string;
24
+ /** `null` is the check, as left out. */
22
25
  icon?: ComponentType<{
23
26
  className?: string;
24
27
  }> | null;
@@ -4,8 +4,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  // issue: the editor opens its section or tab (`reveal`), then the field scrolls into view, takes
5
5
  // the cursor and flashes red once. Nothing saves while an issue stands. Every editor and form in
6
6
  // the suite uses this in place of a silent disabled Save.
7
- import { CheckIcon, TriangleAlertIcon } from 'lucide-react';
7
+ import { CheckIcon, LoaderCircleIcon, TriangleAlertIcon } from 'lucide-react';
8
8
  import { useState } from 'react';
9
+ import { IconButton } from './icon-button.js';
9
10
  import { cn } from './lib/utils.js';
10
11
  import { Button } from './ui/button.js';
11
12
  import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
@@ -37,7 +38,8 @@ export function goToIssue(issue) {
37
38
  }
38
39
  /**
39
40
  * Save, and before it, while anything needs fixing, a chip with the count that lists each issue.
40
- * Save stays tappable then and takes the person to the first issue instead of saving.
41
+ * Save stays tappable then and takes the person to the first issue instead of saving. Save is an
42
+ * icon (DESIGN.md, Actions: icon first): `label` is its tooltip and accessible name, never text.
41
43
  */
42
44
  export function SaveButton({ issues, onSave, label = 'Save', icon: Icon = CheckIcon, busy = false, disabled = false, type = 'button', className, }) {
43
45
  const [open, setOpen] = useState(false);
@@ -47,14 +49,14 @@ export function SaveButton({ issues, onSave, label = 'Save', icon: Icon = CheckI
47
49
  setOpen(false);
48
50
  goToIssue(issue);
49
51
  };
50
- return (_jsxs(_Fragment, { children: [blocked && (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(TriangleAlertIcon, {}), issues.length, " to fix"] }) }), _jsxs(PopoverContent, { align: "end", className: "w-80 gap-1 p-1", children: [_jsxs("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: ["Fix ", issues.length === 1 ? 'this' : 'these', " to save. Tap one to go to it."] }), issues.map((issue, i) => (_jsxs(Button
52
+ return (_jsxs(_Fragment, { children: [blocked && (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `${issues.length} to fix`, title: `${issues.length} to fix`, className: "num text-destructive hover:text-destructive", children: [_jsx(TriangleAlertIcon, {}), issues.length] }) }), _jsxs(PopoverContent, { align: "end", className: "w-80 gap-1 p-1", children: [_jsxs("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: ["Fix ", issues.length === 1 ? 'this' : 'these', " to save. Tap one to go to it."] }), issues.map((issue, i) => (_jsxs(Button
51
53
  // biome-ignore lint/suspicious/noArrayIndexKey: two issues can share a place and message.
52
- , { type: "button", variant: "ghost", size: "sm", className: "h-auto w-full flex-col items-start gap-0 px-2 py-1.5 text-left font-normal whitespace-normal", onClick: () => go(issue), children: [_jsx("span", { className: "text-xs font-medium", children: issue.where }), _jsx("span", { className: "text-xs text-destructive", children: issue.message })] }, i)))] })] })), _jsxs(Button, { type: blocked ? 'button' : type, size: "sm", disabled: disabled || busy, "aria-disabled": blocked || undefined, className: cn(blocked && 'opacity-50', className), onClick: (e) => {
54
+ , { type: "button", variant: "ghost", size: "sm", className: "h-auto w-full flex-col items-start gap-0 px-2 py-1.5 text-left font-normal whitespace-normal", onClick: () => go(issue), children: [_jsx("span", { className: "text-xs font-medium", children: issue.where }), _jsx("span", { className: "text-xs text-destructive", children: issue.message })] }, i)))] })] })), _jsx(IconButton, { icon: busy ? LoaderCircleIcon : (Icon ?? CheckIcon), label: blocked ? `${label}: ${issues.length} to fix` : label, variant: "default", size: "icon-sm", type: blocked ? 'button' : type, busy: busy, disabled: disabled || busy, "aria-disabled": blocked || undefined, className: cn(blocked && 'opacity-50', className), onClick: (e) => {
53
55
  if (first) {
54
56
  e.preventDefault();
55
57
  go(first);
56
58
  }
57
59
  else if (type === 'button')
58
60
  onSave?.();
59
- }, children: [Icon && _jsx(Icon, {}), label] })] }));
61
+ } })] }));
60
62
  }
package/dist/index.d.ts CHANGED
@@ -82,6 +82,7 @@ export { cn } from './lib/utils.js';
82
82
  export { type HealthProbe, StatusHealth } from './status-health.js';
83
83
  export { type StatusMessage, type StatusMessageTone, StatusMessages, groupStatusMessages, } from './status-messages.js';
84
84
  export { TimeZoneSelect } from './status.js';
85
+ export { LumenStatusBar, type StatusBarModel, StatusBarProvider, type StatusFact, type StatusPart, type StatusSync, type StatusSyncState, querySync, statusWhen, useStatus, } from './status-bar.js';
85
86
  export { type AccountFacts, type JoinReaders, JoinsProvider, SourceStatusItems, YOURS_COLOR, editedCells, editedKey, frameColor, useFrameEdits, useJoins, usedJoinKeys, } from './joins.js';
86
87
  export { OriginLegend, originsOf } from './origin-legend.js';
87
88
  export { type ConnectedDomain, ConnectOffer, connectionFields, linkGroupId } from './connections.js';
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ export { cn } from './lib/utils.js';
89
89
  export { StatusHealth } from './status-health.js';
90
90
  export { StatusMessages, groupStatusMessages, } from './status-messages.js';
91
91
  export { TimeZoneSelect } from './status.js';
92
+ export { LumenStatusBar, StatusBarProvider, querySync, statusWhen, useStatus, } from './status-bar.js';
92
93
  export { JoinsProvider, SourceStatusItems, YOURS_COLOR, editedCells, editedKey, frameColor, useFrameEdits, useJoins, usedJoinKeys, } from './joins.js';
93
94
  export { OriginLegend, originsOf } from './origin-legend.js';
94
95
  export { ConnectOffer, connectionFields, linkGroupId } from './connections.js';
@@ -9,7 +9,7 @@ export interface LumenDrawerProps {
9
9
  description?: ReactNode;
10
10
  /** Stacked DrawerSections; the drawer pads and scrolls them. */
11
11
  children: ReactNode;
12
- /** The footer's actions, right-aligned (a destructive action, Cancel and Save); Close when left out. */
12
+ /** The footer's actions, right-aligned, icon first (`IconButton`, `SaveButton`); no footer when left out. */
13
13
  footer?: ReactNode;
14
14
  /** Left side of the footer: a count or status line ("12 releases"). */
15
15
  footerStart?: ReactNode;
@@ -6,7 +6,6 @@ import { useIsMobile } from './hooks/use-mobile.js';
6
6
  import { IconButton } from './icon-button.js';
7
7
  import { lazyPart } from './lazy-part.js';
8
8
  import { ResizeEdge, tabState, useDockSlot } from './side-dock.js';
9
- import { Button } from './ui/button.js';
10
9
  import { pickerOpen } from './ui/overlay-guard.js';
11
10
  import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from './ui/sheet.js';
12
11
  /** The resizable panels load with the first drawer opened; until then it opens at its default width. */
@@ -34,16 +33,14 @@ export function LumenDrawer({ open, onClose, title, description, children, foote
34
33
  }, [isMobile]);
35
34
  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' })] }));
36
35
  const body = _jsx("div", { className: "flex flex-col px-4 pb-4", children: children });
37
- // Every drawer ends on a footer, styled like a dialog's: the body scrolls above it and always
38
- // ends on a clear edge. Without actions of its own it offers Close, except on a phone, where the
39
- // sheet's X already closes it and a second Close only took room.
40
- const plain = !footer && !footerStart;
41
- const foot = (_jsxs("footer", { "data-slot": "drawer-footer", className: "flex shrink-0 items-center gap-2 border-t bg-muted/50 px-4 py-2.5", children: [footerStart && (_jsx("div", { className: "min-w-0 flex-1 truncate text-muted-foreground text-xs", children: footerStart })), _jsx("div", { className: "ml-auto flex items-center gap-2", children: footer ?? (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: onClose, children: "Close" })) })] }));
36
+ // A drawer with actions ends on a footer, styled like a dialog's: the body scrolls above it and
37
+ // ends on a clear edge. Without actions it has none: the header's × already closes it.
38
+ const foot = (footer || footerStart) && (_jsxs("footer", { "data-slot": "drawer-footer", className: "flex shrink-0 items-center gap-2 border-t bg-muted/50 px-4 py-2.5", children: [footerStart && (_jsx("div", { className: "min-w-0 flex-1 truncate text-muted-foreground text-xs", children: footerStart })), _jsx("div", { className: "ml-auto flex items-center gap-2", children: footer })] }));
42
39
  if (dock && !isMobile && slot)
43
40
  return open
44
41
  ? createPortal(_jsx(DockedDrawer, { dock: dock, title: title, description: description, onClose: onClose, defaultSize: defaultSize, minSize: minSize, maxSize: maxSize, body: body, foot: foot }), slot)
45
42
  : null;
46
- return (_jsx(Sheet, { open: open, onOpenChange: (o) => !o && onClose(), modal: isMobile, children: isMobile ? (_jsxs(SheetContent, { side: "bottom", className: "max-h-[85svh] gap-0 overflow-hidden", children: [_jsx(SheetHeader, { className: "order-first shrink-0 border-b pt-3", children: head }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: body }), !plain && foot] })) : (_jsx(SheetContent, { side: "right", showCloseButton: false,
43
+ return (_jsx(Sheet, { open: open, onOpenChange: (o) => !o && onClose(), modal: isMobile, children: isMobile ? (_jsxs(SheetContent, { side: "bottom", className: "max-h-[85svh] gap-0 overflow-hidden", children: [_jsx(SheetHeader, { className: "order-first shrink-0 border-b pt-3", children: head }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: body }), foot] })) : (_jsx(SheetContent, { side: "right", showCloseButton: false,
47
44
  // The resize handle's grab zone is wider than the handle, so a drag can start on the
48
45
  // overlay; the panels library marks that pointerdown handled, and then it doesn't close.
49
46
  onPointerDownOutside: (e) => e.detail.originalEvent.defaultPrevented && e.preventDefault(), className: "pointer-events-none! gap-0 border-0 bg-transparent shadow-none data-[side=right]:left-0 data-[side=right]:w-auto data-[side=right]:border-l-0 data-[side=right]:sm:max-w-none", children: _jsx(DrawerPanels, { defaultSize: defaultSize, minSize: minSize, maxSize: maxSize, children: _jsxs("section", { className: "flex h-full flex-col overflow-hidden", children: [_jsxs("header", { className: "flex shrink-0 items-center gap-2 border-b px-4 py-3", children: [head, _jsx(SheetClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close", className: "ml-auto" }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: body }), foot] }) }) })) }));
package/dist/page.d.ts CHANGED
@@ -16,7 +16,7 @@ export interface PageHeaderProps {
16
16
  layout?: ReactNode;
17
17
  /** A short status line beside the breadcrumb (under the title on phone). */
18
18
  meta?: ReactNode;
19
- /** This page's status bar items (`StatusItem`), shown after the app's own. */
19
+ /** @deprecated Old JSX items, desktop only; call `useStatus` with data instead. */
20
20
  status?: ReactNode;
21
21
  }
22
22
  /** The fixed-height header; `Page` renders it, or a page with several states renders it itself. */
package/dist/shell.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type LumenFinderConfig } from './lumen-finder.js';
3
3
  import { type SettingsGroupDef, type SettingsSectionDef } from './settings.js';
4
+ import { type StatusBarModel } from './status-bar.js';
4
5
  interface ShellContext {
5
6
  /** The app this page renders in (`data-app` on the root `<html>`). */
6
7
  app: string;
@@ -53,7 +54,10 @@ export interface LumenShellProps {
53
54
  brand: ReactNode;
54
55
  /** The app's sidebar content: `NavGroup`s and `SidebarSection`s. */
55
56
  nav: ReactNode;
56
- /** App-wide status bar items (health, freshness); pages add their own through `Page`. */
57
+ /** The app's status bar (DESIGN.md, Status bar): its sync and messages as data; pages add theirs
58
+ * with `useStatus`. Running Tasks of this app show by themselves. */
59
+ bar?: StatusBarModel;
60
+ /** @deprecated Old JSX items, desktop only; pass `bar`, and pages call `useStatus`. */
57
61
  status?: ReactNode;
58
62
  /** The services the app checks, as one `StatusHealth` chip at the status bar's end. */
59
63
  health?: ReactNode;
@@ -97,7 +101,7 @@ export interface LumenShellProps {
97
101
  /** The routed page (usually `<Outlet />`). */
98
102
  children: ReactNode;
99
103
  }
100
- export declare function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, settingsGroups, user, signOutUrl, profile, security, onRefresh, hideAmounts, onLogs, headerActions, panel, finder, wrap, children, }: LumenShellProps): import("react").JSX.Element;
104
+ export declare function LumenShell({ app, appLabel, brand, nav, bar, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, settingsGroups, user, signOutUrl, profile, security, onRefresh, hideAmounts, onLogs, headerActions, panel, finder, wrap, children, }: LumenShellProps): import("react").JSX.Element;
101
105
  /** A status bar entry: an optional health dot, a label and its value ("Mail · Up to date"). */
102
106
  export declare function StatusItem({ label, value, ok, className, }: {
103
107
  label: ReactNode;
package/dist/shell.js CHANGED
@@ -16,6 +16,7 @@ import { LumenSettings, ThemeToggle, allSettings, } from './settings.js';
16
16
  import { DockSlot } from './side-dock.js';
17
17
  import { SourceHealthProvider } from './source-health.js';
18
18
  import { StatusTimeZone } from './status.js';
19
+ import { LumenStatusBar, StatusBarProvider } from './status-bar.js';
19
20
  import { Avatar, AvatarFallback } from './ui/avatar.js';
20
21
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
21
22
  import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarRail, useSidebar, } from './ui/sidebar.js';
@@ -95,7 +96,7 @@ const settingsFromUrl = (() => {
95
96
  window.history.replaceState(window.history.state, '', url);
96
97
  return at || 'profile';
97
98
  })();
98
- export function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, settingsGroups, user = null, signOutUrl, profile, security, onRefresh, hideAmounts = false, onLogs, headerActions = null, panel, finder, wrap = (frame) => frame, children, }) {
99
+ export function LumenShell({ app, appLabel, brand, nav, bar, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, settingsGroups, user = null, signOutUrl, profile, security, onRefresh, hideAmounts = false, onLogs, headerActions = null, panel, finder, wrap = (frame) => frame, children, }) {
99
100
  const [statusSlot, setStatusSlot] = useState(null);
100
101
  const [dockSlot, setDockSlot] = useState(null);
101
102
  const [refreshing, setRefreshing] = useState(false);
@@ -141,7 +142,7 @@ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEn
141
142
  refreshing,
142
143
  hideAmounts,
143
144
  headerActions,
144
- }, children: _jsx(DockSlot.Provider, { value: dockSlot, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsxs("footer", { "data-slot": "shell-status", "aria-label": "Status", className: "sticky bottom-0 z-10 flex h-(--shell-status-height) shrink-0 items-center gap-4 overflow-x-auto overflow-y-hidden border-t bg-background px-(--shell-gutter) text-xs whitespace-nowrap text-muted-foreground", children: [status, _jsx("div", { ref: setStatusSlot, className: "flex shrink-0 items-center gap-4" }), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: [health, releases && _jsx(WhatsNew, { api: releases, apps: releaseApps }), statusEnd, _jsx(StatusTimeZone, {})] })] })] }), _jsx("div", { ref: setDockSlot, "data-slot": "shell-dock", className: "contents" }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, security: security, sections: settings, shared: shared, sharedLabel: sharedLabel, groups: settingsGroups }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }) }));
145
+ }, children: _jsx(StatusBarProvider, { children: _jsx(DockSlot.Provider, { value: dockSlot, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsx(LumenStatusBar, { app: app, model: bar, legacy: status, slot: setStatusSlot, end: _jsxs(_Fragment, { children: [health, releases && _jsx(WhatsNew, { api: releases, apps: releaseApps }), statusEnd, _jsx(StatusTimeZone, {})] }) })] }), _jsx("div", { ref: setDockSlot, "data-slot": "shell-dock", className: "contents" }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, security: security, sections: settings, shared: shared, sharedLabel: sharedLabel, groups: settingsGroups }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }) }) }));
145
146
  }
146
147
  /** A status bar entry: an optional health dot, a label and its value ("Mail · Up to date"). */
147
148
  export function StatusItem({ label, value, ok, className, }) {
@@ -0,0 +1,70 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type StatusMessage } from './status-messages.js';
3
+ /** How fresh the page's data is: up to date, reading now, an old copy, no network, or failed. */
4
+ export type StatusSyncState = 'ok' | 'syncing' | 'stale' | 'offline' | 'failed';
5
+ export interface StatusSync {
6
+ state: StatusSyncState;
7
+ /** What it syncs with (`Bank feed`); left out, the line reads "Updated 3m ago". */
8
+ label?: string;
9
+ /** When the data shown was last read (ms or ISO); null when never. */
10
+ at?: number | string | null;
11
+ /** Progress of a sync that counts (`40` of `112`). */
12
+ done?: number | null;
13
+ total?: number | null;
14
+ /** More on hover or tap (why it failed). */
15
+ note?: string;
16
+ }
17
+ /** A fact this page has nowhere else to show (`Next run in 12m`): plain data, at most two. */
18
+ export interface StatusFact {
19
+ label: string;
20
+ value?: string | number;
21
+ /** A time shown as `in 12m` or `3m ago` after the value, kept current. */
22
+ at?: number | string | null;
23
+ /** Green when true, red when false, grey while unknown; no dot when left out. */
24
+ ok?: boolean | null;
25
+ }
26
+ /** What a page (or any part) adds to the bar through `useStatus`. */
27
+ export interface StatusPart {
28
+ sync?: StatusSync | null;
29
+ /** Newest first. */
30
+ messages?: StatusMessage[];
31
+ facts?: StatusFact[];
32
+ }
33
+ /** The app's own bar on `LumenShell` (`bar`): its sync and messages, and whether running Tasks show. */
34
+ export interface StatusBarModel {
35
+ sync?: StatusSync | null;
36
+ messages?: StatusMessage[];
37
+ /** App-wide facts (`Next run in 12m`); a page's own come first, two show in all. */
38
+ facts?: StatusFact[];
39
+ /** Shows this app's running task from the Tasks list (default true where Tasks is mounted). */
40
+ work?: boolean;
41
+ }
42
+ /** Holds every part posted with `useStatus`; the shell mounts it around the page and the bar. */
43
+ export declare function StatusBarProvider({ children }: {
44
+ children: ReactNode;
45
+ }): import("react").JSX.Element;
46
+ /**
47
+ * Adds this page's sync, messages and facts to the status bar while it is mounted. Plain data
48
+ * only; the newest part's sync wins over the app's, messages add up, the first two facts show.
49
+ */
50
+ export declare function useStatus(part: StatusPart): void;
51
+ /** A TanStack Query result's freshness as a `StatusSync`: the page's main read, with no app code. */
52
+ export declare function querySync(q: {
53
+ status: 'pending' | 'error' | 'success';
54
+ fetchStatus: 'fetching' | 'paused' | 'idle';
55
+ dataUpdatedAt: number;
56
+ error?: unknown;
57
+ }, label?: string): StatusSync;
58
+ /** `just now`, `3m ago`, `in 12m`, `2h ago`, `3d ago`. */
59
+ export declare function statusWhen(at: number | string | null | undefined, now?: number): string;
60
+ /** The bar itself; `LumenShell` renders it. */
61
+ export declare function LumenStatusBar({ app, model, legacy, slot, end, }: {
62
+ app: string;
63
+ model?: StatusBarModel;
64
+ /** Items from the old `status` prop and pages' `status` (desktop only). */
65
+ legacy?: ReactNode;
66
+ /** The pages' old portal slot. */
67
+ slot: (el: HTMLElement | null) => void;
68
+ /** Health, What's new, the app's end items and the time zone, in that order. */
69
+ end: ReactNode;
70
+ }): import("react").JSX.Element;
@@ -0,0 +1,224 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The status bar's model: what is true right now, at a glance, that nobody has to act on
3
+ // (DESIGN.md, Status bar). A banner is for what must be acted on, a toast for what someone just
4
+ // did, a badge for one row or card. Apps and pages pass data, never JSX; the bar draws five slots
5
+ // in a fixed order: Sync · Work · Message · Page facts · End (health, What's new, time zone).
6
+ // On a phone it shows the one most urgent item and a chip that opens the rest in a sheet, so a
7
+ // row never scrolls sideways. Nothing here waits: it draws from what the page already holds.
8
+ import { isActive } from '@columenlabs/lumen-ui-types/tasks';
9
+ import { InfoIcon } from 'lucide-react';
10
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState, } from 'react';
11
+ import { useIsMobile } from './hooks/use-mobile.js';
12
+ import { cn } from './lib/utils.js';
13
+ import { STATUS_TRIGGER, StatusMessages, StatusPop, groupStatusMessages, } from './status-messages.js';
14
+ import { useTasks } from './tasks.js';
15
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from './ui/sheet.js';
16
+ const MAX_FACTS = 2;
17
+ const RegistryCtx = createContext(null);
18
+ const PartsCtx = createContext([]);
19
+ /** Holds every part posted with `useStatus`; the shell mounts it around the page and the bar. */
20
+ export function StatusBarProvider({ children }) {
21
+ const [parts, setParts] = useState(() => new Map());
22
+ const set = useCallback((id, part) => {
23
+ setParts((prev) => {
24
+ const next = new Map(prev);
25
+ if (part)
26
+ next.set(id, part);
27
+ else
28
+ next.delete(id);
29
+ return next;
30
+ });
31
+ }, []);
32
+ const registry = useMemo(() => ({ set }), [set]);
33
+ const list = useMemo(() => [...parts.values()], [parts]);
34
+ return (_jsx(RegistryCtx.Provider, { value: registry, children: _jsx(PartsCtx.Provider, { value: list, children: children }) }));
35
+ }
36
+ /**
37
+ * Adds this page's sync, messages and facts to the status bar while it is mounted. Plain data
38
+ * only; the newest part's sync wins over the app's, messages add up, the first two facts show.
39
+ */
40
+ export function useStatus(part) {
41
+ const registry = useContext(RegistryCtx);
42
+ const id = useId();
43
+ const key = JSON.stringify(part);
44
+ useEffect(() => {
45
+ if (!registry)
46
+ return;
47
+ registry.set(id, JSON.parse(key));
48
+ return () => registry.set(id, null);
49
+ }, [registry, id, key]);
50
+ }
51
+ /** A TanStack Query result's freshness as a `StatusSync`: the page's main read, with no app code. */
52
+ export function querySync(q, label) {
53
+ const at = q.dataUpdatedAt || null;
54
+ if (q.fetchStatus === 'paused')
55
+ return { state: 'offline', label, at };
56
+ if (q.fetchStatus === 'fetching')
57
+ return { state: 'syncing', label, at };
58
+ if (q.status === 'error') {
59
+ const note = q.error instanceof Error ? q.error.message : undefined;
60
+ return { state: at ? 'stale' : 'failed', label, at, note };
61
+ }
62
+ return { state: q.status === 'success' ? 'ok' : 'syncing', label, at };
63
+ }
64
+ const toMs = (at) => at == null ? null : typeof at === 'number' ? at : Date.parse(at) || null;
65
+ /** `just now`, `3m ago`, `in 12m`, `2h ago`, `3d ago`. */
66
+ export function statusWhen(at, now = Date.now()) {
67
+ const ms = toMs(at);
68
+ if (ms === null)
69
+ return '—';
70
+ const d = ms - now;
71
+ const abs = Math.abs(d);
72
+ if (abs < 45_000)
73
+ return 'just now';
74
+ const n = abs < 3_600_000
75
+ ? `${Math.round(abs / 60_000)}m`
76
+ : abs < 86_400_000
77
+ ? `${Math.round(abs / 3_600_000)}h`
78
+ : `${Math.round(abs / 86_400_000)}d`;
79
+ return d > 0 ? `in ${n}` : `${n} ago`;
80
+ }
81
+ /** The clock relative times read, ticking every 30 s only while the tab shows. */
82
+ function useNow() {
83
+ const [now, setNow] = useState(() => Date.now());
84
+ useEffect(() => {
85
+ let timer;
86
+ const start = () => {
87
+ setNow(Date.now());
88
+ timer = setInterval(() => setNow(Date.now()), 30_000);
89
+ };
90
+ const stop = () => clearInterval(timer);
91
+ const onVis = () => (document.hidden ? stop() : start());
92
+ if (!document.hidden)
93
+ start();
94
+ document.addEventListener('visibilitychange', onVis);
95
+ return () => {
96
+ stop();
97
+ document.removeEventListener('visibilitychange', onVis);
98
+ };
99
+ }, []);
100
+ return now;
101
+ }
102
+ function Mark({ tone }) {
103
+ if (tone === 'none')
104
+ return null;
105
+ if (tone === 'busy')
106
+ return (_jsx("span", { "aria-hidden": true, className: "size-2.5 shrink-0 animate-spin rounded-full border-2 border-primary border-r-transparent motion-reduce:animate-none" }));
107
+ return (_jsx("span", { "aria-hidden": true, className: cn('size-2 shrink-0 rounded-full', tone === 'ok' && 'bg-gain', tone === 'warning' && 'bg-warning', tone === 'error' && 'bg-destructive', tone === 'idle' && 'bg-muted-foreground/40') }));
108
+ }
109
+ const TONE_TEXT = {
110
+ warning: 'text-warning',
111
+ error: 'text-destructive',
112
+ };
113
+ function syncEntry(s, now) {
114
+ const when = statusWhen(s.at, now);
115
+ const count = s.done != null && s.total ? `${s.done.toLocaleString()} of ${s.total.toLocaleString()}` : '';
116
+ const base = { id: 'sync', detail: s.note };
117
+ switch (s.state) {
118
+ case 'syncing':
119
+ return {
120
+ ...base,
121
+ rank: 5,
122
+ tone: 'busy',
123
+ text: s.label ? `Syncing ${s.label}` : 'Updating',
124
+ side: count || undefined,
125
+ };
126
+ case 'stale':
127
+ return { ...base, rank: 3, tone: 'warning', text: 'Saved copy', side: when };
128
+ case 'offline':
129
+ return { ...base, rank: 3, tone: 'warning', text: 'Offline · saved copy', side: when };
130
+ case 'failed':
131
+ return {
132
+ ...base,
133
+ rank: 1,
134
+ tone: 'error',
135
+ text: s.label ? `${s.label} did not update` : 'Did not update',
136
+ };
137
+ default:
138
+ return {
139
+ ...base,
140
+ rank: 8,
141
+ tone: 'ok',
142
+ text: s.label ? `${s.label} synced` : 'Updated',
143
+ side: when,
144
+ };
145
+ }
146
+ }
147
+ function factEntry(f, i, now) {
148
+ const when = f.at != null ? statusWhen(f.at, now) : '';
149
+ const value = [f.value, when].filter((v) => v !== undefined && v !== '').join(' ');
150
+ return {
151
+ id: `fact:${i}`,
152
+ rank: 6,
153
+ tone: f.ok === undefined ? 'none' : f.ok === null ? 'idle' : f.ok ? 'ok' : 'error',
154
+ text: f.label,
155
+ side: value || undefined,
156
+ };
157
+ }
158
+ /** Builds the bar's entries from the app's model and every posted part. */
159
+ function useEntries(model, app) {
160
+ const parts = useContext(PartsCtx);
161
+ const tasks = useTasks();
162
+ const now = useNow();
163
+ const entries = [];
164
+ const sync = [...parts].reverse().find((p) => p.sync)?.sync ?? model?.sync ?? null;
165
+ if (sync)
166
+ entries.push(syncEntry(sync, now));
167
+ if (model?.work !== false && tasks?.list) {
168
+ const mine = tasks.list.tasks.filter((t) => isActive(t) && (t.app === app || !t.app));
169
+ const t = mine[0];
170
+ if (t) {
171
+ const share = t.total ? Math.round(((t.done ?? 0) / t.total) * 100) : null;
172
+ entries.push({
173
+ id: 'work',
174
+ rank: 4,
175
+ tone: 'busy',
176
+ text: mine.length > 1 ? `${t.title} +${mine.length - 1}` : t.title,
177
+ side: share !== null
178
+ ? `${share}%`
179
+ : t.done != null
180
+ ? `${t.done.toLocaleString()} ${t.unit ?? ''}`.trim()
181
+ : undefined,
182
+ });
183
+ }
184
+ }
185
+ const messages = [...parts.flatMap((p) => p.messages ?? []), ...(model?.messages ?? [])];
186
+ const groups = groupStatusMessages(messages);
187
+ const top = groups[0];
188
+ if (top) {
189
+ const tone = top.tone === 'error' ? 'error' : top.tone === 'warning' ? 'warning' : 'none';
190
+ entries.push({
191
+ id: 'messages',
192
+ rank: tone === 'error' ? 0 : tone === 'warning' ? 2 : 7,
193
+ tone,
194
+ text: top.count > 1 ? `${top.text} ×${top.count}` : top.text,
195
+ side: groups.length > 1 ? `+${groups.length - 1}` : undefined,
196
+ bar: _jsx(StatusMessages, { messages: messages }),
197
+ });
198
+ }
199
+ [...parts.flatMap((p) => p.facts ?? []), ...(model?.facts ?? [])]
200
+ .slice(0, MAX_FACTS)
201
+ .forEach((f, i) => entries.push(factEntry(f, i, now)));
202
+ return entries;
203
+ }
204
+ function EntryLine({ e }) {
205
+ if (e.bar)
206
+ return _jsx(_Fragment, { children: e.bar });
207
+ const line = (_jsxs(_Fragment, { children: [_jsx(Mark, { tone: e.tone }), _jsx("span", { className: cn('min-w-0 truncate', TONE_TEXT[e.tone]), children: e.text }), e.side && _jsx("span", { className: "shrink-0 tabular-nums text-foreground/80", children: e.side })] }));
208
+ if (!e.detail)
209
+ return _jsx("span", { className: "flex h-6 min-w-0 items-center gap-1.5 px-1.5", children: line });
210
+ return (_jsx(StatusPop, { className: cn(STATUS_TRIGGER, 'min-w-0'), content: _jsx("p", { className: TONE_TEXT[e.tone], children: e.detail }), children: line }));
211
+ }
212
+ function SheetRow({ e }) {
213
+ return (_jsxs("li", { className: "flex items-start gap-2 border-b py-2 last:border-b-0", children: [_jsx("span", { className: "flex h-5 items-center", children: _jsx(Mark, { tone: e.tone === 'none' ? 'idle' : e.tone }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: cn('break-words', TONE_TEXT[e.tone]), children: e.text }), e.detail && _jsx("span", { className: "block text-muted-foreground", children: e.detail })] }), e.side && _jsx("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: e.side })] }));
214
+ }
215
+ /** The bar itself; `LumenShell` renders it. */
216
+ export function LumenStatusBar({ app, model, legacy, slot, end, }) {
217
+ const mobile = useIsMobile();
218
+ const entries = useEntries(model, app);
219
+ const [open, setOpen] = useState(false);
220
+ const urgent = [...entries].sort((a, b) => a.rank - b.rank)[0];
221
+ return (_jsx("footer", { "data-slot": "shell-status", "aria-label": "Status", className: "sticky bottom-0 z-10 flex h-(--shell-status-height) shrink-0 items-center gap-3 overflow-hidden border-t bg-background px-(--shell-gutter) text-xs whitespace-nowrap text-muted-foreground", children: mobile ? (_jsxs(_Fragment, { children: [urgent && (_jsx("span", { "aria-live": "polite", className: "flex min-w-0 items-center", children: _jsx(EntryLine, { e: { ...urgent, bar: undefined, detail: undefined } }) })), _jsx("div", { ref: slot, hidden: true }), entries.length > 0 ? (_jsxs(Sheet, { open: open, onOpenChange: setOpen, children: [_jsxs(SheetTrigger, { className: cn(STATUS_TRIGGER, 'num ml-auto shrink-0 border text-foreground/80'), "aria-label": `Status, ${entries.length} items`, children: [_jsx(InfoIcon, { className: "size-3.5" }), entries.length] }), _jsxs(SheetContent, { side: "bottom", className: "max-h-[75svh] gap-0 rounded-t-2xl p-0 text-sm", onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsxs(SheetHeader, { className: "border-b px-4 py-3", children: [_jsx(SheetTitle, { children: "Status" }), _jsx(SheetDescription, { className: "sr-only", children: "What is true right now" })] }), _jsx("ul", { className: "overflow-y-auto px-4", children: [...entries]
222
+ .sort((a, b) => a.rank - b.rank)
223
+ .map((e) => (_jsx(SheetRow, { e: e }, e.id))) }), _jsx("div", { className: "flex items-center justify-end gap-1 border-t px-4 py-2 text-xs text-muted-foreground", children: end })] })] })) : (_jsx("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: end }))] })) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-live": "polite", className: "flex min-w-0 items-center gap-3", children: entries.map((e) => (_jsx(EntryLine, { e: e }, e.id))) }), legacy, _jsx("div", { ref: slot, className: "flex shrink-0 items-center gap-4" }), _jsx("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: end })] })) }));
224
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  export type StatusMessageTone = 'info' | 'success' | 'warning' | 'error';
2
3
  export interface StatusMessage {
3
4
  text: string;
@@ -11,6 +12,18 @@ interface Group extends Required<StatusMessage> {
11
12
  }
12
13
  /** One entry per distinct message (same tone, same words), counted, in the order given. */
13
14
  export declare function groupStatusMessages(messages: readonly StatusMessage[]): Group[];
15
+ /**
16
+ * A status bar trigger whose pop-up opens on hover with a mouse and on tap with a finger or
17
+ * keyboard, like the services chip, so a phone can read the full text too (a tooltip never opens
18
+ * on touch). Styled on its own classes only: a trigger owns `data-state`.
19
+ */
20
+ export declare function StatusPop({ label: name, className, children, content, }: {
21
+ label?: string;
22
+ className: string;
23
+ children: ReactNode;
24
+ content: ReactNode;
25
+ }): import("react").JSX.Element;
26
+ export declare const STATUS_TRIGGER = "flex h-6 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none";
14
27
  /**
15
28
  * The app's messages in the status bar, newest first. Only the latest shows, on one line that ends
16
29
  * in an ellipsis past `--status-message-max`, with its full text on hover or tap; a new message
@@ -45,13 +45,13 @@ const COLLISION_PAD = 8;
45
45
  * keyboard, like the services chip, so a phone can read the full text too (a tooltip never opens
46
46
  * on touch). Styled on its own classes only: a trigger owns `data-state`.
47
47
  */
48
- function Pop({ label: name, className, children, content, }) {
48
+ export function StatusPop({ label: name, className, children, content, }) {
49
49
  const { open, setOpen, hover, content: pop } = useHoverOpen();
50
50
  return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { "aria-label": name, className: className, ...hover, children: children }), _jsx(PopoverContent, { side: "top", align: "start", collisionPadding: COLLISION_PAD, className: "w-auto max-w-sm gap-1.5 text-xs break-words", ...pop, children: content })] }));
51
51
  }
52
- const TRIGGER = 'flex h-6 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none';
52
+ export const STATUS_TRIGGER = 'flex h-6 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none';
53
53
  function Line({ group }) {
54
- return (_jsxs(Pop, { className: cn(TRIGGER, 'min-w-0 max-w-(--status-message-max)'), content: _jsx("p", { className: TEXT[group.tone], children: label(group) }), children: [_jsx(Dot, { tone: group.tone }), _jsx("span", { className: cn('min-w-0 truncate', TEXT[group.tone]), children: group.text }), group.count > 1 && (_jsxs("span", { className: "num shrink-0 text-foreground/80", "aria-label": `${group.count} times`, children: ["\u00D7", group.count] }))] }));
54
+ return (_jsxs(StatusPop, { className: cn(STATUS_TRIGGER, 'min-w-0 max-w-(--status-message-max)'), content: _jsx("p", { className: TEXT[group.tone], children: label(group) }), children: [_jsx(Dot, { tone: group.tone }), _jsx("span", { className: cn('min-w-0 truncate', TEXT[group.tone]), children: group.text }), group.count > 1 && (_jsxs("span", { className: "num shrink-0 text-foreground/80", "aria-label": `${group.count} times`, children: ["\u00D7", group.count] }))] }));
55
55
  }
56
56
  /**
57
57
  * The app's messages in the status bar, newest first. Only the latest shows, on one line that ends
@@ -63,5 +63,5 @@ export function StatusMessages({ messages, className, }) {
63
63
  const [latest, ...earlier] = groupStatusMessages(messages);
64
64
  if (!latest)
65
65
  return null;
66
- return (_jsxs("span", { "aria-live": "polite", className: cn('flex min-w-0 items-center gap-2', className), children: [_jsx(Line, { group: latest }), earlier.length > 0 && (_jsxs(Pop, { label: `${earlier.length} earlier messages`, className: cn(TRIGGER, 'num shrink-0 text-foreground/80'), content: _jsx("ul", { className: "flex flex-col gap-1", children: earlier.map((g) => (_jsxs("li", { className: "flex items-start gap-1.5", children: [_jsx("span", { className: "flex h-4 items-center", children: _jsx(Dot, { tone: g.tone }) }), _jsx("span", { className: TEXT[g.tone], children: label(g) })] }, `${g.tone}:${g.text}`))) }), children: ["+", earlier.length] }))] }));
66
+ return (_jsxs("span", { "aria-live": "polite", className: cn('flex min-w-0 items-center gap-2', className), children: [_jsx(Line, { group: latest }), earlier.length > 0 && (_jsxs(StatusPop, { label: `${earlier.length} earlier messages`, className: cn(STATUS_TRIGGER, 'num shrink-0 text-foreground/80'), content: _jsx("ul", { className: "flex flex-col gap-1", children: earlier.map((g) => (_jsxs("li", { className: "flex items-start gap-1.5", children: [_jsx("span", { className: "flex h-4 items-center", children: _jsx(Dot, { tone: g.tone }) }), _jsx("span", { className: TEXT[g.tone], children: label(g) })] }, `${g.tone}:${g.text}`))) }), children: ["+", earlier.length] }))] }));
67
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.151",
3
+ "version": "0.1.153",
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.151",
52
+ "@columenlabs/lumen-ui-types": "0.1.153",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",