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
@@ -0,0 +1,175 @@
1
+ import { use, useEffect, useState } from "react";
2
+ import type { CellRange } from "vastgrid-spec";
3
+ import { checkedCount } from "../core/checked.js";
4
+ import type { GridStore } from "../core/store.js";
5
+ import { LocaleContext, SaveContext } from "./context.js";
6
+ import { useGridSelector, useGridStore } from "./hooks.js";
7
+
8
+ interface Summary {
9
+ count: number;
10
+ numericCount: number;
11
+ sum: unknown;
12
+ average: unknown;
13
+ }
14
+
15
+ /** How long the selection must stay before it is summed. */
16
+ const SUMMARY_DELAY = 150;
17
+
18
+ /**
19
+ * The count and sum of a selection of one range of several cells (`summarize`, §8.2), read once the selection has
20
+ * stayed for a moment, so that holding a key or dragging never waits for it; null otherwise.
21
+ */
22
+ function useSummary(store: GridStore, selection: readonly CellRange[] | undefined): Summary | null {
23
+ const [result, setResult] = useState<{ selection: readonly CellRange[]; summary: Summary } | null>(null);
24
+ const summarizes = useGridSelector((snapshot) => snapshot.table?.capabilities.summarize === true);
25
+ const range = selection?.length === 1 ? selection[0] : undefined;
26
+ const several =
27
+ range !== undefined &&
28
+ (range.rows === "all" || range.rows[1] - range.rows[0] > 1 || range.columns === "all" || range.columns.length > 1);
29
+ useEffect(() => {
30
+ if (!summarizes || !several || !selection || !range) return;
31
+ const abort = new AbortController();
32
+ const timer = setTimeout(() => {
33
+ const snapshot = store.getSnapshot();
34
+ const rowCount = snapshot.view?.rowCount ?? 0;
35
+ const rows: [number, number] = range.rows === "all" ? [0, rowCount] : [range.rows[0], range.rows[1]];
36
+ const columns = range.columns === "all" ? snapshot.displayed.map((item) => item.column.name) : range.columns;
37
+ store
38
+ .summarize([rows], columns, abort.signal)
39
+ .then((summary) => setResult({ selection, summary }))
40
+ .catch(() => {});
41
+ }, SUMMARY_DELAY);
42
+ return () => {
43
+ clearTimeout(timer);
44
+ abort.abort();
45
+ };
46
+ }, [store, summarizes, several, selection, range]);
47
+ return result && result.selection === selection ? result.summary : null;
48
+ }
49
+
50
+ function format(number: Intl.NumberFormat, value: unknown) {
51
+ if (typeof value === "number") return number.format(value);
52
+ return typeof value === "string" ? value : "";
53
+ }
54
+
55
+ interface StatusBarProps {
56
+ announce: (text: string) => void;
57
+ /** Gives the focus back to the grid, once a button that had it is gone. */
58
+ onDone: () => void;
59
+ }
60
+
61
+ /**
62
+ * The last write that failed (§6.6 of react.md): why, with Retry (Overwrite after a conflict) while the table
63
+ * has the revision its rows were read at, and Discard. Both give the focus back to the grid.
64
+ */
65
+ function FailedWrite({ announce, onDone }: StatusBarProps) {
66
+ const store = useGridStore();
67
+ const { messages } = use(LocaleContext);
68
+ const failed = useGridSelector((snapshot) => snapshot.failedWrite);
69
+ const revision = useGridSelector((snapshot) => snapshot.table?.revision);
70
+ if (!failed) return null;
71
+ const retry = failed.revision === revision ? failed.retry : null;
72
+ const text =
73
+ failed.error.code === "revision_changed" || (failed.retry !== null && retry === null)
74
+ ? messages.writeRevisionChanged
75
+ : failed.error.code === "edit_conflict"
76
+ ? messages.writeConflict
77
+ : messages.writeFailed(failed.error.message);
78
+ const send = () => {
79
+ onDone();
80
+ void store.retryWrite().then((outcome) => {
81
+ if (!outcome.ok && !store.getSnapshot().failedWrite) announce(messages.writeFailed(outcome.error.message));
82
+ });
83
+ };
84
+ return (
85
+ <span className="vg-statusbar__failed" role="alert">
86
+ <span className="vg-statusbar__error">{text}</span>
87
+ {retry && (
88
+ <button type="button" className="vg-button vg-button--small" onClick={send}>
89
+ {retry === "overwrite" ? messages.overwrite : messages.retry}
90
+ </button>
91
+ )}
92
+ <button
93
+ type="button"
94
+ className="vg-button vg-button--small"
95
+ onClick={() => {
96
+ onDone();
97
+ store.discardWrite();
98
+ }}
99
+ >
100
+ {messages.discard}
101
+ </button>
102
+ </span>
103
+ );
104
+ }
105
+
106
+ /**
107
+ * The status bar: the active cell, the sum of the selection, what the grid is doing (saving and the outcome of the
108
+ * last save included) or that the file changed on disk, a write that failed, the cells not saved yet and the rows.
109
+ */
110
+ export function StatusBar({ announce, onDone }: StatusBarProps) {
111
+ const store = useGridStore();
112
+ const { messages, number } = use(LocaleContext);
113
+ const saving = use(SaveContext);
114
+ const saves = useGridSelector((snapshot) => snapshot.table?.capabilities.save === true);
115
+ const edits = useGridSelector((snapshot) => snapshot.table?.edits);
116
+ const revision = useGridSelector((snapshot) => snapshot.table?.revision);
117
+ const fileChanged = useGridSelector((snapshot) => snapshot.table?.fileChanged === true);
118
+ const rowCount = useGridSelector((snapshot) => snapshot.view?.rowCount);
119
+ const building = useGridSelector((snapshot) => snapshot.view?.status === "building");
120
+ const pending = useGridSelector((snapshot) => snapshot.pending !== null);
121
+ const failedWrite = useGridSelector((snapshot) => snapshot.failedWrite !== null);
122
+ const error = useGridSelector((snapshot) => snapshot.error);
123
+ const active = useGridSelector((snapshot) => snapshot.session.active);
124
+ const selection = useGridSelector((snapshot) => snapshot.session.selection);
125
+ const summary = useSummary(store, selection);
126
+ const checked = useGridSelector((snapshot) =>
127
+ checkedCount(snapshot.session.checked, snapshot.document.plan, snapshot.view?.rowCount ?? 0),
128
+ );
129
+ const doing = error
130
+ ? messages.failed(error.message)
131
+ : rowCount === undefined
132
+ ? messages.loading
133
+ : saving?.pending
134
+ ? messages.saving
135
+ : saving?.outcome && edits && revision === saving.outcome.revision && edits.version <= saving.outcome.version
136
+ ? saving.outcome.text
137
+ : fileChanged
138
+ ? messages.changedOnDisk
139
+ : pending || building
140
+ ? messages.reading
141
+ : "";
142
+ return (
143
+ <div className="vg-statusbar">
144
+ <span className="vg-statusbar__cell">
145
+ {active && rowCount !== undefined && rowCount > 0
146
+ ? messages.cell(number.format(active.row + 1), active.column)
147
+ : ""}
148
+ </span>
149
+ <span className="vg-toolbar__spacer" />
150
+ {summary && (
151
+ <span className="vg-statusbar__summary">
152
+ <span>{messages.count(number.format(summary.count))}</span>
153
+ {summary.numericCount > 0 && <span>{messages.sum(format(number, summary.sum))}</span>}
154
+ {summary.numericCount > 0 && <span>{messages.average(format(number, summary.average))}</span>}
155
+ </span>
156
+ )}
157
+ {failedWrite ? (
158
+ <FailedWrite announce={announce} onDone={onDone} />
159
+ ) : (
160
+ <span className="vg-statusbar__muted" role="status">
161
+ {doing}
162
+ </span>
163
+ )}
164
+ {checked !== 0 && (
165
+ <span className="vg-statusbar__checked">
166
+ {checked === null ? messages.checkedUnknown : messages.checkedRows(number.format(checked))}
167
+ </span>
168
+ )}
169
+ {saves && edits && edits.cells > 0 && (
170
+ <span className="vg-statusbar__unsaved">{messages.unsaved(number.format(edits.cells))}</span>
171
+ )}
172
+ <span className="vg-toolbar__count">{rowCount === undefined ? "" : messages.rows(number.format(rowCount))}</span>
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,69 @@
1
+ import { type ReactNode, Suspense, type ToggleEvent, use, useEffect, useId, useRef, useState } from "react";
2
+ import { LocaleContext } from "./context.js";
3
+
4
+ /**
5
+ * A button of the toolbar and its popover, whose content is rendered only while it is open, so that a content loaded
6
+ * with `lazy` is downloaded when it is first opened (§10 of react.md) and starts from the state as it is then. The
7
+ * popover opens under the button, its right edge at the button's, and follows its content's size.
8
+ */
9
+ export function ToolbarPopover({
10
+ label,
11
+ title,
12
+ className,
13
+ pressed,
14
+ children,
15
+ }: {
16
+ label: ReactNode;
17
+ title: string;
18
+ className: string;
19
+ pressed?: boolean;
20
+ children: (close: () => void) => ReactNode;
21
+ }) {
22
+ const { messages } = use(LocaleContext);
23
+ const [open, setOpen] = useState(false);
24
+ const id = useId();
25
+ const buttonRef = useRef<HTMLButtonElement>(null);
26
+ const menuRef = useRef<HTMLDivElement>(null);
27
+
28
+ useEffect(() => {
29
+ const menu = menuRef.current;
30
+ if (!open || !menu) return;
31
+ const place = () => {
32
+ const button = buttonRef.current;
33
+ if (!button) return;
34
+ const rect = button.getBoundingClientRect();
35
+ menu.style.top = `${rect.bottom + 4}px`;
36
+ menu.style.left = `${Math.max(8, rect.right - menu.offsetWidth)}px`;
37
+ };
38
+ place();
39
+ // The content may arrive after the popover opened.
40
+ const observer = new ResizeObserver(place);
41
+ observer.observe(menu);
42
+ return () => observer.disconnect();
43
+ }, [open]);
44
+
45
+ const onToggle = (event: ToggleEvent<HTMLDivElement>) => setOpen(event.newState === "open");
46
+ // By id, not by the ref: the content gets it during render.
47
+ const close = () => document.getElementById(id)?.hidePopover();
48
+
49
+ return (
50
+ <>
51
+ <button ref={buttonRef} type="button" className="vg-button" popoverTarget={id} aria-pressed={pressed}>
52
+ {label}
53
+ </button>
54
+ <div
55
+ ref={menuRef}
56
+ id={id}
57
+ popover="auto"
58
+ role="dialog"
59
+ className={`vg-menu ${className}`}
60
+ aria-label={title}
61
+ onToggle={onToggle}
62
+ >
63
+ {open && (
64
+ <Suspense fallback={<p className="vg-filter__note">{messages.loading}</p>}>{children(close)}</Suspense>
65
+ )}
66
+ </div>
67
+ </>
68
+ );
69
+ }
@@ -0,0 +1,152 @@
1
+ import { lazy, use, useDeferredValue, useEffect, useState, useTransition } from "react";
2
+ import { type Expr, partsOf } from "vastgrid-spec";
3
+ import { isApple } from "../canvas/keys.js";
4
+ import { offers } from "../core/measures.js";
5
+ import { LocaleContext, SaveContext } from "./context.js";
6
+ import { ExportMenu } from "./export-menu.js";
7
+ import { useGridSelector, useGridStore } from "./hooks.js";
8
+ import { ToolbarPopover } from "./toolbar-popover.js";
9
+
10
+ // The menus load when they first open (§10 of react.md).
11
+ const ConditionsPanel = lazy(() => import("./conditions-menu.js"));
12
+ const ColumnsPanel = lazy(() => import("./columns-menu.js"));
13
+ const GroupPanel = lazy(() => import("./group-menu.js"));
14
+ const FormulaPanel = lazy(() => import("./formula-menu.js"));
15
+
16
+ /** How long typing must pause before the search is set. */
17
+ const SEARCH_DELAY = 200;
18
+
19
+ /** The text of the search part of a plan's `where` (§6.2.1). */
20
+ function searchOf(where: Expr | undefined): string {
21
+ for (const part of partsOf(where)) if ("search" in part) return part.search;
22
+ return "";
23
+ }
24
+
25
+ /**
26
+ * The toolbar: a search box when the engine searches (§3.5), the conditions menu when it filters (§6.2.1), the
27
+ * columns menu (§6.3), the group menu when it groups rows (§4.5), the formula
28
+ * menu when it derives columns (§4.11), a
29
+ * button that saves the written cells when the engine saves (§7.5), and one that reads the file again when it
30
+ * changed on disk while cells were written (§7.6). The
31
+ * search's text is React state, which updates as the person types; `search.set` follows through
32
+ * `useDeferredValue` once typing pauses.
33
+ */
34
+ export function Toolbar({ announce }: { announce: (text: string) => void }) {
35
+ const store = useGridStore();
36
+ const { messages } = use(LocaleContext);
37
+ const fileChanged = useGridSelector((snapshot) => snapshot.table?.fileChanged === true);
38
+ const [reloading, startReload] = useTransition();
39
+ const reload = () =>
40
+ startReload(async () => {
41
+ try {
42
+ await store.reload();
43
+ } catch (error) {
44
+ announce(messages.failed(error instanceof Error ? error.message : String(error)));
45
+ }
46
+ });
47
+ const searches = useGridSelector(
48
+ (snapshot) => snapshot.table !== null && snapshot.table.capabilities.plan?.search !== false,
49
+ );
50
+ const saving = use(SaveContext);
51
+ const saves = useGridSelector((snapshot) => snapshot.table?.capabilities.save === true);
52
+ const unsaved = useGridSelector((snapshot) => snapshot.table?.edits.cells ?? 0);
53
+ const current = useGridSelector((snapshot) => searchOf(snapshot.document.plan?.where));
54
+ const [text, setText] = useState(current);
55
+ const [shown, setShown] = useState(current);
56
+ // A search set elsewhere (an agent, a saved view) replaces the text.
57
+ if (shown !== current) {
58
+ setShown(current);
59
+ setText(current);
60
+ }
61
+ const deferred = useDeferredValue(text);
62
+ useEffect(() => {
63
+ if (deferred === current) return;
64
+ const timer = setTimeout(() => {
65
+ void store.dispatch({ command: "search.set", text: deferred });
66
+ }, SEARCH_DELAY);
67
+ return () => clearTimeout(timer);
68
+ }, [store, deferred, current]);
69
+ const showsSave = saves && saving !== null;
70
+ const exports = useGridSelector((snapshot) => (snapshot.table?.capabilities.export?.formats.length ?? 0) > 0);
71
+ const derives = useGridSelector(
72
+ (snapshot) => snapshot.table !== null && snapshot.table.capabilities.plan?.derive !== false,
73
+ );
74
+ const groups = useGridSelector((snapshot) => snapshot.table !== null && offers(snapshot.table.capabilities, "count"));
75
+ const grouped = useGridSelector((snapshot) => snapshot.document.plan?.aggregate !== undefined);
76
+ const filters = useGridSelector(
77
+ (snapshot) => snapshot.table !== null && snapshot.table.capabilities.plan?.where !== false,
78
+ );
79
+ const conditions = useGridSelector(
80
+ (snapshot) => partsOf(snapshot.document.plan?.where).filter((part) => !("search" in part)).length,
81
+ );
82
+ const opened = useGridSelector((snapshot) => snapshot.view !== null);
83
+ const detail = useGridSelector((snapshot) => snapshot.session.detail?.open === true);
84
+ if (!searches && !showsSave && !fileChanged && !exports && !derives && !groups && !opened) return null;
85
+ return (
86
+ <div className="vg-toolbar">
87
+ {searches && (
88
+ <input
89
+ type="search"
90
+ className="vg-input vg-toolbar__search"
91
+ aria-label={messages.search}
92
+ placeholder={messages.search}
93
+ value={text}
94
+ onChange={(event) => setText(event.target.value)}
95
+ />
96
+ )}
97
+ <span className="vg-toolbar__spacer" />
98
+ {fileChanged && (
99
+ <button type="button" className="vg-button" disabled={reloading} onClick={reload}>
100
+ {unsaved > 0 ? messages.discardAndReload : messages.reload}
101
+ </button>
102
+ )}
103
+ {opened && (
104
+ <button
105
+ type="button"
106
+ className="vg-button"
107
+ aria-pressed={detail}
108
+ onClick={() => void store.dispatch({ command: detail ? "detail.close" : "detail.open" })}
109
+ >
110
+ {messages.details}
111
+ </button>
112
+ )}
113
+ {filters && (
114
+ <ToolbarPopover
115
+ label={conditions > 0 ? messages.conditionsCount(String(conditions)) : messages.conditions}
116
+ title={messages.conditionsTitle}
117
+ className="vg-conditions"
118
+ pressed={conditions > 0}
119
+ >
120
+ {(close) => <ConditionsPanel close={close} />}
121
+ </ToolbarPopover>
122
+ )}
123
+ {opened && (
124
+ <ToolbarPopover label={messages.columns} title={messages.columns} className="vg-columns">
125
+ {() => <ColumnsPanel />}
126
+ </ToolbarPopover>
127
+ )}
128
+ {groups && (
129
+ <ToolbarPopover label={messages.groups} title={messages.groups} className="vg-groups" pressed={grouped}>
130
+ {() => <GroupPanel />}
131
+ </ToolbarPopover>
132
+ )}
133
+ {derives && (
134
+ <ToolbarPopover label={messages.formulas} title={messages.formulas} className="vg-formulas">
135
+ {() => <FormulaPanel />}
136
+ </ToolbarPopover>
137
+ )}
138
+ <ExportMenu announce={announce} />
139
+ {showsSave && (
140
+ <button
141
+ type="button"
142
+ className="vg-button"
143
+ aria-keyshortcuts={isApple() ? "Meta+S" : "Control+S"}
144
+ disabled={unsaved === 0 || saving.pending}
145
+ onClick={saving.save}
146
+ >
147
+ {messages.save}
148
+ </button>
149
+ )}
150
+ </div>
151
+ );
152
+ }
package/src/server.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * What a Server Component or a server render calls (§7 of react.md). Imports no React.
3
+ */
4
+ import {
5
+ checkPlan,
6
+ displayedColumns,
7
+ emptyDocument,
8
+ type GridDocument,
9
+ planOptions,
10
+ repairDocument,
11
+ } from "vastgrid-spec";
12
+ import { GridClient, type Transport } from "./core/client.js";
13
+ import type { InitialView, OpenOptions, RowsResult, TableInfo } from "./core/types.js";
14
+
15
+ export interface ReadInitialOptions {
16
+ /** The `ref` of the table to open (§8.2). */
17
+ table: string;
18
+ open?: OpenOptions;
19
+ /** The document the grid starts with; its plan is the view read. */
20
+ document?: GridDocument;
21
+ /** Rows to read from the start of the view; 100 by default. */
22
+ rows?: number;
23
+ signal?: AbortSignal;
24
+ }
25
+
26
+ /**
27
+ * Reads the table, the view of a document and its first rows, for `initial` (§7 of react.md): give the result,
28
+ * or the promise of it, to `DataGrid`. Opens the table and closes it again.
29
+ */
30
+ export async function readInitialView(transport: Transport, options: ReadInitialOptions): Promise<InitialView> {
31
+ const client = new GridClient(transport);
32
+ const info = await client.request<TableInfo>(
33
+ "open",
34
+ { ref: options.table, ...(options.open ? { read: options.open } : {}) },
35
+ options.signal,
36
+ );
37
+ try {
38
+ const document = repairDocument(
39
+ options.document ?? emptyDocument(),
40
+ info.columns,
41
+ planOptions(info.capabilities),
42
+ ).document;
43
+ const plan = document.plan ?? {};
44
+ const view = checkPlan(plan, info.columns, planOptions(info.capabilities));
45
+ const columns = displayedColumns(document, view.columns).map((item) => item.column.name);
46
+ if (document.rowKey !== undefined && !columns.includes(document.rowKey)) columns.push(document.rowKey);
47
+ const wanted = options.rows ?? 100;
48
+ const end = info.rowCount === null ? wanted : Math.min(wanted, info.rowCount);
49
+ const rows = await client.request<RowsResult>(
50
+ "rows",
51
+ { table: info.table, revision: info.revision, plan, start: 0, end: Math.max(end, 1), columns },
52
+ options.signal,
53
+ );
54
+ return { table: info, plan, rows };
55
+ } finally {
56
+ await client.request("close", { table: info.table }).catch(() => {});
57
+ }
58
+ }