@columenlabs/lumen-ui 0.1.171 → 0.1.173
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 +2 -1
- package/dist/index.js +2 -1
- package/dist/lumen-view.js +21 -3
- package/dist/nav.d.ts +7 -0
- package/dist/nav.js +18 -1
- package/dist/shell.d.ts +4 -1
- package/dist/shell.js +3 -2
- package/dist/status-bar.js +1 -1
- package/dist/tag-chips.d.ts +22 -0
- package/dist/tag-chips.js +96 -0
- package/dist/theme.css +8 -0
- package/dist/view-model.d.ts +7 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { LumenLayout, type LumenLayoutCard, type LumenLayoutProps, type LumenLay
|
|
|
3
3
|
export { LumenViewCard, type LumenViewCardProps } from './lumen-view-card.js';
|
|
4
4
|
export { LumenSelector, LumenSelectorList, type LumenSelectorListHandle, type LumenSelectorProps, type LumenOption, type LumenOptionGroup, SelectorRowContent, } from './lumen-selector.js';
|
|
5
5
|
export { ColorChip, ColorDot, colorIcon } from './color-dot.js';
|
|
6
|
+
export { TagChips, tagColor, tagIcon } from './tag-chips.js';
|
|
6
7
|
export { EditCell, EditInputCell } from './view-edit-cell.js';
|
|
7
8
|
export { type CardOnScreen, cardsOnScreen, kindName, onScreen, pointAt, pointAtCard, type PointedAt, type SectionOnScreen, sectionsOnScreen, spotlight, targetEl, textOf, } from './spotlight.js';
|
|
8
9
|
export { LinkDot, LinkPicker, type LinkColor, linkVar } from './lumen-links.js';
|
|
@@ -45,7 +46,7 @@ export { RenameField, type RenameFieldProps } from './rename-field.js';
|
|
|
45
46
|
export { LumenStackedList, type LumenStackedListProps, LumenStackedRow, type LumenStackedRowProps, type StackedColumn, } from './lumen-stacked-list.js';
|
|
46
47
|
export { ResetButton, type ResetButtonProps } from './reset-button.js';
|
|
47
48
|
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';
|
|
49
|
+
export { FoldGroup, LumenTabBar, NavGroup, type NavItem, type SectionItem, type SectionSubItem, SidebarSection, } from './nav.js';
|
|
49
50
|
export { type OverviewPageProps, type OverviewStage, type OverviewTone, type OverviewWidget, OverviewList, OverviewPage, ToneDot, } from './overview.js';
|
|
50
51
|
export { type Crumb, IconButton, type IconButtonProps, Page, PageBody, PageHeader, type PageHeaderProps, } from './page.js';
|
|
51
52
|
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
|
@@ -10,6 +10,7 @@ export { LumenLayout, domainKeys, followRows, } from './lumen-layout.js';
|
|
|
10
10
|
export { LumenViewCard } from './lumen-view-card.js';
|
|
11
11
|
export { LumenSelector, LumenSelectorList, SelectorRowContent, } from './lumen-selector.js';
|
|
12
12
|
export { ColorChip, ColorDot, colorIcon } from './color-dot.js';
|
|
13
|
+
export { TagChips, tagColor, tagIcon } from './tag-chips.js';
|
|
13
14
|
export { EditCell, EditInputCell } from './view-edit-cell.js';
|
|
14
15
|
export { cardsOnScreen, kindName, onScreen, pointAt, pointAtCard, sectionsOnScreen, spotlight, targetEl, textOf, } from './spotlight.js';
|
|
15
16
|
export { LinkDot, LinkPicker, linkVar } from './lumen-links.js';
|
|
@@ -52,7 +53,7 @@ export { RenameField } from './rename-field.js';
|
|
|
52
53
|
export { LumenStackedList, LumenStackedRow, } from './lumen-stacked-list.js';
|
|
53
54
|
export { ResetButton } from './reset-button.js';
|
|
54
55
|
export { GrowthChart, KEY_DATE_KINDS, KeyDateChips, PALETTE, benchColour, keyDateMarkers, paletteAt, } from './growth-chart.js';
|
|
55
|
-
export { FoldGroup, NavGroup, SidebarSection, } from './nav.js';
|
|
56
|
+
export { FoldGroup, LumenTabBar, NavGroup, SidebarSection, } from './nav.js';
|
|
56
57
|
export { OverviewList, OverviewPage, ToneDot, } from './overview.js';
|
|
57
58
|
export { IconButton, Page, PageBody, PageHeader, } from './page.js';
|
|
58
59
|
export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
|
package/dist/lumen-view.js
CHANGED
|
@@ -29,6 +29,7 @@ import { MAX_COMPARE, useSymbolChart } from './market/lumen-chart.js';
|
|
|
29
29
|
import { LumenPageFooter, LumenPager } from './pager.js';
|
|
30
30
|
import { maskAmount, useAmountsHidden } from './shell.js';
|
|
31
31
|
import { useSymbolSheet } from './symbol-sheet-context.js';
|
|
32
|
+
import { TagChips, tagColor, tagIcon } from './tag-chips.js';
|
|
32
33
|
import { Badge } from './ui/badge.js';
|
|
33
34
|
import { Button } from './ui/button.js';
|
|
34
35
|
import { Checkbox } from './ui/checkbox.js';
|
|
@@ -66,6 +67,17 @@ export function mergeRows(domain, rows, groupBy) {
|
|
|
66
67
|
const MINUS = '−';
|
|
67
68
|
/** Text cells longer than this many characters are cut to one line with a tooltip. */
|
|
68
69
|
const LONG_TEXT = 48;
|
|
70
|
+
/** A tags field's tags for a row: its `values`, else its text split on commas. */
|
|
71
|
+
function tagsOf(f, row, v) {
|
|
72
|
+
if (f.values)
|
|
73
|
+
return f.values(row);
|
|
74
|
+
return typeof v === 'string'
|
|
75
|
+
? v
|
|
76
|
+
.split(',')
|
|
77
|
+
.map((t) => t.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
: [];
|
|
80
|
+
}
|
|
69
81
|
/** Rows of a view the assistant sees with each message. */
|
|
70
82
|
const ASSISTANT_ROWS = 10;
|
|
71
83
|
const formatter = (digits) => new Intl.NumberFormat('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
@@ -555,12 +567,16 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
555
567
|
const valueCellOf = (f, row, chip, v) => {
|
|
556
568
|
if (f.render)
|
|
557
569
|
return f.render(row);
|
|
570
|
+
if (f.tags && !f.edit?.many)
|
|
571
|
+
return (_jsx(TagChips, { tags: tagsOf(f, row, v), colorOf: f.color, iconOf: f.iconOf, empty: f.empty }));
|
|
558
572
|
if (!f.edit)
|
|
559
573
|
return (_jsx(ViewValueCell, { value: v, format: f.format, chip: chip, tone: chip ? f.tone?.(row) : undefined, color: typeof v === 'string' ? f.color?.(v) : undefined, amount: f.amount, compact: f.compact, empty: f.empty }));
|
|
560
574
|
const edit = f.edit;
|
|
561
575
|
const many = edit.many;
|
|
562
576
|
if (many)
|
|
563
|
-
return (_jsx(EditManyCell, { label: f.label, values: f.
|
|
577
|
+
return (_jsx(EditManyCell, { label: f.label, values: f.tags
|
|
578
|
+
? tagsOf(f, row, v)
|
|
579
|
+
: (f.values?.(row) ?? (typeof v === 'string' && v !== '' ? [v] : [])), options: options.get(f.key)?.inUse ?? [], unused: options.get(f.key)?.unused, note: edit.note?.(row), colorOf: f.tags ? (t) => f.color?.(t) ?? tagColor(t) : f.color, iconOf: f.tags ? (t) => f.iconOf?.(t) ?? tagIcon(t) : f.iconOf, onSave: (values) => many(row, values) }));
|
|
564
580
|
const save = edit.save;
|
|
565
581
|
if (!save)
|
|
566
582
|
return _jsx(ViewValueCell, { value: v, format: f.format, chip: chip });
|
|
@@ -895,8 +911,10 @@ function PivotTable({ domain, rows, pivot, }) {
|
|
|
895
911
|
/** A group heading's mark: its value's icon in its colour, else its colour's dot, else the
|
|
896
912
|
* field's icon. */
|
|
897
913
|
function GroupMark({ field, value }) {
|
|
898
|
-
|
|
899
|
-
const
|
|
914
|
+
// A tags field's one tag takes its own colour and icon, as its chips do.
|
|
915
|
+
const tag = field.tags && !value.includes(',');
|
|
916
|
+
const color = field.color?.(value) ?? (tag ? tagColor(value) : undefined);
|
|
917
|
+
const Icon = field.iconOf?.(value) ?? (tag ? tagIcon(value) : undefined);
|
|
900
918
|
if (Icon)
|
|
901
919
|
return (_jsx("span", { className: "inline-flex", style: color ? { color } : undefined, children: _jsx(Icon, { className: "size-3.5" }) }));
|
|
902
920
|
if (color)
|
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
|
+
}
|
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, }) {
|
package/dist/status-bar.js
CHANGED
|
@@ -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-
|
|
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
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
type Icon = ComponentType<{
|
|
3
|
+
className?: string;
|
|
4
|
+
}>;
|
|
5
|
+
/** A tag's colour, picked from its name: the same tag is the same colour everywhere. */
|
|
6
|
+
export declare function tagColor(tag: string): string;
|
|
7
|
+
/** A tag's icon from its words (`cybersecurity` a shield, `ai` sparkles), or none when no word fits. */
|
|
8
|
+
export declare function tagIcon(tag: string): Icon | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* A row's tags as chips on one line: each in its colour with its icon. Past `max` the rest fold
|
|
11
|
+
* into a `+N` chip whose hover names them all, so a table row never wraps. `colorOf` and `iconOf`
|
|
12
|
+
* override the name-picked colour and icon (an app's own label colours).
|
|
13
|
+
*/
|
|
14
|
+
export declare function TagChips({ tags, max, colorOf, iconOf, empty, }: {
|
|
15
|
+
tags: readonly string[];
|
|
16
|
+
max?: number;
|
|
17
|
+
colorOf?: (tag: string) => string | undefined;
|
|
18
|
+
iconOf?: (tag: string) => Icon | undefined;
|
|
19
|
+
/** What no tags says; left out: a dash. */
|
|
20
|
+
empty?: string;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Tags (a row's several free-form labels: `ai`, `cybersecurity`, `favorite`) as coloured chips,
|
|
3
|
+
// each with an icon where its words suggest one. The colour comes from the tag's name, so one tag
|
|
4
|
+
// reads the same colour on every row, page and app with nothing stored.
|
|
5
|
+
import { Baby, BookOpen, Bot, Briefcase, Bug, Building2, Car, Check, Clock, Cloud, Code, Cpu, Database, DollarSign, Dumbbell, Factory, Film, Flag, Flame, Gamepad2, Globe, GraduationCap, Heart, HeartPulse, Hourglass, House, Landmark, Leaf, Lock, MapPin, Megaphone, Music, Network, Palette, Plane, Rocket, Server, Shield, ShoppingCart, Smartphone, Sparkles, Star, TriangleAlert, Truck, Users, Utensils, Wrench, Zap, } from 'lucide-react';
|
|
6
|
+
import { ColorChip } from './color-dot.js';
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
8
|
+
// Chart colours that read as a hue in light and dark (not the grey `--chart-8`).
|
|
9
|
+
const TAG_COLORS = [1, 2, 3, 4, 5, 6, 7, 9, 10].map((n) => `var(--chart-${n})`);
|
|
10
|
+
/** A tag's colour, picked from its name: the same tag is the same colour everywhere. */
|
|
11
|
+
export function tagColor(tag) {
|
|
12
|
+
const key = tag.trim().toLowerCase();
|
|
13
|
+
let h = 2166136261;
|
|
14
|
+
for (let i = 0; i < key.length; i++)
|
|
15
|
+
h = Math.imul(h ^ key.charCodeAt(i), 16777619);
|
|
16
|
+
return TAG_COLORS[(h >>> 0) % TAG_COLORS.length];
|
|
17
|
+
}
|
|
18
|
+
// Words to icons, most specific first: a tag takes the icon of the first entry any of its words
|
|
19
|
+
// matches (`data-security` is a shield, `ai-buildout` a server).
|
|
20
|
+
const TAG_ICONS = [
|
|
21
|
+
[/^(fav|favou?rites?|starred|top|pick)$/, Star],
|
|
22
|
+
[/^(cyber|cybersecurity|security|secure|infosec|appsec|privacy)$/, Shield],
|
|
23
|
+
[/^(auth|identity|iam|zero|zerotrust|crypto|encryption)$/, Lock],
|
|
24
|
+
[/^(buildout|datacenters?|dc|infra|infrastructure|servers?|hosting|compute)$/, Server],
|
|
25
|
+
[/^(chips?|semis?|semiconductors?|hardware|gpus?|silicon)$/, Cpu],
|
|
26
|
+
[/^(ai|ml|llm|genai|agents?|agentic)$/, Sparkles],
|
|
27
|
+
[/^(bots?|robots?|robotics|automation)$/, Bot],
|
|
28
|
+
[/^(data|database|db|analytics|warehouse|sql|etl)$/, Database],
|
|
29
|
+
[/^(cloud|saas|serverless)$/, Cloud],
|
|
30
|
+
[/^(network|networking|edge|cdn)$/, Network],
|
|
31
|
+
[/^(bigtech|faang|enterprise|corp|corporate|company)$/, Building2],
|
|
32
|
+
[/^(dev|devtools|developer|developers|code|coding|engineering|software|platform)$/, Code],
|
|
33
|
+
[/^(startup|startups|growth|launch)$/, Rocket],
|
|
34
|
+
[/^(fintech|finance|financial|bank|banking|money|pay|payments|budget)$/, DollarSign],
|
|
35
|
+
[/^(gov|government|public|federal|civic)$/, Landmark],
|
|
36
|
+
[/^(health|healthcare|medical|bio|biotech|pharma)$/, HeartPulse],
|
|
37
|
+
[/^(fitness|gym|workout|sports?)$/, Dumbbell],
|
|
38
|
+
[/^(energy|power|electric|ev|battery|utilities)$/, Zap],
|
|
39
|
+
[/^(climate|green|sustainability|eco|garden|plants?)$/, Leaf],
|
|
40
|
+
[/^(manufacturing|industrial|factory)$/, Factory],
|
|
41
|
+
[/^(mobile|phone|ios|android|apps?)$/, Smartphone],
|
|
42
|
+
[/^(remote|global|international|web|online)$/, Globe],
|
|
43
|
+
[/^(local|onsite|hybrid|office|nearby)$/, MapPin],
|
|
44
|
+
[/^(home|house|family)$/, House],
|
|
45
|
+
[/^(kids?|baby|children)$/, Baby],
|
|
46
|
+
[/^(team|people|social|community|hr)$/, Users],
|
|
47
|
+
[/^(jobs?|work|career|hiring)$/, Briefcase],
|
|
48
|
+
[/^(edu|education|learning|school|course|courses|training)$/, GraduationCap],
|
|
49
|
+
[/^(books?|reading|docs|research)$/, BookOpen],
|
|
50
|
+
[/^(games?|gaming)$/, Gamepad2],
|
|
51
|
+
[/^(music|audio)$/, Music],
|
|
52
|
+
[/^(film|video|movies?|media|streaming)$/, Film],
|
|
53
|
+
[/^(design|art|creative)$/, Palette],
|
|
54
|
+
[/^(marketing|ads|sales|pr)$/, Megaphone],
|
|
55
|
+
[/^(shop|shopping|retail|ecommerce|commerce)$/, ShoppingCart],
|
|
56
|
+
[/^(food|recipes?|dinner|lunch|breakfast|dessert|cooking)$/, Utensils],
|
|
57
|
+
[/^(travel|trip|flights?)$/, Plane],
|
|
58
|
+
[/^(cars?|auto|automotive)$/, Car],
|
|
59
|
+
[/^(logistics|shipping|delivery|supply)$/, Truck],
|
|
60
|
+
[/^(tools?|repair|maintenance|fix)$/, Wrench],
|
|
61
|
+
[/^(bugs?|issues?)$/, Bug],
|
|
62
|
+
[/^(urgent|hot|asap)$/, Flame],
|
|
63
|
+
[/^(risk|warning|alert|blocked)$/, TriangleAlert],
|
|
64
|
+
[/^(done|ok|approved|verified)$/, Check],
|
|
65
|
+
[/^(waiting|pending|later|someday)$/, Hourglass],
|
|
66
|
+
[/^(daily|weekly|monthly|recurring|time)$/, Clock],
|
|
67
|
+
[/^(goal|goals|milestone|priority)$/, Flag],
|
|
68
|
+
[/^(love|loved|liked)$/, Heart],
|
|
69
|
+
];
|
|
70
|
+
const iconCache = new Map();
|
|
71
|
+
/** A tag's icon from its words (`cybersecurity` a shield, `ai` sparkles), or none when no word fits. */
|
|
72
|
+
export function tagIcon(tag) {
|
|
73
|
+
const key = tag.trim().toLowerCase();
|
|
74
|
+
let icon = iconCache.get(key);
|
|
75
|
+
if (icon === undefined) {
|
|
76
|
+
const words = key.split(/[^a-z0-9]+/).filter(Boolean);
|
|
77
|
+
icon = TAG_ICONS.find(([re]) => words.some((w) => re.test(w)))?.[1] ?? null;
|
|
78
|
+
iconCache.set(key, icon);
|
|
79
|
+
}
|
|
80
|
+
return icon ?? undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A row's tags as chips on one line: each in its colour with its icon. Past `max` the rest fold
|
|
84
|
+
* into a `+N` chip whose hover names them all, so a table row never wraps. `colorOf` and `iconOf`
|
|
85
|
+
* override the name-picked colour and icon (an app's own label colours).
|
|
86
|
+
*/
|
|
87
|
+
export function TagChips({ tags, max = 3, colorOf, iconOf, empty, }) {
|
|
88
|
+
if (!tags.length)
|
|
89
|
+
return _jsx("span", { className: "text-muted-foreground", children: empty ?? '—' });
|
|
90
|
+
const shown = tags.slice(0, max);
|
|
91
|
+
const rest = tags.length - shown.length;
|
|
92
|
+
const chips = (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 overflow-hidden", children: [shown.map((t) => (_jsx(ColorChip, { color: colorOf?.(t) ?? tagColor(t), icon: iconOf?.(t) ?? tagIcon(t), children: t }, t))), rest > 0 && _jsxs(ColorChip, { children: ["+", rest] })] }));
|
|
93
|
+
if (!rest)
|
|
94
|
+
return chips;
|
|
95
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: chips }), _jsx(TooltipContent, { className: "max-w-md whitespace-normal", children: tags.join(', ') })] }));
|
|
96
|
+
}
|
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/dist/view-model.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface ViewField<T> {
|
|
|
25
25
|
topic?: string;
|
|
26
26
|
/** Every value a row has for filtering, when it has several (a symbol in Holdings and Watchlist). */
|
|
27
27
|
values?: (row: T) => string[];
|
|
28
|
+
/**
|
|
29
|
+
* The values are tags (a row's several free-form labels): the cell draws each of `values` as a
|
|
30
|
+
* chip in its colour with its icon (`TagChips`), never a comma list. The colour and icon come
|
|
31
|
+
* from the tag's name unless `color` or `iconOf` gives them. Without `values` the text value is
|
|
32
|
+
* split on commas.
|
|
33
|
+
*/
|
|
34
|
+
tags?: boolean;
|
|
28
35
|
format: ViewFormat;
|
|
29
36
|
/** How the value is made, shown under the field in the Edit dialog. */
|
|
30
37
|
formula?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.173",
|
|
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.
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.173",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|