@sheetpane/grid 0.3.0 → 0.4.1

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 (74) hide show
  1. package/README.md +294 -13
  2. package/custom-elements.json +292 -27
  3. package/dist/check.d.ts +75 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +151 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/errors/element.d.ts +90 -12
  8. package/dist/errors/element.d.ts.map +1 -1
  9. package/dist/errors/element.js +572 -50
  10. package/dist/errors/element.js.map +1 -1
  11. package/dist/excel/element.d.ts +168 -15
  12. package/dist/excel/element.d.ts.map +1 -1
  13. package/dist/excel/element.js +752 -90
  14. package/dist/excel/element.js.map +1 -1
  15. package/dist/excel/link.d.ts +40 -0
  16. package/dist/excel/link.d.ts.map +1 -0
  17. package/dist/excel/link.js +90 -0
  18. package/dist/excel/link.js.map +1 -0
  19. package/dist/excel/styles.d.ts.map +1 -1
  20. package/dist/excel/styles.js +37 -5
  21. package/dist/excel/styles.js.map +1 -1
  22. package/dist/frameworks/elements.d.ts +57 -17
  23. package/dist/frameworks/jsx.d.ts +15 -9
  24. package/dist/frameworks/solid.d.ts +51 -11
  25. package/dist/frameworks/svelte.d.ts +28 -16
  26. package/dist/frameworks/vue.d.ts +15 -9
  27. package/dist/index.d.ts +9 -4
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/llms-full.txt +3042 -246
  32. package/dist/llms.txt +92 -86
  33. package/dist/pane/protection.d.ts +61 -0
  34. package/dist/pane/protection.d.ts.map +1 -0
  35. package/dist/pane/protection.js +89 -0
  36. package/dist/pane/protection.js.map +1 -0
  37. package/dist/pane/rules.d.ts +1 -1
  38. package/dist/pane/rules.d.ts.map +1 -1
  39. package/dist/pane/store.d.ts +336 -15
  40. package/dist/pane/store.d.ts.map +1 -1
  41. package/dist/pane/store.js +891 -44
  42. package/dist/pane/store.js.map +1 -1
  43. package/dist/pane/structure.d.ts +58 -0
  44. package/dist/pane/structure.d.ts.map +1 -0
  45. package/dist/pane/structure.js +238 -0
  46. package/dist/pane/structure.js.map +1 -0
  47. package/dist/pane/types.d.ts +195 -6
  48. package/dist/pane/types.d.ts.map +1 -1
  49. package/dist/pane/types.js +57 -0
  50. package/dist/pane/types.js.map +1 -1
  51. package/dist/plugins/types.d.ts +34 -6
  52. package/dist/plugins/types.d.ts.map +1 -1
  53. package/dist/plugins/types.js.map +1 -1
  54. package/dist/shared/detail.d.ts +24 -0
  55. package/dist/shared/detail.d.ts.map +1 -0
  56. package/dist/shared/detail.js +75 -0
  57. package/dist/shared/detail.js.map +1 -0
  58. package/dist/shared/dom.d.ts +3 -0
  59. package/dist/shared/dom.d.ts.map +1 -1
  60. package/dist/shared/dom.js +3 -0
  61. package/dist/shared/dom.js.map +1 -1
  62. package/dist/static.d.ts +81 -0
  63. package/dist/static.d.ts.map +1 -0
  64. package/dist/static.js +352 -0
  65. package/dist/static.js.map +1 -0
  66. package/dist/style.css +3 -3
  67. package/dist/tokens.d.ts +1 -0
  68. package/dist/tokens.d.ts.map +1 -1
  69. package/dist/tokens.js +2 -2
  70. package/dist/tokens.js.map +1 -1
  71. package/dist/vscode.css-custom-data.json +11 -1
  72. package/dist/vscode.html-custom-data.json +89 -2
  73. package/dist/web-types.json +204 -21
  74. package/package.json +12 -4
package/README.md CHANGED
@@ -205,17 +205,24 @@ committed manifest drifts from the elements.
205
205
  | attribute | `plugins` | the enabled plugin ids, e.g. `numfmt formulas` |
