@columenlabs/lumen-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) 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 +658 -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 +614 -0
  117. package/dist/lumen-widget.js +594 -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/spotlight.d.ts +23 -0
  173. package/dist/spotlight.js +138 -0
  174. package/dist/status-health.d.ts +16 -0
  175. package/dist/status-health.js +41 -0
  176. package/dist/status.d.ts +9 -0
  177. package/dist/status.js +12 -0
  178. package/dist/summary-cards.d.ts +56 -0
  179. package/dist/summary-cards.js +270 -0
  180. package/dist/symbol-sheet-context.d.ts +4 -0
  181. package/dist/symbol-sheet-context.js +5 -0
  182. package/dist/symbol-sheet.d.ts +9 -0
  183. package/dist/symbol-sheet.js +24 -0
  184. package/dist/tasks.d.ts +29 -0
  185. package/dist/tasks.js +278 -0
  186. package/dist/theme.css +606 -0
  187. package/dist/tile.d.ts +37 -0
  188. package/dist/tile.js +23 -0
  189. package/dist/ui/alert-dialog.d.ts +18 -0
  190. package/dist/ui/alert-dialog.js +41 -0
  191. package/dist/ui/alert.d.ts +10 -0
  192. package/dist/ui/alert.js +27 -0
  193. package/dist/ui/avatar.d.ts +11 -0
  194. package/dist/ui/avatar.js +22 -0
  195. package/dist/ui/badge.d.ts +9 -0
  196. package/dist/ui/badge.js +27 -0
  197. package/dist/ui/breadcrumb.d.ts +11 -0
  198. package/dist/ui/breadcrumb.js +27 -0
  199. package/dist/ui/button-group.d.ts +11 -0
  200. package/dist/ui/button-group.js +27 -0
  201. package/dist/ui/button.d.ts +10 -0
  202. package/dist/ui/button.js +38 -0
  203. package/dist/ui/card.d.ts +11 -0
  204. package/dist/ui/card.js +24 -0
  205. package/dist/ui/chart.d.ts +44 -0
  206. package/dist/ui/chart.js +117 -0
  207. package/dist/ui/checkbox.d.ts +4 -0
  208. package/dist/ui/checkbox.js +8 -0
  209. package/dist/ui/collapsible.d.ts +5 -0
  210. package/dist/ui/collapsible.js +12 -0
  211. package/dist/ui/combobox.d.ts +24 -0
  212. package/dist/ui/combobox.js +60 -0
  213. package/dist/ui/command.d.ts +18 -0
  214. package/dist/ui/command.js +34 -0
  215. package/dist/ui/dialog.d.ts +23 -0
  216. package/dist/ui/dialog.js +50 -0
  217. package/dist/ui/dropdown-menu.d.ts +29 -0
  218. package/dist/ui/dropdown-menu.js +50 -0
  219. package/dist/ui/emoji-picker.d.ts +6 -0
  220. package/dist/ui/emoji-picker.js +28 -0
  221. package/dist/ui/empty.d.ts +11 -0
  222. package/dist/ui/empty.js +33 -0
  223. package/dist/ui/field.d.ts +24 -0
  224. package/dist/ui/field.js +65 -0
  225. package/dist/ui/hover-card.d.ts +6 -0
  226. package/dist/ui/hover-card.js +13 -0
  227. package/dist/ui/input-group.d.ts +16 -0
  228. package/dist/ui/input-group.js +56 -0
  229. package/dist/ui/input.d.ts +3 -0
  230. package/dist/ui/input.js +6 -0
  231. package/dist/ui/item.d.ts +23 -0
  232. package/dist/ui/item.js +67 -0
  233. package/dist/ui/kbd.d.ts +3 -0
  234. package/dist/ui/kbd.js +9 -0
  235. package/dist/ui/label.d.ts +4 -0
  236. package/dist/ui/label.js +7 -0
  237. package/dist/ui/popover.d.ts +10 -0
  238. package/dist/ui/popover.js +25 -0
  239. package/dist/ui/progress.d.ts +4 -0
  240. package/dist/ui/progress.js +9 -0
  241. package/dist/ui/resizable.d.ts +7 -0
  242. package/dist/ui/resizable.js +13 -0
  243. package/dist/ui/scroll-area.d.ts +5 -0
  244. package/dist/ui/scroll-area.js +10 -0
  245. package/dist/ui/select.d.ts +15 -0
  246. package/dist/ui/select.js +36 -0
  247. package/dist/ui/separator.d.ts +4 -0
  248. package/dist/ui/separator.js +7 -0
  249. package/dist/ui/sheet.d.ts +14 -0
  250. package/dist/ui/sheet.js +54 -0
  251. package/dist/ui/sidebar.d.ts +69 -0
  252. package/dist/ui/sidebar.js +206 -0
  253. package/dist/ui/skeleton.d.ts +2 -0
  254. package/dist/ui/skeleton.js +6 -0
  255. package/dist/ui/slider.d.ts +4 -0
  256. package/dist/ui/slider.js +9 -0
  257. package/dist/ui/sonner.d.ts +3 -0
  258. package/dist/ui/sonner.js +24 -0
  259. package/dist/ui/spinner.d.ts +2 -0
  260. package/dist/ui/spinner.js +7 -0
  261. package/dist/ui/switch.d.ts +6 -0
  262. package/dist/ui/switch.js +7 -0
  263. package/dist/ui/table.d.ts +10 -0
  264. package/dist/ui/table.js +31 -0
  265. package/dist/ui/tabs.d.ts +7 -0
  266. package/dist/ui/tabs.js +22 -0
  267. package/dist/ui/textarea.d.ts +3 -0
  268. package/dist/ui/textarea.js +6 -0
  269. package/dist/ui/toggle-group.d.ts +10 -0
  270. package/dist/ui/toggle-group.js +22 -0
  271. package/dist/ui/toggle.d.ts +9 -0
  272. package/dist/ui/toggle.js +25 -0
  273. package/dist/ui/tooltip.d.ts +7 -0
  274. package/dist/ui/tooltip.js +16 -0
  275. package/dist/view-edit-cell.d.ts +49 -0
  276. package/dist/view-edit-cell.js +198 -0
  277. package/dist/view-editor.d.ts +52 -0
  278. package/dist/view-editor.js +940 -0
  279. package/dist/view-formulas.d.ts +61 -0
  280. package/dist/view-formulas.js +247 -0
  281. package/dist/view-icons.d.ts +20 -0
  282. package/dist/view-icons.js +70 -0
  283. package/dist/view-model.d.ts +390 -0
  284. package/dist/view-model.js +316 -0
  285. package/dist/whats-new.d.ts +3 -0
  286. package/dist/whats-new.js +53 -0
  287. package/dist/widget-controls.d.ts +9 -0
  288. package/dist/widget-controls.js +26 -0
  289. package/dist/widget-settings.d.ts +436 -0
  290. package/dist/widget-settings.js +290 -0
  291. package/dist/widget-thumbs.d.ts +3 -0
  292. package/dist/widget-thumbs.js +29 -0
  293. package/dist/workspace-keys.d.ts +12 -0
  294. package/dist/workspace-keys.js +13 -0
  295. package/dist/workspaces.d.ts +57 -0
  296. package/dist/workspaces.js +473 -0
  297. package/package.json +82 -4
