@particle-academy/fancy-sheets 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ # Recipe: CSV round-trip
2
+
3
+ The CSV utilities are a complete, dependency-free library — usable headless (Node
4
+ import pipelines, exports) or in the browser.
5
+
6
+ ```ts
7
+ import {
8
+ csvToWorkbook,
9
+ workbookToCSV,
10
+ parseCSV,
11
+ stringifyCSV,
12
+ } from "@particle-academy/fancy-sheets";
13
+ ```
14
+
15
+ ## Workbook ⇄ CSV
16
+
17
+ ```ts
18
+ // CSV string → WorkbookData (one sheet)
19
+ const wb = csvToWorkbook("Name,Score\nAda,90\nGrace,95", "Results");
20
+
21
+ // WorkbookData → CSV string. Omitting sheetId uses the active sheet.
22
+ const csv = workbookToCSV(wb);
23
+ const csv2 = workbookToCSV(wb, "sheet-id-2"); // a specific sheet
24
+ ```
25
+
26
+ `workbookToCSV` emits **computed** values for formula cells — run
27
+ `recalculateWorkbook(wb)` first if the workbook was assembled by hand so the
28
+ export carries final numbers, not blanks:
29
+
30
+ ```ts
31
+ import { recalculateWorkbook } from "@particle-academy/fancy-sheets";
32
+ const csv = workbookToCSV(recalculateWorkbook(wb));
33
+ ```
34
+
35
+ ## Raw grid ⇄ CSV
36
+
37
+ When you just want the 2-D array (no workbook wrapper):
38
+
39
+ ```ts
40
+ const rows = parseCSV("a,b\n1,2"); // [["a","b"], ["1","2"]]
41
+ const text = stringifyCSV(rows); // 'a,b\n1,2'
42
+ ```
43
+
44
+ ## Gotchas
45
+
46
+ - **Quoting & escaping** are handled — fields containing commas, quotes, or
47
+ newlines are round-tripped via standard `"…"` quoting with `""` escaping.
48
+ - **Everything is a string on import.** `csvToWorkbook` does not infer numeric
49
+ types; cells hold the raw text. Coerce in your own adapter if you need numbers
50
+ (or let formulas referencing them do the coercion).
51
+ - **One sheet per CSV.** CSV has no notion of multiple sheets — `csvToWorkbook`
52
+ produces a single-sheet workbook, and `workbookToCSV` serializes one sheet at a
53
+ time. Loop over `wb.sheets` and concatenate if you need a multi-tab export.
@@ -0,0 +1,56 @@
1
+ # Recipe: custom formula functions
2
+
3
+ Register your own functions with `registerFunction`. They're available in every
4
+ workbook (the registry is module-global) and callable from any cell formula.
5
+
6
+ ```ts
7
+ import { registerFunction } from "@particle-academy/fancy-sheets";
8
+ import type { FormulaRangeFunction } from "@particle-academy/fancy-sheets";
9
+ ```
10
+
11
+ ## The signature
12
+
13
+ ```ts
14
+ type FormulaRangeFunction = (args: CellValue[][]) => CellValue;
15
+ ```
16
+
17
+ Each argument arrives as an **array of cell values** — because any argument can be
18
+ a range. A scalar argument is a one-element array; a range like `A1:A10` is the
19
+ full list of values. Return any `CellValue` (`number | string | boolean | null`).
20
+ Throwing is allowed — the cell shows `#ERROR!`.
21
+
22
+ ## Scalar example
23
+
24
+ ```ts
25
+ registerFunction("GREET", (args) => {
26
+ const name = args[0]?.[0] ?? "world";
27
+ return `Hello, ${name}`;
28
+ });
29
+ // =GREET("Ada") → "Hello, Ada"
30
+ ```
31
+
32
+ ## Range example — `=NPV(rate, cashflows…)`
33
+
34
+ ```ts
35
+ registerFunction("NPV", (args) => {
36
+ const rate = Number(args[0]?.[0] ?? 0);
37
+ const flows = args.slice(1).flat().map(Number); // flatten all range args
38
+ return flows.reduce((acc, cf, i) => acc + cf / (1 + rate) ** (i + 1), 0);
39
+ });
40
+ // =NPV(0.1, B2:B6)
41
+ ```
42
+
43
+ ## Register early
44
+
45
+ Call `registerFunction` once at module load, before the grid renders (or before
46
+ `recalculateWorkbook` runs headlessly), so the evaluator can find it:
47
+
48
+ ```ts
49
+ // formulas.ts — imported once from your app entry
50
+ registerFunction("TAXED", (args) => Number(args[0]?.[0] ?? 0) * 1.2);
51
+ ```
52
+
53
+ > Names are case-insensitive and upper-cased internally; `=taxed(A1)` and
54
+ > `=TAXED(A1)` resolve to the same function. Re-registering a name overrides it.
55
+
56
+ See [docs/formulas.md](../formulas.md) for the 80+ built-ins.
@@ -0,0 +1,71 @@
1
+ # Recipe: external state sync + autosave
2
+
3
+ Drive `<SheetWorkbook>` from a controlled `data` prop that changes from *outside*
4
+ the component — a server round-trip, an agent bridge, undo/redo, a websocket. As
5
+ of **0.7.6** an externally-replaced `data` prop recalculates correctly (formulas
6
+ re-evaluate against the new cells), so this pattern is solid.
7
+
8
+ ## Controlled component
9
+
10
+ `data` + `onChange` make `<SheetWorkbook>` fully controlled. Hold the workbook in
11
+ your own state and feed it back:
12
+
13
+ ```tsx
14
+ import { SheetWorkbook } from "@particle-academy/fancy-sheets";
15
+ import type { WorkbookData } from "@particle-academy/fancy-sheets";
16
+ import { useState } from "react";
17
+
18
+ function Editor({ initial }: { initial: WorkbookData }) {
19
+ const [wb, setWb] = useState(initial);
20
+ return <SheetWorkbook data={wb} onChange={setWb} />;
21
+ }
22
+ ```
23
+
24
+ Because it's controlled, *anything* can set `wb` — the grid reflects it on the
25
+ next render and re-runs formulas.
26
+
27
+ ## Autosave (debounced)
28
+
29
+ `onChange` fires on every mutation. Debounce it to persist:
30
+
31
+ ```tsx
32
+ function AutosaveEditor({ initial, save }: { initial: WorkbookData; save: (w: WorkbookData) => Promise<void> }) {
33
+ const [wb, setWb] = useState(initial);
34
+ const timer = useRef<ReturnType<typeof setTimeout>>();
35
+
36
+ const onChange = (next: WorkbookData) => {
37
+ setWb(next);
38
+ clearTimeout(timer.current);
39
+ timer.current = setTimeout(() => void save(next), 600);
40
+ };
41
+
42
+ return <SheetWorkbook data={wb} onChange={onChange} />;
43
+ }
44
+ ```
45
+
46
+ ## Agent bridge / collaborative edits
47
+
48
+ Apply remote patches by replacing `data`. Run `recalculateWorkbook` first if the
49
+ patch source didn't compute values (e.g. a raw agent edit):
50
+
51
+ ```tsx
52
+ import { recalculateWorkbook } from "@particle-academy/fancy-sheets";
53
+
54
+ socket.on("workbook:patch", (incoming: WorkbookData) => {
55
+ setWb(recalculateWorkbook(incoming)); // ensure computedValues before paint
56
+ });
57
+ ```
58
+
59
+ ## Undo/redo from outside
60
+
61
+ The component has its own internal undo stack, but if you manage history yourself
62
+ (to coordinate with non-grid state), just push/pop full `WorkbookData` snapshots
63
+ and set them as `data`:
64
+
65
+ ```tsx
66
+ const undo = () => { const prev = history.pop(); if (prev) setWb(prev); };
67
+ ```
68
+
69
+ > Tip: keep one source of truth. Either let the component own state (`defaultData`,
70
+ > read via `onChange`) **or** control it (`data` + `onChange`) — don't mix
71
+ > `defaultData` and `data` on the same instance.
@@ -0,0 +1,67 @@
1
+ # Recipe: headless recalculation (Node, SSR, exports, tests)
2
+
3
+ The formula engine is pure — no React, no DOM. You can compute formula
4
+ `computedValue`s on a server, in a worker, or in a test, without booting a
5
+ component.
6
+
7
+ ```ts
8
+ import { recalculateWorkbook, createEmptyWorkbook } from "@particle-academy/fancy-sheets";
9
+ import type { WorkbookData } from "@particle-academy/fancy-sheets";
10
+
11
+ const wb = createEmptyWorkbook();
12
+ wb.sheets[0].cells = {
13
+ A1: { value: 10 },
14
+ A2: { value: 20 },
15
+ A3: { value: null, formula: "SUM(A1:A2)" }, // note: no leading "="
16
+ B1: { value: null, formula: "A3*2" },
17
+ };
18
+
19
+ const computed = recalculateWorkbook(wb);
20
+ computed.sheets[0].cells.A3.computedValue; // 30
21
+ computed.sheets[0].cells.B1.computedValue; // 60
22
+ ```
23
+
24
+ `recalculateWorkbook` returns a **new** workbook (inputs are not mutated), runs a
25
+ two-pass sweep so cross-sheet references resolve in any order, topologically
26
+ orders dependent cells, and marks circular references `#CIRC!` / runtime failures
27
+ `#ERROR!`.
28
+
29
+ ## Single sheet
30
+
31
+ ```ts
32
+ import { recalculateSheet } from "@particle-academy/fancy-sheets";
33
+
34
+ const sheet = recalculateSheet(mySheet); // intra-sheet only
35
+ const sheet2 = recalculateSheet(mySheet, allSheets); // pass siblings for cross-sheet refs
36
+ ```
37
+
38
+ ## Server-rendered preview (no recalc flash)
39
+
40
+ Compute on the server so the initial HTML already carries final values:
41
+
42
+ ```ts
43
+ // Laravel/Inertia controller, Next.js loader, etc.
44
+ const props = { workbook: recalculateWorkbook(snapshot) };
45
+ // → <SheetWorkbook data={props.workbook} /> renders final values on first paint
46
+ ```
47
+
48
+ ## Low-level lex / parse / evaluate
49
+
50
+ For custom pipelines (linting, transforms, your own grid), the three stages are
51
+ exported too. The formula string is the body **without** the leading `=`:
52
+
53
+ ```ts
54
+ import { lexFormula, parseFormula, evaluateAST } from "@particle-academy/fancy-sheets";
55
+
56
+ const ast = parseFormula(lexFormula("2 + 3 * 4"));
57
+ const value = evaluateAST(
58
+ ast,
59
+ (address) => cellValues[address] ?? null, // getCellValue
60
+ (start, end) => rangeValues(start, end), // getRangeValues
61
+ /* ctx */ { getSheetCellValue, getSheetRangeValues } // optional, for Sheet!A1 refs
62
+ );
63
+ // value === 14
64
+ ```
65
+
66
+ `evaluateAST` is provider-agnostic: you supply the cell/range getters, so it can
67
+ read from any backing store (a plain object, a DB row, an agent's snapshot).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-sheets",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "Spreadsheet editor with formula engine, multi-sheet tabs, and full cell editing",
5
5
  "repository": {
6
6
  "type": "git",