@particle-academy/fancy-sheets 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +392 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,392 @@
1
+ # @particle-academy/fancy-sheets
2
+
3
+ Spreadsheet editor with formula engine, multi-sheet tabs, and full cell editing. Part of the `@particle-academy` component ecosystem.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # npm
9
+ npm install @particle-academy/fancy-sheets
10
+
11
+ # pnpm
12
+ pnpm add @particle-academy/fancy-sheets
13
+
14
+ # yarn
15
+ yarn add @particle-academy/fancy-sheets
16
+ ```
17
+
18
+ **Peer dependencies:** `react >= 18`, `react-dom >= 18`, `@particle-academy/react-fancy >= 1.5`
19
+
20
+ ## Usage
21
+
22
+ ```css
23
+ @import "tailwindcss";
24
+ @import "@particle-academy/fancy-sheets/styles.css";
25
+ @source "../node_modules/@particle-academy/fancy-sheets/dist/**/*.js";
26
+ ```
27
+
28
+ ```tsx
29
+ import { Spreadsheet, createEmptyWorkbook } from "@particle-academy/fancy-sheets";
30
+ import "@particle-academy/fancy-sheets/styles.css";
31
+
32
+ function App() {
33
+ const [data, setData] = useState(createEmptyWorkbook());
34
+
35
+ return (
36
+ <div style={{ height: 500 }}>
37
+ <Spreadsheet data={data} onChange={setData}>
38
+ <Spreadsheet.Toolbar />
39
+ <Spreadsheet.Grid />
40
+ <Spreadsheet.SheetTabs />
41
+ </Spreadsheet>
42
+ </div>
43
+ );
44
+ }
45
+ ```
46
+
47
+ The spreadsheet fills its container — set a fixed height on the parent element.
48
+
49
+ ## Commands
50
+
51
+ ```bash
52
+ pnpm --filter @particle-academy/fancy-sheets build # Build with tsup (ESM + CJS + DTS)
53
+ pnpm --filter @particle-academy/fancy-sheets dev # Watch mode
54
+ pnpm --filter @particle-academy/fancy-sheets lint # Type-check (tsc --noEmit)
55
+ pnpm --filter @particle-academy/fancy-sheets clean # Remove dist/
56
+ ```
57
+
58
+ ## Component API
59
+
60
+ ### Compound Components
61
+
62
+ | Component | Description |
63
+ |-----------|-------------|
64
+ | `Spreadsheet` | Root wrapper — state management, context provider |
65
+ | `Spreadsheet.Toolbar` | Undo/redo, bold/italic/align buttons, formula bar |
66
+ | `Spreadsheet.Grid` | Editable cell grid with headers, selection, editing |
67
+ | `Spreadsheet.SheetTabs` | Multi-sheet tab bar with add/rename/delete |
68
+
69
+ ### Spreadsheet Props
70
+
71
+ ```ts
72
+ interface SpreadsheetProps {
73
+ children: ReactNode;
74
+ className?: string;
75
+ data?: WorkbookData; // Controlled workbook data
76
+ defaultData?: WorkbookData; // Uncontrolled initial data
77
+ onChange?: (data: WorkbookData) => void;
78
+ columnCount?: number; // Default: 26 (A-Z)
79
+ rowCount?: number; // Default: 100
80
+ defaultColumnWidth?: number; // Default: 100px
81
+ rowHeight?: number; // Default: 28px
82
+ readOnly?: boolean; // Default: false
83
+ }
84
+ ```
85
+
86
+ ### useSpreadsheet Hook
87
+
88
+ Access the spreadsheet context from custom components:
89
+
90
+ ```tsx
91
+ import { useSpreadsheet } from "@particle-academy/fancy-sheets";
92
+
93
+ function CustomToolbar() {
94
+ const { selection, setCellValue, setCellFormat, undo, redo } = useSpreadsheet();
95
+ // Build custom toolbar buttons
96
+ }
97
+ ```
98
+
99
+ **Context value:**
100
+
101
+ | Property / Method | Description |
102
+ |-------------------|-------------|
103
+ | `workbook` | Current WorkbookData |
104
+ | `activeSheet` | The currently active SheetData |
105
+ | `selection` | Current SelectionState (activeCell, ranges) |
106
+ | `editingCell` | Address of cell being edited (null if not editing) |
107
+ | `editValue` | Current value in the cell editor |
108
+ | `setCellValue(addr, value)` | Set a cell's value (triggers formula recalc) |
109
+ | `setCellFormat(addrs, format)` | Apply formatting to cells |
110
+ | `setSelection(cell)` | Select a single cell |
111
+ | `extendSelection(cell)` | Extend selection range (shift+click) |
112
+ | `addSelection(cell)` | Add to selection (ctrl+click) |
113
+ | `navigate(direction, extend?)` | Move active cell (arrow keys) |
114
+ | `startEdit(value?)` | Enter edit mode |
115
+ | `confirmEdit()` | Confirm edit and move down |
116
+ | `cancelEdit()` | Cancel edit (Escape) |
117
+ | `resizeColumn(col, width)` | Set column width |
118
+ | `addSheet()` | Add a new sheet |
119
+ | `renameSheet(id, name)` | Rename a sheet |
120
+ | `deleteSheet(id)` | Delete a sheet |
121
+ | `setActiveSheet(id)` | Switch active sheet |
122
+ | `undo()` / `redo()` | Undo/redo (50-step history) |
123
+ | `canUndo` / `canRedo` | Whether undo/redo is available |
124
+ | `getColumnWidth(col)` | Get column width in px |
125
+ | `isCellSelected(addr)` | Check if cell is in selection |
126
+ | `isCellActive(addr)` | Check if cell is the active cell |
127
+
128
+ ## Data Model
129
+
130
+ ### WorkbookData
131
+
132
+ ```ts
133
+ interface WorkbookData {
134
+ sheets: SheetData[];
135
+ activeSheetId: string;
136
+ }
137
+ ```
138
+
139
+ ### SheetData
140
+
141
+ ```ts
142
+ interface SheetData {
143
+ id: string;
144
+ name: string;
145
+ cells: CellMap; // Record<CellAddress, CellData>
146
+ columnWidths: Record<number, number>; // Column width overrides
147
+ mergedRegions: MergedRegion[];
148
+ columnFilters: Record<number, string>;
149
+ sortColumn?: number;
150
+ sortDirection?: "asc" | "desc";
151
+ frozenRows: number;
152
+ frozenCols: number;
153
+ }
154
+ ```
155
+
156
+ ### CellData
157
+
158
+ ```ts
159
+ interface CellData {
160
+ value: CellValue; // string | number | boolean | null
161
+ formula?: string; // Without leading "=", e.g. "SUM(A1:A5)"
162
+ computedValue?: CellValue; // Result of formula evaluation
163
+ format?: CellFormat; // { bold?, italic?, textAlign? }
164
+ }
165
+ ```
166
+
167
+ ### Helpers
168
+
169
+ ```tsx
170
+ import { createEmptyWorkbook, createEmptySheet } from "@particle-academy/fancy-sheets";
171
+
172
+ const workbook = createEmptyWorkbook(); // One empty sheet
173
+ const sheet = createEmptySheet("my-id", "My Sheet"); // Custom sheet
174
+ ```
175
+
176
+ ## Formula Engine
177
+
178
+ Type `=` in any cell to enter a formula. The engine supports cell references, ranges, operators, and functions.
179
+
180
+ ### Cell References
181
+
182
+ | Syntax | Example | Description |
183
+ |--------|---------|-------------|
184
+ | Cell ref | `=A1` | Single cell reference |
185
+ | Range | `=A1:B5` | Rectangular range |
186
+ | Arithmetic | `=A1+B1*2` | Operators: `+`, `-`, `*`, `/`, `^` |
187
+ | Comparison | `=A1>10` | Operators: `=`, `<>`, `<`, `>`, `<=`, `>=` |
188
+ | Concatenation | `=A1&" "&B1` | String join with `&` |
189
+
190
+ ### Built-in Functions
191
+
192
+ **Math:**
193
+
194
+ | Function | Example | Description |
195
+ |----------|---------|-------------|
196
+ | `SUM` | `=SUM(A1:A10)` | Sum of values |
197
+ | `AVERAGE` | `=AVERAGE(B1:B5)` | Mean of values |
198
+ | `MIN` | `=MIN(C1:C10)` | Minimum value |
199
+ | `MAX` | `=MAX(C1:C10)` | Maximum value |
200
+ | `COUNT` | `=COUNT(A1:A20)` | Count of numeric values |
201
+ | `ROUND` | `=ROUND(A1, 2)` | Round to N decimals |
202
+ | `ABS` | `=ABS(A1)` | Absolute value |
203
+
204
+ **Text:**
205
+
206
+ | Function | Example | Description |
207
+ |----------|---------|-------------|
208
+ | `UPPER` | `=UPPER(A1)` | Convert to uppercase |
209
+ | `LOWER` | `=LOWER(A1)` | Convert to lowercase |
210
+ | `LEN` | `=LEN(A1)` | String length |
211
+ | `TRIM` | `=TRIM(A1)` | Remove leading/trailing whitespace |
212
+ | `CONCAT` | `=CONCAT(A1,B1,C1)` | Join values |
213
+
214
+ **Logic:**
215
+
216
+ | Function | Example | Description |
217
+ |----------|---------|-------------|
218
+ | `IF` | `=IF(A1>10,"High","Low")` | Conditional value |
219
+ | `AND` | `=AND(A1>0,B1>0)` | All conditions true |
220
+ | `OR` | `=OR(A1>0,B1>0)` | Any condition true |
221
+ | `NOT` | `=NOT(A1)` | Negate boolean |
222
+
223
+ ### Custom Functions
224
+
225
+ Register custom formula functions:
226
+
227
+ ```tsx
228
+ import { registerFunction } from "@particle-academy/fancy-sheets";
229
+
230
+ registerFunction("DOUBLE", (args) => {
231
+ const val = args.flat()[0];
232
+ return typeof val === "number" ? val * 2 : "#VALUE!";
233
+ });
234
+ // Now use =DOUBLE(A1) in any cell
235
+ ```
236
+
237
+ ### Error Values
238
+
239
+ | Error | Cause |
240
+ |-------|-------|
241
+ | `#ERROR!` | Invalid formula syntax |
242
+ | `#VALUE!` | Wrong value type for operation |
243
+ | `#DIV/0!` | Division by zero |
244
+ | `#NAME?` | Unknown function name |
245
+ | `#CIRC!` | Circular reference detected |
246
+
247
+ ### Dependency Tracking
248
+
249
+ The formula engine builds a dependency graph and recalculates in topological order. When cell A1 changes, all cells that reference A1 (directly or transitively) are automatically recalculated. Circular references are detected and marked with `#CIRC!`.
250
+
251
+ ## Features
252
+
253
+ ### Editing
254
+ - Click to select, double-click or type to start editing
255
+ - Enter confirms and moves down, Tab moves right
256
+ - Escape cancels edit
257
+ - Delete/Backspace clears cell
258
+ - Formula bar shows formula for selected cell
259
+
260
+ ### Navigation
261
+ - Arrow keys move between cells
262
+ - Shift+Arrow extends selection range
263
+ - Tab/Shift+Tab moves right/left
264
+ - Ctrl+Z undo, Ctrl+Y redo
265
+
266
+ ### Selection
267
+ - Click to select single cell
268
+ - Shift+Click to select range
269
+ - Ctrl+Click for multi-select
270
+ - Visual blue overlay on selected ranges
271
+
272
+ ### Formatting
273
+ - Bold (B), Italic (I) toggle buttons
274
+ - Text align: left, center, right
275
+ - Applied via toolbar or programmatically
276
+
277
+ ### Clipboard
278
+ - Ctrl+C copies selected range as TSV
279
+ - Ctrl+V pastes TSV data starting at active cell
280
+ - Works with external data from Excel/Google Sheets
281
+
282
+ ### CSV Support
283
+
284
+ ```tsx
285
+ import { parseCSV, csvToWorkbook, workbookToCSV } from "@particle-academy/fancy-sheets";
286
+
287
+ // Import
288
+ const workbook = csvToWorkbook(csvString, "Imported Data");
289
+
290
+ // Export
291
+ const csv = workbookToCSV(workbook);
292
+ ```
293
+
294
+ ### Multi-Sheet
295
+ - Tab bar at bottom shows all sheets
296
+ - Click to switch, double-click to rename
297
+ - "+" button adds new sheet
298
+ - Delete button removes active sheet (minimum 1)
299
+
300
+ ### Column Resize
301
+ - Drag column header borders to resize
302
+ - Minimum width: 30px
303
+
304
+ ### Undo/Redo
305
+ - 50-step history
306
+ - Every cell edit, format change pushes to undo stack
307
+ - Ctrl+Z / Ctrl+Y keyboard shortcuts
308
+
309
+ ## Customization
310
+
311
+ All components render `data-fancy-sheets-*` attributes:
312
+
313
+ | Attribute | Element |
314
+ |-----------|---------|
315
+ | `data-fancy-sheets` | Root container |
316
+ | `data-fancy-sheets-toolbar` | Toolbar area |
317
+ | `data-fancy-sheets-formula-bar` | Formula bar |
318
+ | `data-fancy-sheets-grid` | Grid container |
319
+ | `data-fancy-sheets-column-headers` | Column header row |
320
+ | `data-fancy-sheets-row-header` | Row number cell |
321
+ | `data-fancy-sheets-cell` | Individual cell |
322
+ | `data-fancy-sheets-cell-editor` | Inline edit input |
323
+ | `data-fancy-sheets-selection` | Selection overlay |
324
+ | `data-fancy-sheets-resize-handle` | Column resize handle |
325
+ | `data-fancy-sheets-tabs` | Sheet tab bar |
326
+
327
+ ## Architecture
328
+
329
+ ```
330
+ src/
331
+ ├── types/
332
+ │ ├── cell.ts # CellValue, CellData, CellFormat
333
+ │ ├── sheet.ts # SheetData, WorkbookData, helpers
334
+ │ ├── selection.ts # CellRange, SelectionState
335
+ │ └── formula.ts # FormulaToken, FormulaASTNode
336
+ ├── engine/
337
+ │ ├── cell-utils.ts # Address parsing (A1 <-> row/col)
338
+ │ ├── clipboard.ts # TSV serialize/parse
339
+ │ ├── csv.ts # CSV import/export
340
+ │ ├── sorting.ts # Column sort
341
+ │ ├── history.ts # Undo/redo stack
342
+ │ └── formula/
343
+ │ ├── lexer.ts # Tokenize formula strings
344
+ │ ├── parser.ts # Recursive descent -> AST
345
+ │ ├── evaluator.ts # Evaluate AST with cell lookups
346
+ │ ├── references.ts # Extract cell refs from AST
347
+ │ ├── dependency-graph.ts # Dep tracking, circular detection
348
+ │ └── functions/
349
+ │ ├── registry.ts # Function registry
350
+ │ ├── math.ts # SUM, AVERAGE, MIN, MAX, etc.
351
+ │ ├── text.ts # UPPER, LOWER, LEN, TRIM, CONCAT
352
+ │ └── logic.ts # IF, AND, OR, NOT
353
+ ├── hooks/
354
+ │ └── use-spreadsheet-store.ts # Central reducer + actions
355
+ ├── components/
356
+ │ ├── Spreadsheet/ # Root compound component
357
+ │ ├── Toolbar/ # Format buttons + formula bar
358
+ │ ├── Grid/ # Cell grid, editor, selection, resize
359
+ │ └── SheetTabs/ # Multi-sheet tab bar
360
+ ├── index.ts # Public API
361
+ └── styles.css # Base styles
362
+ ```
363
+
364
+ ## Demo
365
+
366
+ Demo page at `/react-demos/spreadsheet` in the monorepo with pre-populated product catalog data and formulas.
367
+
368
+ ---
369
+
370
+ ## Agent Guidelines
371
+
372
+ ### Component Pattern
373
+ - Same compound component pattern as react-fancy: `Object.assign(Root, { Toolbar, Grid, SheetTabs })`
374
+ - Context via `SpreadsheetContext` + `useSpreadsheet()` hook
375
+ - State managed by `useReducer` with action-based mutations
376
+
377
+ ### Formula Engine
378
+ - Pure functions in `engine/formula/` — no React imports
379
+ - Lexer → Parser → Evaluator pipeline
380
+ - Dependency graph rebuilt on cell changes, topological sort for recalc order
381
+ - Functions registered via side-effect imports (listed in `sideEffects` in package.json)
382
+
383
+ ### Data Model
384
+ - Sparse cell map (`Record<CellAddress, CellData>`) — only stores non-empty cells
385
+ - Formulas stored as strings, computed values cached in `computedValue`
386
+ - Undo/redo via workbook snapshot stack (max 50)
387
+
388
+ ### Build
389
+ - tsup: ESM, CJS, DTS
390
+ - External: react, react-dom, @particle-academy/react-fancy
391
+ - Zero other dependencies
392
+ - Verify with `npm run build` from monorepo root
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-sheets",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Spreadsheet editor with formula engine, multi-sheet tabs, and full cell editing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",