@particle-academy/fancy-sheets 0.6.2 → 0.6.4

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.
@@ -69,6 +69,7 @@ Takes `SheetData` directly — no workbook wrapper, no tabs, no toolbar:
69
69
  |------|------|-------------|
70
70
  | children | `ReactNode` | Replace default toolbar entirely |
71
71
  | extra | `ReactNode` | Append content after default buttons (before formula bar) |
72
+ | buttons | `ToolbarButton[]` | Which built-in groups to show (default: all). Pass `[]` for only custom `extra`. |
72
73
  | className | `string` | Additional CSS classes |
73
74
 
74
75
  ### SheetWorkbook
@@ -80,6 +81,7 @@ All `Spreadsheet` props plus:
80
81
  | hideToolbar | `boolean` | `false` | Hide the toolbar |
81
82
  | hideTabs | `boolean` | `false` | Hide the sheet tabs |
82
83
  | toolbarExtra | `ReactNode` | - | Extra toolbar content |
84
+ | toolbarButtons | `ToolbarButton[]` | all | Which built-in toolbar groups to show |
83
85
 
84
86
  ### Sheet
85
87
 
@@ -166,51 +168,266 @@ Comments render as:
166
168
  - A 1px border around the cell in the comment color
167
169
  - A hover tooltip showing author + text
168
170
 
169
- ### Context Menu Items
171
+ ### SpreadsheetContextMenuItem
170
172
 
171
173
  ```ts
172
174
  interface SpreadsheetContextMenuItem {
173
175
  label: string;
174
- onClick: (address: string) => void;
176
+ onClick?: (address: string) => void;
175
177
  disabled?: boolean | ((address: string) => boolean);
176
178
  danger?: boolean;
179
+ items?: SpreadsheetContextMenuItem[]; // nested submenu
177
180
  }
178
181
  ```
179
182
 
