@sheetpane/grid 0.0.0-stage → 0.3.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 +429 -2
  3. package/custom-elements.json +1246 -0
  4. package/dist/errors/element.d.ts +108 -0
  5. package/dist/errors/element.d.ts.map +1 -0
  6. package/dist/errors/element.js +456 -0
  7. package/dist/errors/element.js.map +1 -0
  8. package/dist/excel/element.d.ts +434 -0
  9. package/dist/excel/element.d.ts.map +1 -0
  10. package/dist/excel/element.js +3382 -0
  11. package/dist/excel/element.js.map +1 -0
  12. package/dist/excel/geometry.d.ts +81 -0
  13. package/dist/excel/geometry.d.ts.map +1 -0
  14. package/dist/excel/geometry.js +137 -0
  15. package/dist/excel/geometry.js.map +1 -0
  16. package/dist/excel/selection.d.ts +103 -0
  17. package/dist/excel/selection.d.ts.map +1 -0
  18. package/dist/excel/selection.js +189 -0
  19. package/dist/excel/selection.js.map +1 -0
  20. package/dist/excel/styles.d.ts +3 -0
  21. package/dist/excel/styles.d.ts.map +1 -0
  22. package/dist/excel/styles.js +206 -0
  23. package/dist/excel/styles.js.map +1 -0
  24. package/dist/frameworks/elements.d.ts +136 -0
  25. package/dist/frameworks/jsx.d.ts +64 -0
  26. package/dist/frameworks/jsx.js +2 -0
  27. package/dist/frameworks/solid.d.ts +133 -0
  28. package/dist/frameworks/solid.js +2 -0
  29. package/dist/frameworks/svelte.d.ts +102 -0
  30. package/dist/frameworks/svelte.js +2 -0
  31. package/dist/frameworks/vue.d.ts +69 -0
  32. package/dist/frameworks/vue.js +2 -0
  33. package/dist/index.d.ts +23 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +25 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/llms-full.txt +7986 -0
  38. package/dist/llms.txt +108 -0
  39. package/dist/pane/registry.d.ts +33 -0
  40. package/dist/pane/registry.d.ts.map +1 -0
  41. package/dist/pane/registry.js +73 -0
  42. package/dist/pane/registry.js.map +1 -0
  43. package/dist/pane/rules.d.ts +231 -0
  44. package/dist/pane/rules.d.ts.map +1 -0
  45. package/dist/pane/rules.js +792 -0
  46. package/dist/pane/rules.js.map +1 -0
  47. package/dist/pane/store.d.ts +385 -0
  48. package/dist/pane/store.d.ts.map +1 -0
  49. package/dist/pane/store.js +1028 -0
  50. package/dist/pane/store.js.map +1 -0
  51. package/dist/pane/types.d.ts +130 -0
  52. package/dist/pane/types.d.ts.map +1 -0
  53. package/dist/pane/types.js +22 -0
  54. package/dist/pane/types.js.map +1 -0
  55. package/dist/plugins/host.d.ts +56 -0
  56. package/dist/plugins/host.d.ts.map +1 -0
  57. package/dist/plugins/host.js +124 -0
  58. package/dist/plugins/host.js.map +1 -0
  59. package/dist/plugins/types.d.ts +159 -0
  60. package/dist/plugins/types.d.ts.map +1 -0
  61. package/dist/plugins/types.js +12 -0
  62. package/dist/plugins/types.js.map +1 -0
  63. package/dist/rules.schema.json +52 -0
  64. package/dist/shared/dom.d.ts +23 -0
  65. package/dist/shared/dom.d.ts.map +1 -0
  66. package/dist/shared/dom.js +32 -0
  67. package/dist/shared/dom.js.map +1 -0
  68. package/dist/skills-bin.d.ts +3 -0
  69. package/dist/skills-bin.d.ts.map +1 -0
  70. package/dist/skills-bin.js +9 -0
  71. package/dist/skills-bin.js.map +1 -0
  72. package/dist/style.css +3 -0
  73. package/dist/tokens.d.ts +49 -0
  74. package/dist/tokens.d.ts.map +1 -0
  75. package/dist/tokens.js +43 -0
  76. package/dist/tokens.js.map +1 -0
  77. package/dist/vscode.css-custom-data.json +365 -0
  78. package/dist/vscode.html-custom-data.json +218 -0
  79. package/dist/web-types.json +914 -0
  80. package/package.json +70 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arpit Arya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,430 @@
