@sportsdataverse/sdvtables 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.
Files changed (42) hide show
  1. package/README.md +70 -112
  2. package/data/table_themes.json +12136 -0
  3. package/data/table_themes.json.sha256 +1 -0
  4. package/dist/aggrid.d.ts +104 -0
  5. package/dist/aggrid.js +230 -0
  6. package/dist/aggrid.js.map +1 -0
  7. package/dist/chunk-3TJR7TKS.js +223 -0
  8. package/dist/chunk-3TJR7TKS.js.map +1 -0
  9. package/dist/{chunk-UCRLJGDL.js → chunk-R375P3MN.js} +963 -276
  10. package/dist/chunk-R375P3MN.js.map +1 -0
  11. package/dist/chunk-UTOVFIFT.js +15176 -0
  12. package/dist/chunk-UTOVFIFT.js.map +1 -0
  13. package/dist/{chunk-U6W6MWKW.js → chunk-VLF2S6VW.js} +7 -5
  14. package/dist/chunk-VLF2S6VW.js.map +1 -0
  15. package/dist/chunk-WBTFIGYK.js +189 -0
  16. package/dist/chunk-WBTFIGYK.js.map +1 -0
  17. package/dist/{engine-CkfxYXAL.d.ts → engine-BFIPJUlC.d.ts} +1 -1
  18. package/dist/export.d.ts +1 -1
  19. package/dist/export.js +4 -3
  20. package/dist/export.js.map +1 -1
  21. package/dist/html.d.ts +8 -5
  22. package/dist/html.js +4 -3
  23. package/dist/index.d.ts +121 -8
  24. package/dist/index.js +51 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/mui.d.ts +134 -0
  27. package/dist/mui.js +193 -0
  28. package/dist/mui.js.map +1 -0
  29. package/dist/react.d.ts +2 -2
  30. package/dist/react.js +3 -2
  31. package/dist/react.js.map +1 -1
  32. package/dist/spec-A88vJX5A.d.ts +748 -0
  33. package/dist/tanstack-table.d.ts +113 -0
  34. package/dist/tanstack-table.js +98 -0
  35. package/dist/tanstack-table.js.map +1 -0
  36. package/dist/{tokens-CpYFsFtf.d.ts → tokens-CHwBt9gs.d.ts} +1 -1
  37. package/package.json +40 -3
  38. package/dist/chunk-4VKYHOHL.js +0 -2081
  39. package/dist/chunk-4VKYHOHL.js.map +0 -1
  40. package/dist/chunk-U6W6MWKW.js.map +0 -1
  41. package/dist/chunk-UCRLJGDL.js.map +0 -1
  42. package/dist/spec-BEhLDtj_.d.ts +0 -430
