vastgrid 0.1.1 → 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 (312) hide show
  1. package/README.md +175 -13
  2. package/dist/canvas/geometry.d.ts +92 -0
  3. package/dist/canvas/geometry.d.ts.map +1 -0
  4. package/dist/canvas/geometry.js +197 -0
  5. package/dist/canvas/geometry.js.map +1 -0
  6. package/dist/canvas/grid-canvas.d.ts +161 -0
  7. package/dist/canvas/grid-canvas.d.ts.map +1 -0
  8. package/dist/canvas/grid-canvas.js +1146 -0
  9. package/dist/canvas/grid-canvas.js.map +1 -0
  10. package/dist/canvas/hooks.d.ts +61 -0
  11. package/dist/canvas/hooks.d.ts.map +1 -0
  12. package/dist/canvas/hooks.js +2 -0
  13. package/dist/canvas/hooks.js.map +1 -0
  14. package/dist/canvas/keys.d.ts +26 -0
  15. package/dist/canvas/keys.d.ts.map +1 -0
  16. package/dist/canvas/keys.js +133 -0
  17. package/dist/canvas/keys.js.map +1 -0
  18. package/dist/canvas/layout.d.ts +26 -0
  19. package/dist/canvas/layout.d.ts.map +1 -0
  20. package/dist/canvas/layout.js +73 -0
  21. package/dist/canvas/layout.js.map +1 -0
  22. package/dist/canvas/scroll.d.ts +31 -0
  23. package/dist/canvas/scroll.d.ts.map +1 -0
  24. package/dist/canvas/scroll.js +111 -0
  25. package/dist/canvas/scroll.js.map +1 -0
  26. package/dist/canvas/text.d.ts +23 -0
  27. package/dist/canvas/text.d.ts.map +1 -0
  28. package/dist/canvas/text.js +142 -0
  29. package/dist/canvas/text.js.map +1 -0
  30. package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
  31. package/dist/canvas/theme.d.ts.map +1 -0
  32. package/dist/canvas/theme.js +62 -0
  33. package/dist/canvas/theme.js.map +1 -0
  34. package/dist/core/cache.d.ts +48 -0
  35. package/dist/core/cache.d.ts.map +1 -0
  36. package/dist/core/cache.js +222 -0
  37. package/dist/core/cache.js.map +1 -0
  38. package/dist/core/checked.d.ts +20 -0
  39. package/dist/core/checked.d.ts.map +1 -0
  40. package/dist/core/checked.js +61 -0
  41. package/dist/core/checked.js.map +1 -0
  42. package/dist/core/client.d.ts +33 -0
  43. package/dist/core/client.d.ts.map +1 -0
  44. package/dist/core/client.js +103 -0
  45. package/dist/core/client.js.map +1 -0
  46. package/dist/core/conditions.d.ts +48 -0
  47. package/dist/core/conditions.d.ts.map +1 -0
  48. package/dist/core/conditions.js +105 -0
  49. package/dist/core/conditions.js.map +1 -0
  50. package/dist/core/evaluate.d.ts +20 -0
  51. package/dist/core/evaluate.d.ts.map +1 -0
  52. package/dist/core/evaluate.js +210 -0
  53. package/dist/core/evaluate.js.map +1 -0
  54. package/dist/core/filters.d.ts +51 -0
  55. package/dist/core/filters.d.ts.map +1 -0
  56. package/dist/core/filters.js +303 -0
  57. package/dist/core/filters.js.map +1 -0
  58. package/dist/core/format.d.ts +14 -0
  59. package/dist/core/format.d.ts.map +1 -0
  60. package/dist/core/format.js +303 -0
  61. package/dist/core/format.js.map +1 -0
  62. package/dist/core/measures.d.ts +26 -0
  63. package/dist/core/measures.d.ts.map +1 -0
  64. package/dist/core/measures.js +105 -0
  65. package/dist/core/measures.js.map +1 -0
  66. package/dist/core/request-ids.d.ts +20 -0
  67. package/dist/core/request-ids.d.ts.map +1 -0
  68. package/dist/core/request-ids.js +55 -0
  69. package/dist/core/request-ids.js.map +1 -0
  70. package/dist/core/rows.d.ts +79 -0
  71. package/dist/core/rows.d.ts.map +1 -0
  72. package/dist/core/rows.js +173 -0
  73. package/dist/core/rows.js.map +1 -0
  74. package/dist/core/store.d.ts +233 -0
  75. package/dist/core/store.d.ts.map +1 -0
  76. package/dist/core/store.js +1162 -0
  77. package/dist/core/store.js.map +1 -0
  78. package/dist/core/totals.d.ts +24 -0
  79. package/dist/core/totals.d.ts.map +1 -0
  80. package/dist/core/totals.js +49 -0
  81. package/dist/core/totals.js.map +1 -0
  82. package/dist/core/trace.d.ts +7 -0
  83. package/dist/core/trace.d.ts.map +1 -0
  84. package/dist/core/trace.js +26 -0
  85. package/dist/core/trace.js.map +1 -0
  86. package/dist/core/transports.d.ts +50 -0
  87. package/dist/core/transports.d.ts.map +1 -0
  88. package/dist/core/transports.js +160 -0
  89. package/dist/core/transports.js.map +1 -0
  90. package/dist/core/types.d.ts +150 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +45 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/core.d.ts +12 -0
  95. package/dist/core.d.ts.map +1 -0
  96. package/dist/core.js +11 -0
  97. package/dist/core.js.map +1 -0
  98. package/dist/index.d.ts +15 -6
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +13 -7
  101. package/dist/index.js.map +1 -1
  102. package/dist/react/accessible-rows.d.ts +13 -0
  103. package/dist/react/accessible-rows.d.ts.map +1 -0
  104. package/dist/react/accessible-rows.js +320 -0
  105. package/dist/react/accessible-rows.js.map +1 -0
  106. package/dist/react/cell-editor.d.ts +22 -0
  107. package/dist/react/cell-editor.d.ts.map +1 -0
  108. package/dist/react/cell-editor.js +431 -0
  109. package/dist/react/cell-editor.js.map +1 -0
  110. package/dist/react/cell-menu.d.ts +18 -0
  111. package/dist/react/cell-menu.d.ts.map +1 -0
  112. package/dist/react/cell-menu.js +299 -0
  113. package/dist/react/cell-menu.js.map +1 -0
  114. package/dist/react/column-menu.d.ts +16 -0
  115. package/dist/react/column-menu.d.ts.map +1 -0
  116. package/dist/react/column-menu.js +1311 -0
  117. package/dist/react/column-menu.js.map +1 -0
  118. package/dist/react/columns-menu.d.ts +7 -0
  119. package/dist/react/columns-menu.d.ts.map +1 -0
  120. package/dist/react/columns-menu.js +249 -0
  121. package/dist/react/columns-menu.js.map +1 -0
  122. package/dist/react/conditions-menu.d.ts +10 -0
  123. package/dist/react/conditions-menu.d.ts.map +1 -0
  124. package/dist/react/conditions-menu.js +795 -0
  125. package/dist/react/conditions-menu.js.map +1 -0
  126. package/dist/react/context.d.ts +29 -0
  127. package/dist/react/context.d.ts.map +1 -0
  128. package/dist/react/context.js +18 -0
  129. package/dist/react/context.js.map +1 -0
  130. package/dist/react/data-grid.d.ts +79 -0
  131. package/dist/react/data-grid.d.ts.map +1 -0
  132. package/dist/react/data-grid.js +653 -0
  133. package/dist/react/data-grid.js.map +1 -0
  134. package/dist/react/detail-panel.d.ts +22 -0
  135. package/dist/react/detail-panel.d.ts.map +1 -0
  136. package/dist/react/detail-panel.js +242 -0
  137. package/dist/react/detail-panel.js.map +1 -0
  138. package/dist/react/export-menu.d.ts +11 -0
  139. package/dist/react/export-menu.d.ts.map +1 -0
  140. package/dist/react/export-menu.js +304 -0
  141. package/dist/react/export-menu.js.map +1 -0
  142. package/dist/react/find-bar.d.ts +11 -0
  143. package/dist/react/find-bar.d.ts.map +1 -0
  144. package/dist/react/find-bar.js +303 -0
  145. package/dist/react/find-bar.js.map +1 -0
  146. package/dist/react/format-form.d.ts +12 -0
  147. package/dist/react/format-form.d.ts.map +1 -0
  148. package/dist/react/format-form.js +863 -0
  149. package/dist/react/format-form.js.map +1 -0
  150. package/dist/react/formula-menu.d.ts +6 -0
  151. package/dist/react/formula-menu.d.ts.map +1 -0
  152. package/dist/react/formula-menu.js +343 -0
  153. package/dist/react/formula-menu.js.map +1 -0
  154. package/dist/react/grid-body.d.ts +33 -0
  155. package/dist/react/grid-body.d.ts.map +1 -0
  156. package/dist/react/grid-body.js +1389 -0
  157. package/dist/react/grid-body.js.map +1 -0
  158. package/dist/react/group-menu.d.ts +7 -0
  159. package/dist/react/group-menu.d.ts.map +1 -0
  160. package/dist/react/group-menu.js +508 -0
  161. package/dist/react/group-menu.js.map +1 -0
  162. package/dist/react/hooks.d.ts +33 -0
  163. package/dist/react/hooks.d.ts.map +1 -0
  164. package/dist/react/hooks.js +161 -0
  165. package/dist/react/hooks.js.map +1 -0
  166. package/dist/react/messages.d.ts +180 -0
  167. package/dist/react/messages.d.ts.map +1 -0
  168. package/dist/react/messages.js +414 -0
  169. package/dist/react/messages.js.map +1 -0
  170. package/dist/react/save.d.ts +9 -0
  171. package/dist/react/save.d.ts.map +1 -0
  172. package/dist/react/save.js +102 -0
  173. package/dist/react/save.js.map +1 -0
  174. package/dist/react/static-rows.d.ts +9 -0
  175. package/dist/react/static-rows.d.ts.map +1 -0
  176. package/dist/react/static-rows.js +176 -0
  177. package/dist/react/static-rows.js.map +1 -0
  178. package/dist/react/status-bar.d.ts +12 -0
  179. package/dist/react/status-bar.d.ts.map +1 -0
  180. package/dist/react/status-bar.js +413 -0
  181. package/dist/react/status-bar.js.map +1 -0
  182. package/dist/react/toolbar-popover.d.ts +14 -0
  183. package/dist/react/toolbar-popover.d.ts.map +1 -0
  184. package/dist/react/toolbar-popover.js +143 -0
  185. package/dist/react/toolbar-popover.js.map +1 -0
  186. package/dist/react/toolbar.d.ts +13 -0
  187. package/dist/react/toolbar.d.ts.map +1 -0
  188. package/dist/react/toolbar.js +344 -0
  189. package/dist/react/toolbar.js.map +1 -0
  190. package/dist/server.d.ts +22 -0
  191. package/dist/server.d.ts.map +1 -0
  192. package/dist/server.js +45 -0
  193. package/dist/server.js.map +1 -0
  194. package/dist/styles.css +496 -316
  195. package/package.json +42 -17
  196. package/src/canvas/geometry.ts +197 -0
  197. package/src/canvas/grid-canvas.ts +1222 -0
  198. package/src/canvas/hooks.ts +61 -0
  199. package/src/canvas/keys.ts +96 -0
  200. package/src/canvas/layout.ts +77 -0
  201. package/src/canvas/scroll.ts +120 -0
  202. package/src/canvas/text.ts +155 -0
  203. package/src/{theme.ts → canvas/theme.ts} +14 -1
  204. package/src/core/cache.ts +247 -0
  205. package/src/core/checked.ts +64 -0
  206. package/src/core/client.ts +103 -0
  207. package/src/core/conditions.ts +89 -0
  208. package/src/core/evaluate.ts +221 -0
  209. package/src/core/filters.ts +268 -0
  210. package/src/core/format.ts +309 -0
  211. package/src/core/measures.ts +99 -0
  212. package/src/core/request-ids.ts +54 -0
  213. package/src/core/rows.ts +213 -0
  214. package/src/core/store.ts +1122 -0
  215. package/src/core/totals.ts +59 -0
  216. package/src/core/trace.ts +44 -0
  217. package/src/core/transports.ts +177 -0
  218. package/src/core/types.ts +168 -0
  219. package/src/core.ts +39 -0
  220. package/src/index.ts +24 -12
  221. package/src/react/accessible-rows.tsx +242 -0
  222. package/src/react/cell-editor.tsx +216 -0
  223. package/src/react/cell-menu.tsx +166 -0
  224. package/src/react/column-menu.tsx +619 -0
  225. package/src/react/columns-menu.tsx +116 -0
  226. package/src/react/conditions-menu.tsx +289 -0
  227. package/src/react/context.ts +33 -0
  228. package/src/react/data-grid.tsx +286 -0
  229. package/src/react/detail-panel.tsx +92 -0
  230. package/src/react/export-menu.tsx +112 -0
  231. package/src/react/find-bar.tsx +112 -0
  232. package/src/react/format-form.tsx +421 -0
  233. package/src/react/formula-menu.tsx +163 -0
  234. package/src/react/grid-body.tsx +744 -0
  235. package/src/react/group-menu.tsx +214 -0
  236. package/src/react/hooks.ts +104 -0
  237. package/src/react/messages.ts +556 -0
  238. package/src/react/save.ts +42 -0
  239. package/src/react/static-rows.tsx +59 -0
  240. package/src/react/status-bar.tsx +175 -0
  241. package/src/react/toolbar-popover.tsx +69 -0
  242. package/src/react/toolbar.tsx +152 -0
  243. package/src/server.ts +58 -0
  244. package/src/styles.css +496 -316
  245. package/dist/components/bars.d.ts +0 -14
  246. package/dist/components/bars.d.ts.map +0 -1
  247. package/dist/components/bars.js +0 -68
  248. package/dist/components/bars.js.map +0 -1
  249. package/dist/components/cell-inspector.d.ts +0 -11
  250. package/dist/components/cell-inspector.d.ts.map +0 -1
  251. package/dist/components/cell-inspector.js +0 -37
  252. package/dist/components/cell-inspector.js.map +0 -1
  253. package/dist/components/columns-panel.d.ts +0 -12
  254. package/dist/components/columns-panel.d.ts.map +0 -1
  255. package/dist/components/columns-panel.js +0 -11
  256. package/dist/components/columns-panel.js.map +0 -1
  257. package/dist/components/filter-panel.d.ts +0 -22
  258. package/dist/components/filter-panel.d.ts.map +0 -1
  259. package/dist/components/filter-panel.js +0 -169
  260. package/dist/components/filter-panel.js.map +0 -1
  261. package/dist/components/find-bar.d.ts +0 -9
  262. package/dist/components/find-bar.d.ts.map +0 -1
  263. package/dist/components/find-bar.js +0 -46
  264. package/dist/components/find-bar.js.map +0 -1
  265. package/dist/components/popover.d.ts +0 -38
  266. package/dist/components/popover.d.ts.map +0 -1
  267. package/dist/components/popover.js +0 -104
  268. package/dist/components/popover.js.map +0 -1
  269. package/dist/components/stats-panel.d.ts +0 -12
  270. package/dist/components/stats-panel.d.ts.map +0 -1
  271. package/dist/components/stats-panel.js +0 -40
  272. package/dist/components/stats-panel.js.map +0 -1
  273. package/dist/controller.d.ts +0 -227
  274. package/dist/controller.d.ts.map +0 -1
  275. package/dist/controller.js +0 -845
  276. package/dist/controller.js.map +0 -1
  277. package/dist/data-grid.d.ts +0 -51
  278. package/dist/data-grid.d.ts.map +0 -1
  279. package/dist/data-grid.js +0 -704
  280. package/dist/data-grid.js.map +0 -1
  281. package/dist/labels.d.ts +0 -9
  282. package/dist/labels.d.ts.map +0 -1
  283. package/dist/labels.js +0 -38
  284. package/dist/labels.js.map +0 -1
  285. package/dist/messages.d.ts +0 -99
  286. package/dist/messages.d.ts.map +0 -1
  287. package/dist/messages.js +0 -276
  288. package/dist/messages.js.map +0 -1
  289. package/dist/renderer.d.ts +0 -52
  290. package/dist/renderer.d.ts.map +0 -1
  291. package/dist/renderer.js +0 -522
  292. package/dist/renderer.js.map +0 -1
  293. package/dist/text.d.ts +0 -14
  294. package/dist/text.d.ts.map +0 -1
  295. package/dist/text.js +0 -63
  296. package/dist/text.js.map +0 -1
  297. package/dist/theme.d.ts.map +0 -1
  298. package/dist/theme.js +0 -54
  299. package/dist/theme.js.map +0 -1
  300. package/src/components/bars.tsx +0 -180
  301. package/src/components/cell-inspector.tsx +0 -70
  302. package/src/components/columns-panel.tsx +0 -59
  303. package/src/components/filter-panel.tsx +0 -293
  304. package/src/components/find-bar.tsx +0 -104
  305. package/src/components/popover.tsx +0 -194
  306. package/src/components/stats-panel.tsx +0 -153
  307. package/src/controller.ts +0 -945
  308. package/src/data-grid.tsx +0 -1035
  309. package/src/labels.ts +0 -46
  310. package/src/messages.ts +0 -390
  311. package/src/renderer.ts +0 -535
  312. package/src/text.ts +0 -55
