@columenlabs/lumen-ui 0.1.44 → 0.1.46
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/README.md +7 -0
- package/dist/color-dot.d.ts +11 -2
- package/dist/color-dot.js +22 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lumen-echart.js +24 -2
- package/dist/lumen-filter.js +70 -18
- package/dist/lumen-links.js +3 -1
- package/dist/lumen-selector.js +2 -1
- package/dist/lumen-view.d.ts +2 -0
- package/dist/lumen-view.js +10 -4
- package/dist/lumen-widget-charts.js +17 -1
- package/dist/lumen-widget.d.ts +17 -17
- package/dist/nav.d.ts +5 -1
- package/dist/nav.js +50 -9
- package/dist/share-meter.d.ts +12 -0
- package/dist/share-meter.js +6 -0
- package/dist/tile.js +1 -1
- package/dist/view-edit-cell.d.ts +6 -2
- package/dist/view-edit-cell.js +9 -4
- package/dist/view-model.d.ts +7 -3
- package/dist/whats-new.js +7 -5
- package/dist/widget-settings.d.ts +14 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -74,6 +74,13 @@ Every export, its props and an example are on the [docs](https://lumen.columenla
|
|
|
74
74
|
- Pickers are `LumenSelector`, side panels `LumenDrawer`, tabs `LumenTabs`, destructive confirms `LumenConfirm`; reach for the raw `ui/*` part only when no Lumen part fits.
|
|
75
75
|
- Every card body sits in `LumenBoundary`, so one crash never blanks a page.
|
|
76
76
|
- Numbers use tabular figures and a true minus `−`; a gain or loss always shows a sign or arrow.
|
|
77
|
+
- Bars in a column line up: use `ShareMeter` (fixed track, fixed right-aligned number), never a bar sized by its label.
|
|
78
|
+
- Table rows are one line. A cell shows one value (with a small bar or tag at most); anything more (other metrics, the allowance, a breakdown) goes in a hover tooltip, never in stacked text or sub-rows.
|
|
79
|
+
- Chart colours are theme tokens (`var(--chart-1)`, `useChartTokens()`), never hex picked for one theme; `LumenEChart` resolves `var(--…)` in its option so sparklines read in light and dark.
|
|
80
|
+
- "No label" filters come built in: `LumenTokenizedFilter` pins a "No <field>" row on any text field where some rows have no value (an `empty` token); don't add your own toggle for it.
|
|
81
|
+
- Fewer tab rows: one row of tabs at most per card; narrower cuts (a product, a type) are fields in `LumenTokenizedFilter`, and a link to one opens the tab with that filter chip set.
|
|
82
|
+
- One selected sidebar row: give each nav item its own route, and let an open parent look neutral.
|
|
83
|
+
- What's new is on in every app: pass `releases="/api/releases"` to `LumenShell`, served by lumen-ship's `listReleases`.
|
|
77
84
|
- A Save blocked by its form's checks is a `SaveButton` that lists each issue, never a bare `disabled`.
|
|
78
85
|
|
|
79
86
|
## Notices
|
package/dist/color-dot.d.ts
CHANGED
|
@@ -7,8 +7,17 @@ export declare function ColorDot({ color, className }: {
|
|
|
7
7
|
export declare function colorIcon(color: string): ComponentType<{
|
|
8
8
|
className?: string;
|
|
9
9
|
}>;
|
|
10
|
-
/**
|
|
11
|
-
export declare function
|
|
10
|
+
/** `icon` drawn in `color` (a label's icon in the label's colour), kept per pair so it keeps its identity. */
|
|
11
|
+
export declare function tintedIcon(icon: ComponentType<{
|
|
12
|
+
className?: string;
|
|
13
|
+
}>, color: string | undefined): ComponentType<{
|
|
14
|
+
className?: string;
|
|
15
|
+
}>;
|
|
16
|
+
/** A value as a chip in its colour (a label): a tint behind, the colour as text, its icon first. */
|
|
17
|
+
export declare function ColorChip({ color, icon: Icon, children, }: {
|
|
12
18
|
color?: string;
|
|
19
|
+
icon?: ComponentType<{
|
|
20
|
+
className?: string;
|
|
21
|
+
}>;
|
|
13
22
|
children: ReactNode;
|
|
14
23
|
}): import("react").JSX.Element;
|
package/dist/color-dot.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from './lib/utils.js';
|
|
3
3
|
export function ColorDot({ color, className }) {
|
|
4
4
|
return (_jsx("span", { "aria-hidden": true, className: cn('flex size-4 shrink-0 items-center justify-center', className), children: _jsx("span", { className: "size-2.5 rounded-full", style: { background: color } }) }));
|
|
@@ -13,7 +13,25 @@ export function colorIcon(color) {
|
|
|
13
13
|
}
|
|
14
14
|
return icon;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const tinted = new WeakMap();
|
|
17
|
+
/** `icon` drawn in `color` (a label's icon in the label's colour), kept per pair so it keeps its identity. */
|
|
18
|
+
export function tintedIcon(icon, color) {
|
|
19
|
+
if (!color)
|
|
20
|
+
return icon;
|
|
21
|
+
let byColor = tinted.get(icon);
|
|
22
|
+
if (!byColor) {
|
|
23
|
+
byColor = new Map();
|
|
24
|
+
tinted.set(icon, byColor);
|
|
25
|
+
}
|
|
26
|
+
let out = byColor.get(color);
|
|
27
|
+
if (!out) {
|
|
28
|
+
const Icon = icon;
|
|
29
|
+
out = ({ className }) => (_jsx("span", { "aria-hidden": true, className: "flex shrink-0", style: { color }, children: _jsx(Icon, { className: cn('size-4', className) }) }));
|
|
30
|
+
byColor.set(color, out);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** A value as a chip in its colour (a label): a tint behind, the colour as text, its icon first. */
|
|
35
|
+
export function ColorChip({ color, icon: Icon, children, }) {
|
|
36
|
+
return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground", style: color ? { background: `color-mix(in oklab, ${color} 18%, transparent)`, color } : undefined, children: [Icon && _jsx(Icon, { className: "size-3 shrink-0" }), children] }));
|
|
19
37
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { LumenDateInput, type LumenDateInputProps, stepDate, } from './lumen-dat
|
|
|
21
21
|
export { LumenChoice, type LumenChoiceOption } from './lumen-choice.js';
|
|
22
22
|
export { LumenSecret } from './lumen-secret.js';
|
|
23
23
|
export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
|
|
24
|
+
export { ShareMeter } from './share-meter.js';
|
|
24
25
|
export { COVERAGE_LEVELS, CoverageCell, type CoverageJobState, type CoverageLevel, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
25
26
|
export { type CoverageBlock, type CoverageData, type CoverageFact, type CoveragePick, type CoverageRow, type CoverageSummary, LumenCoverageMap, type LumenCoverageMapProps, blockName, blockOf, } from './coverage-map.js';
|
|
26
27
|
export { type CalendarFact, LumenCoverageCalendarMap, type LumenCoverageCalendarMapProps, } from './coverage-calendar-map.js';
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { LumenDateInput, stepDate, } from './lumen-date-input.js';
|
|
|
28
28
|
export { LumenChoice } from './lumen-choice.js';
|
|
29
29
|
export { LumenSecret } from './lumen-secret.js';
|
|
30
30
|
export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
|
|
31
|
+
export { ShareMeter } from './share-meter.js';
|
|
31
32
|
export { COVERAGE_LEVELS, CoverageCell, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
32
33
|
export { LumenCoverageMap, blockName, blockOf, } from './coverage-map.js';
|
|
33
34
|
export { LumenCoverageCalendarMap, } from './coverage-calendar-map.js';
|
package/dist/lumen-echart.js
CHANGED
|
@@ -71,6 +71,24 @@ export function baseOption(t) {
|
|
|
71
71
|
},
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/** `option` with every `var(--name)` string (also inside `color-mix()`) replaced by its value. */
|
|
75
|
+
function resolveVars(option) {
|
|
76
|
+
const css = getComputedStyle(document.documentElement);
|
|
77
|
+
const walk = (v) => {
|
|
78
|
+
if (typeof v === 'string')
|
|
79
|
+
return v.includes('var(--')
|
|
80
|
+
? v.replace(/var\((--[\w-]+)(?:,\s*([^)]+))?\)/g, (_, name, fb) => {
|
|
81
|
+
return css.getPropertyValue(name).trim() || fb?.trim() || '';
|
|
82
|
+
})
|
|
83
|
+
: v;
|
|
84
|
+
if (Array.isArray(v))
|
|
85
|
+
return v.map(walk);
|
|
86
|
+
if (v && typeof v === 'object' && Object.getPrototypeOf(v) === Object.prototype)
|
|
87
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
|
|
88
|
+
return v;
|
|
89
|
+
};
|
|
90
|
+
return walk(option);
|
|
91
|
+
}
|
|
74
92
|
/** An axis in the theme's colours (merge into xAxis or yAxis). */
|
|
75
93
|
export function axisStyle(t) {
|
|
76
94
|
return {
|
|
@@ -106,10 +124,14 @@ export function LumenEChart({ option, height = 260, on, className, label, }) {
|
|
|
106
124
|
chart.current = null;
|
|
107
125
|
};
|
|
108
126
|
}, []);
|
|
127
|
+
// Canvas can't read CSS variables: `var(--chart-1)` in an option would draw black. Each one is
|
|
128
|
+
// swapped for its value, again whenever the theme flips.
|
|
129
|
+
const tokens = useChartTokens();
|
|
130
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: tokens change when the theme flips.
|
|
109
131
|
useEffect(() => {
|
|
110
132
|
if (ready)
|
|
111
|
-
chart.current?.setOption(option, { notMerge: true });
|
|
112
|
-
}, [ready, option]);
|
|
133
|
+
chart.current?.setOption(resolveVars(option), { notMerge: true });
|
|
134
|
+
}, [ready, option, tokens]);
|
|
113
135
|
const names = Object.keys(on ?? {}).join();
|
|
114
136
|
useEffect(() => {
|
|
115
137
|
const c = chart.current;
|
package/dist/lumen-filter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { CONTAINS_KEY, meetsNumber } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
import { CONTAINS_KEY, EMPTY_VALUE, meetsNumber, } from '@columenlabs/lumen-ui-types/views';
|
|
3
3
|
// The one filter box every view uses, redone after p2e's:
|
|
4
4
|
// one chip per field in the sans face, `Account IRA, Roth ×`, included or excluded as a whole
|
|
5
5
|
// (`Symbol not AAPL`). A chip's tap opens its field: Include / Exclude, then its values ticked,
|
|
@@ -8,7 +8,9 @@ import { CONTAINS_KEY, meetsNumber } from '@columenlabs/lumen-ui-types/views';
|
|
|
8
8
|
// fields: all must match (AND). Free text matches the row's text and any field value; picking
|
|
9
9
|
// "Text contains" keeps it as a chip. It filters the rows already loaded, on the client. The box
|
|
10
10
|
// is one row that never grows and always keeps room to type; crowded chips fold to `Sector: 3`,
|
|
11
|
-
// then behind +N. Phones get a bottom sheet.
|
|
11
|
+
// then behind +N. Phones get a bottom sheet. A text field where some rows have no value pins
|
|
12
|
+
// "No <field>" above its values: Include keeps the rows without one, Exclude the rows with any
|
|
13
|
+
// (an `empty` token); typing "no" or "empty" offers it too.
|
|
12
14
|
import { Command as CommandPrimitive } from 'cmdk';
|
|
13
15
|
import { ChevronLeftIcon, ChevronRightIcon, SaveIcon, SearchIcon, TextSearchIcon, XIcon, } from 'lucide-react';
|
|
14
16
|
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -24,14 +26,21 @@ import { Popover, PopoverAnchor, PopoverContent } from './ui/popover.js';
|
|
|
24
26
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './ui/sheet.js';
|
|
25
27
|
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
26
28
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
27
|
-
const OP_SIGN = { gt: '>', lt: '<', eq: '=', between: '' };
|
|
29
|
+
const OP_SIGN = { gt: '>', lt: '<', eq: '=', between: '', empty: '' };
|
|
30
|
+
/** The rows with no value on a field (`not`: with any). */
|
|
31
|
+
const isEmptyToken = (t) => t.op === 'empty';
|
|
32
|
+
const emptyToken = (key, not) => not ? { key, value: EMPTY_VALUE, op: 'empty', not } : { key, value: EMPTY_VALUE, op: 'empty' };
|
|
33
|
+
/** "No label": the empty row's words for a field. */
|
|
34
|
+
const noneOf = (label) => `No ${label.toLowerCase()}`;
|
|
28
35
|
export const tokenText = (t) => `${t.not ? '-' : ''}${t.key}:${t.op ? OP_SIGN[t.op] : ''}${t.value}`;
|
|
29
36
|
/** A number condition as people read it: `> 100`, `0 – 500`. */
|
|
30
37
|
const conditionText = (t) => t.op === 'between' ? t.value.replace('..', ' – ') : `${OP_SIGN[t.op ?? 'eq']} ${t.value}`;
|
|
31
38
|
/** `field:>100`, `field:<0`, `field:=5`, `field:0..500` typed out for a number field. */
|
|
32
39
|
const NUMBER_TYPED = /^(\w+):(>|<|=)?(-?\d+(?:\.\d+)?)(?:\.\.(-?\d+(?:\.\d+)?))?$/;
|
|
33
40
|
/** Same field and value, included or not: a value sits in one chip only. */
|
|
34
|
-
const same = (a, b) => a.key === b.key &&
|
|
41
|
+
const same = (a, b) => a.key === b.key &&
|
|
42
|
+
isEmptyToken(a) === isEmptyToken(b) &&
|
|
43
|
+
a.value.toLowerCase() === b.value.toLowerCase();
|
|
35
44
|
const token = (key, value, not) => not ? { key, value, not } : { key, value };
|
|
36
45
|
/** The `contains` chip's field: free text over the row's own text. */
|
|
37
46
|
const CONTAINS = {
|
|
@@ -55,9 +64,14 @@ function passes(values, nums, tokens, fields, except) {
|
|
|
55
64
|
continue;
|
|
56
65
|
}
|
|
57
66
|
const have = values[f.key] ?? [];
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
67
|
+
// `empty`: no value (included, alongside any values: none or one of these) or any value (excluded).
|
|
68
|
+
const none = of.find((t) => isEmptyToken(t) && !t.not);
|
|
69
|
+
if (of.some((t) => isEmptyToken(t) && t.not) && !have.length)
|
|
70
|
+
return false;
|
|
71
|
+
const plain = of.filter((t) => !isEmptyToken(t));
|
|
72
|
+
const inc = plain.filter((t) => !t.not).map((t) => t.value.toLowerCase());
|
|
73
|
+
const exc = plain.filter((t) => t.not).map((t) => t.value.toLowerCase());
|
|
74
|
+
if ((inc.length || none) && !(none && !have.length) && !have.some((v) => inc.includes(v)))
|
|
61
75
|
return false;
|
|
62
76
|
if (exc.length && have.some((v) => exc.includes(v)))
|
|
63
77
|
return false;
|
|
@@ -92,7 +106,7 @@ text) {
|
|
|
92
106
|
/** A field is included or excluded as a whole: where a field holds both, as
|
|
93
107
|
* filters saved before that do, its included values stay. */
|
|
94
108
|
function onePerField(tokens) {
|
|
95
|
-
const inc = new Set(tokens.filter((t) => !t.not && !t.op).map((t) => t.key));
|
|
109
|
+
const inc = new Set(tokens.filter((t) => !t.not && (!t.op || isEmptyToken(t))).map((t) => t.key));
|
|
96
110
|
return tokens.some((t) => t.not && inc.has(t.key))
|
|
97
111
|
? tokens.filter((t) => !(t.not && inc.has(t.key)))
|
|
98
112
|
: tokens;
|
|
@@ -179,7 +193,12 @@ function chipWords(chip, field, form) {
|
|
|
179
193
|
if (condition)
|
|
180
194
|
return { name, verb: '', shown: conditionText(condition), all: '' };
|
|
181
195
|
const quote = chip.key === CONTAINS_KEY;
|
|
182
|
-
|
|
196
|
+
// An excluded `empty` reads "Label any"; an included one "none" among the values.
|
|
197
|
+
if (chip.not && chip.tokens.length === 1 && chip.tokens[0] && isEmptyToken(chip.tokens[0]))
|
|
198
|
+
return { name, verb: '', shown: 'any', all: 'any' };
|
|
199
|
+
const all = chip.tokens
|
|
200
|
+
.map((t) => (isEmptyToken(t) ? 'none' : quote ? `“${t.value}”` : t.value))
|
|
201
|
+
.join(', ');
|
|
183
202
|
const counted = form === 'count' && chip.tokens.length > 1;
|
|
184
203
|
const verb = chip.not ? (counted ? 'not in:' : 'not') : counted ? ':' : '';
|
|
185
204
|
return { name, verb, shown: counted ? String(chip.tokens.length) : all, all };
|
|
@@ -294,9 +313,20 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
294
313
|
const only = (key, value) => {
|
|
295
314
|
setTokens([...tokens.filter((x) => x.key !== key), token(key, value, notFor(key))]);
|
|
296
315
|
};
|
|
316
|
+
const hasEmpty = (key) => tokens.some((t) => t.key === key && isEmptyToken(t));
|
|
317
|
+
const toggleEmpty = (key, not = notFor(key)) => {
|
|
318
|
+
if (hasEmpty(key))
|
|
319
|
+
setTokens(tokens.filter((t) => !(t.key === key && isEmptyToken(t))));
|
|
320
|
+
else
|
|
321
|
+
setTokens([...tokens, emptyToken(key, not)]);
|
|
322
|
+
if (!field)
|
|
323
|
+
setText('');
|
|
324
|
+
else
|
|
325
|
+
setSearch('');
|
|
326
|
+
};
|
|
297
327
|
const setNot = (key, not) => {
|
|
298
328
|
setPendingNot(not);
|
|
299
|
-
setTokens(tokens.map((t) =>
|
|
329
|
+
setTokens(tokens.map((t) => t.key !== key ? t : isEmptyToken(t) ? emptyToken(key, not) : token(key, t.value, not)));
|
|
300
330
|
};
|
|
301
331
|
// A number field holds one condition: a new one replaces it.
|
|
302
332
|
const setCondition = (key, op, value) => setTokens([...tokens.filter((x) => x.key !== key), { key, value, op }]);
|
|
@@ -335,21 +365,28 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
335
365
|
const dirty = !!onSave && (!!changed || !sameTokens(tokens, saved ?? []));
|
|
336
366
|
// Each field's values with counts. A field counts the rows the other fields' tokens keep, so a
|
|
337
367
|
// chip can take more values of its own field.
|
|
338
|
-
const counts = useMemo(() => {
|
|
339
|
-
const
|
|
368
|
+
const { counts, empties } = useMemo(() => {
|
|
369
|
+
const counts = new Map();
|
|
370
|
+
// Rows with no value on each field, for its "No <field>" row.
|
|
371
|
+
const empties = new Map();
|
|
340
372
|
for (const f of fields) {
|
|
341
373
|
if (f.number)
|
|
342
374
|
continue;
|
|
343
375
|
const m = new Map();
|
|
376
|
+
let none = 0;
|
|
344
377
|
for (const { row, values, nums } of indexed) {
|
|
345
378
|
if (!passes(values, nums, tokens, fields, f.key))
|
|
346
379
|
continue;
|
|
347
|
-
|
|
380
|
+
const vs = new Set(f.values(row));
|
|
381
|
+
if (!vs.size)
|
|
382
|
+
none++;
|
|
383
|
+
for (const v of vs)
|
|
348
384
|
m.set(v, (m.get(v) ?? 0) + 1);
|
|
349
385
|
}
|
|
350
|
-
|
|
386
|
+
counts.set(f.key, m);
|
|
387
|
+
empties.set(f.key, none);
|
|
351
388
|
}
|
|
352
|
-
return
|
|
389
|
+
return { counts, empties };
|
|
353
390
|
}, [fields, indexed, tokens]);
|
|
354
391
|
const picked = (key, value) => tokens.some((t) => same(t, { key, value }));
|
|
355
392
|
/** A field's values matching `n`: picked ones first, then the most common. In the open field
|
|
@@ -358,7 +395,7 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
358
395
|
const first = (v) => (key === field ? pinned.has(v) : picked(key, v));
|
|
359
396
|
const m = new Map(counts.get(key) ?? []);
|
|
360
397
|
for (const t of tokens)
|
|
361
|
-
if (t.key === key && !m.has(t.value))
|
|
398
|
+
if (t.key === key && !isEmptyToken(t) && !m.has(t.value))
|
|
362
399
|
m.set(t.value, 0);
|
|
363
400
|
return [...m]
|
|
364
401
|
.filter(([v]) => !n || v.toLowerCase().includes(n))
|
|
@@ -458,6 +495,13 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
458
495
|
only(key, value);
|
|
459
496
|
}, children: "Only" }), _jsx(CommandShortcut, { className: cn('ml-0 w-8 text-right', shortcut), children: count })] }, `${key}:${value}`));
|
|
460
497
|
};
|
|
498
|
+
// A field's "No <field>" row, pinned above its values: the rows with no value (Exclude: any).
|
|
499
|
+
const emptyRow = (f, include) => {
|
|
500
|
+
const n = empties.get(f.key) ?? 0;
|
|
501
|
+
if (!n && !hasEmpty(f.key))
|
|
502
|
+
return null;
|
|
503
|
+
return (_jsxs(CommandItem, { value: `${f.key}:${EMPTY_VALUE}`, onSelect: () => toggleEmpty(f.key, include ? false : undefined), className: tall, children: [_jsx(Checkbox, { checked: hasEmpty(f.key), tabIndex: -1, "aria-hidden": true, className: "pointer-events-none" }), _jsx("span", { className: "min-w-0 truncate text-muted-foreground italic", children: noneOf(f.label) }), _jsx(CommandShortcut, { className: cn('ml-auto w-8 text-right', shortcut), children: n })] }, `${f.key}:${EMPTY_VALUE}`));
|
|
504
|
+
};
|
|
461
505
|
const summary = (c) => {
|
|
462
506
|
const w = chipWords(c, fieldOf(c.key), 'full');
|
|
463
507
|
return (_jsxs("span", { className: "ml-auto min-w-0 truncate pl-3 text-right", children: [c.not && _jsx("span", { className: "text-destructive", children: "not " }), _jsx("span", { className: "tabular-nums", children: w.shown })] }));
|
|
@@ -470,7 +514,8 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
470
514
|
focus();
|
|
471
515
|
}, children: _jsx(ChevronLeftIcon, {}) }), _jsx(activeField.icon, { className: "size-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: activeField.label }), !activeField.number && (_jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "ml-auto", value: notFor(activeField.key) ? 'exclude' : 'include', onValueChange: (v) => v && setNot(activeField.key, v === 'exclude'), "aria-label": `${activeField.label}: include or exclude`, children: [_jsx(ToggleGroupItem, { value: "include", className: "h-7 px-2 text-xs font-normal", children: "Include" }), _jsx(ToggleGroupItem, { value: "exclude", className: "h-7 px-2 text-xs font-normal", children: "Exclude" })] }))] }), activeField.number && (_jsx(NumberCondition, { label: activeField.label, current: tokens.find((t) => t.key === activeField.key), tall: mobile, count: (op, value) => numberCount(activeField.key, op, value), onSet: (op, value) => setCondition(activeField.key, op, value) }, activeField.key))] }), active === CONTAINS_KEY ? (_jsxs(CommandGroup, { className: "px-1 py-0.5", children: [containsWord && !picked(CONTAINS_KEY, containsWord) && (_jsxs(CommandItem, { value: `${CONTAINS_KEY}:${containsWord}`, onSelect: () => toggle(CONTAINS_KEY, containsWord), className: tall, children: [_jsx(TextSearchIcon, { className: "text-muted-foreground" }), _jsxs("span", { className: "min-w-0 truncate", children: ["\u201C", containsWord, "\u201D"] }), _jsx(CommandShortcut, { className: shortcut, children: wordOf(containsWord, notFor(CONTAINS_KEY)) })] })), tokens
|
|
472
516
|
.filter((t) => t.key === CONTAINS_KEY)
|
|
473
|
-
.map((t) => valueRow(CONTAINS_KEY, t.value, wordOf(t.value, !!t.not)))] })) : (!activeField.number && (
|
|
517
|
+
.map((t) => valueRow(CONTAINS_KEY, t.value, wordOf(t.value, !!t.not)))] })) : (!activeField.number && (_jsxs(CommandGroup, { className: "px-1 py-0.5", children: [(!needle || noneOf(activeField.label).toLowerCase().includes(needle)) &&
|
|
518
|
+
emptyRow(activeField), valuesOf(activeField.key, needle, 200).map(([v, n]) => valueRow(activeField.key, v, n))] }))), _jsx(CommandEmpty, { children: "No matching values." })] })) : q ? (_jsxs(_Fragment, { children: [_jsx(CommandEmpty, { children: "No matching values." }), word && (_jsx(CommandGroup, { className: "px-1 py-0.5", children: _jsxs(CommandItem, { value: `${CONTAINS_KEY}:${word}`, onSelect: () => {
|
|
474
519
|
setTokens([
|
|
475
520
|
...tokens.filter((t) => !same(t, { key: CONTAINS_KEY, value: word })),
|
|
476
521
|
token(CONTAINS_KEY, word, chipOf(CONTAINS_KEY)?.not ?? negate),
|
|
@@ -478,7 +523,14 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
478
523
|
setText('');
|
|
479
524
|
}, className: tall, children: [_jsx(TextSearchIcon, { className: "text-muted-foreground" }), _jsxs("span", { className: "min-w-0 truncate", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Text ", negate ? 'not contains' : 'contains', ' '] }), "\u201C", word, "\u201D"] }), _jsx(CommandShortcut, { className: shortcut, children: wordOf(word, negate) })] }) })), fields.some((f) => f.label.toLowerCase().startsWith(q) || f.key.startsWith(q)) && (_jsx(CommandGroup, { heading: "Fields", className: "px-1 py-0.5", children: fields
|
|
480
525
|
.filter((f) => f.label.toLowerCase().startsWith(q) || f.key.startsWith(q))
|
|
481
|
-
.map((f) => (_jsxs(CommandItem, { value: `field:${f.key}`, onSelect: () => openField(f.key), className: tall, children: [_jsx(f.icon, { className: "text-muted-foreground" }), f.label, _jsx(ChevronRightIcon, { className: "ml-auto text-muted-foreground" })] }, f.key))) })),
|
|
526
|
+
.map((f) => (_jsxs(CommandItem, { value: `field:${f.key}`, onSelect: () => openField(f.key), className: tall, children: [_jsx(f.icon, { className: "text-muted-foreground" }), f.label, _jsx(ChevronRightIcon, { className: "ml-auto text-muted-foreground" })] }, f.key))) })), (q.startsWith('no') || (q.length > 1 && 'empty'.startsWith(q))) &&
|
|
527
|
+
fields.some((f) => !f.number &&
|
|
528
|
+
(empties.get(f.key) ?? 0) > 0 &&
|
|
529
|
+
(q.startsWith('no') ? noneOf(f.label).toLowerCase().startsWith(q) : true)) && (_jsx(CommandGroup, { heading: "Quick filters", className: "px-1 py-0.5", children: fields
|
|
530
|
+
.filter((f) => !f.number &&
|
|
531
|
+
(empties.get(f.key) ?? 0) > 0 &&
|
|
532
|
+
(q.startsWith('no') ? noneOf(f.label).toLowerCase().startsWith(q) : true))
|
|
533
|
+
.map((f) => emptyRow(f, true)) })), fields
|
|
482
534
|
.filter((f) => !f.number)
|
|
483
535
|
.map((f) => {
|
|
484
536
|
const vs = valuesOf(f.key, q, PER_FIELD);
|
package/dist/lumen-links.js
CHANGED
|
@@ -156,7 +156,9 @@ export function followedBy(tokens, fields, keys) {
|
|
|
156
156
|
/** What a card follows, as its title says it: `IRA, Roth`. */
|
|
157
157
|
export function followsText(tokens) {
|
|
158
158
|
return tokens
|
|
159
|
-
.map((t) =>
|
|
159
|
+
.map((t) => t.op === 'empty'
|
|
160
|
+
? `${t.key} ${t.not ? 'any' : 'none'}`
|
|
161
|
+
: `${t.not ? 'not ' : ''}${t.op ? `${t.key} ${t.value.replace('..', '–')}` : (t.text ?? t.value)}`)
|
|
160
162
|
.join(', ');
|
|
161
163
|
}
|
|
162
164
|
/** The square itself: filled in its colour, or an outline for none. */
|
package/dist/lumen-selector.js
CHANGED
|
@@ -11,6 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
// - Triggers: `field` (a form select), `chip` (a compact header pill), `icon` (an icon button).
|
|
12
12
|
// `LumenSelectorList` is the fourth, `none`: the list anchored to a text box (the assistant @ and /).
|
|
13
13
|
// - Phone: a bottom sheet with the search on top and, for multiple, Done.
|
|
14
|
+
// - Desktop, multiple: a footer with the count, Clear all and Done (picks save as they're made).
|
|
14
15
|
// - An option with a `target` (a card or a sidebar page) lights it on the page while hovered.
|
|
15
16
|
import { Combobox as Base } from '@base-ui/react';
|
|
16
17
|
import { CheckIcon, ChevronDownIcon, PlusIcon, SearchIcon, XIcon } from 'lucide-react';
|
|
@@ -255,7 +256,7 @@ export function LumenSelector(props) {
|
|
|
255
256
|
// No ring on the first button: the person taps what they want.
|
|
256
257
|
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
|
}
|
|
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,
|
|
259
|
+
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, multiple && (_jsxs("div", { className: "flex items-center gap-3 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, _jsx(Button, { size: "xs", className: "ml-auto", onClick: () => setOpen(false), children: "Done" })] }))] }) }) }));
|
|
259
260
|
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
|
}
|
|
261
262
|
/**
|
package/dist/lumen-view.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface ServerGroups {
|
|
|
88
88
|
more?: () => void;
|
|
89
89
|
/** A group header's click: open or close it. */
|
|
90
90
|
onToggle: (key: string) => void;
|
|
91
|
+
/** Expand all (true) or Collapse all (false): open or close every group listed. */
|
|
92
|
+
onToggleAll?: (open: boolean) => void;
|
|
91
93
|
}
|
|
92
94
|
export interface LumenViewProps<T> {
|
|
93
95
|
/** The view's domain, its formula columns included (`withViewFields`). */
|
package/dist/lumen-view.js
CHANGED
|
@@ -445,7 +445,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
445
445
|
const edit = f.edit;
|
|
446
446
|
const many = edit.many;
|
|
447
447
|
if (many)
|
|
448
|
-
return (_jsx(EditManyCell, { label: f.label, values: f.values?.(row) ?? (typeof v === 'string' && v !== '' ? [v] : []), options: options.get(f.key)?.inUse ?? [], unused: options.get(f.key)?.unused, note: edit.note?.(row), colorOf: f.color, onSave: (values) => many(row, values) }));
|
|
448
|
+
return (_jsx(EditManyCell, { label: f.label, values: f.values?.(row) ?? (typeof v === 'string' && v !== '' ? [v] : []), options: options.get(f.key)?.inUse ?? [], unused: options.get(f.key)?.unused, note: edit.note?.(row), colorOf: f.color, iconOf: f.iconOf, onSave: (values) => many(row, values) }));
|
|
449
449
|
const save = edit.save;
|
|
450
450
|
if (!save)
|
|
451
451
|
return _jsx(ViewValueCell, { value: v, format: f.format, chip: chip });
|
|
@@ -531,7 +531,9 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
531
531
|
const startsOpen = (i) => rows.length <= OPEN_ALL || i === 0;
|
|
532
532
|
const isOpen = (key, i) => serverGroups ? !!served.get(key)?.open : startsOpen(i) !== flipped.has(key);
|
|
533
533
|
const anyClosed = !!groups?.some(([k], i) => !isOpen(k, i));
|
|
534
|
-
const setAll = (open) =>
|
|
534
|
+
const setAll = (open) => serverGroups
|
|
535
|
+
? serverGroups.onToggleAll?.(open)
|
|
536
|
+
: setFlipped(new Set((groups ?? []).filter((_, i) => startsOpen(i) !== open).map(([k]) => k)));
|
|
535
537
|
const keyOf = domain.rowKey;
|
|
536
538
|
const toggle = (keys, on) => {
|
|
537
539
|
if (!selection)
|
|
@@ -626,7 +628,11 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
626
628
|
const g = served.get(key);
|
|
627
629
|
const out = rowsOf(list, key);
|
|
628
630
|
if (!g?.more && !g?.loading)
|
|
629
|
-
return out
|
|
631
|
+
return out.length
|
|
632
|
+
? out
|
|
633
|
+
: [
|
|
634
|
+
_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-2 text-center text-xs text-muted-foreground", children: "No rows match." }) }, `${key}:none`),
|
|
635
|
+
];
|
|
630
636
|
return [
|
|
631
637
|
...out,
|
|
632
638
|
_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: g.loading ? (_jsx("span", { className: "px-2 text-xs text-muted-foreground", children: "Loading\u2026" })) : (_jsxs(Button, { variant: "ghost", size: "xs", onClick: g.more, children: [_jsx(ChevronsDownIcon, {}), "Show more", g.n !== null && (_jsxs("span", { className: "num font-normal text-muted-foreground", children: ["of ", (g.n - list.length).toLocaleString()] }))] })) }) }, `${key}:more`),
|
|
@@ -641,7 +647,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
641
647
|
? 'ascending'
|
|
642
648
|
: h.column.getIsSorted() === 'desc'
|
|
643
649
|
? 'descending'
|
|
644
|
-
: undefined, children: [i === 0 && groups && !serverGroups && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
|
|
650
|
+
: undefined, children: [i === 0 && groups && (!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
|
|
645
651
|
? groups.map(([key, members], gi) => {
|
|
646
652
|
const open = isOpen(key, gi);
|
|
647
653
|
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, children: [selection && (_jsx(TableCell, { className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
|
|
@@ -42,7 +42,8 @@ export function StackedColumns({ rows, x, parts, label, height = 220, tick = Str
|
|
|
42
42
|
const total = (r) => parts.reduce((n, p) => n + (Number(r[p.key]) || 0), 0);
|
|
43
43
|
// The highest part with a value carries the column's rounded cap.
|
|
44
44
|
const top = (r) => parts.findLast((p) => Number(r[p.key]) > 0)?.key;
|
|
45
|
-
|
|
45
|
+
const ticks = niceTicks(Math.max(0, ...rows.map(total)));
|
|
46
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ChartContainer, { config: config, role: "img", "aria-label": label, className: "aspect-auto w-full", style: { height }, children: _jsxs(BarChart, { data: rows, margin: { left: 0, right: 0, top: 8, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: x, tickLine: false, axisLine: false, tickMargin: 8, minTickGap: 12, tickFormatter: (v) => tick(String(v)) }), _jsx(YAxis, { tickLine: false, axisLine: false, domain: [0, ticks.at(-1) ?? 'auto'], ticks: ticks, width: axisWidth(ticks.map(fmt)), tickFormatter: (v) => fmt(Number(v)) }), _jsx(ChartTooltip, { cursor: { fill: 'var(--muted)', opacity: 0.5 }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
|
|
46
47
|
const r = payload?.[0]?.payload;
|
|
47
48
|
return r ? (_jsxs("span", { className: "flex w-full gap-3", children: [_jsx("span", { children: tick(String(r[x])) }), _jsx("span", { className: "num ml-auto", children: fmt(total(r)) })] })) : null;
|
|
48
49
|
}, formatter: (v, name) => (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-sm", style: { background: config[String(name)]?.color } }), _jsx("span", { children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v)) })] })) }) }), parts.map((p) => (_jsx(Bar, { dataKey: p.key, stackId: "a", fill: p.fill, isAnimationActive: false, shape: (b) => (_jsx(Rectangle, { ...b, radius: top(b.payload) === p.key ? CAP : 0 })) }, p.key)))] }) }), legend && (_jsx("ul", { className: "flex flex-wrap justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: parts.map((p) => (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "size-2 rounded-sm", style: { background: p.fill } }), p.name] }, p.key))) }))] }));
|
|
@@ -84,3 +85,18 @@ export function AllocationBar({ items, total, config, tooltip, share, }) {
|
|
|
84
85
|
return (_jsxs("text", { x: Number(p.x) + 8, y: Number(p.y) + Number(p.height) / 2, dy: "0.35em", fontSize: 11, fontWeight: 600, style: { fill: 'var(--background)' }, children: [name, _jsx("tspan", { fontWeight: 400, dx: 6, className: "num", children: share(s.v) })] }));
|
|
85
86
|
} }) }, s.key)))] }) }));
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Round axis steps (1, 2, 2.5 or 5 times a power of ten) from 0 to just above `max`, about four
|
|
90
|
+
* of them, so a value axis reads 0, 100k, 200k, 300k rather than 95k, 190k, 285k.
|
|
91
|
+
*/
|
|
92
|
+
function niceTicks(max, count = 4) {
|
|
93
|
+
if (!(max > 0))
|
|
94
|
+
return [0, 1];
|
|
95
|
+
const raw = max / count;
|
|
96
|
+
const pow = 10 ** Math.floor(Math.log10(raw));
|
|
97
|
+
const step = ([1, 2, 2.5, 5, 10].find((m) => m * pow >= raw) ?? 10) * pow;
|
|
98
|
+
const n = Math.ceil(max / step - 1e-9);
|
|
99
|
+
return Array.from({ length: n + 1 }, (_, i) => Math.round(i * step * 1e6) / 1e6);
|
|
100
|
+
}
|
|
101
|
+
/** Room for the widest tick label (about 7px a character at text-xs), so none is clipped. */
|
|
102
|
+
const axisWidth = (labels) => Math.max(24, Math.max(0, ...labels.map((l) => l.length)) * 7 + 8);
|
package/dist/lumen-widget.d.ts
CHANGED
|
@@ -174,7 +174,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
174
174
|
key: string;
|
|
175
175
|
value: string;
|
|
176
176
|
not?: boolean | undefined;
|
|
177
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
177
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
178
178
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
179
179
|
}[];
|
|
180
180
|
source?: string | undefined;
|
|
@@ -237,7 +237,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
237
237
|
key: string;
|
|
238
238
|
value: string;
|
|
239
239
|
not?: boolean | undefined;
|
|
240
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
240
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
241
241
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
242
242
|
}[];
|
|
243
243
|
source?: string | undefined;
|
|
@@ -272,7 +272,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
272
272
|
key: string;
|
|
273
273
|
value: string;
|
|
274
274
|
not?: boolean | undefined;
|
|
275
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
275
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
276
276
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
277
277
|
}[];
|
|
278
278
|
source?: string | undefined;
|
|
@@ -297,7 +297,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
297
297
|
key: string;
|
|
298
298
|
value: string;
|
|
299
299
|
not?: boolean | undefined;
|
|
300
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
300
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
301
301
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
302
302
|
}[];
|
|
303
303
|
sign: boolean;
|
|
@@ -310,7 +310,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
310
310
|
key: string;
|
|
311
311
|
value: string;
|
|
312
312
|
not?: boolean | undefined;
|
|
313
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
313
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
314
314
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
315
315
|
}[];
|
|
316
316
|
suffix: string;
|
|
@@ -348,7 +348,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
348
348
|
key: string;
|
|
349
349
|
value: string;
|
|
350
350
|
not?: boolean | undefined;
|
|
351
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
351
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
352
352
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
353
353
|
}[];
|
|
354
354
|
source?: string | undefined;
|
|
@@ -372,7 +372,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
372
372
|
key: string;
|
|
373
373
|
value: string;
|
|
374
374
|
not?: boolean | undefined;
|
|
375
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
375
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
376
376
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
377
377
|
}[];
|
|
378
378
|
sign: boolean;
|
|
@@ -385,7 +385,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
385
385
|
key: string;
|
|
386
386
|
value: string;
|
|
387
387
|
not?: boolean | undefined;
|
|
388
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
388
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
389
389
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
390
390
|
}[];
|
|
391
391
|
suffix: string;
|
|
@@ -418,7 +418,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
418
418
|
key: string;
|
|
419
419
|
value: string;
|
|
420
420
|
not?: boolean | undefined;
|
|
421
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
421
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
422
422
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
423
423
|
}[];
|
|
424
424
|
source?: string | undefined;
|
|
@@ -455,7 +455,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
455
455
|
key: string;
|
|
456
456
|
value: string;
|
|
457
457
|
not?: boolean | undefined;
|
|
458
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
458
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
459
459
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
460
460
|
}[];
|
|
461
461
|
source?: string | undefined;
|
|
@@ -490,7 +490,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
490
490
|
key: string;
|
|
491
491
|
value: string;
|
|
492
492
|
not?: boolean | undefined;
|
|
493
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
493
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
494
494
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
495
495
|
}[];
|
|
496
496
|
source?: string | undefined;
|
|
@@ -525,7 +525,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
525
525
|
key: string;
|
|
526
526
|
value: string;
|
|
527
527
|
not?: boolean | undefined;
|
|
528
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
528
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
529
529
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
530
530
|
}[];
|
|
531
531
|
source?: string | undefined;
|
|
@@ -563,7 +563,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
563
563
|
key: string;
|
|
564
564
|
value: string;
|
|
565
565
|
not?: boolean | undefined;
|
|
566
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
566
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
567
567
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
568
568
|
}[];
|
|
569
569
|
source?: string | undefined;
|
|
@@ -598,7 +598,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
598
598
|
key: string;
|
|
599
599
|
value: string;
|
|
600
600
|
not?: boolean | undefined;
|
|
601
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
601
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
602
602
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
603
603
|
}[];
|
|
604
604
|
source?: string | undefined;
|
|
@@ -631,7 +631,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
631
631
|
key: string;
|
|
632
632
|
value: string;
|
|
633
633
|
not?: boolean | undefined;
|
|
634
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
634
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
635
635
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
636
636
|
}[];
|
|
637
637
|
source?: string | undefined;
|
|
@@ -664,7 +664,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
664
664
|
key: string;
|
|
665
665
|
value: string;
|
|
666
666
|
not?: boolean | undefined;
|
|
667
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
667
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
668
668
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
669
669
|
}[];
|
|
670
670
|
source?: string | undefined;
|
|
@@ -701,7 +701,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
701
701
|
key: string;
|
|
702
702
|
value: string;
|
|
703
703
|
not?: boolean | undefined;
|
|
704
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
704
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
705
705
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
706
706
|
}[];
|
|
707
707
|
source?: string | undefined;
|
package/dist/nav.d.ts
CHANGED
|
@@ -50,8 +50,12 @@ export interface SectionSubItem {
|
|
|
50
50
|
id: string;
|
|
51
51
|
title: string;
|
|
52
52
|
to: string;
|
|
53
|
-
/**
|
|
53
|
+
/** Its icon, drawn in `color` (a label's icon in the label's colour). */
|
|
54
|
+
icon?: NavIcon;
|
|
55
|
+
/** The icon's colour; without an icon, a colour dot before the title (a chart line's colour). */
|
|
54
56
|
color?: string;
|
|
57
|
+
/** A count on the right, lined up with the section items' badges. */
|
|
58
|
+
badge?: ReactNode;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
61
|
* A section of things a person keeps (views, trades, a watchlist): `+` adds one, the
|
package/dist/nav.js
CHANGED
|
@@ -12,9 +12,33 @@ import { useFolded } from './fold.js';
|
|
|
12
12
|
import { cn } from './lib/utils.js';
|
|
13
13
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible.js';
|
|
14
14
|
import { SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useSidebar, } from './ui/sidebar.js';
|
|
15
|
+
/**
|
|
16
|
+
* How well a link matches where the person is: -1 when it doesn't, else higher for a closer
|
|
17
|
+
* match. A link's query (`/resources?label=a`) must be in the URL too, and each matched key
|
|
18
|
+
* outranks a bare path, so `?label=a` beats `/resources` on `/resources?label=a`.
|
|
19
|
+
*/
|
|
20
|
+
function useMatch() {
|
|
21
|
+
const { path, search } = useRouterState({
|
|
22
|
+
select: (s) => ({ path: s.location.pathname, search: s.location.searchStr }),
|
|
23
|
+
});
|
|
24
|
+
const here = new URLSearchParams(search);
|
|
25
|
+
return ({ to, exact }) => {
|
|
26
|
+
const [base = '', query] = to.split('?');
|
|
27
|
+
const want = new URLSearchParams(query);
|
|
28
|
+
for (const [k, v] of want)
|
|
29
|
+
if (here.get(k) !== v)
|
|
30
|
+
return -1;
|
|
31
|
+
const keys = [...want.keys()].length;
|
|
32
|
+
if (path === base)
|
|
33
|
+
return 2 + keys * 10;
|
|
34
|
+
if (!exact && path.startsWith(`${base.replace(/\/$/, '')}/`))
|
|
35
|
+
return 1 + keys * 10;
|
|
36
|
+
return -1;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
15
39
|
function useActive() {
|
|
16
|
-
const
|
|
17
|
-
return (
|
|
40
|
+
const match = useMatch();
|
|
41
|
+
return (i) => match(i) >= 0;
|
|
18
42
|
}
|
|
19
43
|
/**
|
|
20
44
|
* A sidebar group whose header folds its items: bold caps, no icon. At rest
|
|
@@ -36,13 +60,13 @@ export function NavGroup({ label, items, pageType, }) {
|
|
|
36
60
|
}
|
|
37
61
|
const SECTION_BADGE = 'num group-data-[collapsible=icon]:hidden';
|
|
38
62
|
/** One section item: a link, its badge, and the chevron and nested items when it has any. */
|
|
39
|
-
function SectionRow({ item,
|
|
63
|
+
function SectionRow({ item, activeId, onRemove, removeLabel, }) {
|
|
40
64
|
const subs = item.items ?? [];
|
|
41
|
-
const subActive =
|
|
42
|
-
const isActive =
|
|
65
|
+
const subActive = activeId?.startsWith(`${item.id}/`) ?? false;
|
|
66
|
+
const isActive = activeId === item.id;
|
|
43
67
|
// The open item shows no trash icon.
|
|
44
68
|
const remove = item.fixed || isActive ? undefined : onRemove;
|
|
45
|
-
const row = (_jsxs(_Fragment, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.title, isActive: isActive, children: _jsxs(Link, { to: item.to, children: [_jsx(item.icon, {}), _jsx("span", { children: item.title })] }) }), item.badge !== undefined && item.badge !== null && (_jsx(SidebarMenuBadge, { className: cn(SECTION_BADGE,
|
|
69
|
+
const row = (_jsxs(_Fragment, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.title, isActive: isActive, children: _jsxs(Link, { to: item.to, children: [_jsx(item.icon, {}), _jsx("span", { children: item.title })] }) }), item.badge !== undefined && item.badge !== null && (_jsx(SidebarMenuBadge, { className: cn(SECTION_BADGE,
|
|
46
70
|
// The remove icon shows on hover from md up, and always below it: make room there.
|
|
47
71
|
remove &&
|
|
48
72
|
'max-md:right-7 md:group-focus-within/menu-item:opacity-0 md:group-hover/menu-item:opacity-0'), children: item.badge })), remove && (_jsx(LumenConfirm, { title: `${removeLabel(item)}?`, action: "Remove", onConfirm: () => remove(item), children: _jsx(SidebarMenuAction, { showOnHover: true, "aria-label": removeLabel(item), title: removeLabel(item),
|
|
@@ -50,7 +74,10 @@ function SectionRow({ item, active, onRemove, removeLabel, }) {
|
|
|
50
74
|
className: "text-muted-foreground peer-hover/menu-button:text-muted-foreground hover:bg-transparent hover:text-destructive", children: _jsx(Trash2Icon, {}) }) }))] }));
|
|
51
75
|
if (!subs.length)
|
|
52
76
|
return _jsx(SidebarMenuItem, { children: row });
|
|
53
|
-
return (_jsx(Collapsible, { asChild: true, defaultOpen: item.defaultOpen || subActive ||
|
|
77
|
+
return (_jsx(Collapsible, { asChild: true, defaultOpen: item.defaultOpen || subActive || isActive, className: "group/collapsible", children: _jsxs(SidebarMenuItem, { children: [row, _jsx(CollapsibleTrigger, { asChild: true, children: _jsx(SidebarMenuAction, { "aria-label": `Show or hide ${item.title}`, className: "right-auto left-1.5 bg-sidebar opacity-0 group-hover/menu-item:opacity-100 focus-visible:opacity-100 peer-data-[active=true]/menu-button:bg-sidebar-accent [&>svg]:transition-transform data-[state=open]:[&>svg]:rotate-90", children: _jsx(ChevronRightIcon, {}) }) }), _jsx(CollapsibleContent, { children: _jsx(SidebarMenuSub, { className: "mr-0 pr-0", children: subs.map((sub) => {
|
|
78
|
+
const hasBadge = sub.badge !== undefined && sub.badge !== null;
|
|
79
|
+
return (_jsxs(SidebarMenuSubItem, { children: [_jsx(SidebarMenuSubButton, { asChild: true, isActive: activeId === `${item.id}/${sub.id}`, className: cn(hasBadge && 'pr-8'), children: _jsxs(Link, { to: sub.to, title: sub.title, children: [sub.icon ? (_jsx("span", { "aria-hidden": true, className: "flex shrink-0 [&>svg]:size-4", style: { color: sub.color }, children: _jsx(sub.icon, {}) })) : (sub.color && (_jsx("span", { "aria-hidden": true, className: "mx-1 size-2 shrink-0 rounded-full", style: { background: sub.color } }))), _jsx("span", { children: sub.title })] }) }), hasBadge && (_jsx("span", { className: cn('pointer-events-none absolute top-1 right-1 flex h-5 min-w-5 items-center justify-center px-1 text-xs text-muted-foreground', typeof sub.badge === 'number' && 'num'), children: sub.badge }))] }, sub.id));
|
|
80
|
+
}) }) })] }) }));
|
|
54
81
|
}
|
|
55
82
|
/**
|
|
56
83
|
* A section of things a person keeps (views, trades, a watchlist): `+` adds one, the
|
|
@@ -66,8 +93,22 @@ export function SidebarSection({ label, items, onAdd, addLabel = `Add to ${label
|
|
|
66
93
|
type: assistantSubType ?? 'page',
|
|
67
94
|
})),
|
|
68
95
|
]), label, pageType);
|
|
69
|
-
const
|
|
96
|
+
const match = useMatch();
|
|
97
|
+
// One selected row per section: the closest match wins, the first on a tie, so two items that
|
|
98
|
+
// open the same page never both look selected. An open parent is never selected for being open.
|
|
99
|
+
let activeId = null;
|
|
100
|
+
let best = -1;
|
|
101
|
+
for (const i of items) {
|
|
102
|
+
const own = match({ to: i.to, exact: true });
|
|
103
|
+
if (own > best)
|
|
104
|
+
[best, activeId] = [own, i.id];
|
|
105
|
+
for (const sub of i.items ?? []) {
|
|
106
|
+
const m = match({ to: sub.to, exact: true });
|
|
107
|
+
if (m > best)
|
|
108
|
+
[best, activeId] = [m, `${i.id}/${sub.id}`];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
70
111
|
return (_jsx(FoldGroup, { label: label, action: onAdd && (_jsx(SidebarGroupAction, { title: addLabel, "aria-label": addLabel, onClick: onAdd, children: _jsx(PlusIcon, {}) })), children: _jsxs(SidebarGroupContent, { children: [_jsxs(SidebarMenu, { children: [loading
|
|
71
112
|
? [0, 1].map((i) => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuSkeleton, { showIcon: true }) }, i)))
|
|
72
|
-
: items.map((item) => (_jsx(SectionRow, { item: item,
|
|
113
|
+
: items.map((item) => (_jsx(SectionRow, { item: item, activeId: activeId, onRemove: onRemove, removeLabel: removeLabel }, item.id))), !loading && !items.length && empty && (_jsx("li", { className: "px-2 py-1 text-xs text-muted-foreground group-data-[collapsible=icon]:hidden", children: empty }))] }), children] }) }));
|
|
73
114
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function ShareMeter({ value, label, color, wide, className, }: {
|
|
3
|
+
/** 0 to 1 fills the bar; past 1 it stays full. */
|
|
4
|
+
value: number;
|
|
5
|
+
/** The number beside the bar, e.g. `113%`; left out, the rounded percent. */
|
|
6
|
+
label?: ReactNode;
|
|
7
|
+
/** The bar's colour, a theme token (`var(--loss)` when over). */
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Fills its cell (a list row) instead of the fixed table width. */
|
|
10
|
+
wide?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from './lib/utils.js';
|
|
3
|
+
import { Progress } from './ui/progress.js';
|
|
4
|
+
export function ShareMeter({ value, label, color = 'var(--primary)', wide, className, }) {
|
|
5
|
+
return (_jsxs("span", { className: cn('inline-flex items-center gap-2', wide && 'w-full', className), children: [_jsx(Progress, { value: Math.max(0, Math.min(value, 1)) * 100, className: cn('h-1.5 shrink-0 [&>[data-slot=progress-indicator]]:bg-(--meter-color)', wide ? 'w-full shrink' : 'w-14'), style: { '--meter-color': color } }), _jsx("span", { className: "num w-11 shrink-0 text-right text-xs", children: label ?? `${Math.round(value * 100)}%` })] }));
|
|
6
|
+
}
|
package/dist/tile.js
CHANGED
|
@@ -6,7 +6,7 @@ function TileBody({ label, value, caption, lines, valueClassName, }) {
|
|
|
6
6
|
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: cn('num truncate text-xl leading-7 font-semibold', valueClassName), children: value }), caption ? (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", children: caption })) : null, lines?.map((l, i) => (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", children: l }, i)))] }));
|
|
7
7
|
}
|
|
8
8
|
export function Tile({ label, value, caption, lines, className, valueClassName, }) {
|
|
9
|
-
return (_jsx(Card, { size: "sm", className: cn('min-w-0 gap-0.5 px-3', className), children: _jsx(TileBody, { label: label, value: value, caption: caption, lines: lines, valueClassName: valueClassName }) }));
|
|
9
|
+
return (_jsx(Card, { size: "sm", className: cn('min-w-0 flex-1 basis-2/5 gap-0.5 px-3 lg:basis-auto', TILE_WIDTH, className), children: _jsx(TileBody, { label: label, value: value, caption: caption, lines: lines, valueClassName: valueClassName }) }));
|
|
10
10
|
}
|
|
11
11
|
/** Width of every header tile on desktop, toggle tiles and plain Tiles alike; shrinks to fit the row. */
|
|
12
12
|
export const TILE_WIDTH = 'lg:w-(--tile-width) lg:flex-initial';
|
package/dist/view-edit-cell.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
2
|
export declare function EditCell({ label, value, options, unused, chip, note, colorOf, fixed, edited, onSave, }: {
|
|
3
3
|
/** The field's name, e.g. `Basket`. */
|
|
4
4
|
label: string;
|
|
@@ -23,13 +23,17 @@ export declare function EditCell({ label, value, options, unused, chip, note, co
|
|
|
23
23
|
* a tap opens a picker that ticks any number and takes a typed name, and closing it saves them all
|
|
24
24
|
* at once. Clicks stay off the row, which opens its details.
|
|
25
25
|
*/
|
|
26
|
-
export declare function EditManyCell({ label, values, options, unused, note, colorOf, onSave, }: {
|
|
26
|
+
export declare function EditManyCell({ label, values, options, unused, note, colorOf, iconOf, onSave, }: {
|
|
27
27
|
label: string;
|
|
28
28
|
values: string[];
|
|
29
29
|
options: string[];
|
|
30
30
|
unused?: string[];
|
|
31
31
|
note?: string;
|
|
32
32
|
colorOf?: (value: string) => string | undefined;
|
|
33
|
+
/** A value's icon (a label's), drawn in its colour on its chip and in the picker. */
|
|
34
|
+
iconOf?: (value: string) => ComponentType<{
|
|
35
|
+
className?: string;
|
|
36
|
+
}> | undefined;
|
|
33
37
|
onSave: (values: string[]) => Promise<unknown>;
|
|
34
38
|
}): import("react").JSX.Element;
|
|
35
39
|
/**
|
package/dist/view-edit-cell.js
CHANGED
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// opens its details.
|
|
5
5
|
import { PencilLineIcon, PlusIcon, XIcon } from 'lucide-react';
|
|
6
6
|
import { useEffect, useRef, useState } from 'react';
|
|
7
|
-
import { ColorChip, colorIcon } from './color-dot.js';
|
|
7
|
+
import { ColorChip, colorIcon, tintedIcon } from './color-dot.js';
|
|
8
8
|
import { cn } from './lib/utils.js';
|
|
9
9
|
import { LumenSelector } from './lumen-selector.js';
|
|
10
10
|
import { Input } from './ui/input.js';
|
|
@@ -69,7 +69,7 @@ export function EditCell({ label, value, options, unused = [], chip, note, color
|
|
|
69
69
|
* a tap opens a picker that ticks any number and takes a typed name, and closing it saves them all
|
|
70
70
|
* at once. Clicks stay off the row, which opens its details.
|
|
71
71
|
*/
|
|
72
|
-
export function EditManyCell({ label, values, options, unused = [], note, colorOf, onSave, }) {
|
|
72
|
+
export function EditManyCell({ label, values, options, unused = [], note, colorOf, iconOf, onSave, }) {
|
|
73
73
|
const [open, setOpen] = useState(false);
|
|
74
74
|
const [draft, setDraft] = useState(values);
|
|
75
75
|
const [error, setError] = useState(null);
|
|
@@ -99,7 +99,12 @@ export function EditManyCell({ label, values, options, unused = [], note, colorO
|
|
|
99
99
|
};
|
|
100
100
|
const row = (o) => {
|
|
101
101
|
const c = colorOf?.(o);
|
|
102
|
-
|
|
102
|
+
const i = iconOf?.(o);
|
|
103
|
+
return {
|
|
104
|
+
value: o,
|
|
105
|
+
label: o,
|
|
106
|
+
...(i ? { icon: tintedIcon(i, c) } : c ? { icon: colorIcon(c) } : {}),
|
|
107
|
+
};
|
|
103
108
|
};
|
|
104
109
|
const all = [...options, ...unused];
|
|
105
110
|
const picked = draft.filter((v) => !all.includes(v));
|
|
@@ -107,7 +112,7 @@ export function EditManyCell({ label, values, options, unused = [], note, colorO
|
|
|
107
112
|
{ label: 'In use', options: [...options, ...picked].map(row) },
|
|
108
113
|
{ label: 'Unused', options: unused.map(row) },
|
|
109
114
|
];
|
|
110
|
-
return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1", onClick: stop, onKeyDown: stop, children: [values.length > 0 && (_jsx("button", { type: "button", "aria-label": `${label}: ${values.join(', ')}`, className: "flex min-w-0
|
|
115
|
+
return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1", onClick: stop, onKeyDown: stop, children: [values.length > 0 && (_jsx("button", { type: "button", "aria-label": `${label}: ${values.join(', ')}`, className: "flex min-w-0 items-center gap-1 overflow-hidden rounded text-left", onClick: start, children: values.map((v) => (_jsx(ColorChip, { color: colorOf?.(v), icon: iconOf?.(v), children: v }, v))) })), _jsx(LumenSelector, { multiple: true, trigger: "icon", icon: values.length ? PencilLineIcon : PlusIcon, size: "sm", label: values.length ? `Change ${label.toLowerCase()}` : `Add ${label.toLowerCase()}`, className: cn('size-6 text-muted-foreground', values.length
|
|
111
116
|
? 'opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100 max-md:opacity-100'
|
|
112
117
|
: '-ml-1'), groups: groups, value: draft, disabled: saving, empty: `Type a ${label.toLowerCase()} name.`, create: (q) => q.trim() || null, createLabel: (q) => `Add “${q.trim()}”`, open: open, onOpenChange: (o) => {
|
|
113
118
|
if (o)
|
package/dist/view-model.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { JoinKey } from '@columenlabs/lumen-ui-types/domains';
|
|
2
2
|
import { type CustomViewConfig, type ViewAgg, type ViewChip, type ViewFormat, type ViewTotalAgg } from '@columenlabs/lumen-ui-types/views';
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
-
import type { ReactNode } from 'react';
|
|
4
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
5
5
|
import type { ChartBand, ChartMarker, Series } from './growth-chart.js';
|
|
6
6
|
export type ViewCellValue = number | string | null;
|
|
7
7
|
/** One field of a data domain: how to read it, show it, filter, group and roll it up. */
|
|
@@ -107,6 +107,10 @@ export interface ViewField<T> {
|
|
|
107
107
|
* cell, its chip and the edit picker. A CSS colour; a theme token reads in light and dark.
|
|
108
108
|
*/
|
|
109
109
|
color?: (value: string) => string | undefined;
|
|
110
|
+
/** A text value's icon (a label's), drawn in its `color` on its chip and in the edit picker. */
|
|
111
|
+
iconOf?: (value: string) => ComponentType<{
|
|
112
|
+
className?: string;
|
|
113
|
+
}> | undefined;
|
|
110
114
|
/** The cell's background, a theme colour mix (a P/L heat grid). */
|
|
111
115
|
tint?: (row: T) => string | undefined;
|
|
112
116
|
/** Left out on phones, where the table has no room for it (a returns table's longer windows). */
|
|
@@ -323,7 +327,7 @@ export declare const tilesOf: (c: CustomViewConfig) => ({
|
|
|
323
327
|
key: string;
|
|
324
328
|
value: string;
|
|
325
329
|
not?: boolean | undefined;
|
|
326
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
330
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
327
331
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
328
332
|
}[];
|
|
329
333
|
sign: boolean;
|
|
@@ -336,7 +340,7 @@ export declare const tilesOf: (c: CustomViewConfig) => ({
|
|
|
336
340
|
key: string;
|
|
337
341
|
value: string;
|
|
338
342
|
not?: boolean | undefined;
|
|
339
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
343
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
340
344
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
341
345
|
}[];
|
|
342
346
|
suffix: string;
|
package/dist/whats-new.js
CHANGED
|
@@ -3,7 +3,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
// every app): the version in the status bar opens the release notes, newest
|
|
4
4
|
// first, each release's Key takeaways only. An app turns it on with LumenShell's `releases`, its
|
|
5
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
|
|
6
|
+
// open; a host that answers 404 (self-host an app) keeps the button hidden. Until the first release
|
|
7
|
+
// the button reads "What's new" and opens an empty state, so a new app shows it from day one. With `apps`, each
|
|
7
8
|
// takeaway carries a chip of the app it is about (`apps` on the takeaway; none: everyone), and a
|
|
8
9
|
// release where one app has `GROUP_AT` or more is split into one sub-section per app.
|
|
9
10
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
@@ -50,12 +51,13 @@ export function WhatsNew({ api, apps, everyone = 'Global', }) {
|
|
|
50
51
|
staleTime: 30 * 60_000,
|
|
51
52
|
retry: false,
|
|
52
53
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!latest)
|
|
54
|
+
// Hidden while loading, on an error, and when the host has no releases API (404).
|
|
55
|
+
if (!q.data || q.data.pages[0] === null)
|
|
56
56
|
return null;
|
|
57
|
+
const releases = q.data.pages.flatMap((p) => p ?? []);
|
|
58
|
+
const latest = releases[0];
|
|
57
59
|
const day = (at) => new Date(at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone });
|
|
58
|
-
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
|
|
60
|
+
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 ?? "What's new" }), _jsxs(LumenDrawer, { open: open, onClose: () => setOpen(false), title: "What's new", description: "Key takeaways of each release, newest first", footerStart: releases.length ? `${releases.length} releases` : undefined, 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.length && (_jsx("p", { className: "py-2 text-muted-foreground text-sm", children: "No releases yet. Each release's key takeaways show here once it ships." })), releases.map((r, i) => (_jsx(DrawerSection, { title: r.tag, hint: day(r.published_at), defaultOpen: i === 0, children: r.takeaways.length ? (_jsx(Notes, { takeaways: r.takeaways, apps: apps, everyone: everyone })) : (_jsx("p", { className: "py-1 pl-1 text-muted-foreground text-sm", children: "Nothing new for you in this one." })) }, r.tag)))] })] }));
|
|
59
61
|
}
|
|
60
62
|
/** One release's takeaways: a flat list, or one sub-section per app once one app has many. */
|
|
61
63
|
function Notes({ takeaways, apps, everyone, }) {
|
|
@@ -72,7 +72,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
72
72
|
key: string;
|
|
73
73
|
value: string;
|
|
74
74
|
not?: boolean | undefined;
|
|
75
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
75
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
76
76
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
77
77
|
}[];
|
|
78
78
|
source?: string | undefined;
|
|
@@ -135,7 +135,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
135
135
|
key: string;
|
|
136
136
|
value: string;
|
|
137
137
|
not?: boolean | undefined;
|
|
138
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
138
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
139
139
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
140
140
|
}[];
|
|
141
141
|
source?: string | undefined;
|
|
@@ -170,7 +170,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
170
170
|
key: string;
|
|
171
171
|
value: string;
|
|
172
172
|
not?: boolean | undefined;
|
|
173
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
173
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
174
174
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
175
175
|
}[];
|
|
176
176
|
source?: string | undefined;
|
|
@@ -195,7 +195,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
195
195
|
key: string;
|
|
196
196
|
value: string;
|
|
197
197
|
not?: boolean | undefined;
|
|
198
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
198
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
199
199
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
200
200
|
}[];
|
|
201
201
|
sign: boolean;
|
|
@@ -208,7 +208,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
208
208
|
key: string;
|
|
209
209
|
value: string;
|
|
210
210
|
not?: boolean | undefined;
|
|
211
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
211
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
212
212
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
213
213
|
}[];
|
|
214
214
|
suffix: string;
|
|
@@ -246,7 +246,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
246
246
|
key: string;
|
|
247
247
|
value: string;
|
|
248
248
|
not?: boolean | undefined;
|
|
249
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
249
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
250
250
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
251
251
|
}[];
|
|
252
252
|
source?: string | undefined;
|
|
@@ -270,7 +270,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
270
270
|
key: string;
|
|
271
271
|
value: string;
|
|
272
272
|
not?: boolean | undefined;
|
|
273
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
273
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
274
274
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
275
275
|
}[];
|
|
276
276
|
sign: boolean;
|
|
@@ -283,7 +283,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
283
283
|
key: string;
|
|
284
284
|
value: string;
|
|
285
285
|
not?: boolean | undefined;
|
|
286
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
286
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
287
287
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
288
288
|
}[];
|
|
289
289
|
suffix: string;
|
|
@@ -316,7 +316,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
316
316
|
key: string;
|
|
317
317
|
value: string;
|
|
318
318
|
not?: boolean | undefined;
|
|
319
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
319
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
320
320
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
321
321
|
}[];
|
|
322
322
|
source?: string | undefined;
|
|
@@ -353,7 +353,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
353
353
|
key: string;
|
|
354
354
|
value: string;
|
|
355
355
|
not?: boolean | undefined;
|
|
356
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
356
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
357
357
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
358
358
|
}[];
|
|
359
359
|
source?: string | undefined;
|
|
@@ -388,7 +388,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
388
388
|
key: string;
|
|
389
389
|
value: string;
|
|
390
390
|
not?: boolean | undefined;
|
|
391
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
391
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
392
392
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
393
393
|
}[];
|
|
394
394
|
source?: string | undefined;
|
|
@@ -423,7 +423,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
423
423
|
key: string;
|
|
424
424
|
value: string;
|
|
425
425
|
not?: boolean | undefined;
|
|
426
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
426
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
427
427
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
428
428
|
}[];
|
|
429
429
|
source?: string | undefined;
|
|
@@ -461,7 +461,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
461
461
|
key: string;
|
|
462
462
|
value: string;
|
|
463
463
|
not?: boolean | undefined;
|
|
464
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
464
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
465
465
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
466
466
|
}[];
|
|
467
467
|
source?: string | undefined;
|
|
@@ -493,7 +493,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
493
493
|
key: string;
|
|
494
494
|
value: string;
|
|
495
495
|
not?: boolean | undefined;
|
|
496
|
-
op?: "between" | "eq" | "gt" | "lt" | undefined;
|
|
496
|
+
op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
|
|
497
497
|
link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
|
|
498
498
|
}[];
|
|
499
499
|
source?: string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@codemirror/lang-javascript": "6.2.5",
|
|
42
42
|
"@codemirror/lang-json": "6.0.2",
|
|
43
43
|
"@codemirror/lang-sql": "6.10.0",
|
|
44
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
44
|
+
"@columenlabs/lumen-ui-types": "0.1.46",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|