@particle-academy/fancy-sheets 0.6.3 → 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.
- package/docs/Spreadsheet.md +237 -20
- package/package.json +1 -1
package/docs/Spreadsheet.md
CHANGED
|
@@ -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
|
-
###
|
|
171
|
+
### SpreadsheetContextMenuItem
|
|
170
172
|
|
|
171
173
|
```ts
|
|
172
174
|
interface SpreadsheetContextMenuItem {
|
|
173
175
|
label: string;
|
|
174
|
-
onClick
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|