@columenlabs/lumen-ui 0.1.45 → 0.1.47

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 CHANGED
@@ -74,6 +74,16 @@ 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
+ - A page's header numbers are metric cards: `TileRow` of `Tile` (or `TileToggleGroup` when they pick what the page shows), with `fill` so they share the row, each with `spark` set to its daily history where it has one. Don't build a card of your own for this.
78
+ - A metered column (use against an allowance) shows one number per cell with `UnitValue` (right-aligned number, fixed muted unit slot, status dot), never `value [pct]`. Offer both readings through the field's `showAs` (Amount / % of allowance, picked per column in Edit view, saved as `shows`); default amounts for sizes (storage) and shares for capped counts. Put the other reading, the allowance and the cost formula in a hover card. Totals use `rollRender`: sum amounts with their unit; a share is the rows' total over their allowance, never a sum of percents.
79
+ - A floating bulk-action bar over a table's selection: Apply clears the selection and the bar goes away; the toast's Undo restores the change, not the selection. Cancel keeps the selection.
80
+ - Bars in a column line up: use `ShareMeter` (fixed track, fixed right-aligned number), never a bar sized by its label.
81
+ - 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.
82
+ - 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.
83
+ - "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.
84
+ - 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.
85
+ - One selected sidebar row: give each nav item its own route, and let an open parent look neutral.
86
+ - What's new is on in every app: pass `releases="/api/releases"` to `LumenShell`, served by lumen-ship's `listReleases`.
77
87
  - A Save blocked by its form's checks is a `SaveButton` that lists each issue, never a bare `disabled`.
78
88
 
79
89
  ## Notices
