@columenlabs/lumen-ui 0.0.0-stage → 0.2.1

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.
Files changed (301) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +12 -0
  3. package/README.md +4 -2
  4. package/dist/add-column.d.ts +23 -0
  5. package/dist/add-column.js +62 -0
  6. package/dist/api-fetch.d.ts +9 -0
  7. package/dist/api-fetch.js +60 -0
  8. package/dist/app-switcher.d.ts +21 -0
  9. package/dist/app-switcher.js +12 -0
  10. package/dist/app-views.d.ts +18 -0
  11. package/dist/app-views.js +41 -0
  12. package/dist/assistant-context.d.ts +72 -0
  13. package/dist/assistant-context.js +30 -0
  14. package/dist/assistant-panel.d.ts +58 -0
  15. package/dist/assistant-panel.js +244 -0
  16. package/dist/assistant-screen.d.ts +124 -0
  17. package/dist/assistant-screen.js +252 -0
  18. package/dist/card-switch.d.ts +16 -0
  19. package/dist/card-switch.js +16 -0
  20. package/dist/color-dot.d.ts +14 -0
  21. package/dist/color-dot.js +19 -0
  22. package/dist/confirm.d.ts +18 -0
  23. package/dist/confirm.js +5 -0
  24. package/dist/connections.d.ts +45 -0
  25. package/dist/connections.js +275 -0
  26. package/dist/coverage-calendar-map.d.ts +27 -0
  27. package/dist/coverage-calendar-map.js +84 -0
  28. package/dist/coverage-cells.d.ts +55 -0
  29. package/dist/coverage-cells.js +85 -0
  30. package/dist/coverage-map.d.ts +112 -0
  31. package/dist/coverage-map.js +428 -0
  32. package/dist/details-table.d.ts +20 -0
  33. package/dist/details-table.js +13 -0
  34. package/dist/drawer-section.d.ts +27 -0
  35. package/dist/drawer-section.js +23 -0
  36. package/dist/floating-head.d.ts +2 -0
  37. package/dist/floating-head.js +57 -0
  38. package/dist/fold.d.ts +16 -0
  39. package/dist/fold.js +70 -0
  40. package/dist/form-issues.d.ts +32 -0
  41. package/dist/form-issues.js +60 -0
  42. package/dist/formula-input.d.ts +17 -0
  43. package/dist/formula-input.js +104 -0
  44. package/dist/frame-script.d.ts +7 -0
  45. package/dist/frame-script.js +103 -0
  46. package/dist/freshness.d.ts +24 -0
  47. package/dist/freshness.js +99 -0
  48. package/dist/group-button.d.ts +33 -0
  49. package/dist/group-button.js +67 -0
  50. package/dist/growth-chart.d.ts +77 -0
  51. package/dist/growth-chart.js +195 -0
  52. package/dist/hooks/use-keyboard-fit.d.ts +15 -0
  53. package/dist/hooks/use-keyboard-fit.js +29 -0
  54. package/dist/hooks/use-mobile.d.ts +1 -0
  55. package/dist/hooks/use-mobile.js +15 -0
  56. package/dist/icon-button.d.ts +21 -0
  57. package/dist/icon-button.js +22 -0
  58. package/dist/icon-picker.d.ts +14 -0
  59. package/dist/icon-picker.js +36 -0
  60. package/dist/index.d.ts +83 -0
  61. package/dist/index.js +90 -0
  62. package/dist/integrations.d.ts +57 -0
  63. package/dist/integrations.js +73 -0
  64. package/dist/joins.d.ts +128 -0
  65. package/dist/joins.js +767 -0
  66. package/dist/kit-config.d.ts +13 -0
  67. package/dist/kit-config.js +15 -0
  68. package/dist/lib/utils.d.ts +2 -0
  69. package/dist/lib/utils.js +5 -0
  70. package/dist/lumen-boundary.d.ts +7 -0
  71. package/dist/lumen-boundary.js +10 -0
  72. package/dist/lumen-code-editor-impl.d.ts +2 -0
  73. package/dist/lumen-code-editor-impl.js +16 -0
  74. package/dist/lumen-code-editor.d.ts +11 -0
  75. package/dist/lumen-code-editor.js +11 -0
  76. package/dist/lumen-code.d.ts +22 -0
  77. package/dist/lumen-code.js +75 -0
  78. package/dist/lumen-drawer.d.ts +30 -0
  79. package/dist/lumen-drawer.js +27 -0
  80. package/dist/lumen-echart.d.ts +58 -0
  81. package/dist/lumen-echart.js +129 -0
  82. package/dist/lumen-filter.d.ts +75 -0
  83. package/dist/lumen-filter.js +546 -0
  84. package/dist/lumen-finder.d.ts +62 -0
  85. package/dist/lumen-finder.js +275 -0
  86. package/dist/lumen-form.d.ts +34 -0
  87. package/dist/lumen-form.js +81 -0
  88. package/dist/lumen-geo-map.d.ts +9 -0
  89. package/dist/lumen-geo-map.js +135 -0
  90. package/dist/lumen-grid.d.ts +49 -0
  91. package/dist/lumen-grid.js +203 -0
  92. package/dist/lumen-layout.d.ts +150 -0
  93. package/dist/lumen-layout.js +661 -0
  94. package/dist/lumen-links.d.ts +96 -0
  95. package/dist/lumen-links.js +213 -0
  96. package/dist/lumen-page-state.d.ts +20 -0
  97. package/dist/lumen-page-state.js +15 -0
  98. package/dist/lumen-selector.d.ts +139 -0
  99. package/dist/lumen-selector.js +308 -0
  100. package/dist/lumen-source.d.ts +137 -0
  101. package/dist/lumen-source.js +327 -0
  102. package/dist/lumen-tab-drawer.d.ts +27 -0
  103. package/dist/lumen-tab-drawer.js +26 -0
  104. package/dist/lumen-tabs.d.ts +21 -0
  105. package/dist/lumen-tabs.js +6 -0
  106. package/dist/lumen-toast.d.ts +11 -0
  107. package/dist/lumen-toast.js +16 -0
  108. package/dist/lumen-trend.d.ts +18 -0
  109. package/dist/lumen-trend.js +157 -0
  110. package/dist/lumen-view-card.d.ts +35 -0
  111. package/dist/lumen-view-card.js +50 -0
  112. package/dist/lumen-view.d.ts +172 -0
  113. package/dist/lumen-view.js +645 -0
  114. package/dist/lumen-widget-charts.d.ts +57 -0
  115. package/dist/lumen-widget-charts.js +69 -0
  116. package/dist/lumen-widget.d.ts +720 -0
  117. package/dist/lumen-widget.js +609 -0
  118. package/dist/market/compare-chart.d.ts +34 -0
  119. package/dist/market/compare-chart.js +292 -0
  120. package/dist/market/indicator-picker.d.ts +29 -0
  121. package/dist/market/indicator-picker.js +125 -0
  122. package/dist/market/indicators.d.ts +63 -0
  123. package/dist/market/indicators.js +314 -0
  124. package/dist/market/live.d.ts +120 -0
  125. package/dist/market/live.js +444 -0
  126. package/dist/market/lumen-chart.d.ts +88 -0
  127. package/dist/market/lumen-chart.js +645 -0
  128. package/dist/market/market-widget.d.ts +45 -0
  129. package/dist/market/market-widget.js +346 -0
  130. package/dist/market/option-chain.d.ts +19 -0
  131. package/dist/market/option-chain.js +53 -0
  132. package/dist/market/price-chart.d.ts +128 -0
  133. package/dist/market/price-chart.js +619 -0
  134. package/dist/mcp-settings.d.ts +20 -0
  135. package/dist/mcp-settings.js +65 -0
  136. package/dist/nav.d.ts +77 -0
  137. package/dist/nav.js +73 -0
  138. package/dist/note-line.d.ts +13 -0
  139. package/dist/note-line.js +5 -0
  140. package/dist/notifications.d.ts +9 -0
  141. package/dist/notifications.js +166 -0
  142. package/dist/origin-legend.d.ts +14 -0
  143. package/dist/origin-legend.js +38 -0
  144. package/dist/overview.d.ts +84 -0
  145. package/dist/overview.js +105 -0
  146. package/dist/page-inputs.d.ts +14 -0
  147. package/dist/page-inputs.js +16 -0
  148. package/dist/page.d.ts +32 -0
  149. package/dist/page.js +45 -0
  150. package/dist/pager.d.ts +10 -0
  151. package/dist/pager.js +14 -0
  152. package/dist/prefs.d.ts +71 -0
  153. package/dist/prefs.js +126 -0
  154. package/dist/query-client.d.ts +7 -0
  155. package/dist/query-client.js +19 -0
  156. package/dist/refresh.d.ts +4 -0
  157. package/dist/refresh.js +19 -0
  158. package/dist/rename-field.d.ts +11 -0
  159. package/dist/rename-field.js +26 -0
  160. package/dist/reset-button.d.ts +9 -0
  161. package/dist/reset-button.js +10 -0
  162. package/dist/route-error.d.ts +42 -0
  163. package/dist/route-error.js +229 -0
  164. package/dist/schedules.d.ts +88 -0
  165. package/dist/schedules.js +221 -0
  166. package/dist/settings.d.ts +51 -0
  167. package/dist/settings.js +181 -0
  168. package/dist/shell.d.ts +99 -0
  169. package/dist/shell.js +137 -0
  170. package/dist/source-health.d.ts +12 -0
  171. package/dist/source-health.js +58 -0
  172. package/dist/sourced-card.d.ts +10 -0
  173. package/dist/sourced-card.js +69 -0
  174. package/dist/spotlight.d.ts +23 -0
  175. package/dist/spotlight.js +138 -0
  176. package/dist/status-health.d.ts +16 -0
  177. package/dist/status-health.js +41 -0
  178. package/dist/status.d.ts +9 -0
  179. package/dist/status.js +12 -0
  180. package/dist/summary-cards.d.ts +56 -0
  181. package/dist/summary-cards.js +270 -0
  182. package/dist/symbol-sheet-context.d.ts +4 -0
  183. package/dist/symbol-sheet-context.js +5 -0
  184. package/dist/symbol-sheet.d.ts +9 -0
  185. package/dist/symbol-sheet.js +24 -0
  186. package/dist/tasks.d.ts +29 -0
  187. package/dist/tasks.js +278 -0
  188. package/dist/theme.css +606 -0
  189. package/dist/tile.d.ts +37 -0
  190. package/dist/tile.js +23 -0
  191. package/dist/ui/alert-dialog.d.ts +18 -0
  192. package/dist/ui/alert-dialog.js +41 -0
  193. package/dist/ui/alert.d.ts +10 -0
  194. package/dist/ui/alert.js +27 -0
  195. package/dist/ui/avatar.d.ts +11 -0
  196. package/dist/ui/avatar.js +22 -0
  197. package/dist/ui/badge.d.ts +9 -0
  198. package/dist/ui/badge.js +27 -0
  199. package/dist/ui/breadcrumb.d.ts +11 -0
  200. package/dist/ui/breadcrumb.js +27 -0
  201. package/dist/ui/button-group.d.ts +11 -0
  202. package/dist/ui/button-group.js +27 -0
  203. package/dist/ui/button.d.ts +10 -0
  204. package/dist/ui/button.js +38 -0
  205. package/dist/ui/card.d.ts +11 -0
  206. package/dist/ui/card.js +24 -0
  207. package/dist/ui/chart.d.ts +44 -0
  208. package/dist/ui/chart.js +117 -0
  209. package/dist/ui/checkbox.d.ts +4 -0
  210. package/dist/ui/checkbox.js +8 -0
  211. package/dist/ui/collapsible.d.ts +5 -0
  212. package/dist/ui/collapsible.js +12 -0
  213. package/dist/ui/combobox.d.ts +24 -0
  214. package/dist/ui/combobox.js +60 -0
  215. package/dist/ui/command.d.ts +18 -0
  216. package/dist/ui/command.js +34 -0
  217. package/dist/ui/dialog.d.ts +23 -0
  218. package/dist/ui/dialog.js +50 -0
  219. package/dist/ui/dropdown-menu.d.ts +29 -0
  220. package/dist/ui/dropdown-menu.js +50 -0
  221. package/dist/ui/emoji-picker.d.ts +6 -0
  222. package/dist/ui/emoji-picker.js +28 -0
  223. package/dist/ui/empty.d.ts +11 -0
  224. package/dist/ui/empty.js +33 -0
  225. package/dist/ui/field.d.ts +24 -0
  226. package/dist/ui/field.js +65 -0
  227. package/dist/ui/hover-card.d.ts +6 -0
  228. package/dist/ui/hover-card.js +13 -0
  229. package/dist/ui/input-group.d.ts +16 -0
  230. package/dist/ui/input-group.js +56 -0
  231. package/dist/ui/input.d.ts +3 -0
  232. package/dist/ui/input.js +6 -0
  233. package/dist/ui/item.d.ts +23 -0
  234. package/dist/ui/item.js +67 -0
  235. package/dist/ui/kbd.d.ts +3 -0
  236. package/dist/ui/kbd.js +9 -0
  237. package/dist/ui/label.d.ts +4 -0
  238. package/dist/ui/label.js +7 -0
  239. package/dist/ui/popover.d.ts +10 -0
  240. package/dist/ui/popover.js +25 -0
  241. package/dist/ui/progress.d.ts +4 -0
  242. package/dist/ui/progress.js +9 -0
  243. package/dist/ui/resizable.d.ts +7 -0
  244. package/dist/ui/resizable.js +13 -0
  245. package/dist/ui/scroll-area.d.ts +5 -0
  246. package/dist/ui/scroll-area.js +10 -0
  247. package/dist/ui/select.d.ts +15 -0
  248. package/dist/ui/select.js +36 -0
  249. package/dist/ui/separator.d.ts +4 -0
  250. package/dist/ui/separator.js +7 -0
  251. package/dist/ui/sheet.d.ts +14 -0
  252. package/dist/ui/sheet.js +54 -0
  253. package/dist/ui/sidebar.d.ts +69 -0
  254. package/dist/ui/sidebar.js +206 -0
  255. package/dist/ui/skeleton.d.ts +2 -0
  256. package/dist/ui/skeleton.js +6 -0
  257. package/dist/ui/slider.d.ts +4 -0
  258. package/dist/ui/slider.js +9 -0
  259. package/dist/ui/sonner.d.ts +3 -0
  260. package/dist/ui/sonner.js +24 -0
  261. package/dist/ui/spinner.d.ts +2 -0
  262. package/dist/ui/spinner.js +7 -0
  263. package/dist/ui/switch.d.ts +6 -0
  264. package/dist/ui/switch.js +7 -0
  265. package/dist/ui/table.d.ts +10 -0
  266. package/dist/ui/table.js +31 -0
  267. package/dist/ui/tabs.d.ts +7 -0
  268. package/dist/ui/tabs.js +22 -0
  269. package/dist/ui/textarea.d.ts +3 -0
  270. package/dist/ui/textarea.js +6 -0
  271. package/dist/ui/toggle-group.d.ts +10 -0
  272. package/dist/ui/toggle-group.js +22 -0
  273. package/dist/ui/toggle.d.ts +9 -0
  274. package/dist/ui/toggle.js +25 -0
  275. package/dist/ui/tooltip.d.ts +7 -0
  276. package/dist/ui/tooltip.js +16 -0
  277. package/dist/view-edit-cell.d.ts +49 -0
  278. package/dist/view-edit-cell.js +198 -0
  279. package/dist/view-editor.d.ts +52 -0
  280. package/dist/view-editor.js +1081 -0
  281. package/dist/view-formulas.d.ts +61 -0
  282. package/dist/view-formulas.js +247 -0
  283. package/dist/view-icons.d.ts +20 -0
  284. package/dist/view-icons.js +70 -0
  285. package/dist/view-model.d.ts +390 -0
  286. package/dist/view-model.js +316 -0
  287. package/dist/whats-new.d.ts +3 -0
  288. package/dist/whats-new.js +53 -0
  289. package/dist/widget-controls.d.ts +9 -0
  290. package/dist/widget-controls.js +26 -0
  291. package/dist/widget-data.d.ts +4 -0
  292. package/dist/widget-data.js +32 -0
  293. package/dist/widget-settings.d.ts +516 -0
  294. package/dist/widget-settings.js +290 -0
  295. package/dist/widget-thumbs.d.ts +3 -0
  296. package/dist/widget-thumbs.js +29 -0
  297. package/dist/workspace-keys.d.ts +12 -0
  298. package/dist/workspace-keys.js +13 -0
  299. package/dist/workspaces.d.ts +57 -0
  300. package/dist/workspaces.js +473 -0
  301. package/package.json +82 -4
