@columenlabs/lumen-ui 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +4 -2
- package/dist/add-column.d.ts +23 -0
- package/dist/add-column.js +62 -0
- package/dist/api-fetch.d.ts +9 -0
- package/dist/api-fetch.js +60 -0
- package/dist/app-switcher.d.ts +21 -0
- package/dist/app-switcher.js +12 -0
- package/dist/app-views.d.ts +18 -0
- package/dist/app-views.js +41 -0
- package/dist/assistant-context.d.ts +72 -0
- package/dist/assistant-context.js +30 -0
- package/dist/assistant-panel.d.ts +58 -0
- package/dist/assistant-panel.js +244 -0
- package/dist/assistant-screen.d.ts +124 -0
- package/dist/assistant-screen.js +252 -0
- package/dist/card-switch.d.ts +16 -0
- package/dist/card-switch.js +16 -0
- package/dist/color-dot.d.ts +14 -0
- package/dist/color-dot.js +19 -0
- package/dist/confirm.d.ts +18 -0
- package/dist/confirm.js +5 -0
- package/dist/connections.d.ts +45 -0
- package/dist/connections.js +275 -0
- package/dist/coverage-calendar-map.d.ts +27 -0
- package/dist/coverage-calendar-map.js +84 -0
- package/dist/coverage-cells.d.ts +55 -0
- package/dist/coverage-cells.js +85 -0
- package/dist/coverage-map.d.ts +112 -0
- package/dist/coverage-map.js +428 -0
- package/dist/details-table.d.ts +20 -0
- package/dist/details-table.js +13 -0
- package/dist/drawer-section.d.ts +27 -0
- package/dist/drawer-section.js +23 -0
- package/dist/floating-head.d.ts +2 -0
- package/dist/floating-head.js +57 -0
- package/dist/fold.d.ts +16 -0
- package/dist/fold.js +70 -0
- package/dist/form-issues.d.ts +32 -0
- package/dist/form-issues.js +60 -0
- package/dist/formula-input.d.ts +17 -0
- package/dist/formula-input.js +104 -0
- package/dist/frame-script.d.ts +7 -0
- package/dist/frame-script.js +103 -0
- package/dist/freshness.d.ts +24 -0
- package/dist/freshness.js +99 -0
- package/dist/group-button.d.ts +33 -0
- package/dist/group-button.js +67 -0
- package/dist/growth-chart.d.ts +77 -0
- package/dist/growth-chart.js +195 -0
- package/dist/hooks/use-keyboard-fit.d.ts +15 -0
- package/dist/hooks/use-keyboard-fit.js +29 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/icon-button.d.ts +21 -0
- package/dist/icon-button.js +22 -0
- package/dist/icon-picker.d.ts +14 -0
- package/dist/icon-picker.js +36 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +90 -0
- package/dist/integrations.d.ts +57 -0
- package/dist/integrations.js +73 -0
- package/dist/joins.d.ts +128 -0
- package/dist/joins.js +767 -0
- package/dist/kit-config.d.ts +13 -0
- package/dist/kit-config.js +15 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/lumen-boundary.d.ts +7 -0
- package/dist/lumen-boundary.js +10 -0
- package/dist/lumen-code-editor-impl.d.ts +2 -0
- package/dist/lumen-code-editor-impl.js +16 -0
- package/dist/lumen-code-editor.d.ts +11 -0
- package/dist/lumen-code-editor.js +11 -0
- package/dist/lumen-code.d.ts +22 -0
- package/dist/lumen-code.js +75 -0
- package/dist/lumen-drawer.d.ts +30 -0
- package/dist/lumen-drawer.js +27 -0
- package/dist/lumen-echart.d.ts +58 -0
- package/dist/lumen-echart.js +129 -0
- package/dist/lumen-filter.d.ts +75 -0
- package/dist/lumen-filter.js +546 -0
- package/dist/lumen-finder.d.ts +62 -0
- package/dist/lumen-finder.js +275 -0
- package/dist/lumen-form.d.ts +34 -0
- package/dist/lumen-form.js +81 -0
- package/dist/lumen-geo-map.d.ts +9 -0
- package/dist/lumen-geo-map.js +135 -0
- package/dist/lumen-grid.d.ts +49 -0
- package/dist/lumen-grid.js +203 -0
- package/dist/lumen-layout.d.ts +150 -0
- package/dist/lumen-layout.js +658 -0
- package/dist/lumen-links.d.ts +96 -0
- package/dist/lumen-links.js +213 -0
- package/dist/lumen-page-state.d.ts +20 -0
- package/dist/lumen-page-state.js +15 -0
- package/dist/lumen-selector.d.ts +139 -0
- package/dist/lumen-selector.js +308 -0
- package/dist/lumen-source.d.ts +137 -0
- package/dist/lumen-source.js +327 -0
- package/dist/lumen-tab-drawer.d.ts +27 -0
- package/dist/lumen-tab-drawer.js +26 -0
- package/dist/lumen-tabs.d.ts +21 -0
- package/dist/lumen-tabs.js +6 -0
- package/dist/lumen-toast.d.ts +11 -0
- package/dist/lumen-toast.js +16 -0
- package/dist/lumen-trend.d.ts +18 -0
- package/dist/lumen-trend.js +157 -0
- package/dist/lumen-view-card.d.ts +35 -0
- package/dist/lumen-view-card.js +50 -0
- package/dist/lumen-view.d.ts +172 -0
- package/dist/lumen-view.js +645 -0
- package/dist/lumen-widget-charts.d.ts +57 -0
- package/dist/lumen-widget-charts.js +69 -0
- package/dist/lumen-widget.d.ts +614 -0
- package/dist/lumen-widget.js +594 -0
- package/dist/market/compare-chart.d.ts +34 -0
- package/dist/market/compare-chart.js +292 -0
- package/dist/market/indicator-picker.d.ts +29 -0
- package/dist/market/indicator-picker.js +125 -0
- package/dist/market/indicators.d.ts +63 -0
- package/dist/market/indicators.js +314 -0
- package/dist/market/live.d.ts +120 -0
- package/dist/market/live.js +444 -0
- package/dist/market/lumen-chart.d.ts +88 -0
- package/dist/market/lumen-chart.js +645 -0
- package/dist/market/market-widget.d.ts +45 -0
- package/dist/market/market-widget.js +346 -0
- package/dist/market/option-chain.d.ts +19 -0
- package/dist/market/option-chain.js +53 -0
- package/dist/market/price-chart.d.ts +128 -0
- package/dist/market/price-chart.js +619 -0
- package/dist/mcp-settings.d.ts +20 -0
- package/dist/mcp-settings.js +65 -0
- package/dist/nav.d.ts +77 -0
- package/dist/nav.js +73 -0
- package/dist/note-line.d.ts +13 -0
- package/dist/note-line.js +5 -0
- package/dist/notifications.d.ts +9 -0
- package/dist/notifications.js +166 -0
- package/dist/origin-legend.d.ts +14 -0
- package/dist/origin-legend.js +38 -0
- package/dist/overview.d.ts +84 -0
- package/dist/overview.js +105 -0
- package/dist/page-inputs.d.ts +14 -0
- package/dist/page-inputs.js +16 -0
- package/dist/page.d.ts +32 -0
- package/dist/page.js +45 -0
- package/dist/pager.d.ts +10 -0
- package/dist/pager.js +14 -0
- package/dist/prefs.d.ts +71 -0
- package/dist/prefs.js +126 -0
- package/dist/query-client.d.ts +7 -0
- package/dist/query-client.js +19 -0
- package/dist/refresh.d.ts +4 -0
- package/dist/refresh.js +19 -0
- package/dist/rename-field.d.ts +11 -0
- package/dist/rename-field.js +26 -0
- package/dist/reset-button.d.ts +9 -0
- package/dist/reset-button.js +10 -0
- package/dist/route-error.d.ts +42 -0
- package/dist/route-error.js +229 -0
- package/dist/schedules.d.ts +88 -0
- package/dist/schedules.js +221 -0
- package/dist/settings.d.ts +51 -0
- package/dist/settings.js +181 -0
- package/dist/shell.d.ts +99 -0
- package/dist/shell.js +137 -0
- package/dist/source-health.d.ts +12 -0
- package/dist/source-health.js +58 -0
- package/dist/spotlight.d.ts +23 -0
- package/dist/spotlight.js +138 -0
- package/dist/status-health.d.ts +16 -0
- package/dist/status-health.js +41 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.js +12 -0
- package/dist/summary-cards.d.ts +56 -0
- package/dist/summary-cards.js +270 -0
- package/dist/symbol-sheet-context.d.ts +4 -0
- package/dist/symbol-sheet-context.js +5 -0
- package/dist/symbol-sheet.d.ts +9 -0
- package/dist/symbol-sheet.js +24 -0
- package/dist/tasks.d.ts +29 -0
- package/dist/tasks.js +278 -0
- package/dist/theme.css +606 -0
- package/dist/tile.d.ts +37 -0
- package/dist/tile.js +23 -0
- package/dist/ui/alert-dialog.d.ts +18 -0
- package/dist/ui/alert-dialog.js +41 -0
- package/dist/ui/alert.d.ts +10 -0
- package/dist/ui/alert.js +27 -0
- package/dist/ui/avatar.d.ts +11 -0
- package/dist/ui/avatar.js +22 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/badge.js +27 -0
- package/dist/ui/breadcrumb.d.ts +11 -0
- package/dist/ui/breadcrumb.js +27 -0
- package/dist/ui/button-group.d.ts +11 -0
- package/dist/ui/button-group.js +27 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/button.js +38 -0
- package/dist/ui/card.d.ts +11 -0
- package/dist/ui/card.js +24 -0
- package/dist/ui/chart.d.ts +44 -0
- package/dist/ui/chart.js +117 -0
- package/dist/ui/checkbox.d.ts +4 -0
- package/dist/ui/checkbox.js +8 -0
- package/dist/ui/collapsible.d.ts +5 -0
- package/dist/ui/collapsible.js +12 -0
- package/dist/ui/combobox.d.ts +24 -0
- package/dist/ui/combobox.js +60 -0
- package/dist/ui/command.d.ts +18 -0
- package/dist/ui/command.js +34 -0
- package/dist/ui/dialog.d.ts +23 -0
- package/dist/ui/dialog.js +50 -0
- package/dist/ui/dropdown-menu.d.ts +29 -0
- package/dist/ui/dropdown-menu.js +50 -0
- package/dist/ui/emoji-picker.d.ts +6 -0
- package/dist/ui/emoji-picker.js +28 -0
- package/dist/ui/empty.d.ts +11 -0
- package/dist/ui/empty.js +33 -0
- package/dist/ui/field.d.ts +24 -0
- package/dist/ui/field.js +65 -0
- package/dist/ui/hover-card.d.ts +6 -0
- package/dist/ui/hover-card.js +13 -0
- package/dist/ui/input-group.d.ts +16 -0
- package/dist/ui/input-group.js +56 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +6 -0
- package/dist/ui/item.d.ts +23 -0
- package/dist/ui/item.js +67 -0
- package/dist/ui/kbd.d.ts +3 -0
- package/dist/ui/kbd.js +9 -0
- package/dist/ui/label.d.ts +4 -0
- package/dist/ui/label.js +7 -0
- package/dist/ui/popover.d.ts +10 -0
- package/dist/ui/popover.js +25 -0
- package/dist/ui/progress.d.ts +4 -0
- package/dist/ui/progress.js +9 -0
- package/dist/ui/resizable.d.ts +7 -0
- package/dist/ui/resizable.js +13 -0
- package/dist/ui/scroll-area.d.ts +5 -0
- package/dist/ui/scroll-area.js +10 -0
- package/dist/ui/select.d.ts +15 -0
- package/dist/ui/select.js +36 -0
- package/dist/ui/separator.d.ts +4 -0
- package/dist/ui/separator.js +7 -0
- package/dist/ui/sheet.d.ts +14 -0
- package/dist/ui/sheet.js +54 -0
- package/dist/ui/sidebar.d.ts +69 -0
- package/dist/ui/sidebar.js +206 -0
- package/dist/ui/skeleton.d.ts +2 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/slider.d.ts +4 -0
- package/dist/ui/slider.js +9 -0
- package/dist/ui/sonner.d.ts +3 -0
- package/dist/ui/sonner.js +24 -0
- package/dist/ui/spinner.d.ts +2 -0
- package/dist/ui/spinner.js +7 -0
- package/dist/ui/switch.d.ts +6 -0
- package/dist/ui/switch.js +7 -0
- package/dist/ui/table.d.ts +10 -0
- package/dist/ui/table.js +31 -0
- package/dist/ui/tabs.d.ts +7 -0
- package/dist/ui/tabs.js +22 -0
- package/dist/ui/textarea.d.ts +3 -0
- package/dist/ui/textarea.js +6 -0
- package/dist/ui/toggle-group.d.ts +10 -0
- package/dist/ui/toggle-group.js +22 -0
- package/dist/ui/toggle.d.ts +9 -0
- package/dist/ui/toggle.js +25 -0
- package/dist/ui/tooltip.d.ts +7 -0
- package/dist/ui/tooltip.js +16 -0
- package/dist/view-edit-cell.d.ts +49 -0
- package/dist/view-edit-cell.js +198 -0
- package/dist/view-editor.d.ts +52 -0
- package/dist/view-editor.js +940 -0
- package/dist/view-formulas.d.ts +61 -0
- package/dist/view-formulas.js +247 -0
- package/dist/view-icons.d.ts +20 -0
- package/dist/view-icons.js +70 -0
- package/dist/view-model.d.ts +390 -0
- package/dist/view-model.js +316 -0
- package/dist/whats-new.d.ts +3 -0
- package/dist/whats-new.js +53 -0
- package/dist/widget-controls.d.ts +9 -0
- package/dist/widget-controls.js +26 -0
- package/dist/widget-settings.d.ts +436 -0
- package/dist/widget-settings.js +290 -0
- package/dist/widget-thumbs.d.ts +3 -0
- package/dist/widget-thumbs.js +29 -0
- package/dist/workspace-keys.d.ts +12 -0
- package/dist/workspace-keys.js +13 -0
- package/dist/workspaces.d.ts +57 -0
- package/dist/workspaces.js +473 -0
- package/package.json +82 -4
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// LumenSelector: the one
|
|
3
|
+
// picker for every "pick one or more things from a list", on the shell's base-ui Combobox.
|
|
4
|
+
//
|
|
5
|
+
// - Groups with sticky headings, in the order given; a group over 6 rows shows 6 and "N more"
|
|
6
|
+
// until searched. Same label twice: `detail` tells them apart (a dev warning when it can't).
|
|
7
|
+
// - One aligned detail column on the right; the selected row has a muted fill. Only multiple
|
|
8
|
+
// selection shows checks (a checkbox per row); a single pick never shows a checkmark.
|
|
9
|
+
// - Search shows past 8 options (`search="auto"`); `create` turns a typed value into an option.
|
|
10
|
+
// - Single or multiple (chips in the trigger, "+N" past four, Clear all, picked order kept).
|
|
11
|
+
// - Triggers: `field` (a form select), `chip` (a compact header pill), `icon` (an icon button).
|
|
12
|
+
// `LumenSelectorList` is the fourth, `none`: the list anchored to a text box (the assistant @ and /).
|
|
13
|
+
// - Phone: a bottom sheet with the search on top and, for multiple, Done.
|
|
14
|
+
// - An option with a `target` (a card or a sidebar page) lights it on the page while hovered.
|
|
15
|
+
import { Combobox as Base } from '@base-ui/react';
|
|
16
|
+
import { CheckIcon, ChevronDownIcon, PlusIcon, SearchIcon, XIcon } from 'lucide-react';
|
|
17
|
+
import { useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
18
|
+
import { useIsMobile } from './hooks/use-mobile.js';
|
|
19
|
+
import { cn } from './lib/utils.js';
|
|
20
|
+
import { spotlight } from './spotlight.js';
|
|
21
|
+
import { Button } from './ui/button.js';
|
|
22
|
+
import { Sheet, SheetContent, SheetDescription, SheetTitle } from './ui/sheet.js';
|
|
23
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
24
|
+
const SHOWN = 6;
|
|
25
|
+
const SEARCH_PAST = 8;
|
|
26
|
+
const CHIPS = 4;
|
|
27
|
+
const CREATE = '\u0000create';
|
|
28
|
+
/** The options as groups, in order. */
|
|
29
|
+
function toGroups(options, groups) {
|
|
30
|
+
if (groups)
|
|
31
|
+
return groups.filter((g) => g.options.length);
|
|
32
|
+
const byLabel = new Map();
|
|
33
|
+
for (const o of options ?? []) {
|
|
34
|
+
const g = o.group ?? '';
|
|
35
|
+
byLabel.set(g, [...(byLabel.get(g) ?? []), o]);
|
|
36
|
+
}
|
|
37
|
+
return [...byLabel].map(([label, list]) => ({ label, options: list }));
|
|
38
|
+
}
|
|
39
|
+
const matches = (o, group, q) => [o.label, o.detail, group, ...(o.keywords ?? [])].some((s) => s?.toLowerCase().includes(q));
|
|
40
|
+
/**
|
|
41
|
+
* How well `q` matches `s` as a subsequence (`prc` finds Price), higher is better; null when it
|
|
42
|
+
* doesn't. Letters in a row and at word starts score more, a prefix most.
|
|
43
|
+
*/
|
|
44
|
+
export function fuzzyScore(q, s) {
|
|
45
|
+
const t = s.toLowerCase();
|
|
46
|
+
if (!q)
|
|
47
|
+
return 0;
|
|
48
|
+
if (t === q)
|
|
49
|
+
return 1000;
|
|
50
|
+
if (t.startsWith(q))
|
|
51
|
+
return 500 - t.length;
|
|
52
|
+
let score = 0;
|
|
53
|
+
let at = -1;
|
|
54
|
+
let run = 0;
|
|
55
|
+
for (const ch of q) {
|
|
56
|
+
const i = t.indexOf(ch, at + 1);
|
|
57
|
+
if (i < 0)
|
|
58
|
+
return null;
|
|
59
|
+
run = i === at + 1 ? run + 1 : 0;
|
|
60
|
+
const start = i === 0 || /[\s_.\-%(]/.test(t[i - 1] ?? '');
|
|
61
|
+
score += 1 + run * 3 + (start ? 5 : 0) - Math.min(i - at - 1, 5) * 0.2;
|
|
62
|
+
at = i;
|
|
63
|
+
}
|
|
64
|
+
return score - t.length * 0.05;
|
|
65
|
+
}
|
|
66
|
+
/** The best fuzzy score of an option over its label, value and keywords. */
|
|
67
|
+
const fuzzyOf = (o, q) => [o.label, o.value, ...(o.keywords ?? [])].reduce((best, s) => {
|
|
68
|
+
const v = fuzzyScore(q, s);
|
|
69
|
+
return v === null ? best : best === null ? v : Math.max(best, v);
|
|
70
|
+
}, null);
|
|
71
|
+
/**
|
|
72
|
+
* What the list shows: matching rows by group, a long group cut to 6 (plus any picked row) until
|
|
73
|
+
* it is opened or searched. Shared by every trigger so rows behave the same.
|
|
74
|
+
*/
|
|
75
|
+
export function useSelectorRows(options, groups, query, picked,
|
|
76
|
+
/** Match letters in order, not a substring, and put the best matches first. */
|
|
77
|
+
fuzzy = false) {
|
|
78
|
+
const all = useMemo(() => toGroups(options, groups), [options, groups]);
|
|
79
|
+
const [open, setOpen] = useState(new Set());
|
|
80
|
+
const q = query.trim().toLowerCase();
|
|
81
|
+
const shown = useMemo(() => {
|
|
82
|
+
if (fuzzy && q) {
|
|
83
|
+
const scored = all.flatMap((g) => {
|
|
84
|
+
const rows = g.options
|
|
85
|
+
.map((o) => ({ o, v: fuzzyOf(o, q) }))
|
|
86
|
+
.filter((r) => r.v !== null)
|
|
87
|
+
.sort((a, b) => b.v - a.v);
|
|
88
|
+
return rows.length ? [{ label: g.label, rows, best: rows[0]?.v ?? 0 }] : [];
|
|
89
|
+
});
|
|
90
|
+
return scored
|
|
91
|
+
.sort((a, b) => b.best - a.best)
|
|
92
|
+
.map((g) => ({ label: g.label, rows: g.rows.map((r) => r.o), more: 0 }));
|
|
93
|
+
}
|
|
94
|
+
return all.flatMap((g) => {
|
|
95
|
+
const rows = q ? g.options.filter((o) => matches(o, g.label, q)) : [...g.options];
|
|
96
|
+
if (!rows.length)
|
|
97
|
+
return [];
|
|
98
|
+
if (q || open.has(g.label) || rows.length <= SHOWN + 1)
|
|
99
|
+
return [{ label: g.label, rows, more: 0 }];
|
|
100
|
+
const keep = rows.filter((o, i) => i < SHOWN || picked.has(o.value));
|
|
101
|
+
return [{ label: g.label, rows: keep, more: rows.length - keep.length }];
|
|
102
|
+
});
|
|
103
|
+
}, [all, q, open, picked, fuzzy]);
|
|
104
|
+
const count = useMemo(() => all.reduce((n, g) => n + g.options.length, 0), [all]);
|
|
105
|
+
useDuplicateWarning(all);
|
|
106
|
+
return {
|
|
107
|
+
all,
|
|
108
|
+
shown,
|
|
109
|
+
count,
|
|
110
|
+
flat: shown.flatMap((g) => g.rows),
|
|
111
|
+
expand: (label) => setOpen((s) => new Set([...s, label])),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Two rows that read the same can't be told apart: say so while building (design rule 2). */
|
|
115
|
+
function useDuplicateWarning(all) {
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!import.meta.env?.DEV)
|
|
118
|
+
return;
|
|
119
|
+
for (const g of all) {
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const o of g.options) {
|
|
122
|
+
const k = `${o.label}\u0000${o.detail ?? ''}`;
|
|
123
|
+
if (seen.has(k))
|
|
124
|
+
console.warn(`LumenSelector: two "${o.label}" rows in "${g.label}" share their detail`);
|
|
125
|
+
seen.add(k);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, [all]);
|
|
129
|
+
}
|
|
130
|
+
const ROW = 'relative flex w-full cursor-default items-center gap-2 rounded-md px-2 text-sm outline-hidden select-none h-8 max-md:h-11 max-md:px-4 max-md:text-[15px] data-highlighted:bg-accent data-highlighted:text-accent-foreground data-selected:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4';
|
|
131
|
+
/** A row with a description or thumbnail grows to fit it. */
|
|
132
|
+
const TALL = 'h-auto min-h-8 py-1.5 max-md:h-auto';
|
|
133
|
+
const GROUP_LABEL = 'sticky top-0 z-10 bg-popover px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground max-md:px-4';
|
|
134
|
+
const MORE = 'flex h-7 w-full items-center gap-1.5 rounded-md pr-2 pl-8 text-left text-xs text-muted-foreground hover:bg-accent max-md:pl-11';
|
|
135
|
+
/** One row's inside: box or icon, label, detail, and the check when picked. */
|
|
136
|
+
export function SelectorRowContent({ option, selected, multiple, trailing, }) {
|
|
137
|
+
const Icon = option.icon;
|
|
138
|
+
return (_jsxs(_Fragment, { children: [option.indent && _jsx("span", { "aria-hidden": true, className: "w-4 shrink-0" }), multiple && (_jsx("span", { "aria-hidden": true, className: cn('flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input', selected && 'border-primary bg-primary text-primary-foreground'), children: selected && _jsx(CheckIcon, { className: "size-3" }) })), option.thumb ? (_jsx("span", { "aria-hidden": true, className: "shrink-0", children: option.thumb })) : (Icon && _jsx(Icon, { className: "text-muted-foreground" })), option.description ? (_jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate", children: option.content ?? option.label }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: option.description })] })) : (_jsx("span", { className: "min-w-0 flex-1 truncate", children: option.content ?? option.label })), option.detail && (_jsx("span", { className: "shrink-0 text-xs whitespace-nowrap text-muted-foreground", children: option.detail })), trailing] }));
|
|
139
|
+
}
|
|
140
|
+
function Rows({ shown, picked, multiple, full, expand, }) {
|
|
141
|
+
return shown.map((g) => (_jsxs(Base.Group, { items: g.rows.map((o) => o.value), children: [g.label && _jsx(Base.GroupLabel, { className: GROUP_LABEL, children: g.label }), g.rows.map((o) => {
|
|
142
|
+
const row = (_jsx(Base.Item, { value: o.value, disabled: !!o.disabled || (full && !picked.has(o.value)), className: cn(ROW, (o.description || o.thumb) && TALL), children: _jsx(SelectorRowContent, { option: o, selected: picked.has(o.value), multiple: multiple && o.value !== CREATE }) }, o.value));
|
|
143
|
+
return typeof o.disabled === 'string' ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("div", { children: row }) }), _jsx(TooltipContent, { children: o.disabled })] }, o.value)) : (row);
|
|
144
|
+
}), g.more > 0 && (_jsxs("button", { type: "button", className: MORE, onMouseDown: (e) => e.preventDefault(), onClick: () => expand(g.label), children: [_jsx(ChevronDownIcon, { className: "size-3.5" }), g.more, " more"] }))] }, g.label || '\u0000')));
|
|
145
|
+
}
|
|
146
|
+
const FIELD = "flex w-fit min-w-0 items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
|
|
147
|
+
/** The one picker. See the file header for what every use gets. */
|
|
148
|
+
export function LumenSelector(props) {
|
|
149
|
+
const { options, groups, trigger = 'field', icon: LeadIcon, search = 'auto', create, createLabel = (q) => `Add “${q}”`, placeholder = 'Pick one', empty = 'Nothing matches', label, size = 'default', className, disabled, header, align = 'start', id, } = props;
|
|
150
|
+
const multiple = props.multiple === true;
|
|
151
|
+
const values = multiple
|
|
152
|
+
? props.value
|
|
153
|
+
: props.value
|
|
154
|
+
? [props.value]
|
|
155
|
+
: [];
|
|
156
|
+
const picked = useMemo(() => new Set(values), [values]);
|
|
157
|
+
const [query, setQuery] = useState('');
|
|
158
|
+
const chipsRef = useRef(null);
|
|
159
|
+
const [ownOpen, setOwnOpen] = useState(false);
|
|
160
|
+
const open = props.open ?? ownOpen;
|
|
161
|
+
// Inside a modal dialog or sheet, the list portals into it: a modal's focus trap would pull
|
|
162
|
+
// focus back out of a list portalled to the body, so its search box could never be typed in.
|
|
163
|
+
const markRef = useRef(null);
|
|
164
|
+
const [host, setHost] = useState(null);
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
if (open)
|
|
167
|
+
setHost(markRef.current?.closest('[role="dialog"]') ?? null);
|
|
168
|
+
}, [open]);
|
|
169
|
+
const setOpen = (o) => {
|
|
170
|
+
setOwnOpen(o);
|
|
171
|
+
props.onOpenChange?.(o);
|
|
172
|
+
if (!o) {
|
|
173
|
+
setQuery('');
|
|
174
|
+
spotlight(null);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const phone = useIsMobile();
|
|
178
|
+
const sheetTitle = useId();
|
|
179
|
+
const { all, shown, count, flat, expand } = useSelectorRows(options, groups, query, picked);
|
|
180
|
+
const searchable = search === true || (search === 'auto' && count > SEARCH_PAST) || !!create;
|
|
181
|
+
const byValue = useMemo(() => {
|
|
182
|
+
const m = new Map();
|
|
183
|
+
for (const g of all)
|
|
184
|
+
for (const o of g.options)
|
|
185
|
+
m.set(o.value, o);
|
|
186
|
+
return m;
|
|
187
|
+
}, [all]);
|
|
188
|
+
const q = query.trim();
|
|
189
|
+
// The create row shows only for text `create` would take (a known ticker, a valid spec).
|
|
190
|
+
const canCreate = !!create &&
|
|
191
|
+
!!q &&
|
|
192
|
+
!flat.some((o) => o.label.toLowerCase() === q.toLowerCase()) &&
|
|
193
|
+
create(q) !== null;
|
|
194
|
+
const rows = canCreate
|
|
195
|
+
? [
|
|
196
|
+
...shown,
|
|
197
|
+
{
|
|
198
|
+
label: '',
|
|
199
|
+
rows: [{ value: CREATE, label: createLabel(q), icon: PlusIcon }],
|
|
200
|
+
more: 0,
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
: shown;
|
|
204
|
+
// Multiple shows chips in the trigger, typed text after them searching; an icon trigger keeps
|
|
205
|
+
// the search in the popup.
|
|
206
|
+
const inChips = multiple && trigger !== 'icon';
|
|
207
|
+
const max = multiple ? props.max : undefined;
|
|
208
|
+
const full = max !== undefined && values.length >= max;
|
|
209
|
+
const labelOf = (v) => byValue.get(v)?.label ?? v;
|
|
210
|
+
const pick = (next) => {
|
|
211
|
+
const made = (v) => v === CREATE ? (create?.(q) ?? null) : v;
|
|
212
|
+
if (multiple) {
|
|
213
|
+
const list = (Array.isArray(next) ? next : []).map(made).filter((v) => v !== null);
|
|
214
|
+
props.onChange([...new Set(list)]);
|
|
215
|
+
setQuery('');
|
|
216
|
+
}
|
|
217
|
+
else if (typeof next === 'string') {
|
|
218
|
+
const v = made(next);
|
|
219
|
+
if (v !== null)
|
|
220
|
+
props.onChange(v);
|
|
221
|
+
setOpen(false);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const selected = multiple ? null : (byValue.get(values[0] ?? '') ?? null);
|
|
225
|
+
const Lead = selected?.icon ?? LeadIcon;
|
|
226
|
+
const fieldInside = (_jsxs(_Fragment, { children: [Lead && _jsx(Lead, { className: "text-muted-foreground" }), _jsx("span", { className: cn('min-w-0 flex-1 truncate text-left', !selected && !values.length && 'text-muted-foreground'), children: selected ? (selected.content ?? selected.label) : values[0] ? values[0] : placeholder }), _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 text-muted-foreground" })] }));
|
|
227
|
+
const list = (_jsxs(_Fragment, { children: [header, _jsx(Base.List, { className: cn('overscroll-contain p-1 data-empty:p-0', phone
|
|
228
|
+
? 'min-h-0 flex-1 overflow-y-auto pb-4'
|
|
229
|
+
: 'max-h-[min(22rem,calc(var(--available-height)-3rem))] scroll-pt-8 overflow-y-auto'), children: _jsx(Rows, { shown: rows, picked: picked, multiple: multiple, full: full, expand: expand }) }), _jsx(Base.Empty, { className: "hidden px-2 py-3 text-center text-sm text-muted-foreground group-data-empty/selector:block", children: empty })] }));
|
|
230
|
+
const clearAll = multiple && values.length > 0 && (_jsx("button", { type: "button", className: "text-xs text-muted-foreground hover:text-foreground", onClick: () => props.onChange([]), children: "Clear all" }));
|
|
231
|
+
const searchBox = (_jsxs("div", { className: cn('flex items-center gap-2 border-b px-2', phone ? 'mx-4 h-11 rounded-md border' : 'h-9', !searchable && 'sr-only'), children: [_jsx(SearchIcon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx(Base.Input, { readOnly: !searchable, placeholder: create ? 'Search or type a new one' : `Search ${count}`, "aria-label": label ? `Search ${label}` : 'Search', className: "h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground max-md:text-[15px]" })] }));
|
|
232
|
+
const rootProps = {
|
|
233
|
+
items: rows.map((g) => ({ value: g.label, items: g.rows.map((o) => o.value) })),
|
|
234
|
+
filteredItems: rows.map((g) => ({ value: g.label, items: g.rows.map((o) => o.value) })),
|
|
235
|
+
itemToStringLabel: labelOf,
|
|
236
|
+
inputValue: query,
|
|
237
|
+
onInputValueChange: (v) => setQuery(v),
|
|
238
|
+
open,
|
|
239
|
+
onOpenChange: setOpen,
|
|
240
|
+
disabled,
|
|
241
|
+
autoHighlight: true,
|
|
242
|
+
onItemHighlighted: (v) => {
|
|
243
|
+
const o = v ? byValue.get(v) : undefined;
|
|
244
|
+
spotlight(o?.target, o?.targetName ?? o?.label);
|
|
245
|
+
if (o && o.value !== CREATE)
|
|
246
|
+
props.onHighlight?.(o.value);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
// Multiple: chips in the trigger, typed text after them searches.
|
|
250
|
+
const chips = multiple && (_jsxs(_Fragment, { children: [values.slice(0, CHIPS).map((v) => (_jsxs(Base.Chip, { className: "flex h-[calc(--spacing(5.25))] items-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap", children: [labelOf(v), _jsx(Base.ChipRemove, { "aria-label": `Remove ${labelOf(v)}`, className: "-mr-0.5 rounded-sm opacity-50 hover:opacity-100", children: _jsx(XIcon, { className: "size-3" }) })] }, v))), values.length > CHIPS && (_jsxs("span", { className: "rounded-sm bg-muted px-1.5 text-xs leading-[calc(--spacing(5.25))] font-medium", children: ["+", values.length - CHIPS] }))] }));
|
|
251
|
+
if (phone) {
|
|
252
|
+
// The trigger opens a bottom sheet; the list is the combobox inline in it.
|
|
253
|
+
const opener = trigger === 'icon' ? (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": label, disabled: disabled, className: className, onClick: () => setOpen(true), children: LeadIcon && _jsx(LeadIcon, {}) })) : (_jsx("button", { type: "button", id: id, "data-size": size, "aria-label": label, disabled: disabled, className: cn(FIELD, multiple && 'h-auto min-h-8 flex-wrap justify-start py-1', trigger === 'chip' && 'rounded-full', className), onClick: () => setOpen(true), children: multiple ? (values.length ? (_jsxs("span", { className: "flex flex-1 flex-wrap gap-1", children: [values.slice(0, CHIPS).map((v) => (_jsx("span", { className: "rounded-sm bg-muted px-1.5 text-xs leading-[calc(--spacing(5.25))] font-medium", children: labelOf(v) }, v))), values.length > CHIPS && (_jsxs("span", { className: "rounded-sm bg-muted px-1.5 text-xs leading-[calc(--spacing(5.25))]", children: ["+", values.length - CHIPS] }))] })) : (_jsx("span", { className: "flex-1 text-left text-muted-foreground", children: placeholder }))) : (fieldInside) }));
|
|
254
|
+
return (_jsxs(_Fragment, { children: [opener, _jsx(Sheet, { open: open, onOpenChange: setOpen, children: _jsxs(SheetContent, { side: "bottom", showCloseButton: false, "aria-labelledby": sheetTitle,
|
|
255
|
+
// No ring on the first button: the person taps what they want.
|
|
256
|
+
onOpenAutoFocus: (e) => e.preventDefault(), className: "group/selector flex max-h-[85dvh] flex-col gap-2 rounded-t-xl pt-2", children: [_jsx("span", { "aria-hidden": true, className: "mx-auto h-1 w-9 rounded-full bg-border" }), _jsxs("div", { className: "flex items-center justify-between px-4", children: [_jsx(SheetTitle, { id: sheetTitle, children: label ?? placeholder }), multiple && (_jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Done" }))] }), _jsx(SheetDescription, { className: "sr-only", children: multiple ? 'Pick any, then Done' : 'Pick one' }), _jsxs(Base.Root, { ...rootProps, inline: true, open: true, onOpenChange: () => undefined, multiple: multiple, value: multiple ? [...values] : (values[0] ?? null), onValueChange: (v) => pick(v), children: [multiple && values.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5 px-4", children: values.map((v) => (_jsx("span", { className: "rounded-sm bg-muted px-2 py-1 text-[13px]", children: labelOf(v) }, v))) })), searchable && searchBox, list] }), multiple && (_jsxs("div", { className: "flex items-center justify-between border-t px-4 pt-3 pb-5", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: [values.length, " picked"] }), clearAll] }))] }) })] }));
|
|
257
|
+
}
|
|
258
|
+
const popup = (_jsx(Base.Portal, { container: host ?? undefined, children: _jsx(Base.Positioner, { side: "bottom", sideOffset: 4, align: align, anchor: inChips ? chipsRef : undefined, className: "pointer-events-auto isolate z-[60]", children: _jsxs(Base.Popup, { "data-slot": "lumen-selector", className: "group/selector max-w-(--available-width) min-w-[max(var(--anchor-width),14rem)] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", children: [!inChips && searchBox, list, clearAll && (_jsxs("div", { className: "flex items-center justify-between border-t px-2 py-1.5", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: [values.length, " picked", max !== undefined ? ` of ${max}` : ''] }), clearAll] }))] }) }) }));
|
|
259
|
+
return (_jsxs(Base.Root, { ...rootProps, multiple: multiple, value: multiple ? [...values] : (values[0] ?? null), onValueChange: (v) => pick(v), children: [inChips ? (_jsxs(Base.Chips, { ref: chipsRef, className: cn('flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent px-1 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 dark:bg-input/30', className), children: [chips, _jsx(Base.Input, { id: id, "aria-label": label, disabled: disabled, readOnly: !searchable, placeholder: values.length ? '' : placeholder, className: "min-w-16 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground" })] })) : trigger === 'icon' ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Base.Trigger, { render: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": label, className: className }), children: LeadIcon && _jsx(LeadIcon, {}) }) }), _jsx(TooltipContent, { children: label })] })) : (_jsx(Base.Trigger, { id: id, "data-size": size, "aria-label": label, className: cn(FIELD, trigger === 'chip' && 'rounded-full', className), children: fieldInside })), _jsx("span", { ref: markRef, hidden: true }), popup] }));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `trigger="none"`: the list anchored to a text box the page owns (the assistant @ and /). The text box
|
|
263
|
+
* passes the query and its keys (`handle.key`); Tab or Enter takes the highlighted row, the first
|
|
264
|
+
* by default.
|
|
265
|
+
*/
|
|
266
|
+
export function LumenSelectorList({ options, groups, query, onPick, onClose, empty, header, className, handle, label, fuzzy, }) {
|
|
267
|
+
const none = useMemo(() => new Set(), []);
|
|
268
|
+
const { shown, flat, expand } = useSelectorRows(options, groups, query, none, fuzzy);
|
|
269
|
+
const [at, setAt] = useState(0);
|
|
270
|
+
const listId = useId();
|
|
271
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: a new query starts at the first row.
|
|
272
|
+
useEffect(() => setAt(0), [query]);
|
|
273
|
+
const index = Math.min(at, flat.length - 1);
|
|
274
|
+
const active = flat[index];
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
spotlight(active?.target, active?.targetName ?? active?.label);
|
|
277
|
+
}, [active]);
|
|
278
|
+
useEffect(() => () => spotlight(null), []);
|
|
279
|
+
useImperativeHandle(handle, () => ({
|
|
280
|
+
key: (e) => {
|
|
281
|
+
if (!flat.length)
|
|
282
|
+
return false;
|
|
283
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
const step = e.key === 'ArrowDown' ? 1 : -1;
|
|
286
|
+
setAt((i) => (Math.min(i, flat.length - 1) + step + flat.length) % flat.length);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if ((e.key === 'Enter' && !e.shiftKey) || e.key === 'Tab') {
|
|
290
|
+
if (!active || active.disabled)
|
|
291
|
+
return false;
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
onPick(active);
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if (e.key === 'Escape' && onClose) {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
onClose();
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
},
|
|
303
|
+
}), [flat, active, onPick, onClose]);
|
|
304
|
+
return (_jsxs("div", { "data-slot": "lumen-selector-list", className: cn('flex max-h-80 flex-col overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10', className), children: [header, _jsxs("div", { id: listId, "aria-label": label, className: "min-h-0 overflow-y-auto overscroll-contain p-1", children: [!flat.length && (_jsx("p", { className: "px-2 py-3 text-center text-sm text-muted-foreground", children: empty ?? 'Nothing matches' })), shown.map((g) => (_jsxs("section", { "aria-label": g.label || undefined, children: [g.label && _jsx("div", { className: GROUP_LABEL, children: g.label }), g.rows.map((o) => {
|
|
305
|
+
const i = flat.indexOf(o);
|
|
306
|
+
return (_jsx("button", { type: "button", tabIndex: -1, "aria-current": i === index || undefined, disabled: !!o.disabled, "data-highlighted": i === index ? '' : undefined, className: cn(ROW, 'text-left'), onMouseMove: () => i !== index && setAt(i), onMouseDown: (e) => e.preventDefault(), onClick: () => !o.disabled && onPick(o), children: _jsx(SelectorRowContent, { option: o, trailing: i === 0 ? (_jsx("kbd", { className: "rounded border px-1 font-mono text-[11px] text-muted-foreground", children: "Tab" })) : undefined }) }, o.value));
|
|
307
|
+
}), g.more > 0 && (_jsxs("button", { type: "button", className: MORE, onMouseDown: (e) => e.preventDefault(), onClick: () => expand(g.label), children: [_jsx(ChevronDownIcon, { className: "size-3.5" }), g.more, " more ", g.label.toLowerCase()] }))] }, g.label || '\u0000')))] })] }));
|
|
308
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type QueryKey, type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
/** One component's loading state, from `useLumenSource` or `useLumenLoad`. */
|
|
4
|
+
export interface LumenLoad {
|
|
5
|
+
/** Nothing to show yet: the first load runs, or waits until the card scrolls into view. */
|
|
6
|
+
loading: boolean;
|
|
7
|
+
/** A request is in flight (first load or refresh). */
|
|
8
|
+
fetching: boolean;
|
|
9
|
+
/** A request is in flight while data is on screen. */
|
|
10
|
+
refreshing: boolean;
|
|
11
|
+
/** A background refresh (a poll, refetch on focus) is in flight: silent unless a view shows it. */
|
|
12
|
+
background: boolean;
|
|
13
|
+
/** The load failed and there is nothing to show. */
|
|
14
|
+
error: Error | null;
|
|
15
|
+
/** A refresh failed; the older data stays on screen. */
|
|
16
|
+
stale: Error | null;
|
|
17
|
+
/** Reloads this component's source only, skipping the server cache. */
|
|
18
|
+
refresh: () => Promise<void>;
|
|
19
|
+
/** Reads the source again quietly, through the server cache (a live view's poll). */
|
|
20
|
+
poll?: () => void;
|
|
21
|
+
/** The last load, request start to data, in ms; null before the first. */
|
|
22
|
+
tookMs: number | null;
|
|
23
|
+
/** The data on screen came from this browser's cache, with no request. */
|
|
24
|
+
cached: boolean;
|
|
25
|
+
/** When the data on screen arrived (ms since epoch); null before the first. */
|
|
26
|
+
at: number | null;
|
|
27
|
+
/**
|
|
28
|
+
* When the source itself was last updated, apart from when this browser loaded it (the assistant
|
|
29
|
+
* frame's last run of its script): the card's header shows it as a signal dot, its time and why
|
|
30
|
+
* its last update failed on hover. `live` sources are fresh for 5 minutes,
|
|
31
|
+
* others for the day; `fixed` ones never go stale; `about` is one line on what the source is.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Opens the code that makes this source (the assistant frame's script) to read and edit; the signal dot's
|
|
35
|
+
* popover and the card's ⋯ menu offer it.
|
|
36
|
+
*/
|
|
37
|
+
editScript?: () => void;
|
|
38
|
+
updated?: {
|
|
39
|
+
at: number;
|
|
40
|
+
error?: string | null;
|
|
41
|
+
kind?: 'live' | 'asof' | 'fixed';
|
|
42
|
+
about?: string;
|
|
43
|
+
};
|
|
44
|
+
/** What the card's ↻ does, when it is more than a reload (the assistant frame runs its script again). */
|
|
45
|
+
refreshLabel?: string;
|
|
46
|
+
}
|
|
47
|
+
/** What `useLumenLoad` reads of a TanStack query. */
|
|
48
|
+
type QueryState = Pick<UseQueryResult<unknown, unknown>, 'data' | 'error' | 'isError' | 'isFetching' | 'dataUpdatedAt' | 'refetch'> & Partial<Pick<UseQueryResult<unknown, unknown>, 'isPlaceholderData'>>;
|
|
49
|
+
/** The loading state of queries a page already runs (rows built from several reads). */
|
|
50
|
+
export declare function useLumenLoad(...queries: QueryState[]): LumenLoad;
|
|
51
|
+
/**
|
|
52
|
+
* Cards that draw one shared source (a view and the widgets over its rows). A card's ↻ still reloads
|
|
53
|
+
* that source, since every card draws the same rows, but only the card that asked spins, dims and
|
|
54
|
+
* shows the new load time; the others keep still with their own last time and take the new rows
|
|
55
|
+
* quietly. A reload no card asked for (the page's Refresh, a refetch) shows on every card. Returns a
|
|
56
|
+
* function giving each card its own load.
|
|
57
|
+
*/
|
|
58
|
+
export declare function useCardLoads(): (id: string, load: LumenLoad | undefined) => LumenLoad | undefined;
|
|
59
|
+
/** Runs one source. `lazy` holds the query until the card (`ref`) scrolls into view. */
|
|
60
|
+
export declare function useLumenSource<TQueryFnData, TError, TData, TKey extends QueryKey>(options: UseQueryOptions<TQueryFnData, TError, TData, TKey>, { lazy }?: {
|
|
61
|
+
lazy?: boolean;
|
|
62
|
+
}): {
|
|
63
|
+
data: TData | undefined;
|
|
64
|
+
load: LumenLoad;
|
|
65
|
+
ref: (node?: Element | null) => void;
|
|
66
|
+
};
|
|
67
|
+
/** Mounts `children` once they scroll into view: cards whose data is their own (live market). */
|
|
68
|
+
export declare function LumenLazy({ children, fallback, }: {
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
fallback?: ReactNode;
|
|
71
|
+
}): import("react").JSX.Element;
|
|
72
|
+
/** The thin indeterminate bar along the top of a view while its query runs. */
|
|
73
|
+
export declare function LumenProgress({ load }: {
|
|
74
|
+
load: LumenLoad;
|
|
75
|
+
}): import("react").JSX.Element | null;
|
|
76
|
+
/** ↻ for one card or view: reloads its source past the server cache, spinning while it runs. */
|
|
77
|
+
export declare function LumenRefresh({ load, label }: {
|
|
78
|
+
load: LumenLoad;
|
|
79
|
+
label?: string;
|
|
80
|
+
}): import("react").JSX.Element;
|
|
81
|
+
/** The load time line (`1.8 s · 412 rows · 12:41:08 Pacific`), only with Observability details on. */
|
|
82
|
+
export declare function LumenLoadTime({ load, rows, className, }: {
|
|
83
|
+
load: LumenLoad;
|
|
84
|
+
rows?: number;
|
|
85
|
+
className?: string;
|
|
86
|
+
}): import("react").JSX.Element | null;
|
|
87
|
+
/** A failed source, inside its own card, with Retry. */
|
|
88
|
+
export declare function LumenError({ error, onRetry }: {
|
|
89
|
+
error: Error;
|
|
90
|
+
onRetry?: () => void;
|
|
91
|
+
}): import("react").JSX.Element;
|
|
92
|
+
/** Rows of skeleton the height of a table, so nothing jumps when the data lands. */
|
|
93
|
+
export declare function LumenSkeleton({ rows, className }: {
|
|
94
|
+
rows?: number;
|
|
95
|
+
className?: string;
|
|
96
|
+
}): import("react").JSX.Element;
|
|
97
|
+
/** One component's states from its load: bar (views), skeleton, error with Retry, empty, then the
|
|
98
|
+
* content, dimmed while it refreshes. Place it at the top of a `relative` card body. */
|
|
99
|
+
export declare function LumenStates({ load, kind, skeleton, empty, children, }: {
|
|
100
|
+
load: LumenLoad;
|
|
101
|
+
/** Views (tables, grids) carry the progress bar; widgets a skeleton and their spinning ↻. */
|
|
102
|
+
kind?: 'table' | 'widget';
|
|
103
|
+
skeleton?: ReactNode;
|
|
104
|
+
/** Shown in place of the content when the source loaded nothing. */
|
|
105
|
+
empty?: ReactNode;
|
|
106
|
+
children: ReactNode;
|
|
107
|
+
}): import("react").JSX.Element;
|
|
108
|
+
/** A card's footer line with its load time, rows and clock time, only with Observability details on. */
|
|
109
|
+
export declare function LumenLoadFooter({ load, rows, note, }: {
|
|
110
|
+
load: LumenLoad;
|
|
111
|
+
rows?: number;
|
|
112
|
+
/** Said instead of rows, e.g. a chart drawn from its view's rows. */
|
|
113
|
+
note?: string;
|
|
114
|
+
}): import("react").JSX.Element | null;
|
|
115
|
+
/** How often a view showing live columns reads its source again while the market trades. */
|
|
116
|
+
export declare const LIVE_POLL_MS = 60000;
|
|
117
|
+
/**
|
|
118
|
+
* When US stocks trade at all: pre-market, the session, after-hours and the
|
|
119
|
+
* overnight session run from 8:00 PM New York on Sunday to 8:00 PM on Friday (holidays not known here).
|
|
120
|
+
*/
|
|
121
|
+
export declare function marketTrades(at?: number): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* A view's load while it shows live columns (#861): while the market trades and
|
|
124
|
+
* the tab shows, it reads its source again every minute, quietly, and the card's header carries the
|
|
125
|
+
* green signal dot (Live, with when it last updated). Extended hours and overnight poll too (the
|
|
126
|
+
* app's feed decides whether prices move then); over the weekend nothing polls.
|
|
127
|
+
*/
|
|
128
|
+
export declare function useLiveLoad(load: LumenLoad | undefined, live: boolean): LumenLoad | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* The card header's signal dot for a source with its own update time (`load.updated`): green while
|
|
131
|
+
* fresh, amber once stale (worth a ↻), red when its last update failed, grey for data that never
|
|
132
|
+
* updates; hover (a tap on phone) says when, what and why.
|
|
133
|
+
*/
|
|
134
|
+
export declare function LumenFreshness({ load }: {
|
|
135
|
+
load: LumenLoad;
|
|
136
|
+
}): import("react").JSX.Element | null;
|
|
137
|
+
export {};
|