vastgrid 0.1.0 → 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.
- package/README.md +175 -13
- package/dist/canvas/geometry.d.ts +92 -0
- package/dist/canvas/geometry.d.ts.map +1 -0
- package/dist/canvas/geometry.js +197 -0
- package/dist/canvas/geometry.js.map +1 -0
- package/dist/canvas/grid-canvas.d.ts +161 -0
- package/dist/canvas/grid-canvas.d.ts.map +1 -0
- package/dist/canvas/grid-canvas.js +1146 -0
- package/dist/canvas/grid-canvas.js.map +1 -0
- package/dist/canvas/hooks.d.ts +61 -0
- package/dist/canvas/hooks.d.ts.map +1 -0
- package/dist/canvas/hooks.js +2 -0
- package/dist/canvas/hooks.js.map +1 -0
- package/dist/canvas/keys.d.ts +26 -0
- package/dist/canvas/keys.d.ts.map +1 -0
- package/dist/canvas/keys.js +133 -0
- package/dist/canvas/keys.js.map +1 -0
- package/dist/canvas/layout.d.ts +26 -0
- package/dist/canvas/layout.d.ts.map +1 -0
- package/dist/canvas/layout.js +73 -0
- package/dist/canvas/layout.js.map +1 -0
- package/dist/canvas/scroll.d.ts +31 -0
- package/dist/canvas/scroll.d.ts.map +1 -0
- package/dist/canvas/scroll.js +111 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/text.d.ts +23 -0
- package/dist/canvas/text.d.ts.map +1 -0
- package/dist/canvas/text.js +142 -0
- package/dist/canvas/text.js.map +1 -0
- package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
- package/dist/canvas/theme.d.ts.map +1 -0
- package/dist/canvas/theme.js +62 -0
- package/dist/canvas/theme.js.map +1 -0
- package/dist/core/cache.d.ts +48 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +222 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checked.d.ts +20 -0
- package/dist/core/checked.d.ts.map +1 -0
- package/dist/core/checked.js +61 -0
- package/dist/core/checked.js.map +1 -0
- package/dist/core/client.d.ts +33 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +103 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/conditions.d.ts +48 -0
- package/dist/core/conditions.d.ts.map +1 -0
- package/dist/core/conditions.js +105 -0
- package/dist/core/conditions.js.map +1 -0
- package/dist/core/evaluate.d.ts +20 -0
- package/dist/core/evaluate.d.ts.map +1 -0
- package/dist/core/evaluate.js +210 -0
- package/dist/core/evaluate.js.map +1 -0
- package/dist/core/filters.d.ts +51 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/filters.js +303 -0
- package/dist/core/filters.js.map +1 -0
- package/dist/core/format.d.ts +14 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +303 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/measures.d.ts +26 -0
- package/dist/core/measures.d.ts.map +1 -0
- package/dist/core/measures.js +105 -0
- package/dist/core/measures.js.map +1 -0
- package/dist/core/request-ids.d.ts +20 -0
- package/dist/core/request-ids.d.ts.map +1 -0
- package/dist/core/request-ids.js +55 -0
- package/dist/core/request-ids.js.map +1 -0
- package/dist/core/rows.d.ts +79 -0
- package/dist/core/rows.d.ts.map +1 -0
- package/dist/core/rows.js +173 -0
- package/dist/core/rows.js.map +1 -0
- package/dist/core/store.d.ts +233 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +1162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/totals.d.ts +24 -0
- package/dist/core/totals.d.ts.map +1 -0
- package/dist/core/totals.js +49 -0
- package/dist/core/totals.js.map +1 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transports.d.ts +50 -0
- package/dist/core/transports.d.ts.map +1 -0
- package/dist/core/transports.js +160 -0
- package/dist/core/transports.js.map +1 -0
- package/dist/core/types.d.ts +150 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +45 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +11 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/react/accessible-rows.d.ts +13 -0
- package/dist/react/accessible-rows.d.ts.map +1 -0
- package/dist/react/accessible-rows.js +320 -0
- package/dist/react/accessible-rows.js.map +1 -0
- package/dist/react/cell-editor.d.ts +22 -0
- package/dist/react/cell-editor.d.ts.map +1 -0
- package/dist/react/cell-editor.js +431 -0
- package/dist/react/cell-editor.js.map +1 -0
- package/dist/react/cell-menu.d.ts +18 -0
- package/dist/react/cell-menu.d.ts.map +1 -0
- package/dist/react/cell-menu.js +299 -0
- package/dist/react/cell-menu.js.map +1 -0
- package/dist/react/column-menu.d.ts +16 -0
- package/dist/react/column-menu.d.ts.map +1 -0
- package/dist/react/column-menu.js +1311 -0
- package/dist/react/column-menu.js.map +1 -0
- package/dist/react/columns-menu.d.ts +7 -0
- package/dist/react/columns-menu.d.ts.map +1 -0
- package/dist/react/columns-menu.js +249 -0
- package/dist/react/columns-menu.js.map +1 -0
- package/dist/react/conditions-menu.d.ts +10 -0
- package/dist/react/conditions-menu.d.ts.map +1 -0
- package/dist/react/conditions-menu.js +795 -0
- package/dist/react/conditions-menu.js.map +1 -0
- package/dist/react/context.d.ts +29 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +18 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/data-grid.d.ts +79 -0
- package/dist/react/data-grid.d.ts.map +1 -0
- package/dist/react/data-grid.js +653 -0
- package/dist/react/data-grid.js.map +1 -0
- package/dist/react/detail-panel.d.ts +22 -0
- package/dist/react/detail-panel.d.ts.map +1 -0
- package/dist/react/detail-panel.js +242 -0
- package/dist/react/detail-panel.js.map +1 -0
- package/dist/react/export-menu.d.ts +11 -0
- package/dist/react/export-menu.d.ts.map +1 -0
- package/dist/react/export-menu.js +304 -0
- package/dist/react/export-menu.js.map +1 -0
- package/dist/react/find-bar.d.ts +11 -0
- package/dist/react/find-bar.d.ts.map +1 -0
- package/dist/react/find-bar.js +303 -0
- package/dist/react/find-bar.js.map +1 -0
- package/dist/react/format-form.d.ts +12 -0
- package/dist/react/format-form.d.ts.map +1 -0
- package/dist/react/format-form.js +863 -0
- package/dist/react/format-form.js.map +1 -0
- package/dist/react/formula-menu.d.ts +6 -0
- package/dist/react/formula-menu.d.ts.map +1 -0
- package/dist/react/formula-menu.js +343 -0
- package/dist/react/formula-menu.js.map +1 -0
- package/dist/react/grid-body.d.ts +33 -0
- package/dist/react/grid-body.d.ts.map +1 -0
- package/dist/react/grid-body.js +1389 -0
- package/dist/react/grid-body.js.map +1 -0
- package/dist/react/group-menu.d.ts +7 -0
- package/dist/react/group-menu.d.ts.map +1 -0
- package/dist/react/group-menu.js +508 -0
- package/dist/react/group-menu.js.map +1 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +161 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/messages.d.ts +180 -0
- package/dist/react/messages.d.ts.map +1 -0
- package/dist/react/messages.js +414 -0
- package/dist/react/messages.js.map +1 -0
- package/dist/react/save.d.ts +9 -0
- package/dist/react/save.d.ts.map +1 -0
- package/dist/react/save.js +102 -0
- package/dist/react/save.js.map +1 -0
- package/dist/react/static-rows.d.ts +9 -0
- package/dist/react/static-rows.d.ts.map +1 -0
- package/dist/react/static-rows.js +176 -0
- package/dist/react/static-rows.js.map +1 -0
- package/dist/react/status-bar.d.ts +12 -0
- package/dist/react/status-bar.d.ts.map +1 -0
- package/dist/react/status-bar.js +413 -0
- package/dist/react/status-bar.js.map +1 -0
- package/dist/react/toolbar-popover.d.ts +14 -0
- package/dist/react/toolbar-popover.d.ts.map +1 -0
- package/dist/react/toolbar-popover.js +143 -0
- package/dist/react/toolbar-popover.js.map +1 -0
- package/dist/react/toolbar.d.ts +13 -0
- package/dist/react/toolbar.d.ts.map +1 -0
- package/dist/react/toolbar.js +344 -0
- package/dist/react/toolbar.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +496 -316
- package/package.json +43 -18
- package/src/canvas/geometry.ts +197 -0
- package/src/canvas/grid-canvas.ts +1222 -0
- package/src/canvas/hooks.ts +61 -0
- package/src/canvas/keys.ts +96 -0
- package/src/canvas/layout.ts +77 -0
- package/src/canvas/scroll.ts +120 -0
- package/src/canvas/text.ts +155 -0
- package/src/{theme.ts → canvas/theme.ts} +14 -1
- package/src/core/cache.ts +247 -0
- package/src/core/checked.ts +64 -0
- package/src/core/client.ts +103 -0
- package/src/core/conditions.ts +89 -0
- package/src/core/evaluate.ts +221 -0
- package/src/core/filters.ts +268 -0
- package/src/core/format.ts +309 -0
- package/src/core/measures.ts +99 -0
- package/src/core/request-ids.ts +54 -0
- package/src/core/rows.ts +213 -0
- package/src/core/store.ts +1122 -0
- package/src/core/totals.ts +59 -0
- package/src/core/trace.ts +44 -0
- package/src/core/transports.ts +177 -0
- package/src/core/types.ts +168 -0
- package/src/core.ts +39 -0
- package/src/index.ts +24 -12
- package/src/react/accessible-rows.tsx +242 -0
- package/src/react/cell-editor.tsx +216 -0
- package/src/react/cell-menu.tsx +166 -0
- package/src/react/column-menu.tsx +619 -0
- package/src/react/columns-menu.tsx +116 -0
- package/src/react/conditions-menu.tsx +289 -0
- package/src/react/context.ts +33 -0
- package/src/react/data-grid.tsx +286 -0
- package/src/react/detail-panel.tsx +92 -0
- package/src/react/export-menu.tsx +112 -0
- package/src/react/find-bar.tsx +112 -0
- package/src/react/format-form.tsx +421 -0
- package/src/react/formula-menu.tsx +163 -0
- package/src/react/grid-body.tsx +744 -0
- package/src/react/group-menu.tsx +214 -0
- package/src/react/hooks.ts +104 -0
- package/src/react/messages.ts +556 -0
- package/src/react/save.ts +42 -0
- package/src/react/static-rows.tsx +59 -0
- package/src/react/status-bar.tsx +175 -0
- package/src/react/toolbar-popover.tsx +69 -0
- package/src/react/toolbar.tsx +152 -0
- package/src/server.ts +58 -0
- package/src/styles.css +496 -316
- package/dist/components/bars.d.ts +0 -14
- package/dist/components/bars.d.ts.map +0 -1
- package/dist/components/bars.js +0 -67
- package/dist/components/bars.js.map +0 -1
- package/dist/components/cell-inspector.d.ts +0 -11
- package/dist/components/cell-inspector.d.ts.map +0 -1
- package/dist/components/cell-inspector.js +0 -37
- package/dist/components/cell-inspector.js.map +0 -1
- package/dist/components/columns-panel.d.ts +0 -12
- package/dist/components/columns-panel.d.ts.map +0 -1
- package/dist/components/columns-panel.js +0 -11
- package/dist/components/columns-panel.js.map +0 -1
- package/dist/components/filter-panel.d.ts +0 -17
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -165
- package/dist/components/filter-panel.js.map +0 -1
- package/dist/components/find-bar.d.ts +0 -9
- package/dist/components/find-bar.d.ts.map +0 -1
- package/dist/components/find-bar.js +0 -46
- package/dist/components/find-bar.js.map +0 -1
- package/dist/components/popover.d.ts +0 -38
- package/dist/components/popover.d.ts.map +0 -1
- package/dist/components/popover.js +0 -104
- package/dist/components/popover.js.map +0 -1
- package/dist/components/stats-panel.d.ts +0 -12
- package/dist/components/stats-panel.d.ts.map +0 -1
- package/dist/components/stats-panel.js +0 -40
- package/dist/components/stats-panel.js.map +0 -1
- package/dist/controller.d.ts +0 -218
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -788
- package/dist/controller.js.map +0 -1
- package/dist/data-grid.d.ts +0 -51
- package/dist/data-grid.d.ts.map +0 -1
- package/dist/data-grid.js +0 -698
- package/dist/data-grid.js.map +0 -1
- package/dist/labels.d.ts +0 -9
- package/dist/labels.d.ts.map +0 -1
- package/dist/labels.js +0 -38
- package/dist/labels.js.map +0 -1
- package/dist/messages.d.ts +0 -99
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js +0 -276
- package/dist/messages.js.map +0 -1
- package/dist/renderer.d.ts +0 -52
- package/dist/renderer.d.ts.map +0 -1
- package/dist/renderer.js +0 -522
- package/dist/renderer.js.map +0 -1
- package/dist/text.d.ts +0 -14
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js +0 -63
- package/dist/text.js.map +0 -1
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -54
- package/dist/theme.js.map +0 -1
- package/src/components/bars.tsx +0 -179
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -290
- package/src/components/find-bar.tsx +0 -104
- package/src/components/popover.tsx +0 -194
- package/src/components/stats-panel.tsx +0 -153
- package/src/controller.ts +0 -887
- package/src/data-grid.tsx +0 -1029
- package/src/labels.ts +0 -46
- package/src/messages.ts +0 -390
- package/src/renderer.ts +0 -535
- package/src/text.ts +0 -55
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host adds to the drawing of the grid through props (§5.1 of the specification: functions are not state, and
|
|
3
|
+
* they win over the document's formats and styles), and the column groups of the header.
|
|
4
|
+
*/
|
|
5
|
+
import type { Column, JsonValue } from "vastgrid-spec";
|
|
6
|
+
import type { GridTheme } from "./theme.js";
|
|
7
|
+
|
|
8
|
+
/** A cell of the view that the grid shows. */
|
|
9
|
+
export interface GridCell {
|
|
10
|
+
/** The view position of its row. */
|
|
11
|
+
row: number;
|
|
12
|
+
column: Column;
|
|
13
|
+
value: JsonValue;
|
|
14
|
+
/** The text the grid shows: by the column's format, or the text form (§2.4). */
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The colors of a cell, and what they mean for screen readers. */
|
|
19
|
+
export interface CellStyle {
|
|
20
|
+
color?: string;
|
|
21
|
+
background?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What a cell does when it is clicked or Space is pressed on it: a link goes somewhere, a button acts in place. */
|
|
26
|
+
export interface CellAction {
|
|
27
|
+
kind: "link" | "button";
|
|
28
|
+
/** The name for screen readers, instead of the cell's text. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** The CSS cursor over the cell; `pointer` by default. */
|
|
31
|
+
cursor?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The cell, its box and the canvas, for `drawCell`. */
|
|
35
|
+
export interface DrawCellArgs extends GridCell {
|
|
36
|
+
/** The canvas in CSS pixels, with the cell font, `textBaseline = "middle"` and the column's alignment. */
|
|
37
|
+
ctx: CanvasRenderingContext2D;
|
|
38
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
39
|
+
style: CellStyle | null;
|
|
40
|
+
theme: GridTheme;
|
|
41
|
+
/** Draws the cell as the grid does. */
|
|
42
|
+
drawDefault(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CellHooks {
|
|
46
|
+
/** The text of a cell instead of the grid's; null or undefined keeps the grid's. */
|
|
47
|
+
cellText?: (cell: GridCell) => string | null | undefined;
|
|
48
|
+
/** The colors of a cell instead of the column's style rules; null or undefined keeps them. */
|
|
49
|
+
cellStyle?: (cell: GridCell) => CellStyle | null | undefined;
|
|
50
|
+
/** Draws a cell on the canvas; the canvas's state is restored afterwards. */
|
|
51
|
+
drawCell?: (args: DrawCellArgs) => void;
|
|
52
|
+
/** Makes cells links or buttons; null or undefined for plain cells. */
|
|
53
|
+
cellAction?: (cell: GridCell) => CellAction | null | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A group of columns named in a header row above the column names; nested groups add rows. */
|
|
57
|
+
export interface ColumnGroup {
|
|
58
|
+
name: string;
|
|
59
|
+
/** Column names of the view, and groups. */
|
|
60
|
+
children: (string | ColumnGroup)[];
|
|
61
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { GridCommand } from "vastgrid-spec";
|
|
2
|
+
|
|
3
|
+
/** The members of a keyboard event that the grid reads. */
|
|
4
|
+
export interface KeyInput {
|
|
5
|
+
key: string;
|
|
6
|
+
shiftKey: boolean;
|
|
7
|
+
altKey: boolean;
|
|
8
|
+
ctrlKey: boolean;
|
|
9
|
+
metaKey: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Whether the platform's primary modifier is ⌘ (Apple platforms) rather than Ctrl. */
|
|
13
|
+
export function isApple(): boolean {
|
|
14
|
+
if (typeof navigator === "undefined") return false;
|
|
15
|
+
const platform = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform;
|
|
16
|
+
return /mac|iphone|ipad|ipod/i.test(platform ?? navigator.platform ?? "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ARROWS: Record<string, "up" | "down" | "left" | "right"> = {
|
|
20
|
+
ArrowUp: "up",
|
|
21
|
+
ArrowDown: "down",
|
|
22
|
+
ArrowLeft: "left",
|
|
23
|
+
ArrowRight: "right",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The command of a key on the grid (§6.4 of the specification, the grid pattern of the ARIA Authoring
|
|
28
|
+
* Practices): arrows move, with Shift extend and with the primary modifier jump to the end; Page Up and Page
|
|
29
|
+
* Down move by a page; Home and End go to the first and last column (with the modifier, also row); Tab and
|
|
30
|
+
* Enter walk through the selection; the modifier and A selects everything. Delete and Backspace clear the selected
|
|
31
|
+
* cells, F2 edits the active cell, the modifier and Z undoes (with Shift, or Ctrl+Y off Apple platforms,
|
|
32
|
+
* redoes), the modifier and D or R fills down or right, and the modifier and S saves. Null for other keys: a key that types text starts editing (`typesText`).
|
|
33
|
+
*/
|
|
34
|
+
export function keyCommand(event: KeyInput, apple: boolean): GridCommand | null {
|
|
35
|
+
const primary = apple ? event.metaKey : event.ctrlKey;
|
|
36
|
+
if (event.altKey) return null;
|
|
37
|
+
const flags = (extend: boolean, jump: boolean) => ({
|
|
38
|
+
...(extend ? { extend: true } : {}),
|
|
39
|
+
...(jump ? { jump: true } : {}),
|
|
40
|
+
});
|
|
41
|
+
const direction = ARROWS[event.key];
|
|
42
|
+
if (direction) return { command: "move", direction, ...flags(event.shiftKey, primary) };
|
|
43
|
+
switch (event.key) {
|
|
44
|
+
case "PageUp":
|
|
45
|
+
case "PageDown":
|
|
46
|
+
return {
|
|
47
|
+
command: "move",
|
|
48
|
+
direction: event.key === "PageUp" ? "up" : "down",
|
|
49
|
+
page: true,
|
|
50
|
+
...flags(event.shiftKey, false),
|
|
51
|
+
};
|
|
52
|
+
case "Home":
|
|
53
|
+
return { command: "move.home", ...flags(event.shiftKey, primary) };
|
|
54
|
+
case "End":
|
|
55
|
+
return { command: "move.end", ...flags(event.shiftKey, primary) };
|
|
56
|
+
case "Tab":
|
|
57
|
+
return primary ? null : { command: "advance", axis: "column", ...(event.shiftKey ? { back: true } : {}) };
|
|
58
|
+
case "Enter":
|
|
59
|
+
return primary ? null : { command: "advance", axis: "row", ...(event.shiftKey ? { back: true } : {}) };
|
|
60
|
+
case "a":
|
|
61
|
+
case "A":
|
|
62
|
+
return primary && !event.shiftKey ? { command: "select.all" } : null;
|
|
63
|
+
case "Delete":
|
|
64
|
+
case "Backspace":
|
|
65
|
+
return primary ? null : { command: "edit.clear" };
|
|
66
|
+
case "F2":
|
|
67
|
+
return { command: "edit.begin" };
|
|
68
|
+
case "z":
|
|
69
|
+
case "Z":
|
|
70
|
+
if (!primary) return null;
|
|
71
|
+
return event.shiftKey ? { command: "history.redo" } : { command: "history.undo" };
|
|
72
|
+
case "y":
|
|
73
|
+
case "Y":
|
|
74
|
+
return primary && !apple ? { command: "history.redo" } : null;
|
|
75
|
+
case "s":
|
|
76
|
+
case "S":
|
|
77
|
+
return primary && !event.shiftKey ? { command: "save" } : null;
|
|
78
|
+
// Fill down and right from the first row or column of the selection (§6.5).
|
|
79
|
+
case "d":
|
|
80
|
+
case "D":
|
|
81
|
+
return primary && !event.shiftKey ? { command: "edit.fill", direction: "down" } : null;
|
|
82
|
+
case "r":
|
|
83
|
+
case "R":
|
|
84
|
+
return primary && !event.shiftKey ? { command: "edit.fill", direction: "right" } : null;
|
|
85
|
+
default:
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether a key types text, a character or the start of an IME composition, which starts editing the cell. */
|
|
91
|
+
export function typesText(event: KeyInput & { keyCode?: number; isComposing?: boolean }): boolean {
|
|
92
|
+
if (event.key === "Process" || event.keyCode === 229 || event.isComposing === true) return true;
|
|
93
|
+
// AltGr gives Ctrl+Alt on Windows; Option gives characters on Apple platforms.
|
|
94
|
+
const shortcut = event.metaKey || (event.ctrlKey && !event.altKey);
|
|
95
|
+
return [...event.key].length === 1 && event.key !== " " && !shortcut;
|
|
96
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Positions of columns laid out side by side. Lookups are O(1) by index and O(log n) by x. */
|
|
2
|
+
export class ColumnLayout {
|
|
3
|
+
/** `offsets[i]` is the left edge of column `i`; `offsets[count]` is the total width. */
|
|
4
|
+
readonly offsets: Float64Array;
|
|
5
|
+
|
|
6
|
+
constructor(widths: ArrayLike<number>) {
|
|
7
|
+
this.offsets = new Float64Array(widths.length + 1);
|
|
8
|
+
for (let index = 0; index < widths.length; index++) {
|
|
9
|
+
this.offsets[index + 1] = this.offsets[index] + Math.max(0, widths[index]);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get count() {
|
|
14
|
+
return this.offsets.length - 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get total() {
|
|
18
|
+
return this.offsets[this.count];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
left(index: number) {
|
|
22
|
+
return this.offsets[index];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
right(index: number) {
|
|
26
|
+
return this.offsets[index + 1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
width(index: number) {
|
|
30
|
+
return this.offsets[index + 1] - this.offsets[index];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The column containing `x`, clamped to the existing columns. Returns -1 when there are none. */
|
|
34
|
+
indexAt(x: number): number {
|
|
35
|
+
const count = this.count;
|
|
36
|
+
if (count === 0) return -1;
|
|
37
|
+
if (x <= 0) return 0;
|
|
38
|
+
if (x >= this.total) return count - 1;
|
|
39
|
+
let low = 0;
|
|
40
|
+
let high = count - 1;
|
|
41
|
+
while (low < high) {
|
|
42
|
+
const middle = (low + high + 1) >> 1;
|
|
43
|
+
if (this.offsets[middle] <= x) low = middle;
|
|
44
|
+
else high = middle - 1;
|
|
45
|
+
}
|
|
46
|
+
return low;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Where a column is frozen: at the left, at the right, or not (`null`, it scrolls). */
|
|
51
|
+
export type ColumnPin = "left" | "right" | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Share of the body's width (without the row header) that frozen columns may take together. Columns
|
|
55
|
+
* frozen beyond it scroll with the others until the grid is wide enough again, so that the scrolling
|
|
56
|
+
* columns always keep some room.
|
|
57
|
+
*/
|
|
58
|
+
const MAX_FROZEN_SHARE = 0.75;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The columns drawn frozen at each side: the first `left` and the last `right` columns of `layout`, fewer
|
|
62
|
+
* when together they are wider than `MAX_FROZEN_SHARE` of `available` (the body's width). Columns frozen
|
|
63
|
+
* at the right give way first, then those at the left, each from the scrolling side. With an unknown
|
|
64
|
+
* width (`available` 0 or less) every frozen column stays frozen.
|
|
65
|
+
*/
|
|
66
|
+
export function fitFrozen(layout: ColumnLayout, left: number, right: number, available: number) {
|
|
67
|
+
const count = layout.count;
|
|
68
|
+
let fittedLeft = Math.max(0, Math.min(Math.floor(left), count));
|
|
69
|
+
let fittedRight = Math.max(0, Math.min(Math.floor(right), count - fittedLeft));
|
|
70
|
+
if (available > 0) {
|
|
71
|
+
const limit = available * MAX_FROZEN_SHARE;
|
|
72
|
+
const width = () => layout.left(fittedLeft) + layout.total - layout.left(count - fittedRight);
|
|
73
|
+
while (fittedRight > 0 && width() > limit) fittedRight--;
|
|
74
|
+
while (fittedLeft > 0 && width() > limit) fittedLeft--;
|
|
75
|
+
}
|
|
76
|
+
return { left: fittedLeft, right: fittedRight };
|
|
77
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browsers cap the size of an element (about 16.7M px in Chromium, 17.9M px in Firefox, 33.5M px
|
|
3
|
+
* in Safari). With 24 px rows that is well under a million rows, so a native scrollbar cannot
|
|
4
|
+
* cover the content of a large table. `ScrollAxis` maps a native scroll position, kept below
|
|
5
|
+
* `maxNative`, onto a much larger virtual position:
|
|
6
|
+
*
|
|
7
|
+
* - Small movements (wheel, trackpad, arrow keys) move the virtual position by the same amount, so
|
|
8
|
+
* scrolling feels exactly like a normal page at any depth.
|
|
9
|
+
* - Large jumps (dragging the thumb, clicking the track) and the two ends map proportionally, so the
|
|
10
|
+
* scrollbar still tells where you are and reaches the first and the last row.
|
|
11
|
+
*
|
|
12
|
+
* Content smaller than `maxNative` is never scaled.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_NATIVE_SCROLL = 8_000_000;
|
|
15
|
+
|
|
16
|
+
function clamp(value: number, min: number, max: number) {
|
|
17
|
+
return Math.min(Math.max(value, min), max);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ScrollAxis {
|
|
21
|
+
private contentSize = 0;
|
|
22
|
+
private viewportSize = 0;
|
|
23
|
+
private nativeSize = 0;
|
|
24
|
+
private virtual = 0;
|
|
25
|
+
private lastNative = 0;
|
|
26
|
+
private expectedNative: number | null = null;
|
|
27
|
+
private expectedVirtual = 0;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly maxNative = MAX_NATIVE_SCROLL) {}
|
|
30
|
+
|
|
31
|
+
/** Updates the sizes and returns the size to give the native scroll content. */
|
|
32
|
+
resize(contentSize: number, viewportSize: number): number {
|
|
33
|
+
this.contentSize = Math.max(0, contentSize);
|
|
34
|
+
this.viewportSize = Math.max(0, viewportSize);
|
|
35
|
+
this.nativeSize = Math.min(this.contentSize, this.maxNative);
|
|
36
|
+
this.virtual = clamp(this.virtual, 0, this.virtualRange);
|
|
37
|
+
return this.nativeSize;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whether the native position is mapped (the content is larger than `maxNative`). */
|
|
41
|
+
get scaled() {
|
|
42
|
+
return this.contentSize > this.maxNative;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get virtualRange() {
|
|
46
|
+
return Math.max(0, this.contentSize - this.viewportSize);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get nativeRange() {
|
|
50
|
+
return Math.max(0, this.nativeSize - this.viewportSize);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The current virtual position. */
|
|
54
|
+
get position() {
|
|
55
|
+
return this.virtual;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The native position that corresponds to the current state. */
|
|
59
|
+
get native() {
|
|
60
|
+
return this.lastNative;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private jumpThreshold() {
|
|
64
|
+
return Math.max(this.viewportSize * 2, 1000);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Handles a native scroll event and returns the new virtual position. */
|
|
68
|
+
onNativeScroll(nativePosition: number): number {
|
|
69
|
+
const nativeRange = this.nativeRange;
|
|
70
|
+
const native = clamp(nativePosition, 0, nativeRange);
|
|
71
|
+
if (this.expectedNative !== null && Math.abs(native - this.expectedNative) <= 1) {
|
|
72
|
+
// The event caused by `scrollTo`. Browsers may round the position, so keep the requested one.
|
|
73
|
+
this.expectedNative = null;
|
|
74
|
+
this.lastNative = native;
|
|
75
|
+
this.virtual = this.expectedVirtual;
|
|
76
|
+
return this.virtual;
|
|
77
|
+
}
|
|
78
|
+
this.expectedNative = null;
|
|
79
|
+
if (!this.scaled) {
|
|
80
|
+
this.lastNative = native;
|
|
81
|
+
this.virtual = native;
|
|
82
|
+
return native;
|
|
83
|
+
}
|
|
84
|
+
const delta = native - this.lastNative;
|
|
85
|
+
this.lastNative = native;
|
|
86
|
+
const virtualRange = this.virtualRange;
|
|
87
|
+
if (native <= 0) this.virtual = 0;
|
|
88
|
+
else if (native >= nativeRange) this.virtual = virtualRange;
|
|
89
|
+
else if (Math.abs(delta) > this.jumpThreshold()) this.virtual = (native / nativeRange) * virtualRange;
|
|
90
|
+
else this.virtual = clamp(this.virtual + delta, 0, virtualRange);
|
|
91
|
+
return this.virtual;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Moves to a virtual position and returns the native position to assign to the scroll element.
|
|
96
|
+
* Nearby targets move the native position by the same amount, so a following wheel scroll
|
|
97
|
+
* continues smoothly; distant targets map proportionally.
|
|
98
|
+
*/
|
|
99
|
+
scrollTo(target: number): number {
|
|
100
|
+
const virtualRange = this.virtualRange;
|
|
101
|
+
const virtual = clamp(target, 0, virtualRange);
|
|
102
|
+
let native: number;
|
|
103
|
+
if (!this.scaled) {
|
|
104
|
+
native = virtual;
|
|
105
|
+
} else {
|
|
106
|
+
const nativeRange = this.nativeRange;
|
|
107
|
+
const delta = virtual - this.virtual;
|
|
108
|
+
const local = this.lastNative + delta;
|
|
109
|
+
if (virtual === 0) native = 0;
|
|
110
|
+
else if (virtual === virtualRange) native = nativeRange;
|
|
111
|
+
else if (Math.abs(delta) <= this.jumpThreshold() && local > 0 && local < nativeRange) native = local;
|
|
112
|
+
else native = virtualRange === 0 ? 0 : (virtual / virtualRange) * nativeRange;
|
|
113
|
+
}
|
|
114
|
+
this.virtual = virtual;
|
|
115
|
+
this.expectedVirtual = virtual;
|
|
116
|
+
this.expectedNative = native;
|
|
117
|
+
this.lastNative = native;
|
|
118
|
+
return native;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const ELLIPSIS = "…";
|
|
2
|
+
const MAX_ENTRIES = 20_000;
|
|
3
|
+
/** Characters that do not start a line (closing brackets, Japanese punctuation and small kana). */
|
|
4
|
+
const NO_START = new Set(
|
|
5
|
+
")]}>,.!?:;%、。,.・:;?!‼⁇⁈⁉ー〜~)」』】〕〉》}〙〗〟’”ゝゞヽヾ々〻ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ",
|
|
6
|
+
);
|
|
7
|
+
/** Characters that do not end a line (opening brackets). */
|
|
8
|
+
const NO_END = new Set("([{<(「『【〔〈《{〘〖〝‘“");
|
|
9
|
+
|
|
10
|
+
/** Whether a UTF-16 code unit belongs to a script written without spaces (CJK, kana, Hangul, full width). */
|
|
11
|
+
function isWide(code: number) {
|
|
12
|
+
return (
|
|
13
|
+
(code >= 0x1100 && code <= 0x11ff) ||
|
|
14
|
+
(code >= 0x2e80 && code <= 0x9fff) ||
|
|
15
|
+
(code >= 0xa960 && code <= 0xa97f) ||
|
|
16
|
+
(code >= 0xac00 && code <= 0xd7ff) ||
|
|
17
|
+
// Surrogates of the CJK extensions (U+20000–U+3FFFF).
|
|
18
|
+
(code >= 0xd840 && code <= 0xd8bf) ||
|
|
19
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
20
|
+
(code >= 0xfe30 && code <= 0xfe4f) ||
|
|
21
|
+
(code >= 0xff00 && code <= 0xffef)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isHighSurrogate(code: number) {
|
|
26
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether a line may break between `text[index - 1]` and `text[index]`. */
|
|
30
|
+
function canBreak(text: string, index: number) {
|
|
31
|
+
const before = text[index - 1];
|
|
32
|
+
const after = text[index];
|
|
33
|
+
if (before === " " || after === " ") return true;
|
|
34
|
+
if (NO_START.has(after) || NO_END.has(before)) return false;
|
|
35
|
+
const code = text.charCodeAt(index - 1);
|
|
36
|
+
if (isHighSurrogate(code)) return false;
|
|
37
|
+
return isWide(code) || isWide(text.charCodeAt(index));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Measures and shortens text for one font, caching results because the same strings repeat a lot. */
|
|
41
|
+
export class TextFitter {
|
|
42
|
+
private readonly widths = new Map<string, number>();
|
|
43
|
+
private readonly fits = new Map<string, string>();
|
|
44
|
+
private readonly wrapped = new Map<string, readonly string[]>();
|
|
45
|
+
private ellipsisWidth = -1;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly context: CanvasRenderingContext2D,
|
|
49
|
+
readonly font: string,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
/** Width of `text`. The caller must have set `context.font` to this fitter's font. */
|
|
53
|
+
width(text: string): number {
|
|
54
|
+
let width = this.widths.get(text);
|
|
55
|
+
if (width === undefined) {
|
|
56
|
+
width = this.context.measureText(text).width;
|
|
57
|
+
if (this.widths.size > MAX_ENTRIES) this.widths.clear();
|
|
58
|
+
this.widths.set(text, width);
|
|
59
|
+
}
|
|
60
|
+
return width;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `text`, shortened with "…" to fit in `maxWidth`. */
|
|
64
|
+
fit(text: string, maxWidth: number): string {
|
|
65
|
+
if (maxWidth <= 0 || text === "") return "";
|
|
66
|
+
// Cells never show more than a few hundred characters; skip measuring the rest.
|
|
67
|
+
const source = text.length > 400 ? text.slice(0, 400) : text;
|
|
68
|
+
if (source === text && this.width(text) <= maxWidth) return text;
|
|
69
|
+
const key = `${Math.floor(maxWidth)}\u0001${source}`;
|
|
70
|
+
const cached = this.fits.get(key);
|
|
71
|
+
if (cached !== undefined) return cached;
|
|
72
|
+
if (this.ellipsisWidth < 0) this.ellipsisWidth = this.context.measureText(ELLIPSIS).width;
|
|
73
|
+
let result = "";
|
|
74
|
+
if (this.ellipsisWidth <= maxWidth) {
|
|
75
|
+
const full = this.context.measureText(source).width;
|
|
76
|
+
let low = 0;
|
|
77
|
+
let high = Math.min(source.length, Math.ceil((source.length * maxWidth) / Math.max(1, full)) + 4);
|
|
78
|
+
while (low < high) {
|
|
79
|
+
const middle = (low + high + 1) >> 1;
|
|
80
|
+
if (this.context.measureText(source.slice(0, middle)).width + this.ellipsisWidth <= maxWidth) low = middle;
|
|
81
|
+
else high = middle - 1;
|
|
82
|
+
}
|
|
83
|
+
// Do not split a surrogate pair.
|
|
84
|
+
const code = source.charCodeAt(low - 1);
|
|
85
|
+
if (low > 0 && code >= 0xd800 && code <= 0xdbff) low--;
|
|
86
|
+
result = `${source.slice(0, low)}${ELLIPSIS}`;
|
|
87
|
+
}
|
|
88
|
+
if (this.fits.size > MAX_ENTRIES) this.fits.clear();
|
|
89
|
+
this.fits.set(key, result);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `text` wrapped onto at most `maxLines` lines of `maxWidth`: lines break at spaces and between CJK
|
|
95
|
+
* characters (not before closing punctuation or after opening brackets), words longer than a line
|
|
96
|
+
* are cut, and "\n" starts a new line. When the text needs more lines, the last one ends with "…".
|
|
97
|
+
*/
|
|
98
|
+
lines(text: string, maxWidth: number, maxLines: number): readonly string[] {
|
|
99
|
+
if (maxWidth <= 0 || maxLines < 1) return [];
|
|
100
|
+
const source = text.replace(/\n+$/, "");
|
|
101
|
+
if (source === "") return [];
|
|
102
|
+
const key = `${Math.floor(maxWidth)}\u0001${maxLines}\u0001${source}`;
|
|
103
|
+
const cached = this.wrapped.get(key);
|
|
104
|
+
if (cached !== undefined) return cached;
|
|
105
|
+
const result: string[] = [];
|
|
106
|
+
const paragraphs = source.split("\n");
|
|
107
|
+
// The text the last line was taken from, and whether more text follows it.
|
|
108
|
+
let last = "";
|
|
109
|
+
let cut = false;
|
|
110
|
+
let more = false;
|
|
111
|
+
for (const paragraph of paragraphs) {
|
|
112
|
+
if (result.length === maxLines) {
|
|
113
|
+
more = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
let rest = paragraph;
|
|
117
|
+
do {
|
|
118
|
+
if (result.length === maxLines) {
|
|
119
|
+
more = cut = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
last = rest;
|
|
123
|
+
const end = this.breakAt(rest, maxWidth);
|
|
124
|
+
result.push(rest.slice(0, end).replace(/ +$/, ""));
|
|
125
|
+
rest = rest.slice(end).replace(/^ +/, "");
|
|
126
|
+
} while (rest !== "");
|
|
127
|
+
if (more) break;
|
|
128
|
+
}
|
|
129
|
+
if (more) {
|
|
130
|
+
const index = result.length - 1;
|
|
131
|
+
result[index] = this.fit(cut ? last : `${result[index]}${ELLIPSIS}`, maxWidth);
|
|
132
|
+
}
|
|
133
|
+
if (this.wrapped.size > MAX_ENTRIES) this.wrapped.clear();
|
|
134
|
+
this.wrapped.set(key, result);
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Length of the first line of `text` (one paragraph) in `maxWidth`, at a place where it may break. */
|
|
139
|
+
private breakAt(text: string, maxWidth: number) {
|
|
140
|
+
if (text === "" || this.width(text) <= maxWidth) return text.length;
|
|
141
|
+
// A character is at least a pixel wide, so a line has at most `maxWidth` characters.
|
|
142
|
+
let low = 0;
|
|
143
|
+
let high = Math.min(text.length, Math.ceil(maxWidth));
|
|
144
|
+
while (low < high) {
|
|
145
|
+
const middle = (low + high + 1) >> 1;
|
|
146
|
+
if (this.context.measureText(text.slice(0, middle)).width <= maxWidth) low = middle;
|
|
147
|
+
else high = middle - 1;
|
|
148
|
+
}
|
|
149
|
+
for (let index = low; index > 0; index--) if (canBreak(text, index)) return index;
|
|
150
|
+
// No place to break: cut the word, keeping surrogate pairs, and place at least one character.
|
|
151
|
+
let end = Math.max(1, low);
|
|
152
|
+
if (end < text.length && isHighSurrogate(text.charCodeAt(end - 1))) end = end > 1 ? end - 1 : end + 1;
|
|
153
|
+
return end;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -5,6 +5,8 @@ export interface GridTheme {
|
|
|
5
5
|
smallFont: string;
|
|
6
6
|
fontSize: number;
|
|
7
7
|
fontFamily: string;
|
|
8
|
+
/** Height of each further line of wrapped cell text, in pixels. */
|
|
9
|
+
lineHeight: number;
|
|
8
10
|
background: string;
|
|
9
11
|
text: string;
|
|
10
12
|
muted: string;
|
|
@@ -20,14 +22,20 @@ export interface GridTheme {
|
|
|
20
22
|
selectionBorder: string;
|
|
21
23
|
activeBorder: string;
|
|
22
24
|
accent: string;
|
|
25
|
+
/** Text and marks drawn on `accent`, such as the check of a checkbox. */
|
|
26
|
+
accentText: string;
|
|
27
|
+
/** Background of checked rows. */
|
|
28
|
+
checkedRow: string;
|
|
23
29
|
nullText: string;
|
|
24
30
|
placeholder: string;
|
|
25
31
|
match: string;
|
|
26
32
|
error: string;
|
|
33
|
+
/** Text of cells whose `cellAction` is a link. */
|
|
34
|
+
link: string;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
const colorTokens: Record<
|
|
30
|
-
keyof Omit<GridTheme, "font" | "headerFont" | "smallFont" | "fontSize" | "fontFamily">,
|
|
38
|
+
keyof Omit<GridTheme, "font" | "headerFont" | "smallFont" | "fontSize" | "fontFamily" | "lineHeight">,
|
|
31
39
|
string
|
|
32
40
|
> = {
|
|
33
41
|
background: "--vg-bg",
|
|
@@ -45,10 +53,13 @@ const colorTokens: Record<
|
|
|
45
53
|
selectionBorder: "--vg-selection-border",
|
|
46
54
|
activeBorder: "--vg-active-border",
|
|
47
55
|
accent: "--vg-accent",
|
|
56
|
+
accentText: "--vg-accent-fg",
|
|
57
|
+
checkedRow: "--vg-checked-row-bg",
|
|
48
58
|
nullText: "--vg-null-fg",
|
|
49
59
|
placeholder: "--vg-placeholder",
|
|
50
60
|
match: "--vg-match-bg",
|
|
51
61
|
error: "--vg-error-fg",
|
|
62
|
+
link: "--vg-link-fg",
|
|
52
63
|
};
|
|
53
64
|
|
|
54
65
|
/** Resolves the theme of `root`. Colors given as `light-dark()` or system colors resolve to concrete values. */
|
|
@@ -65,6 +76,7 @@ export function resolveTheme(root: HTMLElement): GridTheme {
|
|
|
65
76
|
const fontFamily = style.getPropertyValue("--vg-font-family").trim() || style.fontFamily || "system-ui, sans-serif";
|
|
66
77
|
const fontSize =
|
|
67
78
|
Number.parseFloat(style.getPropertyValue("--vg-font-size")) || Number.parseFloat(style.fontSize) || 13;
|
|
79
|
+
const lineHeight = Number.parseFloat(style.getPropertyValue("--vg-line-height")) || Math.round(fontSize * 1.4);
|
|
68
80
|
const colors = Object.fromEntries(Object.entries(colorTokens).map(([key, token]) => [key, color(token)])) as Pick<
|
|
69
81
|
GridTheme,
|
|
70
82
|
keyof typeof colorTokens
|
|
@@ -73,6 +85,7 @@ export function resolveTheme(root: HTMLElement): GridTheme {
|
|
|
73
85
|
...colors,
|
|
74
86
|
fontFamily,
|
|
75
87
|
fontSize,
|
|
88
|
+
lineHeight,
|
|
76
89
|
font: `${fontSize}px ${fontFamily}`,
|
|
77
90
|
headerFont: `600 ${fontSize}px ${fontFamily}`,
|
|
78
91
|
smallFont: `${Math.max(9, fontSize - 2)}px ${fontFamily}`,
|