180
- Pass as array (static) or callback (dynamic per cell):
183
+ ---
184
+
185
+ ## Custom Toolbar Buttons
186
+
187
+ Use the `extra` prop on `Spreadsheet.Toolbar` (or `toolbarExtra` on `SheetWorkbook`) to inject your own buttons alongside the built-in ones. They appear after a divider at the end of the button bar.
188
+
189
+ ```tsx
190
+ <Spreadsheet data={data} onChange={setData}>
191
+ <Spreadsheet.Toolbar
192
+ extra={
193
+ <>
194
+ <button
195
+ className="rounded bg-green-600 px-2 py-0.5 text-xs text-white"
196
+ onClick={() => addRow("income")}
197
+ >
198
+ + Income
199
+ </button>
200
+ <button
201
+ className="rounded bg-red-500 px-2 py-0.5 text-xs text-white ml-1"
202
+ onClick={() => addRow("expense")}
203
+ >
204
+ + Expense
205
+ </button>
206
+ </>
207
+ }
208
+ />
209
+ <Spreadsheet.Grid />
210
+ </Spreadsheet>
211
+ ```
212
+
213
+ Or via `SheetWorkbook`:
214
+
215
+ ```tsx
216
+ <SheetWorkbook
217
+ data={data}
218
+ onChange={setData}
219
+ toolbarExtra={<button onClick={exportCSV}>Export</button>}
220
+ />
221
+ ```
222
+
223
+ ## Controlling the Toolbar
224
+
225
+ The `buttons` prop on `Spreadsheet.Toolbar` (or `toolbarButtons` on `SheetWorkbook`) controls which built-in button groups are visible.
226
+
227
+ ```ts
228
+ type ToolbarButton = "undo" | "bold" | "align" | "freeze" | "format" | "decimals" | "formulaBar";
229
+ ```
230
+
231
+ ### Show all (default)
232
+
233
+ ```tsx
234
+ <Spreadsheet.Toolbar />
235
+ ```
236
+
237
+ ### Show specific groups only
238
+
239
+ ```tsx
240
+ <Spreadsheet.Toolbar buttons={["undo", "bold", "formulaBar"]} />
241
+ ```
242
+
243
+ ### Show only custom buttons (hide all built-in)
244
+
245
+ ```tsx
246
+ <Spreadsheet.Toolbar buttons={[]} extra={<MyCustomToolbar />} />
247
+ ```
248
+
249
+ ### Via SheetWorkbook
250
+
251
+ ```tsx
252
+ <SheetWorkbook
253
+ toolbarButtons={["undo", "bold", "format", "formulaBar"]}
254
+ toolbarExtra={<button>Custom</button>}
255
+ />
256
+ ```
257
+
258
+ ### Replace the toolbar entirely
259
+
260
+ Pass `children` to `Spreadsheet.Toolbar` to replace the default UI completely. Use `useSpreadsheet()` inside your custom toolbar to access state and actions.
261
+
262
+ ```tsx
263
+ function MyToolbar() {
264
+ const { undo, redo, canUndo, canRedo, selection } = useSpreadsheet();
265
+ return (
266
+ <div className="flex gap-2 p-2 border-b">
267
+ <button onClick={undo} disabled={!canUndo}>Undo</button>
268
+ <button onClick={redo} disabled={!canRedo}>Redo</button>
269
+ <span>Cell: {selection.activeCell}</span>
270
+ </div>
271
+ );
272
+ }
273
+
274
+ <Spreadsheet data={data} onChange={setData}>
275
+ <Spreadsheet.Toolbar>
276
+ <MyToolbar />
277
+ </Spreadsheet.Toolbar>
278
+ <Spreadsheet.Grid />
279
+ </Spreadsheet>
280
+ ```
281
+
282
+ ## Custom Context Menus
283
+
284
+ The `contextMenuItems` prop adds items to the right-click menu after the built-in items (Copy, Paste, Clear, Freeze), separated by a divider.
285
+
286
+ ### Static items
287
+
288
+ ```tsx
289
+ <Spreadsheet
290
+ data={data}
291
+ onChange={setData}
292
+ contextMenuItems={[
293
+ { label: "Highlight yellow", onClick: (addr) => highlight(addr, "#fef08a") },
294
+ { label: "Clear formatting", onClick: (addr) => clearFormat(addr) },
295
+ ]}
296
+ >
297
+ ```
298
+
299
+ ### Dynamic items (context-aware)
300
+
301
+ Pass a callback to return different items based on the active cell:
302
+
303
+ ```tsx
304
+ <Spreadsheet
305
+ data={data}
306
+ onChange={setData}
307
+ contextMenuItems={(addr) => {
308
+ const cell = activeSheet.cells[addr];
309
+ const row = parseInt(addr.replace(/[A-Z]+/, ""), 10);
310
+
311
+ const items: SpreadsheetContextMenuItem[] = [];
312
+
313
+ // Comment actions depend on whether cell has a comment
314
+ if (cell?.comment) {
315
+ items.push({ label: "Edit Comment", onClick: (a) => openEditor(a) });
316
+ items.push({ label: "Delete Comment", danger: true, onClick: (a) => deleteComment(a) });
317
+ } else {
318
+ items.push({ label: "Add Comment", onClick: (a) => openEditor(a) });
319
+ }
320
+
321
+ // Row actions only for data rows
322
+ if (row >= 2) {
323
+ items.push({ label: "Delete Row", danger: true, onClick: (a) => deleteRow(a) });
324
+ }
325
+
326
+ return items;
327
+ }}
328
+ >
329
+ ```
330
+
331
+ ### Nested submenus
332
+
333
+ Add an `items` array to group actions into submenus:
181
334
 
182
335
  ```tsx
183
- // Static
184
- <Spreadsheet contextMenuItems={[
185
- { label: "Highlight", onClick: (addr) => highlight(addr) },
186
- ]}>
187
-
188
- // Dynamic (context-aware)
189
- <Spreadsheet contextMenuItems={(addr) => {
190
- const cell = sheet.cells[addr];
191
- return cell?.comment
192
- ? [{ label: "Edit Comment", onClick: ... }, { label: "Delete Comment", danger: true, onClick: ... }]
193
- : [{ label: "Add Comment", onClick: ... }];
194
- }}>
336
+ contextMenuItems={(addr) => [
337
+ {
338
+ label: "Comments",
339
+ items: [
340
+ { label: "Add Comment", onClick: (a) => openEditor(a) },
341
+ ],
342
+ },
343
+ {
344
+ label: "Row Actions",
345
+ items: [
346
+ { label: "Mark as Paid", onClick: (a) => markPaid(a) },
347
+ { label: "Duplicate Row", onClick: (a) => duplicateRow(a) },
348
+ { label: "Delete Row", danger: true, onClick: (a) => deleteRow(a) },
349
+ ],
350
+ },
351
+ ]}
195
352
  ```
