@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.
- package/README.md +392 -0
- 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