@@ -0,0 +1,645 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { controlOn, isCountAgg, isDollarFormat, viewPageSizes, viewPaginationSchema, } from '@columenlabs/lumen-ui-types/views';
3
+ // Custom views: any number of views on any app, one page for all of them.
4
+ // A data domain (holdings, chains, bars, series, research results) describes its fields once in a
5
+ // `ViewDomain`; a view is a saved `CustomViewConfig` over that domain, written in the page's Edit
6
+ // dialog or by an agent through MCP. This file is LumenView, the view as a card (its table, grouped
7
+ // one level deep with collapsible group rows, pivot, totals, sorting, paging), LumenTable and the
8
+ // details drawer; LumenLayout (lumen-layout.tsx) is the page around it. Rows are the ones the app
9
+ // already loaded; everything here runs on the client.
10
+ import { DndContext, closestCenter } from '@dnd-kit/core';
11
+ import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
12
+ import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from '@tanstack/react-table';
13
+ import { ArrowDownIcon, ArrowUpIcon, ChartCandlestick, ChartSpline, ChevronLeftIcon, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, Grid3x3Icon, LayoutDashboardIcon, PencilIcon, PlusIcon, Settings2Icon, SigmaIcon, SparklesIcon, TableIcon, UserPenIcon, } from 'lucide-react';
14
+ import Papa from 'papaparse';
15
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { useAssistantNote } from './assistant-context.js';
17
+ import { ColorChip, ColorDot } from './color-dot.js';
18
+ import { DrawerSection } from './drawer-section.js';
19
+ import { useFloatingHead } from './floating-head.js';
20
+ import { FreshnessMark, freshnessLine, freshnessOf } from './freshness.js';
21
+ import { useIsMobile } from './hooks/use-mobile.js';
22
+ import { IconButton } from './icon-button.js';
23
+ import { cn } from './lib/utils.js';
24
+ import { LumenTokenizedFilter, useLumenFilter, } from './lumen-filter.js';
25
+ import { useGridCell } from './lumen-grid.js';
26
+ import { LumenSelector } from './lumen-selector.js';
27
+ import { LumenLazy, LumenLoadFooter, LumenRefresh, LumenSkeleton, LumenStates, useLiveLoad, } from './lumen-source.js';
28
+ import { LumenWidget } from './lumen-widget.js';
29
+ import { MAX_COMPARE, useSymbolChart } from './market/lumen-chart.js';
30
+ import { Page } from './page.js';
31
+ import { LumenPager } from './pager.js';
32
+ import { maskAmount, useAmountsHidden } from './shell.js';
33
+ import { useSymbolSheet } from './symbol-sheet-context.js';
34
+ import { Tile } from './tile.js';
35
+ import { Badge } from './ui/badge.js';
36
+ import { Button } from './ui/button.js';
37
+ import { Card } from './ui/card.js';
38
+ import { Checkbox } from './ui/checkbox.js';
39
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
40
+ import { Popover, PopoverAnchor, PopoverContent } from './ui/popover.js';
41
+ import { Skeleton } from './ui/skeleton.js';
42
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
43
+ import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
44
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
45
+ import { EditCell, EditInputCell, EditManyCell } from './view-edit-cell.js';
46
+ import { ViewEditDialog } from './view-editor.js';
47
+ import { fieldOptions } from './view-formulas.js';
48
+ import { CHIP_TONE, FORMULAS_GROUP, chipsOf, computedOf, filterFieldsOf, groupOf, label, pick, rollUp, rollsUp, subgroupOf, withSavedFilters, withViewFields, } from './view-model.js';
49
+ export { CHIP_TONE, FORMULAS_GROUP, RUN_STATE_TONE } from './view-model.js';
50
+ export { rollUp } from './view-model.js';
51
+ /** Rows a table draws per group, or per unpaged table, before "Show more". */
52
+ const DRAW_STEP = 200;
53
+ /** Past this many rows a grouped table opens with only its first group open. */
54
+ const OPEN_ALL = 500;
55
+ /**
56
+ * Rows that share the domain's merge key, folded into one row each: within each group, or across
57
+ * every row when nothing is grouped (no duplicate rows in the flat table).
58
+ */
59
+ export function mergeRows(domain, rows, groupBy) {
60
+ const merge = domain.merge;
61
+ if (!merge)
62
+ return rows;
63
+ const m = new Map();
64
+ for (const r of rows) {
65
+ const k = `${groupBy ? label(groupBy.value(r)) : ''}\u0000${merge.key(r)}`;
66
+ const b = m.get(k);
67
+ b ? b.push(r) : m.set(k, [r]);
68
+ }
69
+ return [...m.values()].map((rs) => (rs.length > 1 ? merge.rows(rs) : rs[0]));
70
+ }
71
+ const MINUS = '−';
72
+ /** Text cells longer than this many characters are cut to one line with a tooltip. */
73
+ const LONG_TEXT = 48;
74
+ /** Rows of a view the assistant sees with each message. */
75
+ const ASSISTANT_ROWS = 10;
76
+ const formatter = (digits) => new Intl.NumberFormat('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits });
77
+ const FMT = { 0: formatter(0), 2: formatter(2) };
78
+ /** Integer: no decimals and no thousands commas (a year reads 2026). */
79
+ const PLAIN = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0, useGrouping: false });
80
+ const digitsOf = (format, compact) => compact || format === 'compact'
81
+ ? COMPACT
82
+ : format === 'integer'
83
+ ? PLAIN
84
+ : format === 'rounded' || format === 'price_rounded'
85
+ ? FMT[0]
86
+ : FMT[2];
87
+ const COMPACT = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
88
+ /** A value as its field's format reads: tabular figures, a true minus, gain/loss on signed values. */
89
+ export function ViewValueCell({ value, format, chip, tone, color, amount, compact, empty, }) {
90
+ const hidden = useAmountsHidden() && !!amount;
91
+ if (value === null || (typeof value === 'number' && !Number.isFinite(value))) {
92
+ return _jsx("span", { className: "num text-muted-foreground", children: empty ?? '—' });
93
+ }
94
+ if (chip && typeof value === 'string')
95
+ return color ? (_jsx(ColorChip, { color: color, children: value })) : (_jsx(Badge, { variant: "secondary", className: cn('font-normal', tone && CHIP_TONE[tone]), children: value }));
96
+ if (color && typeof value === 'string')
97
+ return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1", children: [_jsx(ColorDot, { color: color, className: "size-3" }), _jsx("span", { className: "truncate", children: value })] }));
98
+ if (typeof value === 'string' || format === 'text') {
99
+ const text = hidden ? maskAmount(String(value)) : String(value);
100
+ // Long text stays on one line, cut with an ellipsis; the whole text shows on hover.
101
+ if (text.length <= LONG_TEXT)
102
+ return _jsx("span", { children: text });
103
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "block max-w-[24rem] truncate", children: text }) }), _jsx(TooltipContent, { className: "max-w-md whitespace-normal", children: text })] }));
104
+ }
105
+ const abs = hidden ? maskAmount('0') : digitsOf(format, compact).format(Math.abs(value));
106
+ const signed = format === 'signed' || format === 'percent' || format === 'price_signed';
107
+ const sign = value < 0 ? MINUS : signed && value > 0 ? '+' : '';
108
+ const dollar = isDollarFormat(format) ? '$' : '';
109
+ return (_jsx("span", { className: cn('num whitespace-nowrap', signed && (value > 0 ? 'text-gain' : value < 0 ? 'text-loss' : '')), children: `${sign}${dollar}${abs}${format === 'percent' || format === 'weight' ? '%' : ''}` }));
110
+ }
111
+ /** A row's first cell with its inline chips (a view's `inline` chips), editable where the field is. */
112
+ function FirstCell({ value, chips, }) {
113
+ return (_jsxs("span", { className: "flex items-center gap-1.5 whitespace-nowrap", children: [value, chips.map((c) => (_jsx(Fragment, { children: c.node }, c.key)))] }));
114
+ }
115
+ /** The row styling every Total and group header shares. */
116
+ const ROLLUP = 'bg-muted/50 font-medium hover:bg-muted/50';
117
+ // One scroller for the whole view: the tiles, filter and table scroll together
118
+ // in one box, one screen tall, so a swipe never stalls where the header pins and the tiles leave the
119
+ // screen before the rows move. The header and group rows pin with native sticky inside it (the shadcn
120
+ // sticky-header data table pattern); group rows sit right under the header, overlapping it by 1px so
121
+ // no gap ever shows, and the next group row slides over the one pinned. Wide tables scroll sideways
122
+ // in the same box while the tiles and filter stay put. Cards draw their outline as a border inside
123
+ // their box, so no scroller clips it. The box spans the
124
+ // page edge to edge and draws the gutter and max width as its own side padding, so its scrollbar sits
125
+ // at the screen's edge, never over the cards; its page body drops its side
126
+ // padding and max width (`VIEW_PAGE`).
127
+ // The bottom gutter is its padding too, and the box is always one screen tall (not just at most), so a
128
+ // sideways scrollbar sits on the status bar even on a short page, never floating inside it. Rows pinned to the view's width use `--view-w`, the box's width less its gutters and
129
+ // scrollbar (`100cqw` counts the scrollbar, so it alone overflows by the bar's width and draws a
130
+ // needless sideways bar); the scroller sets `--view-sb` to its vertical bar's width.
131
+ export const VIEW_SCROLL = '@container h-[calc(100svh-var(--shell-header-height)-var(--shell-status-height)-var(--shell-gutter))] overflow-auto px-[max(var(--shell-gutter),calc((100%-var(--shell-max-width))/2+var(--shell-gutter)))] pb-(--shell-gutter) [--view-w:calc(100cqw-var(--view-sb,0px))]';
132
+ /** The page body around `VIEW_SCROLL`: the scroller draws the side and bottom gutter and max width. */
133
+ export const VIEW_PAGE = 'max-w-none px-0 pb-0';
134
+ /** A fit table's text cells wrap (numbers keep their own nowrap), so the table fits its width. */
135
+ const FIT = '[&_td]:whitespace-normal [&_td]:break-words [&_th]:whitespace-normal';
136
+ /** The table inside the view's scroller: it never scrolls on its own, so sticky rows pin to the view.
137
+ * It clips up and down (clip, unlike hidden, makes no scroller), which hides the pinned header's
138
+ * cap while the header sits at the table's top. */
139
+ const IN_VIEW = '[&>[data-slot=table-container]]:overflow-x-visible [&>[data-slot=table-container]]:overflow-y-clip';
140
+ /** A pinned header cell. Its cap fills the band between a sticky tab row and the header (the
141
+ * tab row's fade, --intro-fade) with the card, so rows scrolled past the header never show above
142
+ * it; the card still fades out under the tabs. */
143
+ const PINNED_HEAD = "sticky top-(--pin-top,0px) z-20 bg-card shadow-[inset_0_-1px_0_var(--color-border)] before:pointer-events-none before:absolute before:inset-x-0 before:bottom-full before:h-(--intro-fade,0px) before:bg-card before:content-['']";
144
+ const PINNED_GROUP = 'sticky top-[calc(var(--pin-top,0px)+var(--row-h)-1px)] z-10 bg-[color-mix(in_oklab,var(--color-muted)_50%,var(--color-card))] shadow-[inset_0_1px_0_var(--color-border),inset_0_-1px_0_var(--color-border)]';
145
+ function SortIcon({ dir }) {
146
+ if (dir === 'asc')
147
+ return _jsx(ArrowUpIcon, { className: "size-3" });
148
+ if (dir === 'desc')
149
+ return _jsx(ArrowDownIcon, { className: "size-3" });
150
+ return null;
151
+ }
152
+ /** The details: a folded row's own rows as a table, then the domain's sections. */
153
+ export function DetailsBody({ domain, row, columns, chips, }) {
154
+ const parts = domain.parts?.rows(row) ?? [];
155
+ const by = domain.fields.find((f) => f.key === domain.parts?.by);
156
+ const sections = [
157
+ ...(parts.length > (domain.parts?.others ? 0 : 1) && by && domain.parts
158
+ ? [
159
+ {
160
+ id: '_parts',
161
+ label: domain.parts.label,
162
+ icon: by.icon,
163
+ open: true,
164
+ count: () => parts.length,
165
+ render: () => (_jsx("div", { className: "-mx-1 rounded-md border", children: _jsx(LumenTable, { domain: domain,
166
+ // Text the rows all share (the symbol, the kind) says nothing here.
167
+ columns: [
168
+ by,
169
+ ...columns.filter((f) => f.key !== by.key &&
170
+ (f.format !== 'text' || new Set(parts.map((r) => f.value(r))).size > 1)),
171
+ ], rows: parts, groupBy: null, chips: chips }) })),
172
+ },
173
+ ]
174
+ : []),
175
+ ...(domain.details ?? []),
176
+ ];
177
+ return (_jsx(_Fragment, { children: sections.map((s) => (_jsx(DrawerSection, { icon: s.icon, title: s.label, count: s.count?.(row), hint: s.hint?.(row), defaultOpen: typeof s.open === 'function' ? s.open(row) : s.open, children: s.render(row) }, `${domain.rowKey(row)}:${s.id}`))) }));
178
+ }
179
+ /** Columns in a view's header: every field of the domain, ticked when it shows. */
180
+ function ColumnPicker({ domain, columns, onColumns, open, onOpenChange, className, }) {
181
+ const keys = columns.map((f) => f.key);
182
+ return (_jsx(LumenSelector, { multiple: true, trigger: "icon", icon: Columns3CogIcon, label: "Columns", align: "end", search: true, open: open, onOpenChange: onOpenChange, className: className, options: fieldOptions(domain.fields, domain), value: keys, onChange: (next) => onColumns(next.some((k) => !keys.includes(k))
183
+ ? domain.fields.map((x) => x.key).filter((k) => next.includes(k))
184
+ : keys.filter((k) => next.includes(k))) }));
185
+ }
186
+ /**
187
+ * The view's rows as a CSV file: the filtered rows in the view's sort, its
188
+ * shown columns (the grouping first when it is not one), plain numbers; amounts stay masked while
189
+ * Hide amounts is on.
190
+ */
191
+ function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
192
+ const fields = groupBy && !columns.includes(groupBy) ? [groupBy, ...columns] : columns;
193
+ const by = sort && fields.find((f) => f.key === sort.key);
194
+ const sorted = by
195
+ ? [...rows].sort((a, b) => {
196
+ const x = by.value(a);
197
+ const y = by.value(b);
198
+ if (x === null || y === null)
199
+ return x === y ? 0 : x === null ? 1 : -1;
200
+ const c = typeof x === 'number' && typeof y === 'number'
201
+ ? x - y
202
+ : String(x).localeCompare(String(y));
203
+ return sort?.desc ? -c : c;
204
+ })
205
+ : rows;
206
+ const cell = (f, r) => {
207
+ const v = f.value(r);
208
+ if (v === null || (typeof v === 'number' && !Number.isFinite(v)))
209
+ return '';
210
+ return hidden && f.amount ? maskAmount(String(v)) : v;
211
+ };
212
+ const csv = Papa.unparse({
213
+ fields: fields.map((f) => f.label),
214
+ data: sorted.map((r) => fields.map((f) => cell(f, r))),
215
+ });
216
+ const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
217
+ const a = document.createElement('a');
218
+ a.href = url;
219
+ a.download = `${name.trim().replace(/[^\w.-]+/g, '-') || 'view'}-${new Date().toISOString().slice(0, 10)}.csv`;
220
+ a.click();
221
+ URL.revokeObjectURL(url);
222
+ }
223
+ /**
224
+ * LumenView (a card of its own): a data
225
+ * view over one domain, as a card. Its header has the view's name, the row count and pager, its ↻
226
+ * and ⋯, and its own filter and Group (they narrow this table only); its body
227
+ * the table (grouped one level, or the pivot) with the loading states.
228
+ */
229
+ export function LumenView({ domain, config, rows, total = rows.length, groupBy = null, pivot = false, subgroup, load: sourceLoad, state, title = config.name, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller = false, cardId = 'view', tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort = config.sort, onSort, shown, }) {
230
+ // Its controls (Edit widget ▸ Controls): Pager, Columns and Download here.
231
+ const on = (k) => controlOn({ kind: 'table', controls: shown }, k);
232
+ const cell = useGridCell();
233
+ // A table on the grid scrolls sideways inside its own card; a full-width one out of the grid
234
+ // scrolls with the page.
235
+ const inLayout = inScroller && !cell;
236
+ const columns = pick(domain.fields, config.columns);
237
+ // Live columns on screen keep their source reading while the market trades (#861).
238
+ const load = useLiveLoad(sourceLoad, columns.some((c) => {
239
+ const r = freshnessOf(domain, c);
240
+ return r === 'live' || r === 'calc';
241
+ }));
242
+ const chips = chipsOf(config);
243
+ const tableRows = useMemo(() => mergeRows(domain, rows, groupBy), [domain, rows, groupBy]);
244
+ const amountsHidden = useAmountsHidden();
245
+ // Flat tables page their rows, 25 a page unless the view says otherwise; the size picker and
246
+ // the page buttons sit in the card's header. Views saved before then
247
+ // carry no pagination, so they read the defaults.
248
+ const isMobile = useIsMobile();
249
+ const pagination = config.pagination ?? viewPaginationSchema.parse({});
250
+ const [pageSize, setPageSize] = useState(pagination.size);
251
+ const [pageIndex, setPageIndex] = useState(0);
252
+ useEffect(() => setPageSize(pagination.size), [pagination.size]);
253
+ const sub = subgroupOf(domain, subgroup === undefined ? config.subgroup : subgroup);
254
+ // Folded lines and their rows stay on one page, so a fold shows every row like a grouping.
255
+ const paging = pagination.on && on('pager') && !state && !groupBy && !pivot && !sub?.under;
256
+ // A fit table wraps its text to the width there is; the rest scroll sideways.
257
+ // A phone has no width to fit to: every table scrolls sideways there.
258
+ const fit = config.width === 'fit' && !pivot && !isMobile;
259
+ const pages = Math.max(1, Math.ceil(tableRows.length / pageSize));
260
+ const page = Math.min(pageIndex, pages - 1);
261
+ const count = rows.length === total ? `${total} rows` : `${rows.length} of ${total} rows`;
262
+ // A new filter or page size starts from the first page.
263
+ // biome-ignore lint/correctness/useExhaustiveDependencies: resets on these changes only
264
+ useEffect(() => setPageIndex(0), [rows, pageSize]);
265
+ // Checked rows with a chart symbol open one compare chart (up to 8).
266
+ const openChart = useSymbolChart();
267
+ const chartField = domain.fields.find((f) => f.chart);
268
+ const picked = useMemo(() => {
269
+ if (!selection?.keys.size || !chartField?.chart)
270
+ return [];
271
+ const out = new Set();
272
+ for (const r of rows)
273
+ if (selection.keys.has(domain.rowKey(r))) {
274
+ const t = chartField.chart(r);
275
+ if (t)
276
+ out.add(t);
277
+ }
278
+ return [...out].slice(0, MAX_COMPARE);
279
+ }, [selection, chartField, rows, domain]);
280
+ const compare = openChart && picked.length > 1 ? (_jsx(IconButton, { icon: ChartSpline, label: `Compare ${picked.join(', ')}`, variant: "outline", onClick: () => openChart(picked) })) : null;
281
+ const pager = paging ? (_jsx(LumenPager, { total: tableRows.length, page: page, size: pageSize, onPage: setPageIndex, onSize: setPageSize, noun: rows.length === total ? 'rows' : `of ${total} rows` })) : (_jsx("span", { className: "num px-2 text-xs text-muted-foreground", children: count }));
282
+ const table = pivot && config.pivot ? (_jsx(PivotTable, { domain: domain, rows: rows, pivot: config.pivot })) : (_jsx(LumenTable, { domain: domain, columns: columns, rows: tableRows, groupBy: groupBy, subgroup: sub, chips: chips, page: paging ? { index: page, size: pageSize } : null, inView: inLayout, fit: fit, onDetails: onDetails, tapped: tapped, selection: selection, groupActions: groupActions, sort: sort, onSort: onSort, total: config.total !== false, add: onAddColumn || onColumns
283
+ ? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
284
+ : undefined }));
285
+ return (_jsx(LumenWidget, { id: cardId, kind: "table", style: "table", title: title, controls: controls, tools: state ? (tools) : (_jsxs(_Fragment, { children: [tools, compare, pager, onColumns && !onAddColumn && on('columns') && (_jsx(ColumnPicker, { domain: domain, columns: columns, onColumns: onColumns })), !pivot && on('download') && (_jsx(IconButton, { icon: DownloadIcon, label: "Download CSV", onClick: () => downloadCsv({
286
+ name: title,
287
+ columns,
288
+ rows: tableRows,
289
+ groupBy,
290
+ sort: config.sort,
291
+ hidden: amountsHidden,
292
+ }) }))] })), filter: state ? undefined : filter, filterCount: filterCount, strip: state ? undefined : strip, link: link, note: note, load: load, shown: shown, rows: total, pinned: inLayout && !fit, className: cn('relative', !inLayout && 'overflow-x-auto overflow-y-hidden [--pin-top:0px]'), children: state ??
293
+ (load ? (_jsx(LumenStates, { load: load, skeleton: _jsx(LumenSkeleton, { rows: 10 }), children: table })) : (table)) }));
294
+ }
295
+ /**
296
+ * The + at the end of a table's header (Edits in place): Field opens the
297
+ * Columns picker here, Formula the view's formula editor, Your data a column the person types,
298
+ * Ask the assistant the composer pointed at this card. On phone each opens as a sheet.
299
+ */
300
+ function AddColumnMenu({ add, fieldOpen, onFieldOpen, }) {
301
+ const yoursOff = add.yours && typeof add.yours !== 'function' ? add.yours.disabled : null;
302
+ const formulaOff = add.formula && typeof add.formula !== 'function' ? add.formula.disabled : null;
303
+ const only = add.field && !add.formula && !add.yours;
304
+ return (_jsxs("span", { className: "relative inline-flex items-center", children: [add.field && (_jsx(ColumnPicker, { ...add.field, open: fieldOpen, onOpenChange: onFieldOpen, className: only ? undefined : 'absolute inset-0 size-full opacity-0' })), !only && (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: PlusIcon, size: "icon-xs", label: "Add column", className: "relative text-muted-foreground" }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-52", children: [add.field && (_jsxs(DropdownMenuItem, { onSelect: () => onFieldOpen(true), children: [_jsx(Columns3CogIcon, {}), " Field", _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: "the data model" })] })), add.formula && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: _jsxs(DropdownMenuItem, { disabled: !!formulaOff, onSelect: typeof add.formula === 'function' ? add.formula : undefined, children: [_jsx(SigmaIcon, {}), " Formula", _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: "over columns" })] }) }) }), formulaOff && _jsx(TooltipContent, { children: formulaOff })] })), add.yours && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: _jsxs(DropdownMenuItem, { disabled: !!yoursOff, onSelect: typeof add.yours === 'function' ? add.yours : undefined, children: [_jsx(UserPenIcon, {}), " Your data", _jsx("span", { className: "ml-auto text-muted-foreground text-xs", children: "you type it" })] }) }) }), yoursOff && _jsx(TooltipContent, { children: yoursOff })] }))] })] }))] }));
305
+ }
306
+ /** A field's colour for its header: its topic's source (a broker green, an app brown) where
307
+ * the group names one, else its group's (Holding, Symbol, Account, Formulas). */
308
+ export function colorOf(domain, f) {
309
+ if (!f || !domain.groups?.length)
310
+ return null;
311
+ const id = groupOf(domain, f);
312
+ if (id === FORMULAS_GROUP)
313
+ return 'var(--color-muted-foreground)';
314
+ const g = domain.groups.find((x) => x.id === id);
315
+ return (f.topic && g?.sources?.[f.topic]?.color) || g?.color || null;
316
+ }
317
+ /** Where a field comes from, for its header's tooltip: Holding › Basket,
318
+ * Symbol › Analysts › Target · a broker live. */
319
+ function pathOf(domain, f) {
320
+ const id = groupOf(domain, f);
321
+ const g = id === FORMULAS_GROUP ? undefined : domain.groups?.find((x) => x.id === id);
322
+ const path = [id === FORMULAS_GROUP ? 'Formulas' : g?.label, f.topic, f.label]
323
+ .filter(Boolean)
324
+ .join(' › ');
325
+ const src = f.topic ? g?.sources?.[f.topic] : undefined;
326
+ return src ? `${path} · ${[src.name, src.detail].filter(Boolean).join(' ')}` : path;
327
+ }
328
+ /** Every header carries its colour (`colorOf`), text and a 2px rule. */
329
+ const ruleOf = (color) => color ? { boxShadow: `inset 0 -2px 0 ${color}` } : undefined;
330
+ const alignOf = (format) => (format === 'text' ? 'text-left' : 'text-right');
331
+ /** A column's cell classes: its alignment, and hidden on phones where the field says so. */
332
+ /** An ISO date, alone or with a time after it: `2026-10-14`, `2026-10-14 16:17`. */
333
+ const isDateText = (v) => typeof v === 'string' && v.length <= 25 && /^\d{4}-\d{2}-\d{2}(?:$|[ T])/.test(v);
334
+ const colOf = (f) => cn(alignOf(f?.format ?? 'text'), f?.hideOnPhone && 'max-md:hidden');
335
+ /** The flat or grouped table: sortable headers, a Total row, group rows that collapse. */
336
+ // Sorting, and paging of flat tables (the page is LumenView's, set right of the row count).
337
+ const SORTING = tableFeatures({
338
+ rowSortingFeature,
339
+ rowPaginationFeature,
340
+ sortedRowModel: createSortedRowModel(),
341
+ paginatedRowModel: createPaginatedRowModel(),
342
+ sortFns,
343
+ });
344
+ /** The plain table LumenView draws, also standalone for lists that are not a whole view; with
345
+ * `load` it shows its own skeleton, progress bar and failure. */
346
+ export function LumenTable({ load, ...props }) {
347
+ if (!load)
348
+ return _jsx(LumenTableRows, { ...props });
349
+ return (_jsx("div", { className: "relative", children: _jsx(LumenStates, { load: load, children: _jsx(LumenTableRows, { ...props }) }) }));
350
+ }
351
+ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null, chips = [], page = null, inView = false, fit = false, onDetails, tapped, selection, groupActions, sort, onSort, total = true, subgroup = null, add, }) {
352
+ const [sorting, setSorting] = useState(sort && columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : []);
353
+ // A saved sort patched while the table is open (the assistant's card move) applies at once.
354
+ const savedSort = JSON.stringify(sort ?? null);
355
+ const savedSortRef = useRef(savedSort);
356
+ useEffect(() => {
357
+ if (savedSortRef.current === savedSort)
358
+ return;
359
+ savedSortRef.current = savedSort;
360
+ setSorting(sort && columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : []);
361
+ }, [savedSort, sort, columns]);
362
+ const paging = page !== null && !groupBy;
363
+ const pagination = {
364
+ pageIndex: paging ? page.index : 0,
365
+ pageSize: paging ? page.size : Math.max(rows.length, 1),
366
+ };
367
+ // Groups whose state differs from where they start: open, or past OPEN_ALL rows only the first.
368
+ const [flipped, setFlipped] = useState(new Set());
369
+ const [more, setMore] = useState(new Map());
370
+ // The grouped field is the group row's label, so its column hides.
371
+ // Each key once: two columns under one React key can lose a cell on a re-render, and every
372
+ // row after it slides left by one.
373
+ const visible = columns.filter((f, i) => f.key !== groupBy?.key && columns.findIndex((x) => x.key === f.key) === i);
374
+ // Inline chips sit on the first column, next to the row's name, not in their own.
375
+ const inline = visible.filter((f, i) => i > 0 && chips.some((c) => c.key === f.key && c.placement === 'inline'));
376
+ const shown = visible.filter((f) => !inline.includes(f));
377
+ const computed = useMemo(() => computedOf(domain, rows, groupBy), [domain, rows, groupBy]);
378
+ // Values per editable field, offered when a cell is edited: the field's own list when it has
379
+ // one (every value, in use or not), else the values in the shown rows.
380
+ const options = useMemo(() => {
381
+ const m = new Map();
382
+ for (const f of visible)
383
+ if (f.edit)
384
+ m.set(f.key, f.edit.options?.() ?? {
385
+ inUse: [
386
+ ...new Set(rows
387
+ .flatMap((r) => (f.values ? f.values(r) : [f.value(r)]))
388
+ .filter((v) => typeof v === 'string' && v !== '')),
389
+ ].sort(),
390
+ unused: [],
391
+ });
392
+ return m;
393
+ }, [visible, rows]);
394
+ // A symbol cell opens the app's chart dialog, where the app mounts one.
395
+ // Where the app also mounts the Symbol sheet, the ticker opens the sheet and the chart
396
+ // icon still opens the chart dialog.
397
+ const openSheet = useSymbolSheet();
398
+ const openChart = useSymbolChart();
399
+ const cellOf = (f, row, chip) => {
400
+ const ticker = (openSheet || openChart) && f.chart?.(row);
401
+ const cell = plainCellOf(f, row, chip);
402
+ if (!ticker)
403
+ return cell;
404
+ const stop = (e) => e.stopPropagation();
405
+ // Muted until hovered, then lit.
406
+ const icon = (_jsx(ChartCandlestick, { className: "size-3.5 text-muted-foreground/50 transition-colors group-hover/button:text-primary group-focus-visible/button:text-primary" }));
407
+ const button = (label, open, children) => (_jsx(Button, { variant: "link", className: "h-auto gap-1.5 p-0 font-[inherit] text-inherit", "aria-label": label, onClick: (e) => {
408
+ stop(e);
409
+ open(ticker);
410
+ }, onKeyDown: stop, children: children }));
411
+ if (!openSheet)
412
+ return (openChart &&
413
+ button(`Chart ${ticker}`, openChart, _jsxs(_Fragment, { children: [cell, icon] })));
414
+ return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [button(`Open ${ticker}`, openSheet, cell), openChart && button(`Chart ${ticker}`, (t) => openChart(t), icon)] }));
415
+ };
416
+ // A date never wraps, in any table, fit or not.
417
+ const plainCellOf = (f, row, chip) => {
418
+ const v = computed.value(f, row);
419
+ const cell = valueCellOf(f, row, chip, v);
420
+ return isDateText(v) ? _jsx("span", { className: "whitespace-nowrap", children: cell }) : cell;
421
+ };
422
+ const valueCellOf = (f, row, chip, v) => {
423
+ if (f.render)
424
+ return f.render(row);
425
+ if (!f.edit)
426
+ return (_jsx(ViewValueCell, { value: v, format: f.format, chip: chip, tone: chip ? f.tone?.(row) : undefined, color: typeof v === 'string' ? f.color?.(v) : undefined, amount: f.amount, compact: f.compact, empty: f.empty }));
427
+ const edit = f.edit;
428
+ const many = edit.many;
429
+ if (many)
430
+ 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) }));
431
+ const save = edit.save;
432
+ if (!save)
433
+ return _jsx(ViewValueCell, { value: v, format: f.format, chip: chip });
434
+ if (edit.input)
435
+ return (_jsx(EditInputCell, { label: f.label, value: typeof v === 'number' || typeof v === 'string' ? v : null, type: edit.input, edited: edit.edited?.(row) ?? false, onSave: (value) => save(row, value), children: _jsx(ViewValueCell, { value: v, format: f.format, amount: f.amount, compact: f.compact }) }));
436
+ return (_jsx(EditCell, { label: f.label, value: typeof v === 'string' && v !== '' ? v : null, options: options.get(f.key)?.inUse ?? [], unused: options.get(f.key)?.unused, chip: chip, note: edit.note?.(row), colorOf: f.color, fixed: edit.fixed, edited: edit.edited?.(row) ?? false, onSave: (value) => save(row, value) }));
437
+ };
438
+ // A column's `cell` function is a component type to flexRender, so a new function means every
439
+ // cell remounts (a cell being typed into loses its box). The defs change
440
+ // only when the columns do; each cell reads the latest renderers through `latest`.
441
+ const latest = useRef({ shown, cellOf, inline, chips, computed });
442
+ latest.current = { shown, cellOf, inline, chips, computed };
443
+ const shownKeys = shown.map((f) => f.key).join(',');
444
+ const inlineOn = inline.length > 0;
445
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the columns shown; the rest comes through `latest`
446
+ const defs = useMemo(() => shown.map((f, i) => ({
447
+ id: f.key,
448
+ accessorFn: (r) => {
449
+ const { shown: now, computed } = latest.current;
450
+ return computed.value(now[i] ?? f, r) ?? undefined;
451
+ },
452
+ sortUndefined: 'last',
453
+ header: f.label,
454
+ cell: ({ row }) => {
455
+ const { shown: now, cellOf, inline, chips, computed } = latest.current;
456
+ const field = now[i] ?? f;
457
+ const chip = chips.some((c) => c.key === field.key);
458
+ return i === 0 && inline.length ? (_jsx(FirstCell, { value: cellOf(field, row.original, chip), chips: inline.flatMap((x) => {
459
+ const v = computed.value(x, row.original);
460
+ if (!x.edit && (v === null || v === ''))
461
+ return [];
462
+ return [{ key: x.key, node: cellOf(x, row.original, true) }];
463
+ }) })) : (cellOf(field, row.original, chip));
464
+ },
465
+ })), [shownKeys, inlineOn]);
466
+ const table = useTable({
467
+ features: SORTING,
468
+ data: rows,
469
+ columns: defs,
470
+ state: { sorting, pagination },
471
+ onSortingChange: (u) => {
472
+ const next = typeof u === 'function' ? u(sorting) : u;
473
+ setSorting(next);
474
+ const first = next[0];
475
+ onSort?.(first ? { key: first.id, desc: first.desc } : null);
476
+ },
477
+ autoResetPageIndex: false,
478
+ });
479
+ // Phone: a long press on a header opens the Columns picker as a sheet.
480
+ const [fieldOpen, setFieldOpen] = useState(false);
481
+ const press = useRef(null);
482
+ const pressStart = () => {
483
+ if (!add?.field)
484
+ return;
485
+ press.current = setTimeout(() => setFieldOpen(true), 500);
486
+ };
487
+ const pressEnd = () => {
488
+ if (press.current)
489
+ clearTimeout(press.current);
490
+ press.current = null;
491
+ };
492
+ const sorted = table.getRowModel().rows;
493
+ const groups = useMemo(() => {
494
+ if (!groupBy)
495
+ return null;
496
+ const m = new Map();
497
+ for (const r of sorted) {
498
+ const k = label(groupBy.value(r.original));
499
+ const b = m.get(k);
500
+ b ? b.push(r) : m.set(k, [r]);
501
+ }
502
+ return [...m];
503
+ }, [groupBy, sorted]);
504
+ const span = shown.length + (selection ? 1 : 0);
505
+ const startsOpen = (i) => rows.length <= OPEN_ALL || i === 0;
506
+ const isOpen = (key, i) => startsOpen(i) !== flipped.has(key);
507
+ const anyClosed = !!groups?.some(([k], i) => !isOpen(k, i));
508
+ const setAll = (open) => setFlipped(new Set((groups ?? []).filter((_, i) => startsOpen(i) !== open).map(([k]) => k)));
509
+ const keyOf = domain.rowKey;
510
+ const toggle = (keys, on) => {
511
+ if (!selection)
512
+ return;
513
+ const next = new Set(selection.keys);
514
+ for (const k of keys)
515
+ on ? next.add(k) : next.delete(k);
516
+ selection.onChange(next);
517
+ };
518
+ const allKeys = rows.map(keyOf);
519
+ const allOn = allKeys.length > 0 && allKeys.every((k) => selection?.keys.has(k));
520
+ const someOn = allKeys.some((k) => selection?.keys.has(k));
521
+ const selectCell = (key, name) => selection && (_jsx(TableCell, { className: "w-8 pr-0", onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: selection.keys.has(key), onCheckedChange: (on) => toggle([key], on === true), "aria-label": `Select ${name}` }) }));
522
+ const [openParts, setOpenParts] = useState(new Set());
523
+ const hasTotal = total && shown.some(rollsUp);
524
+ // A group row's label spans the leading columns that roll nothing up.
525
+ const rolls = (f) => !f.noRollup && (!!f.aggregate || !!f.rollFormula || !!f.share);
526
+ const lead = Math.max(1, shown.findIndex((f, i) => i > 0 && rolls(f)) < 0
527
+ ? shown.length
528
+ : shown.findIndex((f, i) => i > 0 && rolls(f)));
529
+ 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;
530
+ const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { className: colOf(f), children: rollCell(f, members) }, f.key)));
531
+ // A row's chevron opens one level under it, closed by default: its panel, else the view's
532
+ // sub-group ("Each row opens"; None opens nothing).
533
+ const partsOf = (row) => (domain.panel ? [] : (subgroup?.rows?.(row) ?? []));
534
+ const panelOf = (row) => domain.panel?.render(row) ?? null;
535
+ const opens = (row) => (domain.panel ? panelOf(row) !== null : partsOf(row).length > 1);
536
+ const openLabel = () => domain.panel?.label ?? subgroup?.label.toLowerCase() ?? 'parts';
537
+ const folds = !domain.panel && !!subgroup?.under;
538
+ const nests = folds || sorted.some((r) => opens(r.original));
539
+ const chevron = (k, name) => (_jsx(Button, { variant: "ghost", size: "icon-xs", className: "-ml-1", "aria-expanded": openParts.has(k), "aria-label": `${openParts.has(k) ? 'Hide' : 'Show'} ${name}`, onClick: (e) => {
540
+ e.stopPropagation();
541
+ setOpenParts((o) => {
542
+ const n = new Set(o);
543
+ n.has(k) ? n.delete(k) : n.add(k);
544
+ return n;
545
+ });
546
+ }, children: _jsx(ChevronRightIcon, { className: cn('transition-transform', openParts.has(k) && 'rotate-90') }) }));
547
+ const partRow = (p, name) => (_jsxs(TableRow, { className: cn('bg-muted/30 text-muted-foreground hover:bg-muted/40', onDetails && 'cursor-pointer'), onClick: onDetails ? () => onDetails(p) : undefined, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { className: cn(colOf(f), i === 0 && 'pl-12'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(CornerDownRightIcon, { className: "size-3.5 shrink-0" }), name
548
+ ? name(p)
549
+ : cellOf(f, p, chips.some((c) => c.key === f.key))] })) : (cellOf(f, p, chips.some((c) => c.key === f.key))) }, f.key)))] }, `part:${keyOf(p)}`));
550
+ // The whole row opens the details drawer.
551
+ const dataRow = (r) => (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": domain.rowKey(r.original), "data-state": tapped?.(r.original) ? 'selected' : undefined, className: cn('group/row', onDetails && 'cursor-pointer'), tabIndex: onDetails ? 0 : undefined, onClick: onDetails ? () => onDetails(r.original) : undefined, onKeyDown: onDetails
552
+ ? (e) => {
553
+ if (e.target !== e.currentTarget)
554
+ return;
555
+ if (e.key === 'Enter' || e.key === ' ') {
556
+ e.preventDefault();
557
+ onDetails(r.original);
558
+ }
559
+ }
560
+ : undefined, children: [selectCell(keyOf(r.original), keyOf(r.original)), r.getAllCells().map((c, i) => (_jsx(TableCell, { className: cn(colOf(shown[i]), groupBy && i === 0 && 'pl-8'), style: { background: shown[i]?.tint?.(r.original) }, children: i === 0 && nests ? (_jsxs("span", { className: "flex items-center gap-1", children: [opens(r.original) ? (chevron(keyOf(r.original), openLabel())) : (_jsx("span", { className: "-ml-1 size-6 shrink-0" })), flexRender(c.column.columnDef.cell, c.getContext())] })) : (flexRender(c.column.columnDef.cell, c.getContext())) }, c.id)))] }, domain.rowKey(r.original)), openParts.has(keyOf(r.original)) &&
561
+ partsOf(r.original).map((p) => partRow(p, subgroup?.name)), openParts.has(keyOf(r.original)) && domain.panel ? (_jsx(TableRow, { "data-slot": "row-panel", className: "bg-muted/20 hover:bg-muted/20", children: _jsx(TableCell, { colSpan: shown.length + (selection ? 1 : 0), className: "p-0 whitespace-normal", children: _jsx("div", { className: "sticky left-0 w-[min(56rem,calc(100vw-2rem))] max-w-full px-4 py-3 md:pl-10", children: panelOf(r.original) }) }) }, `panel:${keyOf(r.original)}`)) : null] }, domain.rowKey(r.original)));
562
+ // Rows sharing the sub-group's `under` key fold into one rolled-up line that opens them; a key
563
+ // with one row keeps that row (NVDA opens Shares, then each option).
564
+ const foldRow = (key, members, scope) => {
565
+ const k = `under:${scope}\u0000${key}`;
566
+ const ms = members.map((m) => m.original);
567
+ return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": k, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { className: cn(colOf(f), groupBy && i === 0 && 'pl-8'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-1", children: [chevron(k, `${key} ${openLabel()}`), _jsx("span", { className: "font-medium", children: key }), _jsx("span", { className: "num text-xs text-muted-foreground", children: ms.length })] })) : (rollCell(f, ms)) }, f.key)))] }), openParts.has(k) && ms.map((p) => partRow(p, subgroup?.name))] }, k));
568
+ };
569
+ const rowsOf = (list, scope) => {
570
+ const under = folds ? subgroup?.under : undefined;
571
+ if (!under)
572
+ return list.map(dataRow);
573
+ const m = new Map();
574
+ for (const r of list) {
575
+ const k = under(r.original);
576
+ const b = m.get(k);
577
+ b ? b.push(r) : m.set(k, [r]);
578
+ }
579
+ return [...m].map(([k, ms]) => ms.length > 1 || subgroup?.always
580
+ ? foldRow(k, ms, scope)
581
+ : dataRow(ms[0]));
582
+ };
583
+ // A table draws at most DRAW_STEP rows per group (or per unpaged table) until asked for more
584
+ // (a grouped view of a quarter's holdings history drew ~1M elements and
585
+ // crashed the browser).
586
+ const drawn = (list, scope) => {
587
+ const limit = more.get(scope) ?? DRAW_STEP;
588
+ const out = rowsOf(list.slice(0, limit), scope);
589
+ const left = list.length - limit;
590
+ if (left <= 0)
591
+ return out;
592
+ return [
593
+ ...out,
594
+ _jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: () => setMore((m) => new Map(m).set(scope, limit + DRAW_STEP)), children: [_jsx(ChevronsDownIcon, {}), "Show ", Math.min(left, DRAW_STEP).toLocaleString(), " more", _jsxs("span", { className: "num font-normal text-muted-foreground", children: ["of ", left.toLocaleString()] })] }) }) }, `${scope}:more`),
595
+ ];
596
+ };
597
+ const box = useRef(null);
598
+ useFloatingHead(box, !inView);
599
+ return (_jsx("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: _jsxs(Table, { children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { className: cn(colOf(shown[i]), PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
600
+ if (press.current)
601
+ e.preventDefault();
602
+ }, "aria-sort": h.column.getIsSorted() === 'asc'
603
+ ? 'ascending'
604
+ : h.column.getIsSorted() === 'desc'
605
+ ? 'descending'
606
+ : undefined, children: [i === 0 && groups && (_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.getToggleSortingHandler(), 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
607
+ ? groups.map(([key, members], gi) => {
608
+ const open = isOpen(key, gi);
609
+ 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)))
610
+ ? true
611
+ : members.some((m) => selection.keys.has(keyOf(m.original)))
612
+ ? 'indeterminate'
613
+ : false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsxs(TableCell, { colSpan: lead, className: PINNED_GROUP, children: [_jsxs(Button, { variant: "ghost", size: "xs", className: "-mx-2 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => setFlipped((c) => {
614
+ const n = new Set(c);
615
+ n.has(key) ? n.delete(key) : n.add(key);
616
+ return n;
617
+ }), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(groupBy.icon, {}), key, _jsx("span", { className: "num font-normal text-muted-foreground", children: members.length })] }), groupActions?.(key, members.map((m) => m.original))] }, f.key)) : (_jsx(TableCell, { className: cn(colOf(f), PINNED_GROUP), children: rollCell(f, members.map((m) => m.original)) }, f.key)))] }), open && drawn(members, key)] }, key));
618
+ })
619
+ : drawn(sorted, ''), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { children: "Total" }, "total")) : (cell))] }))] })] }) }));
620
+ }
621
+ /** One value rolled up into a rows × columns grid, with totals where the roll-up adds. */
622
+ function PivotTable({ domain, rows, pivot, }) {
623
+ const field = (k) => domain.fields.find((f) => f.key === k);
624
+ const rf = field(pivot.rows);
625
+ const cf = field(pivot.columns);
626
+ const vf = field(pivot.value);
627
+ const grid = useMemo(() => {
628
+ if (!rf || !cf || !vf)
629
+ return null;
630
+ const rowKeys = [...new Set(rows.map((r) => label(rf.value(r))))].sort();
631
+ const colKeys = [...new Set(rows.map((r) => label(cf.value(r))))].sort();
632
+ const cell = (rk, ck) => rollUp(rows
633
+ .filter((r) => (rk === null || label(rf.value(r)) === rk) &&
634
+ (ck === null || label(cf.value(r)) === ck))
635
+ .map(vf.value), pivot.agg);
636
+ return { rowKeys, colKeys, cell };
637
+ }, [rows, rf, cf, vf, pivot.agg]);
638
+ if (!grid || !rf || !cf || !vf) {
639
+ return (_jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "This view's pivot names a field the data does not have." }));
640
+ }
641
+ const totals = pivot.agg === 'sum' || pivot.agg === 'count';
642
+ const fmt = isCountAgg(pivot.agg) ? 'rounded' : vf.format;
643
+ const amount = pivot.agg !== 'count' && vf.amount;
644
+ return (_jsx("div", { className: IN_VIEW, "data-testid": "view-pivot", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: PINNED_HEAD, style: ruleOf(colorOf(domain, rf)), children: _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(rf.icon, { className: "size-3.5" }), _jsx("span", { style: { color: colorOf(domain, rf) ?? undefined }, children: rf.label }), " \\", ' ', _jsx("span", { style: { color: colorOf(domain, cf) ?? undefined }, children: cf.label }), _jsxs("span", { className: "font-normal text-muted-foreground", children: ["(", _jsx("span", { style: { color: colorOf(domain, vf) ?? undefined }, children: vf.label }), ",", ' ', pivot.agg, ")"] })] }) }), grid.colKeys.map((c) => (_jsx(TableHead, { className: cn('text-right', PINNED_HEAD), style: ruleOf(colorOf(domain, cf)), children: c }, c))), totals && _jsx(TableHead, { className: cn('text-right', PINNED_HEAD), children: "Total" })] }) }), _jsxs(TableBody, { children: [grid.rowKeys.map((r) => (_jsxs(TableRow, { "data-row": r, children: [_jsx(TableCell, { children: r }), grid.colKeys.map((c) => (_jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(r, c), format: fmt, amount: amount }) }, c))), totals && (_jsx(TableCell, { className: cn('text-right', ROLLUP), children: _jsx(ViewValueCell, { value: grid.cell(r, null), format: fmt, amount: amount }) }))] }, r))), totals && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, children: [_jsx(TableCell, { children: "Total" }), grid.colKeys.map((c) => (_jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(null, c), format: fmt, amount: amount }) }, c))), _jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(null, null), format: fmt, amount: amount }) })] }))] })] }) }));
645
+ }