@particle-academy/fancy-sheets 0.4.5 → 0.5.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.
- package/README.md +27 -438
- package/dist/index.cjs +75 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -3
- package/dist/index.d.ts +90 -3
- package/dist/index.js +74 -9
- package/dist/index.js.map +1 -1
- package/dist/styles.css.map +1 -1
- package/docs/Spreadsheet.md +216 -0
- package/docs/csv.md +75 -0
- package/docs/formulas.md +161 -0
- package/package.json +63 -56
package/README.md
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
# @particle-academy/fancy-sheets
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A full-featured spreadsheet component with formulas, formatting, selection, multi-sheet workbooks, clipboard, CSV import/export, and undo/redo. Custom engine — no third-party dependency.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# npm
|
|
9
8
|
npm install @particle-academy/fancy-sheets
|
|
10
|
-
|
|
11
|
-
#
|
|
12
|
-
pnpm add @particle-academy/fancy-sheets
|
|
13
|
-
|
|
14
|
-
# yarn
|
|
15
|
-
yarn add @particle-academy/fancy-sheets
|
|
9
|
+
# or: pnpm add @particle-academy/fancy-sheets
|
|
10
|
+
# or: yarn add @particle-academy/fancy-sheets
|
|
16
11
|
```
|
|
17
12
|
|
|
18
13
|
**Peer dependencies:** `react >= 18`, `react-dom >= 18`, `@particle-academy/react-fancy >= 1.5`
|
|
19
14
|
|
|
20
|
-
##
|
|
15
|
+
## Setup
|
|
21
16
|
|
|
22
17
|
```css
|
|
23
18
|
@import "tailwindcss";
|
|
@@ -25,16 +20,15 @@ yarn add @particle-academy/fancy-sheets
|
|
|
25
20
|
@source "../node_modules/@particle-academy/fancy-sheets/dist/**/*.js";
|
|
26
21
|
```
|
|
27
22
|
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
28
25
|
```tsx
|
|
29
|
-
import { Spreadsheet
|
|
30
|
-
import "@particle-academy/fancy-sheets/styles.css";
|
|
26
|
+
import { Spreadsheet } from "@particle-academy/fancy-sheets";
|
|
31
27
|
|
|
32
28
|
function App() {
|
|
33
|
-
const [data, setData] = useState(createEmptyWorkbook());
|
|
34
|
-
|
|
35
29
|
return (
|
|
36
|
-
<div style={{ height:
|
|
37
|
-
<Spreadsheet
|
|
30
|
+
<div style={{ height: 600 }}>
|
|
31
|
+
<Spreadsheet>
|
|
38
32
|
<Spreadsheet.Toolbar />
|
|
39
33
|
<Spreadsheet.Grid />
|
|
40
34
|
<Spreadsheet.SheetTabs />
|
|
@@ -44,7 +38,15 @@ function App() {
|
|
|
44
38
|
}
|
|
45
39
|
```
|
|
46
40
|
|
|
47
|
-
The
|
|
41
|
+
The component fills its container — wrap it in a sized element (height is required).
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
| Topic | Doc |
|
|
46
|
+
|-------|-----|
|
|
47
|
+
| Full component API (props, sub-components, `useSpreadsheet` hook, data model, keyboard shortcuts) | [docs/Spreadsheet.md](./docs/Spreadsheet.md) |
|
|
48
|
+
| 80+ built-in formula functions + custom function registration | [docs/formulas.md](./docs/formulas.md) |
|
|
49
|
+
| CSV import / export utilities | [docs/csv.md](./docs/csv.md) |
|
|
48
50
|
|
|
49
51
|
## Commands
|
|
50
52
|
|
|
@@ -55,428 +57,15 @@ pnpm --filter @particle-academy/fancy-sheets lint # Type-check (tsc --noEmit
|
|
|
55
57
|
pnpm --filter @particle-academy/fancy-sheets clean # Remove dist/
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
##
|
|
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 (80+)
|
|
191
|
-
|
|
192
|
-
**Math (25):**
|
|
193
|
-
|
|
194
|
-
| Function | Example | Description |
|
|
195
|
-
|----------|---------|-------------|
|
|
196
|
-
| `SUM` | `=SUM(A1:A10)` | Sum of values |
|
|
197
|
-
| `AVERAGE` | `=AVERAGE(B1:B5)` | Mean of values |
|
|
198
|
-
| `MEDIAN` | `=MEDIAN(A1:A10)` | Median value |
|
|
199
|
-
| `MIN` | `=MIN(C1:C10)` | Minimum value |
|
|
200
|
-
| `MAX` | `=MAX(C1:C10)` | Maximum value |
|
|
201
|
-
| `COUNT` | `=COUNT(A1:A20)` | Count of numeric values |
|
|
202
|
-
| `PRODUCT` | `=PRODUCT(A1:A5)` | Multiply all values |
|
|
203
|
-
| `ROUND` | `=ROUND(A1, 2)` | Round to N decimals |
|
|
204
|
-
| `ABS` | `=ABS(A1)` | Absolute value |
|
|
205
|
-
| `SQRT` | `=SQRT(A1)` | Square root |
|
|
206
|
-
| `POWER` | `=POWER(2,10)` | Exponentiation |
|
|
207
|
-
| `MOD` | `=MOD(10,3)` | Remainder |
|
|
208
|
-
| `INT` | `=INT(3.7)` | Round down to integer |
|
|
209
|
-
| `TRUNC` | `=TRUNC(3.789,1)` | Truncate decimals |
|
|
210
|
-
| `FLOOR` | `=FLOOR(2.7,1)` | Round down to multiple |
|
|
211
|
-
| `CEILING` | `=CEILING(2.1,1)` | Round up to multiple |
|
|
212
|
-
| `SIGN` | `=SIGN(-5)` | Returns -1, 0, or 1 |
|
|
213
|
-
| `FACT` | `=FACT(5)` | Factorial (120) |
|
|
214
|
-
| `PI` | `=PI()` | 3.14159... |
|
|
215
|
-
| `EXP` | `=EXP(1)` | e^n |
|
|
216
|
-
| `LN` | `=LN(10)` | Natural logarithm |
|
|
217
|
-
| `LOG` | `=LOG(100,10)` | Logarithm (default base 10) |
|
|
218
|
-
| `LOG10` | `=LOG10(1000)` | Base-10 logarithm |
|
|
219
|
-
| `RAND` | `=RAND()` | Random 0-1 |
|
|
220
|
-
| `RANDBETWEEN` | `=RANDBETWEEN(1,100)` | Random integer |
|
|
221
|
-
|
|
222
|
-
**Text (19):**
|
|
223
|
-
|
|
224
|
-
| Function | Example | Description |
|
|
225
|
-
|----------|---------|-------------|
|
|
226
|
-
| `UPPER` | `=UPPER(A1)` | Uppercase |
|
|
227
|
-
| `LOWER` | `=LOWER(A1)` | Lowercase |
|
|
228
|
-
| `PROPER` | `=PROPER("hello world")` | Title Case |
|
|
229
|
-
| `LEN` | `=LEN(A1)` | String length |
|
|
230
|
-
| `TRIM` | `=TRIM(A1)` | Remove whitespace |
|
|
231
|
-
| `LEFT` | `=LEFT(A1,3)` | First N characters |
|
|
232
|
-
| `RIGHT` | `=RIGHT(A1,3)` | Last N characters |
|
|
233
|
-
| `MID` | `=MID(A1,2,3)` | Substring from position |
|
|
234
|
-
| `FIND` | `=FIND("x",A1)` | Case-sensitive position |
|
|
235
|
-
| `SEARCH` | `=SEARCH("x",A1)` | Case-insensitive position |
|
|
236
|
-
| `SUBSTITUTE` | `=SUBSTITUTE(A1,"old","new")` | Replace text |
|
|
237
|
-
| `REPLACE` | `=REPLACE(A1,2,3,"xyz")` | Replace by position |
|
|
238
|
-
| `CONCAT` | `=CONCAT(A1,B1)` | Join values |
|
|
239
|
-
| `REPT` | `=REPT("*",5)` | Repeat string |
|
|
240
|
-
| `EXACT` | `=EXACT(A1,B1)` | Case-sensitive compare |
|
|
241
|
-
| `VALUE` | `=VALUE("42")` | Text to number |
|
|
242
|
-
| `TEXT` | `=TEXT(0.5,"0%")` | Format number as text |
|
|
243
|
-
| `CHAR` | `=CHAR(65)` | Character from code |
|
|
244
|
-
| `CODE` | `=CODE("A")` | Code of first character |
|
|
245
|
-
|
|
246
|
-
**Logic (8):**
|
|
247
|
-
|
|
248
|
-
| Function | Example | Description |
|
|
249
|
-
|----------|---------|-------------|
|
|
250
|
-
| `IF` | `=IF(A1>10,"High","Low")` | Conditional value |
|
|
251
|
-
| `AND` | `=AND(A1>0,B1>0)` | All conditions true |
|
|
252
|
-
| `OR` | `=OR(A1>0,B1>0)` | Any condition true |
|
|
253
|
-
| `NOT` | `=NOT(A1)` | Negate boolean |
|
|
254
|
-
| `IFERROR` | `=IFERROR(A1/B1,0)` | Fallback on error |
|
|
255
|
-
| `IFBLANK` | `=IFBLANK(A1,"N/A")` | Fallback if empty |
|
|
256
|
-
| `SWITCH` | `=SWITCH(A1,1,"One",2,"Two")` | Multi-case conditional |
|
|
257
|
-
| `CHOOSE` | `=CHOOSE(2,"a","b","c")` | Select by index |
|
|
258
|
-
|
|
259
|
-
**Conditional Aggregates (8):**
|
|
260
|
-
|
|
261
|
-
| Function | Example | Description |
|
|
262
|
-
|----------|---------|-------------|
|
|
263
|
-
| `SUMIF` | `=SUMIF(A1:A10,">5",B1:B10)` | Conditional sum |
|
|
264
|
-
| `SUMIFS` | `=SUMIFS(C1:C10,A1:A10,">5",B1:B10,"<10")` | Multi-criteria sum |
|
|
265
|
-
| `COUNTIF` | `=COUNTIF(A1:A10,"Yes")` | Conditional count |
|
|
266
|
-
| `COUNTIFS` | `=COUNTIFS(A1:A10,">5",B1:B10,"<10")` | Multi-criteria count |
|
|
267
|
-
| `AVERAGEIF` | `=AVERAGEIF(A1:A10,">0")` | Conditional average |
|
|
268
|
-
| `AVERAGEIFS` | `=AVERAGEIFS(C1:C10,A1:A10,">5")` | Multi-criteria average |
|
|
269
|
-
| `MINIFS` | `=MINIFS(C1:C10,A1:A10,">5")` | Conditional min |
|
|
270
|
-
| `MAXIFS` | `=MAXIFS(C1:C10,A1:A10,">5")` | Conditional max |
|
|
271
|
-
|
|
272
|
-
**Lookup (8):**
|
|
273
|
-
|
|
274
|
-
| Function | Example | Description |
|
|
275
|
-
|----------|---------|-------------|
|
|
276
|
-
| `VLOOKUP` | `=VLOOKUP(key,A1:C10,3)` | Vertical lookup |
|
|
277
|
-
| `HLOOKUP` | `=HLOOKUP(key,A1:Z3,2)` | Horizontal lookup |
|
|
278
|
-
| `INDEX` | `=INDEX(A1:A10,3)` | Value at position |
|
|
279
|
-
| `MATCH` | `=MATCH("x",A1:A10)` | Find position of value |
|
|
280
|
-
| `ROWS` | `=ROWS(A1:A10)` | Count rows in range |
|
|
281
|
-
| `COLUMNS` | `=COLUMNS(A1:C1)` | Count columns in range |
|
|
282
|
-
| `ROW` | `=ROW(A5)` | Row number |
|
|
283
|
-
| `COLUMN` | `=COLUMN(C1)` | Column number |
|
|
284
|
-
|
|
285
|
-
**Date/Time (12):**
|
|
286
|
-
|
|
287
|
-
| Function | Example | Description |
|
|
288
|
-
|----------|---------|-------------|
|
|
289
|
-
| `TODAY` | `=TODAY()` | Current date (serial) |
|
|
290
|
-
| `NOW` | `=NOW()` | Current date+time (serial) |
|
|
291
|
-
| `DATE` | `=DATE(2024,6,15)` | Create date |
|
|
292
|
-
| `YEAR` | `=YEAR(A1)` | Extract year |
|
|
293
|
-
| `MONTH` | `=MONTH(A1)` | Extract month |
|
|
294
|
-
| `DAY` | `=DAY(A1)` | Extract day |
|
|
295
|
-
| `HOUR` | `=HOUR(A1)` | Extract hour |
|
|
296
|
-
| `MINUTE` | `=MINUTE(A1)` | Extract minute |
|
|
297
|
-
| `SECOND` | `=SECOND(A1)` | Extract second |
|
|
298
|
-
| `WEEKDAY` | `=WEEKDAY(A1)` | Day of week (1-7) |
|
|
299
|
-
| `DATEDIF` | `=DATEDIF(A1,B1,"M")` | Date difference |
|
|
300
|
-
| `EDATE` | `=EDATE(A1,3)` | Date + N months |
|
|
301
|
-
|
|
302
|
-
**Info (6):**
|
|
303
|
-
|
|
304
|
-
| Function | Example | Description |
|
|
305
|
-
|----------|---------|-------------|
|
|
306
|
-
| `ISBLANK` | `=ISBLANK(A1)` | Is empty |
|
|
307
|
-
| `ISNUMBER` | `=ISNUMBER(A1)` | Is numeric |
|
|
308
|
-
| `ISTEXT` | `=ISTEXT(A1)` | Is text |
|
|
309
|
-
| `ISERROR` | `=ISERROR(A1)` | Is error value |
|
|
310
|
-
| `ISLOGICAL` | `=ISLOGICAL(A1)` | Is boolean |
|
|
311
|
-
| `TYPE` | `=TYPE(A1)` | Type code (1=num, 2=text, 4=bool, 16=error) |
|
|
312
|
-
|
|
313
|
-
### Custom Functions
|
|
314
|
-
|
|
315
|
-
Register custom formula functions:
|
|
316
|
-
|
|
317
|
-
```tsx
|
|
318
|
-
import { registerFunction } from "@particle-academy/fancy-sheets";
|
|
319
|
-
|
|
320
|
-
registerFunction("DOUBLE", (args) => {
|
|
321
|
-
const val = args.flat()[0];
|
|
322
|
-
return typeof val === "number" ? val * 2 : "#VALUE!";
|
|
323
|
-
});
|
|
324
|
-
// Now use =DOUBLE(A1) in any cell
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### Error Values
|
|
328
|
-
|
|
329
|
-
| Error | Cause |
|
|
330
|
-
|-------|-------|
|
|
331
|
-
| `#ERROR!` | Invalid formula syntax |
|
|
332
|
-
| `#VALUE!` | Wrong value type for operation |
|
|
333
|
-
| `#DIV/0!` | Division by zero |
|
|
334
|
-
| `#NAME?` | Unknown function name |
|
|
335
|
-
| `#CIRC!` | Circular reference detected |
|
|
336
|
-
|
|
337
|
-
### Dependency Tracking
|
|
338
|
-
|
|
339
|
-
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!`.
|
|
340
|
-
|
|
341
|
-
## Features
|
|
342
|
-
|
|
343
|
-
### Editing
|
|
344
|
-
- Click to select, double-click or type to start editing
|
|
345
|
-
- Enter confirms and moves down, Tab moves right
|
|
346
|
-
- Escape cancels edit
|
|
347
|
-
- Delete/Backspace clears cell
|
|
348
|
-
- Formula bar shows formula for selected cell
|
|
349
|
-
|
|
350
|
-
### Navigation
|
|
351
|
-
- Arrow keys move between cells
|
|
352
|
-
- Shift+Arrow extends selection range
|
|
353
|
-
- Tab/Shift+Tab moves right/left
|
|
354
|
-
- Ctrl+Z undo, Ctrl+Y redo
|
|
355
|
-
|
|
356
|
-
### Selection
|
|
357
|
-
- Click to select single cell
|
|
358
|
-
- Shift+Click to select range
|
|
359
|
-
- Ctrl+Click for multi-select
|
|
360
|
-
- Visual blue overlay on selected ranges
|
|
361
|
-
|
|
362
|
-
### Formatting
|
|
363
|
-
- Bold (B), Italic (I) toggle buttons
|
|
364
|
-
- Text align: left, center, right
|
|
365
|
-
- Applied via toolbar or programmatically
|
|
366
|
-
|
|
367
|
-
### Clipboard
|
|
368
|
-
- Ctrl+C copies selected range as TSV
|
|
369
|
-
- Ctrl+V pastes TSV data starting at active cell
|
|
370
|
-
- Works with external data from Excel/Google Sheets
|
|
371
|
-
|
|
372
|
-
### CSV Support
|
|
373
|
-
|
|
374
|
-
```tsx
|
|
375
|
-
import { parseCSV, csvToWorkbook, workbookToCSV } from "@particle-academy/fancy-sheets";
|
|
376
|
-
|
|
377
|
-
// Import
|
|
378
|
-
const workbook = csvToWorkbook(csvString, "Imported Data");
|
|
379
|
-
|
|
380
|
-
// Export
|
|
381
|
-
const csv = workbookToCSV(workbook);
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### Multi-Sheet
|
|
385
|
-
- Tab bar at bottom shows all sheets
|
|
386
|
-
- Click to switch, double-click to rename
|
|
387
|
-
- "+" button adds new sheet
|
|
388
|
-
- Delete button removes active sheet (minimum 1)
|
|
389
|
-
|
|
390
|
-
### Column Resize
|
|
391
|
-
- Drag column header borders to resize
|
|
392
|
-
- Minimum width: 30px
|
|
393
|
-
|
|
394
|
-
### Undo/Redo
|
|
395
|
-
- 50-step history
|
|
396
|
-
- Every cell edit, format change pushes to undo stack
|
|
397
|
-
- Ctrl+Z / Ctrl+Y keyboard shortcuts
|
|
398
|
-
|
|
399
|
-
## Customization
|
|
400
|
-
|
|
401
|
-
All components render `data-fancy-sheets-*` attributes:
|
|
402
|
-
|
|
403
|
-
| Attribute | Element |
|
|
404
|
-
|-----------|---------|
|
|
405
|
-
| `data-fancy-sheets` | Root container |
|
|
406
|
-
| `data-fancy-sheets-toolbar` | Toolbar area |
|
|
407
|
-
| `data-fancy-sheets-formula-bar` | Formula bar |
|
|
408
|
-
| `data-fancy-sheets-grid` | Grid container |
|
|
409
|
-
| `data-fancy-sheets-column-headers` | Column header row |
|
|
410
|
-
| `data-fancy-sheets-row-header` | Row number cell |
|
|
411
|
-
| `data-fancy-sheets-cell` | Individual cell |
|
|
412
|
-
| `data-fancy-sheets-cell-editor` | Inline edit input |
|
|
413
|
-
| `data-fancy-sheets-selection` | Selection overlay |
|
|
414
|
-
| `data-fancy-sheets-resize-handle` | Column resize handle |
|
|
415
|
-
| `data-fancy-sheets-tabs` | Sheet tab bar |
|
|
416
|
-
|
|
417
|
-
## Architecture
|
|
418
|
-
|
|
419
|
-
```
|
|
420
|
-
src/
|
|
421
|
-
├── types/
|
|
422
|
-
│ ├── cell.ts # CellValue, CellData, CellFormat
|
|
423
|
-
│ ├── sheet.ts # SheetData, WorkbookData, helpers
|
|
424
|
-
│ ├── selection.ts # CellRange, SelectionState
|
|
425
|
-
│ └── formula.ts # FormulaToken, FormulaASTNode
|
|
426
|
-
├── engine/
|
|
427
|
-
│ ├── cell-utils.ts # Address parsing (A1 <-> row/col)
|
|
428
|
-
│ ├── clipboard.ts # TSV serialize/parse
|
|
429
|
-
│ ├── csv.ts # CSV import/export
|
|
430
|
-
│ ├── sorting.ts # Column sort
|
|
431
|
-
│ ├── history.ts # Undo/redo stack
|
|
432
|
-
│ └── formula/
|
|
433
|
-
│ ├── lexer.ts # Tokenize formula strings
|
|
434
|
-
│ ├── parser.ts # Recursive descent -> AST
|
|
435
|
-
│ ├── evaluator.ts # Evaluate AST with cell lookups
|
|
436
|
-
│ ├── references.ts # Extract cell refs from AST
|
|
437
|
-
│ ├── dependency-graph.ts # Dep tracking, circular detection
|
|
438
|
-
│ └── functions/
|
|
439
|
-
│ ├── registry.ts # Function registry
|
|
440
|
-
│ ├── math.ts # SUM, AVERAGE, MIN, MAX, etc.
|
|
441
|
-
│ ├── text.ts # UPPER, LOWER, LEN, TRIM, CONCAT
|
|
442
|
-
│ └── logic.ts # IF, AND, OR, NOT
|
|
443
|
-
├── hooks/
|
|
444
|
-
│ └── use-spreadsheet-store.ts # Central reducer + actions
|
|
445
|
-
├── components/
|
|
446
|
-
│ ├── Spreadsheet/ # Root compound component
|
|
447
|
-
│ ├── Toolbar/ # Format buttons + formula bar
|
|
448
|
-
│ ├── Grid/ # Cell grid, editor, selection, resize
|
|
449
|
-
│ └── SheetTabs/ # Multi-sheet tab bar
|
|
450
|
-
├── index.ts # Public API
|
|
451
|
-
└── styles.css # Base styles
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
## Demo
|
|
455
|
-
|
|
456
|
-
Demo page at `/react-demos/spreadsheet` in the monorepo with pre-populated product catalog data and formulas.
|
|
457
|
-
|
|
458
|
-
---
|
|
459
|
-
|
|
460
|
-
## Agent Guidelines
|
|
461
|
-
|
|
462
|
-
### Component Pattern
|
|
463
|
-
- Same compound component pattern as react-fancy: `Object.assign(Root, { Toolbar, Grid, SheetTabs })`
|
|
464
|
-
- Context via `SpreadsheetContext` + `useSpreadsheet()` hook
|
|
465
|
-
- State managed by `useReducer` with action-based mutations
|
|
60
|
+
## At a Glance
|
|
466
61
|
|
|
467
|
-
|
|
468
|
-
-
|
|
469
|
-
-
|
|
470
|
-
-
|
|
471
|
-
-
|
|
62
|
+
- **Compound component API** — `Spreadsheet` + `Toolbar` / `Grid` / `SheetTabs`
|
|
63
|
+
- **`useSpreadsheet` hook** — full workbook state and actions accessible from any child
|
|
64
|
+
- **80+ built-in formulas** — Math, Text, Logic, Conditional aggregates, Lookup, Date/Time, Info
|
|
65
|
+
- **Multi-sheet workbooks** — cross-sheet references via `Sheet2!A1` syntax
|
|
66
|
+
- **Features** — editing, navigation, selection (single, range, multi-range), formatting (bold/italic/align), clipboard (copy/cut/paste with TSV), column resize, freeze rows/cols, undo/redo (50 steps), drag-to-select
|
|
67
|
+
- **Zero third-party dependencies** — custom lexer, parser, evaluator, and dependency graph
|
|
472
68
|
|
|
473
|
-
|
|
474
|
-
- Sparse cell map (`Record<CellAddress, CellData>`) — only stores non-empty cells
|
|
475
|
-
- Formulas stored as strings, computed values cached in `computedValue`
|
|
476
|
-
- Undo/redo via workbook snapshot stack (max 50)
|
|
69
|
+
## License
|
|
477
70
|
|
|
478
|
-
|
|
479
|
-
- tsup: ESM, CJS, DTS
|
|
480
|
-
- External: react, react-dom, @particle-academy/react-fancy
|
|
481
|
-
- Zero other dependencies
|
|
482
|
-
- Verify with `npm run build` from monorepo root
|
|
71
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1838,6 +1838,21 @@ var Cell = react.memo(function Cell2({ address, row, col }) {
|
|
|
1838
1838
|
if (cell?.format?.bold) formatStyle.fontWeight = "bold";
|
|
1839
1839
|
if (cell?.format?.italic) formatStyle.fontStyle = "italic";
|
|
1840
1840
|
if (cell?.format?.textAlign) formatStyle.textAlign = cell.format.textAlign;
|
|
1841
|
+
if (cell?.format?.backgroundColor) formatStyle.backgroundColor = cell.format.backgroundColor;
|
|
1842
|
+
if (cell?.format?.color) formatStyle.color = cell.format.color;
|
|
1843
|
+
if (cell?.format?.fontSize) formatStyle.fontSize = cell.format.fontSize;
|
|
1844
|
+
if (cell?.format?.borderTop) {
|
|
1845
|
+
formatStyle.borderTopWidth = 1;
|
|
1846
|
+
formatStyle.borderTopStyle = "solid";
|
|
1847
|
+
formatStyle.borderTopColor = cell.format.borderTop;
|
|
1848
|
+
}
|
|
1849
|
+
if (cell?.format?.borderRight) formatStyle.borderRightColor = cell.format.borderRight;
|
|
1850
|
+
if (cell?.format?.borderBottom) formatStyle.borderBottomColor = cell.format.borderBottom;
|
|
1851
|
+
if (cell?.format?.borderLeft) {
|
|
1852
|
+
formatStyle.borderLeftWidth = 1;
|
|
1853
|
+
formatStyle.borderLeftStyle = "solid";
|
|
1854
|
+
formatStyle.borderLeftColor = cell.format.borderLeft;
|
|
1855
|
+
}
|
|
1841
1856
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1842
1857
|
"div",
|
|
1843
1858
|
{
|
|
@@ -1997,7 +2012,8 @@ function SpreadsheetGrid({ className }) {
|
|
|
1997
2012
|
setFrozenCols,
|
|
1998
2013
|
extendSelection,
|
|
1999
2014
|
undo,
|
|
2000
|
-
redo
|
|
2015
|
+
redo,
|
|
2016
|
+
contextMenuItems
|
|
2001
2017
|
} = useSpreadsheet();
|
|
2002
2018
|
const containerRef = react.useRef(null);
|
|
2003
2019
|
const handleKeyDown = react.useCallback(
|
|
@@ -2190,14 +2206,31 @@ function SpreadsheetGrid({ className }) {
|
|
|
2190
2206
|
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => {
|
|
2191
2207
|
const col = parseAddress(selection.activeCell).col;
|
|
2192
2208
|
setFrozenCols(activeSheet.frozenCols > 0 ? 0 : col);
|
|
2193
|
-
}, disabled: readOnly, children: activeSheet.frozenCols > 0 ? "Unfreeze columns" : "Freeze columns left" })
|
|
2209
|
+
}, disabled: readOnly, children: activeSheet.frozenCols > 0 ? "Unfreeze columns" : "Freeze columns left" }),
|
|
2210
|
+
(() => {
|
|
2211
|
+
const items = typeof contextMenuItems === "function" ? contextMenuItems(selection.activeCell) : contextMenuItems;
|
|
2212
|
+
if (!items || items.length === 0) return null;
|
|
2213
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2214
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
|
|
2215
|
+
items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2216
|
+
reactFancy.ContextMenu.Item,
|
|
2217
|
+
{
|
|
2218
|
+
onClick: () => item.onClick(selection.activeCell),
|
|
2219
|
+
disabled: typeof item.disabled === "function" ? item.disabled(selection.activeCell) : item.disabled,
|
|
2220
|
+
danger: item.danger,
|
|
2221
|
+
children: item.label
|
|
2222
|
+
},
|
|
2223
|
+
i
|
|
2224
|
+
))
|
|
2225
|
+
] });
|
|
2226
|
+
})()
|
|
2194
2227
|
] })
|
|
2195
2228
|
] });
|
|
2196
2229
|
}
|
|
2197
2230
|
SpreadsheetGrid.displayName = "SpreadsheetGrid";
|
|
2198
2231
|
var btnClass = "inline-flex items-center justify-center rounded px-2 py-1 text-[12px] font-medium text-zinc-600 transition-colors hover:bg-zinc-100 disabled:opacity-40 dark:text-zinc-300 dark:hover:bg-zinc-800";
|
|
2199
2232
|
var activeBtnClass = "bg-zinc-200 dark:bg-zinc-700";
|
|
2200
|
-
function DefaultToolbar() {
|
|
2233
|
+
function DefaultToolbar({ extra }) {
|
|
2201
2234
|
const {
|
|
2202
2235
|
selection,
|
|
2203
2236
|
activeSheet,
|
|
@@ -2389,7 +2422,11 @@ function DefaultToolbar() {
|
|
|
2389
2422
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[8px]", children: "\u2192" })
|
|
2390
2423
|
]
|
|
2391
2424
|
}
|
|
2392
|
-
)
|
|
2425
|
+
),
|
|
2426
|
+
extra && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2427
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700" }),
|
|
2428
|
+
extra
|
|
2429
|
+
] })
|
|
2393
2430
|
] }),
|
|
2394
2431
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { "data-fancy-sheets-formula-bar": "", className: "flex items-center gap-2 border-b border-zinc-200 px-2 py-1 dark:border-zinc-700", children: [
|
|
2395
2432
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-12 shrink-0 text-center text-[11px] font-medium text-zinc-500 dark:text-zinc-400", children: selection.activeCell }),
|
|
@@ -2408,8 +2445,8 @@ function DefaultToolbar() {
|
|
|
2408
2445
|
] })
|
|
2409
2446
|
] });
|
|
2410
2447
|
}
|
|
2411
|
-
function SpreadsheetToolbar({ children, className }) {
|
|
2412
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fancy-sheets-toolbar": "", className: reactFancy.cn("", className), children: children ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultToolbar, {}) });
|
|
2448
|
+
function SpreadsheetToolbar({ children, className, extra }) {
|
|
2449
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fancy-sheets-toolbar": "", className: reactFancy.cn("", className), children: children ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultToolbar, { extra }) });
|
|
2413
2450
|
}
|
|
2414
2451
|
SpreadsheetToolbar.displayName = "SpreadsheetToolbar";
|
|
2415
2452
|
function SpreadsheetSheetTabs({ className }) {
|
|
@@ -2506,7 +2543,8 @@ function SpreadsheetRoot({
|
|
|
2506
2543
|
rowCount = 100,
|
|
2507
2544
|
defaultColumnWidth = 100,
|
|
2508
2545
|
rowHeight = 28,
|
|
2509
|
-
readOnly = false
|
|
2546
|
+
readOnly = false,
|
|
2547
|
+
contextMenuItems
|
|
2510
2548
|
}) {
|
|
2511
2549
|
const { state, actions } = useSpreadsheetStore(data ?? defaultData);
|
|
2512
2550
|
const onChangeRef = react.useRef(onChange);
|
|
@@ -2571,9 +2609,10 @@ function SpreadsheetRoot({
|
|
|
2571
2609
|
getColumnWidth,
|
|
2572
2610
|
isCellSelected,
|
|
2573
2611
|
isCellActive,
|
|
2612
|
+
contextMenuItems,
|
|
2574
2613
|
_isDragging: isDraggingRef
|
|
2575
2614
|
}),
|
|
2576
|
-
[state, activeSheet, columnCount, rowCount, defaultColumnWidth, rowHeight, readOnly, actions, getColumnWidth, isCellSelected, isCellActive]
|
|
2615
|
+
[state, activeSheet, columnCount, rowCount, defaultColumnWidth, rowHeight, readOnly, actions, getColumnWidth, isCellSelected, isCellActive, contextMenuItems]
|
|
2577
2616
|
);
|
|
2578
2617
|
return /* @__PURE__ */ jsxRuntime.jsx(SpreadsheetContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2579
2618
|
"div",
|
|
@@ -2590,6 +2629,32 @@ var Spreadsheet = Object.assign(SpreadsheetRoot, {
|
|
|
2590
2629
|
Grid: SpreadsheetGrid,
|
|
2591
2630
|
SheetTabs: SpreadsheetSheetTabs
|
|
2592
2631
|
});
|
|
2632
|
+
function Sheet({ data, onChange, contextMenuItems, ...props }) {
|
|
2633
|
+
const workbook = react.useMemo(
|
|
2634
|
+
() => data ? { sheets: [data], activeSheetId: data.id } : void 0,
|
|
2635
|
+
[data]
|
|
2636
|
+
);
|
|
2637
|
+
const handleChange = react.useMemo(() => {
|
|
2638
|
+
if (!onChange) return void 0;
|
|
2639
|
+
return (wb) => onChange(wb.sheets[0]);
|
|
2640
|
+
}, [onChange]);
|
|
2641
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Spreadsheet, { data: workbook, onChange: handleChange, contextMenuItems, ...props, children: /* @__PURE__ */ jsxRuntime.jsx(Spreadsheet.Grid, {}) });
|
|
2642
|
+
}
|
|
2643
|
+
Sheet.displayName = "Sheet";
|
|
2644
|
+
function SheetWorkbook({
|
|
2645
|
+
hideToolbar = false,
|
|
2646
|
+
hideTabs = false,
|
|
2647
|
+
toolbarExtra,
|
|
2648
|
+
contextMenuItems,
|
|
2649
|
+
...props
|
|
2650
|
+
}) {
|
|
2651
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(Spreadsheet, { ...props, contextMenuItems, children: [
|
|
2652
|
+
!hideToolbar && /* @__PURE__ */ jsxRuntime.jsx(Spreadsheet.Toolbar, { extra: toolbarExtra }),
|
|
2653
|
+
/* @__PURE__ */ jsxRuntime.jsx(Spreadsheet.Grid, {}),
|
|
2654
|
+
!hideTabs && /* @__PURE__ */ jsxRuntime.jsx(Spreadsheet.SheetTabs, {})
|
|
2655
|
+
] });
|
|
2656
|
+
}
|
|
2657
|
+
SheetWorkbook.displayName = "SheetWorkbook";
|
|
2593
2658
|
|
|
2594
2659
|
// src/engine/csv.ts
|
|
2595
2660
|
function parseCSV(text) {
|
|
@@ -2687,6 +2752,8 @@ function workbookToCSV(workbook, sheetId) {
|
|
|
2687
2752
|
return stringifyCSV(data);
|
|
2688
2753
|
}
|
|
2689
2754
|
|
|
2755
|
+
exports.Sheet = Sheet;
|
|
2756
|
+
exports.SheetWorkbook = SheetWorkbook;
|
|
2690
2757
|
exports.Spreadsheet = Spreadsheet;
|
|
2691
2758
|
exports.columnToLetter = columnToLetter;
|
|
2692
2759
|
exports.createEmptySheet = createEmptySheet;
|