@@ -0,0 +1 @@
1
+ 041e0790eca4a937841d19c76af2259bb791a4158ae5199cc4706276f5ea3744
@@ -0,0 +1,104 @@
1
+ import { League, Which, SeasonInput, IdSystem } from '@sportsdataverse/sdvplot';
2
+ import { Theme, ThemeDefaultParams, ColDef, GetRowIdParams, RowClassParams, RowStyle } from 'ag-grid-community';
3
+ import { T as TableSpec } from './spec-A88vJX5A.js';
4
+
5
+ /**
6
+ * The AG Grid adapter (`ag-grid-community` 36, the MIT community package only): an sdvtables spec as the grid options
7
+ * `createGrid(el, options)` (or `<AgGridReact {...options} />`) takes. Columns keep sdvtables' number formats,
8
+ * alignment, px widths and sort; logo, wordmark and headshot columns draw their images through cell renderers that
9
+ * work in plain JavaScript and in ag-grid-react alike; team-coloured rows and cells take sdvplot's team palette with
10
+ * black or white text by contrast, kept through AG Grid's hover and selection; the theme's tokens become an AG Grid
11
+ * theme through its Theming API, from the base theme the caller passes in.
12
+ * Pure: AG Grid is never imported at runtime (types only), and nothing touches the DOM until AG Grid renders a cell, so
13
+ * the module imports on a server.
14
+ */
15
+
16
+ /** What `gridOptions` returns: pass it to `createGrid(el, options)`, or spread it onto `<AgGridReact />`. */
17
+ interface SdvGridOptions<Row> {
18
+ /** One column per spec column. */
19
+ columnDefs: ColDef<Row>[];
20
+ /** The rows, in their order. */
21
+ rowData: Row[];
22
+ /** `String(row[spec.rowKey])`, else the row's index in `rows`; checked unique and not blank when built. */
23
+ getRowId: (params: GetRowIdParams<Row>) => string;
24
+ /** A team row's fill, text colour and hover and selected shades (`teamRows`); other rows keep the theme. */
25
+ getRowStyle: (params: RowClassParams<Row>) => RowStyle | undefined;
26
+ /** The theme's density as a row height in px, tall enough for the tallest image. */
27
+ rowHeight: number;
28
+ /** The theme as AG Grid theme params over the `theme` option's base; absent without that option. */
29
+ theme?: Theme<ThemeDefaultParams>;
30
+ }
31
+ /** `gridOptions` options. */
32
+ interface GridOptionsOptions<Row> {
33
+ /**
34
+ * The AG Grid base theme to lay the sdvtables theme over, usually `themeQuartz` from `ag-grid-community` (or
35
+ * `themeAlpine`, `themeBalham`). Passed in so this module never imports AG Grid. Without it, no `theme` is returned
36
+ * and the grid keeps its own.
37
+ */
38
+ theme?: Theme<ThemeDefaultParams>;
39
+ /**
40
+ * Fill each row with its team's colour, from the row's `key` value (an abbreviation, a name or a numeric id).
41
+ * The colours are sdvplot's team palette for the theme's background (3:1 against it, distinct between teams) laid
42
+ * over it at `alpha` (default 0.4), with black or white text, whichever reads better on the fill. A row whose team
43
+ * is not found keeps the theme.
44
+ */
45
+ teamRows?: {
46
+ key: keyof Row & string;
47
+ league: League;
48
+ /** "primary" (default) or "secondary". */
49
+ which?: Which;
50
+ /** The fill's opacity over the theme's background, 0 to 1. Default 0.4 (as the `teamColorBg` column). */
51
+ alpha?: number;
52
+ season?: SeasonInput;
53
+ idSystem?: IdSystem;
54
+ };
55
+ }
56
+ /**
57
+ * An sdvtables spec as AG Grid options: `createGrid(el, gridOptions(spec, rows, { theme: themeQuartz }))`, or
58
+ * `<AgGridReact {...gridOptions(spec, rows, { theme: themeQuartz })} />`.
59
+ *
60
+ * - **Columns**: one `ColDef` per spec column, with the HTML table's label, alignment and px `width`; numbers (`num`,
61
+ * `int`, `pct`, `rank`, and a `scaleNote`'s divisor and label suffix) keep sdvtables' formats, filter as numbers, and
62
+ * sort with sdvtables' own comparator, missing values last both ways (a blank number is missing, never 0).
63
+ * - **Images**: `logo`, `wordmark` and `headshot` columns draw `<img>` cells `height` px tall, in plain JavaScript and
64
+ * in ag-grid-react alike. A team or player with no image keeps its text, with one warning. Team values may be
65
+ * abbreviations, names or numeric ids.
66
+ * - **Team colours**: `teamColorBg` and `teamColorBar` columns, and whole rows with `teamRows`, take sdvplot's team
67
+ * palette for the theme's background, with black or white text by contrast; a team row keeps a shade of its fill
68
+ * when AG Grid hovers or selects it.
69
+ * - **Theme**: with `theme` (AG Grid's `themeQuartz`, say), the theme's fonts, colours, header style, rules, stripes and
70
+ * colour scheme as AG Grid theme params; its density as the row height.
71
+ *
72
+ * Other column kinds (pills, bars, sparklines...) show their plain value, with one warning per kind. Synchronous: call
73
+ * `await prepare(spec)` (`@sportsdataverse/sdvtables/html`) first. Images and colours are worked out once, from
74
+ * `rows`: build the options again when the rows change. Community features only (MIT `ag-grid-community`):
75
+ * register `AllCommunityModule`; nothing here needs AG Grid Enterprise.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { AllCommunityModule, ModuleRegistry, themeQuartz } from "ag-grid-community";
80
+ * import { defineTable } from "@sportsdataverse/sdvtables";
81
+ * import { gridOptions } from "@sportsdataverse/sdvtables/aggrid";
82
+ * import { prepare } from "@sportsdataverse/sdvtables/html";
83
+ *
84
+ * ModuleRegistry.registerModules([AllCommunityModule]);
85
+ * // 2024 regular season, nflverse games
86
+ * const rows = [
87
+ * { team: "KC", wins: 15, pf: 385, pa: 326 },
88
+ * { team: "BUF", wins: 13, pf: 525, pa: 368 },
89
+ * { team: "LAC", wins: 11, pf: 402, pa: 301 },
90
+ * ];
91
+ * const spec = defineTable<(typeof rows)[number]>()
92
+ * .columns((c) => [c.logo("team", { league: "nfl", height: 28 }), c.int("wins"), c.int("pf"), c.int("pa")])
93
+ * .theme("sdv")
94
+ * .rowKey("team")
95
+ * .build();
96
+ * await prepare(spec);
97
+ * const options = gridOptions(spec, rows, { theme: themeQuartz, teamRows: { key: "team", league: "nfl" } });
98
+ * // in the page: createGrid(document.querySelector("#grid"), options), from ag-grid-community
99
+ * options.columnDefs.map((c) => [c.headerName, c.cellDataType]);
100
+ * ```
101
+ */
102
+ declare function gridOptions<Row>(spec: TableSpec<Row>, rows: readonly Row[], o?: GridOptionsOptions<Row>): SdvGridOptions<Row>;
103
+
104
+ export { type GridOptionsOptions, type SdvGridOptions, gridOptions };
package/dist/aggrid.js ADDED
@@ -0,0 +1,230 @@
1
+ import { opaque, scaleNotes, missingLast, pxWidth, formatterFor, unmapped, teamColorOf, checkHeight, headshotUrls, logoMarks, teamRowFills, rowIds, value, tint, cell, rankParts } from './chunk-WBTFIGYK.js';
2
+ import { alignOf, labelOf, teamNameSync } from './chunk-R375P3MN.js';
3
+ import { resolveTheme, NUMERIC_KINDS, toNumber } from './chunk-UTOVFIFT.js';
4
+ import { onColor, hex6, mix } from '@sportsdataverse/sdvplot';
5
+ import { teamColorPalette } from '@sportsdataverse/sdvplot/tanstack';
6
+
7
+ var ROW_HEIGHT = { compact: 32, comfortable: 42, social: 52 };
8
+ var HOVER = 0.08;
9
+ var SELECTED = 0.16;
10
+ var AG = {
11
+ fn: "gridOptions",
12
+ ns: "aggrid",
13
+ grid: "AG Grid",
14
+ short: "AG Grid",
15
+ built: "options"
16
+ };
17
+ var shade = (f, t) => mix(f.fill, f.ink === "#000000" ? "#ffffff" : "#000000", t);
18
+ function rendererOf(draw) {
19
+ return class SdvCell {
20
+ gui;
21
+ init(p) {
22
+ const doc = p.eGridCell?.ownerDocument ?? document;
23
+ const gui = doc.createElement("span");
24
+ gui.style.display = "inline-flex";
25
+ gui.style.alignItems = "center";
26
+ gui.style.gap = "0.4em";
27
+ gui.append(draw(p, doc));
28
+ this.gui = gui;
29
+ }
30
+ getGui() {
31
+ return this.gui;
32
+ }
33
+ refresh() {
34
+ return false;
35
+ }
36
+ };
37
+ }
38
+ function image(doc, src, alt, height, id, kind) {
39
+ const img = doc.createElement("img");
40
+ img.src = src;
41
+ img.alt = alt;
42
+ img.height = height;
43
+ img.style.height = `${height}px`;
44
+ img.style.width = "auto";
45
+ img.dataset.sdvId = id;
46
+ img.dataset.sdvKind = kind;
47
+ return img;
48
+ }
49
+ function gridOptions(spec, rows, o = {}) {
50
+ const t = resolveTheme(spec.theme).tokens;
51
+ const bg = opaque(t);
52
+ let tallest = 0;
53
+ const { scaled, labelSuffix } = scaleNotes(spec);
54
+ const header = headerStyle(t);
55
+ const columnDefs = spec.columns.map((col) => {
56
+ const align = alignOf(col);
57
+ const sortBy = missingLast(col, rows);
58
+ const numeric = NUMERIC_KINDS.has(col.kind);
59
+ const def = {
60
+ colId: col.key,
61
+ field: col.key,
62
+ headerName: labelOf(col) + (labelSuffix.get(col.key) ?? ""),
63
+ // AG Grid's docs (colDef.d.ts: `isDescending` is "not to be used for inverting the return value") assume a
64
+ // comparator that orders ascending, which the grid then reverses (RowNodeSorter: `descending ? -result : result`).
65
+ // Ours is not that: withMissingLast(cmp, "desc") already orders descending AND puts missing values last, and
66
+ // reversing it would put them first. So it is negated here, and the grid's own negation undoes that: the final
67
+ // order is exactly withMissingLast's, both ways. Do not drop the negation: test/dom/aggrid.test.ts "sorting" sorts
68
+ // a real grid both ways and fails without it (a blank value goes first when descending).
69
+ comparator: (a, b, _na, _nb, desc) => {
70
+ const r = sortBy(desc ? "desc" : "asc")(a, b);
71
+ return desc ? -r : r;
72
+ },
73
+ cellStyle: { textAlign: align },
74
+ ...align === "right" ? { headerClass: "ag-right-aligned-header" } : {},
75
+ ...header === void 0 ? {} : { headerStyle: header },
76
+ ...col.subheader === void 0 ? {} : { headerTooltip: col.subheader },
77
+ ...col.sortable === void 0 ? {} : { sortable: col.sortable },
78
+ ...col.filterable === void 0 ? {} : { filter: col.filterable },
79
+ ...numeric ? {
80
+ cellDataType: "number",
81
+ // with filtering on, AG Grid gives the column its number filter
82
+ valueGetter: (p) => toNumber(value(p.data, col.key))
83
+ } : { cellDataType: false }
84
+ };
85
+ const width = pxWidth(col, AG);
86
+ if (width !== void 0) def.width = width;
87
+ const format = formatterFor(col, scaled);
88
+ return {
89
+ ...def,
90
+ ...format === void 0 ? {} : { valueFormatter: ({ value: v }) => format(v) },
91
+ ...kindProps(col, def.cellStyle)
92
+ };
93
+ });
94
+ function kindProps(col, base) {
95
+ switch (col.kind) {
96
+ case "text":
97
+ case "num":
98
+ case "int":
99
+ case "pct":
100
+ return {};
101
+ case "rank":
102
+ return col.superscript ? {
103
+ // the HTML table's superscript suffix: 1<sup>st</sup>
104
+ cellRenderer: rendererOf(({ value: v }, doc) => {
105
+ const r = rankParts(v);
106
+ if (r === null) return "";
107
+ const out = doc.createDocumentFragment();
108
+ const sup = doc.createElement("sup");
109
+ sup.style.fontSize = col.suffixSize;
110
+ sup.textContent = r.suffix;
111
+ out.append(r.num, sup);
112
+ return out;
113
+ })
114
+ } : {};
115
+ case "logo":
116
+ case "wordmark": {
117
+ const h = checkHeight(col.height, col.kind);
118
+ tallest = Math.max(tallest, h);
119
+ const marks = logoMarks(col, rows);
120
+ const includeName = col.kind === "logo" && col.includeName;
121
+ const kind = col.kind;
122
+ return {
123
+ cellRenderer: rendererOf(({ value: raw }, doc) => {
124
+ const text = cell(raw);
125
+ const m = marks.get(text);
126
+ if (m === void 0) return text;
127
+ const img = image(doc, m.url, teamNameSync(col.league, m.id) ?? text, h, m.id, kind);
128
+ if (!includeName) return img;
129
+ const both = doc.createDocumentFragment();
130
+ both.append(img, text);
131
+ return both;
132
+ })
133
+ };
134
+ }
135
+ case "headshot": {
136
+ const h = checkHeight(col.height, "headshot");
137
+ tallest = Math.max(tallest, h);
138
+ const urls = headshotUrls(col, rows, AG);
139
+ return {
140
+ cellRenderer: rendererOf(({ value: raw }, doc) => {
141
+ const text = cell(raw);
142
+ const url = urls.get(text);
143
+ return url === void 0 ? text : image(doc, url, "", h, text, "headshot");
144
+ })
145
+ };
146
+ }
147
+ case "teamColorBg":
148
+ case "teamColorBar": {
149
+ const colorOf = teamColorOf(col, rows, bg, teamColorPalette);
150
+ const bar = col.kind === "teamColorBar";
151
+ const styleOf = (raw) => {
152
+ const color = colorOf(raw);
153
+ if (bar)
154
+ return {
155
+ ...base,
156
+ borderLeft: `${col.barWidth}px solid ${hex6(color, { dropAlpha: true })}`
157
+ };
158
+ const f = tint(color, col.alpha, bg);
159
+ return { ...base, backgroundColor: f.fill, color: f.ink };
160
+ };
161
+ return { cellStyle: ({ value: raw }) => styleOf(raw) };
162
+ }
163
+ default:
164
+ unmapped(col.kind, AG);
165
+ return {};
166
+ }
167
+ }
168
+ const rowFill = /* @__PURE__ */ new Map();
169
+ if (o.teamRows)
170
+ for (const [v, f] of teamRowFills(rows, o.teamRows, bg, teamColorPalette))
171
+ rowFill.set(v, {
172
+ backgroundColor: f.fill,
173
+ color: f.ink,
174
+ // AG Grid's own row variables, read on this row: the filler after the last column, and the hover and
175
+ // selected overlays it paints over the row (opaque shades of the fill, so the ink above still reads)
176
+ "--ag-data-background-color": f.fill,
177
+ "--ag-row-hover-color": shade(f, HOVER),
178
+ "--ag-selected-row-background-color": shade(f, SELECTED)
179
+ });
180
+ const teamKey = o.teamRows?.key;
181
+ const getRowStyle = ({ data }) => teamKey === void 0 ? void 0 : rowFill.get(cell(value(data, teamKey)));
182
+ const idOf = rowIds(rows, spec.rowKey, AG);
183
+ const getRowId = ({ data }) => idOf(data);
184
+ const rowHeight = Math.max(ROW_HEIGHT[spec.theme.density], Math.ceil(tallest + 8));
185
+ return {
186
+ columnDefs,
187
+ rowData: rows.slice(),
188
+ getRowId,
189
+ getRowStyle,
190
+ rowHeight,
191
+ ...o.theme === void 0 ? {} : { theme: o.theme.withParams(themeParams(t)) }
192
+ };
193
+ }
194
+ var set = (v) => v !== "transparent" && v !== "inherit" && v !== "none" && v !== "";
195
+ function headerStyle(t) {
196
+ const s = {
197
+ ...set(t.labelTransform) ? { textTransform: t.labelTransform } : {},
198
+ ...set(t.labelTracking) && t.labelTracking !== "normal" ? { letterSpacing: t.labelTracking } : {},
199
+ ...set(t.labelStyle) && t.labelStyle !== "normal" ? { fontStyle: t.labelStyle } : {}
200
+ };
201
+ return Object.keys(s).length === 0 ? void 0 : s;
202
+ }
203
+ function themeParams(t) {
204
+ const bg = opaque(t);
205
+ const head = set(t.labelBg) ? t.labelBg : bg;
206
+ return {
207
+ backgroundColor: bg,
208
+ foregroundColor: t.text,
209
+ textColor: t.text,
210
+ cellTextColor: t.text,
211
+ subtleTextColor: t.muted,
212
+ borderColor: t.rule,
213
+ ...set(t.accent) ? { accentColor: t.accent } : {},
214
+ headerBackgroundColor: head,
215
+ headerTextColor: set(t.labelColor) ? t.labelColor : t.text,
216
+ headerFontFamily: t.fontLabel,
217
+ headerFontWeight: t.labelWeight,
218
+ headerFontSize: t.labelSize,
219
+ fontFamily: t.fontBody,
220
+ fontSize: t.bodySize,
221
+ cellFontWeight: t.bodyWeight,
222
+ oddRowBackgroundColor: set(t.stripe) ? t.stripe : bg,
223
+ // dark themes get AG Grid's dark scrollbars and form controls
224
+ browserColorScheme: onColor(bg) === "#ffffff" ? "dark" : "light"
225
+ };
226
+ }
227
+
228
+ export { gridOptions };
229
+ //# sourceMappingURL=aggrid.js.map
230
+ //# sourceMappingURL=aggrid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/aggrid.ts"],"names":[],"mappings":"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","file":"aggrid.js","sourcesContent":["/**\n * The AG Grid adapter (`ag-grid-community` 36, the MIT community package only): an sdvtables spec as the grid options\n * `createGrid(el, options)` (or `<AgGridReact {...options} />`) takes. Columns keep sdvtables' number formats,\n * alignment, px widths and sort; logo, wordmark and headshot columns draw their images through cell renderers that\n * work in plain JavaScript and in ag-grid-react alike; team-coloured rows and cells take sdvplot's team palette with\n * black or white text by contrast, kept through AG Grid's hover and selection; the theme's tokens become an AG Grid\n * theme through its Theming API, from the base theme the caller passes in.\n * Pure: AG Grid is never imported at runtime (types only), and nothing touches the DOM until AG Grid renders a cell, so\n * the module imports on a server.\n */\nimport {\n type IdSystem,\n type League,\n type SeasonInput,\n type Which,\n hex6,\n mix,\n onColor,\n} from \"@sportsdataverse/sdvplot\";\nimport { teamColorPalette } from \"@sportsdataverse/sdvplot/tanstack\";\nimport type {\n CellStyle,\n ColDef,\n GetRowIdParams,\n HeaderStyle,\n ICellRendererComp,\n ICellRendererParams,\n RowClassParams,\n RowStyle,\n Theme,\n ThemeDefaultParams,\n} from \"ag-grid-community\";\nimport {\n type Fill,\n type GridName,\n cell,\n checkHeight,\n formatterFor,\n headshotUrls,\n logoMarks,\n missingLast,\n opaque,\n pxWidth,\n rankParts,\n rowIds,\n scaleNotes,\n teamColorOf,\n teamRowFills,\n tint,\n unmapped,\n value,\n} from \"./_grid.js\";\nimport { NUMERIC_KINDS } from \"./engine.js\";\nimport { toNumber } from \"./format.js\";\nimport { alignOf, labelOf } from \"./html/parts.js\";\nimport type { ColumnSpec, TableSpec } from \"./spec.js\";\nimport { teamNameSync } from \"./team-name.js\";\nimport { resolveTheme } from \"./themes/index.js\";\nimport type { ThemeTokens } from \"./themes/tokens.js\";\n\n/** What `gridOptions` returns: pass it to `createGrid(el, options)`, or spread it onto `<AgGridReact />`. */\nexport interface SdvGridOptions<Row> {\n /** One column per spec column. */\n columnDefs: ColDef<Row>[];\n /** The rows, in their order. */\n rowData: Row[];\n /** `String(row[spec.rowKey])`, else the row's index in `rows`; checked unique and not blank when built. */\n getRowId: (params: GetRowIdParams<Row>) => string;\n /** A team row's fill, text colour and hover and selected shades (`teamRows`); other rows keep the theme. */\n getRowStyle: (params: RowClassParams<Row>) => RowStyle | undefined;\n /** The theme's density as a row height in px, tall enough for the tallest image. */\n rowHeight: number;\n /** The theme as AG Grid theme params over the `theme` option's base; absent without that option. */\n theme?: Theme<ThemeDefaultParams>;\n}\n/** `gridOptions` options. */\nexport interface GridOptionsOptions<Row> {\n /**\n * The AG Grid base theme to lay the sdvtables theme over, usually `themeQuartz` from `ag-grid-community` (or\n * `themeAlpine`, `themeBalham`). Passed in so this module never imports AG Grid. Without it, no `theme` is returned\n * and the grid keeps its own.\n */\n theme?: Theme<ThemeDefaultParams>;\n /**\n * Fill each row with its team's colour, from the row's `key` value (an abbreviation, a name or a numeric id).\n * The colours are sdvplot's team palette for the theme's background (3:1 against it, distinct between teams) laid\n * over it at `alpha` (default 0.4), with black or white text, whichever reads better on the fill. A row whose team\n * is not found keeps the theme.\n */\n teamRows?: {\n key: keyof Row & string;\n league: League;\n /** \"primary\" (default) or \"secondary\". */\n which?: Which;\n /** The fill's opacity over the theme's background, 0 to 1. Default 0.4 (as the `teamColorBg` column). */\n alpha?: number;\n season?: SeasonInput;\n idSystem?: IdSystem;\n };\n}\n\n// a row height per density: compact, AG Grid Quartz's own (42), and a roomier one for social cards\nconst ROW_HEIGHT = { compact: 32, comfortable: 42, social: 52 } as const;\n// how far a team row's hover and selected shades move away from its text colour (so the text only gains contrast)\nconst HOVER = 0.08;\nconst SELECTED = 0.16;\n\nconst AG: GridName = {\n fn: \"gridOptions\",\n ns: \"aggrid\",\n grid: \"AG Grid\",\n short: \"AG Grid\",\n built: \"options\",\n};\n/** A fill moved `t` away from its ink: lighter under black text, darker under white, so the text reads at least as well. */\nconst shade = (f: Fill, t: number): string => mix(f.fill, f.ink === \"#000000\" ? \"#ffffff\" : \"#000000\", t);\n\n/**\n * An AG Grid cell renderer class whose cell is `draw(params)`: a DOM node, or text. A class with `getGui` because\n * ag-grid-react renders a plain function as a React component; a class is a plain-JavaScript component in both.\n */\nfunction rendererOf(\n draw: (p: ICellRendererParams, doc: Document) => Node | string,\n): new () => ICellRendererComp {\n return class SdvCell implements ICellRendererComp {\n private gui: HTMLElement | undefined;\n init(p: ICellRendererParams): void {\n const doc = p.eGridCell?.ownerDocument ?? document;\n const gui = doc.createElement(\"span\");\n gui.style.display = \"inline-flex\";\n gui.style.alignItems = \"center\";\n gui.style.gap = \"0.4em\";\n gui.append(draw(p, doc));\n this.gui = gui;\n }\n getGui(): HTMLElement {\n return this.gui as HTMLElement;\n }\n refresh(): boolean {\n return false; // AG Grid builds the cell again\n }\n };\n}\nfunction image(\n doc: Document,\n src: string,\n alt: string,\n height: number,\n id: string,\n kind: string,\n): HTMLImageElement {\n const img = doc.createElement(\"img\");\n img.src = src;\n img.alt = alt;\n img.height = height;\n img.style.height = `${height}px`;\n img.style.width = \"auto\";\n img.dataset.sdvId = id;\n img.dataset.sdvKind = kind;\n return img;\n}\n\n/**\n * An sdvtables spec as AG Grid options: `createGrid(el, gridOptions(spec, rows, { theme: themeQuartz }))`, or\n * `<AgGridReact {...gridOptions(spec, rows, { theme: themeQuartz })} />`.\n *\n * - **Columns**: one `ColDef` per spec column, with the HTML table's label, alignment and px `width`; numbers (`num`,\n * `int`, `pct`, `rank`, and a `scaleNote`'s divisor and label suffix) keep sdvtables' formats, filter as numbers, and\n * sort with sdvtables' own comparator, missing values last both ways (a blank number is missing, never 0).\n * - **Images**: `logo`, `wordmark` and `headshot` columns draw `<img>` cells `height` px tall, in plain JavaScript and\n * in ag-grid-react alike. A team or player with no image keeps its text, with one warning. Team values may be\n * abbreviations, names or numeric ids.\n * - **Team colours**: `teamColorBg` and `teamColorBar` columns, and whole rows with `teamRows`, take sdvplot's team\n * palette for the theme's background, with black or white text by contrast; a team row keeps a shade of its fill\n * when AG Grid hovers or selects it.\n * - **Theme**: with `theme` (AG Grid's `themeQuartz`, say), the theme's fonts, colours, header style, rules, stripes and\n * colour scheme as AG Grid theme params; its density as the row height.\n *\n * Other column kinds (pills, bars, sparklines...) show their plain value, with one warning per kind. Synchronous: call\n * `await prepare(spec)` (`@sportsdataverse/sdvtables/html`) first. Images and colours are worked out once, from\n * `rows`: build the options again when the rows change. Community features only (MIT `ag-grid-community`):\n * register `AllCommunityModule`; nothing here needs AG Grid Enterprise.\n *\n * @example\n * ```ts\n * import { AllCommunityModule, ModuleRegistry, themeQuartz } from \"ag-grid-community\";\n * import { defineTable } from \"@sportsdataverse/sdvtables\";\n * import { gridOptions } from \"@sportsdataverse/sdvtables/aggrid\";\n * import { prepare } from \"@sportsdataverse/sdvtables/html\";\n *\n * ModuleRegistry.registerModules([AllCommunityModule]);\n * // 2024 regular season, nflverse games\n * const rows = [\n * { team: \"KC\", wins: 15, pf: 385, pa: 326 },\n * { team: \"BUF\", wins: 13, pf: 525, pa: 368 },\n * { team: \"LAC\", wins: 11, pf: 402, pa: 301 },\n * ];\n * const spec = defineTable<(typeof rows)[number]>()\n * .columns((c) => [c.logo(\"team\", { league: \"nfl\", height: 28 }), c.int(\"wins\"), c.int(\"pf\"), c.int(\"pa\")])\n * .theme(\"sdv\")\n * .rowKey(\"team\")\n * .build();\n * await prepare(spec);\n * const options = gridOptions(spec, rows, { theme: themeQuartz, teamRows: { key: \"team\", league: \"nfl\" } });\n * // in the page: createGrid(document.querySelector(\"#grid\"), options), from ag-grid-community\n * options.columnDefs.map((c) => [c.headerName, c.cellDataType]);\n * ```\n */\nexport function gridOptions<Row>(\n spec: TableSpec<Row>,\n rows: readonly Row[],\n o: GridOptionsOptions<Row> = {},\n): SdvGridOptions<Row> {\n const t = resolveTheme(spec.theme).tokens;\n const bg = opaque(t);\n let tallest = 0;\n const { scaled, labelSuffix } = scaleNotes(spec);\n const header = headerStyle(t);\n\n const columnDefs = spec.columns.map((col): ColDef<Row> => {\n const align = alignOf(col);\n const sortBy = missingLast(col, rows);\n const numeric = NUMERIC_KINDS.has(col.kind);\n const def: ColDef<Row> = {\n colId: col.key,\n field: col.key as unknown as NonNullable<ColDef<Row>[\"field\"]>,\n headerName: labelOf(col) + (labelSuffix.get(col.key) ?? \"\"),\n // AG Grid's docs (colDef.d.ts: `isDescending` is \"not to be used for inverting the return value\") assume a\n // comparator that orders ascending, which the grid then reverses (RowNodeSorter: `descending ? -result : result`).\n // Ours is not that: withMissingLast(cmp, \"desc\") already orders descending AND puts missing values last, and\n // reversing it would put them first. So it is negated here, and the grid's own negation undoes that: the final\n // order is exactly withMissingLast's, both ways. Do not drop the negation: test/dom/aggrid.test.ts \"sorting\" sorts\n // a real grid both ways and fails without it (a blank value goes first when descending).\n comparator: (a, b, _na, _nb, desc) => {\n const r = sortBy(desc ? \"desc\" : \"asc\")(a, b);\n return desc ? -r : r;\n },\n cellStyle: { textAlign: align },\n ...(align === \"right\" ? { headerClass: \"ag-right-aligned-header\" } : {}),\n ...(header === undefined ? {} : { headerStyle: header }),\n ...(col.subheader === undefined ? {} : { headerTooltip: col.subheader }),\n ...(col.sortable === undefined ? {} : { sortable: col.sortable }),\n ...(col.filterable === undefined ? {} : { filter: col.filterable }),\n ...(numeric\n ? {\n cellDataType: \"number\", // with filtering on, AG Grid gives the column its number filter\n valueGetter: (p) => toNumber(value(p.data, col.key)),\n }\n : { cellDataType: false }),\n };\n const width = pxWidth(col, AG);\n if (width !== undefined) def.width = width;\n const format = formatterFor(col, scaled);\n return {\n ...def,\n ...(format === undefined ? {} : { valueFormatter: ({ value: v }) => format(v) }),\n ...kindProps(col, def.cellStyle as CellStyle),\n };\n });\n\n function kindProps(col: ColumnSpec<Row>, base: CellStyle): Partial<ColDef<Row>> {\n switch (col.kind) {\n case \"text\":\n case \"num\":\n case \"int\":\n case \"pct\":\n return {};\n case \"rank\":\n return col.superscript\n ? {\n // the HTML table's superscript suffix: 1<sup>st</sup>\n cellRenderer: rendererOf(({ value: v }, doc) => {\n const r = rankParts(v);\n if (r === null) return \"\";\n const out = doc.createDocumentFragment();\n const sup = doc.createElement(\"sup\");\n sup.style.fontSize = col.suffixSize;\n sup.textContent = r.suffix;\n out.append(r.num, sup);\n return out;\n }),\n }\n : {};\n case \"logo\":\n case \"wordmark\": {\n const h = checkHeight(col.height, col.kind);\n tallest = Math.max(tallest, h);\n const marks = logoMarks(col, rows);\n const includeName = col.kind === \"logo\" && col.includeName;\n const kind = col.kind;\n return {\n cellRenderer: rendererOf(({ value: raw }, doc) => {\n const text = cell(raw);\n const m = marks.get(text);\n if (m === undefined) return text; // blank, unknown team or no image: the text, as the HTML table keeps it\n const img = image(doc, m.url, teamNameSync(col.league, m.id) ?? text, h, m.id, kind);\n if (!includeName) return img;\n const both = doc.createDocumentFragment();\n both.append(img, text);\n return both;\n }),\n };\n }\n case \"headshot\": {\n const h = checkHeight(col.height, \"headshot\");\n tallest = Math.max(tallest, h);\n const urls = headshotUrls(col, rows, AG);\n return {\n cellRenderer: rendererOf(({ value: raw }, doc) => {\n const text = cell(raw);\n const url = urls.get(text);\n // decorative: the spec gives a headshot no name, and the player's name sits in its row\n return url === undefined ? text : image(doc, url, \"\", h, text, \"headshot\");\n }),\n };\n }\n case \"teamColorBg\":\n case \"teamColorBar\": {\n const colorOf = teamColorOf(col, rows, bg, teamColorPalette);\n const bar = col.kind === \"teamColorBar\";\n const styleOf = (raw: unknown): CellStyle => {\n const color = colorOf(raw);\n if (bar)\n return {\n ...base,\n borderLeft: `${col.barWidth}px solid ${hex6(color, { dropAlpha: true })}`,\n };\n const f = tint(color, col.alpha, bg);\n return { ...base, backgroundColor: f.fill, color: f.ink };\n };\n return { cellStyle: ({ value: raw }) => styleOf(raw) };\n }\n default:\n unmapped(col.kind, AG);\n return {};\n }\n }\n\n // team rows: each team value's fill, ink and hover / selected shades, worked out once\n const rowFill = new Map<string, RowStyle>();\n if (o.teamRows)\n for (const [v, f] of teamRowFills(rows, o.teamRows, bg, teamColorPalette))\n rowFill.set(v, {\n backgroundColor: f.fill,\n color: f.ink,\n // AG Grid's own row variables, read on this row: the filler after the last column, and the hover and\n // selected overlays it paints over the row (opaque shades of the fill, so the ink above still reads)\n \"--ag-data-background-color\": f.fill,\n \"--ag-row-hover-color\": shade(f, HOVER),\n \"--ag-selected-row-background-color\": shade(f, SELECTED),\n } as RowStyle);\n const teamKey = o.teamRows?.key;\n const getRowStyle = ({ data }: RowClassParams<Row>): RowStyle | undefined =>\n teamKey === undefined ? undefined : rowFill.get(cell(value(data, teamKey)));\n\n // every row's id, checked once: a repeat would give AG Grid one id twice, and it would drop a row\n const idOf = rowIds(rows, spec.rowKey, AG);\n const getRowId = ({ data }: GetRowIdParams<Row>): string => idOf(data);\n\n const rowHeight = Math.max(ROW_HEIGHT[spec.theme.density], Math.ceil(tallest + 8));\n return {\n columnDefs,\n rowData: rows.slice(),\n getRowId,\n getRowStyle,\n rowHeight,\n ...(o.theme === undefined ? {} : { theme: o.theme.withParams(themeParams(t)) }),\n };\n}\n\nconst set = (v: string): boolean => v !== \"transparent\" && v !== \"inherit\" && v !== \"none\" && v !== \"\";\n\n/** The header text style AG Grid has no theme param for: case, tracking and italics. */\nfunction headerStyle(t: ThemeTokens): HeaderStyle | undefined {\n const s: HeaderStyle = {\n ...(set(t.labelTransform) ? { textTransform: t.labelTransform } : {}),\n ...(set(t.labelTracking) && t.labelTracking !== \"normal\" ? { letterSpacing: t.labelTracking } : {}),\n ...(set(t.labelStyle) && t.labelStyle !== \"normal\" ? { fontStyle: t.labelStyle } : {}),\n };\n return Object.keys(s).length === 0 ? undefined : s;\n}\n\n/** The theme's tokens as AG Grid theme params (Theming API), for `base.withParams(...)`. */\nfunction themeParams(t: ThemeTokens): Partial<ThemeDefaultParams> {\n const bg = opaque(t);\n const head = set(t.labelBg) ? t.labelBg : bg;\n return {\n backgroundColor: bg,\n foregroundColor: t.text,\n textColor: t.text,\n cellTextColor: t.text,\n subtleTextColor: t.muted,\n borderColor: t.rule,\n ...(set(t.accent) ? { accentColor: t.accent } : {}),\n headerBackgroundColor: head,\n headerTextColor: set(t.labelColor) ? t.labelColor : t.text,\n headerFontFamily: t.fontLabel,\n headerFontWeight: t.labelWeight as ThemeDefaultParams[\"headerFontWeight\"],\n headerFontSize: t.labelSize,\n fontFamily: t.fontBody,\n fontSize: t.bodySize,\n cellFontWeight: t.bodyWeight as ThemeDefaultParams[\"cellFontWeight\"],\n oddRowBackgroundColor: set(t.stripe) ? t.stripe : bg,\n // dark themes get AG Grid's dark scrollbars and form controls\n browserColorScheme: onColor(bg) === \"#ffffff\" ? \"dark\" : \"light\",\n };\n}\n"]}
@@ -0,0 +1,223 @@
1
+ import { tableId, escapeHtml, escapeAttr, renderParts, assemble, tableHTML } from './chunk-R375P3MN.js';
2
+ import { nextSortDir } from './chunk-UTOVFIFT.js';
3
+
4
+ // src/html/interactive.ts
5
+ function tableRenderOptions(table) {
6
+ const sel = table.getSelection();
7
+ return {
8
+ interactive: true,
9
+ sort: table.state.sort,
10
+ hidden: table.state.hidden,
11
+ selected: new Set(table.rows.flatMap((r, i) => sel.has(table.rowId(r)) ? [i] : [])),
12
+ // A49: always the full source rows, so colours, legends, outlier limits and row-accent palettes hold still
13
+ // while the user pages, types, sorts or brushes, and index row selectors mean source rows
14
+ domainRows: table.allRows,
15
+ // Task 10 (A48): a selectable grid whose one tab stop is the cursor row
16
+ grid: table.state.cursor
17
+ };
18
+ }
19
+ function pagerLabel(table) {
20
+ return `Page ${table.state.page + 1} of ${table.pageCount}`;
21
+ }
22
+ function filterInputs(table, labels) {
23
+ return table.columns.flatMap((c) => {
24
+ const label = labels.get(c.key);
25
+ const f = table.state.filters[c.key];
26
+ return c.filterable === true && label !== void 0 ? [{ key: c.key, label, value: typeof f === "string" ? f : "" }] : [];
27
+ });
28
+ }
29
+ var SR_ONLY = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";
30
+ var searchInput = (id, label, placeholder, cls, data, value) => `<label for="${escapeAttr(id)}" style="${SR_ONLY}">${label}</label><input type="search" id="${escapeAttr(id)}" class="${cls}" ${data} placeholder="${placeholder}" value="${escapeAttr(value)}"/>`;
31
+ function renderToolbar(table, labels = renderParts(table.spec, table.rows, tableRenderOptions(table)).labels) {
32
+ const id = tableId(table.spec);
33
+ const global = searchInput(
34
+ `${id}-search`,
35
+ "Search all columns",
36
+ "Search",
37
+ "sdvt-global-filter",
38
+ 'data-sdv-global-filter=""',
39
+ table.state.globalFilter
40
+ );
41
+ const perColumn = filterInputs(table, labels).map((f, j) => {
42
+ const label = `Filter ${escapeHtml(f.label)}`;
43
+ return searchInput(
44
+ `${id}-filter-${j}`,
45
+ label,
46
+ label,
47
+ "sdvt-filter",
48
+ `data-sdv-filter="${escapeAttr(f.key)}"`,
49
+ f.value
50
+ );
51
+ }).join("");
52
+ return `<div class="sdvt-toolbar">${global}${perColumn}</div>`;
53
+ }
54
+ function renderPager(table) {
55
+ const prev = table.state.page === 0 ? ' aria-disabled="true"' : "";
56
+ const next = table.state.page >= table.pageCount - 1 ? ' aria-disabled="true"' : "";
57
+ return `<nav class="sdvt-pager" data-sdv-pager="" aria-label="Pagination"><button type="button" class="sdvt-page" data-sdv-page="prev" aria-label="Previous page"${prev}>\u2039</button><span class="sdvt-page-label" data-sdv-page-label="" aria-live="polite">${pagerLabel(table)}</span><button type="button" class="sdvt-page" data-sdv-page="next" aria-label="Next page"${next}>\u203A</button></nav>`;
58
+ }
59
+ function renderInteractive(table, opts = {}) {
60
+ const p = renderParts(table.spec, table.rows, { ...opts, ...tableRenderOptions(table) });
61
+ const pager = table.state.pageSize === Number.POSITIVE_INFINITY ? "" : renderPager(table);
62
+ return assemble(
63
+ p,
64
+ `${renderToolbar(table, p.labels)}<div class="sdvt-body" data-sdv-body="">${tableHTML(p)}</div>${pager}`
65
+ );
66
+ }
67
+
68
+ // src/html/controls.ts
69
+ var BODY_ROW = "[data-sdv-body] tr.sdvt-row[data-row]";
70
+ function rowIdAt(table, target) {
71
+ const tr = target instanceof Element ? target.closest(BODY_ROW) : null;
72
+ const row = tr === null ? void 0 : table.rows[Number(tr.getAttribute("data-row"))];
73
+ const id = row === void 0 ? "" : table.rowId(row);
74
+ return id === "" ? null : id;
75
+ }
76
+ function applyHover(root, table, id) {
77
+ for (const tr of Array.from(root.querySelectorAll(BODY_ROW))) {
78
+ const row = id === null ? void 0 : table.rows[Number(tr.getAttribute("data-row"))];
79
+ tr.classList.toggle("sdvt-hover", row !== void 0 && table.rowId(row) === id);
80
+ }
81
+ }
82
+ function handleClick(table, target) {
83
+ const el = target instanceof Element ? target.closest("[data-sdv-sort],[data-sdv-page]") : null;
84
+ if (!el) {
85
+ const id = rowIdAt(table, target);
86
+ if (id === null) return;
87
+ const next = new Set(table.getSelection());
88
+ if (!next.delete(id)) next.add(id);
89
+ table.setSelection(next);
90
+ return;
91
+ }
92
+ if (el.getAttribute("aria-disabled") === "true") return;
93
+ const sortCol = el.getAttribute("data-sdv-sort");
94
+ const page = el.getAttribute("data-sdv-page");
95
+ if (sortCol !== null) table.setSort(sortCol, nextSortDir(table.state.sort, sortCol));
96
+ else if (page === "prev") table.setPage(table.state.page - 1);
97
+ else if (page === "next") table.setPage(table.state.page + 1);
98
+ }
99
+ function handleInput(table, target) {
100
+ if (!(target instanceof HTMLInputElement)) return;
101
+ const col = target.getAttribute("data-sdv-filter");
102
+ if (col !== null) table.setFilter(col, target.value === "" ? null : target.value);
103
+ else if (target.hasAttribute("data-sdv-global-filter")) table.setGlobalFilter(target.value);
104
+ }
105
+ function handleHover(table, target) {
106
+ table.setHover(rowIdAt(table, target));
107
+ }
108
+ var CONTROLS = ["data-sdv-sort", "data-sdv-filter", "data-sdv-global-filter", "data-sdv-page"];
109
+ var FOCUSABLE = "a[href],button,input,select,textarea,[tabindex]";
110
+ function captureFocus(el) {
111
+ const root = el.getRootNode();
112
+ const focused = root.activeElement ?? el.ownerDocument.activeElement;
113
+ if (focused === null || !el.contains(focused)) return () => {
114
+ };
115
+ const name = CONTROLS.find((a) => focused.hasAttribute(a));
116
+ const value = name === void 0 ? null : focused.getAttribute(name);
117
+ const caret = focused instanceof HTMLInputElement ? { start: focused.selectionStart, end: focused.selectionEnd } : void 0;
118
+ const td = name === void 0 ? focused.closest("[data-sdv-body] tr[data-row] td[data-col]") : null;
119
+ const cellAt = td ? {
120
+ row: td.parentElement?.getAttribute("data-row"),
121
+ col: td.getAttribute("data-col"),
122
+ n: Array.from(td.querySelectorAll(FOCUSABLE)).indexOf(focused)
123
+ } : void 0;
124
+ const row = name === void 0 && focused.matches(BODY_ROW) ? focused.getAttribute("data-row") : null;
125
+ return () => {
126
+ if (el.contains(focused)) return;
127
+ if (name !== void 0) {
128
+ const next = Array.from(el.querySelectorAll(`[${name}]`)).find(
129
+ (n) => n.getAttribute(name) === value
130
+ );
131
+ next?.focus();
132
+ if (next instanceof HTMLInputElement && caret?.start != null && caret.end != null)
133
+ next.setSelectionRange(caret.start, caret.end);
134
+ } else if (row !== null) {
135
+ Array.from(el.querySelectorAll(BODY_ROW)).find((r) => r.getAttribute("data-row") === row)?.focus();
136
+ } else if (cellAt && cellAt.n >= 0) {
137
+ const tds = Array.from(el.querySelectorAll("[data-sdv-body] tr[data-row]")).find((r) => r.getAttribute("data-row") === cellAt.row)?.querySelectorAll("td[data-col]");
138
+ const cell = Array.from(tds ?? []).find((c) => c.getAttribute("data-col") === cellAt.col);
139
+ cell?.querySelectorAll(FOCUSABLE)[cellAt.n]?.focus();
140
+ }
141
+ };
142
+ }
143
+ var KEYS = /* @__PURE__ */ new Map([
144
+ ["j", "down"],
145
+ ["k", "up"],
146
+ ["h", "left"],
147
+ ["l", "right"],
148
+ ["s", "sort"],
149
+ ["/", "search"],
150
+ ["ArrowDown", "down"],
151
+ ["ArrowUp", "up"],
152
+ ["ArrowLeft", "left"],
153
+ ["ArrowRight", "right"],
154
+ ["Enter", "toggle"],
155
+ [" ", "toggle"]
156
+ ]);
157
+ var LETTERS = /* @__PURE__ */ new Set(["j", "k", "h", "l", "s", "/"]);
158
+ var clamp = (i, n) => Math.max(0, Math.min(i, n - 1));
159
+ function keyAction(key, k) {
160
+ const what = KEYS.get(key);
161
+ if (what === void 0 || !k.hotkeys && LETTERS.has(key)) return null;
162
+ const shown = (c) => c !== null && k.cols.includes(c);
163
+ const col = shown(k.cursor.col) ? k.cursor.col : shown(k.sorted) ? k.sorted : k.cols[0] ?? null;
164
+ const row = k.focused ?? k.cursor.row;
165
+ switch (what) {
166
+ case "down":
167
+ case "up": {
168
+ const at = k.order.indexOf(row);
169
+ if (at < 0) return null;
170
+ const next = k.focused === null ? row : k.order[clamp(at + (what === "down" ? 1 : -1), k.order.length)];
171
+ return next === void 0 ? null : { type: "cursor", row: next, col: k.cursor.col };
172
+ }
173
+ case "left":
174
+ case "right": {
175
+ if (k.focused !== null && !k.onRow) return null;
176
+ const step = what === "right" ? 1 : -1;
177
+ const next = col === null ? void 0 : k.cols[clamp(k.cols.indexOf(col) + step, k.cols.length)];
178
+ return next === void 0 ? null : { type: "cursor", row, col: next };
179
+ }
180
+ case "sort":
181
+ return col === null ? null : { type: "sort", col };
182
+ case "search":
183
+ return { type: "search" };
184
+ case "toggle":
185
+ return k.onRow && k.focused !== null ? { type: "toggle", row: k.focused } : null;
186
+ }
187
+ }
188
+ var TEXT_FIELD = 'input,textarea,select,[contenteditable]:not([contenteditable="false"])';
189
+ function handleKeydown(table, root, e) {
190
+ const t = e.target;
191
+ if (e.defaultPrevented || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;
192
+ if (!(t instanceof Element) || t.closest(TEXT_FIELD) !== null) return;
193
+ const rows = Array.from(root.querySelectorAll(BODY_ROW));
194
+ const sorts = Array.from(root.querySelectorAll("[data-sdv-body] [data-sdv-sort]"));
195
+ const near = t.closest(BODY_ROW);
196
+ const rowEl = near !== null && root.contains(near) ? near : null;
197
+ const s = table.state;
198
+ const action = keyAction(e.key, {
199
+ focused: rowEl === null ? null : Number(rowEl.getAttribute("data-row")),
200
+ onRow: rowEl === t,
201
+ cursor: s.cursor,
202
+ sorted: s.sort?.col ?? null,
203
+ cols: sorts.map((b) => b.getAttribute("data-sdv-sort") ?? ""),
204
+ order: rows.map((r) => Number(r.getAttribute("data-row"))),
205
+ hotkeys: table.spec.interactive?.hotkeys !== false
206
+ });
207
+ const box = action?.type === "search" ? root.querySelector("[data-sdv-global-filter]") : null;
208
+ if (action === null || action.type === "search" && box === null) return;
209
+ e.preventDefault();
210
+ if (action.type === "search") box?.focus();
211
+ else if (action.type === "sort")
212
+ handleClick(table, sorts.find((b) => b.getAttribute("data-sdv-sort") === action.col) ?? null);
213
+ else if (action.type === "toggle") {
214
+ if (!e.repeat) handleClick(table, rowEl);
215
+ } else {
216
+ rows.find((r) => r.getAttribute("data-row") === String(action.row))?.focus();
217
+ table.setCursor(action.row, action.col);
218
+ }
219
+ }
220
+
221
+ export { SR_ONLY, applyHover, captureFocus, filterInputs, handleClick, handleHover, handleInput, handleKeydown, keyAction, pagerLabel, renderInteractive, renderPager, renderToolbar, rowIdAt, tableRenderOptions };
222
+ //# sourceMappingURL=chunk-3TJR7TKS.js.map
223
+ //# sourceMappingURL=chunk-3TJR7TKS.js.map