# Nori 0.2.0: complete documentation Generated from repository Markdown. Refer to /nori/docs-manifest.json for per-page sources and hashes. --- Source: docs/agents.md Page: /nori/agents.html Markdown: /nori/markdown/agents.md # Build with an AI agent This guide describes the implemented Nori 0.1.0 API. It is an integration contract, not a promise of future Excel parity. ## Retrieval entry points - [llms.txt](/nori/llms.txt): compact reading order and per-page links. - [llms-full.txt](/nori/llms-full.txt): all documentation with code includes expanded. - [docs-manifest.json](/nori/docs-manifest.json): package version, routes, source paths and SHA-256 content hashes. - [Public declaration index](/nori/types/index.d.ts): generated root API; relative declaration files are served beside it. CSV declarations are at `types/csv.d.ts`. Other entries include `/types/react.d.ts`, `/types/core.d.ts`, `/types/model.d.ts`, `/types/xlsx.d.ts`, `/types/formula.d.ts`, `/types/pivot.d.ts`. - Every page is available under `/markdown/PAGE.md`, for example React source. These downloads are generated from the same source as the rendered site, with no navigation markup. The published documentation is at [byfungsi.github.io/nori](https://byfungsi.github.io/nori/). Resolve artifact paths below the site base (`/nori/` on GitHub Pages, `/` for local preview); generated indexes and manifests include that base. Local preview runs at `http://127.0.0.1:4179`. ## Integration rules 1. Import only from `@byfungsi/nori` or its documented subpaths. Never depend on internal workspace names, compiled chunk paths, or private schemas. 2. Check `result.ok` before accessing `.value`. Parsers, workbook creation, commands and pivots can fail. Do not treat them as promises; XLSX parsing is synchronous. 3. Distinguish `WorkbookSnapshot` (serializable immutable document) from `Workbook` (live runtime). Mutate through `dispatch`; never assign into snapshot cells. 4. Obtain branded sheet IDs from validated snapshots/runtime state, or use `parseSheetId`. Do not cast arbitrary strings to IDs. 5. Cell formula text omits the leading `=` in the model. Set `{value:null,formula:'SUM(A1:A2)'}`. UI editors accept the leading equals sign. 6. Read calculated values through `getCellValue`, not cached snapshot values. Results may be scalars, tagged errors or arrays. 7. Keep runtime identity stable in React. Prefer `useState` with a lazy initializer; do not create a new workbook on every render. 8. `readOnly` is a React view policy. Host-owned mutation controls must obey it; server permissions belong to the host. `SpreadsheetPreview` never edits. 9. Keep `File`, filesystem, DOM, workers and native APIs outside headless packages. Future native work should reuse core rather than importing `/react`. 10. Read import warnings and the support matrix. Do not invent `writeXlsx`, `createWorkbookStore`, `FormulaEngine`, dynamic spilling, filter menus, or React Native exports. ## Task-oriented reading order | Task | Required context | | ------------------------ | ---------------------------------------------------------------- | | Embed a sheet | Quick start → React → Interactions | | Import attachments | Importing → Support → API import section | | Build chat attachments | Preview → React read-only policy | | Extend calculations | API formula section → Support → Architecture calculation section | | Change library internals | Architecture → Contributing → affected package tests | | Build pivots | Recipes → API pivot section | ## Validation checklist for generated integrations - Typecheck imports against the public package, not editor autocomplete alone. - Exercise malformed input and Result error branches. - Verify formula values after edits and undo. - Unsubscribe host listeners on teardown. - Confirm read-only actions do not mutate snapshots. - Preserve sparse storage, styles and merge anchors when issuing commands. - State limitations explicitly; current support and roadmap are separate documents. The complete quick-start and React examples are checked against source types on every repository typecheck. Quick-start calculations and persistence are executed in tests. Production builds validate site links, and the docs verifier checks plain-text exports and declaration availability. For CSV, read [CSV import](/nori/csv.html). Use `parseCsv` from `/csv` or the root, check its Result, then construct the runtime from `.value.snapshot`. Text mode is the default. Do not auto-evaluate CSV strings beginning with `=`; the adapter deliberately keeps them literal. --- Source: docs/api.md Page: /nori/api.html Markdown: /nori/markdown/api.md # Public API notes ## Model `parseWorkbookSnapshot(unknown)` validates, copies, and freezes version 1 snapshots. Every sheet has a stable branded ID, unique case-insensitive name, logical extents, and an A1-keyed sparse cell record. Cell values are number/string/boolean/null or a tagged spreadsheet error. Formula text and workbook styles are optional. `createEmptySnapshot()` makes a 100 × 26 empty sheet. `parseAddress('$b$12')` returns zero-based `{ row: 11, column: 1 }`; `addressOf(position)` encodes already-valid coordinates. Addresses are bounded to XFD1048576. For arbitrary numeric inputs, validate coordinates through the model boundary before calling `addressOf`. `parseSheetId` refines external IDs; most callers obtain IDs directly from parsed snapshots. `formatNumber(value, numberFormat)` from `@byfungsi/nori/model` formats the supported numeric/currency subset without React. It returns `undefined` for unsupported formats; see [Currency formatting](/nori/currency.html). ## Import ```ts const result = parseXlsx(bytes, { maxInputBytes: 20_000_000, maxUncompressedBytes: 100_000_000, maxCells: 200_000, }); ``` The same values are defaults. Limits must be positive safe integers. Limits bound ordinary archives using their declared ZIP sizes; this synchronous adapter is not an adversarial archive sandbox. Applications accepting hostile documents should isolate CPU/memory work and enforce their own upload policy. A successful result is `{ snapshot, warnings }`. Failure is `XlsxError` with `reason: 'invalid-file' | 'unsupported' | 'limit'`. Never discard warnings in a fidelity-sensitive import workflow. ## Runtime ```ts const created = createWorkbook(snapshot, { historyLimit: 100, // functions: an optional function registry copied at construction }); ``` - `getState()` is referentially stable until a state transition. Subscribe callbacks are synchronous and called once per successful commit. - `subscribe(listener)` returns an unsubscribe function. Keep listeners nonthrowing. - `dispatch(command)` returns a Result. Set cells with explicit scalar input or `{ value: null, formula: 'SUM(A1:A3)' }`. `clearCell` deletes a sparse entry. Set cells outside current extents grow the extents, within Excel bounds. - `undo()` / `redo()` return false when unavailable. - `getCellValue(sheetId, address)` returns current calculated scalar/array data. Snapshot cell values are caches, not authoritative calculated output. - `setActiveSheet(sheetId)` clears selection. `setSelection(selection | null)` sets explicit corners; state exposes normalized `range`, `anchor`, and `focus`. `selectCell(sheetId, position, { extend: true })` extends from the current anchor. `moveSelection(direction, { extend, bounds })` supports platform-neutral arrow navigation and skips hidden/filtered entries. None of these operations enter document history. - `exportSnapshot()` returns immutable JSON-compatible model data. Pass parsed JSON back to `createWorkbook` for safe rehydration. `getVisibleRange(layout, viewport, extent)` computes an inclusive rectangle from logical units; no pixel, DOM, canvas, or native measurement objects cross its contract. ## Formula ```ts import { createFunctionRegistry, parseFormula, evaluateFormula, } from "@byfungsi/nori/formula"; import { cellError } from "@byfungsi/nori/model"; const functions = createFunctionRegistry(); functions.set("DOUBLE", (args) => typeof args[0] === "number" ? args[0] * 2 : cellError("#VALUE!"), ); const ast = parseFormula("DOUBLE(21)"); if (ast.ok) { const result = evaluateFormula( ast.value, { cell: () => cellError("#REF!"), range: () => cellError("#REF!"), }, functions, ); // 42 } ``` Function names are uppercased by the parser, so register uppercase names. `IF` and `IFERROR` are reserved evaluator-owned lazy forms. Changing the source registry after workbook construction does not change that workbook. `collectReferences` returns syntactic references, retaining compact ranges and both conditional branches. `DependencyGraph` manages opaque keys and transitive dependent traversal; core owns workbook-specific key construction. ## Pivot ```ts const result = createPivot( [ { region: "West", revenue: 100 }, { region: "West", revenue: 50 }, ], { rows: ["region"], values: [{ id: "total", field: "revenue", aggregate: "sum" }], }, ); // result.value.groups[0] = { key: ['West'], values: { total: 150 } } // result.value.totals = { total: 150 } ``` Rows may contain multiple grouping fields. Groups retain first-seen order; numeric/string keys remain distinct. All configured fields must exist in every record. Numeric measures ignore text, blanks, booleans, and error values. Count counts nonblank, non-error values, including text. Empty numeric groups produce sum/count 0 and average/min/max null. No grouping fields produces one group for nonempty data. Grand totals aggregate original rows (an average of all records, not an average of group averages). Source records are typed semantic input; parse less-trusted external records before calling this API. Measures must have unique IDs. Results are immutable. ## Layout and merge commands ```ts workbook.dispatch({ type: "resizeColumn", sheetId, column: 1, width: 240 }); workbook.dispatch({ type: "resizeRow", sheetId, row: 3, height: 64 }); // null removes an override and restores the sheet default. workbook.dispatch({ type: "resizeColumn", sheetId, column: 1, width: null }); workbook.dispatch({ type: "mergeCells", sheetId, range: { start: { row: 0, column: 0 }, end: { row: 1, column: 2 } }, }); workbook.dispatch({ type: "unmergeCells", sheetId, range }); ``` Sizes are positive logical units up to 10,000. UI gestures clamp to a practical minimum of 40 for columns and 20 for rows; imported smaller dimensions are retained. `getColumnWidth` and `getRowHeight` resolve overrides and defaults. `getVisibleRange` accepts sparse `columnWidths`/`rowHeights` in addition to uniform base dimensions. `SheetSnapshot.merges` contains nonoverlapping normalized inclusive rectangles. Merge commands reject single cells, out-of-bounds or partial-overlap ranges, and any content outside the new top-left anchor. Explicit merge commands can combine fully contained existing merges only when the content invariant holds. Unmerge removes every region intersecting the given range. All errors leave the document unchanged. `SheetSnapshot.frozen` stores leading row/column counts. `hiddenRows` and `hiddenColumns` are sorted, unique, zero-based indexes. `autoFilter` stores a range and semantic value-list or custom comparison criteria; `sort` stores value-sort columns and directions. `getVisibleRows(sheet, { start, end, limit, read })` accepts a scalar reader for current formula values; without it, it uses snapshot caches. Hidden rows explicitly saved in the file remain hidden even if an edited value later matches a filter. --- Source: docs/architecture.md Page: /nori/architecture.html Markdown: /nori/markdown/architecture.md # Architecture ```mermaid graph TD facade["@byfungsi/nori facade"] --> react["React adapter"] facade --> xlsx["XLSX adapter"] facade --> core["Headless core"] facade --> pivot["Pivot"] facade --> formula["Formula"] facade --> model["Model"] react --> core react --> model react --> formula core --> formula core --> model formula --> model pivot --> model csv["CSV adapter"] --> model facade --> csv xlsx --> model ``` Arrows are allowed source dependencies, not a claim that the root export loads React. The root facade exports only model, XLSX, and runtime APIs. Only `/react` exports the renderer. A boundary checker enforces the graph; a packed-consumer bundler test verifies runtime isolation. ## Owners and boundary decisions This is a new repository, with no existing owners to extend. Each package has a separate reason to change: - **Model** owns versioned, sparse, serializable `WorkbookSnapshot` data and validation. Removing this boundary would make parser output and runtime state drift apart. Zod refines and copies external data, brands `SheetId`, and freezes nested snapshot structures. Coordinates remain zero-based indexes, and sparse keys are canonical A1 strings. Version 1 is the only accepted version. - **XLSX** owns ZIP and XML mechanics. `fflate` and `fast-xml-parser` remain private implementation dependencies. Its public result contains only a canonical snapshot and semantic import warnings. It accepts bytes, never paths, `File`, XML nodes, ZIP entries, or browser APIs. Removing this adapter would leak file-format decisions into every renderer and runtime. - **Core** owns mutable runtime state behind the `Workbook` interface: atomic command commits, immutable revisions, history, listeners, active sheet, selection, calculation caches, and the dependency graph. The snapshot does not contain any of these services. Renderers cannot mutate the model directly. A separate store facade was considered but adds forwarding without new ownership, so `Workbook` itself is the store. - **Formula** owns pure parsing/evaluation and registry contracts. It resolves references through a supplied callback contract, so it does not depend on core or OOXML. ASTs retain absolute-reference flags. Results distinguish scalars from arrays. This boundary permits formula evaluation outside a spreadsheet and future alternative runtimes. - **Pivot** owns semantic field grouping and measures; the host supplies records and renders results. It does not know about cells, UI widgets, XLSX pivot caches, or React. Keeping it independent allows use on native platforms and server data. - **React** owns DOM, React subscription lifetimes, interaction translation, and CSS mapping. It subscribes through `useSyncExternalStore`, translates edits into commands, and leaves calculation/state ownership in core. A future React Native adapter can consume the same runtime and model. - **Facade** is the intentional public re-export boundary. Private packages are bundled into its ESM distribution and declaration files. External runtime dependencies remain declared in its manifest; no private workspace package needs publishing. ## Mutation and history Commands include `setCell`, `clearCell`, `renameSheet`, `resizeColumn`, `resizeRow`, `mergeCells`, `unmergeCells`, and atomic batches. Commands build a candidate snapshot and validate it before any state is committed. Invalid batches produce no partial changes, notifications, or history entries. Successful batches create one history entry and one revision notification. Mutating after undo clears redo. History is bounded to 100 entries by default and can be disabled with `historyLimit: 0`. Active-sheet and selection changes are transient state, do not enter document history, and are not exported. Selection coordinates are copied and bounds-checked. State retains a stable anchor, a moving focus, and the normalized rectangle expanded over whole merged regions. Pointer gestures are owned by React; selection extension, navigation and merge closure are platform-neutral. Sheet renaming is currently rejected when any formulas exist, because safe formula rewriting has not shipped. Sheet insertion/deletion/reorder are not implemented. History keeps immutable snapshots, which favors correctness and simplicity over large-workbook memory efficiency. Snapshot validation and copying are O(stored cells) per command. Command inverses and structural sharing are planned. ## Calculation Core parses formula text into ASTs and constructs a forward/reverse dependency index after each atomic edit or history transition. Ranges expand into edges up to 100,000 cells. Values are evaluated lazily and memoized per workbook revision. A visiting set detects cycles. Cross-sheet names resolve case-insensitively. Blank references return null; arithmetic treats blanks as zero. Missing sheets return `#REF!`. This first milestone deliberately rebuilds the graph and invalidates all cached values on document edits. `DependencyGraph.affectedBy` provides the tested traversal needed for incremental recalculation, but incremental scheduling is not yet connected. Transient selection changes preserve calculation caches. Range evaluation is capped at 100,000 cells, dependency depth at 256, expression nesting at 128, and formula length at 8192 characters. Over-budget evaluation returns `#NUM!`; over-budget syntax returns a parse error. Arrays are evaluation results, not mutations. Reading a range returns a rectangular array; using that array in a scalar binary operator currently returns `#VALUE!`. There is no spill allocation or implicit intersection. Exported snapshots retain imported/command-provided cached values; authoritative current values come from `getCellValue`. Export is canonical JSON, not an XLSX writer. ## Host adapters and React Native Core compiles with `lib: ["ES2022"]`, `types: []`; it has no DOM, Node, filesystem, browser-worker, or timer assumptions. The demo is the browser composition root and converts `File.arrayBuffer()` to bytes. A native host can provide bytes and use the same parser/runtime. File access, asynchronous scheduling, cancellation, platform text measurement, and workers belong in adapters. The synchronous parser should be scheduled outside latency-sensitive UI work for larger files. React Native binding, Metro packaging, and device performance have not been verified. ## Known tradeoffs This implementation uses a small Result union rather than introducing a full effect runtime. Expected boundary/command failures are typed values; internal invariants may throw. Custom formula functions are application-supplied pure functions; defects thrown by them or subscription listeners propagate to the host. One listener throwing can prevent later listeners from being called, after state is already committed. Neither callbacks nor malformed hand-constructed ASTs are treated as untrusted data; public text/snapshot parsers are the supported trust boundaries. ### Declaration portability decision Public canonical contracts are plain TypeScript interfaces, structurally checked against each private Zod schema using `satisfies z.ZodType`. This is a deliberate exception to deriving public types directly from schemas: the isolated consumer check found that inferred Zod declaration types exposed a `URL` ambient type requirement to headless consumers. Keeping validation implementation types private removes that platform leakage and shrinks the public declarations. The domain brand is local to Nori; a length-checked schema transform is its only cast. Future schema changes must preserve the structural checks and packed-consumer test. ## Document layout and read-only views Optional version-1 snapshot fields carry dimension defaults/overrides, merged rectangles, frozen leading row/column counts, hidden indexes, filter criteria, and saved sort metadata. Older snapshots remain valid. Covered merged cells must have null values and no formulas; style-only cells are allowed. Only the top-left cell holds content. Set/clear commands targeting a covered cell address edit the merge anchor, while formula references to covered cells remain blank. Merge commands never silently delete non-anchor content. Unmerge preserves the anchor and styles, and all layout commands support history and serialization. The renderer sets real column widths and row heights, clips cell content, and applies wrap metadata. Sticky frozen cells use sums of visible dimensions. Merges are projected into the rendered window, so a merge whose anchor is outside that window still displays its anchor value. Merges crossing a freeze boundary move as one unit on that axis, rather than being split into multiple independent cells. `getVisibleRows`, `getVisibleColumns`, `isRowVisible`, and `isColumnVisible` are headless view operations. They retain original sheet coordinates while excluding hidden/filtered entries. Saved sort metadata describes the order already physically stored in an XLSX: import does not re-sort values or rewrite formulas. The editor shows sort/filter indicators. A read-only `SpreadsheetPreview` subscribes to this same runtime but keeps its tab state local; it never calls runtime mutation/selection methods. The CSV adapter depends only on the model. It accepts decoded text, preserves quoted fields, and returns a one-sheet snapshot. Value inference is explicit and never creates formulas. File decoding remains in the host adapter. --- Source: docs/contributing.md Page: /nori/contributing.html Markdown: /nori/markdown/contributing.md # Contributing and verification ## Local development ```sh npm ci npm run dev # React demo npm run docs:dev # Documentation, port 4179 ``` Package source lives in `packages/{model,xlsx,core,formula,pivot,react,nori}`. The facade is the only intended consumer package. `examples/react-demo` is the browser host, while `docs/` is the VitePress documentation application. ## Required verification ```sh npm run verify # Boundaries, typechecks, tests, package/demo builds, packed consumer npm run docs:build # Library declarations, agent exports, static docs, output checks ``` For browser checks, start the demo then run `npm run verify:browser`. To verify the documentation UI, build it, run `npm run docs:preview`, then run `npm run docs:verify-browser`. Set `NORI_BROWSER_EXECUTABLE` if agent-browser needs an explicit Chrome executable path. These commands use a locally installed agent-browser CLI; it is not needed to build the library or static documentation. ## Documentation ownership Edit Markdown in `docs/`. Keep implementation status in `support.md` distinct from future intentions in `roadmap.md`. Add new pages to the sidebar in `.vitepress/config.mts`. Complete examples live in `docs/examples`; the quick start is executed by `tests/docs.test.ts`, and examples are included in the workspace typecheck. The generator builds plain-text and agent artifacts into `docs/public`. Do not hand-edit generated files. `docs:build` refreshes package declarations first, expands code includes, records source/content hashes, and builds the site with dead-link validation. The generated directory, VitePress cache and build output are ignored by version control. ## Hosting `npm run docs:build` produces a static site at `docs/.vitepress/dist`. A root-hosted local build is the default. For the published GitHub Pages site, use: ```sh NORI_DOCS_BASE=/nori/ npm run docs:build ``` The base path is applied to site assets, navigation, the agent manifest and plain-text download links. Project-site builds use explicit `.html` page routes so direct links do not depend on server rewrites. The `pages.yml` workflow verifies the library, builds the docs, uploads the static artifact, and deploys to GitHub Pages on pushes to `main` or manual dispatch. Pull requests run verification without deployment. Pages uses the GitHub Actions source, `pages: write` and `id-token: write` deployment permissions, and the `github-pages` environment. No custom deployment credential is needed. The published site is [byfungsi.github.io/nori](https://byfungsi.github.io/nori/). All generated text/Markdown/type artifacts are served under the same `/nori/` prefix. The npm package is not automatically published by this workflow. The docs build also builds `examples/react-demo` with relative asset paths and embeds it on the playground page. The static app and synthetic XLSX samples are copied into the Pages artifact. Rebuild/restart `docs:dev` after changing playground code; documentation Markdown itself supports hot reload. --- Source: docs/csv.md Page: /nori/csv.html Markdown: /nori/markdown/csv.md # Importing CSV CSV import is available from the root package or `@byfungsi/nori/csv`. It produces one canonical worksheet and works without React, DOM, Node APIs, or an XLSX parser dependency. ```ts import { parseCsv } from "@byfungsi/nori/csv"; import { createWorkbook } from "@byfungsi/nori/core"; const imported = parseCsv("Region,Revenue\r\nWest,1200\r\nEast,850", { sheetName: "Revenue", valueMode: "infer", }); if (!imported.ok) { console.error( imported.error.reason, imported.error.offset, imported.error.message, ); } else { const created = createWorkbook(imported.value.snapshot); if (!created.ok) console.error(created.error.message); else console.log( created.value.getCellValue(created.value.getState().activeSheetId, "B2"), ); // 1200 } ``` ## Input and options `parseCsv(text, options?)` accepts decoded text and returns `Result`. A successful `CsvImport` contains `snapshot`. It does not contain XLSX-specific warnings. In a browser, decode a file with `await file.text()`; for Node, use `readFile(path, 'utf8')`. Other encodings must be decoded by the host first. | Option | Default | Behavior | | -------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | | `delimiter` | `','` | Explicit comma, semicolon (`';'`), tab (`'\t'`) or pipe (`' | '`); no auto-detection | | `sheetName` | `'Sheet1'` | Must satisfy canonical sheet-name validation | | `valueMode` | `'text'` | `'text'` preserves nonempty field contents; `'infer'` converts canonical finite numbers and case-insensitive TRUE/FALSE | | `maxInputCharacters` | `20_000_000` | Positive safe integer; UTF-16 code-unit input budget | | `maxCells` | `200_000` | Positive safe integer; counts all fields, including blanks | The first record stays in row 1; it is not consumed as metadata. Ragged records are accepted, with the widest record setting the sheet's width. Empty fields are sparse blanks, and empty input becomes an empty 1×1 worksheet. Blank records are retained; one final line ending does not append an extra record. ## Quoting and values Quoted delimiters, doubled quotes, embedded newlines, CRLF/LF/CR record endings, and one leading Unicode BOM are supported. Whitespace inside a field is preserved. A quote in an unquoted field, an unclosed quote, or any characters between a closing quote and its delimiter/newline are rejected. Parsing is strict: even whitespace after a closing quote is invalid. Text mode keeps `00123`, `TRUE`, `123` and `=1+1` as text. Infer mode preserves leading-zero identifiers, surrounding whitespace, unsafe integer values and nonfinite numeric strings as text. It does not infer dates, currencies, percentages, or locale-specific numbers. Decimal conversion follows JavaScript number precision. CSV never creates formulas, even in infer mode. `=SUM(A1:A2)` remains a literal string. To intentionally create a formula, issue a `setCell` command after import. CSV cannot represent styles, merges, multiple sheets, hidden dimensions, or frozen panes. `CsvError.reason` is `invalid-csv`, `invalid-options`, or `limit`. `offset` is a zero-based UTF-16 location (zero for option/input-budget failures). Failed imports return no partial snapshot. Excel worksheet dimension limits also apply. ## Try it live The [playground](/nori/playground.html) accepts `.csv` and `.xlsx` files and includes a downloadable CSV sample. It uses infer mode so ordinary amounts become numbers while identifiers and formula-like strings remain text. Uploaded files stay in the browser. CSV export and streaming parsing are not implemented. --- Source: docs/currency.md Page: /nori/currency.html Markdown: /nori/markdown/currency.md # Currency formatting Available since **0.2.0**. Nori displays currency in both `Spreadsheet` and `SpreadsheetPreview`. Formatting uses `Cell.style.numberFormat`; the stored value and formula results stay numeric. No React theme setting or `renderCell` override is required. ## Set a currency format ```ts const sheetId = workbook.getState().activeSheetId; const result = workbook.dispatch({ type: "setCell", sheetId, address: "B2", cell: { value: 1234.5, style: { numberFormat: '"Rp "#,##0.00' }, }, }); if (!result.ok) throw result.error; // Both views display: Rp 1,234.50 // workbook.getCellValue(sheetId, "B2") remains 1234.5. ``` `setCell` replaces the cell. When formatting an existing cell, spread its current contents and style so you retain formulas and other formatting: ```ts const sheet = workbook.getState().snapshot.sheets.find((s) => s.id === sheetId); const cell = sheet?.cells["B2"] ?? { value: null }; workbook.dispatch({ type: "setCell", sheetId, address: "B2", cell: { ...cell, style: { ...cell.style, numberFormat: "$#,##0.00" } }, }); ``` These commands use normal history and subscriptions. Editing a formatted cell still edits its raw value; enter `1234.5`, not a string such as `$1,234.50`. Formatting does not parse currency text or perform currency conversion. ## Supported examples | Format | Value | Display | | ----------------------- | ------- | ------------ | | `$#,##0.00` | 1234.5 | $1,234.50 | | `$#,##0.00` | -1234.5 | -$1,234.50 | | `"Rp "#,##0` | 1234.5 | Rp 1,235 | | `"USD "#,##0.00` | 1234.5 | USD 1,234.50 | | `£#,##0.00` | 1234.5 | £1,234.50 | | `¥#,##0` | 1234.5 | ¥1,235 | | `#,##0.00" EUR"` | 1234.5 | 1,234.50 EUR | | `$#,##0.00;($#,##0.00)` | -1234.5 | ($1,234.50) | | `$0.00;($0.00);"-"` | 0 | - | | `[$€-407]#,##0.00` | 1234.5 | €1,234.50 | Currency symbols may be unquoted. Text currency codes/labels must be quoted or escaped. Decimal precision is explicit in the format; Nori does not infer a currency's minor units. Supported numeric patterns are `0` and `#,##0`, optionally followed by a decimal point and 1–10 zeros, optionally followed by `%`. Existing integer, decimal, and percent display remains supported. Values round for display only; a value such as `1.005` with `$0.00` displays `$1.01` without changing the stored number. ## Sections and Excel import One section applies to all numbers, with a minus sign automatically added for negatives. Two sections specify positive/zero and negative display. Three specify positive, negative, and zero display. An empty selected section hides its display; quoted literal-only sections can show a zero as a dash. Semicolons inside quotes or escaped with a backslash remain literal. A fourth section is reserved for text in Excel; Nori leaves text values unchanged. Known color tags such as `[Red]` are accepted but do not set text color. Set `Cell.style.color` separately if desired. Excel underscore padding is accepted but omitted because this formatter returns plain text. XLSX import preserves custom format strings. The adapter also recognizes common English/US built-in currency formats 5–8 and grouped-number formats 3–4. Built-in currency IDs are locale-dependent in Excel; Nori uses dollar formats for these IDs. An explicit custom currency symbol is more reliable for international workbooks. ## Headless formatting ```ts import { formatNumber } from "@byfungsi/nori/model"; formatNumber(1234.5, '"Rp "#,##0.00'); // "Rp 1,234.50" formatNumber(1234.5, "mm-dd-yy"); // undefined: unsupported ``` This helper has no React or DOM dependency. It returns `undefined` for unsupported formats or non-finite numbers. The React `formatCellValue` helper uses it and falls back to the raw numeric string. Both the full grid and preview use that same display path, including calculated cells, labels, and tooltips. ## Current limits - Output uses comma grouping and a decimal point, independent of the browser's language. An `Rp` label does **not** select Indonesian separators. The locale suffix in `[$€-407]` is preserved by import but does not control output locale. - Accounting fill/repetition (`*`), conditional sections (`[>100]`), date/time, scientific, fractions, scaling commas, optional decimal placeholders, and full Excel format parity are not supported. Unsupported formats fall back to the raw numeric value. - Currency does not imply financial decimal arithmetic; calculations still use JavaScript numbers. - Text values are not coerced into amounts. CSV has no format metadata; apply a number format after import to numeric cells. - There is no built-in currency-picker toolbar or automatic currency detection. For Excel's full format syntax, see [Microsoft's custom-number-format guidelines](https://support.microsoft.com/en-us/excel/review-guidelines-for-customizing-a-number-format). Nori implements the subset described here. --- Source: docs/getting-started.md Page: /nori/getting-started.html Markdown: /nori/markdown/getting-started.md # Quick start ## Install ```sh npm install @byfungsi/nori ``` For the React renderer, also install compatible React peers: ```sh npm install react@^19 react-dom@^19 ``` Headless use does not require React. The package is ESM; use an ESM application or a compatible bundler. Repository development requires Node 22.12 or later. Native bindings are future work. For editor appearance, see [Styling and theming](/nori/styling.html). Nori does not ship an editor stylesheet; the guide provides a complete application CSS starter. ## Calculate and persist a workbook This is a complete, typechecked example. Expected failures are Result values; this short program chooses to throw after checking them. An application should display or return these errors at its boundary. ```ts import { createEmptySnapshot, createWorkbook } from "@byfungsi/nori"; export function makeWorkbook() { const created = createWorkbook(createEmptySnapshot()); if (!created.ok) throw created.error; const workbook = created.value; const sheetId = workbook.getState().activeSheetId; const edited = workbook.dispatch({ type: "batch", commands: [ { type: "setCell", sheetId, address: "A1", cell: { value: 100 } }, { type: "setCell", sheetId, address: "A2", cell: { value: 50 } }, { type: "setCell", sheetId, address: "A3", cell: { value: null, formula: "SUM(A1:A2)" }, }, ], }); if (!edited.ok) throw edited.error; return workbook; } const workbook = makeWorkbook(); const sheetId = workbook.getState().activeSheetId; console.log(workbook.getCellValue(sheetId, "A3")); // 150 const json = JSON.stringify(workbook.exportSnapshot()); const restored = createWorkbook(JSON.parse(json)); if (!restored.ok) throw restored.error; ``` `WorkbookSnapshot` is immutable JSON-compatible document data. `Workbook` is a live runtime with commands, subscriptions, history and calculated values. Persist the snapshot, never the runtime object. Exported cell values may be old formula caches; read calculated results through `getCellValue`. ## Choose an import | Import | Purpose | | ------------------------ | ----------------------------------------------------------------- | | `@byfungsi/nori` | Convenient model constructors, XLSX parser and runtime | | `@byfungsi/nori/model` | Canonical types, validation, addresses, ranges and Result helpers | | `@byfungsi/nori/core` | Runtime, commands, selection and layout | | `@byfungsi/nori/csv` | Text CSV to a single-sheet snapshot | | `@byfungsi/nori/xlsx` | Byte-to-snapshot adapter | | `@byfungsi/nori/formula` | AST parser, evaluator, registry and dependency graph | | `@byfungsi/nori/pivot` | Semantic grouping and aggregation | | `@byfungsi/nori/react` | React DOM components and hooks | Only `/react` imports React. Prefer `/core` when you do not need XLSX dependencies. Internal workspace names are implementation details and must not appear in consumer code. Continue with [React](/nori/react.html), [XLSX import](/nori/importing.html), or [headless recipes](/nori/recipes.html). --- Source: docs/importing.md Page: /nori/importing.html Markdown: /nori/markdown/importing.md # Importing XLSX The XLSX adapter accepts `Uint8Array` bytes and returns a canonical workbook snapshot. It is synchronous and has no file-picker or filesystem dependency. ```ts import { parseXlsx, createWorkbook } from "@byfungsi/nori"; // Browser boundary: `file` is a File selected by the host. const imported = parseXlsx(new Uint8Array(await file.arrayBuffer())); if (!imported.ok) { console.error(imported.error.reason, imported.error.message); } else { // Retain these notes in your UI; successful import is not lossless fidelity. console.info(imported.value.warnings); const created = createWorkbook(imported.value.snapshot); if (!created.ok) console.error(created.error.message); else showWorkbook(created.value); // Your application's callback. } ``` In Node, pass `new Uint8Array(await readFile(path))` from `node:fs/promises`. A native application supplies bytes through its platform file adapter; Nori has no React Native binding yet. ## Limits and errors `parseXlsx(bytes, options)` accepts `maxInputBytes` (20,000,000 default), `maxUncompressedBytes` (100,000,000), and `maxCells` (200,000). Values must be positive safe integers. `XlsxError.reason` is `invalid-file`, `unsupported`, or `limit`. Declared ZIP sizes limit ordinary input, not hostile resource consumption. For public uploads, the host owns upload policy and CPU/memory isolation. Worker scheduling belongs to a platform adapter; core does not assume Web Workers. ## Layout and saved views Multiple sheets, dimensions, merged regions, frozen panes, hidden rows/columns, worksheet value/custom filters and saved value-sort metadata are supported. Saved sorted rows remain in their existing order; edits do not automatically re-sort. Advanced and table-level filters/sorts emit warnings. See the precise [support matrix](/nori/support.html). The parser never exposes XML nodes, ZIP entries or OOXML names as canonical model fields. If you need to construct a workbook without XLSX, use `parseWorkbookSnapshot` or `createEmptySnapshot`. ## Output and persistence Use `JSON.stringify(workbook.exportSnapshot())` to save canonical document data, and `createWorkbook(JSON.parse(json))` to rehydrate with validation. There is no `writeXlsx` API yet. Formula caches in snapshots may differ from current calculated results; use `getCellValue` for display or downstream processing. --- Source: docs/index.md Page: /nori/ Markdown: /nori/markdown/index.md # Nori documentation ## Before you integrate Nori is an initial 0.1.0 milestone, not a full Excel replacement. This repository has not published the package to npm. Use a local packed build today; the stable public import path is `@byfungsi/nori`. Read the [support matrix](/nori/support.html) before promising Excel fidelity. XLSX export, dynamic-array spilling, advanced filters, charts, and a React Native renderer are not implemented. ## Pick your entry point | Task | Start here | | ----------------------------------------- | --------------------------------------------- | | Create a workbook and calculate a formula | [Quick start](/nori/getting-started.html) | | Import an Excel attachment | [XLSX import](/nori/importing.html) | | Embed an editable or read-only sheet | [React integration](/nori/react.html) | | Render an agent's spreadsheet attachment | [Chat preview](/nori/preview.html) | | Generate integration code with an agent | [Agent guide](/nori/agents.html), [llms.txt](/nori/llms.txt) | [Download all documentation as plain text](/nori/llms-full.txt) · [Machine-readable page manifest](/nori/docs-manifest.json) --- Source: docs/interactions.md Page: /nori/interactions.html Markdown: /nori/markdown/interactions.md # Editor interactions - **Drag selection:** press the primary pointer in a cell and drag across the grid. Pointer capture continues the gesture outside the initial cell. At the scrollport edge, the view auto-scrolls. Releasing commits the current selection; cancellation or loss of capture stops the gesture. - **Shift-click:** extend from the original anchor to another cell; repeated extensions retain that anchor, including when extending in reverse. Arrow keys move focus; Shift-arrows extend/contract the selection. Keyboard movement is bounded to the rendered window, skips hidden/filtered entries, and steps past a merged region as a unit. - **Resize:** drag a column or row boundary from its header or anywhere along a cell edge. A six-unit hit area on either side of the boundary gives a stable target; hovering or dragging highlights the whole boundary across the viewport. At intersections the nearest edge wins, with columns winning exact ties. Merged cells expose only their perimeter. The drag is a temporary preview until release, creating a single undoable command. Escape or pointer cancellation restores the original size. Handles are keyboard-focusable: directional arrows change size by 10 units, Shift-arrows by one, and Home resets to the sheet default. Double-click also resets; it is not auto-fit. - **Merge:** select a rectangle and choose Merge cells in `SelectionToolbar`. The top-left cell is the only content owner. A nonempty covered cell causes a useful error instead of data loss. Unmerge cells removes intersecting merges without losing the anchor. Both commands participate in undo/redo and snapshot serialization. The model and core own all document/selection semantics. React owns pointer capture, DOM hit testing, animation-frame auto-scroll, keyboard events and temporary resize feedback. `Spreadsheet` is the default composition; these behaviors are also available through `SheetGrid` and the separate `SelectionToolbar`. The full grid shows at most 100 visible rows and 26 visible columns, retaining leading frozen dimensions when given a later window. This is bounded rendering, not full virtualization. Large frozen regions can occupy the entire rendered window. Style `.nori-resize-handle`, `.nori-selection-toolbar`, `.nori-view-status`, and the existing grid classes. The full resize guide uses `--nori-resize-border`. Selection colors can be set with `--nori-selection-fill` and `--nori-selection-border`. Frozen headers/cells use `--nori-header-background` and `--nori-cell-background` when the document has not supplied a background. ## Editing and read-only views Double-click a cell, or press Enter/F2 on a focused cell, to edit its value or formula inline. Enter or moving focus commits through workbook commands; Escape discards the draft. There is no modal overlay. Use `` or `` for composed views. `SheetGrid`, `FormulaBar`, and `SelectionToolbar` also accept `readOnly`. A provider's read-only setting cannot be overridden by its children. Selection, navigation and sheet tabs remain available; editors, resize gestures and merge actions cannot change the document. Switching to read-only cancels an unfinished inline edit or resize. `useWorkbookReadOnly()` lets host-owned controls disable undo/redo or other mutation actions. This is a view policy: the host can still update the headless workbook programmatically. The separate `SpreadsheetPreview` always remains read-only. --- Source: docs/playground.md Page: /nori/playground.html Markdown: /nori/markdown/playground.md # Try Nori live Edit a real workbook below—no installation or account needed. This example uses the same public `@byfungsi/nori` package and React components described in the guides. ## Try these actions 1. Double-click **Sales B2**, change `1200` to `100`, and press Enter. The total in **B6** becomes **1550**; the pivot updates too. 2. Switch to **Summary** to see the cross-sheet total, then choose **Undo** to restore the original values. 3. Drag across cells, Shift-click to extend selection, or drag a row/column boundary to resize it. 4. Turn on **Read only** to try a navigable sheet without editing. 5. Download an XLSX or CSV sample, then open it with **Open .xlsx or .csv**. The XLSX layout sample includes merged cells, frozen panes, hidden columns and saved filters. [Open the live playground](/nori/demo/) ## Your files stay in this browser The example parses selected files locally. It does not upload workbook contents, save edits to a server, or persist them between page reloads. Choose **Reset example** to restore the starting workbook. The sample files contain synthetic data. This is a bounded example, not complete Excel compatibility: the editor renders at most 100 visible rows and 26 visible columns, and XLSX export is not available. Import notes identify supported warnings. Read the [support matrix](/nori/support.html) before relying on document fidelity. ## Build your own Start with the [React integration guide](/nori/react.html) or the [headless quick start](/nori/getting-started.html). The complete runnable example lives in [examples/react-demo](https://github.com/byfungsi/nori/tree/main/examples/react-demo). Its chat preview, sheet tabs and pivot share one live workbook runtime. --- Source: docs/preview.md Page: /nori/preview.html Markdown: /nori/markdown/preview.md # Chat / thumbnail preview `SpreadsheetPreview` is a separate read-only React component intended for agentic chat messages, file attachments and thumbnails. It does not embed the editable grid. ```tsx import { SpreadsheetPreview } from "@byfungsi/nori/react"; showEditor(workbook)} />; ``` Props: | Prop | Behavior | | --------------------------- | ----------------------------------------------------------------------------- | | `workbook` | Live runtime; subscribes to data/calculation updates but never mutates it | | `title` | Attachment filename; default `Workbook.xlsx` | | `theme` | `light` (default) or `dark` | | `maxRows` / `maxColumns` | Defaults 8/6; bounded to 20/12 for thumbnail rendering | | `onOpen` | Optional action to open the host's full editor; no action is shown if omitted | | `sheetId` / `onSheetChange` | Optional controlled tab; otherwise tab state is local to the card | | `className` / `style` | Host styling hooks on the outer figure | Preview sheet changes do not alter the full editor's active sheet, selection or undo history. There are no cell editors, selection gestures, resize handles or merge controls. It honors hidden/filtered rows and columns, shows calculated values and merged-cell anchor content. The card fits its parent's width, including narrow chat bubbles. The table and tab strip scroll horizontally inside the card; they never force the page wider. Headers can wrap the file/open controls. Cells use compact fixed row heights and bounded column widths, intentionally independent of the full editor's large document geometry. Long values are clipped with full text in their title attribute. The footer states the displayed row/column counts. Workbook colors/styles are preserved separately from the card theme. Imported explicit colors may need host-specific adaptation for strong contrast in a dark chat design. Full font fidelity, annotations such as hand-drawn circles, and automatic row fitting are not part of this component. See [Styling and theming](/nori/styling.html#styling-the-chat-preview) for palette behavior, CSS precedence, sizing, and the distinction between preview and editor themes. Currency display is supported since 0.2.0 through `Cell.style.numberFormat`. See [Currency formatting](/nori/currency.html) for syntax, imports, and locale limits. --- Source: docs/publishing.md Page: /nori/publishing.html Markdown: /nori/markdown/publishing.md # Publishing to npm Only `@byfungsi/nori` is published. The root, demo, and internal packages remain private; the facade bundles internal code and exposes stable subpaths. Version `0.1.0` is published on npm. ## Prepare an archive Use Node 22.20.0 and install dependencies with `npm ci`. Then run: ```sh npm run release:prepare ``` This runs boundaries, type checks, tests, builds, and isolated consumer checks, packs the facade, checks every exported file, and dry-runs publication. The archive and its file list/integrity are saved under `artifacts/release/`. Nothing is uploaded to npm. React stays an optional peer dependency. Nori uses the MIT license. Its text is included in the repository and the published package. The publishing workflow checks for the license metadata and bundled license file. ## First release An npm owner of the `byfungsi` organization can bootstrap the package locally: ```sh npm whoami npm org ls byfungsi npm run release:prepare -- --require-license npm publish ./artifacts/release/byfungsi-nori-0.1.0.tgz --access public --registry https://registry.npmjs.org/ ``` The last command publishes publicly and cannot overwrite an existing version. Run it only when ready to release. npm may request account verification or a one-time password. Keep credentials out of repository files. Initial local publication does not have GitHub Actions provenance. Verify the published version and test installation in a separate project: ```sh npm view @byfungsi/nori@0.1.0 version dist.integrity npm install @byfungsi/nori@0.1.0 ``` Compare the registry integrity to `artifacts/release/manifest.json`. Keep the installation documentation aligned with each release. ## Subsequent releases from GitHub Configure a trusted publisher in the npm package settings: | Setting | Value | | ----------------- | ------------------------------------- | | Provider | GitHub Actions | | Organization | `byfungsi` | | Repository | `nori` | | Workflow filename | `npm-publish.yml` | | Environment | `npm` | | Allowed action | Direct publication with `npm publish` | Create the matching GitHub environment `npm`; restrict it to the `main` branch. Required reviewers can be added if desired. The workflow uses short-lived OIDC credentials, with no stored npm publishing token. npm generates provenance for supported trusted publication from public repositories. See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) for setup requirements. For each release: 1. Update the public package version with `npm version 0.1.1 --workspace @byfungsi/nori --no-git-tag-version` (substitute the intended version), and commit the manifest and lockfile alongside release notes. 2. Push the reviewed changes to `main` and let CI pass. 3. Open GitHub Actions → **Publish npm package** → **Run workflow**. Select `main` and enter the exact package version. 4. The workflow checks the version and license, runs all verification, saves the archive as an Actions artifact, and publishes that exact archive. 5. Verify the registry version/integrity and create a matching `v0.1.1` Git tag on the workflow's commit for traceability. Ordinary pushes and tags never trigger npm publication. A duplicate version fails; release fixes under a new version. The workflow publishes to `latest`; prerelease channels require an explicit workflow change before use. --- Source: docs/react.md Page: /nori/react.html Markdown: /nori/markdown/react.md # React integration ## Ready-made editor ```tsx import { useState } from "react"; import { Spreadsheet, SpreadsheetPreview } from "@byfungsi/nori/react"; import { makeWorkbook } from "./quick-start"; export function WorkbookExample() { // Keep runtime identity stable across renders. const [workbook] = useState(makeWorkbook); const [readOnly, setReadOnly] = useState(false); return ( <> ); } ``` The helper `makeWorkbook` is the [quick-start example](/nori/getting-started.html#calculate-and-persist-a-workbook), not a library export. Create or import the runtime once, outside render or in a lazy state initializer. The renderer subscribes with `useSyncExternalStore` and also supports server rendering. ## Compose your own view ```tsx import { WorkbookProvider, FormulaBar, SelectionToolbar, SheetGrid, WorkbookTabs, } from "@byfungsi/nori/react"; // `workbook` is the live runtime created by your host. ; ``` | Primitive | Responsibility | | ----------------------- | ---------------------------------------------------------------- | | `WorkbookProvider` | Supplies the runtime and optional `readOnly` policy | | `useWorkbook()` | Returns the runtime for host controls | | `useWorkbookState()` | Subscribes to stable state snapshots | | `useWorkbookReadOnly()` | Reads the provider's view policy | | `FormulaBar` | Displays/edits the selection anchor's raw value or formula | | `SelectionToolbar` | Shows selection range and merge actions | | `SheetGrid` | Bounded grid, selection, inline editing, resize, imported layout | | `WorkbookTabs` | Switches runtime active sheet and clears selection | | `Spreadsheet` | Default composition of the above | | `SpreadsheetPreview` | Independent, always-read-only chat card | `Spreadsheet` accepts `workbook`, `readOnly`, `className`, `style`, `range`, and `renderCell`. `SheetGrid` accepts `range`, `renderCell`, `className`, `readOnly`, and `onError`. `FormulaBar` and `SelectionToolbar` accept `className`, `readOnly`, and `onError`. A child primitive cannot disable its provider's read-only policy. `range` is an inclusive zero-based rectangle. Rendering is capped at 100 visible rows and 26 visible columns, including frozen entries. It is not full virtualization. ## Read-only is a view policy `readOnly` blocks built-in document mutations, including resizing and merging. Selection, arrows and sheet switching remain usable. A host-owned undo button must check `useWorkbookReadOnly()` itself. The runtime command API remains available for programmatic updates; this flag is not an authorization boundary. The [chat preview](/nori/preview.html) always stays read-only and keeps tab state local. Use it for messages and thumbnails; use `Spreadsheet readOnly` for a navigable full grid. ## Styling and custom cells See the [complete styling and theming guide](/nori/styling.html) for a copyable light/dark stylesheet, every CSS variable and class hook, preview customization, and troubleshooting. Nori supplies essential geometry inline and semantic CSS classes for application styling. It does not export a stylesheet or impose a design-system dependency. Style `.nori-workbook`, `.nori-grid`, `.nori-tabs`, `.nori-formula-bar`, and `.nori-selection-toolbar` in your application. The repository's demo CSS is an example, not a public package export. Workbook styles (fill, text color, alignment and number-format metadata) belong to cells. Application chrome belongs to CSS. Customize selection through `--nori-selection-fill`, `--nori-selection-border`; resize through `--nori-resize-border`; frozen surfaces through `--nori-header-background`, `--nori-cell-background`; and inline editors through `--nori-editor-background`, `--nori-editor-color`. `renderCell(context)` receives `sheet`, `address`, `cell`, calculated `value`, `selected`, and `mergedRange`. Render noninteractive content: it is placed inside the grid's cell button, so nested buttons/inputs are invalid. Merged cells receive their anchor's value. See [interactions](/nori/interactions.html) for editing and keyboard behavior. Currency display is supported since 0.2.0 through `Cell.style.numberFormat`. See [Currency formatting](/nori/currency.html) for syntax, imports, and locale limits. --- Source: docs/recipes.md Page: /nori/recipes.html Markdown: /nori/markdown/recipes.md # Headless recipes Examples below assume a validated live `workbook` and its `sheetId`, obtained from `workbook.getState().activeSheetId` as in the [quick start](/nori/getting-started.html). All changes return Results; inspect failures before reporting success. ## Subscribe and clean up ```ts const unsubscribe = workbook.subscribe(() => { const state = workbook.getState(); console.log(state.revision, workbook.getCellValue(state.activeSheetId, "A3")); }); // On host teardown: unsubscribe(); ``` Listeners run synchronously. Keep them nonthrowing. A throwing listener can interrupt later notifications after a command has already committed. ## One history entry for multiple edits ```ts const result = workbook.dispatch({ type: "batch", commands: [ { type: "setCell", sheetId, address: "B1", cell: { value: 10 } }, { type: "setCell", sheetId, address: "B2", cell: { value: null, formula: "B1*2" }, }, ], }); if (!result.ok) console.error(result.error.message); else console.log(workbook.getCellValue(sheetId, "B2")); // 20 workbook.undo(); // Boolean: false if unavailable. workbook.redo(); ``` A failed batch is atomic: no partial data changes, history or notifications. Nested batches are not supported. `historyLimit: 0` disables history when creating the runtime. ## Select and resize without a renderer ```ts const selected = workbook.selectCell(sheetId, { row: 0, column: 0 }); const extended = workbook.selectCell( sheetId, { row: 2, column: 1 }, { extend: true }, ); const resized = workbook.dispatch({ type: "resizeColumn", sheetId, column: 1, width: 240, }); for (const result of [selected, extended, resized]) { if (!result.ok) console.error(result.error.message); } ``` Selection is transient and does not enter history. Layout mutations do. Coordinates are zero-based; address strings use Excel's A1 convention. Merge closure and hidden/filter navigation live in core. [API details](/nori/api.html#layout-and-merge-commands). ## Extend formulas The [formula API example](/nori/api.html#formula) shows a `DOUBLE` function. Register uppercase names before constructing the workbook and pass `{functions}` to `createWorkbook`. The registry is copied at construction. Functions receive scalar/array arguments and return `FormulaResult`; `IF` and `IFERROR` are evaluator-owned lazy forms. Functions should be pure and synchronous. Thrown defects propagate to the host. Return `cellError('#VALUE!')` for a modeled spreadsheet error. Arrays are supported as values but do not spill into neighboring cells. ## Build a pivot ```ts import { createPivot } from "@byfungsi/nori/pivot"; const result = createPivot( [ { region: "West", amount: 100 }, { region: "West", amount: 50 }, { region: "East", amount: 20 }, ], { rows: ["region"], values: [{ id: "total", field: "amount", aggregate: "sum" }], }, ); if (!result.ok) console.error(result.error.message); else console.log(result.value.groups, result.value.totals); // grand total 170 ``` Pivots consume records, not cell addresses. Extract calculated cell values in your host adapter when using workbook data. Groups preserve first-seen order. Measures support sum/count/average/min/max. This does not import Excel pivot caches or render a pivot widget. --- Source: docs/roadmap.md Page: /nori/roadmap.html Markdown: /nori/markdown/roadmap.md # Roadmap 1. **Compatibility corpus:** check in independently generated Excel/LibreOffice fixtures; expand malformed-file cases, number/date formats, formula coercion and error precedence. Report import losses more comprehensively. 2. **Runtime scale:** structural sharing, inverse-command history, compact range dependency indexing, incremental recalculation and benchmarks on sparse large workbooks. Scheduling/cancellation must remain host-supplied. 3. **Formula breadth:** SUMIF/COUNTIF, lookup and date functions, named ranges, structured references, reference rewriting for rename/copy/fill, spill ownership and collision errors, precise Excel semantics. 4. **Document editing:** sheet lifecycle, row/column insertion/deletion, richer styles, round-trip XLSX writing with explicit preservation guarantees. 5. **React editor:** expanded keyboard shortcuts, clipboard, virtualized rows/columns, auto-fit, interactive sort/filter editors, accessible editing refinements and native selection adapters. 6. **Pivots:** column axes, filtering, subtotals, sorting, richer measure semantics and XLSX pivot definition/cache adapters. 7. **React Native:** native renderer using the existing store, native file/byte adapter, platform text measurement, Metro/Hermes checks, touch selection and scheduling integration. No DOM primitives should be moved into core to enable this work. 8. **Release preparation:** choose licensing, add release/version automation, API stability policy, compatibility guarantees, generated API reference, and package-size budgets before a public stable release. --- Source: docs/styling.md Page: /nori/styling.html Markdown: /nori/markdown/styling.md # Styling and theming Nori 0.1.0 exposes styling hooks for React applications. The full editor uses your application's CSS; the compact preview has built-in light and dark palettes. There is no theme picker in the playground, no full-editor `theme` prop, and no exported Nori stylesheet to import. This guide describes the published API, including the limits of what can be customized without changing the renderer. ## What `className` means `className` is a React prop that adds a CSS class to a rendered HTML element. It is not a visible label, a workbook field, or a setting in Excel. You choose the name and write its rules in your application stylesheet: ```tsx import { Spreadsheet } from "@byfungsi/nori/react"; import "./spreadsheet.css"; // workbook is a live runtime created by createWorkbook(...). ; ``` ```css /* spreadsheet.css in your application */ .my-sheet { font-family: system-ui, sans-serif; color: #24372c; background: white; --nori-selection-border: #286b48; } ``` Inspect the element in browser developer tools: the editor's outer `div` now has `class="my-sheet"`. The CSS file must actually be loaded by your application. Installing Nori does not install these example styles automatically. **A supplied `className` replaces the component's default class.** It is not appended. To retain the default root hook, use `className="nori-workbook my-sheet"`. Changing the editor's root class does not change its children's default classes, such as `.nori-grid`. ## Three separate styling layers | Layer | Examples | Where it lives | | ------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------ | | Application appearance | Toolbar colors, tabs, borders, focus, font family | Your CSS and React styling props | | Workbook formatting | Explicit cell fill/text color, bold, wrapping, alignment, number format | `Cell.style` in the snapshot/runtime | | Layout and interaction geometry | Column widths, row heights, merged cells, frozen offsets, resize guides | Workbook layout and the renderer's inline styles | Changing a theme does not mutate the workbook or its exported snapshot. Imported explicit cell colors take precedence over ordinary inherited application colors. A pale imported fill with no explicit text color can be hard to read in a dark application. Review representative workbooks; dark application chrome does not guarantee dark-compatible document formatting. ## Component styling props | Component | Props and target | | -------------------- | ---------------------------------------------------------------------------------------------------- | | `Spreadsheet` | `className`, `style` target the outer `div`; `renderCell` customizes cell contents | | `SheetGrid` | `className` targets the **table**, not its scrolling wrapper; supports `renderCell`; no `style` prop | | `FormulaBar` | `className` targets its outer `div` or `form`, depending on selection; no `style` prop | | `SelectionToolbar` | `className` targets the outer `div`; no `style` prop | | `WorkbookTabs` | `className` targets the `nav`; no `style` prop | | `SpreadsheetPreview` | `className`, `style` target the outer `figure`; `theme` accepts `light` or `dark` | | `WorkbookProvider` | Context only; no visual wrapper or styling props | The editor does not forward arbitrary HTML attributes such as `data-theme`. Put those attributes on a host wrapper if you need them. The preview sets its own `data-theme` from its `theme` prop. ## Complete editor CSS starter Copy the following CSS into your application. It includes light and dark application palettes, formula controls, selection toolbar, grid borders, sheet tabs, keyboard focus, and narrow-screen wrapping. These are application-owned examples, not built-in Nori themes. Explicit workbook colors are preserved. ```ts /* Application-owned theme: this file is not an npm package export. */ .my-sheet { --app-surface: #ffffff; --app-chrome: #f3f6f2; --app-text: #24372c; --app-muted: #526457; --app-border: #cbd7ce; --app-active: #dceedd; --app-focus: #286b48; --nori-header-background: var(--app-chrome); --nori-cell-background: var(--app-surface); --nori-editor-background: var(--app-surface); --nori-editor-color: var(--app-text); --nori-selection-fill: rgb(40 107 72 / 14%); --nori-selection-border: var(--app-focus); --nori-resize-border: var(--app-focus); color-scheme: light; color: var(--app-text); background: var(--app-surface); font: 14px/1.4 system-ui, sans-serif; min-width: 0; width: 100%; border: 1px solid var(--app-border); border-radius: 12px; } .my-sheet--dark { --app-surface: #18211c; --app-chrome: #243229; --app-text: #edf5ef; --app-muted: #bdcec1; --app-border: #4a6151; --app-active: #365b43; --app-focus: #8fe0ab; --nori-selection-fill: rgb(143 224 171 / 16%); color-scheme: dark; } .my-sheet .nori-formula-bar, .my-sheet .nori-selection-toolbar, .my-sheet .nori-view-status, .my-sheet .nori-tabs { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--app-chrome); border-bottom: 1px solid var(--app-border); } .my-sheet .nori-formula-bar, .my-sheet .nori-selection-toolbar, .my-sheet .nori-view-status { flex-wrap: wrap; } .my-sheet .nori-formula-bar input { flex: 1 1 140px; min-width: 0; padding: 8px; border: 1px solid var(--app-border); border-radius: 6px; background: var(--app-surface); color: var(--app-text); font: inherit; } .my-sheet .nori-formula-bar button, .my-sheet .nori-selection-toolbar button, .my-sheet .nori-tabs button { font: inherit; color: var(--app-text); background: var(--app-surface); border: 1px solid var(--app-border); border-radius: 6px; padding: 7px 10px; cursor: pointer; } .my-sheet button:disabled { opacity: 0.5; cursor: default; } .my-sheet button:focus-visible, .my-sheet input:focus-visible { outline: 2px solid var(--app-focus); outline-offset: -2px; } .my-sheet .nori-view-status, .my-sheet .nori-selection-toolbar output { color: var(--app-muted); } .my-sheet [role="alert"] { flex-basis: 100%; font-weight: 600; } .my-sheet .nori-grid { font: inherit; border-collapse: collapse; border-spacing: 0; } .my-sheet .nori-grid th, .my-sheet .nori-grid td { border-right: 1px solid var(--app-border); border-bottom: 1px solid var(--app-border); } .my-sheet .nori-grid th { color: var(--app-muted); font-weight: 500; } .my-sheet .nori-grid td { background-color: var(--app-surface); } .my-sheet .nori-tabs { overflow-x: auto; border-top: 1px solid var(--app-border); border-bottom: 0; } .my-sheet .nori-tabs button { flex-shrink: 0; white-space: nowrap; } .my-sheet .nori-tabs button[aria-pressed="true"] { background: var(--app-active); font-weight: 700; } .my-sheet .nori-resize-handle:focus-visible { outline: 2px solid var(--app-focus); } @media (max-width: 480px) { .my-sheet .nori-formula-bar, .my-sheet .nori-selection-toolbar, .my-sheet .nori-tabs { padding: 8px; } } ``` Use either appearance with the same runtime: ```tsx ``` In a real application, render one editor and toggle the class from your existing theme state: ```tsx ``` `darkMode` is your application's boolean, not a Nori export. No workbook recreation is needed when it changes. The `--app-*` variables in this example belong to this stylesheet; Nori itself only reads the `--nori-*` variables listed below. ## All editor CSS variables Set these on the editor root or an ancestor so they inherit into the grid. They affect the full editor, including composed primitives, but do not theme `SpreadsheetPreview`. | Variable | Built-in fallback | Effect | | -------------------------- | ----------------------- | ----------------------------------------------- | | `--nori-selection-fill` | `rgba(107,151,71,0.13)` | Overlay fill on selected cells | | `--nori-selection-border` | `#789a55` | Selection inset border and inline editor border | | `--nori-resize-border` | `#4b7d34` | Full boundary guide while hovering/resizing | | `--nori-header-background` | `#f6f8f1` | Row/column header backgrounds | | `--nori-cell-background` | `white` | Frozen cells without an explicit workbook fill | | `--nori-editor-background` | `white` | Inline editing input background | | `--nori-editor-color` | `#17251b` | Inline editing input text | `--nori-cell-background` alone does **not** paint every ordinary cell. Set the root/table or ordinary `td` background in your CSS, as in the starter. Grid lines, header text, formula-bar input styling, tab colors, and font family also need your CSS; they are not additional built-in tokens. ### Inline styles and TypeScript `style` is useful for per-instance values. TypeScript's `CSSProperties` does not directly list custom properties; explicitly extend it: ```tsx import type { CSSProperties } from "react"; const appearance: CSSProperties & { "--nori-selection-border": string; "--nori-selection-fill": string; } = { "--nori-selection-border": "#7856d8", "--nori-selection-fill": "rgb(120 86 216 / 15%)", borderRadius: 8, }; ; ``` Use `style` for the root only. It does not automatically override inline styles on descendants. ## CSS hooks and interaction states Scope your selectors beneath your editor class to avoid changing other tables or buttons in your app. | Hook | What it selects | | ---------------------------------------- | ----------------------------------------------------------------------------------- | | `.nori-workbook` | Default editor root | | `.nori-formula-bar` | Formula display/edit controls | | `.nori-selection-toolbar` | Selection summary and merge/unmerge controls | | `.nori-view-status` | Imported filter/freeze status, when present | | `.nori-scroll` | Scroll container generated by `SheetGrid` | | `.nori-grid` | Default grid table | | `.nori-tabs` | Sheet navigation | | `.nori-tabs button[aria-pressed="true"]` | Active sheet button | | `.nori-grid td[data-selected]` | Selected cells; attribute is absent when unselected | | `.nori-grid td[data-merged]` | Merged anchor cells | | `.nori-grid td[data-address="B2"]` | A rendered cell by its A1 address | | `.nori-grid button[data-cell-address]` | Cell interaction buttons | | `.nori-selection-fill` | Noninteractive selection overlay | | `.nori-resize-handle` | Header resize handle; axis classes are `.nori-resize-column` and `.nori-resize-row` | | `.nori-resize-guide` | Whole-border hover/drag indicator | | `[role="alert"]` | Error text; scope this to your editor | Do not remove pointer-event behavior, change sticky positioning/z-index, or repurpose resize handles to draw ordinary grid borders. Keep the selection overlay noninteractive. Do not hide keyboard focus outlines without providing an equally visible replacement. These hooks describe the current 0.1.0 DOM. Prefer public props and variables; audit descendant selectors when upgrading, especially selectors tied to internal nesting. ## Compose a custom toolbar layout Use the primitives when the default editor arrangement does not fit your application: ```tsx import { WorkbookProvider, FormulaBar, SelectionToolbar, SheetGrid, WorkbookTabs, } from "@byfungsi/nori/react";
; ``` Retaining the default class names allows the starter stylesheet to continue matching. The same styling works in a `readOnly` editor; read-only mode changes interaction policy, not appearance. Style disabled actions and read-only inputs if you want a visual distinction. ## Custom cell content `renderCell` changes display content inside an existing cell button. It does not replace the `td`, change the stored value, or change calculations/exported formatting. ```tsx import { Spreadsheet, formatCellValue } from "@byfungsi/nori/react"; ( {formatCellValue(value, cell)} )} />; ``` The context includes `sheet`, `address`, `cell`, calculated `value`, `selected`, and `mergedRange`. `value` may be a scalar, error, or array result. `formatCellValue` handles these current result forms; avoid calling numeric methods without narrowing the value first. Merged cells use their anchor's value. Render noninteractive content: nested buttons, links, and inputs inside the cell button interfere with semantics and editing. The built-in title and accessible cell label continue to use the default formatted value, so custom content should preserve its meaning. The preview has no `renderCell` prop in 0.1.0. ## Styling the chat preview ```tsx import { SpreadsheetPreview } from "@byfungsi/nori/react"; ; ``` The preview is always read-only, with local sheet navigation. Its built-in palette styles the header, grid lines, tabs, footer, and text. `style` is merged last on the outer figure, so outer sizing, border, background, and font family can be customized there. The internal table uses a fixed 12px font size; changing the figure's font size does not scale all descendants. The preview uses inline colors on internal elements. A root background override or the editor's CSS variables will not replace that full palette. Version 0.1.0 has no custom palette object, theme provider, or automatic system-theme detection. Pass `theme` from your host's theme state. Extensive internal recoloring would require overriding inline declarations; prefer the built-in palettes or a host-owned renderer for that level of control. The card fits its container and scrolls columns horizontally. Default limits are 8 rows and 6 columns; maximum limits are 20 rows and 12 columns. Column widths are clamped to 90–180px for a compact card. Imported row heights are not reproduced; text is ellipsized. Workbook fills and text colors remain separate from its palette. See [chat preview](/nori/preview.html) for the full prop reference. ## Responsive layout, sizing, and specificity - Give flex/grid children containing the editor `min-width: 0`. For CSS grid, `minmax(0, 1fr)` prevents a wide sheet from stretching the page. - Let `.nori-scroll` scroll horizontally. Do not squeeze all spreadsheet columns to fit a phone or override the renderer's calculated table/column widths. - Row heights, column widths, frozen offsets, and merge geometry come from the workbook. Change them through supported runtime commands, not arbitrary cell CSS. Font-size increases do not auto-fit rows. - `SheetGrid` renders a bounded window (up to 100 visible rows and 26 visible columns); styling does not enable virtualization or remove that limit. - The scroll container has an inline `max-height: 520px`. There is no public height prop in 0.1.0. If necessary, a scoped `.my-sheet .nori-scroll { max-height: 65vh !important; }` overrides that one declaration. Treat it as a version-specific workaround and test frozen panes and resize guides afterward. - Essential cell padding, positioning, button geometry, and workbook formatting are inline. Ordinary CSS rules do not override inline declarations. Avoid broad `!important` rules that erase workbook styles or break interaction geometry. - The starter uses `background-color` on `td` so an explicit inline workbook background wins. Avoid blanket `td { color: ... }` if you intend unformatted cells to inherit root text color. ### CSS Modules and utility CSS With CSS Modules, use `className={styles.sheet}` and `:global(.nori-grid)` inside the scoped rules to target Nori's global descendant classes. Import the module in your application; there is no module export from Nori. Utility classes can style the root through `className`. For the many descendant hooks and custom properties, a scoped stylesheet is usually clearer. No Tailwind plugin or theme adapter ships with Nori. ## Troubleshooting and validation | Symptom | Check | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | My class does nothing | Confirm the CSS file is loaded and the rendered element has that class. A class name does not generate styles by itself. | | Default selector stopped matching | A custom `className` replaces the default; pass both names if needed. | | Grid headers remain light | Set `--nori-header-background`; their background is assigned inline using this variable. | | Frozen cells remain white | Set `--nori-cell-background`; also style ordinary cells separately. | | Toolbar input remains light | Style `.nori-formula-bar input`; editor variables only style the inline cell editor. | | Imported cell looks wrong in dark mode | Inspect its explicit workbook fill and text color; application theme changes preserve them. | | My row size or padding rule is ignored | Inspect inline layout styles. Use layout commands for dimensions and avoid breaking cell geometry. | | Preview ignores editor variables | Use its `theme` prop; preview colors are a separate implementation. | | CSS affects another workbook | Scope rules beneath a per-instance class; avoid global `table`, `button`, or `td` rules. | Before shipping your stylesheet, check keyboard focus, selected ranges, active tabs, disabled/read-only controls, formula input, inline editing, error text, hover/drag resize borders, frozen rows/columns, merged cells, long text, and imported explicit colors. Check narrow screens and horizontal scrolling as well as a desktop viewport. Maintain readable contrast; do not use color alone to communicate errors or selection. There is currently no unified theme object, full-editor light/dark preset, theme persistence, exported CSS bundle, or automatic contrast correction for imported formatting. The [React integration guide](/nori/react.html), [interaction guide](/nori/interactions.html), and [Excel support matrix](/nori/support.html) describe the surrounding capabilities. Currency display is supported since 0.2.0 through `Cell.style.numberFormat`. See [Currency formatting](/nori/currency.html) for syntax, imports, and locale limits. --- Source: docs/support.md Page: /nori/support.html Markdown: /nori/markdown/support.md # Current support and compatibility The goal is a useful, honest initial slice. These are explicit support boundaries, not a claim of Excel parity. | Area | Implemented | Not yet supported | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Files | CSV text import (explicit delimiters, quoting, optional type inference); basic ZIP/OOXML `.xlsx`, workbook relationships, multiple worksheets | `.xls`, `.xlsb`, encryption, XLSX writing, macro execution | | Cells | Sparse A1 storage, numbers, text, booleans, blanks, errors; shared/rich/inline strings | Rich text runs retain concatenated text only | | Formula import | Ordinary formula text, cached values | Shared/array/data-table formulas retain cached values with warnings | | References | Relative/absolute A1, rectangular ranges, case-insensitive cross-sheet names, quoted sheet names | Named ranges, tables/structured references, external files, whole-column/row and 3D references | | Formula syntax | `+ - * / ^ &`, comparison operators, unary signs, postfix `%`, parentheses, strings with doubled quotes | Locale separators, union/intersection, formula error literals, full Excel grammar | | Aggregation | `SUM`, `AVERAGE`, `MIN`, `MAX`, `COUNT`, `COUNTA` | Criteria functions such as SUMIF/COUNTIF, lookup functions | | Other functions | `IF`, `IFERROR`, `AND`, `OR`, `NOT`, `ABS`, `ROUND`, `CONCAT`, `LEN`; registry extensions | Date/time, finance, statistical, volatile and async functions | | Recalculation | Lazy cache, dependency graph, cross-sheet calculation, cycle errors | Incremental scheduling, iterative calculation, Excel coercion/error precedence parity | | Arrays | First-class rectangular results, range arguments | Dynamic-array spilling, broadcasting, implicit intersection | | Styles | Bold/italic, explicit RGB text/fill, horizontal/vertical alignment, wrapping, number-format metadata | Themes/indexed colors, borders, conditional formats, font families/sizes | | Number display | General, fixed decimals, grouping, percent, common currency formats (see [currency](/nori/currency.html)) | Locale-aware Excel format engine, date display, full accounting | | Dates | Preserve numeric serials and 1900/1904 metadata; ISO date cells remain strings with a warning | JavaScript Date conversion and Excel leap-day semantics | | Pivot | Semantic row grouping, sum/count/average/min/max, grand totals | Imported XLSX pivot caches/layout, column axes, filters, subtotals, calculated measures | | UI | Sheet tabs, drag/Shift selection, arrows, resize, merge/unmerge, editing, history, responsive read-only preview | Full virtualization, clipboard, fill handles, row/column insertion/deletion | | Platforms | Pure JS/TS headless layers, Node import smoke test, browser React demo, React SSR | React Native binding and device validation | Import warnings flag shared/array formulas, drawings, conditional formatting, validations, named ranges, external links, split panes, unsupported filter/sort variants, column-level styles, and hidden-sheet state. Row/column hiding and frozen panes are supported; entire hidden sheets are still imported as visible. Unsupported document features outside this subset may be ignored. A successful parse does not guarantee lossless document preservation. Graphical/style properties outside the listed subset are not retained. XLSX visibility is not an access-control boundary: all worksheets are imported as visible. Coercion is intentionally limited. Numeric aggregations consider numeric values, including range values, and ignore text/booleans. This differs from Excel for literal arguments such as `SUM(TRUE, "2")`. Scalar arithmetic converts numeric strings, booleans, and blanks; comparisons are case-insensitive for strings but do not implement Excel's complete cross-type ordering. Logical functions use JavaScript truthiness for scalar values. ROUND is based on binary floating point and may differ at precision boundaries. Error propagation is a useful subset, not Excel's full rules. LEN uses UTF-16 code units. Negation binds before exponentiation, and chained powers are evaluated left-to-right, following [Microsoft’s operator precedence](https://support.microsoft.com/en-us/excel/calculation-operators-and-precedence). Formula parse failures become `#ERROR!` in runtime values; unknown function calls become `#NAME?`. Bare unknown names are currently syntax errors, not named-range lookups. Cycles use Nori's `#CYCLE!` error. Evaluation resource bounds use `#NUM!`. The editor displays `[array]` for array-valued cells and exposes arrays to custom renderers. It supports roving cell focus and arrow navigation, but does not claim full Excel keyboard or ARIA grid parity. ## Imported layout and saved views - Explicit column widths, row heights and worksheet defaults are preserved in logical units. Column conversion assumes a seven-unit maximum digit width; it is approximate for nondefault fonts. See [Microsoft's column width definition](https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.column). Row heights use a 96-unit-per-inch coordinate convention. Font measurement and automatic row-height fitting are not implemented. - Merged cells, wrap text, and vertical alignment are imported. Explicit row heights control wrapping; the renderer clips overflow rather than expanding an imported row unpredictably. Single-cell/overlapping merges and nonempty covered cells are rejected as invalid canonical data. - Frozen and frozen-split panes with integer row/column counts are supported. Ordinary split panes and the saved scroll origin are not reproduced. Leading frozen cells use sticky placement; a merge spanning a freeze boundary scrolls as one cell on that axis. - Hidden rows/columns are omitted without renumbering addresses. Hidden sheets remain visible and receive a warning. Hidden state is presentation metadata, not access control. - Worksheet AutoFilter value lists (including blanks) and one/two custom comparisons with AND/OR are supported. Equality supports `*`, `?` and `~` escapes. Date grouping, dynamic/time-relative, top-N, color and icon filters receive warnings; their saved hidden-row state is retained. Supported criteria additionally filter current calculated values. Excel table-level filters/sorts are not imported and receive a warning. - Value-sort metadata and direction indicators are preserved. XLSX stores its saved sorted data in worksheet order, which Nori retains. Import does not apply the sort a second time. Interactive sort/filter editing menus, re-sorting after cell edits, left-to-right sorting, and color/icon/custom-list sort semantics are not implemented. CSV import has its own [format and inference contract](/nori/csv.html). CSV export is not implemented. --- Source: docs/troubleshooting.md Page: /nori/troubleshooting.html Markdown: /nori/markdown/troubleshooting.md # Troubleshooting | Symptom | Check and remedy | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Root import pulls in React | Import `/core` or the root; only `/react` should require React. Verify the installed archive is the latest build. | | My formula displays `#NAME?` | Function is not registered. Consult the current support list; common Excel functions are still missing. | | My formula displays `#ERROR!` | Syntax is unsupported or malformed. Use `parseFormula` to inspect the parse Result. | | Numbers do not update after editing snapshot fields | Snapshots are immutable. Use `dispatch`, then read `getCellValue`. | | Merging is rejected | Covered cells must be blank and the range must not partially overlap an existing merge. Move content intentionally first. | | Resizing does not work | Check `readOnly`, host pointer-event CSS, and whether you are using the always-read-only preview. | | Double-click does not open an editor | Use `SheetGrid`/`Spreadsheet`, not the preview; ensure read-only is disabled. Enter/F2 also starts editing. Host overlays may intercept events. | | Imported columns look clipped | Explicit widths/heights are preserved approximately; automatic font measurement and row fitting are not implemented. | | Rows are missing | Check explicit hidden rows and saved filter criteria. Original row numbers are retained. | | Sorted data does not re-sort after an edit | Saved XLSX order is preserved; interactive re-sorting is not implemented. | | My editor jumps back to its initial state | Keep the live workbook instance stable across React renders. | | The preview changes the wrong tab | Preview tabs are local by default; use its `sheetId` and `onSheetChange` for controlled state. | | A stylesheet import fails | Nori has no public CSS export. Apply host CSS to documented classes/variables. | | A huge file blocks the UI | Parsing is synchronous. Move it to a host-owned worker/isolation adapter and enforce upload budgets. | If diagnosing an import issue, retain the import warnings, a minimal reproducible workbook, the package version, and expected versus actual values/layout. Do not include sensitive workbook contents in public reports. --- Source: docs/verification.md Page: /nori/verification.html Markdown: /nori/markdown/verification.md # Verification record Verified locally on 18 September 2026 using Node 22.20.0 for the initial milestone and Node 25.2.1 for later local checks. Supported repository environments remain Node 22.12+; Vitest 5 officially supports Node 22, 24 and 26+ rather than the local odd-numbered Node 25 runtime. - Strict workspace typecheck and separate headless compilation with only ES2022 ambient types. - Package dependency graph and forbidden-platform-global checks. - 107 tests covering model validation/address properties, XLSX import/errors/limits, formula evaluation/errors/registry/graph, commands/history/selection/layout, pivots, and React upload-path rendering/editing/tab behavior. - Public ESM/declaration build and Vite production demo build. - Packed archive installed in a temporary standalone consumer: every public subpath resolves, headless imports work without React installed, declarations compile without DOM/Node ambient types, bundling core excludes XLSX/React, and React SSR works after installing React. - Independent OOXML interoperability: openpyxl 3.1.5 reads the hand-authored sample, and Nori reads/recalculates a separately generated openpyxl workbook. - Browser verification in local Chrome via agent-browser: loaded demo, uploaded basic.xlsx, edited Sales B2 from 1200 to 100, observed total 1550.00, switched to Summary and observed 1550, then undid and observed 2650. No application console errors or framework error overlay remained. At a 390px viewport the page fits horizontally and the grid scrolls within its container. - Extended browser checks cover double-click inline editing, formula recalculation, Escape cancellation, read-only toggling, pointer drag with edge auto-scroll, Shift selection, merge/unmerge, row/column resize from headers and both sides of body borders, full-boundary hover feedback and undo, an independently generated XLSX with imported widths, hidden/filter rows, frozen panes and saved sort indicators, plus a 390px read-only chat preview. Run `npm run verify:browser` with the demo running; set `NORI_BROWSER_EXECUTABLE` if Chrome is not automatically available. Screenshots are saved under ignored `artifacts/`. - Dependency audit reported zero vulnerabilities after locking the initial toolchain. The production build emits two harmless annotation-placement warnings from Zod dependency comments. The package bundling check can report ignored bare imports because generated chunks are explicitly side-effect-free; the behavioral and installed-consumer checks pass. No React Native device test, real Excel/LibreOffice corpus, adversarial archive isolation test, or large-workbook performance benchmark is claimed. See the support matrix and roadmap for limitations. Documentation verification: VitePress production build, 17 rendered pages, local search, desktop/mobile navigation, no console errors, agent text exports with SHA-256 consistency checks, copied declaration dependency closure, and executable/typechecked examples. The docs target modern ES2022-capable browsers. VitePress 1 uses an explicit Vite 6.4.3 override to avoid older development-server advisories; the resolved dependency audit reports zero vulnerabilities. The live-playground browser check covers embedded selection, editing and recalculation, cross-sheet totals, undo, read-only mode, reset, importing the hosted XLSX sample, and narrow mobile layout. Run `npm run docs:verify-playground`, setting `NORI_DOCS_URL` to a served documentation base URL. CSV coverage includes quotes/escapes, embedded line endings, BOM, ragged and empty rows, optional inference, literal formula strings, invalid syntax, parsing budgets, and randomized quoted-record round trips. The installed-consumer check resolves `/csv` without React or XLSX dependencies, and the playground check imports the hosted CSV sample before calculating a total.