@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,138 @@
|
|
|
1
|
+
// Hover shows the target, or point at the page. An option that stands for something on screen names
|
|
2
|
+
// it as its `target`: `card:<id>` for a LumenLayout card, or an app path for a sidebar page.
|
|
3
|
+
//
|
|
4
|
+
// spotlight(target, name) the page's other cards dim and a name tag sits above the card; a
|
|
5
|
+
// sidebar page gets the sidebar's muted fill. No border, no glow.
|
|
6
|
+
// pointAtCard() the cards dim, the one under the cursor lights up; a click picks it,
|
|
7
|
+
// Esc cancels. Works on any LumenLayout card with no per-page setup.
|
|
8
|
+
//
|
|
9
|
+
// Both drive the DOM directly through data attributes the theme styles: one spotlight at a time.
|
|
10
|
+
import { WIDGET_STYLES } from '@columenlabs/lumen-ui-types/views';
|
|
11
|
+
const CARD = '[data-slot="lumen-card"][data-card]';
|
|
12
|
+
const cardEl = (id) => document.querySelector(`${CARD.slice(0, -1)}="${CSS.escape(id)}"]`);
|
|
13
|
+
const navEl = (path) => [...document.querySelectorAll('[data-sidebar="sidebar"] a[href]')].find((a) => new URL(a.href).pathname.replace(/\/$/, '').endsWith(path.replace(/\/$/, ''))) ?? null;
|
|
14
|
+
export const cardOf = (el) => ({
|
|
15
|
+
id: el.dataset.card ?? '',
|
|
16
|
+
title: el.dataset.cardTitle ?? '',
|
|
17
|
+
kind: el.dataset.cardKind ?? 'table:table',
|
|
18
|
+
});
|
|
19
|
+
/** The cards on the page now, top to bottom. */
|
|
20
|
+
export const cardsOnScreen = () => [...document.querySelectorAll(CARD)].map(cardOf);
|
|
21
|
+
let tag = null;
|
|
22
|
+
let lit = null;
|
|
23
|
+
let scrollTimer;
|
|
24
|
+
function clear() {
|
|
25
|
+
clearTimeout(scrollTimer);
|
|
26
|
+
if (lit) {
|
|
27
|
+
delete lit.dataset.spotlit;
|
|
28
|
+
lit = null;
|
|
29
|
+
}
|
|
30
|
+
delete document.body.dataset.spotlight;
|
|
31
|
+
tag?.remove();
|
|
32
|
+
tag = null;
|
|
33
|
+
}
|
|
34
|
+
function light(el, name, nav) {
|
|
35
|
+
if (lit !== el) {
|
|
36
|
+
if (lit)
|
|
37
|
+
delete lit.dataset.spotlit;
|
|
38
|
+
lit = el;
|
|
39
|
+
el.dataset.spotlit = '';
|
|
40
|
+
}
|
|
41
|
+
if (nav) {
|
|
42
|
+
document.body.dataset.spotlight = 'nav';
|
|
43
|
+
tag?.remove();
|
|
44
|
+
tag = null;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
document.body.dataset.spotlight = 'card';
|
|
48
|
+
if (!name)
|
|
49
|
+
return;
|
|
50
|
+
tag ??= document.body.appendChild(document.createElement('div'));
|
|
51
|
+
tag.dataset.slot = 'spotlight-tag';
|
|
52
|
+
tag.textContent = name;
|
|
53
|
+
const r = el.getBoundingClientRect();
|
|
54
|
+
tag.style.left = `${Math.max(8, r.left)}px`;
|
|
55
|
+
tag.style.top = `${Math.max(8, r.top - 28)}px`;
|
|
56
|
+
}
|
|
57
|
+
/** Where a target is on screen now: a card on the page, a sidebar page, or neither. */
|
|
58
|
+
export function onScreen(target) {
|
|
59
|
+
if (!target)
|
|
60
|
+
return null;
|
|
61
|
+
if (target.startsWith('card:'))
|
|
62
|
+
return cardEl(target.slice(5)) ? 'card' : null;
|
|
63
|
+
return navEl(target) ? 'nav' : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Lights one target (`card:<id>` or an app path) and dims the page around it; null clears. The
|
|
67
|
+
* target scrolls into view only after a short hover.
|
|
68
|
+
*/
|
|
69
|
+
export function spotlight(target, name) {
|
|
70
|
+
if (!target)
|
|
71
|
+
return clear();
|
|
72
|
+
const card = target.startsWith('card:');
|
|
73
|
+
const el = card ? cardEl(target.slice(5)) : navEl(target);
|
|
74
|
+
if (!el)
|
|
75
|
+
return clear();
|
|
76
|
+
light(el, name, !card);
|
|
77
|
+
clearTimeout(scrollTimer);
|
|
78
|
+
scrollTimer = setTimeout(() => {
|
|
79
|
+
const r = el.getBoundingClientRect();
|
|
80
|
+
if (r.bottom < 0 || r.top > window.innerHeight)
|
|
81
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
82
|
+
if (card && name)
|
|
83
|
+
light(el, name, false);
|
|
84
|
+
}, 600);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Point at the page: the cards dim and the one under the cursor (or finger) lights up with its
|
|
88
|
+
* name; a click picks it, Esc or a click off any card cancels.
|
|
89
|
+
*/
|
|
90
|
+
export function pointAtCard() {
|
|
91
|
+
clear();
|
|
92
|
+
document.body.dataset.pointing = '';
|
|
93
|
+
return new Promise((done) => {
|
|
94
|
+
const under = (e) => e.target?.closest?.(CARD) ?? null;
|
|
95
|
+
const move = (e) => {
|
|
96
|
+
const el = under(e);
|
|
97
|
+
if (el)
|
|
98
|
+
light(el, `${el.dataset.cardTitle ?? ''} · ${kindName(cardOf(el).kind)}`, false);
|
|
99
|
+
else if (lit) {
|
|
100
|
+
delete lit.dataset.spotlit;
|
|
101
|
+
lit = null;
|
|
102
|
+
tag?.remove();
|
|
103
|
+
tag = null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const end = (picked) => {
|
|
107
|
+
document.removeEventListener('pointermove', move, true);
|
|
108
|
+
document.removeEventListener('click', click, true);
|
|
109
|
+
document.removeEventListener('keydown', key, true);
|
|
110
|
+
delete document.body.dataset.pointing;
|
|
111
|
+
clear();
|
|
112
|
+
done(picked);
|
|
113
|
+
};
|
|
114
|
+
const click = (e) => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
const el = under(e);
|
|
118
|
+
end(el ? cardOf(el) : null);
|
|
119
|
+
};
|
|
120
|
+
const key = (e) => {
|
|
121
|
+
if (e.key !== 'Escape')
|
|
122
|
+
return;
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
end(null);
|
|
126
|
+
};
|
|
127
|
+
document.addEventListener('pointermove', move, true);
|
|
128
|
+
document.addEventListener('click', click, true);
|
|
129
|
+
document.addEventListener('keydown', key, true);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** A card's style (`kind:style`) as a person says it: a table, a donut, a page's own card. */
|
|
133
|
+
export function kindName(styleId) {
|
|
134
|
+
if (styleId === 'page' || styleId.startsWith('page:'))
|
|
135
|
+
return 'card';
|
|
136
|
+
const [kind = '', style] = styleId.split(':');
|
|
137
|
+
return (WIDGET_STYLES.find((w) => w.kind === kind && w.style === style)?.name.toLowerCase() ?? kind);
|
|
138
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface HealthProbe {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Up when true, down when false, not checked yet when null. */
|
|
5
|
+
ok: boolean | null;
|
|
6
|
+
ms?: number | null;
|
|
7
|
+
/** Why it is down, or anything else worth a line. */
|
|
8
|
+
note?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function StatusHealth({ probes, checked, error, }: {
|
|
11
|
+
probes: HealthProbe[];
|
|
12
|
+
/** When the services were last checked, as the app words it. */
|
|
13
|
+
checked?: ReactNode;
|
|
14
|
+
/** The check itself failed (no answer from the Worker). */
|
|
15
|
+
error?: string;
|
|
16
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Service health in the status bar ("consolidate into a clickable or hoverable
|
|
3
|
+
// pop-up"): one chip with the worst state of every service the app checks, opening a list of each
|
|
4
|
+
// service and its latency, on hover with a mouse and on tap with a finger or keyboard.
|
|
5
|
+
import { useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { cn } from './lib/utils.js';
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
|
|
8
|
+
/** How long the list stays after the mouse leaves, so it can travel from chip to list. */
|
|
9
|
+
const HOVER_CLOSE_MS = 150;
|
|
10
|
+
/** Keeps the list off the window's edge on a narrow screen. */
|
|
11
|
+
const COLLISION_PAD = 8;
|
|
12
|
+
const dot = (ok) => cn('size-2 shrink-0 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive');
|
|
13
|
+
/** The chip's word: every service up, the one that is down, or how many are. */
|
|
14
|
+
function summary(probes) {
|
|
15
|
+
const down = probes.filter((p) => p.ok === false);
|
|
16
|
+
if (down.length === 1)
|
|
17
|
+
return { ok: false, text: `${down[0]?.name} down` };
|
|
18
|
+
if (down.length)
|
|
19
|
+
return { ok: false, text: `${down.length} down` };
|
|
20
|
+
if (probes.some((p) => p.ok === null))
|
|
21
|
+
return { ok: null, text: 'Checking' };
|
|
22
|
+
return { ok: true, text: 'All up' };
|
|
23
|
+
}
|
|
24
|
+
export function StatusHealth({ probes, checked, error, }) {
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const timer = useRef(undefined);
|
|
27
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
28
|
+
const hover = (on) => (e) => {
|
|
29
|
+
if (e.pointerType !== 'mouse')
|
|
30
|
+
return;
|
|
31
|
+
clearTimeout(timer.current);
|
|
32
|
+
if (on)
|
|
33
|
+
setOpen(true);
|
|
34
|
+
else
|
|
35
|
+
timer.current = setTimeout(() => setOpen(false), HOVER_CLOSE_MS);
|
|
36
|
+
};
|
|
37
|
+
if (!probes.length && !error)
|
|
38
|
+
return null;
|
|
39
|
+
const s = error ? { ok: false, text: 'Check failed' } : summary(probes);
|
|
40
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { "aria-label": `Services: ${s.text}`, className: "flex h-6 shrink-0 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground", onPointerEnter: hover(true), onPointerLeave: hover(false), children: [_jsx("span", { "aria-hidden": true, className: dot(s.ok) }), "Services", _jsx("span", { className: cn('num', s.ok === false ? 'text-destructive' : 'text-foreground/80'), children: s.text })] }), _jsxs(PopoverContent, { side: "top", align: "end", collisionPadding: COLLISION_PAD, className: "w-auto min-w-56 gap-1.5 text-xs", onPointerEnter: hover(true), onPointerLeave: hover(false), onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsx("ul", { "aria-label": "Services", className: "flex flex-col gap-1", children: probes.map((p) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: dot(p.ok) }), _jsxs("span", { className: "flex-1", children: [p.name, p.note && _jsx("span", { className: "ml-1.5 text-muted-foreground", children: p.note })] }), _jsx("span", { className: cn('num', p.ok === false ? 'text-destructive' : 'text-foreground/80'), children: p.ok === false ? 'down' : p.ms != null ? `${p.ms} ms` : '—' })] }, p.name))) }), error && _jsx("p", { className: "text-destructive", children: error }), checked && (_jsxs("p", { className: "border-t pt-1.5 text-muted-foreground", children: ["Checked ", _jsx("span", { className: "num", children: checked })] }))] })] }));
|
|
41
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Time zone picker: on the right of the status bar, and in Settings. */
|
|
2
|
+
export declare function TimeZoneSelect({ className, }: {
|
|
3
|
+
className?: string;
|
|
4
|
+
/** Kept for callers; the list flips upward by itself where there is no room below. */
|
|
5
|
+
side?: 'top' | 'bottom';
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function StatusTimeZone({ className }: {
|
|
8
|
+
className?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from './lib/utils.js';
|
|
3
|
+
import { LumenSelector } from './lumen-selector.js';
|
|
4
|
+
import { TIME_ZONES, setPref, usePrefs } from './prefs.js';
|
|
5
|
+
/** Time zone picker: on the right of the status bar, and in Settings. */
|
|
6
|
+
export function TimeZoneSelect({ className, }) {
|
|
7
|
+
const { timeZone } = usePrefs();
|
|
8
|
+
return (_jsx(LumenSelector, { label: "Time zone", size: "sm", className: className, options: TIME_ZONES.map((z) => ({ value: z.id, label: z.label })), value: timeZone, onChange: (v) => setPref('timeZone', v) }));
|
|
9
|
+
}
|
|
10
|
+
export function StatusTimeZone({ className }) {
|
|
11
|
+
return (_jsx("span", { className: cn('flex items-center', className), children: _jsx(TimeZoneSelect, { side: "top", className: "h-6 border-0 px-1.5 text-xs shadow-none" }) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type CardStyle, type MeterKind, type ViewTile } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import { type FilterField } from './lumen-filter.js';
|
|
4
|
+
import { type ViewDomain } from './view-model.js';
|
|
5
|
+
/**
|
|
6
|
+
* A LumenCard's number and lines: its frame shows the label, so the body is
|
|
7
|
+
* the main number and its lines; a domain's own card keeps the app's drawing without its frame.
|
|
8
|
+
*/
|
|
9
|
+
export declare function LumenCard<T>({ domain, tile: t, rows, fields, drawn, loading, style, target, meter, bands, }: {
|
|
10
|
+
domain: ViewDomain<T>;
|
|
11
|
+
tile: ViewTile;
|
|
12
|
+
rows: T[];
|
|
13
|
+
fields: FilterField<T>[];
|
|
14
|
+
/** Draws one of the domain's own cards over `rows`. */
|
|
15
|
+
drawn?: (key: string, rows: T[]) => ReactNode;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
/** Its number, or that number against `target` as a bar or a ring. */
|
|
18
|
+
style?: CardStyle;
|
|
19
|
+
target?: number;
|
|
20
|
+
/** What the share of `target` means, which colours the bar or ring. */
|
|
21
|
+
meter?: MeterKind;
|
|
22
|
+
bands?: {
|
|
23
|
+
warn: number;
|
|
24
|
+
bad: number;
|
|
25
|
+
};
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
/** A Summary's cards over its rows; null when it has none to show. */
|
|
28
|
+
export declare function SummaryTiles<T>({ domain, tiles, rows, fields, tile, loading, }: {
|
|
29
|
+
domain: ViewDomain<T>;
|
|
30
|
+
tiles: ViewTile[];
|
|
31
|
+
/** The rows the Summary shows: the view's, narrowed by the Summary's own filter. */
|
|
32
|
+
rows: T[];
|
|
33
|
+
fields: FilterField<T>[];
|
|
34
|
+
/** Draws one of the domain's own cards over `rows`. */
|
|
35
|
+
tile?: (key: string, rows: T[]) => ReactNode;
|
|
36
|
+
/** While the view's rows load, each card holds its place. */
|
|
37
|
+
loading?: boolean;
|
|
38
|
+
}): import("react").JSX.Element | null;
|
|
39
|
+
/** What is wrong with each of the cards: a key, a label or a field; null where nothing. */
|
|
40
|
+
export declare function summaryCardIssues(tiles: ViewTile[], fields: string[], domainTiles: string[]): (string | null)[];
|
|
41
|
+
/**
|
|
42
|
+
* The Summary's cards in Edit widget: a grid to pick and drag, the domain's own cards to add or
|
|
43
|
+
* remove, and the picked card of the view's own: its main number (label, roll-up, field, rows,
|
|
44
|
+
* colour by sign) and its secondary lines.
|
|
45
|
+
*/
|
|
46
|
+
export declare function SummaryCardsEditor<T>({ domain, tiles, onChange, rows, fields, problems, one, }: {
|
|
47
|
+
domain: ViewDomain<T>;
|
|
48
|
+
tiles: ViewTile[];
|
|
49
|
+
onChange: (tiles: ViewTile[]) => void;
|
|
50
|
+
/** One card only (LumenCard): a built-in replaces it and there is no Add card. */
|
|
51
|
+
one?: boolean;
|
|
52
|
+
/** The Summary's rows, for the previews. */
|
|
53
|
+
rows: T[];
|
|
54
|
+
fields: FilterField<T>[];
|
|
55
|
+
problems: (string | null)[];
|
|
56
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MAX_TILE_LINES, isCountAgg, isOwnTile, meterTone, viewAggs, } from '@columenlabs/lumen-ui-types/views';
|
|
3
|
+
// The Summary widget's cards:
|
|
4
|
+
// each card is one main number and any secondary lines. A domain's own cards (Value, Day P&L) are
|
|
5
|
+
// drawn by the app; a card of the view's own rolls one field or formula column up over the rows
|
|
6
|
+
// the Summary shows, narrowed by its own filter, and its lines are other roll-ups, a percent of a
|
|
7
|
+
// roll-up over all the Summary's rows, or plain text. `SummaryTiles` draws them; `SummaryCardsEditor`
|
|
8
|
+
// is the cards part of the Summary's Edit widget. Everything is worked out here from the rows the
|
|
9
|
+
// view already loaded.
|
|
10
|
+
import { DndContext, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
|
|
11
|
+
import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from '@dnd-kit/sortable';
|
|
12
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
13
|
+
import { PlusIcon, Trash2Icon, XIcon } from 'lucide-react';
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import { PolarAngleAxis, RadialBar, RadialBarChart } from 'recharts';
|
|
16
|
+
import { LumenConfirm } from './confirm.js';
|
|
17
|
+
import { IconButton } from './icon-button.js';
|
|
18
|
+
import { cn } from './lib/utils.js';
|
|
19
|
+
import { LumenTokenizedFilter, filterByTokens, useLumenFilter, } from './lumen-filter.js';
|
|
20
|
+
import { LumenSelector } from './lumen-selector.js';
|
|
21
|
+
import { ViewValueCell } from './lumen-view.js';
|
|
22
|
+
import { Tile } from './tile.js';
|
|
23
|
+
import { Badge } from './ui/badge.js';
|
|
24
|
+
import { Button } from './ui/button.js';
|
|
25
|
+
import { ChartContainer } from './ui/chart.js';
|
|
26
|
+
import { Input } from './ui/input.js';
|
|
27
|
+
import { Label } from './ui/label.js';
|
|
28
|
+
import { Progress } from './ui/progress.js';
|
|
29
|
+
import { Skeleton } from './ui/skeleton.js';
|
|
30
|
+
import { Switch } from './ui/switch.js';
|
|
31
|
+
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
32
|
+
import { AGG_LABELS, MAX_TILES, fieldOptions } from './view-formulas.js';
|
|
33
|
+
import { computedOf, rollUp, takesFormulas, } from './view-model.js';
|
|
34
|
+
/** A card's main number and lines over `rows` (the Summary's rows); null when its field is gone. */
|
|
35
|
+
function figuresOf(domain, t, rows, fields) {
|
|
36
|
+
const computed = computedOf(domain, rows, null);
|
|
37
|
+
const field = (k) => domain.fields.find((f) => f.key === k);
|
|
38
|
+
const roll = (f, agg, rs) => rollUp(rs.map((r) => computed.value(f, r)), agg);
|
|
39
|
+
const f = field(t.field);
|
|
40
|
+
if (!f)
|
|
41
|
+
return null;
|
|
42
|
+
const count = isCountAgg(t.agg);
|
|
43
|
+
const value = roll(f, t.agg, filterByTokens(rows, fields, t.filter));
|
|
44
|
+
const main = {
|
|
45
|
+
kind: 'value',
|
|
46
|
+
value,
|
|
47
|
+
format: (count ? 'rounded' : f.format),
|
|
48
|
+
amount: !count && !!f.amount,
|
|
49
|
+
suffix: t.suffix || undefined,
|
|
50
|
+
};
|
|
51
|
+
const lines = t.lines.flatMap((l) => {
|
|
52
|
+
if (l.kind === 'text')
|
|
53
|
+
return [{ kind: 'text', text: l.text }];
|
|
54
|
+
const lf = field(l.field);
|
|
55
|
+
if (!lf)
|
|
56
|
+
return [];
|
|
57
|
+
if (l.kind === 'pct') {
|
|
58
|
+
const of = roll(lf, l.agg, rows);
|
|
59
|
+
const pct = typeof value === 'number' && typeof of === 'number' && of !== 0 ? (value / of) * 100 : null;
|
|
60
|
+
return [
|
|
61
|
+
{ kind: 'value', value: pct, format: 'weight', amount: false, suffix: `of ${lf.label}` },
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
const c = isCountAgg(l.agg);
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
kind: 'value',
|
|
68
|
+
value: roll(lf, l.agg, filterByTokens(rows, fields, l.filter)),
|
|
69
|
+
format: c ? 'rounded' : lf.format,
|
|
70
|
+
amount: !c && !!lf.amount,
|
|
71
|
+
suffix: l.suffix || undefined,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
});
|
|
75
|
+
return { main, lines };
|
|
76
|
+
}
|
|
77
|
+
function FigureText({ f }) {
|
|
78
|
+
if (f.kind === 'text')
|
|
79
|
+
return _jsx(_Fragment, { children: f.text });
|
|
80
|
+
return (_jsxs(_Fragment, { children: [_jsx(ViewValueCell, { value: f.value, format: f.format, amount: f.amount }), f.suffix ? ` ${f.suffix}` : null] }));
|
|
81
|
+
}
|
|
82
|
+
/** A card of the view's own as the page shows it. */
|
|
83
|
+
function OwnTile({ domain, tile: t, rows, fields, }) {
|
|
84
|
+
const figs = useMemo(() => figuresOf(domain, t, rows, fields), [domain, t, rows, fields]);
|
|
85
|
+
if (!figs)
|
|
86
|
+
return null;
|
|
87
|
+
const v = figs.main.value;
|
|
88
|
+
return (_jsx(Tile, { label: t.label, value: _jsx(FigureText, { f: figs.main }), valueClassName: cn(t.sign && typeof v === 'number' && (v > 0 ? 'text-gain' : v < 0 ? 'text-loss' : '')), lines: figs.lines.map((l, i) => (_jsx(FigureText, { f: l }, i))) }));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A LumenCard's number and lines: its frame shows the label, so the body is
|
|
92
|
+
* the main number and its lines; a domain's own card keeps the app's drawing without its frame.
|
|
93
|
+
*/
|
|
94
|
+
export function LumenCard({ domain, tile: t, rows, fields, drawn, loading, style = 'number', target, meter, bands, }) {
|
|
95
|
+
const figs = useMemo(() => (isOwnTile(t) ? figuresOf(domain, t, rows, fields) : null), [domain, t, rows, fields]);
|
|
96
|
+
if (loading)
|
|
97
|
+
return _jsx(Skeleton, { className: "h-7 w-24" });
|
|
98
|
+
if (!isOwnTile(t))
|
|
99
|
+
return (_jsx("div", { className: "[&>[data-slot=card]]:bg-transparent [&>[data-slot=card]]:px-0 [&>[data-slot=card]]:py-0 [&>[data-slot=card]]:ring-0 [&>[data-slot=card]>span:first-child]:hidden", children: drawn?.(t.key, rows) }));
|
|
100
|
+
if (!figs)
|
|
101
|
+
return _jsx("span", { className: "text-xs text-muted-foreground", children: "Its field is gone." });
|
|
102
|
+
const v = figs.main.value;
|
|
103
|
+
const share = style !== 'number' && target && typeof v === 'number'
|
|
104
|
+
? Math.max(0, Math.min(100, (v / target) * 100))
|
|
105
|
+
: null;
|
|
106
|
+
const tone = meterTone(meter, share, bands);
|
|
107
|
+
const of = target ? (_jsx(ViewValueCell, { value: target, format: figs.main.format, amount: figs.main.amount })) : null;
|
|
108
|
+
if (style === 'progress_circle')
|
|
109
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(ProgressRing, { value: share, tone: tone }), _jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "num truncate text-xl leading-7 font-semibold", children: _jsx(FigureText, { f: figs.main }) }), _jsx("span", { className: "num truncate text-xs text-muted-foreground", children: of ? _jsxs(_Fragment, { children: ["of ", of] }) : 'Set a target in Edit widget' })] })] }));
|
|
110
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: cn('num truncate text-2xl leading-8 font-semibold', t.sign && typeof v === 'number' && (v > 0 ? 'text-gain' : v < 0 ? 'text-loss' : '')), children: _jsx(FigureText, { f: figs.main }) }), style === 'progress_bar' && (_jsxs("div", { className: "flex flex-col gap-1 pt-1", children: [_jsx(Progress, { value: share ?? 0, className: cn('h-2', tone && TONE_BAR[tone]), "aria-label": "Progress to target" }), _jsx("span", { className: "num text-xs text-muted-foreground", children: of ? (_jsxs(_Fragment, { children: [share === null ? '—' : `${share.toFixed(0)}%`, " of ", of] })) : ('Set a target in Edit widget') })] })), figs.lines.map((l, i) => (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", children: _jsx(FigureText, { f: l }) }, i)))] }));
|
|
111
|
+
}
|
|
112
|
+
/** A meter's tone on a bar's fill (theme tokens, no glow). */
|
|
113
|
+
const TONE_BAR = {
|
|
114
|
+
success: '[&>[data-slot=progress-indicator]]:bg-success',
|
|
115
|
+
warning: '[&>[data-slot=progress-indicator]]:bg-warning',
|
|
116
|
+
destructive: '[&>[data-slot=progress-indicator]]:bg-destructive',
|
|
117
|
+
};
|
|
118
|
+
/** A number's share of its target as a ring, its percent in the middle (Recharts); a meter's
|
|
119
|
+
* tone colours it, else the first chart colour. */
|
|
120
|
+
function ProgressRing({ value, tone, }) {
|
|
121
|
+
const config = {
|
|
122
|
+
v: { label: 'Progress', color: tone ? `var(--${tone})` : 'var(--chart-1)' },
|
|
123
|
+
};
|
|
124
|
+
return (_jsx(ChartContainer, { config: config, className: "aspect-square size-16 shrink-0", children: _jsxs(RadialBarChart, { data: [{ v: value ?? 0 }], startAngle: 90, endAngle: -270, innerRadius: 24, outerRadius: 32, children: [_jsx(PolarAngleAxis, { type: "number", domain: [0, 100], tick: false }), _jsx(RadialBar, { dataKey: "v", fill: "var(--color-v)", background: { fill: 'var(--muted)' }, cornerRadius: 8, isAnimationActive: false }), _jsx("text", { x: "50%", y: "50%", textAnchor: "middle", dominantBaseline: "middle", className: "num fill-foreground", fontSize: 12, fontWeight: 600, children: value === null ? '—' : `${value.toFixed(0)}%` })] }) }));
|
|
125
|
+
}
|
|
126
|
+
/** A Summary's cards over its rows; null when it has none to show. */
|
|
127
|
+
export function SummaryTiles({ domain, tiles, rows, fields, tile, loading, }) {
|
|
128
|
+
const shown = tiles.filter((t) => isOwnTile(t) || tile);
|
|
129
|
+
if (!shown.length)
|
|
130
|
+
return null;
|
|
131
|
+
const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-fr md:grid-flow-col';
|
|
132
|
+
if (loading)
|
|
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, '[&>*: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
|
+
}
|
|
136
|
+
// ── Edit ───────────────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
const dragOptions = { activationConstraint: { distance: 4 } };
|
|
138
|
+
const isNumber = (f) => f.format !== 'text' && !f.render;
|
|
139
|
+
/** What is wrong with each of the cards: a key, a label or a field; null where nothing. */
|
|
140
|
+
export function summaryCardIssues(tiles, fields, domainTiles) {
|
|
141
|
+
return tiles.map((t, i) => {
|
|
142
|
+
if (!isOwnTile(t))
|
|
143
|
+
return null;
|
|
144
|
+
if (domainTiles.includes(t.key) || tiles.slice(0, i).some((x) => x.key === t.key))
|
|
145
|
+
return `Key ${t.key} is taken`;
|
|
146
|
+
if (!t.label.trim())
|
|
147
|
+
return 'Give it a label';
|
|
148
|
+
if (!fields.includes(t.field))
|
|
149
|
+
return `Pick a field (${t.field || 'none'} is not one)`;
|
|
150
|
+
const line = t.lines.find((l) => l.kind !== 'text' && !fields.includes(l.field));
|
|
151
|
+
if (line)
|
|
152
|
+
return 'Pick a field for each line';
|
|
153
|
+
return null;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function FieldPick({ value, fields, domain, label, onChange, }) {
|
|
157
|
+
return (_jsx(LumenSelector, { label: label, size: "sm", className: "w-full min-w-0", placeholder: "Pick a field", options: fieldOptions(fields, domain), value: value, onChange: onChange }));
|
|
158
|
+
}
|
|
159
|
+
function AggPick({ value, label, onChange, }) {
|
|
160
|
+
return (_jsx(LumenSelector, { label: label, size: "sm", className: "w-full min-w-0", options: viewAggs.map((a) => ({ value: a, label: AGG_LABELS[a] })), value: value, onChange: onChange }));
|
|
161
|
+
}
|
|
162
|
+
/** A card in the editor's grid: its label, its value now and where it comes from. */
|
|
163
|
+
function CardChip({ id, selected, onSelect, label, value, how, problem, domId, }) {
|
|
164
|
+
const s = useSortable({ id });
|
|
165
|
+
return (_jsxs(Button, { ref: s.setNodeRef, id: domId, variant: "outline", style: { transform: CSS.Transform.toString(s.transform), transition: s.transition }, className: cn('h-auto min-h-16 min-w-0 flex-col items-start justify-start gap-0.5 rounded-lg px-2.5 py-2 text-left font-normal whitespace-normal', selected && 'border-primary ring-3 ring-primary/15', problem && 'border-destructive', s.isDragging && 'z-20 shadow-md'), ...s.attributes, ...s.listeners, "aria-pressed": selected, onClick: onSelect, children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: "num w-full truncate text-sm font-semibold", children: value }), _jsx("span", { className: "w-full truncate text-[0.7rem] text-muted-foreground", children: how })] }));
|
|
166
|
+
}
|
|
167
|
+
/** A card's own rows filter, kept in step with the card. */
|
|
168
|
+
function CardFilter({ rows, fields, tokens, onChange, }) {
|
|
169
|
+
const filter = useLumenFilter(rows, fields, { initial: tokens });
|
|
170
|
+
const next = JSON.stringify(filter.tokens);
|
|
171
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reports the box's tokens when they change
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (next !== JSON.stringify(tokens))
|
|
174
|
+
onChange(filter.tokens);
|
|
175
|
+
}, [next]);
|
|
176
|
+
return (_jsx(LumenTokenizedFilter, { filter: filter, label: "Card filter", placeholder: "All the Summary's rows" }));
|
|
177
|
+
}
|
|
178
|
+
const LINE_KINDS = [
|
|
179
|
+
{ value: 'value', label: 'Number' },
|
|
180
|
+
{ value: 'pct', label: '% of' },
|
|
181
|
+
{ value: 'text', label: 'Text' },
|
|
182
|
+
];
|
|
183
|
+
function LineRow({ line, nums, fields, domain, onChange, onRemove, }) {
|
|
184
|
+
const first = nums[0]?.key ?? '';
|
|
185
|
+
const as = (kind) => kind === 'text'
|
|
186
|
+
? { kind, text: 'Note' }
|
|
187
|
+
: kind === 'pct'
|
|
188
|
+
? { kind, field: line.kind !== 'text' ? line.field : first, agg: 'sum' }
|
|
189
|
+
: {
|
|
190
|
+
kind,
|
|
191
|
+
field: line.kind !== 'text' ? line.field : first,
|
|
192
|
+
agg: 'sum',
|
|
193
|
+
filter: [],
|
|
194
|
+
suffix: '',
|
|
195
|
+
};
|
|
196
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(ToggleGroup, { type: "single", size: "sm", variant: "outline", spacing: 0, value: line.kind, onValueChange: (v) => v && onChange(as(v)), "aria-label": "Line kind", children: LINE_KINDS.map((k) => (_jsx(ToggleGroupItem, { value: k.value, className: "px-2 text-xs", children: k.label }, k.value))) }), _jsx(IconButton, { icon: XIcon, label: "Remove line", className: "ml-auto", onClick: onRemove })] }), line.kind === 'text' ? (_jsx(Input, { "aria-label": "Line text", value: line.text, maxLength: 60, onChange: (e) => onChange({ ...line, text: e.target.value }) })) : (_jsxs("div", { className: "grid grid-cols-[repeat(2,minmax(0,1fr))] gap-1.5", children: [_jsx(AggPick, { value: line.agg, label: "Line roll-up", onChange: (agg) => onChange({ ...line, agg }) }), _jsx(FieldPick, { value: line.field, fields: isCountAgg(line.agg) ? fields : nums, domain: domain, label: "Line field", onChange: (field) => onChange({ ...line, field }) }), line.kind === 'value' && (_jsx(Input, { "aria-label": "Words after the number", className: "col-span-2", placeholder: "Words after the number (contracts)", value: line.suffix, maxLength: 30, onChange: (e) => onChange({ ...line, suffix: e.target.value }) }))] }))] }));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The Summary's cards in Edit widget: a grid to pick and drag, the domain's own cards to add or
|
|
200
|
+
* remove, and the picked card of the view's own: its main number (label, roll-up, field, rows,
|
|
201
|
+
* colour by sign) and its secondary lines.
|
|
202
|
+
*/
|
|
203
|
+
export function SummaryCardsEditor({ domain, tiles, onChange, rows, fields, problems, one = false, }) {
|
|
204
|
+
const sensors = useSensors(useSensor(PointerSensor, dragOptions), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
205
|
+
const builtIns = domain.tiles ?? [];
|
|
206
|
+
const [current, setCurrent] = useState(tiles.find(isOwnTile)?.key ?? tiles[0]?.key ?? null);
|
|
207
|
+
const nums = domain.fields.filter(isNumber);
|
|
208
|
+
const pickable = domain.fields.filter((f) => !f.render);
|
|
209
|
+
const fieldOf = (k) => domain.fields.find((f) => f.key === k);
|
|
210
|
+
const i = tiles.findIndex((t) => t.key === current);
|
|
211
|
+
const picked = tiles[i];
|
|
212
|
+
const own = picked && isOwnTile(picked) ? picked : undefined;
|
|
213
|
+
const set = (p) => onChange(tiles.map((t, j) => (j === i && isOwnTile(t) ? { ...t, ...p } : t)));
|
|
214
|
+
const add = () => {
|
|
215
|
+
let n = tiles.length + 1;
|
|
216
|
+
while (tiles.some((t) => t.key === `tile_${n}`) || builtIns.some((w) => w.key === `tile_${n}`))
|
|
217
|
+
n++;
|
|
218
|
+
const key = `tile_${n}`;
|
|
219
|
+
const field = (nums.find((f) => f.amount) ?? nums[0])?.key ?? '';
|
|
220
|
+
onChange([
|
|
221
|
+
...tiles,
|
|
222
|
+
{ key, label: `Card ${n}`, field, agg: 'sum', filter: [], sign: false, lines: [] },
|
|
223
|
+
]);
|
|
224
|
+
setCurrent(key);
|
|
225
|
+
};
|
|
226
|
+
const ownCount = tiles.filter(isOwnTile).length;
|
|
227
|
+
const off = builtIns.filter((w) => !tiles.some((t) => t.key === w.key));
|
|
228
|
+
const preview = (t) => {
|
|
229
|
+
if (!isOwnTile(t))
|
|
230
|
+
return { value: 'Built in', how: 'Built in' };
|
|
231
|
+
const figs = figuresOf(domain, t, rows, fields);
|
|
232
|
+
return {
|
|
233
|
+
value: figs ? _jsx(FigureText, { f: figs.main }) : '—',
|
|
234
|
+
how: `${AGG_LABELS[t.agg]} of ${fieldOf(t.field)?.label ?? t.field}`,
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: (e) => {
|
|
238
|
+
const keys = tiles.map((t) => t.key);
|
|
239
|
+
const from = keys.indexOf(String(e.active.id));
|
|
240
|
+
const to = e.over ? keys.indexOf(String(e.over.id)) : -1;
|
|
241
|
+
if (from >= 0 && to >= 0 && from !== to)
|
|
242
|
+
onChange(arrayMove(tiles, from, to));
|
|
243
|
+
}, children: _jsx(SortableContext, { items: tiles.map((t) => t.key), strategy: rectSortingStrategy, children: _jsxs("div", { className: "grid grid-cols-3 gap-1.5", "data-testid": "summary-cards", children: [tiles.map((t, j) => {
|
|
244
|
+
const p = preview(t);
|
|
245
|
+
const label = isOwnTile(t)
|
|
246
|
+
? t.label
|
|
247
|
+
: (builtIns.find((w) => w.key === t.key)?.label ?? t.key);
|
|
248
|
+
return (_jsx(CardChip, { id: t.key, selected: current === t.key, onSelect: () => setCurrent(t.key), label: label, value: p.value, how: p.how, problem: !!problems[j], domId: `summary-card-${j}` }, t.key));
|
|
249
|
+
}), takesFormulas(domain) && !(one && tiles.length) && (_jsxs(Button, { variant: "outline", className: "h-auto min-h-16 border-dashed font-normal text-muted-foreground", disabled: ownCount >= MAX_TILES, onClick: add, children: [_jsx(PlusIcon, {}), " Add card"] }))] }) }) }), off.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Built in:" }), off.map((w) => (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
250
|
+
onChange(one ? [{ key: w.key }] : [...tiles, { key: w.key }]);
|
|
251
|
+
setCurrent(w.key);
|
|
252
|
+
}, children: [_jsx(PlusIcon, {}), " ", w.label] }, w.key)))] })), picked && !own && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border p-3", children: [_jsx(Badge, { variant: "secondary", className: "font-normal", children: "Built in" }), _jsxs("span", { className: "flex-1 text-sm", children: [builtIns.find((w) => w.key === picked.key)?.label ?? picked.key, " works out its own lines."] }), _jsx(IconButton, { icon: Trash2Icon, label: "Remove card", onClick: () => {
|
|
253
|
+
onChange(tiles.filter((_, j) => j !== i));
|
|
254
|
+
setCurrent(null);
|
|
255
|
+
} })] })), own && (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 rounded-lg border p-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex-1 text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Main number" }), _jsx(LumenConfirm, { title: `Remove card ${own.label}?`, description: "It leaves this Summary when you save.", action: "Remove", onConfirm: () => {
|
|
256
|
+
onChange(tiles.filter((_, j) => j !== i));
|
|
257
|
+
setCurrent(null);
|
|
258
|
+
}, children: _jsx(IconButton, { icon: Trash2Icon, label: "Remove card", variant: "destructive" }) })] }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1.5", children: [_jsx(Label, { htmlFor: "card-label", children: "Label" }), _jsx(Input, { id: "card-label", value: own.label, maxLength: 40, onChange: (e) => set({ label: e.target.value }) })] }), _jsxs("div", { className: "grid grid-cols-[repeat(2,minmax(0,1fr))] gap-1.5", children: [_jsx(AggPick, { value: own.agg, label: "Roll-up", onChange: (agg) => set({ agg }) }), _jsx(FieldPick, { value: own.field, fields: isCountAgg(own.agg) ? pickable : nums, domain: domain, label: "Field", onChange: (field) => set({ field }) }), _jsx(Input, { "aria-label": "Words after the number", className: "col-span-2", placeholder: "Words after the number (places)", value: own.suffix ?? '', maxLength: 30, onChange: (e) => set({ suffix: e.target.value }) })] }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1.5", children: [_jsx(Label, { children: "Only rows where" }), _jsx(CardFilter, { rows: rows, fields: fields, tokens: own.filter, onChange: (filter) => set({ filter }) }, own.key)] }), _jsxs(Label, { className: "flex items-center gap-2 font-normal", children: [_jsx(Switch, { checked: own.sign, onCheckedChange: (sign) => set({ sign }) }), "Green or red by sign"] }), _jsx("span", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Secondary lines" }), own.lines.map((l, j) => (_jsx(LineRow
|
|
259
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: lines have no key of their own
|
|
260
|
+
, { line: l, nums: nums, fields: pickable, domain: domain, onChange: (next) => set({ lines: own.lines.map((x, k) => (k === j ? next : x)) }), onRemove: () => set({ lines: own.lines.filter((_, k) => k !== j) }) }, j))), _jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", disabled: own.lines.length >= MAX_TILE_LINES, onClick: () => set({
|
|
261
|
+
lines: [
|
|
262
|
+
...own.lines,
|
|
263
|
+
{
|
|
264
|
+
kind: 'pct',
|
|
265
|
+
field: (nums.find((f) => f.amount) ?? nums[0])?.key ?? '',
|
|
266
|
+
agg: 'sum',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}), children: [_jsx(PlusIcon, {}), " Add line"] }), _jsx("div", { className: "rounded-lg bg-muted/50 p-2", children: _jsx(OwnTile, { domain: domain, tile: own, rows: rows, fields: fields }) }), problems[i] && (_jsx("p", { role: "alert", className: "text-xs text-destructive", children: problems[i] }))] }))] }));
|
|
270
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Opens the Symbol sheet (symbol-sheet.tsx); its own file so the views can read it. */
|
|
2
|
+
export declare const SheetContext: import("react").Context<((symbol: string) => void) | null>;
|
|
3
|
+
/** Opens the Symbol sheet; null where the app mounted none (a symbol then opens its chart). */
|
|
4
|
+
export declare const useSymbolSheet: () => ((symbol: string) => void) | null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
/** Opens the Symbol sheet (symbol-sheet.tsx); its own file so the views can read it. */
|
|
3
|
+
export const SheetContext = createContext(null);
|
|
4
|
+
/** Opens the Symbol sheet; null where the app mounted none (a symbol then opens its chart). */
|
|
5
|
+
export const useSymbolSheet = () => useContext(SheetContext);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Mounts the Symbol sheet once for an app. `sections` draws the app's own sections for a symbol
|
|
4
|
+
* (DrawerSections), under the shell's chart.
|
|
5
|
+
*/
|
|
6
|
+
export declare function SymbolSheetProvider({ sections, children, }: {
|
|
7
|
+
sections?: (symbol: string) => ReactNode;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The Symbol sheet (the assistant drafts step 1): a tap on any symbol in any view opens one sheet
|
|
3
|
+
// about it, the same drawer as a row's details (bottom on phone, right on desktop). The shell draws
|
|
4
|
+
// its header and LumenChart; the app adds its own sections
|
|
5
|
+
// (an app: live price, P/E, estimates, earnings, your holdings by account, its screeners). A
|
|
6
|
+
// app that mounts no sheet keeps opening the chart dialog straight away.
|
|
7
|
+
import { LineChartIcon } from 'lucide-react';
|
|
8
|
+
import { useCallback, useState } from 'react';
|
|
9
|
+
import { DrawerSection } from './drawer-section.js';
|
|
10
|
+
import { LumenDrawer } from './lumen-drawer.js';
|
|
11
|
+
import { LumenChart } from './market/lumen-chart.js';
|
|
12
|
+
import { SheetContext } from './symbol-sheet-context.js';
|
|
13
|
+
/**
|
|
14
|
+
* Mounts the Symbol sheet once for an app. `sections` draws the app's own sections for a symbol
|
|
15
|
+
* (DrawerSections), under the shell's chart.
|
|
16
|
+
*/
|
|
17
|
+
export function SymbolSheetProvider({ sections, children, }) {
|
|
18
|
+
const [symbol, setSymbol] = useState(null);
|
|
19
|
+
const open = useCallback((s) => setSymbol(s.toUpperCase()), []);
|
|
20
|
+
return (_jsxs(SheetContext.Provider, { value: open, children: [children, _jsx(LumenDrawer, { open: symbol !== null, title: symbol ?? '', onClose: () => setSymbol(null), children: symbol && _jsx(SymbolSheetBody, { symbol: symbol, sections: sections }, symbol) })] }));
|
|
21
|
+
}
|
|
22
|
+
function SymbolSheetBody({ symbol, sections, }) {
|
|
23
|
+
return (_jsxs(_Fragment, { children: [_jsx(DrawerSection, { icon: LineChartIcon, title: "Price", defaultOpen: true, children: _jsx(LumenChart, { symbols: [symbol], window: "1Y", type: "line", indicators: [] }) }), sections?.(symbol)] }));
|
|
24
|
+
}
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Task, TaskList } from '@columenlabs/lumen-ui-types/tasks';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
export interface TasksConfig {
|
|
4
|
+
/** The app's Tasks API base, e.g. `/api/tasks`. */
|
|
5
|
+
api: string;
|
|
6
|
+
/** Each app's short name by its id, for a product whose tasks come from several apps: each row
|
|
7
|
+
* then names its app. Left out, rows name none. */
|
|
8
|
+
apps?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
interface TasksState {
|
|
11
|
+
list: TaskList | null;
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
cancel: (id: string) => Promise<void>;
|
|
14
|
+
seen: () => Promise<void>;
|
|
15
|
+
/** Deletes one ended task, or every ended one; open tasks stay. */
|
|
16
|
+
clear: (id?: string) => Promise<void>;
|
|
17
|
+
open: (t: Task) => void;
|
|
18
|
+
/** `TasksConfig.apps`. */
|
|
19
|
+
apps?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/** The Tasks list, or null where the app has none; `refresh()` after starting one shows it at once. */
|
|
22
|
+
export declare function useTasks(): TasksState | null;
|
|
23
|
+
export declare function TasksProvider({ config, children }: {
|
|
24
|
+
config?: TasksConfig;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
|
|
27
|
+
/** The header button: a ring while anything runs, the count, a gold dot for ended unseen tasks. */
|
|
28
|
+
export declare function TasksButton(): import("react").JSX.Element | null;
|
|
29
|
+
export {};
|