@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 CHANGED
@@ -1,23 +1,18 @@
1
1
  # @particle-academy/fancy-sheets
2
2
 
3
- Spreadsheet editor with formula engine, multi-sheet tabs, and full cell editing. Part of the `@particle-academy` component ecosystem.
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
- # pnpm
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
- ## Usage
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, createEmptyWorkbook } from "@particle-academy/fancy-sheets";
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: 500 }}>
37
- <Spreadsheet data={data} onChange={setData}>
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 spreadsheet fills its container — set a fixed height on the parent element.
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
- ## 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 (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
- ### Formula Engine
468
- - Pure functions in `engine/formula/` no React imports
469
- - Lexer Parser Evaluator pipeline
470
- - Dependency graph rebuilt on cell changes, topological sort for recalc order
471
- - Functions registered via side-effect imports (listed in `sideEffects` in package.json)
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
- ### Data Model
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
- ### Build
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;