1
- # Temporary Holding Version
1
+ # @sheetpane/grid
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **View and edit `.xlsx` and `.csv` files inside any web page, and review them.**
4
+ Two web components, always in sync through one in-memory workbook:
5
+
6
+ - `<sheetpane-excel>` — the sheet. View or Edit; every change stays in memory;
7
+ **Download** hands back the file — as opened when nothing changed, else a
8
+ copy of the uploaded file with the changes in it.
9
+ - `<sheetpane-errors>` — the errors flagged on the sheet: what's wrong, what it
10
+ should be. Put it anywhere — beside the sheet, in a side column, in another tab.
11
+
12
+ Nothing is uploaded: the file is read in the browser tab. No framework inside
13
+ or outside; no font of its own — yours is used.
14
+
15
+ ```sh
16
+ pnpm add @sheetpane/grid # or npm / yarn
17
+ ```
18
+
19
+ Try it first in the [live playground](https://arpitarya.github.io/sheetpane/) or
20
+ open a [starter in StackBlitz](https://github.com/arpitarya/sheetpane/blob/main/examples/starters/README.md)
21
+ (from the first published release).
22
+
23
+ ## Plain HTML
24
+
25
+ ```html
26
+ <script type="module">import '@sheetpane/grid';</script>
27
+
28
+ <sheetpane-excel pane="q3" src="/files/holdings.xlsx" mode="edit" author="Arpit" style="height:640px"></sheetpane-excel>
29
+ <sheetpane-errors pane="q3" author="Arpit" style="height:640px"></sheetpane-errors>
30
+ ```
31
+
32
+ The same `pane` id links them. Mount order does not matter, and they may sit
33
+ in different containers, tabs or dialogs.
34
+
35
+ ## React 19
36
+
37
+ React 19 sets properties and listens to custom events on custom elements
38
+ natively:
39
+
40
+ ```tsx
41
+ import '@sheetpane/grid';
42
+
43
+ export function Review({ file }: { file: File }) {
44
+ return (
45
+ <>
46
+ <sheetpane-excel pane="q3" mode="edit" file={file} onsheetpane-change={(e) => console.log(e.detail)} />
47
+ <sheetpane-errors pane="q3" onsheetpane-errors={(e) => save(e.detail.errors)} />
48
+ </>
49
+ );
50
+ }
51
+ ```
52
+
53
+ **Typed JSX:** add `import type {} from '@sheetpane/grid/jsx';` once — e.g. in
54
+ a `sheetpane.d.ts` beside your sources — and both tags, their attributes,
55
+ properties and `onsheetpane-…` props are typed, payloads included
56
+ (`e.detail.ref`). See [Typings and editor support](#typings-and-editor-support).
57
+
58
+ **React 18** sets attributes only: use a `ref` for properties and events.
59
+
60
+ ```tsx
61
+ const ref = useRef<HTMLElement & { file?: File }>(null);
62
+ useEffect(() => {
63
+ const el = ref.current!;
64
+ el.file = file;
65
+ const on = (e: Event) => save((e as CustomEvent).detail.errors);
66
+ el.addEventListener('sheetpane-errors', on);
67
+ return () => el.removeEventListener('sheetpane-errors', on);
68
+ }, [file]);
69
+ return <sheetpane-excel ref={ref} pane="q3" />;
70
+ ```
71
+
72
+ ### React hooks (recipes)
73
+
74
+ Not a package (SR-LAW-4: no framework wrappers in v1) — copy
75
+ [`examples/react/src/sheetpane-hooks.ts`](../../examples/react/src/sheetpane-hooks.ts)
76
+ into your app. Each hook takes a `ref` to `<sheetpane-excel>`, is built on
77
+ `useSyncExternalStore` with a stable selector (a component re-renders only
78
+ when what it reads changes), removes its listeners on unmount, and is safe
79
+ under StrictMode. React 19 and 18.
80
+
81
+ ```tsx
82
+ const sheet = useRef<SheetpaneExcel>(null);
83
+ const sel = useSelection(sheet); // { sheet, ref, range, column: { index, letter, header }, value, text }
84
+ const edit = useCellEdit(sheet); // { editing, ref, column, draft } while the user types
85
+ const { count, canUndo } = useChanges(sheet);
86
+ const { errors, open } = useErrors(sheet);
87
+ const wb = useWorkbook(sheet); // the pane's store
88
+ const rules = useColumnRules(sheet, { Age: { type: 'integer', min: 18 } }); // { checking, done, total }
89
+ useSheetpaneEvent(sheet, 'sheetpane-beforechange', (e) => { if (Number(e.detail.now) < 0) e.preventDefault(); });
90
+ return <sheetpane-excel ref={sheet} pane="r1" mode="edit" src="people.xlsx" />;
91
+ ```
92
+
93
+ The primitive under all of them, for a hook of your own:
94
+
95
+ ```ts
96
+ const units = useSheetpane(sheet, (store) => store.value('Holdings', 'F3'), null);
97
+ ```
98
+
99
+ ## Vue, Svelte, Solid, Angular
100
+
101
+ The elements are standard custom elements, so every framework renders them
102
+ once `import '@sheetpane/grid'` has run in the browser. No wrapper package;
103
+ an `import type` line types them.
104
+
105
+ **Vue 3** — tell the compiler the tags are custom elements, then bind
106
+ properties with `.prop` and listen with `@`:
107
+
108
+ ```js
109
+ // vite.config.js
110
+ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('sheetpane-') } } })
111
+ ```
112
+
113
+ ```vue
114
+ <script setup lang="ts">
115
+ import '@sheetpane/grid';
116
+ import type {} from '@sheetpane/grid/vue'; // GlobalComponents: props and @sheetpane-… typed
117
+ </script>
118
+ <template>
119
+ <sheetpane-excel pane="q3" mode="edit" :file.prop="file" @sheetpane-change="(e) => log(e.detail.ref)" />
120
+ <sheetpane-errors pane="q3" @sheetpane-errors="(e) => save(e.detail.errors)" />
121
+ </template>
122
+ ```
123
+
124
+ **Svelte** — `import type {} from '@sheetpane/grid/svelte';` once (e.g. in
125
+ `src/app.d.ts`); Svelte 5 listens with `onsheetpane-change={…}`, Svelte 4 with
126
+ `on:sheetpane-change={…}`; both set properties when the element has one.
127
+
128
+ ```svelte
129
+ <sheetpane-excel pane="q3" mode="edit" file={file} onsheetpane-change={(e) => log(e.detail.ref)}></sheetpane-excel>
130
+ ```
131
+
132
+ **Solid** — `import type {} from '@sheetpane/grid/solid';` once. Solid sets a
133
+ custom element's props as properties, so write an attribute that has no
134
+ property with `attr:`, and listen with `on:`:
135
+
136
+ ```tsx
137
+ <sheetpane-excel attr:pane="q3" attr:header-row="2" mode="edit" prop:file={file()} on:sheetpane-change={(e) => log(e.detail.ref)} />
138
+ ```
139
+
140
+ **Angular** — no package and no typings to import: add
141
+ `CUSTOM_ELEMENTS_SCHEMA` to the component that renders the tags, bind
142
+ properties with `[…]` and events with `(…)`:
143
+
144
+ ```ts
145
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
146
+ import '@sheetpane/grid';
147
+
148
+ @Component({
149
+ selector: 'app-review',
150
+ standalone: true,
151
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
152
+ template: `
153
+ <sheetpane-excel pane="q3" mode="edit" [file]="file" (sheetpane-change)="onChange($event)"></sheetpane-excel>
154
+ <sheetpane-errors pane="q3" (sheetpane-errors)="save($event.detail.errors)"></sheetpane-errors>`,
155
+ })
156
+ export class ReviewComponent { file: File | null = null; onChange(e: Event) { console.log((e as CustomEvent).detail); } save(errors: unknown[]) {} }
157
+ ```
158
+
159
+ **Next.js and other SSR** — the import is safe on the server (the elements
160
+ just do not register); render the tags in a client component.
161
+
162
+ ## Typings and editor support
163
+
164
+ | file | for |
165
+ |---|---|
166
+ | `@sheetpane/grid/custom-elements.json` | the [Custom Elements Manifest](https://github.com/webcomponents/custom-elements-manifest) (schema 2.1.0): every attribute, property, method, event (with its payload type), slot, CSS part and `--sp-*` property; `package.json`'s `customElements` points at it |
167
+ | `@sheetpane/grid/jsx` | React 19 — `React.JSX.IntrinsicElements` |
168
+ | `@sheetpane/grid/vue` | Vue 3 — `GlobalComponents` (Volar / vue-tsc) |
169
+ | `@sheetpane/grid/svelte` | Svelte 4 and 5 — `SvelteHTMLElements` |
170
+ | `@sheetpane/grid/solid` | Solid — `JSX.IntrinsicElements` |
171
+ | `@sheetpane/grid/vscode.html-custom-data.json` | VS Code: tags, attributes and their values in `.html` |
172
+ | `@sheetpane/grid/vscode.css-custom-data.json` | VS Code: the `--sp-*` properties in `.css` |
173
+ | `@sheetpane/grid/web-types.json` | JetBrains IDEs (WebStorm, IntelliJ) — read automatically through `package.json`'s `web-types` |
174
+
175
+ Each typing entry is types only (`import type {} from '…'`); the framework is
176
+ never a dependency. Each also exports the element types it builds on —
177
+ `SheetpaneExcelEventMap`, `SheetpaneExcelAttributes`,
178
+ `SheetpaneExcelProperties` and the same for `SheetpaneErrors` — for typing
179
+ your own wrappers.
180
+
181
+ **VS Code** — point the workspace settings at the editor data:
182
+
183
+ ```json
184
+ // .vscode/settings.json
185
+ {
186
+ "html.customData": ["./node_modules/@sheetpane/grid/dist/vscode.html-custom-data.json"],
187
+ "css.customData": ["./node_modules/@sheetpane/grid/dist/vscode.css-custom-data.json"]
188
+ }
189
+ ```
190
+
191
+ All of it is generated from the element sources and their TSDoc
192
+ (`scripts/build-manifest.mjs`, run by the build); a test fails when the
193
+ committed manifest drifts from the elements.
194
+
195
+ ## `<sheetpane-excel>`
196
+
197
+ | kind | name | what |
198
+ |---|---|---|
199
+ | attribute | `pane` | the shared id; the same on both elements |
200
+ | attribute | `src` | a URL (read with a GET), a `data:` URL or base64 |
201
+ | attribute | `mode` | `view` (default) or `edit` |
202
+ | attribute | `display` | `tab` (default; fills its container) or `popup` (a modal dialog; `open` shows it) |
203
+ | attribute | `open` | shows the pop-up |
204
+ | attribute | `author` | written on errors flagged here |
205
+ | attribute | `plugins` | the enabled plugin ids, e.g. `numfmt formulas` |
206
+ | attribute | `header-row` | the row holding column titles (default `1`) — every event names the column by it, and column rules match by it |
207
+ | attribute | `theme` | `light` (default), `dark` or `auto` (follows the page's `prefers-color-scheme`) |
208
+ | attribute / property | `on-invalid` / `onInvalid` | what a failing column rule does by default: `flag` (default) or `block` |
209
+ | attribute / property | `rules-src` / `rules` | a rules file by URL, or as an object — see [Rules file](#rules-file) |
210
+ | attribute / property | `flagging` | `on` (default) or `off`: no Flag issue, no composer, no pop-up card — errors are read in `<sheetpane-errors>` |
211
+ | property | `columns`, `validate` | column rules by title, and a check function — see [Column rules](#column-rules) |
212
+ | property | `file` | a `File`, `Blob`, `ArrayBuffer` or `Uint8Array` to read |
213
+ | property | `workbook` | the pane's store (get, or set your own from `createWorkbook()`) |
214
+ | property | `errors`, `errorTypes` | the review; the error-type catalog |
215
+ | method | `load(src, name?)` | read a file (any of the above) |
216
+ | method | `export(options?, { format? })` | `Promise<Blob>` — the upload with every change; `format: 'csv'` from an `.xlsx` is the current sheet, `'xlsx'` from a `.csv` one sheet |
217
+ | method | `select(sheet, ref)`, `getErrors()`, `setErrors(list)`, `undo()`, `redo()` | |
218
+ | event | `sheetpane-load`, `sheetpane-errors {errors, plugins}`, `sheetpane-mode {mode}`, `sheetpane-history`, `sheetpane-export {blob, kind, name, format, converted, original?}` (cancelable — prevent it to save the file yourself; `original: true` when Download hands back the untouched file, `converted` when ▾ chose the other format), `sheetpane-close` | bubbling, composed |
219
+
220
+ ### Following an edit
221
+
222
+ Every edit fires these, in this order, on the element where it happened;
223
+ every payload names the column `{ index, letter, header }` (the title from
224
+ `header-row`):
225
+
226
+ | event | fires | detail | cancel it to |
227
+ |---|---|---|---|
228
+ | `sheetpane-select` | the selected cell moves | `{sheet, ref, column, value}` | — |
229
+ | `sheetpane-editstart` | the cell bar opens on a cell | `{sheet, ref, column, value}` | keep the cell read-only |
230
+ | `sheetpane-input` | each keystroke, before saving | `{sheet, ref, column, draft, was}` | — |
231
+ | `sheetpane-beforechange` | Enter, Tab, blur, Delete, or each cell of a paste | `{sheet, ref, column, was, now}` | refuse the value |
232
+ | `sheetpane-change` | after saving | `{sheet, ref, kind, was, now}` | — |
233
+ | `sheetpane-editcancel` | Esc, or a column rule set to `block` refused the value | `{sheet, ref}` (refused: `{sheet, ref, reason: 'blocked', message}`) | — |
234
+ | `sheetpane-sheet` | the sheet tab changes | `{sheet}` | — |
235
+
236
+ A refused value never reaches the change map, undo or a plugin.
237
+
238
+ ```js
239
+ excel.addEventListener('sheetpane-beforechange', (e) => {
240
+ if (e.detail.column.header === 'Units' && Number(e.detail.now) < 0) e.preventDefault();
241
+ });
242
+ ```
243
+
244
+ **Keys:** arrows, Shift+arrows, Home/End, Ctrl+Home/End, PageUp/PageDown;
245
+ Enter or F2 edits; typing replaces; Delete clears; Ctrl/⌘+V pastes tab-separated cells; **F flags an issue**;
246
+ **right-click, Shift+F10 or the Menu key** opens the selection actions (Flag issue, Copy ref, Copy value and
247
+ plugin actions) under the cell — the same actions sit at the right end of the cell bar, never over the cells; Esc
248
+ cancels or closes the pop-up; Ctrl/⌘+Z undo, Ctrl+Y / ⌘+Shift+Z redo. The `?`
249
+ button lists them, plugins' shortcuts included.
250
+
251
+ **Selecting a range:** drag (the grid scrolls when the pointer reaches an edge), Shift+click, or click a
252
+ column header, row header or the corner — drag or Shift+click headers for several. Ctrl/⌘+arrow jumps to
253
+ the edge of the data (with Shift: selects to it); Ctrl+Space selects the column, Shift+Space the row,
254
+ Ctrl/⌘+A the data and then the sheet; Shift+Backspace goes back to the active cell; Enter and Tab move
255
+ inside a range. On a range, Ctrl/⌘+C copies it so Excel and Google Sheets paste a grid, Delete clears it
256
+ (Edit, one undo step), one pasted value fills it, and the status line shows **Count · Sum · Average**.
257
+ `sheetpane-select` carries `range` (`B3:D8`) and fires once a drag is released. On a touch screen a finger
258
+ scrolls, a tap selects, and a range shows two handles to extend it.
259
+
260
+ **Resizing:** drag a column header's right edge or a row header's bottom edge; double-click it to fit the
261
+ text; with several whole columns or rows selected, one drag or fit sizes them all. Right-click a header (or
262
+ Shift+F10 with a whole column or row selected) for *Fit to data*, *Column width…* and *Reset*. Rows keep the
263
+ file's heights. Sizes are a view setting, like zoom — not a change, not in the download; save and restore
264
+ them with the `sizes` property and the `sheetpane-resize` event:
265
+
266
+ ```js
267
+ excel.addEventListener('sheetpane-resize', () => localStorage.setItem('sizes', JSON.stringify(excel.sizes)));
268
+ excel.sizes = JSON.parse(localStorage.getItem('sizes') ?? '{}'); // { Sheet1: { cols: { B: 120 }, rows: { 4: 40 } } }
269
+ ```
270
+
271
+ ## Column rules
272
+
273
+ Check what a user types, by column title. Rules are plain JSON — save and load
274
+ them as a file if you like — and failures go into the one error list as
275
+ `author: "host"` errors that resolve themselves when the cell passes.
276
+
277
+ ```js
278
+ excel.columns = {
279
+ Name: { required: true },
280
+ Email: { pattern: '^[^@\\s]+@[^@\\s]+\\.[a-z]{2,}$', unique: true },
281
+ Age: { type: 'integer', min: 18, max: 99, onInvalid: 'block' },
282
+ Status: { oneOf: ['Active', 'Inactive'], severity: 'warning' },
283
+ };
284
+ excel.validate = async (ctx, signal) =>
285
+ ctx.column.header === 'Email' && ctx.phase !== 'input' && (await api.taken(ctx.value, { signal }))
286
+ ? { severity: 'error', message: 'Email already used' }
287
+ : null;
288
+ ```
289
+
290
+ | rule | passes when |
291
+ |---|---|
292
+ | `required` | the cell is not blank (every other rule skips a blank cell) |
293
+ | `type` | `text` · `number` · `integer` · `date` · `boolean` |
294
+ | `min`, `max` | a number in range; for `type: 'date'`, an ISO date or a serial |
295
+ | `minLength`, `maxLength` | the text's length |
296
+ | `pattern` | the text matches (values over 10,000 characters are not tested — **avoid patterns that backtrack catastrophically**: they run on the page) |
297
+ | `oneOf` | the value is one of the list (trimmed, case-insensitive) |
298
+ | `unique` | no other row of the column holds it |
299
+ | `message`, `severity`, `onInvalid` | replace the generated message; `error` (default) · `warning` · `note`; `flag` (default) · `block` |
300
+
301
+ - **Titles** match trimmed and case-insensitive (an exact title wins). A
302
+ title the file lacks is one `note` error; a duplicated title checks the
303
+ first column and adds one `note`. The file always loads.
304
+ - **`validate(ctx, signal)`** runs for every cell of a titled column, sync or
305
+ async. `ctx` is `{phase, sheet, ref, row, column, value, was, original,
306
+ rowValues}`, `rowValues` keyed by title, so a rule can read the row; editing
307
+ any cell asks the row again. A newer edit to a cell aborts the older check
308
+ (`signal`) and its result is dropped. Return `null` or `{severity, message,
309
+ expected?}`.
310
+ - **Phases:** `input` — 150 ms after typing stops, an outline and a message
311
+ at the cell bar, nothing stored; `commit` — on save, `flag` stores the value
312
+ and adds an error, `block` refuses it (only a JSON rule or a synchronous
313
+ result can block; an async result flags); `load` — every row, in slices,
314
+ with the progress in the status line.
315
+ - Setting `columns`, `validate` or `onInvalid` again re-checks every row;
316
+ host errors no longer produced are removed. People's errors are never
317
+ touched, and a rule never edits a cell.
318
+ - Rules belong to the pane: `workbook.setColumnRules({ columns, validate,
319
+ onInvalid, headerRow })` does the same without an element.
320
+
321
+ ## Rules file
322
+
323
+ Ship the rules next to your code as JSON and point the sheet at them; every
324
+ cell they cover is checked on load and the failures are listed in
325
+ `<sheetpane-errors>`. With `flagging="off"` the sheet shows no Flag issue and
326
+ opens no pop-up — the errors panel is the one place errors are read.
327
+
328
+ ```html
329
+ <sheetpane-excel pane="q3" src="./q3.xlsx" rules-src="./sheetpane.rules.json" flagging="off"></sheetpane-excel>
330
+ <sheetpane-errors pane="q3"></sheetpane-errors>
331
+ ```
332
+
333
+ ```json
334
+ {
335
+ "$schema": "https://unpkg.com/@sheetpane/grid/rules.schema.json",
336
+ "version": 1,
337
+ "sheets": {
338
+ "*": {
339
+ "columns": { "Email": { "pattern": "^[^@\\s]+@[^@\\s]+\\.[a-z]{2,}$", "unique": true } }
340
+ },
341
+ "Accounts": {
342
+ "headerRow": 2,
343
+ "columns": {
344
+ "Account ID": { "required": true, "pattern": "^AC-\\d{5}$", "message": "Account IDs are AC- and five digits" },
345
+ "Units": { "type": "integer", "min": 0 },
346
+ "Status": { "oneOf": ["Active", "Inactive"], "noneOf": ["TBD", "N/A"], "severity": "warning" }
347
+ },
348
+ "cells": {
349
+ "B1": { "equals": "Q3 review", "note": "the title cell the export pipeline reads" },
350
+ "H3:H40": { "type": "number", "min": 0, "max": 1 }
351
+ }
352
+ }
353
+ }
354
+ }
355
+ ```
356
+
357
+ - **`sheets`** by name; `"*"` applies to every sheet and a named sheet's rule
358
+ for the same title wins. Each sheet has `headerRow`, `columns` (by title,
359
+ as [Column rules](#column-rules)) and `cells` (an A1 cell or range).
360
+ - **The words** are Column rules' plus `equals` (the one valid value; a blank
361
+ cell fails it), `noneOf` (values never valid) and `note` (for whoever
362
+ maintains the file — never shown as the message).
363
+ - **`$schema`** gives your editor autocomplete and red squiggles: the schema
364
+ ships as `@sheetpane/grid/rules.schema.json`.
365
+ - **Never silent, never a throw:** an unknown key, word or version, a sheet,
366
+ title or cell the workbook lacks, or a pattern that is not a regular
367
+ expression becomes one `note` in the errors panel; the rest still applies.
368
+ - `rules-src` is read with a GET like `src`; `excel.rules = object` does the
369
+ same without a fetch. Setting either checks every row again. The element's
370
+ `columns` and `validate` still work beside it — the file's rule wins for the
371
+ same title, with a note.
372
+ - **YAML or TOML:** parse it yourself and pass the object — the package reads
373
+ JSON only, so it carries no parser:
374
+
375
+ ```js
376
+ import { parse } from 'yaml'; // or: import { parse } from 'smol-toml';
377
+ excel.rules = parse(await (await fetch('./sheetpane.rules.yaml')).text());
378
+ ```
379
+
380
+ ## `<sheetpane-errors>`
381
+
382
+ | kind | name | what |
383
+ |---|---|---|
384
+ | attribute | `pane`, `author`, `plugins` | as above |
385
+ | attribute | `readonly` | hides the form, Resolve and Delete — a sign-off screen |
386
+ | property / method | `errors`, `errorTypes`, `getErrors()`, `setErrors(list, { plugins })` | |
387
+ | event | `sheetpane-errors {errors, plugins}`, `sheetpane-reveal {error}` | **Show in sheet**: switch to the sheet's tab or open the pop-up |
388
+
389
+ ## Saving and restoring a review
390
+
391
+ Errors are plain JSON (`SheetpaneError`, schema `v: 1`). Listen to
392
+ `sheetpane-errors`, store `detail.errors` and `detail.plugins` where you like,
393
+ and give them back after the file loads:
394
+
395
+ ```js
396
+ excel.addEventListener('sheetpane-load', () => errors.setErrors(saved.errors, { plugins: saved.plugins }));
397
+ ```
398
+
399
+ An error whose cell no longer shows what was found is tagged *stale*, never
400
+ moved.
401
+
402
+ ## Plugins
403
+
404
+ Features beyond review — every Excel number format, live formulas, cell
405
+ styles, data validation, conditional formatting, pictures, charts, comments —
406
+ are separate packages that plug into six hooks and six slots. With none
407
+ installed the grid is the plain review tool. **See each one on the same
408
+ workbook:** the demos' plugin gallery (`just demo-html` or `just demo-react`)
409
+ has a tab per plugin, plus Vanilla and All plugins.
410
+
411
+ ```js
412
+ import '@sheetpane/numfmt'; // registers "numfmt"
413
+ ```
414
+
415
+ ```html
416
+ <sheetpane-excel pane="q3" plugins="numfmt"></sheetpane-excel>
417
+ ```
418
+
419
+ Or build the store yourself: `el.workbook = createWorkbook({ plugins: [...] })`.
420
+
421
+ ## Styling
422
+
423
+ Colours, spacing and radii are `--sp-*` custom properties on the elements
424
+ (`--sp-accent`, `--sp-ink`, `--sp-line`, …); `@sheetpane/grid/style.css`
425
+ lists the defaults, and `theme="dark"` swaps in the dark set. The font is yours: set it on the element or any ancestor.
426
+
427
+ The specs are the records [SR-GRID](../../records/0105_GRID.md) and
428
+ [SR-ERRORS](../../records/0106_ERRORS.md).
429
+
430
+ **Using an AI coding agent?** `npx sheetpane-skills install` puts sheetpane's skills for Claude Code, Kiro and Codex into your repository.