@@ -1,104 +0,0 @@
1
- import { type KeyboardEvent, useEffect, useRef, useState } from "react";
2
- import { isAbortError } from "vastgrid-core";
3
- import type { Messages } from "../messages.js";
4
-
5
- /** Find in the table (⌘F / Ctrl+F): moves the active cell to each match, wrapping at the ends. */
6
- export function FindBar({
7
- messages,
8
- initial,
9
- onFind,
10
- onClose,
11
- }: {
12
- messages: Messages;
13
- initial: string;
14
- onFind: (text: string, direction: 1 | -1, caseSensitive: boolean, signal: AbortSignal) => Promise<boolean>;
15
- onClose: () => void;
16
- }) {
17
- const [text, setText] = useState(initial);
18
- const [caseSensitive, setCaseSensitive] = useState(false);
19
- const [status, setStatus] = useState<"idle" | "searching" | "notFound">("idle");
20
- const input = useRef<HTMLInputElement>(null);
21
- const pending = useRef<AbortController | null>(null);
22
-
23
- useEffect(() => {
24
- input.current?.focus();
25
- input.current?.select();
26
- return () => pending.current?.abort();
27
- }, []);
28
-
29
- async function find(direction: 1 | -1) {
30
- pending.current?.abort();
31
- const controller = new AbortController();
32
- pending.current = controller;
33
- setStatus("searching");
34
- try {
35
- const found = await onFind(text, direction, caseSensitive, controller.signal);
36
- if (!controller.signal.aborted) setStatus(found || !text ? "idle" : "notFound");
37
- } catch (error) {
38
- if (!controller.signal.aborted && !isAbortError(error)) setStatus("notFound");
39
- }
40
- }
41
-
42
- function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
43
- if (event.key === "Enter") {
44
- event.preventDefault();
45
- void find(event.shiftKey ? -1 : 1);
46
- } else if (event.key === "Escape") {
47
- event.preventDefault();
48
- onClose();
49
- }
50
- }
51
-
52
- return (
53
- <search className="vg-find" aria-label={messages.find}>
54
- <input
55
- ref={input}
56
- type="search"
57
- className="vg-input"
58
- placeholder={messages.findPlaceholder}
59
- aria-label={messages.findPlaceholder}
60
- value={text}
61
- onChange={(event) => {
62
- setText(event.target.value);
63
- setStatus("idle");
64
- }}
65
- onKeyDown={onKeyDown}
66
- />
67
- <span className="vg-find__status" role="status">
68
- {status === "searching" ? messages.searching : status === "notFound" ? messages.notFound : ""}
69
- </span>
70
- <button
71
- type="button"
72
- className="vg-icon-button"
73
- aria-label={messages.findPrevious}
74
- title={`${messages.findPrevious} (Shift+Enter)`}
75
- onClick={() => void find(-1)}
76
- >
77
- ↑
78
- </button>
79
- <button
80
- type="button"
81
- className="vg-icon-button"
82
- aria-label={messages.findNext}
83
- title={`${messages.findNext} (Enter)`}
84
- onClick={() => void find(1)}
85
- >
86
- ↓
87
- </button>
88
- <label className="vg-find__case" title={messages.matchCase}>
89
- <input type="checkbox" checked={caseSensitive} onChange={(event) => setCaseSensitive(event.target.checked)} />
90
- <span aria-hidden="true">Aa</span>
91
- <span className="vg-sr-only">{messages.matchCase}</span>
92
- </label>
93
- <button
94
- type="button"
95
- className="vg-icon-button"
96
- aria-label={messages.close}
97
- title={`${messages.close} (Esc)`}
98
- onClick={onClose}
99
- >
100
- ×
101
- </button>
102
- </search>
103
- );
104
- }
@@ -1,194 +0,0 @@
1
- import {
2
- type KeyboardEvent,
3
- type ReactNode,
4
- type RefObject,
5
- useEffect,
6
- useLayoutEffect,
7
- useRef,
8
- useState,
9
- } from "react";
10
-
11
- export interface Anchor {
12
- /** Position relative to the grid's root element. */
13
- x: number;
14
- y: number;
15
- /** Height of the anchor, so that the popover can open above it when there is no room below. */
16
- height?: number;
17
- }
18
-
19
- /**
20
- * A panel positioned inside the grid's root. Closes on Escape and on pointer presses outside, and
21
- * returns focus to `returnFocus` when it closes.
22
- */
23
- export function Popover({
24
- anchor,
25
- container,
26
- onClose,
27
- returnFocus,
28
- label,
29
- role = "dialog",
30
- className,
31
- children,
32
- initialFocus = true,
33
- }: {
34
- anchor: Anchor;
35
- container: RefObject<HTMLElement | null>;
36
- onClose: () => void;
37
- returnFocus?: RefObject<HTMLElement | null>;
38
- label: string;
39
- role?: "dialog" | "menu";
40
- className?: string;
41
- children: ReactNode;
42
- initialFocus?: boolean;
43
- }) {
44
- const ref = useRef<HTMLDivElement>(null);
45
- const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
46
- const close = useRef(onClose);
47
- close.current = onClose;
48
-
49
- useLayoutEffect(() => {
50
- const element = ref.current;
51
- const root = container.current;
52
- if (!element || !root) return;
53
- const width = element.offsetWidth;
54
- const height = element.offsetHeight;
55
- const maxLeft = Math.max(4, root.clientWidth - width - 4);
56
- const below = anchor.y + (anchor.height ?? 0) + 2;
57
- const above = anchor.y - height - 2;
58
- const top = below + height <= root.clientHeight - 4 || above < 4 ? below : above;
59
- setPosition({ left: Math.min(Math.max(4, anchor.x), maxLeft), top: Math.max(4, top) });
60
- }, [anchor, container]);
61
-
62
- useEffect(() => {
63
- if (initialFocus) {
64
- const target = ref.current?.querySelector<HTMLElement>(
65
- "[data-autofocus], input, button, [tabindex='0'], [role='menuitem']",
66
- );
67
- target?.focus({ preventScroll: true });
68
- }
69
- const returnTo = returnFocus?.current;
70
- const element = ref.current;
71
- function onPointerDown(event: PointerEvent) {
72
- if (element && !element.contains(event.target as Node)) close.current();
73
- }
74
- document.addEventListener("pointerdown", onPointerDown, true);
75
- return () => {
76
- document.removeEventListener("pointerdown", onPointerDown, true);
77
- // Return focus when it was inside the panel (it falls back to the body when the panel is removed),
78
- // but not when the user moved it elsewhere, for example by clicking outside.
79
- const active = document.activeElement;
80
- if (returnTo && (!active || active === document.body || element?.contains(active))) {
81
- returnTo.focus({ preventScroll: true });
82
- }
83
- };
84
- }, [initialFocus, returnFocus]);
85
-
86
- function onKeyDown(event: KeyboardEvent) {
87
- if (event.key === "Escape") {
88
- event.preventDefault();
89
- event.stopPropagation();
90
- returnFocus?.current?.focus({ preventScroll: true });
91
- onClose();
92
- }
93
- }
94
-
95
- const props = {
96
- ref,
97
- className: `vg-popover${className ? ` ${className}` : ""}`,
98
- "aria-label": label,
99
- // The first render uses the anchor; the layout effect corrects the position before the browser paints.
100
- // (A hidden first render would make the panel unfocusable when the focus effect runs.)
101
- style: position ? { left: position.left, top: position.top } : { left: anchor.x, top: anchor.y },
102
- onKeyDown,
103
- };
104
- return role === "menu" ? (
105
- <div role="menu" {...props}>
106
- {children}
107
- </div>
108
- ) : (
109
- <div role="dialog" {...props}>
110
- {children}
111
- </div>
112
- );
113
- }
114
-
115
- export interface MenuItem {
116
- label: string;
117
- onSelect: () => void;
118
- disabled?: boolean;
119
- /** Draws a separator above the item. */
120
- separator?: boolean;
121
- checked?: boolean;
122
- shortcut?: string;
123
- }
124
-
125
- /** A menu list with arrow-key navigation. Selecting an item closes the menu. */
126
- export function MenuList({ items, onClose }: { items: MenuItem[]; onClose: () => void }) {
127
- const ref = useRef<HTMLDivElement>(null);
128
-
129
- function move(event: KeyboardEvent, step: number) {
130
- const buttons = [...(ref.current?.querySelectorAll<HTMLButtonElement>("[role^='menuitem']:not(:disabled)") ?? [])];
131
- if (buttons.length === 0) return;
132
- event.preventDefault();
133
- const index = buttons.indexOf(document.activeElement as HTMLButtonElement);
134
- const next =
135
- step === Number.POSITIVE_INFINITY
136
- ? buttons.length - 1
137
- : step === Number.NEGATIVE_INFINITY
138
- ? 0
139
- : (index + step + buttons.length) % buttons.length;
140
- buttons[next]?.focus();
141
- }
142
-
143
- function onKeyDown(event: KeyboardEvent) {
144
- if (event.key === "ArrowDown") move(event, 1);
145
- else if (event.key === "ArrowUp") move(event, -1);
146
- else if (event.key === "Home") move(event, Number.NEGATIVE_INFINITY);
147
- else if (event.key === "End") move(event, Number.POSITIVE_INFINITY);
148
- else if (event.key === "Tab") {
149
- event.preventDefault();
150
- onClose();
151
- }
152
- }
153
-
154
- return (
155
- // biome-ignore lint/a11y/noStaticElementInteractions: the menu items inside are the interactive elements; this handles their arrow keys.
156
- <div ref={ref} className="vg-menu" onKeyDown={onKeyDown}>
157
- {items.map((item) => (
158
- <div key={item.label}>
159
- {item.separator && <hr className="vg-menu__separator" />}
160
- {item.checked === undefined ? (
161
- <button
162
- type="button"
163
- role="menuitem"
164
- className="vg-menu__item"
165
- disabled={item.disabled}
166
- onClick={() => {
167
- onClose();
168
- item.onSelect();
169
- }}
170
- >
171
- <span>{item.label}</span>
172
- {item.shortcut && <kbd>{item.shortcut}</kbd>}
173
- </button>
174
- ) : (
175
- <button
176
- type="button"
177
- role="menuitemcheckbox"
178
- aria-checked={item.checked}
179
- className="vg-menu__item"
180
- disabled={item.disabled}
181
- onClick={() => {
182
- onClose();
183
- item.onSelect();
184
- }}
185
- >
186
- <span>{item.label}</span>
187
- {item.shortcut && <kbd>{item.shortcut}</kbd>}
188
- </button>
189
- )}
190
- </div>
191
- ))}
192
- </div>
193
- );
194
- }
@@ -1,153 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { type ColumnSchema, type ColumnStats, isAbortError, isTemporalType, type ValueFormatter } from "vastgrid-core";
3
- import type { Messages } from "../messages.js";
4
-
5
- function temporalLabel(seconds: number, type: ColumnSchema["type"]) {
6
- const date = new Date(seconds * 1000);
7
- if (Number.isNaN(date.getTime())) return String(seconds);
8
- const iso = date.toISOString();
9
- return type === "date" ? iso.slice(0, 10) : iso.slice(0, 16).replace("T", " ");
10
- }
11
-
12
- /** Statistics of one column: counts, range, distribution and the most common values. */
13
- export function StatsPanel({
14
- schema,
15
- messages,
16
- formatter,
17
- count,
18
- number,
19
- load,
20
- }: {
21
- schema: ColumnSchema;
22
- messages: Messages;
23
- formatter: ValueFormatter;
24
- count: (value: number) => string;
25
- number: (value: number) => string;
26
- load: (signal: AbortSignal) => Promise<ColumnStats> | null;
27
- }) {
28
- const [stats, setStats] = useState<ColumnStats | null>(null);
29
- const [error, setError] = useState<string | null>(null);
30
-
31
- useEffect(() => {
32
- const controller = new AbortController();
33
- load(controller.signal)
34
- ?.then((result) => {
35
- if (!controller.signal.aborted) setStats(result);
36
- })
37
- .catch((cause: unknown) => {
38
- if (controller.signal.aborted || isAbortError(cause)) return;
39
- setError(cause instanceof Error ? cause.message : String(cause));
40
- });
41
- return () => controller.abort();
42
- }, [load]);
43
-
44
- const s = messages.stats;
45
- const value = (item: ColumnStats["min"]) =>
46
- item === undefined || item === null ? "—" : formatter.format(item, schema.type);
47
- const percent = (part: number, whole: number) =>
48
- whole === 0 ? "0%" : `${((part / whole) * 100).toFixed(part === 0 || part === whole ? 0 : 1)}%`;
49
- const histogram = stats?.histogram ?? null;
50
- const peak = histogram ? Math.max(1, ...histogram.map((bin) => bin.count)) : 1;
51
- const topPeak = stats?.top?.length ? Math.max(1, ...stats.top.map((item) => item.count)) : 1;
52
- const edge = (point: number) => (isTemporalType(schema.type) ? temporalLabel(point, schema.type) : number(point));
53
-
54
- return (
55
- <div className="vg-stats">
56
- <p className="vg-panel__title">
57
- {schema.name}
58
- <span className="vg-panel__muted">
59
- {" "}
60
- · {messages.types[schema.type] ?? schema.type}
61
- {schema.sourceType ? ` (${schema.sourceType})` : ""}
62
- </span>
63
- </p>
64
- {error && <p className="vg-panel__error">{error}</p>}
65
- {!stats && !error && <p className="vg-panel__muted">{messages.loading}</p>}
66
- {stats && (
67
- <>
68
- <dl className="vg-stats__list">
69
- <dt>{s.rows}</dt>
70
- <dd>{count(stats.rows)}</dd>
71
- <dt>{s.nulls}</dt>
72
- <dd>
73
- {count(stats.nulls)} ({percent(stats.nulls, stats.rows)})
74
- </dd>
75
- <dt>{s.distinct}</dt>
76
- <dd>
77
- {stats.distinct === null ? "—" : `${stats.distinctApproximate ? "≈ " : ""}${count(stats.distinct)}`}
78
- </dd>
79
- <dt>{s.min}</dt>
80
- <dd>{value(stats.min)}</dd>
81
- <dt>{s.max}</dt>
82
- <dd>{value(stats.max)}</dd>
83
- {stats.mean !== undefined && stats.mean !== null && !isTemporalType(schema.type) && (
84
- <>
85
- <dt>{s.mean}</dt>
86
- <dd>{number(stats.mean)}</dd>
87
- </>
88
- )}
89
- {stats.stddev !== undefined && stats.stddev !== null && !isTemporalType(schema.type) && (
90
- <>
91
- <dt>{s.stddev}</dt>
92
- <dd>{number(stats.stddev)}</dd>
93
- </>
94
- )}
95
- {stats.quantiles && (
96
- <>
97
- <dt>{s.quartiles}</dt>
98
- <dd>
99
- {stats.quantiles.map((item) => (typeof item === "number" ? number(item) : value(item))).join(" / ")}
100
- </dd>
101
- </>
102
- )}
103
- {stats.minLength !== undefined && stats.minLength !== null && (
104
- <>
105
- <dt>{s.length}</dt>
106
- <dd>
107
- {count(stats.minLength)} – {count(stats.maxLength ?? stats.minLength)}
108
- </dd>
109
- </>
110
- )}
111
- </dl>
112
- {histogram && histogram.length > 1 && (
113
- <figure className="vg-stats__histogram">
114
- <figcaption>{s.histogram}</figcaption>
115
- <div className="vg-stats__bars">
116
- {histogram.map((bin) => (
117
- <span
118
- key={bin.start}
119
- className="vg-stats__bar"
120
- style={{ height: `${Math.max(bin.count > 0 ? 3 : 0, (bin.count / peak) * 100)}%` }}
121
- title={`${edge(bin.start)} – ${edge(bin.end)}: ${count(bin.count)}`}
122
- />
123
- ))}
124
- </div>
125
- <div className="vg-stats__axis">
126
- <span>{edge(histogram[0].start)}</span>
127
- <span>{edge(histogram[histogram.length - 1].end)}</span>
128
- </div>
129
- </figure>
130
- )}
131
- {stats.top && stats.top.length > 0 && (
132
- <figure className="vg-stats__top">
133
- <figcaption>{s.top}</figcaption>
134
- <ul>
135
- {stats.top.map((item) => (
136
- <li key={`${typeof item.value}:${String(item.value)}`}>
137
- <span className="vg-stats__top-bar" style={{ width: `${(item.count / topPeak) * 100}%` }} />
138
- <span className="vg-stats__top-label">
139
- {item.value === null || item.value === ""
140
- ? messages.blank
141
- : formatter.format(item.value, schema.type)}
142
- </span>
143
- <span className="vg-stats__top-count">{count(item.count)}</span>
144
- </li>
145
- ))}
146
- </ul>
147
- </figure>
148
- )}
149
- </>
150
- )}
151
- </div>
152
- );
153
- }