@columenlabs/lumen-ui 0.1.98 → 0.1.100
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/details-table.js +1 -1
- package/dist/emoji-tab.d.ts +3 -0
- package/dist/emoji-tab.js +6 -0
- package/dist/icon-picker.js +9 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lumen-echart.js +23 -2
- package/dist/lumen-grid.js +6 -1
- package/dist/lumen-view.js +3 -3
- package/dist/page.js +1 -1
- package/dist/query-client.d.ts +13 -1
- package/dist/query-client.js +88 -3
- package/dist/shell.js +1 -1
- package/dist/summary-cards.js +3 -1
- package/dist/tasks.js +29 -9
- package/dist/tile.js +1 -1
- package/dist/view-editor.js +1 -1
- package/dist/view-icons.d.ts +3 -4
- package/dist/view-icons.js +38 -7
- package/package.json +2 -2
package/dist/details-table.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, Table
|
|
|
4
4
|
export function DetailsTable({ head, rows, foot, selected, onSelect }) {
|
|
5
5
|
const marked = head.findIndex((h) => h.grow);
|
|
6
6
|
const grow = marked >= 0 ? marked : head.length - 1;
|
|
7
|
-
const cell = (i) => cn(
|
|
7
|
+
const cell = (i) => cn(head[i]?.right && 'num text-right', i !== grow && 'w-px', head[i]?.hideOnPhone && 'max-md:hidden');
|
|
8
8
|
return (_jsx("div", { className: "-mx-1 overflow-x-auto overflow-y-hidden rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: head.map((h, i) => (_jsx(TableHead, { className: cn(h.right && 'text-right', i !== grow && 'w-px', h.hideOnPhone && 'max-md:hidden'), children: h.label }, h.label))) }) }), _jsx(TableBody, { children: rows.map((r) => (_jsx(TableRow, { "data-state": r.key === selected ? 'selected' : undefined, "aria-current": r.key === selected || undefined, tabIndex: onSelect ? 0 : undefined, className: cn(onSelect &&
|
|
9
9
|
'cursor-pointer focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring'), onClick: onSelect && (() => onSelect(r.key)), onKeyDown: onSelect &&
|
|
10
10
|
((e) => {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The icon picker's Emoji tab, loaded the first time it opens (the emoji set and its search).
|
|
3
|
+
import { EmojiPicker, EmojiPickerContent, EmojiPickerSearch } from './ui/emoji-picker.js';
|
|
4
|
+
export function EmojiTab({ onPick }) {
|
|
5
|
+
return (_jsxs(EmojiPicker, { className: "h-80 w-full", columns: 9, onEmojiSelect: ({ emoji }) => onPick(emoji), children: [_jsx(EmojiPickerSearch, { placeholder: "Search emoji" }), _jsx(EmojiPickerContent, {})] }));
|
|
6
|
+
}
|
package/dist/icon-picker.js
CHANGED
|
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { RotateCcwIcon, SearchIcon, ShapesIcon, SmileIcon } from 'lucide-react';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { IconButton } from './icon-button.js';
|
|
5
|
+
import { lazyPart } from './lazy-part.js';
|
|
5
6
|
import { Button } from './ui/button.js';
|
|
6
|
-
import { EmojiPicker, EmojiPickerContent, EmojiPickerSearch } from './ui/emoji-picker.js';
|
|
7
7
|
import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group.js';
|
|
8
8
|
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
|
|
9
9
|
import { Separator } from './ui/separator.js';
|
|
10
10
|
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
11
|
-
import { VIEW_ICONS, iconOf,
|
|
11
|
+
import { VIEW_ICONS, iconOf, useLucideIconNames } from './view-icons.js';
|
|
12
|
+
const EmojiTab = lazyPart(() => import('./emoji-tab.js').then((m) => m.EmojiTab), _jsx("div", { className: "h-80 w-full" }));
|
|
12
13
|
/** Icon matches shown at once; search narrows the rest of lucide's set. */
|
|
13
14
|
const ICON_MATCHES = 120;
|
|
14
15
|
export function IconPicker({ value, onChange, fallback, label, resetLabel = 'Use the default icon', variant, id, }) {
|
|
@@ -16,6 +17,8 @@ export function IconPicker({ value, onChange, fallback, label, resetLabel = 'Use
|
|
|
16
17
|
const [kind, setKind] = useState('icons');
|
|
17
18
|
const [q, setQ] = useState('');
|
|
18
19
|
const Icon = iconOf(value, fallback);
|
|
20
|
+
// The full list loads when the picker first opens.
|
|
21
|
+
const lucideIconNames = useLucideIconNames(open);
|
|
19
22
|
const pick = (icon) => {
|
|
20
23
|
onChange(icon);
|
|
21
24
|
setOpen(false);
|
|
@@ -29,8 +32,10 @@ export function IconPicker({ value, onChange, fallback, label, resetLabel = 'Use
|
|
|
29
32
|
setOpen(o);
|
|
30
33
|
if (o)
|
|
31
34
|
setQ('');
|
|
32
|
-
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(IconButton, { id: id, icon: Icon, label: label, variant: variant }) }), _jsxs(PopoverContent, { align: "start", collisionPadding: 8, className: "flex w-80 max-w-[calc(100vw-1rem)] flex-col gap-0 p-0", children: [_jsx("div", { className: "p-2", children: _jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "w-full", "aria-label": "Icons or emoji", value: kind, onValueChange: (v) => v && setKind(v), children: [_jsxs(ToggleGroupItem, { value: "icons", className: "flex-1", children: [_jsx(ShapesIcon, {}), "Icons"] }), _jsxs(ToggleGroupItem, { value: "emoji", className: "flex-1", children: [_jsx(SmileIcon, {}), "Emoji"] })] }) }), _jsx(Separator, {}), kind === 'icons' ? (_jsxs("div", { className: "flex h-80 flex-col", children: [_jsx("div", { className: "border-b p-2", children: _jsxs(InputGroup, { children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { "aria-label": "Search icons", placeholder:
|
|
35
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(IconButton, { id: id, icon: Icon, label: label, variant: variant }) }), _jsxs(PopoverContent, { align: "start", collisionPadding: 8, className: "flex w-80 max-w-[calc(100vw-1rem)] flex-col gap-0 p-0", children: [_jsx("div", { className: "p-2", children: _jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "w-full", "aria-label": "Icons or emoji", value: kind, onValueChange: (v) => v && setKind(v), children: [_jsxs(ToggleGroupItem, { value: "icons", className: "flex-1", children: [_jsx(ShapesIcon, {}), "Icons"] }), _jsxs(ToggleGroupItem, { value: "emoji", className: "flex-1", children: [_jsx(SmileIcon, {}), "Emoji"] })] }) }), _jsx(Separator, {}), kind === 'icons' ? (_jsxs("div", { className: "flex h-80 flex-col", children: [_jsx("div", { className: "border-b p-2", children: _jsxs(InputGroup, { children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { "aria-label": "Search icons", placeholder: lucideIconNames.length
|
|
36
|
+
? `Search ${lucideIconNames.length.toLocaleString()} icons`
|
|
37
|
+
: 'Search icons', value: q, onChange: (e) => setQ(e.target.value) })] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-2", children: names.length ? (_jsx("div", { className: "grid grid-cols-8 gap-1", children: names.map((n) => {
|
|
33
38
|
const I = iconOf(n, fallback);
|
|
34
39
|
return (_jsx(Button, { variant: value === n ? 'secondary' : 'ghost', size: "icon-sm", title: n, "aria-label": `Icon ${n}`, "aria-pressed": value === n, onClick: () => pick(n), children: _jsx(I, {}) }, n));
|
|
35
|
-
}) })) : (_jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: "No icon found." })) })] })) : (
|
|
40
|
+
}) })) : (_jsx("p", { className: "py-8 text-center text-sm text-muted-foreground", children: "No icon found." })) })] })) : (_jsx(EmojiTab, { onPick: pick })), _jsx(Separator, {}), _jsx(IconButton, { icon: RotateCcwIcon, label: resetLabel, className: "m-1", disabled: !value, onClick: () => pick(null) })] })] }));
|
|
36
41
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export { LumenChatgptSignin, type LumenChatgptSigninProps } from './chatgpt-sign
|
|
|
55
55
|
export { LumenExpandList, LumenExpandRow } from './expand-list.js';
|
|
56
56
|
export { type Fact, FactList, type FactListProps } from './fact-list.js';
|
|
57
57
|
export { LumenPager } from './pager.js';
|
|
58
|
-
export { createDeskQueryClient } from './query-client.js';
|
|
58
|
+
export { clearPersistedQueries, createDeskQueryClient, type PersistOptions } from './query-client.js';
|
|
59
59
|
export { type ClientPoint, type ClientPointKind, type LumenTelemetryOptions, apiRoute, reportClient, reportClientError, reportFailedFetch, stackFingerprint, useLumenTelemetry, } from './telemetry.js';
|
|
60
60
|
export { refreshHeaders, withRefreshToken } from './refresh.js';
|
|
61
61
|
export { type LumenLoad, LumenError, LumenLazy, LumenLoadFooter, LumenLoadTime, LumenProgress, LumenRefresh, LumenSkeleton, LumenStates, marketTrades, useLumenLoad, useLumenSource, useLiveLoad, } from './lumen-source.js';
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,7 @@ export { LumenChatgptSignin } from './chatgpt-signin.js';
|
|
|
62
62
|
export { LumenExpandList, LumenExpandRow } from './expand-list.js';
|
|
63
63
|
export { FactList } from './fact-list.js';
|
|
64
64
|
export { LumenPager } from './pager.js';
|
|
65
|
-
export { createDeskQueryClient } from './query-client.js';
|
|
65
|
+
export { clearPersistedQueries, createDeskQueryClient } from './query-client.js';
|
|
66
66
|
export { apiRoute, reportClient, reportClientError, reportFailedFetch, stackFingerprint, useLumenTelemetry, } from './telemetry.js';
|
|
67
67
|
export { refreshHeaders, withRefreshToken } from './refresh.js';
|
|
68
68
|
export { LumenError, LumenLazy, LumenLoadFooter, LumenLoadTime, LumenProgress, LumenRefresh, LumenSkeleton, LumenStates, marketTrades, useLumenLoad, useLumenSource, useLiveLoad, } from './lumen-source.js';
|
package/dist/lumen-echart.js
CHANGED
|
@@ -99,6 +99,28 @@ export function axisStyle(t) {
|
|
|
99
99
|
nameTextStyle: { color: t.muted, fontSize: 11 },
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
/** `over` laid on `under`, plain objects merged key by key: what the chart sets wins. */
|
|
103
|
+
function layer(under, over) {
|
|
104
|
+
const plain = (v) => !!v && typeof v === 'object' && Object.getPrototypeOf(v) === Object.prototype;
|
|
105
|
+
if (!plain(under) || !plain(over))
|
|
106
|
+
return over === undefined ? under : over;
|
|
107
|
+
const out = { ...under };
|
|
108
|
+
for (const [k, v] of Object.entries(over))
|
|
109
|
+
out[k] = layer(under[k], v);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/** `option` with its axes in the theme's colours under whatever the chart sets itself, so a chart
|
|
113
|
+
* that names no axis colours still draws its grid lines faint in dark mode too. */
|
|
114
|
+
function themedAxes(option, t) {
|
|
115
|
+
const style = axisStyle(t);
|
|
116
|
+
const theme = (a) => Array.isArray(a) ? a.map((x) => layer(style, x)) : layer(style, a);
|
|
117
|
+
const o = option;
|
|
118
|
+
return {
|
|
119
|
+
...option,
|
|
120
|
+
...(o.xAxis ? { xAxis: theme(o.xAxis) } : {}),
|
|
121
|
+
...(o.yAxis ? { yAxis: theme(o.yAxis) } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
102
124
|
export function LumenEChart({ option, height = 260, on, className, label, }) {
|
|
103
125
|
const box = useRef(null);
|
|
104
126
|
const chart = useRef(null);
|
|
@@ -132,12 +154,11 @@ export function LumenEChart({ option, height = 260, on, className, label, }) {
|
|
|
132
154
|
const tokens = useChartTokens();
|
|
133
155
|
// An app re-rendering hands a new but equal option (a hover, a refetch): drawing it again
|
|
134
156
|
// replays the line's entry animation, so the chart blanks under the tooltip. Only a change redraws.
|
|
135
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: tokens change when the theme flips.
|
|
136
157
|
useEffect(() => {
|
|
137
158
|
const c = chart.current;
|
|
138
159
|
if (!ready || !c)
|
|
139
160
|
return;
|
|
140
|
-
const next = resolveVars(option);
|
|
161
|
+
const next = resolveVars(themedAxes(option, tokens));
|
|
141
162
|
const key = JSON.stringify(next);
|
|
142
163
|
if (key === drawn.current)
|
|
143
164
|
return;
|
package/dist/lumen-grid.js
CHANGED
|
@@ -21,6 +21,11 @@ const GUESS = GRID_BLOCK_ROWS * 2;
|
|
|
21
21
|
const GridCellContext = createContext(null);
|
|
22
22
|
export const useGridCell = () => useContext(GridCellContext);
|
|
23
23
|
const CARD = '[data-slot=lumen-card]';
|
|
24
|
+
/** What a card's part adds to its height: nothing when it floats (a card's corner actions). */
|
|
25
|
+
const flowHeight = (el) => {
|
|
26
|
+
const at = getComputedStyle(el).position;
|
|
27
|
+
return at === 'absolute' || at === 'fixed' ? 0 : el.offsetHeight;
|
|
28
|
+
};
|
|
24
29
|
const rowsOf = (px) => Math.max(2, Math.ceil((px + GRID_GAP) / GRID_ROW_PX));
|
|
25
30
|
const byPlace = (a, b) => a.y - b.y || a.x - b.x;
|
|
26
31
|
/** Cards placed where they say and, in order after them, the ones that do not, row by row. */
|
|
@@ -120,7 +125,7 @@ function Cell({ id, fixed, report, children, }) {
|
|
|
120
125
|
// filter, body and footer, plus whatever else the wrapper holds.
|
|
121
126
|
const card = el.matches(CARD) ? el : el.querySelector(`:scope > ${CARD}`);
|
|
122
127
|
const px = card
|
|
123
|
-
? [...card.children].reduce((s, c) => s + c
|
|
128
|
+
? [...card.children].reduce((s, c) => s + flowHeight(c), 0) +
|
|
124
129
|
(card.offsetHeight - card.clientHeight) +
|
|
125
130
|
(el.offsetHeight - card.offsetHeight)
|
|
126
131
|
: el.offsetHeight;
|
package/dist/lumen-view.js
CHANGED
|
@@ -11,7 +11,6 @@ import { DndContext, closestCenter } from '@dnd-kit/core';
|
|
|
11
11
|
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
|
|
12
12
|
import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from '@tanstack/react-table';
|
|
13
13
|
import { ArrowDownIcon, ArrowUpIcon, ChartCandlestick, ChartSpline, ChevronLeftIcon, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, Grid3x3Icon, LayoutDashboardIcon, ListCollapseIcon, PencilIcon, PlusIcon, Settings2Icon, SigmaIcon, SparklesIcon, TableIcon, UserPenIcon, } from 'lucide-react';
|
|
14
|
-
import Papa from 'papaparse';
|
|
15
14
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
15
|
import { useAssistantNote } from './assistant-context.js';
|
|
17
16
|
import { ColorChip, ColorDot } from './color-dot.js';
|
|
@@ -189,9 +188,9 @@ function ColumnPicker({ domain, columns, onColumns, open, onOpenChange, classNam
|
|
|
189
188
|
/**
|
|
190
189
|
* The view's rows as a CSV file: the filtered rows in the view's sort, its
|
|
191
190
|
* shown columns (the grouping first when it is not one), plain numbers; amounts stay masked while
|
|
192
|
-
* Hide amounts is on.
|
|
191
|
+
* Hide amounts is on. The CSV writer loads on the first download, not with the page.
|
|
193
192
|
*/
|
|
194
|
-
function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
193
|
+
async function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
195
194
|
const fields = groupBy && !columns.includes(groupBy) ? [groupBy, ...columns] : columns;
|
|
196
195
|
const by = sort && fields.find((f) => f.key === sort.key);
|
|
197
196
|
const sorted = by
|
|
@@ -212,6 +211,7 @@ function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
|
212
211
|
return '';
|
|
213
212
|
return hidden && f.amount ? maskAmount(String(v)) : v;
|
|
214
213
|
};
|
|
214
|
+
const { default: Papa } = await import('papaparse');
|
|
215
215
|
const csv = Papa.unparse({
|
|
216
216
|
fields: fields.map((f) => f.label),
|
|
217
217
|
data: sorted.map((r) => fields.map((f) => cell(f, r))),
|
package/dist/page.js
CHANGED
|
@@ -31,7 +31,7 @@ export function PageHeader({ crumbs, actions, layout, meta, status }) {
|
|
|
31
31
|
label: current ? crumbName(current) : '',
|
|
32
32
|
data: { page: crumbs.map(crumbName).filter(Boolean).join(' › ') },
|
|
33
33
|
});
|
|
34
|
-
return (_jsxs(_Fragment, { children: [_jsxs("header", { "data-slot": "shell-header", className: "sticky top-0 z-20 flex h-(--shell-header-height) shrink-0 items-center gap-2 overflow-hidden border-b bg-background px-(--shell-gutter)", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(SidebarTrigger, { className: "-ml-1" }) }), _jsx(TooltipContent, { children: "Toggle sidebar" })] }), _jsx(Separator, { orientation: "vertical", className: "hidden data-vertical:h-4 data-vertical:self-center md:block" }), parent && (_jsx(IconButton, { icon: ChevronLeftIcon, label: `Back to ${crumbName(parent)}`, className: "md:hidden", onClick: () => router.navigate({ to: parent.to }) })), !isMobile && (_jsx(Breadcrumb, { className: "min-w-0", children: _jsx(BreadcrumbList, { className: "flex-nowrap", children: crumbs.map((c, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(BreadcrumbSeparator, {}), _jsx(BreadcrumbItem, { className: "min-w-0", children: c.to && i < crumbs.length - 1 ? (_jsx(BreadcrumbLink, { asChild: true, children: _jsx(Link, { to: c.to, children: c.label }) })) : i === crumbs.length - 1 ? (_jsx(BreadcrumbPage, { className: "truncate", children: _jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate font-semibold", children: c.label }) })) : (_jsx("span", { className: "truncate", children: c.label })) })] }, `${i}-${crumbName(c)}`))) }) })), isMobile && (_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate text-sm leading-tight font-semibold", children: current?.label }), meta && (_jsx("div", { className: "flex items-center gap-1.5
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { "data-slot": "shell-header", className: "sticky top-0 z-20 flex h-(--shell-header-height) shrink-0 items-center gap-2 overflow-hidden border-b bg-background px-(--shell-gutter)", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(SidebarTrigger, { className: "-ml-1" }) }), _jsx(TooltipContent, { children: "Toggle sidebar" })] }), _jsx(Separator, { orientation: "vertical", className: "hidden data-vertical:h-4 data-vertical:self-center md:block" }), parent && (_jsx(IconButton, { icon: ChevronLeftIcon, label: `Back to ${crumbName(parent)}`, className: "md:hidden", onClick: () => router.navigate({ to: parent.to }) })), !isMobile && (_jsx(Breadcrumb, { className: "min-w-0", children: _jsx(BreadcrumbList, { className: "flex-nowrap", children: crumbs.map((c, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(BreadcrumbSeparator, {}), _jsx(BreadcrumbItem, { className: "min-w-0", children: c.to && i < crumbs.length - 1 ? (_jsx(BreadcrumbLink, { asChild: true, children: _jsx(Link, { to: c.to, children: c.label }) })) : i === crumbs.length - 1 ? (_jsx(BreadcrumbPage, { className: "truncate", children: _jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate font-semibold", children: c.label }) })) : (_jsx("span", { className: "truncate", children: c.label })) })] }, `${i}-${crumbName(c)}`))) }) })), isMobile && (_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate text-sm leading-tight font-semibold", children: current?.label }), meta && (_jsx("div", { className: "flex min-w-0 items-center gap-1.5 text-[11px] leading-tight text-muted-foreground", children: _jsx("span", { className: "truncate", children: meta }) }))] })), meta && !isMobile && (_jsx("div", { className: "ml-1 hidden min-w-0 shrink items-center gap-2 truncate border-l pl-3 text-xs text-muted-foreground md:flex", children: meta })), _jsxs("div", { className: cn('ml-auto flex shrink-0 items-center gap-0.5',
|
|
35
35
|
// Phone: a page's text buttons show as icons too; their text stays as the accessible name.
|
|
36
36
|
isMobile &&
|
|
37
37
|
'[&_[data-slot=button]]:size-8 [&_[data-slot=button]]:gap-0 [&_[data-slot=button]]:px-0 [&_[data-slot=button]]:text-[0px]'), children: [actions && (_jsxs(_Fragment, { children: [_jsx("div", { "data-slot": "shell-page-actions", className: "flex items-center gap-0.5", children: actions }), _jsx(Separator, { orientation: "vertical", className: "mx-2 data-vertical:h-5 data-vertical:self-center" })] })), _jsxs("div", { "data-slot": "shell-actions", className: "flex items-center gap-0.5", children: [layout, _jsx(LumenFinderButton, {}), hideAmounts && (_jsx(IconButton, { icon: hidden ? EyeOffIcon : EyeIcon, label: hidden ? 'Show amounts' : 'Hide amounts', "aria-pressed": hidden, onClick: () => setPref('hideAmounts', !hidden) })), refresh && (_jsx(IconButton, { icon: RefreshCwIcon, busy: refreshing, label: "Refresh (skip cache)", disabled: refreshing, onClick: () => void refresh() })), headerActions] })] })] }), status && statusSlot && createPortal(status, statusSlot)] }));
|
package/dist/query-client.d.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { QueryClient, type QueryClientConfig } from '@tanstack/react-query';
|
|
2
2
|
type Retry = NonNullable<NonNullable<QueryClientConfig['defaultOptions']>['queries']>['retry'];
|
|
3
|
+
export interface PersistOptions {
|
|
4
|
+
/** The storage key; one per app (`career`), so apps never read each other's shapes. */
|
|
5
|
+
key: string;
|
|
6
|
+
/** Changes when stored shapes would no longer fit; a mismatch drops them. Defaults to the
|
|
7
|
+
* page's entry script address, which has a new hash on every build. */
|
|
8
|
+
buster?: string;
|
|
9
|
+
/** Oldest data shown at load, in ms (a day by default). */
|
|
10
|
+
maxAge?: number;
|
|
11
|
+
}
|
|
3
12
|
/** The app's one QueryClient; `retry` is the app's own rule for failed requests. */
|
|
4
|
-
export declare function createDeskQueryClient({ retry }?: {
|
|
13
|
+
export declare function createDeskQueryClient({ retry, persist, }?: {
|
|
5
14
|
retry?: Retry;
|
|
15
|
+
persist?: PersistOptions;
|
|
6
16
|
}): QueryClient;
|
|
17
|
+
/** Signing out anywhere on the site forgets every app's stored reads. */
|
|
18
|
+
export declare function clearPersistedQueries(): void;
|
|
7
19
|
export {};
|
package/dist/query-client.js
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
// again shows what it had without a request. Data changes reach the cache another way: a save
|
|
4
4
|
// invalidates its keys, a running job polls (`refetchInterval`) until it ends, an ended Task
|
|
5
5
|
// invalidates everything (tasks.tsx), and the header's Refresh refetches the page.
|
|
6
|
-
|
|
6
|
+
//
|
|
7
|
+
// With `persist`, the cache also outlives the page: what the last visit read (within a day) is
|
|
8
|
+
// shown at once on the next load or in another app of the same site, and anything older than
|
|
9
|
+
// `staleTime` is read again in the background. Signing out (`[data-sign-out]`) clears it. A query
|
|
10
|
+
// with `meta: { persist: false }` is never written.
|
|
11
|
+
import { QueryClient, dehydrate, hydrate, } from '@tanstack/react-query';
|
|
12
|
+
const PREFIX = 'lumen-cache:';
|
|
13
|
+
const DAY = 24 * 60 * 60_000;
|
|
14
|
+
/** Above this a write is skipped rather than filling the origin's storage. */
|
|
15
|
+
const MAX_BYTES = 2_000_000;
|
|
7
16
|
/** The app's one QueryClient; `retry` is the app's own rule for failed requests. */
|
|
8
|
-
export function createDeskQueryClient({ retry } = {}) {
|
|
9
|
-
|
|
17
|
+
export function createDeskQueryClient({ retry, persist, } = {}) {
|
|
18
|
+
const qc = new QueryClient({
|
|
10
19
|
defaultOptions: {
|
|
11
20
|
queries: {
|
|
12
21
|
retry: retry ?? 1,
|
|
@@ -16,4 +25,80 @@ export function createDeskQueryClient({ retry } = {}) {
|
|
|
16
25
|
},
|
|
17
26
|
},
|
|
18
27
|
});
|
|
28
|
+
if (persist)
|
|
29
|
+
persistQueries(qc, persist);
|
|
30
|
+
return qc;
|
|
31
|
+
}
|
|
32
|
+
/** Signing out anywhere on the site forgets every app's stored reads. */
|
|
33
|
+
export function clearPersistedQueries() {
|
|
34
|
+
try {
|
|
35
|
+
for (const k of Object.keys(localStorage))
|
|
36
|
+
if (k.startsWith(PREFIX))
|
|
37
|
+
localStorage.removeItem(k);
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
function persistQueries(qc, { key, buster = entryScript(), maxAge = DAY }) {
|
|
42
|
+
if (typeof window === 'undefined')
|
|
43
|
+
return;
|
|
44
|
+
const storageKey = PREFIX + key;
|
|
45
|
+
try {
|
|
46
|
+
const raw = localStorage.getItem(storageKey);
|
|
47
|
+
const stored = raw ? JSON.parse(raw) : null;
|
|
48
|
+
if (stored && stored.buster === buster && Date.now() - stored.at < maxAge) {
|
|
49
|
+
hydrate(qc, stored.state);
|
|
50
|
+
}
|
|
51
|
+
else if (raw) {
|
|
52
|
+
localStorage.removeItem(storageKey);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
clearKey(storageKey);
|
|
57
|
+
}
|
|
58
|
+
let timer;
|
|
59
|
+
let off = false;
|
|
60
|
+
const save = () => {
|
|
61
|
+
timer = undefined;
|
|
62
|
+
if (off)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
const state = dehydrate(qc, {
|
|
66
|
+
shouldDehydrateQuery: (q) => q.state.status === 'success' &&
|
|
67
|
+
q.meta?.persist !== false,
|
|
68
|
+
});
|
|
69
|
+
const text = JSON.stringify({ at: Date.now(), buster, state });
|
|
70
|
+
if (text.length > MAX_BYTES)
|
|
71
|
+
return;
|
|
72
|
+
localStorage.setItem(storageKey, text);
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
};
|
|
76
|
+
// One write a second at most, and one more when the tab is hidden (a reload or another app).
|
|
77
|
+
qc.getQueryCache().subscribe((e) => {
|
|
78
|
+
if (e.type !== 'updated' && e.type !== 'removed')
|
|
79
|
+
return;
|
|
80
|
+
if (!timer && !off)
|
|
81
|
+
timer = setTimeout(save, 1000);
|
|
82
|
+
});
|
|
83
|
+
document.addEventListener('visibilitychange', () => {
|
|
84
|
+
if (document.visibilityState === 'hidden')
|
|
85
|
+
save();
|
|
86
|
+
});
|
|
87
|
+
document.addEventListener('click', (e) => {
|
|
88
|
+
if (e.target?.closest?.('[data-sign-out]')) {
|
|
89
|
+
off = true;
|
|
90
|
+
clearPersistedQueries();
|
|
91
|
+
}
|
|
92
|
+
}, true);
|
|
93
|
+
}
|
|
94
|
+
function entryScript() {
|
|
95
|
+
if (typeof document === 'undefined')
|
|
96
|
+
return '';
|
|
97
|
+
return document.querySelector('script[type="module"][src]')?.src ?? '';
|
|
98
|
+
}
|
|
99
|
+
function clearKey(k) {
|
|
100
|
+
try {
|
|
101
|
+
localStorage.removeItem(k);
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
19
104
|
}
|
package/dist/shell.js
CHANGED
|
@@ -143,5 +143,5 @@ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEn
|
|
|
143
143
|
}
|
|
144
144
|
/** A status bar entry: an optional health dot, a label and a mono value. */
|
|
145
145
|
export function StatusItem({ label, value, ok, className, }) {
|
|
146
|
-
return (_jsxs("span", { className: cn('flex items-center gap-1.5', className), children: [ok !== undefined && (_jsx("span", { "aria-hidden": true, className: cn('size-2 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive') })), label, value !== undefined && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "num text-foreground/80", children: value })] }))] }));
|
|
146
|
+
return (_jsxs("span", { className: cn('flex items-center gap-1.5', value === undefined && 'min-w-0', className), children: [ok !== undefined && (_jsx("span", { "aria-hidden": true, className: cn('size-2 shrink-0 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive') })), _jsx("span", { className: value === undefined ? 'min-w-0 truncate' : 'max-md:hidden', children: label }), value !== undefined && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "num shrink-0 text-foreground/80", children: value })] }))] }));
|
|
147
147
|
}
|
package/dist/summary-cards.js
CHANGED
|
@@ -131,7 +131,9 @@ export function SummaryTiles({ domain, tiles, rows, fields, tile, loading, }) {
|
|
|
131
131
|
const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-fr md:grid-flow-col lg:*:*:w-auto';
|
|
132
132
|
if (loading)
|
|
133
133
|
return (_jsx("div", { "data-slot": "view-tiles", "aria-busy": "true", className: grid, children: shown.map((t) => (_jsx(Skeleton, { className: "h-[4.5rem] w-full rounded-xl" }, t.key))) }));
|
|
134
|
-
return (_jsx("div", { "data-slot": "view-tiles", className: cn(grid,
|
|
134
|
+
return (_jsx("div", { "data-slot": "view-tiles", className: cn(grid,
|
|
135
|
+
// Each tile sits in a `contents` holder, so the odd last one's span goes on what it holds.
|
|
136
|
+
'[&>*:last-child:nth-child(odd)>*]:col-span-2 md:[&>*:last-child:nth-child(odd)>*]:col-span-1'), children: shown.map((t) => (_jsx("div", { className: "contents", children: isOwnTile(t) ? (_jsx(OwnTile, { domain: domain, tile: t, rows: rows, fields: fields })) : (tile?.(t.key, rows)) }, t.key))) }));
|
|
135
137
|
}
|
|
136
138
|
// ── Edit ───────────────────────────────────────────────────────────────────────────────────────
|
|
137
139
|
const dragOptions = { activationConstraint: { distance: 4 } };
|
package/dist/tasks.js
CHANGED
|
@@ -11,7 +11,7 @@ import { toast } from 'sonner';
|
|
|
11
11
|
// ended unseen. It opens a popover on desktop and a bottom sheet on phone with the last 7 days;
|
|
12
12
|
// opening it marks the ended ones seen. ✕ clears one ended task, the brush every ended one
|
|
13
13
|
//. A task that ends while the page is open shows a toast with
|
|
14
|
-
// Open. The list polls one cheap read every 3 s only while something runs and the tab is visible;
|
|
14
|
+
// Open. The list polls one cheap read every 3 s (15 s after a minute) only while something runs and the tab is visible;
|
|
15
15
|
// otherwise it reads again on focus. The product mounts `TasksProvider` and `TasksButton` in the
|
|
16
16
|
// shell's slots; where the API answers 404 nothing shows.
|
|
17
17
|
import { LumenConfirm } from './confirm.js';
|
|
@@ -40,6 +40,7 @@ export function useTaskFor(kind, ref) {
|
|
|
40
40
|
return tasks.list.tasks.find((t) => t.kind === kind && t.ref === ref) ?? null;
|
|
41
41
|
}
|
|
42
42
|
const POLL_MS = 3000;
|
|
43
|
+
const SLOW_POLL_MS = 15_000;
|
|
43
44
|
export function TasksProvider({ config, children }) {
|
|
44
45
|
if (!config)
|
|
45
46
|
return children;
|
|
@@ -70,7 +71,15 @@ function TasksLive({ api, apps, children, }) {
|
|
|
70
71
|
const path = base && url.pathname.startsWith(base) ? url.pathname.slice(base.length) : url.pathname;
|
|
71
72
|
void router.navigate({ href: `${path || '/'}${url.search}${url.hash}` });
|
|
72
73
|
}, [router, shell]);
|
|
73
|
-
|
|
74
|
+
// The newest `open` and query client, read through refs so `refresh` stays one function for
|
|
75
|
+
// the provider's life: a new one would re-run the first-read effect and fetch the list again.
|
|
76
|
+
const openRef = useRef(open);
|
|
77
|
+
openRef.current = open;
|
|
78
|
+
const qcRef = useRef(qc);
|
|
79
|
+
qcRef.current = qc;
|
|
80
|
+
// Reads already on the wire share one request.
|
|
81
|
+
const inflight = useRef(null);
|
|
82
|
+
const read = useCallback(async () => {
|
|
74
83
|
const res = await fetch(api, { headers: { accept: 'application/json' } }).catch(() => null);
|
|
75
84
|
if (!res)
|
|
76
85
|
return;
|
|
@@ -85,14 +94,20 @@ function TasksLive({ api, apps, children, }) {
|
|
|
85
94
|
if (before) {
|
|
86
95
|
const ended = next.tasks.filter((t) => before.has(t.id) && !isActive(t));
|
|
87
96
|
for (const t of ended)
|
|
88
|
-
endedToast(t,
|
|
97
|
+
endedToast(t, openRef.current);
|
|
89
98
|
// A task's results are new data: drop the cache once (query-client.ts).
|
|
90
99
|
if (ended.length)
|
|
91
|
-
void
|
|
100
|
+
void qcRef.current.invalidateQueries();
|
|
92
101
|
}
|
|
93
102
|
activeIds.current = new Set(next.tasks.filter(isActive).map((t) => t.id));
|
|
94
103
|
setList(next);
|
|
95
|
-
}, [api
|
|
104
|
+
}, [api]);
|
|
105
|
+
const refresh = useCallback(() => {
|
|
106
|
+
inflight.current ??= read().finally(() => {
|
|
107
|
+
inflight.current = null;
|
|
108
|
+
});
|
|
109
|
+
return inflight.current;
|
|
110
|
+
}, [read]);
|
|
96
111
|
const track = useCallback((t) => {
|
|
97
112
|
activeIds.current = new Set([...(activeIds.current ?? []), t.id]);
|
|
98
113
|
setList((l) => {
|
|
@@ -147,16 +162,21 @@ function TasksLive({ api, apps, children, }) {
|
|
|
147
162
|
document.removeEventListener('visibilitychange', again);
|
|
148
163
|
};
|
|
149
164
|
}, [refresh]);
|
|
150
|
-
// Every 3 s
|
|
165
|
+
// Every 3 s while something is queued or running and the tab is visible, easing to every 15 s
|
|
166
|
+
// once it has run a minute (a long search needs no second-by-second count).
|
|
151
167
|
const active = (list?.active ?? 0) > 0;
|
|
152
168
|
useEffect(() => {
|
|
153
169
|
if (!active)
|
|
154
170
|
return;
|
|
155
|
-
const
|
|
171
|
+
const since = Date.now();
|
|
172
|
+
let id;
|
|
173
|
+
const tick = () => {
|
|
156
174
|
if (document.visibilityState === 'visible')
|
|
157
175
|
void refresh();
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
id = window.setTimeout(tick, Date.now() - since > 60_000 ? SLOW_POLL_MS : POLL_MS);
|
|
177
|
+
};
|
|
178
|
+
id = window.setTimeout(tick, POLL_MS);
|
|
179
|
+
return () => window.clearTimeout(id);
|
|
160
180
|
}, [active, refresh]);
|
|
161
181
|
const value = useMemo(() => (gone ? null : { list, refresh, cancel, seen, clear, open, track, apps }), [gone, list, refresh, cancel, seen, clear, open, track, apps]);
|
|
162
182
|
return _jsx(TasksCtx.Provider, { value: value, children: children });
|
package/dist/tile.js
CHANGED
|
@@ -30,7 +30,7 @@ export function TileRow({ children, className, fill, ...props }) {
|
|
|
30
30
|
}
|
|
31
31
|
export function TileToggleGroup({ options, selected, onSelect, label, className, fill, }) {
|
|
32
32
|
return (_jsx(Card, { size: "sm", className: cn('min-w-0 gap-0 bg-border py-0 lg:w-fit lg:flex-initial', fill &&
|
|
33
|
-
'lg:w-full lg:*:data-[slot=toggle-group]:w-full lg:**:data-[slot=toggle-group-item]:flex-1', className), children: _jsx(ToggleGroup, { type: "single", spacing: 0, value: selected, onValueChange: (v) => v && onSelect(v), "aria-label": label, className: "grid w-full grid-cols-[repeat(auto-fit,minmax(6.5rem,1fr))]
|
|
33
|
+
'lg:w-full lg:*:data-[slot=toggle-group]:w-full lg:**:data-[slot=toggle-group-item]:flex-1', className), children: _jsx(ToggleGroup, { type: "single", spacing: 0, value: selected, onValueChange: (v) => v && onSelect(v), "aria-label": label, className: "grid w-full grid-cols-2 gap-px max-sm:[&>*:last-child:nth-child(odd)]:col-span-2 sm:grid-cols-[repeat(auto-fit,minmax(6.5rem,1fr))] lg:flex lg:w-fit", children: options.map((o) => (_jsx(ToggleGroupItem, { value: o.id, "aria-label": o.label, className: "h-auto min-w-0 flex-col lg:w-(--tile-width) lg:shrink items-start justify-start gap-0.5 rounded-none! bg-card px-3 py-3 text-left font-normal data-[state=on]:bg-primary/5 data-[state=on]:shadow-[inset_0_-2px_0_var(--primary)]", children: _jsx(TileBody, { label: o.label, value: o.value, caption: o.caption, spark: o.spark, sparkColor: o.sparkColor }) }, o.id))) }) }));
|
|
34
34
|
}
|
|
35
35
|
/** A compact tile: a small caps label over one short value (a quote strip's IV, expiry and move). */
|
|
36
36
|
export function TileChip({ label, value }) {
|
package/dist/view-editor.js
CHANGED
|
@@ -46,7 +46,7 @@ import { Textarea } from './ui/textarea.js';
|
|
|
46
46
|
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
47
47
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
48
48
|
import { AGG_LABELS, FORMAT_LABELS, FormatChip, FormulaForm, MAX_FORMULAS, fieldOptions, formatOptions, formulaIssueTarget, formulaIssues, formulaOptions, } from './view-formulas.js';
|
|
49
|
-
import { VIEW_ICONS,
|
|
49
|
+
import { VIEW_ICONS, shapeIcon, viewIcon } from './view-icons.js';
|
|
50
50
|
import { FORMULAS_GROUP, canFilter, chipsOf, formulaBlanks, formulasOf, groupOf, hiddenOf, subgroupLabel, subgroupOf, subgroupsOf, summaryCash, takesFormulas, tilesOf, totalAggOf, withViewFields, } from './view-model.js';
|
|
51
51
|
import { WidgetControlsEditor } from './widget-controls.js';
|
|
52
52
|
import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
|
package/dist/view-icons.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ViewIcon, ViewIconName, ViewShape } from '@columenlabs/lumen-ui-types/views';
|
|
2
2
|
import { type LucideIcon } from 'lucide-react';
|
|
3
|
-
import { iconNames } from 'lucide-react/dynamic';
|
|
4
3
|
import type { NavIcon } from './nav.js';
|
|
5
4
|
export declare const VIEW_ICONS: Record<ViewIconName, LucideIcon>;
|
|
6
5
|
/** A view's sidebar icon: its own pick (a lucide name or an emoji), else its shape's. */
|
|
@@ -10,11 +9,11 @@ export declare const viewIcon: (config: {
|
|
|
10
9
|
}) => NavIcon;
|
|
11
10
|
/** A picked icon (a lucide name or an emoji) as a component; `fallback` when none or unknown. */
|
|
12
11
|
export declare const iconOf: (icon: ViewIcon | null | undefined, fallback: NavIcon) => NavIcon;
|
|
12
|
+
/** Every lucide icon name, for the icon picker's search: empty until `load` asks for the map and it lands. */
|
|
13
|
+
export declare function useLucideIconNames(load?: boolean): readonly string[];
|
|
13
14
|
/** Any lucide icon by name, loaded on first use (lucide's own DynamicIcon). */
|
|
14
|
-
export declare const lucideIcon: (name: string) => NavIcon;
|
|
15
|
+
export declare const lucideIcon: (name: string, fallback?: NavIcon) => NavIcon;
|
|
15
16
|
/** An emoji drawn where an icon goes. */
|
|
16
17
|
export declare const emojiIcon: (emoji: string) => NavIcon;
|
|
17
|
-
/** Every lucide icon name, for the icon picker's search. */
|
|
18
|
-
export { iconNames as lucideIconNames };
|
|
19
18
|
/** A shape's own icon (the Edit dialog's Shape picker). */
|
|
20
19
|
export declare const shapeIcon: (shape: ViewShape) => LucideIcon;
|
package/dist/view-icons.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { BanknoteIcon, BriefcaseIcon, CalendarIcon, ChartBarIcon, ChartLineIcon, ChartPieIcon, ClockIcon, CoinsIcon, FlameIcon, Grid3x3Icon, LandmarkIcon, LayersIcon, ListTreeIcon, PiggyBankIcon, ReceiptIcon, ShieldIcon, StarIcon, TableIcon, TagIcon, TargetIcon, TrendingDownIcon, TrendingUpIcon, WalletIcon, ZapIcon, } from 'lucide-react';
|
|
2
|
-
import {
|
|
3
|
-
import { createElement } from 'react';
|
|
2
|
+
import { createElement, useEffect, useState } from 'react';
|
|
4
3
|
import { cn } from './lib/utils.js';
|
|
5
4
|
export const VIEW_ICONS = {
|
|
6
5
|
table: TableIcon,
|
|
@@ -42,10 +41,44 @@ export const iconOf = (icon, fallback) => {
|
|
|
42
41
|
if (icon in VIEW_ICONS)
|
|
43
42
|
return VIEW_ICONS[icon];
|
|
44
43
|
if (/^[a-z]/.test(icon))
|
|
45
|
-
return
|
|
44
|
+
return lucide && !lucide.names.has(icon) ? fallback : lucideIcon(icon, fallback);
|
|
46
45
|
return emojiIcon(icon);
|
|
47
46
|
};
|
|
48
|
-
|
|
47
|
+
let lucide;
|
|
48
|
+
let loadingLucide;
|
|
49
|
+
function loadLucide() {
|
|
50
|
+
if (!loadingLucide)
|
|
51
|
+
loadingLucide = import('lucide-react/dynamic').then((m) => {
|
|
52
|
+
lucide = { ...m, names: new Set(m.iconNames) };
|
|
53
|
+
return lucide;
|
|
54
|
+
});
|
|
55
|
+
return loadingLucide;
|
|
56
|
+
}
|
|
57
|
+
/** Every lucide icon name, for the icon picker's search: empty until `load` asks for the map and it lands. */
|
|
58
|
+
export function useLucideIconNames(load = true) {
|
|
59
|
+
const [names, setNames] = useState(() => lucide?.iconNames ?? []);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (load && !names.length)
|
|
62
|
+
loadLucide().then((m) => setNames(m.iconNames));
|
|
63
|
+
}, [load, names.length]);
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
66
|
+
/** A lucide icon by name: an empty slot its size while the map loads, `fallback` when it has no such icon. */
|
|
67
|
+
function LucideByName({ name, fallback, className, }) {
|
|
68
|
+
const [, loaded] = useState(!!lucide);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!lucide)
|
|
71
|
+
loadLucide().then(() => loaded(true));
|
|
72
|
+
}, []);
|
|
73
|
+
if (!lucide)
|
|
74
|
+
return createElement('span', {
|
|
75
|
+
'aria-hidden': true,
|
|
76
|
+
className: cn('inline-block size-4 shrink-0', className),
|
|
77
|
+
});
|
|
78
|
+
if (!lucide.names.has(name))
|
|
79
|
+
return fallback ? createElement(fallback, { className }) : null;
|
|
80
|
+
return createElement(lucide.DynamicIcon, { name: name, className });
|
|
81
|
+
}
|
|
49
82
|
const made = new Map();
|
|
50
83
|
/** One component per pick, so React keeps it between renders. */
|
|
51
84
|
const once = (key, make) => {
|
|
@@ -57,14 +90,12 @@ const once = (key, make) => {
|
|
|
57
90
|
return Icon;
|
|
58
91
|
};
|
|
59
92
|
/** Any lucide icon by name, loaded on first use (lucide's own DynamicIcon). */
|
|
60
|
-
export const lucideIcon = (name) => once(`lucide:${name}`, () => (
|
|
93
|
+
export const lucideIcon = (name, fallback) => once(`lucide:${name}`, () => ({ className }) => createElement(LucideByName, { name, fallback, className }));
|
|
61
94
|
/** An emoji drawn where an icon goes. */
|
|
62
95
|
export const emojiIcon = (emoji) => once(`emoji:${emoji}`, () => ({ className }) => createElement('span', {
|
|
63
96
|
'aria-hidden': true,
|
|
64
97
|
'data-slot': 'view-emoji',
|
|
65
98
|
className: cn('inline-grid size-4 shrink-0 place-items-center text-sm leading-none', className),
|
|
66
99
|
}, emoji));
|
|
67
|
-
/** Every lucide icon name, for the icon picker's search. */
|
|
68
|
-
export { iconNames as lucideIconNames };
|
|
69
100
|
/** A shape's own icon (the Edit dialog's Shape picker). */
|
|
70
101
|
export const shapeIcon = (shape) => SHAPE_ICONS[shape];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.100",
|
|
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.100",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|