196
353
 
197
- Items appear after a separator below the built-in items (Copy, Paste, Clear, Freeze).
354
+ Submenus nest arbitrarily deep each item with `items` renders as a hover-to-open submenu with a chevron indicator.
198
355
 
199
356
  ## Custom Formulas
200
357
 
358
+ Register custom functions that users can call in cell formulas with `=FUNCTION_NAME(...)`.
359
+
201
360
  ```tsx
202
361
  import { registerFunction } from "@particle-academy/fancy-sheets";
203
362
  import type { FormulaRangeFunction } from "@particle-academy/fancy-sheets";
363
+ ```
364
+
365
+ ### Basic function
204
366
 
205
- const myFormula: FormulaRangeFunction = (args) => {
367
+ ```tsx
368
+ registerFunction("PRIORITY", (args) => {
206
369
  const value = Number(args[0]?.[0] ?? 0);
207
370
  return value > 100 ? "High" : "Low";
208
- };
371
+ });
372
+
373
+ // In a cell: =PRIORITY(A1)
374
+ ```
375
+
376
+ ### Function with multiple arguments
377
+
378
+ `args` is a 2D array — each argument may be a single value or a range of values:
379
+
380
+ ```tsx
381
+ registerFunction("BUDGET_STATUS", (args) => {
382
+ const spent = Number(args[0]?.[0] ?? 0); // first arg: single cell
383
+ const budget = Number(args[1]?.[0] ?? 0); // second arg: single cell
384
+ if (!budget) return "N/A";
385
+ const ratio = spent / budget;
386
+ if (ratio > 1) return "Over Budget";
387
+ if (ratio > 0.9) return "Near Limit";
388
+ return "On Track";
389
+ });
390
+
391
+ // In a cell: =BUDGET_STATUS(C4, B4)
392
+ ```
209
393
 
210
- registerFunction("PRIORITY", myFormula);
211
- // Usage in cells: =PRIORITY(A1)
394
+ ### Function that operates on ranges
395
+
396
+ When a range like `A1:A10` is passed, `args[n]` contains all values in the range as a flat array:
397
+
398
+ ```tsx
399
+ registerFunction("WEIGHTED_AVG", (args) => {
400
+ const values = args[0] ?? []; // =WEIGHTED_AVG(B2:B10, C2:C10)
401
+ const weights = args[1] ?? [];
402
+ let sumProduct = 0, sumWeights = 0;
403
+ for (let i = 0; i < values.length; i++) {
404
+ const v = Number(values[i] ?? 0);
405
+ const w = Number(weights[i] ?? 0);
406
+ sumProduct += v * w;
407
+ sumWeights += w;
408
+ }
409
+ return sumWeights === 0 ? 0 : sumProduct / sumWeights;
410
+ });
411
+
412
+ // In a cell: =WEIGHTED_AVG(B2:B10, C2:C10)
413
+ ```
414
+
415
+ ### Type signature
416
+
417
+ ```ts
418
+ type FormulaRangeFunction = (args: CellValue[][]) => CellValue;
419
+ // CellValue = string | number | boolean | null
212
420
  ```
213
421
 
422
+ - `args[0]` = first argument's values, `args[1]` = second, etc.
423
+ - Single-cell arguments are `[value]` (array of one). Ranges are flat arrays.
424
+ - Return a `CellValue`. Return a string starting with `#` for errors (e.g., `"#VALUE!"`).
425
+ - Call `registerFunction` at module scope (before render). It's a global side-effect.
426
+
427
+ ### Built-in functions (80+)
428
+
429
+ See [Formulas](./formulas.md) for the complete list: SUM, AVERAGE, IF, VLOOKUP, SUMIF, TODAY, CONCAT, and more.
430
+
214
431
  ## Helpers
215
432
 
216
433
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-sheets",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Spreadsheet editor with formula engine, multi-sheet tabs, and full cell editing",
5
5
  "repository": {
6
6
  "type": "git",