@ricsam/react-spreadsheets 0.0.14 → 0.0.15

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/dist/mjs/lib.mjs CHANGED
@@ -1787,6 +1787,7 @@ var Spreadsheet = forwardRef(({
1787
1787
  "aria-describedby": ariaDescribedBy,
1788
1788
  cellData: controlledCellData,
1789
1789
  onCellDataChange,
1790
+ onCellDataPatch,
1790
1791
  onCellDataChangeError,
1791
1792
  rowCount,
1792
1793
  columnCount,
@@ -1991,6 +1992,7 @@ var Spreadsheet = forwardRef(({
1991
1992
  }, [overlayPlaceholder, containerDivRef, viewport, gridDims, placeholderPos]);
1992
1993
  const effectsRefOb = {
1993
1994
  onCellDataChange,
1995
+ onCellDataPatch,
1994
1996
  onCellDataChangeError,
1995
1997
  controlledCellData,
1996
1998
  parseValue,
@@ -2237,13 +2239,13 @@ var Spreadsheet = forwardRef(({
2237
2239
  return selectionManager.listenToUpdateData((updates) => {
2238
2240
  const editingState = selectionManager.isEditing;
2239
2241
  try {
2240
- const { cellData, onCellDataChange } = effectsRef.current;
2241
- const newData = new Map(cellData);
2242
+ const { cellData, onCellDataChange, onCellDataPatch, controlledCellData } = effectsRef.current;
2243
+ const patch = new Map;
2242
2244
  updates.forEach(({ value, rowIndex, colIndex }) => {
2243
2245
  const key = `${indexToColumn(colIndex)}${rowIndex + 1}`;
2244
2246
  const parseValue = effectsRef.current.parseValue;
2245
2247
  if (value === "") {
2246
- newData.delete(key);
2248
+ patch.set(key, undefined);
2247
2249
  } else {
2248
2250
  let parsedValue = value;
2249
2251
  if (parseValue && typeof value === "string") {
@@ -2263,10 +2265,23 @@ var Spreadsheet = forwardRef(({
2263
2265
  });
2264
2266
  }
2265
2267
  }
2266
- newData.set(key, parsedValue);
2268
+ patch.set(key, parsedValue);
2267
2269
  }
2268
2270
  });
2269
- onCellDataChange?.(newData);
2271
+ if (onCellDataPatch) {
2272
+ onCellDataPatch(patch);
2273
+ if (controlledCellData !== undefined)
2274
+ return;
2275
+ }
2276
+ const newData = new Map(cellData);
2277
+ patch.forEach((value, key) => {
2278
+ if (value === undefined)
2279
+ newData.delete(key);
2280
+ else
2281
+ newData.set(key, value);
2282
+ });
2283
+ if (!onCellDataPatch)
2284
+ onCellDataChange?.(newData);
2270
2285
  setInternalCellData(newData);
2271
2286
  } catch (error) {
2272
2287
  console.error("Failed to update spreadsheet cell data:", error);
@@ -3145,12 +3160,370 @@ import {
3145
3160
  parseCellReference as parseCellReference2,
3146
3161
  getCellReference as getCellReference2
3147
3162
  } from "@ricsam/formula-engine";
3148
- import React3, { useCallback as useCallback4, useMemo as useMemo2, useRef as useRef4, useState as useState6 } from "react";
3163
+ import React4, { useCallback as useCallback4, useMemo as useMemo2, useRef as useRef5, useState as useState7 } from "react";
3164
+
3165
+ // src/workbook/sheet-navigation.tsx
3166
+ import { useEffect as useEffect4, useId as useId2, useRef as useRef4, useState as useState5 } from "react";
3167
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3168
+ function SheetNavigation({
3169
+ sheets,
3170
+ activeSheet,
3171
+ workbookName,
3172
+ panelId,
3173
+ onSelect,
3174
+ onAdd,
3175
+ onRename,
3176
+ onDelete,
3177
+ children
3178
+ }) {
3179
+ const id = useId2();
3180
+ const root = useRef4(null);
3181
+ const menuButton = useRef4(null);
3182
+ const menu = useRef4(null);
3183
+ const input = useRef4(null);
3184
+ const [menuOpen, setMenuOpen] = useState5(false);
3185
+ const [renaming, setRenaming] = useState5(null);
3186
+ const [name, setName] = useState5("");
3187
+ const [error, setError] = useState5("");
3188
+ const [notice, setNotice] = useState5("");
3189
+ const selected = sheets.includes(activeSheet) ? activeSheet : sheets[0];
3190
+ const focusTab = (sheet = selected) => {
3191
+ const tabs = root.current?.querySelectorAll('[role="tab"]');
3192
+ tabs?.[sheets.indexOf(sheet ?? "")]?.focus();
3193
+ };
3194
+ useEffect4(() => {
3195
+ const tab = root.current?.querySelector('[aria-selected="true"]');
3196
+ const strip = tab?.parentElement;
3197
+ if (!tab || !strip)
3198
+ return;
3199
+ const bounds = strip.getBoundingClientRect();
3200
+ const target = tab.getBoundingClientRect();
3201
+ if (target.left < bounds.left)
3202
+ strip.scrollLeft -= bounds.left - target.left;
3203
+ else if (target.right > bounds.right)
3204
+ strip.scrollLeft += target.right - bounds.right;
3205
+ }, [activeSheet, sheets.join("\x00")]);
3206
+ useEffect4(() => {
3207
+ if (renaming) {
3208
+ input.current?.focus();
3209
+ input.current?.select();
3210
+ }
3211
+ }, [renaming]);
3212
+ useEffect4(() => {
3213
+ if (!menuOpen)
3214
+ return;
3215
+ menu.current?.querySelector("button")?.focus();
3216
+ const outside = (event) => {
3217
+ if (!root.current?.contains(event.target))
3218
+ setMenuOpen(false);
3219
+ };
3220
+ document.addEventListener("pointerdown", outside);
3221
+ return () => document.removeEventListener("pointerdown", outside);
3222
+ }, [menuOpen]);
3223
+ useEffect4(() => {
3224
+ setMenuOpen(false);
3225
+ setRenaming(null);
3226
+ setError("");
3227
+ }, [workbookName]);
3228
+ useEffect4(() => {
3229
+ if (renaming && !sheets.includes(renaming)) {
3230
+ setRenaming(null);
3231
+ setError("");
3232
+ }
3233
+ }, [renaming, sheets.join("\x00")]);
3234
+ const run = (action) => {
3235
+ try {
3236
+ action();
3237
+ setError("");
3238
+ return true;
3239
+ } catch (error) {
3240
+ setError(error instanceof Error ? error.message : "Unable to update this sheet. Please try again.");
3241
+ return false;
3242
+ }
3243
+ };
3244
+ const startRename = (sheet) => {
3245
+ setMenuOpen(false);
3246
+ setError("");
3247
+ setRenaming(sheet);
3248
+ setName(sheet);
3249
+ };
3250
+ const cancelRename = () => {
3251
+ setRenaming(null);
3252
+ setError("");
3253
+ focusTab();
3254
+ };
3255
+ const saveRename = (event) => {
3256
+ event.preventDefault();
3257
+ if (!renaming)
3258
+ return;
3259
+ const next = name.trim();
3260
+ if (!next) {
3261
+ setError("Enter a sheet name.");
3262
+ input.current?.focus();
3263
+ return;
3264
+ }
3265
+ if (sheets.some((sheet) => sheet !== renaming && sheet.toLocaleLowerCase() === next.toLocaleLowerCase())) {
3266
+ setError("A sheet with this name already exists. Choose a different name.");
3267
+ input.current?.focus();
3268
+ return;
3269
+ }
3270
+ if (run(() => {
3271
+ if (next !== renaming)
3272
+ onRename(renaming, next);
3273
+ })) {
3274
+ setRenaming(null);
3275
+ setNotice(`Sheet renamed to ${next}.`);
3276
+ menuButton.current?.focus();
3277
+ }
3278
+ };
3279
+ return /* @__PURE__ */ jsxs3("div", {
3280
+ ref: root,
3281
+ className: "rsp-sheet-navigation",
3282
+ onKeyDown: (event) => event.stopPropagation(),
3283
+ children: [
3284
+ /* @__PURE__ */ jsx4("span", {
3285
+ className: "rsp-sr-only",
3286
+ role: "status",
3287
+ children: notice
3288
+ }),
3289
+ renaming && /* @__PURE__ */ jsxs3("form", {
3290
+ className: "rsp-sheet-rename",
3291
+ onSubmit: saveRename,
3292
+ onKeyDown: (event) => {
3293
+ if (event.key === "Escape") {
3294
+ event.preventDefault();
3295
+ cancelRename();
3296
+ }
3297
+ },
3298
+ children: [
3299
+ /* @__PURE__ */ jsx4("label", {
3300
+ htmlFor: `${id}-name`,
3301
+ children: "Sheet name"
3302
+ }),
3303
+ /* @__PURE__ */ jsx4("input", {
3304
+ ref: input,
3305
+ id: `${id}-name`,
3306
+ className: "rsp-sheet-tab-input",
3307
+ value: name,
3308
+ onChange: (event) => {
3309
+ setName(event.target.value);
3310
+ setError("");
3311
+ },
3312
+ "aria-invalid": !!error,
3313
+ "aria-describedby": error ? `${id}-error` : undefined,
3314
+ "data-testid": `rename-sheet-input-${renaming}`
3315
+ }),
3316
+ /* @__PURE__ */ jsx4("button", {
3317
+ type: "submit",
3318
+ className: "rsp-sheet-action",
3319
+ children: "Save"
3320
+ }),
3321
+ /* @__PURE__ */ jsx4("button", {
3322
+ type: "button",
3323
+ className: "rsp-sheet-action",
3324
+ onClick: cancelRename,
3325
+ children: "Cancel"
3326
+ })
3327
+ ]
3328
+ }),
3329
+ error && /* @__PURE__ */ jsx4("div", {
3330
+ id: `${id}-error`,
3331
+ className: "rsp-sheet-error",
3332
+ role: "alert",
3333
+ children: error
3334
+ }),
3335
+ /* @__PURE__ */ jsxs3("div", {
3336
+ className: "rsp-sheet-tabs",
3337
+ children: [
3338
+ /* @__PURE__ */ jsx4("div", {
3339
+ className: "rsp-sheet-tabs-scroll",
3340
+ role: "tablist",
3341
+ "aria-label": `${workbookName} sheets`,
3342
+ children: sheets.map((sheet, index) => /* @__PURE__ */ jsx4("button", {
3343
+ type: "button",
3344
+ role: "tab",
3345
+ className: `rsp-sheet-tab${activeSheet === sheet ? " rsp-sheet-tab-active" : ""}`,
3346
+ "aria-controls": panelId,
3347
+ "aria-selected": activeSheet === sheet,
3348
+ tabIndex: sheet === selected ? 0 : -1,
3349
+ title: sheet,
3350
+ "data-testid": `sheet-tab-${sheet}`,
3351
+ onClick: () => {
3352
+ setMenuOpen(false);
3353
+ onSelect(sheet);
3354
+ },
3355
+ onDoubleClick: () => startRename(sheet),
3356
+ onKeyDown: (event) => {
3357
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
3358
+ let next;
3359
+ if (event.key === "ArrowRight")
3360
+ next = (index + (rtl ? -1 : 1) + sheets.length) % sheets.length;
3361
+ if (event.key === "ArrowLeft")
3362
+ next = (index + (rtl ? 1 : -1) + sheets.length) % sheets.length;
3363
+ if (event.key === "Home")
3364
+ next = 0;
3365
+ if (event.key === "End")
3366
+ next = sheets.length - 1;
3367
+ if (next !== undefined) {
3368
+ event.preventDefault();
3369
+ onSelect(sheets[next]);
3370
+ focusTab(sheets[next]);
3371
+ }
3372
+ if (event.key === "F2") {
3373
+ event.preventDefault();
3374
+ startRename(sheet);
3375
+ }
3376
+ },
3377
+ children: /* @__PURE__ */ jsx4("span", {
3378
+ children: sheet
3379
+ })
3380
+ }, sheet))
3381
+ }),
3382
+ /* @__PURE__ */ jsx4("button", {
3383
+ type: "button",
3384
+ className: "rsp-add-sheet-button",
3385
+ title: "Add Sheet",
3386
+ "aria-label": "Add Sheet",
3387
+ "data-testid": `add-sheet-${workbookName}`,
3388
+ onClick: () => {
3389
+ setMenuOpen(false);
3390
+ run(onAdd);
3391
+ },
3392
+ children: /* @__PURE__ */ jsx4("svg", {
3393
+ "aria-hidden": "true",
3394
+ width: "16",
3395
+ height: "16",
3396
+ viewBox: "0 0 24 24",
3397
+ fill: "none",
3398
+ stroke: "currentColor",
3399
+ strokeWidth: "2",
3400
+ strokeLinecap: "round",
3401
+ children: /* @__PURE__ */ jsx4("path", {
3402
+ d: "M12 5v14M5 12h14"
3403
+ })
3404
+ })
3405
+ }),
3406
+ /* @__PURE__ */ jsx4("button", {
3407
+ ref: menuButton,
3408
+ type: "button",
3409
+ className: "rsp-icon-button rsp-sheet-menu-trigger",
3410
+ title: "Sheet actions",
3411
+ "aria-label": selected ? `Sheet actions for ${selected}` : "Sheet actions",
3412
+ disabled: !selected,
3413
+ "aria-haspopup": "menu",
3414
+ "aria-expanded": menuOpen,
3415
+ "aria-controls": menuOpen ? `${id}-menu` : undefined,
3416
+ onClick: () => setMenuOpen((open) => !open),
3417
+ onKeyDown: (event) => {
3418
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
3419
+ event.preventDefault();
3420
+ setMenuOpen(true);
3421
+ }
3422
+ },
3423
+ children: /* @__PURE__ */ jsxs3("svg", {
3424
+ "aria-hidden": "true",
3425
+ width: "16",
3426
+ height: "16",
3427
+ viewBox: "0 0 24 24",
3428
+ fill: "currentColor",
3429
+ children: [
3430
+ /* @__PURE__ */ jsx4("circle", {
3431
+ cx: "5",
3432
+ cy: "12",
3433
+ r: "2"
3434
+ }),
3435
+ /* @__PURE__ */ jsx4("circle", {
3436
+ cx: "12",
3437
+ cy: "12",
3438
+ r: "2"
3439
+ }),
3440
+ /* @__PURE__ */ jsx4("circle", {
3441
+ cx: "19",
3442
+ cy: "12",
3443
+ r: "2"
3444
+ })
3445
+ ]
3446
+ })
3447
+ }),
3448
+ children
3449
+ ]
3450
+ }),
3451
+ menuOpen && selected && /* @__PURE__ */ jsxs3("div", {
3452
+ ref: menu,
3453
+ id: `${id}-menu`,
3454
+ role: "menu",
3455
+ "aria-label": `Actions for ${selected}`,
3456
+ className: "rsp-sheet-menu",
3457
+ onBlur: (event) => {
3458
+ if (!event.currentTarget.contains(event.relatedTarget) && event.relatedTarget !== menuButton.current)
3459
+ setMenuOpen(false);
3460
+ },
3461
+ onKeyDown: (event) => {
3462
+ const items = Array.from(menu.current?.querySelectorAll("button") ?? []);
3463
+ const index = items.indexOf(document.activeElement);
3464
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
3465
+ event.preventDefault();
3466
+ const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
3467
+ items[next]?.focus();
3468
+ }
3469
+ if (event.key === "Escape") {
3470
+ event.preventDefault();
3471
+ setMenuOpen(false);
3472
+ menuButton.current?.focus();
3473
+ }
3474
+ },
3475
+ children: [
3476
+ /* @__PURE__ */ jsx4("div", {
3477
+ className: "rsp-sheet-menu-name",
3478
+ title: selected,
3479
+ children: selected
3480
+ }),
3481
+ /* @__PURE__ */ jsxs3("button", {
3482
+ type: "button",
3483
+ role: "menuitem",
3484
+ onClick: () => startRename(selected),
3485
+ "data-testid": `rename-sheet-${selected}`,
3486
+ children: [
3487
+ "Rename sheet ",
3488
+ /* @__PURE__ */ jsx4("span", {
3489
+ "aria-hidden": "true",
3490
+ children: "F2"
3491
+ })
3492
+ ]
3493
+ }),
3494
+ /* @__PURE__ */ jsx4("button", {
3495
+ type: "button",
3496
+ role: "menuitem",
3497
+ "aria-disabled": sheets.length <= 1,
3498
+ className: "rsp-sheet-delete",
3499
+ "data-testid": `delete-sheet-${selected}`,
3500
+ onClick: () => {
3501
+ if (sheets.length <= 1)
3502
+ return;
3503
+ if (window.confirm(`Delete sheet "${selected}" and its contents?`)) {
3504
+ if (run(() => onDelete(selected))) {
3505
+ setMenuOpen(false);
3506
+ setNotice(`Sheet ${selected} deleted.`);
3507
+ menuButton.current?.focus();
3508
+ }
3509
+ }
3510
+ },
3511
+ children: "Delete sheet"
3512
+ }),
3513
+ sheets.length <= 1 && /* @__PURE__ */ jsx4("div", {
3514
+ className: "rsp-sheet-menu-hint",
3515
+ children: "Keep at least one sheet."
3516
+ })
3517
+ ]
3518
+ })
3519
+ ]
3520
+ });
3521
+ }
3149
3522
 
3150
3523
  // src/utils/use-scheme-root.ts
3151
- import { useState as useState5 } from "react";
3524
+ import { useState as useState6 } from "react";
3152
3525
  function useSchemeRoot() {
3153
- const [element, setElement] = useState5(null);
3526
+ const [element, setElement] = useState6(null);
3154
3527
  const scheme = useColorScheme(element);
3155
3528
  return { ref: setElement, "data-rsp-scheme": scheme };
3156
3529
  }
@@ -3531,7 +3904,7 @@ class WorkbookSelectionManager {
3531
3904
  }
3532
3905
 
3533
3906
  // src/workbook/workbook.tsx
3534
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3907
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3535
3908
  var getWrapCellStyle = (style) => style?.wrapText ? {
3536
3909
  whiteSpace: "pre-wrap",
3537
3910
  overflowWrap: "anywhere",
@@ -3562,28 +3935,7 @@ var getBorderCellStyle = (style) => (() => {
3562
3935
  ...borderSides.left && { borderLeftColor: style.borderColor }
3563
3936
  };
3564
3937
  })();
3565
- var Edit2Icon = ({ className = "", ...props }) => /* @__PURE__ */ jsxs3("svg", {
3566
- xmlns: "http://www.w3.org/2000/svg",
3567
- width: "16",
3568
- height: "16",
3569
- viewBox: "0 0 24 24",
3570
- fill: "none",
3571
- stroke: "currentColor",
3572
- strokeWidth: "2",
3573
- strokeLinecap: "round",
3574
- strokeLinejoin: "round",
3575
- className,
3576
- ...props,
3577
- children: [
3578
- /* @__PURE__ */ jsx4("path", {
3579
- d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"
3580
- }),
3581
- /* @__PURE__ */ jsx4("path", {
3582
- d: "m15 5 4 4"
3583
- })
3584
- ]
3585
- });
3586
- var Trash2Icon = ({ className = "", ...props }) => /* @__PURE__ */ jsxs3("svg", {
3938
+ var PlusIcon = ({ className = "", ...props }) => /* @__PURE__ */ jsxs4("svg", {
3587
3939
  xmlns: "http://www.w3.org/2000/svg",
3588
3940
  width: "16",
3589
3941
  height: "16",
@@ -3596,51 +3948,15 @@ var Trash2Icon = ({ className = "", ...props }) => /* @__PURE__ */ jsxs3("svg",
3596
3948
  className,
3597
3949
  ...props,
3598
3950
  children: [
3599
- /* @__PURE__ */ jsx4("path", {
3600
- d: "M3 6h18"
3601
- }),
3602
- /* @__PURE__ */ jsx4("path", {
3603
- d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"
3604
- }),
3605
- /* @__PURE__ */ jsx4("path", {
3606
- d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"
3607
- }),
3608
- /* @__PURE__ */ jsx4("line", {
3609
- x1: "10",
3610
- x2: "10",
3611
- y1: "11",
3612
- y2: "17"
3613
- }),
3614
- /* @__PURE__ */ jsx4("line", {
3615
- x1: "14",
3616
- x2: "14",
3617
- y1: "11",
3618
- y2: "17"
3619
- })
3620
- ]
3621
- });
3622
- var PlusIcon = ({ className = "", ...props }) => /* @__PURE__ */ jsxs3("svg", {
3623
- xmlns: "http://www.w3.org/2000/svg",
3624
- width: "16",
3625
- height: "16",
3626
- viewBox: "0 0 24 24",
3627
- fill: "none",
3628
- stroke: "currentColor",
3629
- strokeWidth: "2",
3630
- strokeLinecap: "round",
3631
- strokeLinejoin: "round",
3632
- className,
3633
- ...props,
3634
- children: [
3635
- /* @__PURE__ */ jsx4("path", {
3951
+ /* @__PURE__ */ jsx5("path", {
3636
3952
  d: "M5 12h14"
3637
3953
  }),
3638
- /* @__PURE__ */ jsx4("path", {
3954
+ /* @__PURE__ */ jsx5("path", {
3639
3955
  d: "M12 5v14"
3640
3956
  })
3641
3957
  ]
3642
3958
  });
3643
- var MinusIcon = ({ className = "", ...props }) => /* @__PURE__ */ jsx4("svg", {
3959
+ var MinusIcon = ({ className = "", ...props }) => /* @__PURE__ */ jsx5("svg", {
3644
3960
  xmlns: "http://www.w3.org/2000/svg",
3645
3961
  width: "16",
3646
3962
  height: "16",
@@ -3652,13 +3968,13 @@ var MinusIcon = ({ className = "", ...props }) => /* @__PURE__ */ jsx4("svg", {
3652
3968
  strokeLinejoin: "round",
3653
3969
  className,
3654
3970
  ...props,
3655
- children: /* @__PURE__ */ jsx4("path", {
3971
+ children: /* @__PURE__ */ jsx5("path", {
3656
3972
  d: "M5 12h14"
3657
3973
  })
3658
3974
  });
3659
3975
  function useEngine(engine) {
3660
- const [state, setState] = useState6(() => engine.getState());
3661
- React3.useEffect(() => {
3976
+ const [state, setState] = useState7(() => engine.getState());
3977
+ React4.useEffect(() => {
3662
3978
  return engine.onUpdate(() => {
3663
3979
  setState(engine.getState());
3664
3980
  });
@@ -3764,8 +4080,8 @@ function FormulaSheet({
3764
4080
  ...rest
3765
4081
  }) {
3766
4082
  const schemeRoot = useSchemeRoot();
3767
- const [selectedArea, setSelectedArea] = useState6(null);
3768
- const clipboardManager = React3.useMemo(() => controlledClipboardManager ?? new WorkbookClipboardManager(engine), [controlledClipboardManager, engine]);
4083
+ const [selectedArea, setSelectedArea] = useState7(null);
4084
+ const clipboardManager = React4.useMemo(() => controlledClipboardManager ?? new WorkbookClipboardManager(engine), [controlledClipboardManager, engine]);
3769
4085
  const state = useEngine(engine);
3770
4086
  const sheet = state.workbooks.get(workbookName)?.sheets.get(sheetName);
3771
4087
  const getEngineTableAt = useCallback4((cell) => {
@@ -3804,20 +4120,14 @@ function FormulaSheet({
3804
4120
  ...selection?.navigation,
3805
4121
  getTableAt: selection?.navigation?.getTableAt ?? getEngineTableAt
3806
4122
  }), [getEngineTableAt, selection?.navigation]);
3807
- const onCellDataChange = useCallback4((updatedSpreadsheet) => {
3808
- const data = new Map(updatedSpreadsheet);
3809
- data.forEach((value, key) => {
3810
- if (typeof value === "string") {
4123
+ const onCellDataPatch = useCallback4((patch) => {
4124
+ engine.transact(() => {
4125
+ patch.forEach((value, key) => {
3811
4126
  const { colIndex, rowIndex } = parseCellReference2(key);
3812
- const coercedValue = coerceCellInput(value, engine.getCellDataType({ workbookName, sheetName, colIndex, rowIndex }));
3813
- if (coercedValue === undefined) {
3814
- data.delete(key);
3815
- } else {
3816
- data.set(key, coercedValue);
3817
- }
3818
- }
4127
+ const address = { workbookName, sheetName, colIndex, rowIndex };
4128
+ engine.setCellContent(address, typeof value === "string" ? coerceCellInput(value, engine.getCellDataType(address)) : value);
4129
+ });
3819
4130
  });
3820
- engine.setSheetContent({ sheetName, workbookName }, data);
3821
4131
  }, [sheetName, engine, workbookName]);
3822
4132
  const passedSelectionEffects = selection?.effects;
3823
4133
  const selectionManagerEffects = useCallback4((spreadsheetSelectionManager) => {
@@ -4069,14 +4379,14 @@ function FormulaSheet({
4069
4379
  Object.assign(conditionalStyleProps, fontStyle(conditionalStyle), getWrapContentStyle(conditionalStyle));
4070
4380
  let defaultElement;
4071
4381
  if (showFormulas) {
4072
- defaultElement = /* @__PURE__ */ jsx4("div", {
4382
+ defaultElement = /* @__PURE__ */ jsx5("div", {
4073
4383
  "data-conditional-background": conditionalStyle?.backgroundColor,
4074
4384
  "data-conditional-text-color": conditionalStyle?.color,
4075
4385
  style: conditionalStyleProps,
4076
4386
  children: getCellDisplayText(value)
4077
4387
  });
4078
4388
  } else if (typeof value === "boolean") {
4079
- defaultElement = /* @__PURE__ */ jsx4("div", {
4389
+ defaultElement = /* @__PURE__ */ jsx5("div", {
4080
4390
  "data-conditional-background": conditionalStyle?.backgroundColor,
4081
4391
  "data-conditional-text-color": conditionalStyle?.color,
4082
4392
  "data-type": "boolean",
@@ -4089,7 +4399,7 @@ function FormulaSheet({
4089
4399
  children: value ? "TRUE" : "FALSE"
4090
4400
  });
4091
4401
  } else if (typeof value === "number") {
4092
- defaultElement = /* @__PURE__ */ jsx4("div", {
4402
+ defaultElement = /* @__PURE__ */ jsx5("div", {
4093
4403
  "data-conditional-background": conditionalStyle?.backgroundColor,
4094
4404
  "data-conditional-text-color": conditionalStyle?.color,
4095
4405
  "data-type": "number",
@@ -4100,7 +4410,7 @@ function FormulaSheet({
4100
4410
  })
4101
4411
  });
4102
4412
  } else {
4103
- defaultElement = /* @__PURE__ */ jsx4("div", {
4413
+ defaultElement = /* @__PURE__ */ jsx5("div", {
4104
4414
  "data-conditional-background": conditionalStyle?.backgroundColor,
4105
4415
  "data-conditional-text-color": conditionalStyle?.color,
4106
4416
  style: { ...conditionalStyleProps, width: "100%", textAlign: "left" },
@@ -4125,29 +4435,29 @@ function FormulaSheet({
4125
4435
  customCellRenderer
4126
4436
  ]);
4127
4437
  if (!sheet) {
4128
- return /* @__PURE__ */ jsxs3("div", {
4438
+ return /* @__PURE__ */ jsxs4("div", {
4129
4439
  className: "rsp-root rsp-workbook",
4130
4440
  ...schemeRoot,
4131
4441
  children: [
4132
4442
  toolbar,
4133
- /* @__PURE__ */ jsx4("div", {
4443
+ /* @__PURE__ */ jsx5("div", {
4134
4444
  style: { flex: 1 }
4135
4445
  })
4136
4446
  ]
4137
4447
  });
4138
4448
  }
4139
- return /* @__PURE__ */ jsxs3("div", {
4449
+ return /* @__PURE__ */ jsxs4("div", {
4140
4450
  className: "rsp-root rsp-workbook",
4141
4451
  ...schemeRoot,
4142
4452
  children: [
4143
4453
  toolbar,
4144
- /* @__PURE__ */ jsx4("div", {
4454
+ /* @__PURE__ */ jsx5("div", {
4145
4455
  className: "rsp-workbook-body",
4146
- children: /* @__PURE__ */ jsx4(Spreadsheet, {
4456
+ children: /* @__PURE__ */ jsx5(Spreadsheet, {
4147
4457
  disableClipboard: true,
4148
4458
  style: { height: "100%", width: "100%" },
4149
4459
  cellData: sheet?.content,
4150
- onCellDataChange,
4460
+ onCellDataPatch,
4151
4461
  onCellDataChangeError,
4152
4462
  ...rest,
4153
4463
  components,
@@ -4195,18 +4505,16 @@ function FormulaWorkbook({
4195
4505
  ...rest
4196
4506
  }) {
4197
4507
  const schemeRoot = useSchemeRoot();
4198
- const state = useEngine(engine);
4199
- const [renamingSheet, setRenamingSheet] = useState6(null);
4200
- const [newSheetName, setNewSheetName] = useState6("");
4201
- const [spreadsheetZoom, setSpreadsheetZoom] = useState6(1);
4202
- const renameCommittedRef = useRef4(false);
4203
- const clipboardManager = React3.useMemo(() => controlledClipboardManager ?? new WorkbookClipboardManager(engine), [controlledClipboardManager, engine]);
4508
+ useEngine(engine);
4509
+ const panelId = React4.useId();
4510
+ const [spreadsheetZoom, setSpreadsheetZoom] = useState7(1);
4511
+ const clipboardManager = React4.useMemo(() => controlledClipboardManager ?? new WorkbookClipboardManager(engine), [controlledClipboardManager, engine]);
4204
4512
  const sheetNames = engine.getOrderedSheetNames(workbookName);
4205
- const [internalActiveSheet, setInternalActiveSheet] = useState6(() => {
4513
+ const [internalActiveSheet, setInternalActiveSheet] = useState7(() => {
4206
4514
  return sheetNames[0] || "";
4207
4515
  });
4208
- const isControlledRef = useRef4(controlledActiveSheet !== undefined);
4209
- React3.useEffect(() => {
4516
+ const isControlledRef = useRef5(controlledActiveSheet !== undefined);
4517
+ React4.useEffect(() => {
4210
4518
  const isCurrentlyControlled = controlledActiveSheet !== undefined;
4211
4519
  if (isControlledRef.current !== isCurrentlyControlled) {
4212
4520
  console.error(`FormulaWorkbook: Component changed from ${isControlledRef.current ? "controlled" : "uncontrolled"} to ${isCurrentlyControlled ? "controlled" : "uncontrolled"} mode. ` + `A component should not switch between controlled and uncontrolled modes. ` + `Decide between using activeSheet and onActiveSheetChange props (controlled) or neither (uncontrolled) for the lifetime of the component.`);
@@ -4217,10 +4525,10 @@ function FormulaWorkbook({
4217
4525
  const handleActiveSheetChange = useCallback4((sheetName) => {
4218
4526
  if (onActiveSheetChange) {
4219
4527
  onActiveSheetChange(sheetName);
4220
- } else {
4221
- setInternalActiveSheet(sheetName);
4222
4528
  }
4223
- }, [onActiveSheetChange]);
4529
+ if (controlledActiveSheet === undefined)
4530
+ setInternalActiveSheet(sheetName);
4531
+ }, [onActiveSheetChange, controlledActiveSheet]);
4224
4532
  const activeSheetOverlayChildren = sheetOverlayChildren?.[activeSheet];
4225
4533
  const activeSheetSelectedOverlayId = sheetSelectedOverlayIds?.[activeSheet];
4226
4534
  const activeSheetColumnWidths = sheetLayouts?.[activeSheet]?.columnWidths;
@@ -4238,76 +4546,37 @@ function FormulaWorkbook({
4238
4546
  onSheetRowHeightsChange?.(activeSheet, rowHeights);
4239
4547
  }, [activeSheet, onSheetRowHeightsChange]);
4240
4548
  const addSheet = () => {
4241
- try {
4242
- const newSheet = engine.createSheet({
4243
- workbookName
4244
- });
4245
- handleActiveSheetChange(newSheet.name);
4246
- } catch (error) {
4247
- console.error("Failed to add sheet:", error);
4248
- }
4249
- };
4250
- const startRenaming = (sheetName) => {
4251
- renameCommittedRef.current = false;
4252
- setRenamingSheet(sheetName);
4253
- setNewSheetName(sheetName);
4254
- };
4255
- const cancelRenaming = () => {
4256
- setRenamingSheet(null);
4257
- setNewSheetName("");
4549
+ const newSheet = engine.createSheet({ workbookName });
4550
+ handleActiveSheetChange(newSheet.name);
4258
4551
  };
4259
4552
  const renameSheet = (oldName, newName) => {
4260
- if (renameCommittedRef.current) {
4261
- cancelRenaming();
4262
- return;
4263
- }
4264
- if (!newName.trim() || newName.trim() === oldName) {
4265
- cancelRenaming();
4266
- return;
4267
- }
4268
- try {
4269
- renameCommittedRef.current = true;
4270
- engine.renameSheet({
4271
- workbookName,
4272
- sheetName: oldName,
4273
- newSheetName: newName.trim()
4274
- });
4275
- if (activeSheet === oldName) {
4276
- handleActiveSheetChange(newName.trim());
4277
- }
4278
- cancelRenaming();
4279
- } catch (error) {
4280
- console.error("Failed to rename sheet:", error);
4281
- cancelRenaming();
4282
- }
4553
+ engine.renameSheet({ workbookName, sheetName: oldName, newSheetName: newName });
4554
+ if (activeSheet === oldName)
4555
+ handleActiveSheetChange(newName);
4283
4556
  };
4284
4557
  const deleteSheet = (sheetName) => {
4285
4558
  if (sheetNames.length <= 1) {
4286
4559
  console.error("Cannot delete the last sheet");
4287
4560
  return;
4288
4561
  }
4289
- try {
4290
- engine.removeSheet({ workbookName, sheetName });
4291
- if (activeSheet === sheetName) {
4292
- const remainingSheets = sheetNames.filter((name) => name !== sheetName);
4293
- const nextSheet = remainingSheets[0];
4294
- if (nextSheet) {
4295
- handleActiveSheetChange(nextSheet);
4296
- }
4562
+ engine.removeSheet({ workbookName, sheetName });
4563
+ if (activeSheet === sheetName) {
4564
+ const remainingSheets = sheetNames.filter((name) => name !== sheetName);
4565
+ const nextSheet = remainingSheets[Math.min(sheetNames.indexOf(sheetName), remainingSheets.length - 1)];
4566
+ if (nextSheet) {
4567
+ handleActiveSheetChange(nextSheet);
4297
4568
  }
4298
- } catch (error) {
4299
- console.error("Failed to delete sheet:", error);
4300
4569
  }
4301
4570
  };
4302
- return /* @__PURE__ */ jsxs3("div", {
4571
+ return /* @__PURE__ */ jsxs4("div", {
4303
4572
  className: "rsp-root rsp-workbook",
4304
4573
  ...schemeRoot,
4305
4574
  children: [
4306
4575
  toolbar,
4307
- /* @__PURE__ */ jsxs3("div", {
4576
+ /* @__PURE__ */ jsxs4("div", {
4308
4577
  className: "rsp-workbook-header",
4309
4578
  children: [
4310
- /* @__PURE__ */ jsx4("div", {
4579
+ /* @__PURE__ */ jsx5("div", {
4311
4580
  style: {
4312
4581
  display: "flex",
4313
4582
  alignItems: "center",
@@ -4316,13 +4585,13 @@ function FormulaWorkbook({
4316
4585
  overflow: "hidden",
4317
4586
  textOverflow: "ellipsis"
4318
4587
  },
4319
- children: /* @__PURE__ */ jsx4("span", {
4588
+ children: /* @__PURE__ */ jsx5("span", {
4320
4589
  className: "rsp-workbook-name",
4321
4590
  "data-testid": `workbook-name-${workbookName}`,
4322
4591
  children: workbookName
4323
4592
  })
4324
4593
  }),
4325
- /* @__PURE__ */ jsxs3("span", {
4594
+ /* @__PURE__ */ jsxs4("span", {
4326
4595
  className: "rsp-zoom-value",
4327
4596
  "data-testid": `sheet-count-${workbookName}`,
4328
4597
  children: [
@@ -4332,16 +4601,19 @@ function FormulaWorkbook({
4332
4601
  })
4333
4602
  ]
4334
4603
  }),
4335
- /* @__PURE__ */ jsx4("div", {
4336
- style: { flex: 1, overflow: "hidden" },
4337
- children: /* @__PURE__ */ jsx4("div", {
4604
+ /* @__PURE__ */ jsx5("div", {
4605
+ id: panelId,
4606
+ role: "tabpanel",
4607
+ "aria-label": activeSheet || "Sheet",
4608
+ style: { flex: 1, minHeight: 0, overflow: "hidden" },
4609
+ children: /* @__PURE__ */ jsx5("div", {
4338
4610
  style: {
4339
4611
  transform: spreadsheetZoom !== 1 ? `scale(${spreadsheetZoom})` : undefined,
4340
4612
  transformOrigin: "0 0",
4341
4613
  width: spreadsheetZoom !== 1 ? `${100 / spreadsheetZoom}%` : "100%",
4342
4614
  height: spreadsheetZoom !== 1 ? `${100 / spreadsheetZoom}%` : "100%"
4343
4615
  },
4344
- children: /* @__PURE__ */ jsx4(FormulaSheet, {
4616
+ children: /* @__PURE__ */ jsx5(FormulaSheet, {
4345
4617
  sheetName: activeSheet,
4346
4618
  workbookName,
4347
4619
  engine,
@@ -4365,138 +4637,53 @@ function FormulaWorkbook({
4365
4637
  }, `${workbookName}-${activeSheet}`)
4366
4638
  })
4367
4639
  }),
4368
- /* @__PURE__ */ jsxs3("div", {
4369
- className: "rsp-sheet-tabs",
4370
- children: [
4371
- /* @__PURE__ */ jsxs3("div", {
4372
- className: "rsp-sheet-tabs-scroll",
4373
- children: [
4374
- /* @__PURE__ */ jsx4("div", {
4375
- style: { display: "flex", alignItems: "center", gap: 4 },
4376
- children: sheetNames.map((sheetName) => {
4377
- const isRenaming = renamingSheet === sheetName;
4378
- const isActive = activeSheet === sheetName;
4379
- return /* @__PURE__ */ jsx4("div", {
4380
- children: isRenaming ? /* @__PURE__ */ jsx4("div", {
4381
- style: { display: "flex", alignItems: "center", gap: 4 },
4382
- children: /* @__PURE__ */ jsx4("input", {
4383
- className: "rsp-sheet-tab-input",
4384
- type: "text",
4385
- value: newSheetName,
4386
- onChange: (e) => setNewSheetName(e.target.value),
4387
- onKeyDown: (e) => {
4388
- if (e.key === "Enter") {
4389
- renameSheet(sheetName, newSheetName);
4390
- } else if (e.key === "Escape") {
4391
- cancelRenaming();
4392
- }
4393
- },
4394
- onBlur: () => {
4395
- if (newSheetName.trim() && newSheetName.trim() !== sheetName) {
4396
- renameSheet(sheetName, newSheetName);
4397
- } else {
4398
- cancelRenaming();
4399
- }
4400
- },
4401
- autoFocus: true,
4402
- "data-testid": `rename-sheet-input-${sheetName}`
4403
- })
4404
- }) : /* @__PURE__ */ jsxs3("div", {
4405
- className: cn("rsp-sheet-tab", {
4406
- "rsp-sheet-tab-active": isActive
4407
- }),
4408
- onClick: () => handleActiveSheetChange(sheetName),
4409
- onDoubleClick: () => startRenaming(sheetName),
4410
- "data-testid": `sheet-tab-${sheetName}`,
4411
- children: [
4412
- /* @__PURE__ */ jsx4("span", {
4413
- children: sheetName
4414
- }),
4415
- /* @__PURE__ */ jsxs3("div", {
4416
- className: "sheet-tab-actions",
4417
- children: [
4418
- /* @__PURE__ */ jsx4("button", {
4419
- type: "button",
4420
- className: "rsp-icon-button",
4421
- onClick: (e) => {
4422
- e.stopPropagation();
4423
- startRenaming(sheetName);
4424
- },
4425
- title: "Rename Sheet",
4426
- "data-testid": `rename-sheet-${sheetName}`,
4427
- children: /* @__PURE__ */ jsx4(Edit2Icon, {
4428
- style: { width: 12, height: 12 }
4429
- })
4430
- }),
4431
- sheetNames.length > 1 && /* @__PURE__ */ jsx4("button", {
4432
- type: "button",
4433
- className: "rsp-icon-button rsp-icon-button-delete",
4434
- onClick: (e) => {
4435
- e.stopPropagation();
4436
- if (confirm(`Are you sure you want to delete sheet "${sheetName}"?`)) {
4437
- deleteSheet(sheetName);
4438
- }
4439
- },
4440
- title: "Delete Sheet",
4441
- "data-testid": `delete-sheet-${sheetName}`,
4442
- children: /* @__PURE__ */ jsx4(Trash2Icon, {
4443
- style: { width: 12, height: 12 }
4444
- })
4445
- })
4446
- ]
4447
- })
4448
- ]
4449
- })
4450
- }, sheetName);
4451
- })
4452
- }),
4453
- /* @__PURE__ */ jsx4("button", {
4454
- type: "button",
4455
- className: "rsp-add-sheet-button",
4456
- onClick: addSheet,
4457
- title: "Add Sheet",
4458
- "data-testid": `add-sheet-${workbookName}`,
4459
- children: /* @__PURE__ */ jsx4(PlusIcon, {
4460
- style: { width: 12, height: 12 }
4461
- })
4640
+ /* @__PURE__ */ jsx5(SheetNavigation, {
4641
+ sheets: sheetNames,
4642
+ activeSheet,
4643
+ workbookName,
4644
+ panelId,
4645
+ onSelect: handleActiveSheetChange,
4646
+ onAdd: addSheet,
4647
+ onRename: renameSheet,
4648
+ onDelete: deleteSheet,
4649
+ children: /* @__PURE__ */ jsxs4("div", {
4650
+ className: "rsp-zoom-controls",
4651
+ children: [
4652
+ /* @__PURE__ */ jsx5("button", {
4653
+ type: "button",
4654
+ className: "rsp-icon-button",
4655
+ onClick: () => setSpreadsheetZoom(Math.max(0.5, Math.round((spreadsheetZoom - 0.1) * 10) / 10)),
4656
+ disabled: spreadsheetZoom <= 0.5,
4657
+ title: "Zoom Out",
4658
+ style: { opacity: spreadsheetZoom <= 0.5 ? 0.5 : 1 },
4659
+ children: /* @__PURE__ */ jsx5(MinusIcon, {
4660
+ style: { width: 12, height: 12 }
4462
4661
  })
4463
- ]
4464
- }),
4465
- /* @__PURE__ */ jsxs3("div", {
4466
- className: "rsp-zoom-controls",
4467
- children: [
4468
- /* @__PURE__ */ jsx4("button", {
4469
- type: "button",
4470
- className: "rsp-icon-button",
4471
- onClick: () => setSpreadsheetZoom(Math.max(0.5, spreadsheetZoom - 0.1)),
4472
- disabled: spreadsheetZoom <= 0.5,
4473
- title: "Zoom Out",
4474
- style: { opacity: spreadsheetZoom <= 0.5 ? 0.5 : 1 },
4475
- children: /* @__PURE__ */ jsx4(MinusIcon, {
4476
- style: { width: 12, height: 12 }
4477
- })
4478
- }),
4479
- /* @__PURE__ */ jsxs3("span", {
4480
- className: "rsp-zoom-value",
4481
- children: [
4482
- Math.round(spreadsheetZoom * 100),
4483
- "%"
4484
- ]
4485
- }),
4486
- /* @__PURE__ */ jsx4("button", {
4487
- type: "button",
4488
- className: "rsp-icon-button",
4489
- onClick: () => setSpreadsheetZoom(Math.min(2, spreadsheetZoom + 0.1)),
4490
- disabled: spreadsheetZoom >= 2,
4491
- title: "Zoom In",
4492
- style: { opacity: spreadsheetZoom >= 2 ? 0.5 : 1 },
4493
- children: /* @__PURE__ */ jsx4(PlusIcon, {
4494
- style: { width: 12, height: 12 }
4495
- })
4662
+ }),
4663
+ /* @__PURE__ */ jsxs4("button", {
4664
+ type: "button",
4665
+ className: "rsp-zoom-value rsp-zoom-reset",
4666
+ "aria-label": `Zoom ${Math.round(spreadsheetZoom * 100)}%. Reset to 100%`,
4667
+ title: "Reset zoom to 100%",
4668
+ onClick: () => setSpreadsheetZoom(1),
4669
+ children: [
4670
+ Math.round(spreadsheetZoom * 100),
4671
+ "%"
4672
+ ]
4673
+ }),
4674
+ /* @__PURE__ */ jsx5("button", {
4675
+ type: "button",
4676
+ className: "rsp-icon-button",
4677
+ onClick: () => setSpreadsheetZoom(Math.min(2, Math.round((spreadsheetZoom + 0.1) * 10) / 10)),
4678
+ disabled: spreadsheetZoom >= 2,
4679
+ title: "Zoom In",
4680
+ style: { opacity: spreadsheetZoom >= 2 ? 0.5 : 1 },
4681
+ children: /* @__PURE__ */ jsx5(PlusIcon, {
4682
+ style: { width: 12, height: 12 }
4496
4683
  })
4497
- ]
4498
- })
4499
- ]
4684
+ })
4685
+ ]
4686
+ })
4500
4687
  })
4501
4688
  ]
4502
4689
  });
@@ -4508,9 +4695,9 @@ var DEFAULT_ZOOM = 1;
4508
4695
  var MIN_ZOOM = 0.5;
4509
4696
  var MAX_ZOOM = 3;
4510
4697
  // src/utils/use-click-away.ts
4511
- import { useEffect as useEffect4 } from "react";
4698
+ import { useEffect as useEffect5 } from "react";
4512
4699
  var useClickAway = (ref, callback) => {
4513
- useEffect4(() => {
4700
+ useEffect5(() => {
4514
4701
  const handleClickAway = (event) => {
4515
4702
  if (ref) {
4516
4703
  const away = !ref.contains(event.target);
@@ -4575,4 +4762,4 @@ export {
4575
4762
  useResizeObserver
4576
4763
  };
4577
4764
 
4578
- //# debugId=05DAA767DBF706AE64756E2164756E21
4765
+ //# debugId=66ABFE3983D95F6964756E2164756E21