@columenlabs/lumen-ui 0.1.170 → 0.1.172

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.
package/dist/index.d.ts CHANGED
@@ -45,7 +45,7 @@ export { RenameField, type RenameFieldProps } from './rename-field.js';
45
45
  export { LumenStackedList, type LumenStackedListProps, LumenStackedRow, type LumenStackedRowProps, type StackedColumn, } from './lumen-stacked-list.js';
46
46
  export { ResetButton, type ResetButtonProps } from './reset-button.js';
47
47
  export { type ChartBand, type ChartMarker, GrowthChart, KEY_DATE_KINDS, KeyDateChips, type KeyDateKind, type KeyDateMarker, PALETTE, type Scale, type Series, benchColour, keyDateMarkers, paletteAt, } from './growth-chart.js';
48
- export { FoldGroup, NavGroup, type NavItem, type SectionItem, type SectionSubItem, SidebarSection, } from './nav.js';
48
+ export { FoldGroup, LumenTabBar, NavGroup, type NavItem, type SectionItem, type SectionSubItem, SidebarSection, } from './nav.js';
49
49
  export { type OverviewPageProps, type OverviewStage, type OverviewTone, type OverviewWidget, OverviewList, OverviewPage, ToneDot, } from './overview.js';
50
50
  export { type Crumb, IconButton, type IconButtonProps, Page, PageBody, PageHeader, type PageHeaderProps, } from './page.js';
51
51
  export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ export { RenameField } from './rename-field.js';
52
52
  export { LumenStackedList, LumenStackedRow, } from './lumen-stacked-list.js';
53
53
  export { ResetButton } from './reset-button.js';
54
54
  export { GrowthChart, KEY_DATE_KINDS, KeyDateChips, PALETTE, benchColour, keyDateMarkers, paletteAt, } from './growth-chart.js';
55
- export { FoldGroup, NavGroup, SidebarSection, } from './nav.js';
55
+ export { FoldGroup, LumenTabBar, NavGroup, SidebarSection, } from './nav.js';
56
56
  export { OverviewList, OverviewPage, ToneDot, } from './overview.js';
57
57
  export { IconButton, Page, PageBody, PageHeader, } from './page.js';
58
58
  export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
package/dist/nav.d.ts CHANGED
@@ -79,3 +79,10 @@ export declare function SidebarSection({ label, items, onAdd, addLabel, onRemove
79
79
  /** What its nested items stand for; plain pages by default. */
80
80
  assistantSubType?: AssistantPageType;
81
81
  }): import("react").JSX.Element;
82
+ /**
83
+ * The phone's bottom tab bar (`LumenShell tabs`): up to four of the app's pages, then More, which
84
+ * opens the sidebar with every page. Phone only; the desktop keeps the sidebar.
85
+ */
86
+ export declare function LumenTabBar({ items }: {
87
+ items: NavItem[];
88
+ }): import("react").JSX.Element | null;
package/dist/nav.js CHANGED
@@ -5,7 +5,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  // A section header folds its items: it lights up on hover with a chevron after
6
6
  // the name, a click folds it, the fold is remembered in the browser, and `+` never folds.
7
7
  import { Link, useRouterState } from '@tanstack/react-router';
8
- import { ChevronRightIcon, PlusIcon, Trash2Icon } from 'lucide-react';
8
+ import { ChevronRightIcon, MenuIcon, PlusIcon, Trash2Icon } from 'lucide-react';
9
9
  import { useAssistantPages } from './assistant-screen.js';
10
10
  import { LumenConfirm } from './confirm.js';
11
11
  import { useFolded } from './fold.js';