@@ -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
- /** A value as a chip in its colour (a label): a tint behind, the colour as text. */
11
- export declare function ColorChip({ color, children }: {
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
- /** A value as a chip in its colour (a label): a tint behind, the colour as text. */
17
- export function ColorChip({ color, children }) {
18
- return (_jsx("span", { className: "inline-flex h-5 items-center 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: children }));
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,8 @@ 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';
25
+ export { UnitValue } from './unit-value.js';
24
26
  export { COVERAGE_LEVELS, CoverageCell, type CoverageJobState, type CoverageLevel, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
25
27
  export { type CoverageBlock, type CoverageData, type CoverageFact, type CoveragePick, type CoverageRow, type CoverageSummary, LumenCoverageMap, type LumenCoverageMapProps, blockName, blockOf, } from './coverage-map.js';
26
28
  export { type CalendarFact, LumenCoverageCalendarMap, type LumenCoverageCalendarMapProps, } from './coverage-calendar-map.js';
@@ -66,7 +68,7 @@ export { OriginLegend, originsOf } from './origin-legend.js';
66
68
  export { type ConnectedDomain, ConnectOffer, connectionFields, linkGroupId } from './connections.js';
67
69
  export { FRESHNESS, type Freshness, FreshnessIcon, FreshnessMark, freshnessLine, freshnessOf, } from './freshness.js';
68
70
  export { SourcesLine, useSourceHealth } from './source-health.js';
69
- export { TILE_WIDTH, Tile, TileChip, type TileOption, TileRow, TileToggleGroup, } from './tile.js';
71
+ export { TILE_WIDTH, Tile, TileChip, type TileOption, TileRow, TileSpark, TileToggleGroup, } from './tile.js';
70
72
  export { type FilterField, type FilterToken, LumenTokenizedFilter, type LumenFilterState, useLumenFilter, } from './lumen-filter.js';
71
73
  export { useIsMobile } from './hooks/use-mobile.js';
72
74
  export { VIEW_ICONS, iconOf, viewIcon } from './view-icons.js';
package/dist/index.js CHANGED
@@ -28,6 +28,8 @@ 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';
32
+ export { UnitValue } from './unit-value.js';
31
33
  export { COVERAGE_LEVELS, CoverageCell, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
32
34
  export { LumenCoverageMap, blockName, blockOf, } from './coverage-map.js';
33
35
  export { LumenCoverageCalendarMap, } from './coverage-calendar-map.js';
@@ -73,7 +75,7 @@ export { OriginLegend, originsOf } from './origin-legend.js';
73
75
  export { ConnectOffer, connectionFields, linkGroupId } from './connections.js';
74
76
  export { FRESHNESS, FreshnessIcon, FreshnessMark, freshnessLine, freshnessOf, } from './freshness.js';
75
77
  export { SourcesLine, useSourceHealth } from './source-health.js';
76
- export { TILE_WIDTH, Tile, TileChip, TileRow, TileToggleGroup, } from './tile.js';
78
+ export { TILE_WIDTH, Tile, TileChip, TileRow, TileSpark, TileToggleGroup, } from './tile.js';
77
79
  export { LumenTokenizedFilter, useLumenFilter, } from './lumen-filter.js';
78
80
  export { useIsMobile } from './hooks/use-mobile.js';
79
81
  export { VIEW_ICONS, iconOf, viewIcon } from './view-icons.js';
@@ -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;
@@ -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 && a.value.toLowerCase() === b.value.toLowerCase();
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
- const inc = of.filter((t) => !t.not).map((t) => t.value.toLowerCase());
59
- const exc = of.filter((t) => t.not).map((t) => t.value.toLowerCase());
60
- if (inc.length && !have.some((v) => inc.includes(v)))
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
- const all = chip.tokens.map((t) => (quote ? `“${t.value}”` : t.value)).join(', ');
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) => (t.key === key ? token(key, t.value, not) : 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 out = new Map();
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
- for (const v of new Set(f.values(row)))
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
- out.set(f.key, m);
386
+ counts.set(f.key, m);
387
+ empties.set(f.key, none);
351
388
  }
352
- return out;
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 && (_jsx(CommandGroup, { className: "px-1 py-0.5", children: 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: () => {
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))) })), fields
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);
@@ -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) => `${t.not ? 'not ' : ''}${t.op ? `${t.key} ${t.value.replace('..', '–')}` : (t.text ?? t.value)}`)
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. */
@@ -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, clearAll && (_jsxs("div", { className: "flex items-center justify-between border-t px-2 py-1.5", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: [values.length, " picked", max !== undefined ? ` of ${max}` : ''] }), clearAll] }))] }) }) }));
259
+ 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
  /**
@@ -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 });
@@ -555,7 +555,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
555
555
  const lead = Math.max(1, shown.findIndex((f, i) => i > 0 && rolls(f)) < 0
556
556
  ? shown.length
557
557
  : shown.findIndex((f, i) => i > 0 && rolls(f)));
558
- const rollCell = (f, members) => rollsUp(f) ? (_jsx(ViewValueCell, { value: computed.roll(f, members), format: f.aggregate && isCountAgg(f.aggregate) ? 'rounded' : f.format, amount: f.aggregate !== 'count' && f.amount, compact: f.compact })) : null;
558
+ const rollCell = (f, members) => rollsUp(f) && f.rollRender ? (f.rollRender(members)) : rollsUp(f) ? (_jsx(ViewValueCell, { value: computed.roll(f, members), format: f.aggregate && isCountAgg(f.aggregate) ? 'rounded' : f.format, amount: f.aggregate !== 'count' && f.amount, compact: f.compact })) : null;
559
559
  const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { className: colOf(f), children: rollCell(f, members) }, f.key)));
560
560
  // A row's chevron opens one level under it, closed by default: its panel, else the view's
561
561
  // sub-group ("Each row opens"; None opens nothing).
@@ -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
- 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, width: 36, allowDecimals: false, tickFormatter: (v) => fmt(Number(v)) }), _jsx(ChartTooltip, { cursor: { fill: 'var(--muted)', opacity: 0.5 }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
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);
@@ -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;
@@ -199,6 +199,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
199
199
  }[];
200
200
  formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
201
201
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
202
+ shows?: Record<string, string> | undefined;
202
203
  group: {
203
204
  fields: string[];
204
205
  default: string | null;
@@ -237,7 +238,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
237
238
  key: string;
238
239
  value: string;
239
240
  not?: boolean | undefined;
240
- op?: "between" | "eq" | "gt" | "lt" | undefined;
241
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
241
242
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
242
243
  }[];
243
244
  source?: string | undefined;
@@ -272,7 +273,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
272
273
  key: string;
273
274
  value: string;
274
275
  not?: boolean | undefined;
275
- op?: "between" | "eq" | "gt" | "lt" | undefined;
276
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
276
277
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
277
278
  }[];
278
279
  source?: string | undefined;
@@ -297,7 +298,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
297
298
  key: string;
298
299
  value: string;
299
300
  not?: boolean | undefined;
300
- op?: "between" | "eq" | "gt" | "lt" | undefined;
301
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
301
302
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
302
303
  }[];
303
304
  sign: boolean;
@@ -310,7 +311,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
310
311
  key: string;
311
312
  value: string;
312
313
  not?: boolean | undefined;
313
- op?: "between" | "eq" | "gt" | "lt" | undefined;
314
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
314
315
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
315
316
  }[];
316
317
  suffix: string;
@@ -348,7 +349,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
348
349
  key: string;
349
350
  value: string;
350
351
  not?: boolean | undefined;
351
- op?: "between" | "eq" | "gt" | "lt" | undefined;
352
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
352
353
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
353
354
  }[];
354
355
  source?: string | undefined;
@@ -372,7 +373,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
372
373
  key: string;
373
374
  value: string;
374
375
  not?: boolean | undefined;
375
- op?: "between" | "eq" | "gt" | "lt" | undefined;
376
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
376
377
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
377
378
  }[];
378
379
  sign: boolean;
@@ -385,7 +386,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
385
386
  key: string;
386
387
  value: string;
387
388
  not?: boolean | undefined;
388
- op?: "between" | "eq" | "gt" | "lt" | undefined;
389
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
389
390
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
390
391
  }[];
391
392
  suffix: string;
@@ -418,7 +419,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
418
419
  key: string;
419
420
  value: string;
420
421
  not?: boolean | undefined;
421
- op?: "between" | "eq" | "gt" | "lt" | undefined;
422
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
422
423
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
423
424
  }[];
424
425
  source?: string | undefined;
@@ -455,7 +456,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
455
456
  key: string;
456
457
  value: string;
457
458
  not?: boolean | undefined;
458
- op?: "between" | "eq" | "gt" | "lt" | undefined;
459
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
459
460
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
460
461
  }[];
461
462
  source?: string | undefined;
@@ -490,7 +491,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
490
491
  key: string;
491
492
  value: string;
492
493
  not?: boolean | undefined;
493
- op?: "between" | "eq" | "gt" | "lt" | undefined;
494
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
494
495
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
495
496
  }[];
496
497
  source?: string | undefined;
@@ -525,7 +526,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
525
526
  key: string;
526
527
  value: string;
527
528
  not?: boolean | undefined;
528
- op?: "between" | "eq" | "gt" | "lt" | undefined;
529
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
529
530
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
530
531
  }[];
531
532
  source?: string | undefined;
@@ -563,7 +564,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
563
564
  key: string;
564
565
  value: string;
565
566
  not?: boolean | undefined;
566
- op?: "between" | "eq" | "gt" | "lt" | undefined;
567
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
567
568
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
568
569
  }[];
569
570
  source?: string | undefined;
@@ -598,7 +599,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
598
599
  key: string;
599
600
  value: string;
600
601
  not?: boolean | undefined;
601
- op?: "between" | "eq" | "gt" | "lt" | undefined;
602
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
602
603
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
603
604
  }[];
604
605
  source?: string | undefined;
@@ -631,7 +632,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
631
632
  key: string;
632
633
  value: string;
633
634
  not?: boolean | undefined;
634
- op?: "between" | "eq" | "gt" | "lt" | undefined;
635
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
635
636
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
636
637
  }[];
637
638
  source?: string | undefined;
@@ -664,7 +665,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
664
665
  key: string;
665
666
  value: string;
666
667
  not?: boolean | undefined;
667
- op?: "between" | "eq" | "gt" | "lt" | undefined;
668
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
668
669
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
669
670
  }[];
670
671
  source?: string | undefined;
@@ -701,7 +702,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
701
702
  key: string;
702
703
  value: string;
703
704
  not?: boolean | undefined;
704
- op?: "between" | "eq" | "gt" | "lt" | undefined;
705
+ op?: "between" | "empty" | "eq" | "gt" | "lt" | undefined;
705
706
  link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
706
707
  }[];
707
708
  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
- /** A colour dot before the title, matching the item's line on a chart. */
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