206
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
207
  | attribute | `theme` | `light` (default), `dark` or `auto` (follows the page's `prefers-color-scheme`) |
208
+ | attribute / property | `ref` | the cell or range selected once a file is read, scrolled into view: `B4`, `Data!B4`, `'Q3 data'!B4:D8` — see [Opening at a cell](#opening-at-a-cell-and-links) |
209
+ | attribute / property | `link` | `none` (default) or `hash`: the selection kept in the URL fragment, and *Copy link* in the cell menu |
208
210
  | attribute / property | `on-invalid` / `onInvalid` | what a failing column rule does by default: `flag` (default) or `block` |
209
211
  | attribute / property | `rules-src` / `rules` | a rules file by URL, or as an object — see [Rules file](#rules-file) |
210
212
  | attribute / property | `flagging` | `on` (default) or `off`: no Flag issue, no composer, no pop-up card — errors are read in `<sheetpane-errors>` |
213
+ | attribute / property | `results` | `summary` (default), `marks` or `off` — how passed and not-checked cells show, pane-wide; see [Results](#results-passed-and-not-checked) |
214
+ | attribute / property | `protection` | `honour` (default) or `ignore` — whether a protected sheet's locked cells refuse edits, pane-wide; see [Protected sheets](#protected-sheets) |
211
215
  | property | `columns`, `validate` | column rules by title, and a check function — see [Column rules](#column-rules) |
212
216
  | property | `file` | a `File`, `Blob`, `ArrayBuffer` or `Uint8Array` to read |
213
217
  | property | `workbook` | the pane's store (get, or set your own from `createWorkbook()`) |
214
218
  | property | `errors`, `errorTypes` | the review; the error-type catalog |
219
+ | property | `renderDetail` | your extra detail in the error card (W-68, under *Saving and restoring a review*) |
220
+ | attribute / property | `layers` | the review layers shown, space-separated — the pane's, shared with `<sheetpane-errors>`; none shows every layer (W-51, under *Saving and restoring a review*) |
215
221
  | method | `load(src, name?)` | read a file (any of the above) |
216
222
  | 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 |
223
+ | method | `select(sheet, ref)`, `getErrors({ layer? })`, `setErrors(list or envelope, { layer? })`, `undo()`, `redo()` | |
224
+ | method | `highlight(ranges, { tone, label, sheet, pinned, reveal }?)`, `clearHighlight()` | outline cells without selecting them — and `sheetpane-hover` — see *Pointing at cells* under [Following an edit](#following-an-edit) |
225
+ | event | `sheetpane-load` (`{ status: 'ready', name, kind, sheets, readMs, sha256 }`), `sheetpane-errors {v, errors, plugins, layers, changed}`, `sheetpane-layers {visible, layers}`, `sheetpane-mode {mode}`, `sheetpane-history`, `sheetpane-export {blob, kind, name, format, converted, sha256, 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` — `sha256` is the SHA-256 of the file opened / the blob handed back, `undefined` without Web Crypto (SP-1009) | bubbling, composed |
219
226
 
220
227
  ### Following an edit
221
228
 
@@ -226,11 +233,11 @@ every payload names the column `{ index, letter, header }` (the title from
226
233
  | event | fires | detail | cancel it to |
227
234
  |---|---|---|---|
228
235
  | `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 |
236
+ | `sheetpane-editstart` | the cell bar opens on a cell | `{sheet, ref, column, value, richText?}` — `richText: true` on a rich-text cell, whose edit saves plain text | keep the cell read-only |
230
237
  | `sheetpane-input` | each keystroke, before saving | `{sheet, ref, column, draft, was}` | — |
231
238
  | `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}`) | — |
239
+ | `sheetpane-change` | after saving | `{sheet, ref, kind, was, now}` (an insert or delete: `kind: 'structure'`, `op`, `at`, `count`) | — |
240
+ | `sheetpane-editcancel` | Esc, a column rule set to `block` refused the value, or the cell is locked on a protected sheet | `{sheet, ref}` (refused: `{sheet, ref, reason: 'blocked', code: 'SP-6003', message}`; locked: `{sheet, ref, reason: 'locked', code: 'SP-3026', message: 'Locked in the file'}`) | — |
234
241
  | `sheetpane-sheet` | the sheet tab changes | `{sheet}` | — |
235
242
 
236
243
  A refused value never reaches the change map, undo or a plugin.
@@ -243,8 +250,8 @@ excel.addEventListener('sheetpane-beforechange', (e) => {
243
250
 
244
251
  **Keys:** arrows, Shift+arrows, Home/End, Ctrl+Home/End, PageUp/PageDown;
245
252
  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
253
+ **right-click, Shift+F10 or the Menu key** opens the selection actions (Flag issue, Copy ref, Copy link with
254
+ `link="hash"`, Copy value and plugin actions) under the cell — the same actions sit at the right end of the cell bar, never over the cells; Esc
248
255
  cancels or closes the pop-up; Ctrl/⌘+Z undo, Ctrl+Y / ⌘+Shift+Z redo. The `?`
249
256
  button lists them, plugins' shortcuts included.
250
257
 
@@ -268,6 +275,67 @@ excel.addEventListener('sheetpane-resize', () => localStorage.setItem('sizes', J
268
275
  excel.sizes = JSON.parse(localStorage.getItem('sizes') ?? '{}'); // { Sheet1: { cols: { B: 120 }, rows: { 4: 40 } } }
269
276
  ```
270
277
 
278
+ **Inserting and deleting rows and columns (Edit):** the cell menu and the ⋯ menu offer *Insert row above /
279
+ below*, *Insert column left / right*, *Delete row* and *Delete column* (with counts for a range); a header
280
+ menu offers its axis's three. Ctrl/⌘+Shift+= inserts rows above (columns left when whole columns are
281
+ selected); Ctrl/⌘+− deletes whole rows or columns. Everything that points at a cell moves — values, edits,
282
+ errors, formulas on every sheet (`#REF!` for a deleted target), merges, validations, conditional formats,
283
+ notes, pictures and charts, hidden and grouped rows, freeze, sizes — and the download carries it. Each is one
284
+ undo step and one `sheetpane-change` with `kind: 'structure'`, `op`, `at` (0-based) and `count`. What Excel
285
+ refuses is refused, greyed with its reason: the file's protection — a protected structure, or a protected
286
+ sheet without *Insert rows* / *Delete rows* / *Insert columns* / *Delete columns* (`SP-3026`) — a delete that
287
+ splits a merged range (`SP-3022`), anything that
288
+ would move a pivot table or its source (`SP-3023`), cells pushed off the sheet (`SP-3024`), a table cut
289
+ (`SP-3025`). A host drives the same through the store:
290
+
291
+ ```js
292
+ const book = excel.workbook;
293
+ if (!book.structureRefusal({ sheet: 'Spec', op: 'insertRows', at: 4, count: 1 })) book.insertRows('Spec', 4); // above row 5
294
+ book.deleteCols('Spec', 2, 2); // C and D
295
+ ```
296
+
297
+ **Pointing at cells from outside (W-56):** `sheetpane-hover` names the cell under a mouse or pen pointer —
298
+ `{sheet, ref, column}`, a merge by its top-left, at most once per frame and only when the cell changes —
299
+ and `null` when the pointer leaves the cells; a finger never hovers. `highlight(ranges, options?)` outlines
300
+ cells without selecting them: `B3`, `B3:D8`, `Data!B3` or `{ ref, sheet?, tone? }`, in `tone` `info`
301
+ (default), `pass` or `fail`. The outline lets the pointer through; it is not a change, not undone, not in the
302
+ download. Each call replaces the last; a sheet switch clears it unless `pinned: true`, and so does a new
303
+ file; `reveal: true` scrolls the first range into view; a `label` is shown in the status line and said once
304
+ by screen readers. It returns how many ranges it outlined (a bad ref or a missing sheet is skipped);
305
+ `clearHighlight()` removes it. Together they make "hover a box, its cell lights up — hover a cell, its box
306
+ does":
307
+
308
+ ```js
309
+ excel.addEventListener('sheetpane-hover', (e) => pdf.outline(e.detail && boxFor(e.detail.ref))); // null: none
310
+ pdf.onHover = (box) => (box ? excel.highlight(box.cell, { tone: box.ok ? 'pass' : 'fail', label: box.name }) : excel.clearHighlight());
311
+ ```
312
+
313
+ ### Opening at a cell, and links
314
+
315
+ `ref` opens the file at a cell — the selection once it is read, scrolled into view; set later, it selects
316
+ then. A cell or range, with or without its sheet (`B4`, `Data!B4`, `'Q3 data'!B4:D8`). One that names no
317
+ cell warns `SP-3019` in the console and the sheet opens at A1.
318
+
319
+ ```html
320
+ <sheetpane-excel pane="q3" src="/files/q3.xlsx" ref="Data!F9"></sheetpane-excel>
321
+ ```
322
+
323
+ `link="hash"` keeps the selection in the URL, so a reload or a shared link opens at the same cell, and adds
324
+ *Copy link* to the cell menu. It is off by default. Only one parameter of the fragment is sheetpane's —
325
+ `sheetpane=<pane>:<Sheet>!<ref>` — and the rest is left as it was: after a hash router's path it goes with
326
+ `?` (`#/reports/12?sheetpane=q3:Data!F9`), after an anchor or your own parameters with `&`. It is written
327
+ with `history.replaceState` once the selection settles — no history entry, no `hashchange` for your router
328
+ — and read when a file is read and on back and forward. A link wins over `ref`. Give each sheet its own
329
+ `pane` when a page has two. The fragment never leaves the browser: it is not part of any request.
330
+
331
+ **React, Vue and Solid** keep `ref` for their own refs, so their typings leave it out. Set it from a ref
332
+ callback, or call `select()` once the file is read:
333
+
334
+ ```jsx
335
+ <sheetpane-excel pane="q3" src={url} ref={(el) => el?.setAttribute('ref', 'Data!F9')} />
336
+ <sheetpane-excel pane="q3" src={url} onsheetpane-load={(e) => e.detail.status === 'ready' && e.currentTarget.select('Data', 'F9')} />
337
+ ```
338
+
271
339
  ## Column rules
272
340
 
273
341
  Check what a user types, by column title. Rules are plain JSON — save and load
@@ -377,28 +445,241 @@ opens no pop-up — the errors panel is the one place errors are read.
377
445
  excel.rules = parse(await (await fetch('./sheetpane.rules.yaml')).text());
378
446
  ```
379
447
 
448
+ ## The same checks with no page (`@sheetpane/grid/check`)
449
+
450
+ `check()` runs what the pane runs on load — the rules file, column rules, an
451
+ async `validate`, and each plugin's `validate` after a `derive` plugin has
452
+ recalculated — in Node, a worker or a page, and returns the errors the pane
453
+ would hold. The entry imports no element and touches no DOM.
454
+
455
+ ```js
456
+ import { readFile } from 'node:fs/promises';
457
+ import { check } from '@sheetpane/grid/check';
458
+ import { validation } from '@sheetpane/validation/check';
459
+
460
+ const errors = await check(await readFile('q3.xlsx'), {
461
+ name: 'q3.xlsx',
462
+ rules: JSON.parse(await readFile('sheetpane.rules.json', 'utf8')),
463
+ plugins: [validation()], // objects, or ids registered with registerPlugin
464
+ });
465
+ ```
466
+
467
+ - **Options are the element's:** `rules` (an object, or its JSON text),
468
+ `columns`, `validate(ctx, signal)`, `onInvalid`, `headerRow`, `plugins`, and
469
+ the file's `name` (`.csv` reads CSV; unnamed bytes are sniffed as `load()`
470
+ does).
471
+ - **The same errors** as `<sheetpane-excel>` given the same file and settings
472
+ — only `id` and `createdAt` are per run — sorted by sheet, row and column.
473
+ A file that cannot be read throws its reader's code (SP-10xx); anything but
474
+ bytes throws SP-3020.
475
+ - The store, the rules checker and the plugin contract are exported here too
476
+ (`createWorkbook`, `checkDocument`, `registerPlugin`, the types), for a
477
+ DOM-free plugin or a host's own pipeline. `loadWorkbook(bytes, options)`
478
+ takes the same options and returns the loaded store, every check finished,
479
+ instead of its errors.
480
+
481
+ ## A range as static HTML (`@sheetpane/grid/static`)
482
+
483
+ `renderStatic(book, { sheet, range, errors, theme, headers })` returns one
484
+ self-contained `<table>` — the text the pane shows (the `format` plugin), the
485
+ look it draws (`decorate` plugins, merges, the theme's tokens as inline hex),
486
+ hidden rows and columns left out, and the errors marked — for a report or an
487
+ email. Made with no DOM, in Node, a worker or a page.
488
+
489
+ ```js
490
+ import { readFile, writeFile } from 'node:fs/promises';
491
+ import { loadWorkbook, renderStatic } from '@sheetpane/grid/static';
492
+ import { numfmt } from '@sheetpane/numfmt';
493
+ import { styles } from '@sheetpane/styles';
494
+ import { condfmt } from '@sheetpane/condfmt';
495
+ import { validation } from '@sheetpane/validation/check';
496
+
497
+ const book = await loadWorkbook(await readFile('q3.xlsx'), { name: 'q3.xlsx', rules, plugins: [numfmt(), styles(), condfmt(), validation()] });
498
+ await writeFile('report.html', renderStatic(book, { sheet: 'Holdings', range: 'A1:F20' }));
499
+ ```
500
+
501
+ - **`book`** is a loaded store: `loadWorkbook()`'s, `createWorkbook()`'s after
502
+ `load()`, or an element's `workbook` — its changes and derived values show.
503
+ `sheet` defaults to the selected one, `range` to the used range (≤ 200,000
504
+ cells), `errors` to the visible layers' (`[]` marks nothing; pass `check()`'s
505
+ or a restored review), `theme` to `light` (`dark` too), `headers` to true.
506
+ - **Marks:** a ring and a tint as the grid draws them, and a badge with the
507
+ glyph (✕ ! i ✓, `A` for an accepted deviation) and a number that the footer
508
+ explains — severity, range, type, message, *should be*, the acceptance's
509
+ reason. Never colour alone. Errors about the whole sheet or the workbook
510
+ head the footer and mark no cell. Results (✓ passed, — not checked) show
511
+ as the grid's corner glyph only when the pane's `results` level is
512
+ `marks`. Rich text keeps its runs (italic, bold, colour), never a font.
513
+ - **Inert:** no script, no event attribute, no `class`, no stylesheet, no URL
514
+ (a `decorate` value that loads or escapes is dropped), every value escaped,
515
+ no font family — the reader's font is the font.
516
+ - **Not drawn:** what an `overlay` plugin draws (pictures, charts) — the
517
+ footer says so and `data-sp-skipped` names the plugins. Interactive cell
518
+ controls (a list arrow) are not drawn either.
519
+ - Refusals: SP-3027 when `book` is not a loaded workbook; SP-3028 for an
520
+ unknown sheet, a range that is not A1, or too many cells.
521
+
380
522
  ## `<sheetpane-errors>`
381
523
 
382
524
  | kind | name | what |
383
525
  |---|---|---|
384
526
  | 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 |
527
+ | attribute | `readonly` | hides the form, Resolve, Accept… and Delete — a sign-off screen |
528
+ | attribute / property | `results` | as above; the panel's *On sheet* switch moves the pane between `summary` and `marks` |
529
+ | property / method | `errors`, `errorTypes`, `getErrors({ layer? })`, `setErrors(list or envelope, { plugins?, layer? })` | |
530
+ | property | `renderDetail` | your extra detail in an open row — see below (W-68) |
531
+ | attribute / property | `layers`; `layerNames` | the review layers shown (as on the sheet); what the switch calls each, e.g. `{ pdf: 'PDF check' }` (W-51) |
532
+ | event | `sheetpane-errors {v, errors, plugins, layers, changed}`, `sheetpane-layers {visible, layers}`, `sheetpane-reveal {error}` | **Show in sheet**: switch to the sheet's tab or open the pop-up |
388
533
 
389
534
  ## Saving and restoring a review
390
535
 
391
536
  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:
537
+ `sheetpane-errors`, store its `detail` — the envelope `{ v: 2, errors,
538
+ plugins, layers, changed }` — where you like, and give it back after the
539
+ file loads:
540
+
541
+ ```js
542
+ excel.addEventListener('sheetpane-load', () => errors.setErrors(saved));
543
+ ```
544
+
545
+ A review saved before W-51 (`{ errors, plugins }`) restores the same way, into
546
+ the `review` layer.
547
+
548
+ **Review layers** (W-51): several sets of errors in one pane — say a
549
+ validation run and a PDF check — each replaced whole without touching the
550
+ others. Every error has a `layer` (`review` unless named):
394
551
 
395
552
  ```js
396
- excel.addEventListener('sheetpane-load', () => errors.setErrors(saved.errors, { plugins: saved.plugins }));
553
+ errors.setErrors(validationFindings, { layer: 'validation' });
554
+ errors.setErrors(pdfFindings, { layer: 'pdf' }); // validation is kept
555
+ errors.layerNames = { pdf: 'PDF check' };
556
+ errors.layers = ['pdf']; // or layers="pdf" on either element; null shows all
397
557
  ```
398
558
 
559
+ With two or more layers the panel's header shows a switch — **All**, then
560
+ each layer with its open count. What is shown is the pane's: the sheet's
561
+ pins, marks and counts, the list and the comments an export writes all
562
+ follow it (`export({ layers: [...] })` names another set). Replacing a layer
563
+ keeps undo; a person's new error goes into the layer they are looking at.
564
+ Names are ids: 1–64 characters, no spaces (SP-3021).
565
+
399
566
  An error whose cell no longer shows what was found is tagged *stale*, never
400
567
  moved.
401
568
 
569
+ **An error with no cell** (W-55): `range: ""` is about the whole sheet, and
570
+ `sheet: ""` with `range: ""` about the whole workbook — "no Pages List", "a
571
+ listed tab has no sheet". Add one with `setErrors()`, `workbook.addError()` or
572
+ a `validate` hook (a whole-sheet draft only). The list shows them first — a
573
+ *Workbook* group, then each sheet's — above the cell errors; a whole-sheet
574
+ error is counted on its tab and **Show in sheet** opens the tab; for a
575
+ workbook-level one it is disabled. They are never placed on A1; no pin, no
576
+ cell mark, no `found`. `errorScope(e)` says which (`cell` · `sheet` ·
577
+ `workbook`). **If your code reads `range` as an A1 reference, handle `""`.**
578
+
579
+ **Your data on an error, and your detail beside it** (W-68): `meta` holds any
580
+ JSON of yours (64 KiB at most, `ERROR_META_MAX`); it is saved and restored
581
+ with the error and never written into the file. `renderDetail(error)` — on
582
+ `<sheetpane-excel>` for the error card and on `<sheetpane-errors>` for the
583
+ open row, each its own — returns a string (shown as text, never HTML), a
584
+ Node (yours, placed as it is) or `null`. It is drawn after the message, in
585
+ `::part(detail)`, and runs again only when that error changes.
586
+
587
+ ```js
588
+ workbook.addError({ sheet: 'Spec', range: 'C7', type: 'out-of-range', severity: 'error', meta: { page: 3, measured: '6.63 in' } });
589
+ const detail = (e) => (e.meta?.measured ? `The PDF measures ${e.meta.measured}, outside ± 0.06 in` : null);
590
+ excel.renderDetail = detail;
591
+ errors.renderDetail = detail;
592
+ ```
593
+
594
+ A `meta` that is not JSON or is over the cap is dropped (SP-3016); a hook
595
+ that throws shows nothing (SP-3017).
596
+
597
+ **Accepted deviations** (W-69): a known, approved difference can be accepted
598
+ instead of deleted. **Accept…** in an open row of `<sheetpane-errors>` asks
599
+ for a reason (required) and records it with who (the element's `author`) and
600
+ when: `status: 'accepted'`, `acceptedBy`, `acceptedAt`, `reason`. Accepted
601
+ errors stay in the review and the download but are not counted — not in
602
+ "n open", the severity chips or the sheet's counts — and the **Accepted n**
603
+ chip lists them. Reopen takes an acceptance back; so does undo. From code:
604
+
605
+ ```js
606
+ workbook.acceptError(id, 'Approved by the customer on 18 Oct', { by: 'Ravi' });
607
+ ```
608
+
609
+ A blank reason is refused (SP-3018). Count open problems with
610
+ `status === 'open' && !isResult(e)`. With `@sheetpane/comments`, the export adds a reply
611
+ *Accepted: <reason>* to the error's comment thread.
612
+
613
+ **The Edits view** (W-64): an *Edits n* chip beside *Problems* lists every
614
+ change in the pane, newest first — the cell or range and kind, *was → now*,
615
+ the time, and the errors that edit cleared or raised (each a link to the
616
+ error). Picking a row selects the cell; **Revert** on a row and **Revert
617
+ all** are each one undo step. It shows in Edit mode, and in View mode while
618
+ there are edits. The store gives the same list to your code:
619
+
620
+ ```js
621
+ const edits = workbook.edits(); // [{ key, sheet, ref, kind, was, now, at }], newest first
622
+ workbook.editEffects(edits[0].key); // { cleared, raised, matched } — error ids
623
+ workbook.revertEdits([edits[0].key]); // omit the keys to revert all
624
+ ```
625
+
626
+ ## Results: passed and not checked
627
+
628
+ A host that checks a sheet can report what passed as well as what failed:
629
+ an entry with `severity: 'pass'` (✓) or `'skipped'` (— not checked; its
630
+ `message` says why). Give them with `setErrors()` or from a plugin's
631
+ `validate`. Results stay quiet:
632
+
633
+ | `results=` | errors panel | sheet |
634
+ |---|---|---|
635
+ | `off` | results are dropped on arrival | as without results |
636
+ | `summary` (default) | one line — *1 problem · ✓ 180 passed · — 28 not checked · On sheet* — and *Passed* / *Not checked* chips; the list opens on *Problems* | nothing marked; the status line says *186 of 214 checked* |
637
+ | `marks` (or *On sheet*) | the same, switch on | a 9 px ✓ or — in each result cell's bottom-left corner |
638
+
639
+ Passes are listed grouped by check (`type` + `message`), not-checked cells
640
+ by reason (`message`); a group opens to its cells, and **Show ▸** selects its
641
+ range and marks it even at `summary`. Selecting a result cell names its check
642
+ in the cell bar. A problem on the same cell always wins. Tab counts, the
643
+ toolbar counts, the panel's *n open* and the severity filters count problems
644
+ only, and a download leaves results out (`@sheetpane/comments` writes them
645
+ only with `export({ includeResults: true })`). A host that switches on
646
+ `severity` meets two more values.
647
+
648
+ ```js
649
+ errors.setErrors([
650
+ { v: 1, id: 'ERR-0001', sheet: 'Holdings', range: 'H3', type: 'fee-rate', severity: 'pass', message: 'Fee rate 0.00%–2.00%',
651
+ found: '0.35%', expected: '', cellChanged: false, status: 'open', matchesExpected: false, author: 'host', createdAt: new Date().toISOString() },
652
+ ]);
653
+ ```
654
+
655
+ ## Protected sheets
656
+
657
+ What a file protects stays protected in Edit, with no plugin. On a protected
658
+ sheet (Excel: Review › Protect Sheet) a locked cell — every cell is locked
659
+ until someone unlocks it — shows its value and *🔒 Locked in the file* in the
660
+ cell bar; typing, paste and Delete there are refused with
661
+ `sheetpane-editcancel` `{ reason: 'locked', code: 'SP-3026' }`, before
662
+ `sheetpane-beforechange`. Unlocked cells edit as usual, and a paste or a
663
+ range Delete writes those only, saying how many locked cells it left. A
664
+ formula marked *Hidden* shows its value, never its formula. A workbook whose
665
+ structure is protected keeps its sheets and its rows and columns: Hide and
666
+ Unhide sheet, and every insert and delete, are disabled with the reason; a
667
+ protected sheet refuses the inserts and deletes it does not allow, as Excel
668
+ does (`SP-3026`).
669
+
670
+ ```js
671
+ const wb = excel.workbook;
672
+ wb.isLocked('Spec', 'A2'); // true: a locked cell on a protected sheet
673
+ wb.isFormulaHidden('Spec', 'E2'); // true: shows its value only
674
+ wb.structureLocked(); // true: <workbookProtection lockStructure="1">
675
+ wb.sheetProtection('Spec'); // { formatCells: true, …, hasPassword: true } — never the password
676
+ ```
677
+
678
+ sheetpane never reads, checks or removes a protection password, and the
679
+ download carries the file's protection exactly as it was. `protection="ignore"`
680
+ sets it aside for the pane — **the host's decision**: a host that lets people
681
+ edit what the file locks owns that choice.
682
+
402
683
  ## Plugins
403
684
 
404
685
  Features beyond review — every Excel number format, live formulas, cell