@@ -0,0 +1,112 @@
1
+ import type { QueryKey, UseQueryOptions } from '@tanstack/react-query';
2
+ import { type LucideIcon } from 'lucide-react';
3
+ import { type ReactNode } from 'react';
4
+ import { type CoverageJobState } from './coverage-cells.js';
5
+ /** One count at the finest grain a page has: a day (`2026-10-12`) or a month (`2026-10`). */
6
+ export interface CoverageFact {
7
+ row: string;
8
+ period: string;
9
+ /** Sessions, days or rows there. */
10
+ have: number;
11
+ /** Sessions, days or rows expected; 0 when nothing is. */
12
+ expected: number;
13
+ /** A backfill's state while it touches this period. */
14
+ state?: CoverageJobState;
15
+ /** Shown in the tooltip, e.g. why a job failed. */
16
+ note?: string;
17
+ }
18
+ /** A row's own text and summary values; rows show in this order. */
19
+ export interface CoverageRow {
20
+ row: string;
21
+ /** Shown instead of `row` (a series code). */
22
+ label?: string;
23
+ /** A muted line under the label. */
24
+ sub?: string;
25
+ /** A small tag after the label (a resolution, `Index`, how far behind). */
26
+ badge?: ReactNode;
27
+ /** Section header the row sits under (with `groupBy`). */
28
+ group?: string;
29
+ first?: string | null;
30
+ last?: string | null;
31
+ }
32
+ /** What a coverage source returns (map an API response to it with the query's `select`). */
33
+ export interface CoverageData {
34
+ facts: CoverageFact[];
35
+ rows: CoverageRow[];
36
+ /** The month still running (`YYYY-MM`): its block shows as This period when complete so far. */
37
+ current?: string;
38
+ }
39
+ export type CoverageBlock = 'day' | 'week' | 'month' | 'quarter' | 'year';
40
+ export type CoverageSummary = 'complete' | 'first' | 'last' | 'rows';
41
+ /** A backfill pick: each row with the months its picked blocks cover. */
42
+ export type CoveragePick = {
43
+ row: string;
44
+ months: string[];
45
+ }[];
46
+ /** `TRaw` is what the query fetches; its `select` maps it to facts. */
47
+ export interface LumenCoverageMapProps<TRaw = unknown, TKey extends QueryKey = QueryKey> {
48
+ source: UseQueryOptions<TRaw, Error, CoverageData, TKey>;
49
+ /** Card title; may name the block (`Snapshot days`). */
50
+ title: string;
51
+ icon?: LucideIcon;
52
+ /** Row heading (`Underlying`) and where a row's label links (`/market/$symbol`, its search);
53
+ * a row with no link (undefined) is plain text. */
54
+ rows: {
55
+ title: string;
56
+ link?: (row: string) => {
57
+ to: string;
58
+ params?: Record<string, string>;
59
+ search?: object;
60
+ } | undefined;
61
+ };
62
+ /** `grid`: rows × time. `months`: the first row's facts, one line per year, Jan to Dec. */
63
+ layout?: 'grid' | 'months';
64
+ /** Column size, the same as a backfill block. */
65
+ block?: CoverageBlock;
66
+ /** Block choices shown as chips. */
67
+ zoom?: CoverageBlock[];
68
+ /** The last N blocks of `block`; other zoom levels show all. Opens scrolled to today either way. */
69
+ window?: number | 'all';
70
+ /** What the tooltip counts. */
71
+ unit?: string;
72
+ /** Right-hand columns; a phone keeps Complete only. */
73
+ summary?: CoverageSummary[];
74
+ /** `backfill`: click, shift-click a range, then Queue backfill. `drill`: a click opens the block. */
75
+ select?: 'backfill' | 'drill';
76
+ /** Months a backfill can take (the source's own limits). */
77
+ selectable?: (row: string, month: string) => boolean;
78
+ onQueue?: (pick: CoveragePick) => Promise<unknown> | undefined;
79
+ /** The drilled block (`2026-09`) and the click that changes it. */
80
+ drilled?: string | null;
81
+ onDrill?: (row: string, period: string) => void;
82
+ /** Section headers by each row's `group`. */
83
+ groupBy?: boolean;
84
+ /** Under the grid at the right, e.g. how the cells are shaded. */
85
+ hint?: ReactNode;
86
+ /** Rows a page (grid layout), with the LumenView pager in the header; off when left out. */
87
+ pageSize?: number;
88
+ /** A source that returns one page of rows: the pager in the header turns its pages. */
89
+ pager?: {
90
+ total: number;
91
+ /** 0-based. */
92
+ page: number;
93
+ size: number;
94
+ sizes?: readonly number[];
95
+ onPage: (page: number) => void;
96
+ onSize: (size: number) => void;
97
+ };
98
+ /** Inputs in the header before the zoom chips. */
99
+ tools?: ReactNode;
100
+ /** Its rows' LumenTokenizedFilter in the card header, and how many chips it holds. */
101
+ filter?: ReactNode;
102
+ filterCount?: number;
103
+ /** Shown in place of the grid when the source has no rows. */
104
+ empty?: ReactNode;
105
+ /** Start the query when the card scrolls into view. */
106
+ lazy?: boolean;
107
+ }
108
+ /** The block a period falls in. Month facts never fold into days or weeks. */
109
+ export declare function blockOf(period: string, block: CoverageBlock): string;
110
+ /** `2026-09` → `Sep 2026`, `2026-Q3` → `2026 Q3`, days stay as they are. */
111
+ export declare function blockName(key: string): string;
112
+ export declare function LumenCoverageMap<TRaw, TKey extends QueryKey>({ source, title, icon, rows: rowSpec, layout, block: initialBlock, zoom, window, unit, summary, select, selectable, onQueue, drilled, onDrill, groupBy, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy, }: LumenCoverageMapProps<TRaw, TKey>): import("react").JSX.Element;
@@ -0,0 +1,428 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link } from '@tanstack/react-router';
3
+ import { Grid3x3Icon, ListPlusIcon, XIcon } from 'lucide-react';
4
+ import { isValidElement, memo, useLayoutEffect, useMemo, useRef, useState, } from 'react';
5
+ import { CoverageCell, CoverageLegend, CoverageTip, coverageLabel, coverageLevel, firstJobState, } from './coverage-cells.js';
6
+ import { FoldChevron, useFolds, usePageKey } from './fold.js';
7
+ import { useIsMobile } from './hooks/use-mobile.js';
8
+ import { IconButton } from './icon-button.js';
9
+ import { cn } from './lib/utils.js';
10
+ import { LumenStates, useLumenSource } from './lumen-source.js';
11
+ import { LumenWidget } from './lumen-widget.js';
12
+ import { LumenPager } from './pager.js';
13
+ import { Badge } from './ui/badge.js';
14
+ import { Skeleton } from './ui/skeleton.js';
15
+ import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
16
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
17
+ const BLOCK_LABEL = {
18
+ day: 'Day',
19
+ week: 'Week',
20
+ month: 'Month',
21
+ quarter: 'Quarter',
22
+ year: 'Year',
23
+ };
24
+ /** Monday of `day`'s week. */
25
+ function weekOf(day) {
26
+ const t = Date.parse(day);
27
+ const wd = (new Date(t).getUTCDay() + 6) % 7;
28
+ return new Date(t - wd * 86_400_000).toISOString().slice(0, 10);
29
+ }
30
+ /** The block a period falls in. Month facts never fold into days or weeks. */
31
+ export function blockOf(period, block) {
32
+ if (block === 'year')
33
+ return period.slice(0, 4);
34
+ if (block === 'quarter')
35
+ return `${period.slice(0, 4)}-Q${Math.floor((Number(period.slice(5, 7)) - 1) / 3) + 1}`;
36
+ if (block === 'month' || period.length === 7)
37
+ return period.slice(0, 7);
38
+ return block === 'week' ? weekOf(period) : period;
39
+ }
40
+ /** Every month from `from` to `to` (`YYYY-MM`), inclusive. */
41
+ function monthsBetween(from, to) {
42
+ const out = [];
43
+ let y = Number(from.slice(0, 4));
44
+ let m = Number(from.slice(5, 7));
45
+ const end = Number(to.slice(0, 4)) * 12 + Number(to.slice(5, 7));
46
+ while (y * 12 + m <= end) {
47
+ out.push(`${y}-${String(m).padStart(2, '0')}`);
48
+ m += 1;
49
+ if (m > 12) {
50
+ m = 1;
51
+ y += 1;
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ /** `2026-09` → `Sep 2026`, `2026-Q3` → `2026 Q3`, days stay as they are. */
57
+ export function blockName(key) {
58
+ if (/^\d{4}-\d{2}$/.test(key))
59
+ return `${MON[Number(key.slice(5, 7)) - 1]} ${key.slice(0, 4)}`;
60
+ return key.replace('-Q', ' Q');
61
+ }
62
+ function headLabel(key, block, count) {
63
+ if (block === 'year')
64
+ return `'${key.slice(2, 4)}`;
65
+ if (block === 'quarter')
66
+ return key.endsWith('Q1') ? key.slice(0, 4) : '';
67
+ if (block === 'month') {
68
+ const m = Number(key.slice(5, 7));
69
+ if (count > 60)
70
+ return m === 1 ? key.slice(0, 4) : '';
71
+ return m % 3 === 1 ? `${MON[m - 1]} ${key.slice(2, 4)}` : '';
72
+ }
73
+ return key.slice(8, 10) <= '07' && (block === 'week' || key.slice(8, 10) === '01')
74
+ ? `${MON[Number(key.slice(5, 7)) - 1]}`
75
+ : '';
76
+ }
77
+ const fmtInt = (n) => n.toLocaleString('en-US');
78
+ const fmtPct = (have, expected) => expected > 0 ? `${((Math.min(have, expected) / expected) * 100).toFixed(1)}%` : '–';
79
+ /** Facts → `row → block → sums`, and each row's totals over all its facts. */
80
+ function fold(data, block) {
81
+ const cells = new Map();
82
+ const totals = new Map();
83
+ for (const f of data.facts) {
84
+ const key = blockOf(f.period, block);
85
+ const row = cells.get(f.row) ?? new Map();
86
+ cells.set(f.row, row);
87
+ const c = row.get(key) ?? {
88
+ have: 0,
89
+ expected: 0,
90
+ level: 'none',
91
+ notes: [],
92
+ months: [],
93
+ state: null,
94
+ };
95
+ c.have += f.expected > 0 ? Math.min(f.have, f.expected) : f.have;
96
+ c.expected += f.expected;
97
+ c.state = firstJobState(c.state, f.state);
98
+ if (f.note)
99
+ c.notes.push(f.note);
100
+ const month = f.period.slice(0, 7);
101
+ if (!c.months.includes(month))
102
+ c.months.push(month);
103
+ row.set(key, c);
104
+ const t = totals.get(f.row) ?? { have: 0, expected: 0 };
105
+ t.have += f.expected > 0 ? Math.min(f.have, f.expected) : f.have;
106
+ t.expected += f.expected;
107
+ totals.set(f.row, t);
108
+ }
109
+ for (const row of cells.values()) {
110
+ for (const c of row.values()) {
111
+ c.level = coverageLevel(c.have, c.expected, {
112
+ current: !!data.current && c.months.includes(data.current),
113
+ state: c.state,
114
+ });
115
+ }
116
+ }
117
+ return { cells, totals };
118
+ }
119
+ /** The column blocks: every block from the first fact to today (or the last fact). */
120
+ function columns(data, block) {
121
+ const periods = data.facts.map((f) => f.period).sort();
122
+ const first = periods[0];
123
+ if (!first)
124
+ return [];
125
+ if (block === 'day' || block === 'week') {
126
+ return [...new Set(periods.map((p) => blockOf(p, block)))];
127
+ }
128
+ const last = [periods.at(-1), data.current ?? ''].sort().at(-1);
129
+ const out = [];
130
+ for (const m of monthsBetween(first.slice(0, 7), last.slice(0, 7))) {
131
+ const k = blockOf(m, block);
132
+ if (out.at(-1) !== k)
133
+ out.push(k);
134
+ }
135
+ return out;
136
+ }
137
+ const SUMMARY_HEAD = {
138
+ complete: 'Complete',
139
+ first: 'First',
140
+ last: 'Last',
141
+ rows: '',
142
+ };
143
+ const SUMMARY_WIDTH = {
144
+ complete: '5rem',
145
+ first: '6.5rem',
146
+ last: '6.5rem',
147
+ rows: '5rem',
148
+ };
149
+ /** A grid-shaped skeleton: label column plus empty cells, so nothing jumps when data lands. */
150
+ function GridSkeleton({ rows = 8 }) {
151
+ return (_jsx("div", { "aria-busy": "true", "aria-label": "Loading", className: "flex flex-col gap-[5px] py-1", children: Array.from({ length: rows }, (_, i) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-16 shrink-0" }), _jsx(Skeleton, { className: "h-[18px] flex-1" })] }, i))) }));
152
+ }
153
+ /** `symbols`, `series`: the row title as a count noun. */
154
+ /** The narrowest a block gets before the map scrolls sideways. */
155
+ const CELL_MIN = 14;
156
+ const plural = (title) => {
157
+ const t = title.toLowerCase();
158
+ return t.endsWith('s') ? t : `${t}s`;
159
+ };
160
+ /** Whether two row badges draw the same: same element types and props, children compared alike. */
161
+ function sameNode(a, b) {
162
+ if (Object.is(a, b))
163
+ return true;
164
+ if (Array.isArray(a) && Array.isArray(b))
165
+ return a.length === b.length && a.every((x, i) => sameNode(x, b[i]));
166
+ if (!isValidElement(a) || !isValidElement(b) || a.type !== b.type || a.key !== b.key)
167
+ return false;
168
+ const pa = a.props;
169
+ const pb = b.props;
170
+ const keys = Object.keys(pa);
171
+ return (keys.length === Object.keys(pb).length &&
172
+ keys.every((k) => k === 'children' ? sameNode(pa[k], pb[k]) : Object.is(pa[k], pb[k])));
173
+ }
174
+ /**
175
+ * One row of the grid: its name, cells and summary. It draws again only when something it shows
176
+ * changes (`sig`), so a poll redraws the rows whose blocks moved and a pick redraws one row
177
+ * (a 100-row map used to redraw all 13,000 cells on every poll).
178
+ */
179
+ const MapRow = memo(function MapRow({ r, cols, marks, picks, total, look, }) {
180
+ const text = r.label ?? r.row;
181
+ const link = look.link?.(r.row);
182
+ return (_jsxs(_Fragment, { children: [_jsxs("span", {
183
+ // Rows sit indented under their group's name.
184
+ className: cn('sticky left-0 z-1 flex min-w-0 flex-col bg-card pr-2', look.groupBy && (look.mobile ? 'pl-1.5' : 'pl-3')), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [link ? (_jsx(Link, { ...link, className: "num truncate font-medium hover:text-primary hover:underline", children: text })) : (_jsx("span", { className: "num truncate font-medium", children: text })), r.badge] }), r.sub && !look.mobile && (_jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: r.sub }))] }), cols.map((k, i) => {
185
+ const m = marks[i];
186
+ if (!m)
187
+ return _jsx("span", {}, k);
188
+ return (_jsx(CoverageCell, { level: m.level, label: `${text} · ${blockName(k)}`, tip: `${r.row}|${k}`, picked: picks.includes(k) || look.drilled === k, pickable: look.select === 'backfill' || look.select === 'drill', disabled: look.select === 'backfill' ? !m.canPick : look.select !== 'drill', className: "h-[18px]" }, k));
189
+ }), look.sums.map((s) => (_jsx("span", { className: cn('num pl-3 text-right text-xs whitespace-nowrap', s !== 'complete' && 'text-muted-foreground'), children: s === 'complete'
190
+ ? fmtPct(total.have, total.expected)
191
+ : s === 'first'
192
+ ? (r.first ?? '—')
193
+ : s === 'last'
194
+ ? (r.last ?? '—')
195
+ : fmtInt(total.have) }, s)))] }));
196
+ }, (a, b) => a.sig === b.sig && sameNode(a.r.badge, b.r.badge));
197
+ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowSpec, layout = 'grid', block: initialBlock = 'month', zoom, window = 'all', unit = 'sessions', summary = ['complete', 'last'], select, selectable = () => true, onQueue, drilled = null, onDrill, groupBy = false, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy = false, }) {
198
+ const { data, load: raw, ref } = useLumenSource(source, { lazy });
199
+ // A coverage map refreshes in place: any refresh, asked for or a poll, only spins its ↻; no bar or dimming over the cells.
200
+ const load = { ...raw, fetching: raw.fetching || raw.background };
201
+ const cells = { ...raw, fetching: false, refreshing: false };
202
+ const mobile = useIsMobile();
203
+ const [block, setBlock] = useState(layout === 'months' ? 'month' : initialBlock);
204
+ const [picked, setPicked] = useState(new Set());
205
+ const [queueing, setQueueing] = useState(false);
206
+ const anchor = useRef(null);
207
+ const scroller = useRef(null);
208
+ const [pageSize, setPageSize] = useState(initialPageSize ?? 0);
209
+ const [pageIndex, setPageIndex] = useState(0);
210
+ const folded = useMemo(() => (data ? fold(data, block) : null), [data, block]);
211
+ // Group headers fold their rows, remembered in the browser per page.
212
+ const [folds, setFold] = useFolds();
213
+ const foldKey = `group:${usePageKey()}:${title}:`;
214
+ const cols = useMemo(() => {
215
+ if (!data || layout === 'months')
216
+ return [];
217
+ const all = columns(data, block);
218
+ return typeof window === 'number' && block === initialBlock ? all.slice(-window) : all;
219
+ }, [data, block, layout, window, initialBlock]);
220
+ // Open at today: the newest blocks are on the right. Zoom never moves the page, only this strip.
221
+ // Only a new set of columns scrolls it; a refresh of the same columns keeps where the reader is.
222
+ const strip = `${layout}|${block}|${cols.length}|${cols[0] ?? ''}|${cols.at(-1) ?? ''}|${folded ? 1 : 0}`;
223
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-run when the columns change
224
+ useLayoutEffect(() => {
225
+ const el = scroller.current;
226
+ if (el)
227
+ el.scrollLeft = el.scrollWidth;
228
+ }, [strip]);
229
+ const sums = mobile ? summary.filter((s) => s === 'complete') : summary;
230
+ const rowsHead = `${unit[0]?.toUpperCase() ?? ''}${unit.slice(1)}`;
231
+ const levels = new Set();
232
+ const unitLabel = (have, expected) => expected > 0 ? `${fmtInt(have)} of ${fmtInt(expected)} ${unit}` : 'Not expected';
233
+ const toggle = (row, keys, mode) => setPicked((prev) => {
234
+ const next = new Set(prev);
235
+ const ids = keys.map((k) => `${row}|${k}`);
236
+ const all = ids.every((id) => next.has(id));
237
+ for (const id of ids) {
238
+ if (mode === 'toggle' && all)
239
+ next.delete(id);
240
+ else
241
+ next.add(id);
242
+ }
243
+ return next;
244
+ });
245
+ const pickedMonths = () => {
246
+ const byRow = new Map();
247
+ for (const id of picked) {
248
+ const [row, key] = id.split('|');
249
+ const c = folded?.cells.get(row)?.get(key);
250
+ const set = byRow.get(row) ?? new Set();
251
+ for (const m of c?.months ?? [])
252
+ if (selectable(row, m))
253
+ set.add(m);
254
+ byRow.set(row, set);
255
+ }
256
+ return [...byRow]
257
+ .filter(([, ms]) => ms.size)
258
+ .map(([row, ms]) => ({ row, months: [...ms].sort() }));
259
+ };
260
+ const queue = async () => {
261
+ if (!onQueue)
262
+ return;
263
+ setQueueing(true);
264
+ try {
265
+ await onQueue(pickedMonths());
266
+ setPicked(new Set());
267
+ }
268
+ finally {
269
+ setQueueing(false);
270
+ }
271
+ };
272
+ const rowTotal = layout === 'grid' && data ? data.rows.length || (folded?.cells.size ?? 0) : 0;
273
+ const page = pageSize ? Math.min(pageIndex, Math.max(0, Math.ceil(rowTotal / pageSize) - 1)) : 0;
274
+ // One tooltip for the whole map, filled in for the hovered cell (`row|block`).
275
+ const tipFor = (tip) => {
276
+ const cut = tip.lastIndexOf('|');
277
+ const row = tip.slice(0, cut);
278
+ const k = tip.slice(cut + 1);
279
+ const c = folded?.cells.get(row)?.get(k);
280
+ if (!c)
281
+ return null;
282
+ const text = data?.rows.find((r) => r.row === row)?.label ?? row;
283
+ return {
284
+ label: `${text} · ${blockName(k)}`,
285
+ detail: (_jsxs(_Fragment, { children: [_jsxs("div", { children: [unitLabel(c.have, c.expected), " \u00B7 ", coverageLabel(c.level)] }), c.notes.map((n) => (_jsx("div", { children: n }, n))), layout === 'months' && select === 'drill' && _jsx("div", { children: "Click to see its days" })] })),
286
+ };
287
+ };
288
+ // One click handler for every cell: drill, or pick (shift-click picks a range in the row).
289
+ const onCellClick = (e) => {
290
+ const el = e.target instanceof Element ? e.target.closest('[data-cell]') : null;
291
+ if (!el || el.disabled)
292
+ return;
293
+ const tip = el.dataset.cell ?? '';
294
+ const cut = tip.lastIndexOf('|');
295
+ const row = tip.slice(0, cut);
296
+ const k = tip.slice(cut + 1);
297
+ if (select === 'drill') {
298
+ onDrill?.(row, k);
299
+ return;
300
+ }
301
+ if (select !== 'backfill')
302
+ return;
303
+ const i = cols.indexOf(k);
304
+ if (e.shiftKey && anchor.current?.row === row) {
305
+ const [lo, hi] = [anchor.current.index, i].sort((x, y) => x - y);
306
+ toggle(row, cols.slice(lo, hi + 1), 'add');
307
+ }
308
+ else
309
+ toggle(row, [k], 'toggle');
310
+ anchor.current = { row, index: i };
311
+ };
312
+ let body = null;
313
+ if (data && folded) {
314
+ if (layout === 'months') {
315
+ const row = data.rows[0]?.row ?? data.facts[0]?.row ?? '';
316
+ const cells = folded.cells.get(row) ?? new Map();
317
+ const years = [...new Set([...cells.keys()].map((k) => k.slice(0, 4)))].sort().reverse();
318
+ const sumCols = sums.map((s) => SUMMARY_WIDTH[s]).join(' ');
319
+ const grid = {
320
+ gridTemplateColumns: `${mobile ? '2.75rem' : '4rem'} repeat(12, minmax(${mobile ? '14px' : '18px'}, 1fr)) ${sumCols}`,
321
+ };
322
+ body = (_jsxs("div", { className: "grid items-center gap-[3px]", style: grid, onClick: onCellClick, children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Year" }), MON.map((m, i) => (_jsx("span", { className: "text-xs text-muted-foreground", children: mobile ? (i % 3 === 0 ? m : '') : m }, m))), sums.map((s) => (_jsx("span", { className: "pl-3 text-right text-xs text-muted-foreground", children: s === 'rows' ? rowsHead : SUMMARY_HEAD[s] }, s))), years.map((y) => {
323
+ let have = 0;
324
+ let expected = 0;
325
+ return [
326
+ _jsx("span", { className: "num text-xs", children: y }, `${y}-label`),
327
+ ...MON.map((_, i) => {
328
+ const key = `${y}-${String(i + 1).padStart(2, '0')}`;
329
+ const c = cells.get(key);
330
+ if (!c)
331
+ return _jsx("span", {}, key);
332
+ levels.add(c.level);
333
+ have += c.have;
334
+ expected += c.expected;
335
+ return (_jsx(CoverageCell, { level: c.level, label: `${row} · ${blockName(key)}`, tip: `${row}|${key}`, picked: drilled === key, pickable: select === 'drill', disabled: select !== 'drill', className: mobile ? 'h-4' : 'h-5' }, key));
336
+ }),
337
+ ...sums.map((s) => (_jsx("span", { className: cn('num pl-3 text-right text-xs', s !== 'complete' && 'text-muted-foreground'), children: s === 'complete' ? fmtPct(have, expected) : s === 'rows' ? fmtInt(have) : '' }, `${y}-${s}`))),
338
+ ];
339
+ })] }));
340
+ }
341
+ else {
342
+ // Rows with badges (index tags, a lag) get room for them beside the name.
343
+ const label = mobile ? '4rem' : data.rows.some((r) => r.badge) ? '10rem' : '7rem';
344
+ // Blocks never shrink below a tappable size: past that the map scrolls
345
+ // sideways with the names column pinned, opening at today.
346
+ const cell = `minmax(${CELL_MIN}px, 1fr)`;
347
+ const grid = {
348
+ gridTemplateColumns: `${label} repeat(${cols.length}, ${cell}) ${sums.map((s) => SUMMARY_WIDTH[s]).join(' ')}`,
349
+ };
350
+ const allRows = data.rows.length
351
+ ? data.rows
352
+ : [...folded.cells.keys()].map((row) => ({ row }));
353
+ const rowList = pageSize ? allRows.slice(page * pageSize, (page + 1) * pageSize) : allRows;
354
+ const groupSize = new Map();
355
+ if (groupBy)
356
+ for (const r of rowList)
357
+ groupSize.set(r.group, (groupSize.get(r.group) ?? 0) + 1);
358
+ const pickedBy = new Map();
359
+ for (const id of picked) {
360
+ const cut = id.lastIndexOf('|');
361
+ const row = id.slice(0, cut);
362
+ pickedBy.set(row, [...(pickedBy.get(row) ?? []), id.slice(cut + 1)]);
363
+ }
364
+ const look = { mobile, groupBy, select, sums, drilled, link: rowSpec.link };
365
+ let group;
366
+ body = (_jsxs("div", { className: "grid min-w-min items-center gap-[3px]", style: grid, onClick: onCellClick, children: [_jsx("span", { className: "sticky left-0 z-1 bg-card text-xs text-muted-foreground", children: mobile ? '' : rowSpec.title }), cols.map((k) => (_jsx("span", { className: "num h-4 overflow-visible text-[11px] whitespace-nowrap text-muted-foreground", children: headLabel(k, block, cols.length) }, k))), sums.map((s) => (_jsx("span", { className: "pl-3 text-right text-xs text-muted-foreground", children: s === 'rows' ? rowsHead : SUMMARY_HEAD[s] }, s))), rowList.flatMap((r) => {
367
+ const out = [];
368
+ const shut = groupBy && folds.has(foldKey + (r.group ?? ''));
369
+ if (groupBy && r.group !== group) {
370
+ group = r.group;
371
+ const name = r.group ?? '';
372
+ const toggle = () => setFold(foldKey + name, !shut);
373
+ out.push(_jsx("span", { className: "col-span-full mt-2 text-[11px] font-semibold tracking-[0.06em] text-muted-foreground uppercase", children: _jsxs("span", { className: "sticky left-0 -ml-1.5 inline-flex items-center gap-1.5 py-0.5 pr-1 pl-1.5", children: [name, _jsx(Badge, { variant: "secondary", className: "num h-4 px-1.5 text-[10px]", children: groupSize.get(r.group) ?? 0 }), _jsx(FoldChevron, { folded: shut, label: name, onToggle: toggle, className: "size-4" })] }) }, `group-${r.group}`));
374
+ }
375
+ if (shut)
376
+ return out;
377
+ const rowCells = folded.cells.get(r.row);
378
+ const marks = cols.map((k) => {
379
+ const c = rowCells?.get(k);
380
+ if (!c)
381
+ return null;
382
+ levels.add(c.level);
383
+ const canPick = select === 'backfill' && c.months.some((m) => selectable(r.row, m));
384
+ return { level: c.level, canPick };
385
+ });
386
+ const picks = pickedBy.get(r.row) ?? [];
387
+ out.push(_jsx(MapRow, { r: r, cols: cols, marks: marks, picks: picks, total: folded.totals.get(r.row) ?? { have: 0, expected: 0 }, look: look, sig: [
388
+ r.label,
389
+ r.sub,
390
+ r.first,
391
+ r.last,
392
+ JSON.stringify(rowSpec.link?.(r.row) ?? null),
393
+ mobile,
394
+ groupBy,
395
+ select,
396
+ sums.join(),
397
+ drilled,
398
+ cols.length,
399
+ cols[0],
400
+ cols.at(-1),
401
+ picks.join(),
402
+ marks.map((m) => (m ? `${m.level}${m.canPick ? '+' : ''}` : '')).join(),
403
+ JSON.stringify(folded.totals.get(r.row) ?? null),
404
+ ].join('\u0001') }, r.row));
405
+ return out;
406
+ })] }));
407
+ }
408
+ }
409
+ const pick = picked.size ? pickedMonths() : [];
410
+ const pickedCount = pick.reduce((n, p) => n + p.months.length, 0);
411
+ const head = (_jsxs(_Fragment, { children: [tools, zoom && layout === 'grid' && (_jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: block, onValueChange: (v) => {
412
+ if (!v)
413
+ return;
414
+ setBlock(v);
415
+ setPicked(new Set());
416
+ }, "aria-label": "Block size", className: "max-sm:hidden", children: zoom.map((z) => (_jsx(ToggleGroupItem, { value: z, children: BLOCK_LABEL[z] }, z))) })), pager ? (_jsx(LumenPager, { total: pager.total, page: pager.page, size: pager.size, sizes: pager.sizes, onPage: pager.onPage, onSize: pager.onSize, noun: plural(rowSpec.title) })) : (pageSize > 0 &&
417
+ layout === 'grid' &&
418
+ data && (_jsx(LumenPager, { total: rowTotal, page: page, size: pageSize, onPage: setPageIndex, onSize: (n) => {
419
+ setPageSize(n);
420
+ setPageIndex(0);
421
+ }, noun: plural(rowSpec.title) })))] }));
422
+ return (_jsx("div", { ref: ref, className: "min-w-0", children: _jsx(LumenWidget, { id: title, kind: null, icon: icon, title: title, tools: head, filter: filter, filterCount: filterCount, load: load, rows: data?.rows.length, pad: false, children: _jsx("div", { className: "flex flex-col gap-3 px-3 pt-3 pb-3", children: _jsxs(LumenStates, { load: cells, skeleton: _jsx(GridSkeleton, {}), empty: data && !data.rows.length && !data.facts.length ? empty : undefined, children: [_jsx("div", { ref: scroller, className: "overflow-x-auto overflow-y-hidden pb-1", children: _jsx(CoverageTip, { content: tipFor, className: "w-max min-w-full", children: body }) }), pickedCount > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm", children: [_jsxs("span", { className: "min-w-0 truncate", children: [_jsx("span", { className: "num font-semibold", children: pick.map((p) => p.row).join(', ') }), ` · ${pickedCount} month${pickedCount > 1 ? 's' : ''}`] }), _jsx("span", { className: "flex-1" }), _jsx(IconButton, { icon: XIcon, label: "Clear selection", onClick: () => setPicked(new Set()) }), _jsx(IconButton, { icon: ListPlusIcon, label: `Queue backfill · ${pickedCount} month${pickedCount > 1 ? 's' : ''}`, variant: "default", disabled: queueing || !onQueue, onClick: () => void queue() })] })), _jsx("div", { className: "border-t pt-3", children: _jsx(CoverageLegend, { levels: levels, hint: hint ??
423
+ (select === 'backfill'
424
+ ? 'Click a cell, shift-click for a range, to queue a backfill'
425
+ : select === 'drill'
426
+ ? 'Click a block to see its days'
427
+ : undefined) }) })] }) }) }) }));
428
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface DetailsTableProps {
3
+ /** `grow` marks the column that takes the spare width; the last column does by default. */
4
+ head: {
5
+ label: string;
6
+ right?: boolean;
7
+ grow?: boolean;
8
+ }[];
9
+ rows: {
10
+ key: string;
11
+ cells: ReactNode[];
12
+ }[];
13
+ /** An optional total row, one cell per column. */
14
+ foot?: ReactNode[];
15
+ }
16
+ export declare function DetailsTable({ head, rows, foot }: DetailsTableProps): import("react").JSX.Element;
17
+ /** Facts as a two-row table: the labels as the header, the values under them. */
18
+ export declare function FactsTable({ items }: {
19
+ items: [string, ReactNode][];
20
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from './lib/utils.js';
3
+ import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from './ui/table.js';
4
+ export function DetailsTable({ head, rows, foot }) {
5
+ const marked = head.findIndex((h) => h.grow);
6
+ const grow = marked >= 0 ? marked : head.length - 1;
7
+ const cell = (i) => cn('num', head[i]?.right && 'text-right', i !== grow && 'w-px');
8
+ return (_jsx("div", { className: "-mx-1 overflow-x-auto overflow-y-hidden rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: head.map((h, i) => (_jsx(TableHead, { className: cn(h.right && 'text-right', i !== grow && 'w-px'), children: h.label }, h.label))) }) }), _jsx(TableBody, { children: rows.map((r) => (_jsx(TableRow, { children: r.cells.map((c, i) => (_jsx(TableCell, { className: cell(i), children: c }, i))) }, r.key))) }), foot && (_jsx(TableFooter, { children: _jsx(TableRow, { children: foot.map((c, i) => (_jsx(TableCell, { className: cell(i), children: c }, i))) }) }))] }) }));
9
+ }
10
+ /** Facts as a two-row table: the labels as the header, the values under them. */
11
+ export function FactsTable({ items }) {
12
+ return (_jsx(DetailsTable, { head: items.map(([k], i) => ({ label: k, right: i > 0 })), rows: [{ key: 'values', cells: items.map(([, v]) => v) }] }));
13
+ }
@@ -0,0 +1,27 @@
1
+ import { type LucideIcon } from 'lucide-react';
2
+ import { type ReactNode } from 'react';
3
+ export interface DrawerSectionProps {
4
+ /** An icon before the title; a form's numbered steps leave it out. */
5
+ icon?: LucideIcon;
6
+ title: string;
7
+ /** A count chip after the title (rows in the section). */
8
+ count?: number | null;
9
+ /** A short note on the right of the header ("None"). */
10
+ hint?: string | null;
11
+ /** A short plain line under the header, shown only while closed: what the section is set to. */
12
+ summary?: ReactNode;
13
+ /** Buttons at the right of the header (a `ResetButton`), outside the fold's trigger. */
14
+ actions?: ReactNode;
15
+ /** Sections start collapsed unless set open. */
16
+ defaultOpen?: boolean;
17
+ /** Controlled open state, when the caller opens a section itself. */
18
+ open?: boolean;
19
+ onOpenChange?: (open: boolean) => void;
20
+ children: ReactNode;
21
+ }
22
+ /**
23
+ * One section of a pullout drawer, on every app (p2e style): a header of
24
+ * chevron, icon, title, optional count and note, with no background until hovered; a hairline
25
+ * between sections. Drawers stack these under their title instead of their own headings.
26
+ */
27
+ export declare function DrawerSection({ icon: Icon, title, count, hint, summary, actions, defaultOpen, open, onOpenChange, children, }: DrawerSectionProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronRightIcon } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import { Badge } from './ui/badge.js';
5
+ import { Button } from './ui/button.js';
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible.js';
7
+ /**
8
+ * One section of a pullout drawer, on every app (p2e style): a header of
9
+ * chevron, icon, title, optional count and note, with no background until hovered; a hairline
10
+ * between sections. Drawers stack these under their title instead of their own headings.
11
+ */
12
+ export function DrawerSection({ icon: Icon, title, count, hint, summary, actions, defaultOpen = false, open, onOpenChange, children, }) {
13
+ // Kept here too when the caller leaves it uncontrolled, so the summary knows when to show.
14
+ const [own, setOwn] = useState(defaultOpen);
15
+ const isOpen = open ?? own;
16
+ const trigger = (_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm",
17
+ // No background, grey only on hover, open or not.
18
+ className: "w-full justify-start px-1 aria-expanded:bg-transparent aria-expanded:hover:bg-muted", children: [_jsx(ChevronRightIcon, { className: "text-muted-foreground transition-transform group-data-[state=open]/section:rotate-90" }), Icon && _jsx(Icon, { className: "text-muted-foreground" }), _jsx("span", { className: "truncate", children: title }), count != null && (_jsx(Badge, { variant: "secondary", className: "num", children: count })), hint && _jsx("span", { className: "ml-auto font-normal text-muted-foreground text-xs", children: hint })] }) }));
19
+ return (_jsxs(Collapsible, { open: isOpen, onOpenChange: (o) => {
20
+ setOwn(o);
21
+ onOpenChange?.(o);
22
+ }, className: "group/section border-t py-1 first:border-t-0", children: [actions ? (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "min-w-0 flex-1", children: trigger }), actions] })) : (trigger), summary && !isOpen && (_jsx("p", { className: "line-clamp-3 pr-1 pb-1 pl-5.5 text-muted-foreground text-xs", children: summary })), _jsx(CollapsibleContent, { className: "pt-1 pr-1 pb-2 pl-5.5", children: children })] }));
23
+ }
@@ -0,0 +1,2 @@
1
+ import { type RefObject } from 'react';
2
+ export declare function useFloatingHead(box: RefObject<HTMLElement | null>, on: boolean): void;