vastgrid 0.0.0-stage → 0.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -2
  3. package/dist/components/bars.d.ts +14 -0
  4. package/dist/components/bars.d.ts.map +1 -0
  5. package/dist/components/bars.js +67 -0
  6. package/dist/components/bars.js.map +1 -0
  7. package/dist/components/cell-inspector.d.ts +11 -0
  8. package/dist/components/cell-inspector.d.ts.map +1 -0
  9. package/dist/components/cell-inspector.js +37 -0
  10. package/dist/components/cell-inspector.js.map +1 -0
  11. package/dist/components/columns-panel.d.ts +12 -0
  12. package/dist/components/columns-panel.d.ts.map +1 -0
  13. package/dist/components/columns-panel.js +11 -0
  14. package/dist/components/columns-panel.js.map +1 -0
  15. package/dist/components/filter-panel.d.ts +17 -0
  16. package/dist/components/filter-panel.d.ts.map +1 -0
  17. package/dist/components/filter-panel.js +165 -0
  18. package/dist/components/filter-panel.js.map +1 -0
  19. package/dist/components/find-bar.d.ts +9 -0
  20. package/dist/components/find-bar.d.ts.map +1 -0
  21. package/dist/components/find-bar.js +46 -0
  22. package/dist/components/find-bar.js.map +1 -0
  23. package/dist/components/popover.d.ts +38 -0
  24. package/dist/components/popover.d.ts.map +1 -0
  25. package/dist/components/popover.js +104 -0
  26. package/dist/components/popover.js.map +1 -0
  27. package/dist/components/stats-panel.d.ts +12 -0
  28. package/dist/components/stats-panel.d.ts.map +1 -0
  29. package/dist/components/stats-panel.js +40 -0
  30. package/dist/components/stats-panel.js.map +1 -0
  31. package/dist/controller.d.ts +218 -0
  32. package/dist/controller.d.ts.map +1 -0
  33. package/dist/controller.js +788 -0
  34. package/dist/controller.js.map +1 -0
  35. package/dist/data-grid.d.ts +51 -0
  36. package/dist/data-grid.d.ts.map +1 -0
  37. package/dist/data-grid.js +698 -0
  38. package/dist/data-grid.js.map +1 -0
  39. package/dist/index.d.ts +7 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +7 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/labels.d.ts +9 -0
  44. package/dist/labels.d.ts.map +1 -0
  45. package/dist/labels.js +38 -0
  46. package/dist/labels.js.map +1 -0
  47. package/dist/messages.d.ts +99 -0
  48. package/dist/messages.d.ts.map +1 -0
  49. package/dist/messages.js +276 -0
  50. package/dist/messages.js.map +1 -0
  51. package/dist/renderer.d.ts +52 -0
  52. package/dist/renderer.d.ts.map +1 -0
  53. package/dist/renderer.js +522 -0
  54. package/dist/renderer.js.map +1 -0
  55. package/dist/styles.css +694 -0
  56. package/dist/text.d.ts +14 -0
  57. package/dist/text.d.ts.map +1 -0
  58. package/dist/text.js +63 -0
  59. package/dist/text.js.map +1 -0
  60. package/dist/theme.d.ts +32 -0
  61. package/dist/theme.d.ts.map +1 -0
  62. package/dist/theme.js +54 -0
  63. package/dist/theme.js.map +1 -0
  64. package/package.json +52 -4
  65. package/src/components/bars.tsx +179 -0
  66. package/src/components/cell-inspector.tsx +70 -0
  67. package/src/components/columns-panel.tsx +59 -0
  68. package/src/components/filter-panel.tsx +290 -0
  69. package/src/components/find-bar.tsx +104 -0
  70. package/src/components/popover.tsx +194 -0
  71. package/src/components/stats-panel.tsx +153 -0
  72. package/src/controller.ts +887 -0
  73. package/src/data-grid.tsx +1029 -0
  74. package/src/index.ts +13 -0
  75. package/src/labels.ts +46 -0
  76. package/src/messages.ts +390 -0
  77. package/src/renderer.ts +535 -0
  78. package/src/styles.css +694 -0
  79. package/src/text.ts +55 -0
  80. package/src/theme.ts +89 -0
