@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,316 @@
|
|
|
1
|
+
import { CONTAINS_KEY, whyBlank, } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
import { formulaFields } from './view-formulas.js';
|
|
3
|
+
/** A chip's classes per tone, on the theme's tokens so light and dark both read. */
|
|
4
|
+
export const CHIP_TONE = {
|
|
5
|
+
gain: 'border-transparent bg-gain/15 text-gain',
|
|
6
|
+
loss: 'border-transparent bg-loss/15 text-loss',
|
|
7
|
+
info: 'border-transparent bg-info/15 text-info',
|
|
8
|
+
muted: 'border-transparent bg-muted text-muted-foreground',
|
|
9
|
+
// Two neutral kinds side by side (backfill and incremental jobs), on the chart palette.
|
|
10
|
+
sky: 'border-transparent bg-chart-6/15 text-chart-6',
|
|
11
|
+
amber: 'border-transparent bg-chart-3/15 text-chart-3',
|
|
12
|
+
};
|
|
13
|
+
/** A run's state as a chip, the same on every app: done green, failed
|
|
14
|
+
* red, running blue, queued, waiting and canceled grey. */
|
|
15
|
+
export const RUN_STATE_TONE = {
|
|
16
|
+
done: 'gain',
|
|
17
|
+
failed: 'loss',
|
|
18
|
+
running: 'info',
|
|
19
|
+
queued: 'muted',
|
|
20
|
+
waiting: 'muted',
|
|
21
|
+
canceled: 'muted',
|
|
22
|
+
};
|
|
23
|
+
/** The formulas group: a view's own formula columns. */
|
|
24
|
+
export const FORMULAS_GROUP = '_formulas';
|
|
25
|
+
/** Whether a Summary counts the domain's cash rows: its own choice, else only when it covers
|
|
26
|
+
* every row (no filter of its own, no linked chips). */
|
|
27
|
+
export const summaryCash = (card, narrowed) => card.cash ?? !narrowed;
|
|
28
|
+
/**
|
|
29
|
+
* What a row can open: the domain's own sub-groups, else every groupable field as a second level
|
|
30
|
+
* of grouping (the assistant frame tab groups by class, then by unit). The field's
|
|
31
|
+
* rows sharing a value fold into one rolled-up line that opens them.
|
|
32
|
+
*/
|
|
33
|
+
/** The picker's name: a domain's own sub-groups open rows, field ones group a second level. */
|
|
34
|
+
export function subgroupLabel(domain) {
|
|
35
|
+
return domain.subgroups ? 'Each row opens' : 'Sub-group';
|
|
36
|
+
}
|
|
37
|
+
export function subgroupsOf(domain) {
|
|
38
|
+
if (domain.subgroups)
|
|
39
|
+
return domain.subgroups;
|
|
40
|
+
return domain.fields
|
|
41
|
+
.filter((f) => f.groupable)
|
|
42
|
+
.map((f) => ({
|
|
43
|
+
key: f.key,
|
|
44
|
+
label: f.label,
|
|
45
|
+
icon: f.icon,
|
|
46
|
+
under: (row) => {
|
|
47
|
+
const v = f.value(row);
|
|
48
|
+
return v === null || v === '' ? '—' : String(v);
|
|
49
|
+
},
|
|
50
|
+
always: true,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** The sub-group a view opens: its own pick, else the domain's first; null for none. */
|
|
54
|
+
export function subgroupOf(domain, key) {
|
|
55
|
+
if (key === null)
|
|
56
|
+
return null;
|
|
57
|
+
// A view that says nothing opens the domain's first own sub-group; field ones only when picked.
|
|
58
|
+
if (key === undefined)
|
|
59
|
+
return domain.subgroups?.[0] ?? null;
|
|
60
|
+
return subgroupsOf(domain).find((s) => s.key === key) ?? null;
|
|
61
|
+
}
|
|
62
|
+
/** Rolls values up; nulls are skipped and an empty set is null (count is 0). */
|
|
63
|
+
export function rollUp(values, agg) {
|
|
64
|
+
const have = values.filter((v) => v !== null);
|
|
65
|
+
if (agg === 'count')
|
|
66
|
+
return have.length;
|
|
67
|
+
if (agg === 'distinct')
|
|
68
|
+
return new Set(have.map(String)).size;
|
|
69
|
+
if (!have.length)
|
|
70
|
+
return null;
|
|
71
|
+
if (agg === 'first')
|
|
72
|
+
return have[0] ?? null;
|
|
73
|
+
if (agg === 'last')
|
|
74
|
+
return have[have.length - 1] ?? null;
|
|
75
|
+
const xs = have.filter((v) => typeof v === 'number');
|
|
76
|
+
if (!xs.length)
|
|
77
|
+
return null;
|
|
78
|
+
if (agg === 'sum')
|
|
79
|
+
return xs.reduce((a, b) => a + b, 0);
|
|
80
|
+
if (agg === 'mean')
|
|
81
|
+
return xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
82
|
+
if (agg === 'min')
|
|
83
|
+
return Math.min(...xs);
|
|
84
|
+
return Math.max(...xs);
|
|
85
|
+
}
|
|
86
|
+
/** A view's chip fields and where each shows; views saved before chips existed have none. */
|
|
87
|
+
export const chipsOf = (c) => c.chips ?? [];
|
|
88
|
+
/** Whether views over the domain may add formula columns: every domain but one that opts out. */
|
|
89
|
+
export const takesFormulas = (domain) => domain.formulas !== false;
|
|
90
|
+
/** A view's formula columns; views saved before formulas existed have none. */
|
|
91
|
+
export const formulasOf = (c) => c.formulas ?? [];
|
|
92
|
+
/** Fields listed under Columns but not shown, for their Filter and Group ticks; views saved before it have none. */
|
|
93
|
+
export const hiddenOf = (c) => c.hidden ?? [];
|
|
94
|
+
/** A view's own tiles; views saved before tiles existed have none. */
|
|
95
|
+
export const tilesOf = (c) => c.tiles ?? [];
|
|
96
|
+
/**
|
|
97
|
+
* The fields a view shows: the domain's plus its formula columns (where the domain takes formulas),
|
|
98
|
+
* each number field in the format the view picked for it (`formats`).
|
|
99
|
+
*/
|
|
100
|
+
export function withViewFields(domain, c) {
|
|
101
|
+
const own = takesFormulas(domain) && formulasOf(c).length ? formulaFields(formulasOf(c)) : [];
|
|
102
|
+
const picked = c.formats ?? {};
|
|
103
|
+
const aggs = c.aggs ?? {};
|
|
104
|
+
if (!own.length && !Object.keys(picked).length && !Object.keys(aggs).length)
|
|
105
|
+
return domain;
|
|
106
|
+
const mine = new Set(own.map((f) => f.key));
|
|
107
|
+
// A text formula groups like any text field: each row's value is the formula
|
|
108
|
+
// over that row alone. One that reads group_sum or total_sum depends on the rows shown, so it
|
|
109
|
+
// cannot pick its own group.
|
|
110
|
+
const rowValue = (f) => (r) => computedOf(full, [r], null).value(f, r);
|
|
111
|
+
const fields = [...domain.fields, ...own].map((f) => {
|
|
112
|
+
if (f.format === 'text') {
|
|
113
|
+
if (!mine.has(f.key) || !f.formula)
|
|
114
|
+
return f;
|
|
115
|
+
if (/\b(group_sum|total_sum)\s*\(/.test(f.formula))
|
|
116
|
+
return { ...f, groupNote: 'Reads group_sum or total_sum, so it changes with the group.' };
|
|
117
|
+
const g = { ...f, groupable: true };
|
|
118
|
+
g.value = rowValue(g);
|
|
119
|
+
return g;
|
|
120
|
+
}
|
|
121
|
+
const format = picked[f.key];
|
|
122
|
+
const agg = mine.has(f.key) ? undefined : aggs[f.key];
|
|
123
|
+
const g = format ? { ...f, format } : f;
|
|
124
|
+
if (!agg)
|
|
125
|
+
return g;
|
|
126
|
+
return agg === 'none'
|
|
127
|
+
? { ...g, noRollup: true }
|
|
128
|
+
: { ...g, aggregate: agg, rollFormula: false, noRollup: false };
|
|
129
|
+
});
|
|
130
|
+
const full = { ...domain, fields };
|
|
131
|
+
return full;
|
|
132
|
+
}
|
|
133
|
+
/** Whether group and Total rows show a value for the field. */
|
|
134
|
+
export const rollsUp = (f) => !f.noRollup && (!!f.aggregate || !!f.rollFormula || f.share?.within === 'total');
|
|
135
|
+
/** A number field's Total pick as Edit view shows it: the view's, else the field's own. */
|
|
136
|
+
export function totalAggOf(f) {
|
|
137
|
+
if (f.noRollup)
|
|
138
|
+
return 'none';
|
|
139
|
+
if (f.aggregate)
|
|
140
|
+
return f.aggregate;
|
|
141
|
+
return f.rollFormula || f.share ? 'own' : 'none';
|
|
142
|
+
}
|
|
143
|
+
export const pick = (fields, keys) => keys.length
|
|
144
|
+
? keys.map((k) => fields.find((f) => f.key === k)).filter((f) => !!f)
|
|
145
|
+
: fields;
|
|
146
|
+
/** A field the filter box can offer: one marked filterable, or any number field. */
|
|
147
|
+
export const canFilter = (f) => !!f.filterable || f.format !== 'text';
|
|
148
|
+
/** The fields a view's filter box offers: the ones it lists, else every field marked filterable. */
|
|
149
|
+
export const filterFieldsOf = (fields, keys) => keys.length ? pick(fields.filter(canFilter), keys) : fields.filter((f) => f.filterable);
|
|
150
|
+
/** A view's filters with `tokens` as the ones it opens with, offering every field they use. */
|
|
151
|
+
export function withSavedFilters(fields, filters, tokens) {
|
|
152
|
+
const offered = filterFieldsOf(fields, filters.fields).map((f) => f.key);
|
|
153
|
+
const missing = [...new Set(tokens.map((t) => t.key))].filter((k) => k !== CONTAINS_KEY && !offered.includes(k));
|
|
154
|
+
return { fields: missing.length ? [...offered, ...missing] : filters.fields, default: tokens };
|
|
155
|
+
}
|
|
156
|
+
export const label = (v) => (v === null ? '—' : String(v));
|
|
157
|
+
/**
|
|
158
|
+
* Values over the shown rows: plain fields as read, share fields and formula columns worked out
|
|
159
|
+
* against the row's group (every shown row when not grouped) and every shown row.
|
|
160
|
+
*/
|
|
161
|
+
export function computedOf(domain, rows, groupBy) {
|
|
162
|
+
const byKey = new Map(domain.fields.map((f) => [f.key, f]));
|
|
163
|
+
// A formula reads fields by name (`symbol.price`, a formula's bare key); sums go by field key.
|
|
164
|
+
const byName = nameIndex(domain);
|
|
165
|
+
// An app's own `compute` may read its fields by bare key (`v('day_pl')`), the assistant's formulas by name.
|
|
166
|
+
const field = (k) => byName.get(k) ?? byKey.get(k);
|
|
167
|
+
const keyOf = (name) => field(name)?.key ?? '';
|
|
168
|
+
const groups = new Map();
|
|
169
|
+
if (groupBy)
|
|
170
|
+
for (const r of rows) {
|
|
171
|
+
const k = label(groupBy.value(r));
|
|
172
|
+
const g = groups.get(k);
|
|
173
|
+
if (g)
|
|
174
|
+
g.push(r);
|
|
175
|
+
else
|
|
176
|
+
groups.set(k, [r]);
|
|
177
|
+
}
|
|
178
|
+
const groupOf = (r) => (groupBy ? (groups.get(label(groupBy.value(r))) ?? []) : rows);
|
|
179
|
+
const sums = new Map();
|
|
180
|
+
const sum = (rs, key) => {
|
|
181
|
+
const m = sums.get(rs) ?? new Map();
|
|
182
|
+
sums.set(rs, m);
|
|
183
|
+
const f = byKey.get(key);
|
|
184
|
+
if (!m.has(key))
|
|
185
|
+
m.set(key, f ? rs.reduce((a, r) => a + num(value(f, r)), 0) : 0);
|
|
186
|
+
return m.get(key) ?? 0;
|
|
187
|
+
};
|
|
188
|
+
const pct = (part, whole) => (whole ? (part / whole) * 100 : null);
|
|
189
|
+
// A formula that reaches itself (a draft) reads null rather than looping.
|
|
190
|
+
const busy = new Set();
|
|
191
|
+
// Each worked-out value once per row: sorting, filtering, roll-ups and cells all read it.
|
|
192
|
+
const seen = new Map();
|
|
193
|
+
function value(f, r) {
|
|
194
|
+
if (!f.share && !f.compute)
|
|
195
|
+
return f.value(r);
|
|
196
|
+
const m = seen.get(f.key) ?? new Map();
|
|
197
|
+
seen.set(f.key, m);
|
|
198
|
+
if (m.has(r))
|
|
199
|
+
return m.get(r) ?? null;
|
|
200
|
+
const v = worked(f, r);
|
|
201
|
+
if (!busy.size)
|
|
202
|
+
m.set(r, v);
|
|
203
|
+
return v;
|
|
204
|
+
}
|
|
205
|
+
function worked(f, r) {
|
|
206
|
+
if (f.share) {
|
|
207
|
+
const base = byKey.get(f.share.of);
|
|
208
|
+
if (!base)
|
|
209
|
+
return null;
|
|
210
|
+
const whole = f.share.within === 'group' ? groupOf(r) : rows;
|
|
211
|
+
return pct(num(value(base, r)), sum(whole, f.share.of));
|
|
212
|
+
}
|
|
213
|
+
if (!f.compute)
|
|
214
|
+
return f.value(r);
|
|
215
|
+
if (busy.has(f.key))
|
|
216
|
+
return null;
|
|
217
|
+
busy.add(f.key);
|
|
218
|
+
try {
|
|
219
|
+
return f.compute(r, {
|
|
220
|
+
value: (k) => {
|
|
221
|
+
const x = field(k);
|
|
222
|
+
return x ? value(x, r) : null;
|
|
223
|
+
},
|
|
224
|
+
groupSum: (k) => sum(groupOf(r), keyOf(k)),
|
|
225
|
+
totalSum: (k) => sum(rows, keyOf(k)),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
busy.delete(f.key);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A set of rows' roll-up: a `total` share is their share of every shown row,
|
|
234
|
+
* an `aggregate` rolls the rows' values, and a formula with none runs over the rows' roll-ups of
|
|
235
|
+
* its inputs (a ratio input is itself recomputed; an input with no roll-up reads null).
|
|
236
|
+
*/
|
|
237
|
+
const rolled = new Map();
|
|
238
|
+
function roll(f, rs) {
|
|
239
|
+
if (f.noRollup)
|
|
240
|
+
return null;
|
|
241
|
+
if (f.aggregate)
|
|
242
|
+
return rollUp(rs.map((r) => value(f, r)), f.aggregate);
|
|
243
|
+
if (f.share)
|
|
244
|
+
return f.share.within === 'total' ? pct(sum(rs, f.share.of), sum(rows, f.share.of)) : null;
|
|
245
|
+
if (!f.rollFormula || !f.compute)
|
|
246
|
+
return null;
|
|
247
|
+
const m = rolled.get(rs) ?? new Map();
|
|
248
|
+
rolled.set(rs, m);
|
|
249
|
+
if (m.has(f.key))
|
|
250
|
+
return m.get(f.key) ?? null;
|
|
251
|
+
if (busy.has(f.key))
|
|
252
|
+
return null;
|
|
253
|
+
busy.add(f.key);
|
|
254
|
+
try {
|
|
255
|
+
const v = f.compute(rs[0], {
|
|
256
|
+
value: (k) => {
|
|
257
|
+
const x = field(k);
|
|
258
|
+
return x ? roll(x, rs) : null;
|
|
259
|
+
},
|
|
260
|
+
groupSum: (k) => sum(rs, keyOf(k)),
|
|
261
|
+
totalSum: (k) => sum(rows, keyOf(k)),
|
|
262
|
+
});
|
|
263
|
+
m.set(f.key, v);
|
|
264
|
+
return v;
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
busy.delete(f.key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { value, roll };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Each formula column that is blank on some of the rows, and why (a blank
|
|
274
|
+
* column said nothing): `blank on 24 of 24 rows: dew_point_f is empty`. The editor shows it under
|
|
275
|
+
* the formula, and the assistant reads it after each card move.
|
|
276
|
+
*/
|
|
277
|
+
export function formulaBlanks(domain, rows) {
|
|
278
|
+
const out = new Map();
|
|
279
|
+
const formulas = domain.fields.filter((f) => f.formula && f.compute);
|
|
280
|
+
if (!formulas.length || !rows.length)
|
|
281
|
+
return out;
|
|
282
|
+
const c = computedOf(domain, rows, null);
|
|
283
|
+
const byName = nameIndex(domain);
|
|
284
|
+
const read = (k, r) => {
|
|
285
|
+
const f = byName.get(k);
|
|
286
|
+
return f ? c.value(f, r) : null;
|
|
287
|
+
};
|
|
288
|
+
const total = (k) => rows.reduce((a, r) => a + num(read(k, r)), 0);
|
|
289
|
+
for (const f of formulas) {
|
|
290
|
+
const blank = rows.filter((r) => {
|
|
291
|
+
const v = c.value(f, r);
|
|
292
|
+
return v === null || v === '';
|
|
293
|
+
});
|
|
294
|
+
const first = blank[0];
|
|
295
|
+
if (first === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
const why = whyBlank(f.formula ?? '', {
|
|
298
|
+
value: (k) => read(k, first),
|
|
299
|
+
groupSum: total,
|
|
300
|
+
totalSum: total,
|
|
301
|
+
});
|
|
302
|
+
out.set(f.key, `blank on ${blank.length} of ${rows.length} rows${why ? `: ${why}` : ''}`);
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
export const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
307
|
+
/** A field's group id: its own, else the domain's first group ('' when the domain has none). */
|
|
308
|
+
export const groupOf = (domain, f) => f.group ?? domain.groups?.[0]?.id ?? '';
|
|
309
|
+
/**
|
|
310
|
+
* A field's name in formulas: the one it was made with (a join's
|
|
311
|
+
* `symbol.price`, a frame's `frame.<frame>.<column>`, a linked domain's `tax_lots.count`), a
|
|
312
|
+
* formula's bare key, else `<domain>.<key>` for the domain's own.
|
|
313
|
+
*/
|
|
314
|
+
export const fieldName = (domain, f) => f.name ?? (groupOf(domain, f) === FORMULAS_GROUP ? f.key : `${domain.id}.${f.key}`);
|
|
315
|
+
/** Each field by its name in formulas. */
|
|
316
|
+
export const nameIndex = (domain) => new Map(domain.fields.map((f) => [fieldName(domain, f), f]));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// What's new (in Lumen for
|
|
3
|
+
// every app): the version in the status bar opens the release notes, newest
|
|
4
|
+
// first, each release's Key takeaways only. An app turns it on with LumenShell's `releases`, its
|
|
5
|
+
// releases API (`GET ?limit=&before=` answering `{releases}`), which leaves out what the person can't
|
|
6
|
+
// open; a host that answers 404 (self-host an app) keeps the version hidden.
|
|
7
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import Markdown from 'react-markdown';
|
|
10
|
+
import { DrawerSection } from './drawer-section.js';
|
|
11
|
+
import { LumenDrawer } from './lumen-drawer.js';
|
|
12
|
+
import { usePrefs } from './prefs.js';
|
|
13
|
+
import { Badge } from './ui/badge.js';
|
|
14
|
+
import { Button } from './ui/button.js';
|
|
15
|
+
/** Each takeaway's type chips, in this order, in the shell's status tones. */
|
|
16
|
+
const TYPES = {
|
|
17
|
+
feature: { label: 'Feature', variant: 'info' },
|
|
18
|
+
improvement: { label: 'Improvement', variant: 'success' },
|
|
19
|
+
fix: { label: 'Fix', variant: 'warning' },
|
|
20
|
+
performance: { label: 'Performance', variant: 'secondary' },
|
|
21
|
+
security: { label: 'Security', variant: 'destructive' },
|
|
22
|
+
};
|
|
23
|
+
const ORDER = Object.keys(TYPES);
|
|
24
|
+
const PAGE = 20;
|
|
25
|
+
async function page(api, before) {
|
|
26
|
+
const q = new URLSearchParams({ limit: String(PAGE) });
|
|
27
|
+
if (before)
|
|
28
|
+
q.set('before', before);
|
|
29
|
+
const r = await fetch(`${api}?${q}`, { credentials: 'include' });
|
|
30
|
+
if (r.status === 404)
|
|
31
|
+
return null;
|
|
32
|
+
if (!r.ok)
|
|
33
|
+
throw new Error(`releases ${r.status}`);
|
|
34
|
+
return (await r.json()).releases;
|
|
35
|
+
}
|
|
36
|
+
export function WhatsNew({ api }) {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const { timeZone } = usePrefs();
|
|
39
|
+
const q = useInfiniteQuery({
|
|
40
|
+
queryKey: ['edgeshell', 'releases', api],
|
|
41
|
+
queryFn: ({ pageParam }) => page(api, pageParam),
|
|
42
|
+
initialPageParam: null,
|
|
43
|
+
getNextPageParam: (last) => last && last.length === PAGE ? last.at(-1)?.published_at : undefined,
|
|
44
|
+
staleTime: 30 * 60_000,
|
|
45
|
+
retry: false,
|
|
46
|
+
});
|
|
47
|
+
const releases = q.data?.pages.flatMap((p) => p ?? []) ?? [];
|
|
48
|
+
const latest = releases[0];
|
|
49
|
+
if (!latest)
|
|
50
|
+
return null;
|
|
51
|
+
const day = (at) => new Date(at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone });
|
|
52
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", className: "num h-6 px-1.5 text-muted-foreground text-xs", onClick: () => setOpen(true), title: "What's new", children: latest.tag }), _jsx(LumenDrawer, { open: open, onClose: () => setOpen(false), title: "What's new", description: "Key takeaways of each release, newest first", footerStart: `${releases.length} releases`, footer: _jsxs(_Fragment, { children: [q.hasNextPage && (_jsx(Button, { variant: "outline", size: "sm", disabled: q.isFetchingNextPage, onClick: () => q.fetchNextPage(), children: "Show more" })), _jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Close" })] }), children: releases.map((r, i) => (_jsx(DrawerSection, { title: r.tag, hint: day(r.published_at), defaultOpen: i === 0, children: r.takeaways.length ? (_jsx("ul", { className: "list-disc space-y-1.5 py-1 pr-1 pl-5 text-sm leading-relaxed [&_strong]:font-semibold", children: r.takeaways.map((t) => (_jsxs("li", { children: [t.types.length > 0 && (_jsx("span", { className: "mr-1.5 inline-flex flex-wrap gap-1 align-middle", children: ORDER.filter((k) => t.types.includes(k)).map((k) => (_jsx(Badge, { variant: TYPES[k].variant, className: "px-1.5", children: TYPES[k].label }, k))) })), _jsx(Markdown, { allowedElements: ['strong', 'em', 'code'], unwrapDisallowed: true, children: t.text })] }, t.text))) })) : (_jsx("p", { className: "py-1 pl-1 text-muted-foreground text-sm", children: "Nothing new for you in this one." })) }, r.tag))) })] }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ViewCard } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
type Controls = ViewCard['controls'];
|
|
3
|
+
export declare function WidgetControlsEditor({ kind, style, controls, onChange, }: {
|
|
4
|
+
kind: ViewCard['kind'];
|
|
5
|
+
style?: string;
|
|
6
|
+
controls: Controls;
|
|
7
|
+
onChange: (controls: Controls) => void;
|
|
8
|
+
}): import("react").JSX.Element | null;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Edit widget ▸ Controls: one switch per control the widget's kind has, each starting from the kind's
|
|
3
|
+
// default. A switch back to the default drops the key, so a saved widget keeps only its choices.
|
|
4
|
+
import { CONTROL_DEFAULTS, WIDGET_CONTROL_NAMES, controlOn, viewCardKinds, } from '@columenlabs/lumen-ui-types/views';
|
|
5
|
+
import { useAssistantName } from './assistant-context.js';
|
|
6
|
+
import { Label } from './ui/label.js';
|
|
7
|
+
import { Switch } from './ui/switch.js';
|
|
8
|
+
export function WidgetControlsEditor({ kind, style, controls, onChange, }) {
|
|
9
|
+
const type = viewCardKinds.includes(kind) ? kind : null;
|
|
10
|
+
const assistant = useAssistantName('assistant');
|
|
11
|
+
if (!type)
|
|
12
|
+
return null;
|
|
13
|
+
const keys = Object.keys(CONTROL_DEFAULTS[type]);
|
|
14
|
+
const set = (k, on) => {
|
|
15
|
+
const next = { ...controls };
|
|
16
|
+
if (on === CONTROL_DEFAULTS[type][k])
|
|
17
|
+
delete next[k];
|
|
18
|
+
else
|
|
19
|
+
next[k] = on;
|
|
20
|
+
onChange(Object.keys(next).length ? next : undefined);
|
|
21
|
+
};
|
|
22
|
+
return (_jsx("div", { className: "grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-3", children: keys.map((k) => {
|
|
23
|
+
const id = `control-${k}`;
|
|
24
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: id, size: "sm", checked: controlOn({ kind, style, controls }, k), onCheckedChange: (on) => set(k, on) }), _jsx(Label, { htmlFor: id, className: "font-normal", children: k === 'ask' ? `Ask ${assistant}` : WIDGET_CONTROL_NAMES[k] })] }, k));
|
|
25
|
+
}) }));
|
|
26
|
+
}
|