@@ -112,3 +112,20 @@ export function SidebarSection({ label, items, onAdd, addLabel = `Add to ${label
112
112
  ? [0, 1].map((i) => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuSkeleton, { showIcon: true }) }, i)))
113
113
  : items.map((item) => (_jsx(SectionRow, { item: item, activeId: activeId, onRemove: onRemove, removeLabel: removeLabel }, item.id))), !loading && !items.length && empty && (_jsx("li", { className: "px-2 py-1 text-xs text-muted-foreground group-data-[collapsible=icon]:hidden", children: empty }))] }), children && _jsx("div", { className: "group-data-[collapsible=icon]:hidden", children: children })] }) }));
114
114
  }
115
+ /**
116
+ * The phone's bottom tab bar (`LumenShell tabs`): up to four of the app's pages, then More, which
117
+ * opens the sidebar with every page. Phone only; the desktop keeps the sidebar.
118
+ */
119
+ export function LumenTabBar({ items }) {
120
+ const { isMobile, openMobile, setOpenMobile } = useSidebar();
121
+ const match = useMatch();
122
+ if (!isMobile || !items.length)
123
+ return null;
124
+ const tabs = items.slice(0, 4);
125
+ const scores = tabs.map((t) => match(t));
126
+ const best = Math.max(...scores);
127
+ return (_jsxs("nav", { "data-slot": "shell-tabbar", "aria-label": "Main", className: "sticky bottom-0 z-20 grid h-[calc(var(--shell-tabbar-height)+env(safe-area-inset-bottom))] shrink-0 grid-cols-5 border-t bg-background pb-[env(safe-area-inset-bottom)]", children: [tabs.map((t, i) => {
128
+ const on = best >= 0 && scores[i] === best;
129
+ return (_jsxs(Link, { to: t.to, "aria-current": on ? 'page' : undefined, className: cn('flex min-w-0 flex-col items-center justify-center gap-0.5 text-xs text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', on && 'font-semibold text-primary'), children: [_jsx(t.icon, { className: "size-5" }), _jsx("span", { className: "max-w-full truncate px-1", children: t.title })] }, t.to));
130
+ }), _jsxs("button", { type: "button", "aria-expanded": openMobile, onClick: () => setOpenMobile(true), className: cn('flex min-w-0 flex-col items-center justify-center gap-0.5 text-xs text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', best < 0 && 'font-semibold text-primary'), children: [_jsx(MenuIcon, { className: "size-5" }), _jsx("span", { children: "More" })] })] }));
131
+ }
@@ -1,9 +1,14 @@
1
1
  /** The Settings ▸ Notifications section's body. */
2
- export declare function NotificationsSettings({ api: base, product, sender, }: {
2
+ export declare function NotificationsSettings({ api: base, product, sender, parts, }: {
3
3
  /** The app's Notifications API base, e.g. `/api/notifications`. */
4
4
  api: string;
5
5
  /** The product's name in the copy ("Get Acme alerts by email"). */
6
6
  product: string;
7
7
  /** The address email comes from. */
8
8
  sender: string;
9
+ /**
10
+ * What to draw: the channels and the events grid (default), only the channels (where messages
11
+ * go) or only the events grid (what goes where), so an app can set them in two sections.
12
+ */
13
+ parts?: 'all' | 'channels' | 'events';
9
14
  }): import("react").JSX.Element;
@@ -147,12 +147,22 @@ function Events({ status, api }) {
147
147
  ...status.webhooks.map((h) => ({ id: h.id, name: h.name })),
148
148
  ];
149
149
  if (!targets.length)
150
- return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to send to yet: turn on email or ntfy, or add a webhook, above." }));
151
- const grid = { gridTemplateColumns: `minmax(12rem, 2.4fr) repeat(${targets.length}, 5.5rem)` };
152
- return (_jsxs("div", { className: "grid grid-cols-1 gap-2", children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("div", { "aria-label": "Events", className: "grid min-w-max items-center", style: grid, children: [_jsxs("div", { className: "contents", children: [_jsx("span", { className: "pb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Event" }), targets.map((t) => (_jsx("span", { className: "truncate pb-1 text-center text-xs font-semibold tracking-wide text-muted-foreground uppercase", title: t.name, children: t.name }, t.id)))] }), status.events.map((e) => (_jsxs("div", { className: "contents", children: [_jsxs("span", { className: "grid gap-0.5 border-t py-2 pr-3", children: [_jsx("span", { className: "text-sm", children: e.label }), _jsx("span", { className: "text-xs text-muted-foreground", children: e.detail })] }), targets.map((t) => (_jsx("span", { className: "self-stretch border-t py-2", children: _jsx("span", { className: "flex h-full items-center justify-center", children: _jsx(EventCell, { event: e, target: t.id, name: t.name, api: api }) }) }, t.id)))] }, e.key)))] }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "A new target starts with every event on." })] }));
150
+ return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to send to yet: turn on email or ntfy, or add a webhook, first." }));
151
+ // The event column gives way first, so a phone shows the checkboxes; its detail waits in the hover.
152
+ const grid = { gridTemplateColumns: `minmax(7rem, 1fr) repeat(${targets.length}, 5.5rem)` };
153
+ // Groups in the order they first appear; events without one come first, unlabelled.
154
+ const groups = [];
155
+ for (const e of status.events) {
156
+ const g = groups.find((x) => x.label === e.group);
157
+ if (g)
158
+ g.events.push(e);
159
+ else
160
+ groups.push({ label: e.group, events: [e] });
161
+ }
162
+ return (_jsxs("div", { className: "grid grid-cols-1 gap-2", children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("div", { "aria-label": "Events", className: "grid min-w-max items-center", style: grid, children: [_jsxs("div", { className: "contents", children: [_jsx("span", { className: "pb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Event" }), targets.map((t) => (_jsx("span", { className: "truncate pb-1 text-center text-xs font-semibold tracking-wide text-muted-foreground uppercase", title: t.name, children: t.name }, t.id)))] }), groups.map((g) => (_jsxs("div", { className: "contents", children: [g.label ? (_jsx("span", { className: "border-t pt-3 pb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase", style: { gridColumn: '1 / -1' }, children: g.label })) : null, g.events.map((e) => (_jsxs("div", { className: "contents", children: [_jsxs("span", { className: "flex min-w-0 items-baseline gap-2 border-t py-2 pr-3", title: `${e.label}: ${e.detail}`, children: [_jsx("span", { className: "truncate text-sm whitespace-nowrap sm:shrink-0", children: e.label }), _jsx("span", { className: "hidden truncate text-xs text-muted-foreground sm:inline", children: e.detail })] }), targets.map((t) => (_jsx("span", { className: "self-stretch border-t py-2", children: _jsx("span", { className: "flex h-full items-center justify-center", children: _jsx(EventCell, { event: e, target: t.id, name: t.name, api: api }) }) }, t.id)))] }, e.key)))] }, g.label ?? '')))] }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "A new target starts with every event on." })] }));
153
163
  }
154
164
  /** The Settings ▸ Notifications section's body. */
155
- export function NotificationsSettings({ api: base, product, sender, }) {
165
+ export function NotificationsSettings({ api: base, product, sender, parts = 'all', }) {
156
166
  const api = useNotifications(base, product, sender);
157
167
  const [open, setOpen] = useState(null);
158
168
  const toggle = (id) => (o) => setOpen(o ? id : null);
@@ -161,7 +171,9 @@ export function NotificationsSettings({ api: base, product, sender, }) {
161
171
  if (api.q.isError)
162
172
  return _jsx(ErrorLine, { error: api.q.error });
163
173
  const s = api.q.data;
174
+ if (parts === 'events')
175
+ return _jsx(Events, { status: s, api: api });
164
176
  return (_jsxs("div", { className: "grid grid-cols-1 gap-6", children: [_jsxs("div", { className: "grid grid-cols-1", children: [_jsx(IntegrationRow, { name: "Email", meta: "To your sign-in address", open: open === 'email', onOpenChange: toggle('email'), badge: s.email.connected ? (_jsx(TestBadge, { test: s.email.last_test })) : (_jsx(Badge, { variant: "outline", children: "Off" })), children: _jsx(Email, { status: s.email, api: api }) }), _jsx(IntegrationRow, { name: "ntfy", meta: "Phone notifications through the ntfy app", open: open === 'ntfy', onOpenChange: toggle('ntfy'), badge: s.ntfy.connected ? (_jsx(TestBadge, { test: s.ntfy.last_test })) : (_jsx(Badge, { variant: "outline", children: "Off" })), children: _jsx(Ntfy, { status: s.ntfy, api: api }) }), _jsx(IntegrationRow, { name: "Webhooks", meta: s.webhooks.length
165
177
  ? s.webhooks.map((h) => h.name).join(' · ')
166
- : 'Events posted to your own addresses', open: open === 'webhooks', onOpenChange: toggle('webhooks'), badge: s.webhooks.length ? (s.webhooks.some((h) => h.last_test?.ok === false) ? (_jsxs(Badge, { variant: "outline", className: "text-loss", children: [_jsx(AlertTriangleIcon, {}), "Failed"] })) : (_jsxs(Badge, { variant: "outline", children: [s.webhooks.length, " webhook", s.webhooks.length === 1 ? '' : 's'] }))) : (_jsx(Badge, { variant: "outline", children: "None" })), children: _jsx(Webhooks, { hooks: s.webhooks, api: api }) })] }), _jsxs("section", { className: "grid grid-cols-1 gap-2", children: [_jsx("h4", { className: "font-medium", children: "Events" }), _jsx(Events, { status: s, api: api })] })] }));
178
+ : 'Events posted to your own addresses', open: open === 'webhooks', onOpenChange: toggle('webhooks'), badge: s.webhooks.length ? (s.webhooks.some((h) => h.last_test?.ok === false) ? (_jsxs(Badge, { variant: "outline", className: "text-loss", children: [_jsx(AlertTriangleIcon, {}), "Failed"] })) : (_jsxs(Badge, { variant: "outline", children: [s.webhooks.length, " webhook", s.webhooks.length === 1 ? '' : 's'] }))) : (_jsx(Badge, { variant: "outline", children: "None" })), children: _jsx(Webhooks, { hooks: s.webhooks, api: api }) })] }), parts === 'all' ? (_jsxs("section", { className: "grid grid-cols-1 gap-2", children: [_jsx("h4", { className: "font-medium", children: "Events" }), _jsx(Events, { status: s, api: api })] })) : null] }));
167
179
  }
package/dist/shell.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type LumenFinderConfig } from './lumen-finder.js';
3
+ import { type NavItem } from './nav.js';
3
4
  import { type SettingsGroupDef, type SettingsSectionDef } from './settings.js';
4
5
  import { type StatusBarModel } from './status-bar.js';
5
6
  interface ShellContext {
@@ -54,6 +55,8 @@ export interface LumenShellProps {
54
55
  brand: ReactNode;
55
56
  /** The app's sidebar content: `NavGroup`s and `SidebarSection`s. */
56
57
  nav: ReactNode;
58
+ /** The phone's bottom tab bar: up to four main pages, then More (the sidebar). None by default. */
59
+ tabs?: NavItem[];
57
60
  /** The app's status bar (DESIGN.md, Status bar): its sync and messages as data; pages add theirs
58
61
  * with `useStatus`. Running Tasks of this app show by themselves. */
59
62
  bar?: StatusBarModel;
@@ -101,7 +104,7 @@ export interface LumenShellProps {
101
104
  /** The routed page (usually `<Outlet />`). */
102
105
  children: ReactNode;
103
106
  }
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;
107
+ export declare function LumenShell({ app, appLabel, brand, nav, tabs, 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;
105
108
  /** A status bar entry: an optional health dot, a label and its value ("Mail · Up to date"). */
106
109
  export declare function StatusItem({ label, value, ok, className, }: {
107
110
  label: ReactNode;
package/dist/shell.js CHANGED
@@ -10,6 +10,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState, }
10
10
  import { lazyPart } from './lazy-part.js';
11
11
  import { cn } from './lib/utils.js';
12
12
  import { LumenFinder } from './lumen-finder.js';
13
+ import { LumenTabBar } from './nav.js';
13
14
  import { usePrefs } from './prefs.js';
14
15
  import { withRefreshToken } from './refresh.js';
15
16
  import { LumenSettings, ThemeToggle, allSettings, } from './settings.js';
@@ -96,7 +97,7 @@ const settingsFromUrl = (() => {
96
97
  window.history.replaceState(window.history.state, '', url);
97
98
  return at || 'profile';
98
99
  })();
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, }) {
100
+ export function LumenShell({ app, appLabel, brand, nav, tabs, 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, }) {
100
101
  const [statusSlot, setStatusSlot] = useState(null);
101
102
  const [dockSlot, setDockSlot] = useState(null);
102
103
  const [refreshing, setRefreshing] = useState(false);
@@ -142,7 +143,7 @@ export function LumenShell({ app, appLabel, brand, nav, bar, status, health, sta
142
143
  refreshing,
143
144
  hideAmounts,
144
145
  headerActions,
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" })] }) }) })) }) }) }) }));
146
+ }, 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, {})] }) }), tabs && _jsx(LumenTabBar, { items: tabs })] }), _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" })] }) }) })) }) }) }) }));
146
147
  }
147
148
  /** A status bar entry: an optional health dot, a label and its value ("Mail · Up to date"). */
148
149
  export function StatusItem({ label, value, ok, className, }) {
@@ -218,7 +218,7 @@ export function LumenStatusBar({ app, model, legacy, slot, end, }) {
218
218
  const entries = useEntries(model, app);
219
219
  const [open, setOpen] = useState(false);
220
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]
221
+ return (_jsx("footer", { "data-slot": "shell-status", "aria-label": "Status", className: "sticky bottom-(--shell-tabbar-offset) 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
222
  .sort((a, b) => a.rank - b.rank)
223
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
224
  }
package/dist/theme.css CHANGED
@@ -89,6 +89,9 @@
89
89
  --shell-assistant-width: 26rem;
90
90
  --shell-header-height: 48px;
91
91
  --shell-status-height: 32px;
92
+ /* The phone tab bar (LumenShell tabs); 0 until one shows, so the status bar sits above it. */
93
+ --shell-tabbar-height: 56px;
94
+ --shell-tabbar-offset: 0px;
92
95
  /* A status bar message ends in an ellipsis here (StatusMessages), so it fits beside the bar's
93
96
  other items; the full text opens on hover or tap. */
94
97
  --status-message-max: 55vw;
@@ -851,3 +854,8 @@ body[data-spotlight="nav"] [data-spotlit] {
851
854
  filter: none;
852
855
  opacity: 1;
853
856
  }
857
+
858
+ /* With a phone tab bar under it, the status bar rides above the bar instead of the screen's edge. */
859
+ [data-slot="sidebar-inset"]:has(> [data-slot="shell-tabbar"]) {
860
+ --shell-tabbar-offset: calc(var(--shell-tabbar-height) + env(safe-area-inset-bottom));
861
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.170",
3
+ "version": "0.1.172",
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.170",
52
+ "@columenlabs/lumen-ui-types": "0.1.172",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",