package/dist/text.js ADDED
@@ -0,0 +1,63 @@
1
+ const ELLIPSIS = "…";
2
+ const MAX_ENTRIES = 20_000;
3
+ /** Measures and shortens text for one font, caching results because the same strings repeat a lot. */
4
+ export class TextFitter {
5
+ context;
6
+ font;
7
+ widths = new Map();
8
+ fits = new Map();
9
+ ellipsisWidth = -1;
10
+ constructor(context, font) {
11
+ this.context = context;
12
+ this.font = font;
13
+ }
14
+ /** Width of `text`. The caller must have set `context.font` to this fitter's font. */
15
+ width(text) {
16
+ let width = this.widths.get(text);
17
+ if (width === undefined) {
18
+ width = this.context.measureText(text).width;
19
+ if (this.widths.size > MAX_ENTRIES)
20
+ this.widths.clear();
21
+ this.widths.set(text, width);
22
+ }
23
+ return width;
24
+ }
25
+ /** `text`, shortened with "…" to fit in `maxWidth`. */
26
+ fit(text, maxWidth) {
27
+ if (maxWidth <= 0 || text === "")
28
+ return "";
29
+ // Cells never show more than a few hundred characters; skip measuring the rest.
30
+ const source = text.length > 400 ? text.slice(0, 400) : text;
31
+ if (source === text && this.width(text) <= maxWidth)
32
+ return text;
33
+ const key = `${Math.floor(maxWidth)}\u0001${source}`;
34
+ const cached = this.fits.get(key);
35
+ if (cached !== undefined)
36
+ return cached;
37
+ if (this.ellipsisWidth < 0)
38
+ this.ellipsisWidth = this.context.measureText(ELLIPSIS).width;
39
+ let result = "";
40
+ if (this.ellipsisWidth <= maxWidth) {
41
+ const full = this.context.measureText(source).width;
42
+ let low = 0;
43
+ let high = Math.min(source.length, Math.ceil((source.length * maxWidth) / Math.max(1, full)) + 4);
44
+ while (low < high) {
45
+ const middle = (low + high + 1) >> 1;
46
+ if (this.context.measureText(source.slice(0, middle)).width + this.ellipsisWidth <= maxWidth)
47
+ low = middle;
48
+ else
49
+ high = middle - 1;
50
+ }
51
+ // Do not split a surrogate pair.
52
+ const code = source.charCodeAt(low - 1);
53
+ if (low > 0 && code >= 0xd800 && code <= 0xdbff)
54
+ low--;
55
+ result = `${source.slice(0, low)}${ELLIPSIS}`;
56
+ }
57
+ if (this.fits.size > MAX_ENTRIES)
58
+ this.fits.clear();
59
+ this.fits.set(key, result);
60
+ return result;
61
+ }
62
+ }
63
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /** Colors and fonts for the canvas, read from the CSS custom properties of the grid's root element. */
2
+ export interface GridTheme {
3
+ font: string;
4
+ headerFont: string;
5
+ smallFont: string;
6
+ fontSize: number;
7
+ fontFamily: string;
8
+ background: string;
9
+ text: string;
10
+ muted: string;
11
+ headerBackground: string;
12
+ headerText: string;
13
+ headerActive: string;
14
+ gutterBackground: string;
15
+ gutterText: string;
16
+ gutterViewText: string;
17
+ line: string;
18
+ lineStrong: string;
19
+ selection: string;
20
+ selectionBorder: string;
21
+ activeBorder: string;
22
+ accent: string;
23
+ nullText: string;
24
+ placeholder: string;
25
+ match: string;
26
+ error: string;
27
+ }
28
+ /** Resolves the theme of `root`. Colors given as `light-dark()` or system colors resolve to concrete values. */
29
+ export declare function resolveTheme(root: HTMLElement): GridTheme;
30
+ /** Reads a length custom property in pixels, or `fallback`. */
31
+ export declare function cssLength(root: HTMLElement, token: string, fallback: number): number;
32
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AA2BD,gHAAgH;AAChH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,CA4BzD;AAED,+DAA+D;AAC/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAG3E"}
package/dist/theme.js ADDED
@@ -0,0 +1,54 @@
1
+ const colorTokens = {
2
+ background: "--vg-bg",
3
+ text: "--vg-fg",
4
+ muted: "--vg-muted",
5
+ headerBackground: "--vg-header-bg",
6
+ headerText: "--vg-header-fg",
7
+ headerActive: "--vg-header-active-bg",
8
+ gutterBackground: "--vg-gutter-bg",
9
+ gutterText: "--vg-gutter-fg",
10
+ gutterViewText: "--vg-gutter-view-fg",
11
+ line: "--vg-line",
12
+ lineStrong: "--vg-line-strong",
13
+ selection: "--vg-selection-bg",
14
+ selectionBorder: "--vg-selection-border",
15
+ activeBorder: "--vg-active-border",
16
+ accent: "--vg-accent",
17
+ nullText: "--vg-null-fg",
18
+ placeholder: "--vg-placeholder",
19
+ match: "--vg-match-bg",
20
+ error: "--vg-error-fg",
21
+ };
22
+ /** Resolves the theme of `root`. Colors given as `light-dark()` or system colors resolve to concrete values. */
23
+ export function resolveTheme(root) {
24
+ const probe = document.createElement("span");
25
+ probe.style.display = "none";
26
+ root.appendChild(probe);
27
+ try {
28
+ const color = (token) => {
29
+ probe.style.color = `var(${token})`;
30
+ return getComputedStyle(probe).color;
31
+ };
32
+ const style = getComputedStyle(root);
33
+ const fontFamily = style.getPropertyValue("--vg-font-family").trim() || style.fontFamily || "system-ui, sans-serif";
34
+ const fontSize = Number.parseFloat(style.getPropertyValue("--vg-font-size")) || Number.parseFloat(style.fontSize) || 13;
35
+ const colors = Object.fromEntries(Object.entries(colorTokens).map(([key, token]) => [key, color(token)]));
36
+ return {
37
+ ...colors,
38
+ fontFamily,
39
+ fontSize,
40
+ font: `${fontSize}px ${fontFamily}`,
41
+ headerFont: `600 ${fontSize}px ${fontFamily}`,
42
+ smallFont: `${Math.max(9, fontSize - 2)}px ${fontFamily}`,
43
+ };
44
+ }
45
+ finally {
46
+ probe.remove();
47
+ }
48
+ }
49
+ /** Reads a length custom property in pixels, or `fallback`. */
50
+ export function cssLength(root, token, fallback) {
51
+ const value = Number.parseFloat(getComputedStyle(root).getPropertyValue(token));
52
+ return Number.isFinite(value) && value > 0 ? value : fallback;
53
+ }
54
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,GAGb;IACF,UAAU,EAAE,SAAS;IACrB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,YAAY;IACnB,gBAAgB,EAAE,gBAAgB;IAClC,UAAU,EAAE,gBAAgB;IAC5B,YAAY,EAAE,uBAAuB;IACrC,gBAAgB,EAAE,gBAAgB;IAClC,UAAU,EAAE,gBAAgB;IAC5B,cAAc,EAAE,qBAAqB;IACrC,IAAI,EAAE,WAAW;IACjB,UAAU,EAAE,kBAAkB;IAC9B,SAAS,EAAE,mBAAmB;IAC9B,eAAe,EAAE,uBAAuB;IACxC,YAAY,EAAE,oBAAoB;IAClC,MAAM,EAAE,aAAa;IACrB,QAAQ,EAAE,cAAc;IACxB,WAAW,EAAE,kBAAkB;IAC/B,KAAK,EAAE,eAAe;IACtB,KAAK,EAAE,eAAe;CACvB,CAAC;AAEF,gHAAgH;AAChH,MAAM,UAAU,YAAY,CAAC,IAAiB;IAC5C,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7C,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAC7B,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE;YAC9B,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,OAAO,KAAK,GAAG,CAAC;YACpC,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC;QACvC,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,UAAU,GAAG,KAAK,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,UAAU,IAAI,uBAAuB,CAAC;QACpH,MAAM,QAAQ,GACZ,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,CAAC,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzG,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAGvG,CAAC;QACF,OAAO;YACL,GAAG,MAAM;YACT,UAAU;YACV,QAAQ;YACR,IAAI,EAAE,GAAG,QAAQ,MAAM,UAAU,EAAE;YACnC,UAAU,EAAE,OAAO,QAAQ,MAAM,UAAU,EAAE;YAC7C,SAAS,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,MAAM,UAAU,EAAE;SAC1D,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,SAAS,CAAC,IAAiB,EAAE,KAAa,EAAE,QAAgB;IAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "vastgrid",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A canvas data grid for React that opens CSV, TSV, Parquet and JSON Lines files of any size, with spreadsheet selection, sorting, filters and statistics.",
5
+ "license": "MIT",
6
+ "author": "yhay81",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yhay81/vastgrid.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "homepage": "https://github.com/yhay81/vastgrid#readme",
13
+ "bugs": "https://github.com/yhay81/vastgrid/issues",
14
+ "type": "module",
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "vastgrid-source": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./styles.css": {
25
+ "vastgrid-source": "./src/styles.css",
26
+ "default": "./dist/styles.css"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src"
32
+ ],
33
+ "keywords": [
34
+ "react",
35
+ "datagrid",
36
+ "data-grid",
37
+ "canvas",
38
+ "spreadsheet",
39
+ "csv",
40
+ "parquet",
41
+ "virtual-scroll",
42
+ "big-data"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsc -b"
46
+ },
47
+ "dependencies": {
48
+ "vastgrid-core": "^0.1.0"
49
+ },
50
+ "peerDependencies": {
51
+ "react": ">=18",
52
+ "react-dom": ">=18"
53
+ }
54
+ }
@@ -0,0 +1,179 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { GridController } from "../controller.js";
3
+ import { countFormat, filterLabel, numberFormat } from "../labels.js";
4
+ import type { Messages } from "../messages.js";
5
+
6
+ /** Search box, active sort and filters, row count and the columns button. */
7
+ export function Toolbar({
8
+ controller,
9
+ messages,
10
+ onColumns,
11
+ }: {
12
+ controller: GridController;
13
+ messages: Messages;
14
+ onColumns: (button: HTMLButtonElement) => void;
15
+ }) {
16
+ const query = controller.query;
17
+ const [search, setSearch] = useState(query.search ?? "");
18
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
19
+ const applied = query.search ?? "";
20
+ const lastApplied = useRef(applied);
21
+ // Follow changes made elsewhere (for example "Clear all").
22
+ if (applied !== lastApplied.current) {
23
+ lastApplied.current = applied;
24
+ if (search !== applied) setSearch(applied);
25
+ }
26
+ useEffect(() => () => clearTimeout(timer.current), []);
27
+
28
+ const ready = controller.info?.status === "ready";
29
+ const capabilities = controller.info?.capabilities;
30
+ const count = countFormat(controller.options.locale);
31
+ const filters = query.filters ?? [];
32
+ const sort = query.sort ?? [];
33
+ const total = controller.totalRows;
34
+ const filtered = controller.view.id !== null && (filters.length > 0 || Boolean(query.search?.trim()));
35
+ let rows: string;
36
+ if (controller.info?.status === "loading") {
37
+ const progress = controller.info.progress;
38
+ rows =
39
+ progress !== null && progress !== undefined && progress > 0
40
+ ? messages.loadingProgress(`${Math.round(progress * 100)}%`)
41
+ : messages.loadingRows(count(controller.readableRows));
42
+ } else if (filtered && total !== null) {
43
+ rows = messages.rowsOf(count(controller.rowCount), count(total));
44
+ } else {
45
+ rows = messages.rows(count(controller.rowCount));
46
+ }
47
+
48
+ function change(value: string) {
49
+ setSearch(value);
50
+ clearTimeout(timer.current);
51
+ timer.current = setTimeout(() => controller.setSearch(value), 350);
52
+ }
53
+
54
+ return (
55
+ <div className="vg-toolbar">
56
+ <input
57
+ type="search"
58
+ className="vg-input vg-toolbar__search"
59
+ placeholder={messages.searchPlaceholder}
60
+ aria-label={messages.search}
61
+ value={search}
62
+ disabled={!ready || !capabilities?.search}
63
+ onChange={(event) => change(event.target.value)}
64
+ onKeyDown={(event) => {
65
+ if (event.key === "Enter") {
66
+ clearTimeout(timer.current);
67
+ controller.setSearch(search);
68
+ }
69
+ }}
70
+ />
71
+ {sort.length > 0 && (
72
+ <span className="vg-chip">
73
+ <span>
74
+ {messages.sortedBy(
75
+ sort
76
+ .map(
77
+ (key) =>
78
+ `${controller.schema[key.column]?.name ?? key.column} ${key.direction === "asc" ? "↑" : "↓"}`,
79
+ )
80
+ .join(", "),
81
+ )}
82
+ </span>
83
+ <button
84
+ type="button"
85
+ className="vg-chip__remove"
86
+ aria-label={messages.clearSort}
87
+ title={messages.clearSort}
88
+ onClick={() => controller.clearSort()}
89
+ >
90
+ ×
91
+ </button>
92
+ </span>
93
+ )}
94
+ {filters.map((filter) => {
95
+ const label = filterLabel(filter, controller.schema[filter.column], controller.formatter, messages);
96
+ return (
97
+ <span className="vg-chip vg-chip--filter" key={filter.column}>
98
+ <span title={label}>{label}</span>
99
+ <button
100
+ type="button"
101
+ className="vg-chip__remove"
102
+ aria-label={messages.removeFilter(label)}
103
+ title={messages.removeFilter(label)}
104
+ onClick={() => controller.setFilter(filter.column, null)}
105
+ >
106
+ ×
107
+ </button>
108
+ </span>
109
+ );
110
+ })}
111
+ {(filters.length > 0 || Boolean(query.search?.trim())) && (
112
+ <button type="button" className="vg-button vg-button--small" onClick={() => controller.clearFilters()}>
113
+ {messages.clearFilters}
114
+ </button>
115
+ )}
116
+ <span className="vg-toolbar__spacer" />
117
+ <span className="vg-toolbar__count" aria-live="polite">
118
+ {controller.viewPending ? messages.buildingView : rows}
119
+ </span>
120
+ <button
121
+ type="button"
122
+ className="vg-button vg-button--small"
123
+ onClick={(event) => onColumns(event.currentTarget)}
124
+ disabled={!controller.info}
125
+ >
126
+ {messages.columns}
127
+ </button>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ /** The active cell and, for multi-cell selections, count, sum, average, minimum and maximum. */
133
+ export function StatusBar({ controller, messages }: { controller: GridController; messages: Messages }) {
134
+ const count = countFormat(controller.options.locale);
135
+ const number = numberFormat(controller.options.locale);
136
+ const active = controller.selection.active;
137
+ const schema = controller.columnSchema(active.col);
138
+ const summary = controller.summary;
139
+ const label = controller.rowLabel(active.row);
140
+ const notes = (controller.info?.notes ?? []).map((note) => messages.notes[note] ?? note);
141
+ return (
142
+ <div className="vg-statusbar">
143
+ {controller.rowCount > 0 && schema && (
144
+ <span className="vg-statusbar__cell">
145
+ {messages.cell(count(active.row + 1), schema.name)}
146
+ {controller.view.id !== null && label !== null && (
147
+ <span className="vg-statusbar__muted"> · {messages.originalRow(count(label))}</span>
148
+ )}
149
+ </span>
150
+ )}
151
+ {notes.length > 0 && <span className="vg-statusbar__note">{notes.join(" ")}</span>}
152
+ <span className="vg-toolbar__spacer" />
153
+ {summary && (
154
+ <span className="vg-statusbar__summary">
155
+ <span>
156
+ {messages.summary.count}: {count(summary.count)}
157
+ </span>
158
+ {summary.numericCount > 0 && summary.sum !== null && (
159
+ <>
160
+ <span>
161
+ {messages.summary.average}: {number(summary.average ?? 0)}
162
+ </span>
163
+ <span>
164
+ {messages.summary.min}: {number(summary.min ?? 0)}
165
+ </span>
166
+ <span>
167
+ {messages.summary.max}: {number(summary.max ?? 0)}
168
+ </span>
169
+ <span>
170
+ {messages.summary.sum}: {number(summary.sum)}
171
+ </span>
172
+ </>
173
+ )}
174
+ </span>
175
+ )}
176
+ {!summary && controller.summaryPending && <span className="vg-statusbar__muted">…</span>}
177
+ </div>
178
+ );
179
+ }
@@ -0,0 +1,70 @@
1
+ import { useEffect, useState } from "react";
2
+ import { type CellValue, type ColumnSchema, isAbortError, plainText } from "vastgrid-core";
3
+ import type { Messages } from "../messages.js";
4
+
5
+ /** Pretty-prints JSON text; returns null when the text is not a JSON object or array. */
6
+ function prettyJson(text: string) {
7
+ const trimmed = text.trim();
8
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return null;
9
+ try {
10
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ /** The full value of a cell, with line breaks, JSON formatting and a copy button. */
17
+ export function CellInspector({
18
+ schema,
19
+ messages,
20
+ location,
21
+ load,
22
+ onCopy,
23
+ }: {
24
+ schema: ColumnSchema;
25
+ messages: Messages;
26
+ location: string;
27
+ load: (signal: AbortSignal) => Promise<CellValue>;
28
+ onCopy: (text: string) => void;
29
+ }) {
30
+ const [value, setValue] = useState<{ text: string; isNull: boolean } | null>(null);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ useEffect(() => {
34
+ const controller = new AbortController();
35
+ load(controller.signal)
36
+ .then((result) => {
37
+ if (!controller.signal.aborted) setValue({ text: plainText(result), isNull: result === null });
38
+ })
39
+ .catch((cause: unknown) => {
40
+ if (controller.signal.aborted || isAbortError(cause)) return;
41
+ setError(cause instanceof Error ? cause.message : String(cause));
42
+ });
43
+ return () => controller.abort();
44
+ }, [load]);
45
+
46
+ const json = value ? prettyJson(value.text) : null;
47
+ return (
48
+ <div className="vg-inspector">
49
+ <p className="vg-panel__title">
50
+ {schema.name}
51
+ <span className="vg-panel__muted">
52
+ {" "}
53
+ · {location} · {messages.types[schema.type] ?? schema.type}
54
+ </span>
55
+ </p>
56
+ {error && <p className="vg-panel__error">{error}</p>}
57
+ {!value && !error && <p className="vg-panel__muted">{messages.loading}</p>}
58
+ {value && (
59
+ <pre className="vg-inspector__value" data-null={value.isNull || undefined}>
60
+ {value.isNull ? "NULL" : (json ?? value.text)}
61
+ </pre>
62
+ )}
63
+ <div className="vg-panel__actions">
64
+ <button type="button" className="vg-button" disabled={!value} onClick={() => value && onCopy(value.text)}>
65
+ {messages.copy}
66
+ </button>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,59 @@
1
+ import { useState } from "react";
2
+ import type { ColumnSchema } from "vastgrid-core";
3
+ import type { ColumnState } from "../controller.js";
4
+ import type { Messages } from "../messages.js";
5
+
6
+ /** Shows and hides columns. */
7
+ export function ColumnsPanel({
8
+ columns,
9
+ schema,
10
+ messages,
11
+ onToggle,
12
+ onShowAll,
13
+ }: {
14
+ columns: ColumnState[];
15
+ schema: ColumnSchema[];
16
+ messages: Messages;
17
+ onToggle: (index: number, hidden: boolean) => void;
18
+ onShowAll: () => void;
19
+ }) {
20
+ const [search, setSearch] = useState("");
21
+ const needle = search.trim().toLowerCase();
22
+ const visibleCount = columns.filter((column) => !column.hidden).length;
23
+ const shown = columns.filter(
24
+ (column) => !needle || (schema[column.index]?.name ?? "").toLowerCase().includes(needle),
25
+ );
26
+ return (
27
+ <div className="vg-columns">
28
+ <p className="vg-panel__title">{messages.columns}</p>
29
+ <input
30
+ type="search"
31
+ className="vg-input"
32
+ placeholder={messages.search}
33
+ aria-label={messages.search}
34
+ value={search}
35
+ data-autofocus
36
+ onChange={(event) => setSearch(event.target.value)}
37
+ />
38
+ <div className="vg-filter__values">
39
+ {shown.map((column) => (
40
+ <label className="vg-check" key={column.index}>
41
+ <input
42
+ type="checkbox"
43
+ checked={!column.hidden}
44
+ disabled={!column.hidden && visibleCount <= 1}
45
+ onChange={(event) => onToggle(column.index, !event.target.checked)}
46
+ />
47
+ <span className="vg-check__label">{schema[column.index]?.name}</span>
48
+ <span className="vg-check__count">{messages.types[schema[column.index]?.type ?? "unknown"]}</span>
49
+ </label>
50
+ ))}
51
+ </div>
52
+ <div className="vg-panel__actions">
53
+ <button type="button" className="vg-button" onClick={onShowAll} disabled={visibleCount === columns.length}>
54
+ {messages.showAllColumns}
55
+ </button>
56
+ </div>
57
+ </div>
58
+ );
59
+ }