@a3s-lab/office 0.37.2 → 0.37.3

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
@@ -309,9 +309,12 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
309
309
 
310
310
  ## Current release
311
311
 
312
- Version `0.37.2` keeps the release surface focused on local, testable
312
+ Version `0.37.3` keeps the release surface focused on local, testable
313
313
  workflows:
314
314
 
315
+ - Spreadsheet Custom Sort now includes a responsive, keyboard-accessible
316
+ preference manager for creating, editing, deleting, and reordering bounded
317
+ user sequences while keeping the seven built-in sequences read-only.
315
318
  - Document, visual Markdown, and Presentation text composition is safe for
316
319
  Chinese and other IMEs: pre-edit text remains local, controlled replacement
317
320
  waits for settlement, and only the committed value reaches the host.
@@ -1,7 +1,7 @@
1
1
  import { __webpack_require__ } from "./0~rslib-runtime.js";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Workbook } from "@fortune-sheet/react";
4
- import { AArrowDown, AArrowUp, AlignCenter, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowDownToLine, ArrowLeft, ArrowLeftToLine, ArrowRight, ArrowRightToLine, ArrowUp, ArrowUpToLine, BadgeJapaneseYen, BarChart3, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, Bookmark, Calculator, CalendarClock, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, ClipboardPaste, Clock, Cloud, Columns3, Copy, CopyPlus, DecimalsArrowLeft, DecimalsArrowRight, Eraser, Eye, EyeOff, FileX2, Globe2, Grid2X2, Grid3X3, Hash, Info, Italic, KeyRound, Layers2, Link2, Link2Off, ListChecks, ListFilter, ListPlus, LocateFixed, Merge, MessageSquareX, MoveHorizontal, MoveVertical, Paintbrush, Palette, PanelLeft, PanelTop, PanelsTopLeft, Pencil, Percent, Plus, Printer, Redo2, RefreshCw, Rows3, Save, Scissors, Search, Settings2, ShieldCheck, Sigma, SlidersHorizontal, SortAsc, SortDesc, Strikethrough, SwatchBook, Table2, TableProperties, Trash2, TriangleAlert, Underline, Undo2, WrapText, X } from "lucide-react";
4
+ import { AArrowDown, AArrowUp, AlignCenter, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowDownToLine, ArrowLeft, ArrowLeftToLine, ArrowRight, ArrowRightToLine, ArrowUp, ArrowUpToLine, BadgeJapaneseYen, BarChart3, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, Bookmark, Calculator, CalendarClock, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, ClipboardPaste, Clock, Cloud, Columns3, Copy, CopyPlus, DecimalsArrowLeft, DecimalsArrowRight, Eraser, Eye, EyeOff, FileX2, Globe2, Grid2X2, Grid3X3, Hash, Info, Italic, KeyRound, Layers2, Link2, Link2Off, ListChecks, ListFilter, ListOrdered, ListPlus, LocateFixed, Merge, MessageSquareX, MoveHorizontal, MoveVertical, Paintbrush, Palette, PanelLeft, PanelTop, PanelsTopLeft, Pencil, Percent, Plus, Printer, Redo2, RefreshCw, Rows3, Save, Scissors, Search, Settings2, ShieldCheck, Sigma, SlidersHorizontal, SortAsc, SortDesc, Strikethrough, SwatchBook, Table2, TableProperties, Trash2, TriangleAlert, Underline, Undo2, WrapText, X } from "lucide-react";
5
5
  import { Fragment as external_react_Fragment, cloneElement, createElement, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
6
  import { convertSpanToShareString, genarate, indexToColumnChar, update as core_update } from "@fortune-sheet/core";
7
7
  import { freezeImportedSpreadsheetCell, normalizeXlsxSemanticColorOrigin, spreadsheetMatrixProfile, withXlsxPatternFill, withoutEditableRange, normalizeSpreadsheetConditionalVisualOptions, xlsxSemanticColorMatchesValue, withXlsxGradientFill, effectiveSpreadsheetPageSetup, attachSpreadsheetShownCommentCells, normalizeXlsxPatternFill, withEditableRange, sameSpreadsheetHistoryValue, isSpreadsheetConditionalComparisonOperator, spreadsheetConditionalIconForValue, defaultSpreadsheetConditionalIconThresholds, xlsxPatternFillTypes, sheetProtectionAuthority, normalizeXlsxGradientFill, xlsxGradientFillFallbackColor, editableRangeCellCount, unlockedCellCount, registerDerivedSpreadsheetMatrix, editableRangeRequiresCredentials, SPREADSHEET_CONDITIONAL_COMPARISON_OPERATORS, normalizeSpreadsheetConditionalIconSetFormat, defaultSpreadsheetDataBarOptions, spreadsheetConditionalThresholdValue, normalizeSheetProtectionAuthority, withSheetProtection, spreadsheetProtectionKey, defaultSpreadsheetColorScaleThresholds, protectedSheetCount, SPREADSHEET_CONDITIONAL_ICON_SETS, withSheetSelectionPermissions, spreadsheetConditionalComparisonNeedsUpperValue, drawSpreadsheetConditionalIcon, isSpreadsheetConditionalIconSetName, spreadsheetConditionalThresholdsEqual, spreadsheetConditionalIconSetCount, spreadsheetConditionalIconAppearance } from "./9333.js";
@@ -27408,6 +27408,385 @@ function SpreadsheetSortCustomListEditor({ error, level, text, onCancel, onChang
27408
27408
  ]
27409
27409
  });
27410
27410
  }
27411
+ function SpreadsheetSortCustomListManagerDialog({ customLists, restoreFocusTarget, onApply, onClose }) {
27412
+ const builtInLists = useMemo(()=>customLists.filter((list)=>'built-in' === list.source), [
27413
+ customLists
27414
+ ]);
27415
+ const initialUserListsRef = useRef(null);
27416
+ initialUserListsRef.current ??= customLists.filter((list)=>'built-in' !== list.source).map((list, index)=>({
27417
+ id: index,
27418
+ entries: Object.freeze([
27419
+ ...list.entries
27420
+ ]),
27421
+ originalEntries: Object.freeze([
27422
+ ...list.entries
27423
+ ])
27424
+ }));
27425
+ const initialUserLists = initialUserListsRef.current;
27426
+ const nextIdRef = useRef(initialUserLists.length);
27427
+ const listRef = useRef(null);
27428
+ const [userLists, setUserLists] = useState(()=>initialUserLists.map(cloneManagedCustomList));
27429
+ const [selection, setSelection] = useState('built-in:0');
27430
+ const [text, setText] = useState(builtInLists[0]?.entries.join('\n') ?? '');
27431
+ const [error, setError] = useState(null);
27432
+ const userListLabels = useMemo(()=>new Map(userLists.map((list)=>[
27433
+ list.id,
27434
+ managedCustomListLabel(list.entries)
27435
+ ])), [
27436
+ userLists
27437
+ ]);
27438
+ useEffect(()=>listRef.current?.focus(), []);
27439
+ const selectedUserIndex = managedUserListIndex(selection, userLists);
27440
+ const selectedUser = userLists[selectedUserIndex];
27441
+ const selectedBuiltIn = managedBuiltInList(selection, builtInLists);
27442
+ const editingNewList = 'new' === selection;
27443
+ const validateDraft = (rows, excludedId)=>{
27444
+ const validation = parseSpreadsheetSortCustomList(text);
27445
+ if (!validation.ok) {
27446
+ setError(validation.message);
27447
+ return null;
27448
+ }
27449
+ const duplicate = [
27450
+ ...builtInLists.map((list)=>list.entries),
27451
+ ...rows.filter((row)=>row.id !== excludedId).map((row)=>row.entries)
27452
+ ].some((entries)=>spreadsheetSortCustomListsEqual(entries, validation.entries));
27453
+ if (duplicate) {
27454
+ setError('该自定义序列已存在。');
27455
+ return null;
27456
+ }
27457
+ setError(null);
27458
+ return Object.freeze([
27459
+ ...validation.entries
27460
+ ]);
27461
+ };
27462
+ const commitSelectedUser = (rows)=>{
27463
+ const index = managedUserListIndex(selection, rows);
27464
+ const selected = rows[index];
27465
+ if (!selected) return rows;
27466
+ const entries = validateDraft(rows, selected.id);
27467
+ if (!entries) return null;
27468
+ return rows.map((row)=>row.id === selected.id ? {
27469
+ ...row,
27470
+ entries
27471
+ } : row);
27472
+ };
27473
+ const select = (nextSelection)=>{
27474
+ let rows = userLists;
27475
+ if (selectedUser) {
27476
+ const committed = commitSelectedUser(rows);
27477
+ if (!committed) return;
27478
+ rows = committed;
27479
+ setUserLists(rows);
27480
+ } else if (editingNewList && text.trim()) return void setError('请先添加当前序列,或清空项目后再切换。');
27481
+ setSelection(nextSelection);
27482
+ setText(managedSelectionText(nextSelection, builtInLists, rows));
27483
+ setError(null);
27484
+ };
27485
+ const saveSelectedUser = ()=>{
27486
+ const committed = commitSelectedUser(userLists);
27487
+ if (!committed) return;
27488
+ setUserLists(committed);
27489
+ setText(managedSelectionText(selection, builtInLists, committed));
27490
+ };
27491
+ const addNewList = ()=>{
27492
+ if (userLists.length >= 32) return void setError("当前编辑器最多保留 32 个自定义序列。");
27493
+ const entries = validateDraft(userLists, null);
27494
+ if (!entries) return;
27495
+ const id = nextIdRef.current;
27496
+ nextIdRef.current += 1;
27497
+ const next = [
27498
+ ...userLists,
27499
+ {
27500
+ id,
27501
+ entries,
27502
+ originalEntries: null
27503
+ }
27504
+ ];
27505
+ setUserLists(next);
27506
+ setSelection(`user:${id}`);
27507
+ setText(entries.join('\n'));
27508
+ };
27509
+ const moveSelectedUser = (offset)=>{
27510
+ const committed = commitSelectedUser(userLists);
27511
+ if (!committed) return;
27512
+ const index = managedUserListIndex(selection, committed);
27513
+ const nextIndex = index + offset;
27514
+ if (index < 0 || nextIndex < 0 || nextIndex >= committed.length) return;
27515
+ const next = committed.map(cloneManagedCustomList);
27516
+ const selected = next[index];
27517
+ const target = next[nextIndex];
27518
+ if (!selected || !target) return;
27519
+ next[index] = target;
27520
+ next[nextIndex] = selected;
27521
+ setUserLists(next);
27522
+ setText(selected.entries.join('\n'));
27523
+ };
27524
+ const deleteSelectedUser = ()=>{
27525
+ if (!selectedUser) return;
27526
+ const next = userLists.filter((row)=>row.id !== selectedUser.id);
27527
+ setUserLists(next);
27528
+ const fallback = next[selectedUserIndex] ?? next[selectedUserIndex - 1];
27529
+ const nextSelection = fallback ? `user:${fallback.id}` : 'built-in:0';
27530
+ setSelection(nextSelection);
27531
+ setText(managedSelectionText(nextSelection, builtInLists, next));
27532
+ setError(null);
27533
+ };
27534
+ const apply = ()=>{
27535
+ let rows = userLists;
27536
+ if (selectedUser) {
27537
+ const committed = commitSelectedUser(rows);
27538
+ if (!committed) return;
27539
+ rows = committed;
27540
+ } else if (editingNewList && text.trim()) {
27541
+ if (rows.length >= 32) return void setError("当前编辑器最多保留 32 个自定义序列。");
27542
+ const entries = validateDraft(rows, null);
27543
+ if (!entries) return;
27544
+ const id = nextIdRef.current;
27545
+ nextIdRef.current += 1;
27546
+ rows = [
27547
+ ...rows,
27548
+ {
27549
+ id,
27550
+ entries,
27551
+ originalEntries: null
27552
+ }
27553
+ ];
27554
+ }
27555
+ onApply(managedCustomListResult(initialUserLists, rows));
27556
+ onClose();
27557
+ };
27558
+ return /*#__PURE__*/ jsxs(Dialog, {
27559
+ title: "自定义序列",
27560
+ description: "管理排序时可复用的本地序列。内置月份和星期序列保持只读。",
27561
+ className: "work-spreadsheet-sort-custom-list-manager",
27562
+ restoreFocusTarget: restoreFocusTarget,
27563
+ onClose: onClose,
27564
+ footer: /*#__PURE__*/ jsxs(Fragment, {
27565
+ children: [
27566
+ /*#__PURE__*/ jsx(button_Button, {
27567
+ tone: "quiet",
27568
+ onClick: onClose,
27569
+ children: "取消"
27570
+ }),
27571
+ /*#__PURE__*/ jsx(button_Button, {
27572
+ tone: "primary",
27573
+ onClick: apply,
27574
+ children: "确定"
27575
+ })
27576
+ ]
27577
+ }),
27578
+ children: [
27579
+ /*#__PURE__*/ jsxs("div", {
27580
+ className: "work-spreadsheet-sort-custom-list-manager-layout",
27581
+ children: [
27582
+ /*#__PURE__*/ jsxs("section", {
27583
+ "aria-label": "可用序列",
27584
+ children: [
27585
+ /*#__PURE__*/ jsxs("label", {
27586
+ children: [
27587
+ /*#__PURE__*/ jsx("span", {
27588
+ children: "自定义序列"
27589
+ }),
27590
+ /*#__PURE__*/ jsxs("select", {
27591
+ ref: listRef,
27592
+ "aria-label": "自定义序列列表",
27593
+ size: 10,
27594
+ value: selection,
27595
+ onChange: (event)=>select(event.currentTarget.value),
27596
+ children: [
27597
+ /*#__PURE__*/ jsx("optgroup", {
27598
+ label: "内置序列",
27599
+ children: builtInLists.map((list, index)=>/*#__PURE__*/ jsx("option", {
27600
+ value: `built-in:${index}`,
27601
+ children: list.label
27602
+ }, `built-in:${index}`))
27603
+ }),
27604
+ userLists.length ? /*#__PURE__*/ jsx("optgroup", {
27605
+ label: "用户序列",
27606
+ children: userLists.map((list)=>/*#__PURE__*/ jsx("option", {
27607
+ value: `user:${list.id}`,
27608
+ children: userListLabels.get(list.id)
27609
+ }, list.id))
27610
+ }) : null
27611
+ ]
27612
+ })
27613
+ ]
27614
+ }),
27615
+ /*#__PURE__*/ jsxs(button_Button, {
27616
+ tone: "quiet",
27617
+ type: "button",
27618
+ disabled: userLists.length >= 32,
27619
+ onClick: ()=>select('new'),
27620
+ children: [
27621
+ /*#__PURE__*/ jsx(ListPlus, {
27622
+ size: 15,
27623
+ "aria-hidden": "true"
27624
+ }),
27625
+ "新建序列"
27626
+ ]
27627
+ })
27628
+ ]
27629
+ }),
27630
+ /*#__PURE__*/ jsxs("section", {
27631
+ "aria-label": "序列项目",
27632
+ children: [
27633
+ /*#__PURE__*/ jsxs("label", {
27634
+ children: [
27635
+ /*#__PURE__*/ jsx("span", {
27636
+ children: "序列项目(每行一个项目)"
27637
+ }),
27638
+ /*#__PURE__*/ jsx("textarea", {
27639
+ "aria-label": "自定义序列项目",
27640
+ "aria-invalid": error ? true : void 0,
27641
+ readOnly: Boolean(selectedBuiltIn),
27642
+ rows: 12,
27643
+ value: text,
27644
+ onChange: (event)=>{
27645
+ setText(event.currentTarget.value);
27646
+ setError(null);
27647
+ }
27648
+ })
27649
+ ]
27650
+ }),
27651
+ /*#__PURE__*/ jsx("p", {
27652
+ children: selectedBuiltIn ? '内置序列不可修改或删除。' : editingNewList ? '每行、英文逗号或中文逗号可分隔一个项目。' : '修改项目后保存;排序和删除在点击“确定”后统一提交。'
27653
+ }),
27654
+ error ? /*#__PURE__*/ jsx("p", {
27655
+ className: "work-spreadsheet-sort-custom-list-error",
27656
+ role: "alert",
27657
+ children: error
27658
+ }) : null,
27659
+ /*#__PURE__*/ jsxs("div", {
27660
+ className: "work-spreadsheet-sort-custom-list-manager-actions",
27661
+ children: [
27662
+ editingNewList ? /*#__PURE__*/ jsxs(button_Button, {
27663
+ tone: "primary",
27664
+ type: "button",
27665
+ onClick: addNewList,
27666
+ children: [
27667
+ /*#__PURE__*/ jsx(ListPlus, {
27668
+ size: 15,
27669
+ "aria-hidden": "true"
27670
+ }),
27671
+ "添加序列"
27672
+ ]
27673
+ }) : selectedUser ? /*#__PURE__*/ jsxs(button_Button, {
27674
+ tone: "primary",
27675
+ type: "button",
27676
+ onClick: saveSelectedUser,
27677
+ children: [
27678
+ /*#__PURE__*/ jsx(Save, {
27679
+ size: 15,
27680
+ "aria-hidden": "true"
27681
+ }),
27682
+ "保存更改"
27683
+ ]
27684
+ }) : null,
27685
+ /*#__PURE__*/ jsx(button_Button, {
27686
+ tone: "quiet",
27687
+ type: "button",
27688
+ "aria-label": "上移序列",
27689
+ title: "上移序列",
27690
+ disabled: selectedUserIndex <= 0,
27691
+ onClick: ()=>moveSelectedUser(-1),
27692
+ children: /*#__PURE__*/ jsx(ArrowUp, {
27693
+ size: 15,
27694
+ "aria-hidden": "true"
27695
+ })
27696
+ }),
27697
+ /*#__PURE__*/ jsx(button_Button, {
27698
+ tone: "quiet",
27699
+ type: "button",
27700
+ "aria-label": "下移序列",
27701
+ title: "下移序列",
27702
+ disabled: selectedUserIndex < 0 || selectedUserIndex >= userLists.length - 1,
27703
+ onClick: ()=>moveSelectedUser(1),
27704
+ children: /*#__PURE__*/ jsx(ArrowDown, {
27705
+ size: 15,
27706
+ "aria-hidden": "true"
27707
+ })
27708
+ }),
27709
+ /*#__PURE__*/ jsx(button_Button, {
27710
+ tone: "quiet",
27711
+ type: "button",
27712
+ "aria-label": "删除序列",
27713
+ title: "删除序列",
27714
+ disabled: !selectedUser,
27715
+ onClick: deleteSelectedUser,
27716
+ children: /*#__PURE__*/ jsx(Trash2, {
27717
+ size: 15,
27718
+ "aria-hidden": "true"
27719
+ })
27720
+ })
27721
+ ]
27722
+ })
27723
+ ]
27724
+ })
27725
+ ]
27726
+ }),
27727
+ /*#__PURE__*/ jsxs("p", {
27728
+ className: "work-spreadsheet-sort-custom-list-manager-note",
27729
+ children: [
27730
+ "最多保存 ",
27731
+ 32,
27732
+ ' ',
27733
+ "个用户序列。修改或删除当前使用的序列会同步更新对应排序条件。"
27734
+ ]
27735
+ })
27736
+ ]
27737
+ });
27738
+ }
27739
+ function managedCustomListResult(initial, current) {
27740
+ const changes = [];
27741
+ for (const original of initial){
27742
+ if (!original.originalEntries) continue;
27743
+ const next = current.find((row)=>row.id === original.id);
27744
+ if (next) {
27745
+ if (!spreadsheetSortCustomListsEqual(original.originalEntries, next.entries)) changes.push({
27746
+ previous: original.originalEntries,
27747
+ next: Object.freeze([
27748
+ ...next.entries
27749
+ ])
27750
+ });
27751
+ } else changes.push({
27752
+ previous: original.originalEntries,
27753
+ next: null
27754
+ });
27755
+ }
27756
+ return Object.freeze({
27757
+ lists: Object.freeze(current.map((row)=>Object.freeze([
27758
+ ...row.entries
27759
+ ]))),
27760
+ changes: Object.freeze(changes)
27761
+ });
27762
+ }
27763
+ function managedUserListIndex(selection, lists) {
27764
+ if (!selection.startsWith('user:')) return -1;
27765
+ const id = Number(selection.slice(5));
27766
+ return Number.isSafeInteger(id) ? lists.findIndex((list)=>list.id === id) : -1;
27767
+ }
27768
+ function managedBuiltInList(selection, lists) {
27769
+ if (!selection.startsWith('built-in:')) return null;
27770
+ const index = Number(selection.slice(9));
27771
+ return Number.isSafeInteger(index) ? lists[index] ?? null : null;
27772
+ }
27773
+ function managedSelectionText(selection, builtInLists, userLists) {
27774
+ return (managedBuiltInList(selection, builtInLists)?.entries ?? userLists[managedUserListIndex(selection, userLists)]?.entries ?? []).join('\n');
27775
+ }
27776
+ function cloneManagedCustomList(list) {
27777
+ return {
27778
+ ...list,
27779
+ entries: Object.freeze([
27780
+ ...list.entries
27781
+ ]),
27782
+ originalEntries: list.originalEntries ? Object.freeze([
27783
+ ...list.originalEntries
27784
+ ]) : null
27785
+ };
27786
+ }
27787
+ function managedCustomListLabel(entries) {
27788
+ return createSpreadsheetSortCustomList(entries, 'session')?.label ?? entries.join(' → ');
27789
+ }
27411
27790
  function SpreadsheetSortOptionsDialog({ value, restoreFocusTarget, onApply, onClose }) {
27412
27791
  const [draft, setDraft] = useState(value);
27413
27792
  const formId = useId();
@@ -27866,7 +28245,7 @@ function spreadsheetSortPositionLabel(orientation, position) {
27866
28245
  function isSpreadsheetSortAppearanceKind(value) {
27867
28246
  return 'cell-color' === value || 'font-color' === value || 'icon' === value;
27868
28247
  }
27869
- function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRememberCustomList, onClose }) {
28248
+ function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRememberCustomList, onUpdateCustomLists, onClose }) {
27870
28249
  const [value, setValue] = useState(()=>({
27871
28250
  hasHeader: source.value.hasHeader,
27872
28251
  keys: source.value.keys.map(cloneSpreadsheetSortKey),
@@ -27876,6 +28255,7 @@ function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRemember
27876
28255
  }));
27877
28256
  const [verticalHasHeader, setVerticalHasHeader] = useState(source.value.hasHeader);
27878
28257
  const [optionsOpen, setOptionsOpen] = useState(false);
28258
+ const [customListManagerOpen, setCustomListManagerOpen] = useState(false);
27879
28259
  const [customLists, setCustomLists] = useState(()=>initialSpreadsheetSortCustomLists(source));
27880
28260
  const fields = 'top-to-bottom' === value.orientation ? source.columns : source.rows;
27881
28261
  const appearanceFields = useMemo(()=>spreadsheetSortAppearanceFields(source.appearanceRows, source.range, value.orientation, value.hasHeader), [
@@ -27887,6 +28267,7 @@ function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRemember
27887
28267
  const [customListDraft, setCustomListDraft] = useState(null);
27888
28268
  const formId = useId();
27889
28269
  const optionsButtonRef = useRef(null);
28270
+ const customListManagerButtonRef = useRef(null);
27890
28271
  const nextKey = value.keys.length < 64 ? nextSpreadsheetSortKey(value.keys, fields, appearanceFields) : null;
27891
28272
  const replaceKey = (index, replacement)=>{
27892
28273
  setValue((current)=>({
@@ -27985,6 +28366,18 @@ function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRemember
27985
28366
  ]
27986
28367
  });
27987
28368
  };
28369
+ const applyCustomListManagement = (result)=>{
28370
+ const currentUserLists = customLists.filter((list)=>'built-in' !== list.source);
28371
+ const preferencesChanged = currentUserLists.length !== result.lists.length || result.lists.some((entries, index)=>!spreadsheetSortCustomListsEqual(currentUserLists[index]?.entries ?? [], entries));
28372
+ const fallback = result.lists.map((entries)=>createSpreadsheetSortCustomList(entries, 'session')).filter((list)=>null !== list);
28373
+ const updated = preferencesChanged ? onUpdateCustomLists?.(result.lists) ?? fallback : currentUserLists;
28374
+ setCustomLists(mergeSpreadsheetSortCustomLists(updated));
28375
+ setCustomListDraft(null);
28376
+ setValue((current)=>({
28377
+ ...current,
28378
+ keys: current.keys.map((key)=>reconcileManagedCustomListKey(key, result.changes))
28379
+ }));
28380
+ };
27988
28381
  return /*#__PURE__*/ jsxs(Fragment, {
27989
28382
  children: [
27990
28383
  /*#__PURE__*/ jsx(Dialog, {
@@ -28015,40 +28408,60 @@ function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRemember
28015
28408
  /*#__PURE__*/ jsxs("div", {
28016
28409
  className: "work-spreadsheet-sort-toolbar",
28017
28410
  children: [
28018
- /*#__PURE__*/ jsxs(button_Button, {
28019
- tone: "quiet",
28020
- type: "button",
28021
- disabled: !nextKey,
28022
- onClick: ()=>{
28023
- if (!nextKey) return;
28024
- setCustomListDraft(null);
28025
- setValue((current)=>({
28026
- ...current,
28027
- keys: [
28028
- ...current.keys,
28029
- cloneSpreadsheetSortKey(nextKey)
28030
- ]
28031
- }));
28032
- },
28411
+ /*#__PURE__*/ jsxs("div", {
28412
+ className: "work-spreadsheet-sort-toolbar-actions",
28033
28413
  children: [
28034
- /*#__PURE__*/ jsx(ListPlus, {
28035
- size: 15,
28036
- "aria-hidden": "true"
28414
+ /*#__PURE__*/ jsxs(button_Button, {
28415
+ tone: "quiet",
28416
+ type: "button",
28417
+ disabled: !nextKey,
28418
+ onClick: ()=>{
28419
+ if (!nextKey) return;
28420
+ setCustomListDraft(null);
28421
+ setValue((current)=>({
28422
+ ...current,
28423
+ keys: [
28424
+ ...current.keys,
28425
+ cloneSpreadsheetSortKey(nextKey)
28426
+ ]
28427
+ }));
28428
+ },
28429
+ children: [
28430
+ /*#__PURE__*/ jsx(ListPlus, {
28431
+ size: 15,
28432
+ "aria-hidden": "true"
28433
+ }),
28434
+ "添加条件"
28435
+ ]
28037
28436
  }),
28038
- "添加条件"
28039
- ]
28040
- }),
28041
- /*#__PURE__*/ jsxs(button_Button, {
28042
- ref: optionsButtonRef,
28043
- tone: "quiet",
28044
- type: "button",
28045
- onClick: ()=>setOptionsOpen(true),
28046
- children: [
28047
- /*#__PURE__*/ jsx(Settings2, {
28048
- size: 15,
28049
- "aria-hidden": "true"
28437
+ /*#__PURE__*/ jsxs(button_Button, {
28438
+ ref: optionsButtonRef,
28439
+ tone: "quiet",
28440
+ type: "button",
28441
+ onClick: ()=>setOptionsOpen(true),
28442
+ children: [
28443
+ /*#__PURE__*/ jsx(Settings2, {
28444
+ size: 15,
28445
+ "aria-hidden": "true"
28446
+ }),
28447
+ "选项…"
28448
+ ]
28050
28449
  }),
28051
- "选项…"
28450
+ /*#__PURE__*/ jsxs(button_Button, {
28451
+ ref: customListManagerButtonRef,
28452
+ tone: "quiet",
28453
+ type: "button",
28454
+ "aria-label": "管理自定义序列",
28455
+ title: "管理自定义序列",
28456
+ onClick: ()=>setCustomListManagerOpen(true),
28457
+ children: [
28458
+ /*#__PURE__*/ jsx(ListOrdered, {
28459
+ size: 15,
28460
+ "aria-hidden": "true"
28461
+ }),
28462
+ "自定义序列…"
28463
+ ]
28464
+ })
28052
28465
  ]
28053
28466
  }),
28054
28467
  /*#__PURE__*/ jsx(OfficeCheckbox, {
@@ -28202,10 +28615,30 @@ function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRemember
28202
28615
  restoreFocusTarget: ()=>optionsButtonRef.current,
28203
28616
  onApply: applyOptions,
28204
28617
  onClose: ()=>setOptionsOpen(false)
28618
+ }) : null,
28619
+ customListManagerOpen ? /*#__PURE__*/ jsx(SpreadsheetSortCustomListManagerDialog, {
28620
+ customLists: customLists,
28621
+ restoreFocusTarget: ()=>customListManagerButtonRef.current,
28622
+ onApply: applyCustomListManagement,
28623
+ onClose: ()=>setCustomListManagerOpen(false)
28205
28624
  }) : null
28206
28625
  ]
28207
28626
  });
28208
28627
  }
28628
+ function reconcileManagedCustomListKey(key, changes) {
28629
+ if (void 0 === key.customList) return key;
28630
+ const change = changes.find((candidate)=>spreadsheetSortCustomListsEqual(candidate.previous, key.customList ?? []));
28631
+ if (!change) return key;
28632
+ return change.next ? {
28633
+ index: key.index,
28634
+ customList: [
28635
+ ...change.next
28636
+ ]
28637
+ } : {
28638
+ index: key.index,
28639
+ direction: 'ascending'
28640
+ };
28641
+ }
28209
28642
  function cloneSpreadsheetSortKey(key) {
28210
28643
  if ('cell-color' === key.sortOn || 'font-color' === key.sortOn) return {
28211
28644
  ...key
@@ -28395,6 +28828,22 @@ function useSpreadsheetSort({ commandsRef, contentRef, focusGrid, getGridFocusTa
28395
28828
  }, [
28396
28829
  customListStore
28397
28830
  ]);
28831
+ const updateCustomLists = useCallback((lists)=>{
28832
+ const normalized = normalizeStoredSpreadsheetSortCustomLists(lists);
28833
+ let source = customListStore ? 'stored' : 'session';
28834
+ if (customListStore) try {
28835
+ customListStore.save(normalized);
28836
+ } catch {
28837
+ source = 'session';
28838
+ showToast('无法写入本地自定义序列;本次更改仅在当前会话中保留。', 'error');
28839
+ }
28840
+ const next = Object.freeze(normalized.map((entries)=>createSpreadsheetSortCustomList(entries, source)).filter((list)=>null !== list));
28841
+ customListsRef.current = next;
28842
+ setCustomLists(next);
28843
+ return next;
28844
+ }, [
28845
+ customListStore
28846
+ ]);
28398
28847
  const sourceForCandidate = useCallback((request, candidate)=>{
28399
28848
  if (!candidate.available) return null;
28400
28849
  const sheet = contentRef.current.sheets.find((item)=>item.id === request.sheetId);
@@ -28421,28 +28870,13 @@ function useSpreadsheetSort({ commandsRef, contentRef, focusGrid, getGridFocusTa
28421
28870
  const list = createSpreadsheetSortCustomList(candidate.entries, source);
28422
28871
  if (!list) return candidate;
28423
28872
  if (customListsRef.current.length >= 32) return candidate;
28424
- const next = Object.freeze([
28425
- ...customListsRef.current,
28426
- list
28873
+ const next = updateCustomLists([
28874
+ ...customListsRef.current.map((item)=>item.entries),
28875
+ list.entries
28427
28876
  ]);
28428
- if (customListStore) try {
28429
- customListStore.save(next.filter((item)=>'stored' === item.source).map((item)=>item.entries));
28430
- } catch {
28431
- showToast('无法写入本地自定义序列;该序列仅在本次会话中保留。', 'error');
28432
- const session = createSpreadsheetSortCustomList(candidate.entries, 'session') ?? candidate;
28433
- const fallback = Object.freeze([
28434
- ...customListsRef.current,
28435
- session
28436
- ]);
28437
- customListsRef.current = fallback;
28438
- setCustomLists(fallback);
28439
- return session;
28440
- }
28441
- customListsRef.current = next;
28442
- setCustomLists(next);
28443
- return list;
28877
+ return next.find((item)=>spreadsheetSortCustomListsEqual(item.entries, list.entries)) ?? list;
28444
28878
  }, [
28445
- customListStore
28879
+ updateCustomLists
28446
28880
  ]);
28447
28881
  const applyAuthorizedRequest = useCallback((request, selectedRange)=>{
28448
28882
  authorizedRequestRef.current = {
@@ -28594,6 +29028,7 @@ function useSpreadsheetSort({ commandsRef, contentRef, focusGrid, getGridFocusTa
28594
29028
  restoreFocusTarget: restoreFocusTarget,
28595
29029
  onApply: apply,
28596
29030
  onRememberCustomList: rememberCustomList,
29031
+ onUpdateCustomLists: updateCustomLists,
28597
29032
  onClose: close
28598
29033
  }) : surface?.kind === 'range' ? /*#__PURE__*/ jsx(SpreadsheetSortRangeDialog, {
28599
29034
  source: surface.source,
@@ -0,0 +1,15 @@
1
+ import { type SpreadsheetSortCustomList } from './spreadsheet-sort-custom-list';
2
+ export interface SpreadsheetSortCustomListManagementChange {
3
+ next: readonly string[] | null;
4
+ previous: readonly string[];
5
+ }
6
+ export interface SpreadsheetSortCustomListManagementResult {
7
+ changes: readonly SpreadsheetSortCustomListManagementChange[];
8
+ lists: readonly (readonly string[])[];
9
+ }
10
+ export declare function SpreadsheetSortCustomListManagerDialog({ customLists, restoreFocusTarget, onApply, onClose, }: {
11
+ customLists: readonly SpreadsheetSortCustomList[];
12
+ restoreFocusTarget: () => HTMLElement | null;
13
+ onApply: (value: SpreadsheetSortCustomListManagementResult) => void;
14
+ onClose: () => void;
15
+ }): import("react").JSX.Element;
@@ -1,9 +1,10 @@
1
1
  import { type SpreadsheetSortDialogSource, type SpreadsheetSortDialogValue } from './spreadsheet-sort';
2
2
  import { type SpreadsheetSortCustomList } from './spreadsheet-sort-custom-list';
3
- export declare function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRememberCustomList, onClose, }: {
3
+ export declare function SpreadsheetSortDialog({ source, restoreFocusTarget, onApply, onRememberCustomList, onUpdateCustomLists, onClose, }: {
4
4
  source: SpreadsheetSortDialogSource;
5
5
  restoreFocusTarget: () => HTMLElement | null;
6
6
  onApply: (value: SpreadsheetSortDialogValue) => boolean;
7
- onRememberCustomList?: (list: SpreadsheetSortCustomList) => SpreadsheetSortCustomList | void;
7
+ onRememberCustomList?: (list: SpreadsheetSortCustomList) => SpreadsheetSortCustomList | undefined;
8
+ onUpdateCustomLists?: (lists: readonly (readonly string[])[]) => readonly SpreadsheetSortCustomList[] | undefined;
8
9
  onClose: () => void;
9
10
  }): import("react").JSX.Element;
Binary file
package/dist/styles.css CHANGED
@@ -18122,6 +18122,13 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
18122
18122
  gap: 6px;
18123
18123
  }
18124
18124
 
18125
+ .work-spreadsheet-sort-toolbar-actions {
18126
+ align-items: center;
18127
+ gap: 6px;
18128
+ min-width: 0;
18129
+ display: flex;
18130
+ }
18131
+
18125
18132
  .work-spreadsheet-sort-toolbar .work-office-checkbox {
18126
18133
  min-height: 34px;
18127
18134
  padding: 6px 9px;
@@ -18314,6 +18321,96 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
18314
18321
  line-height: 1.45;
18315
18322
  }
18316
18323
 
18324
+ .work-spreadsheet-sort-custom-list-manager {
18325
+ --work-office-control-accent: var(--work-spreadsheet-accent, #159469);
18326
+ width: min(720px, 100%);
18327
+ }
18328
+
18329
+ .work-spreadsheet-sort-custom-list-manager-layout {
18330
+ grid-template-columns: minmax(220px, .85fr) minmax(280px, 1.15fr);
18331
+ gap: 14px;
18332
+ display: grid;
18333
+ }
18334
+
18335
+ .work-spreadsheet-sort-custom-list-manager-layout > section, .work-spreadsheet-sort-custom-list-manager-layout label {
18336
+ gap: 7px;
18337
+ min-width: 0;
18338
+ display: grid;
18339
+ }
18340
+
18341
+ .work-spreadsheet-sort-custom-list-manager-layout > section {
18342
+ align-content: start;
18343
+ }
18344
+
18345
+ .work-spreadsheet-sort-custom-list-manager-layout label > span {
18346
+ color: var(--a3s-muted);
18347
+ font-size: 10px;
18348
+ font-weight: 650;
18349
+ }
18350
+
18351
+ .work-spreadsheet-sort-custom-list-manager select, .work-spreadsheet-sort-custom-list-manager textarea {
18352
+ box-sizing: border-box;
18353
+ border: 1px solid var(--a3s-line-strong);
18354
+ background: var(--a3s-panel);
18355
+ width: 100%;
18356
+ min-width: 0;
18357
+ min-height: 252px;
18358
+ color: var(--a3s-ink);
18359
+ font: inherit;
18360
+ border-radius: 6px;
18361
+ padding: 8px;
18362
+ font-size: 11px;
18363
+ line-height: 1.5;
18364
+ }
18365
+
18366
+ .work-spreadsheet-sort-custom-list-manager textarea {
18367
+ resize: vertical;
18368
+ }
18369
+
18370
+ .work-spreadsheet-sort-custom-list-manager textarea[readonly] {
18371
+ background: var(--a3s-panel-soft);
18372
+ color: var(--a3s-muted);
18373
+ }
18374
+
18375
+ .work-spreadsheet-sort-custom-list-manager select:focus-visible, .work-spreadsheet-sort-custom-list-manager textarea:focus-visible {
18376
+ border-color: var(--work-spreadsheet-accent, #159469);
18377
+ outline: 2px solid
18378
+ color-mix(in srgb, var(--work-spreadsheet-accent, #159469) 24%, transparent);
18379
+ outline-offset: 1px;
18380
+ }
18381
+
18382
+ .work-spreadsheet-sort-custom-list-manager-layout section > p, .work-spreadsheet-sort-custom-list-manager-note {
18383
+ color: var(--a3s-muted);
18384
+ margin: 0;
18385
+ font-size: 10px;
18386
+ line-height: 1.45;
18387
+ }
18388
+
18389
+ .work-spreadsheet-sort-custom-list-manager-layout section > .work-spreadsheet-sort-custom-list-error {
18390
+ color: var(--a3s-danger, #b42318);
18391
+ }
18392
+
18393
+ .work-spreadsheet-sort-custom-list-manager-actions {
18394
+ justify-content: flex-end;
18395
+ align-items: center;
18396
+ gap: 6px;
18397
+ display: flex;
18398
+ }
18399
+
18400
+ .work-spreadsheet-sort-custom-list-manager-actions .ds-button {
18401
+ gap: 6px;
18402
+ }
18403
+
18404
+ .work-spreadsheet-sort-custom-list-manager-actions .ds-button[aria-label] {
18405
+ width: 34px;
18406
+ min-width: 34px;
18407
+ padding-inline: 0;
18408
+ }
18409
+
18410
+ .work-spreadsheet-sort-custom-list-manager-note {
18411
+ margin-block-start: 12px;
18412
+ }
18413
+
18317
18414
  .work-spreadsheet-sort-range-dialog {
18318
18415
  --work-office-control-accent: var(--work-spreadsheet-accent, #159469);
18319
18416
  width: min(500px, 100%);
@@ -18395,6 +18492,18 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
18395
18492
  align-items: stretch;
18396
18493
  }
18397
18494
 
18495
+ .work-spreadsheet-sort-toolbar-actions {
18496
+ flex-wrap: wrap;
18497
+ }
18498
+
18499
+ .work-spreadsheet-sort-custom-list-manager-layout {
18500
+ grid-template-columns: minmax(0, 1fr);
18501
+ }
18502
+
18503
+ .work-spreadsheet-sort-custom-list-manager select, .work-spreadsheet-sort-custom-list-manager textarea {
18504
+ min-height: 180px;
18505
+ }
18506
+
18398
18507
  .work-spreadsheet-sort-level {
18399
18508
  grid-template-columns: minmax(0, 1fr);
18400
18509
  }
@@ -1471,14 +1471,19 @@ first/last placement. The
1471
1471
  normalization, duplicate and size guards, seven immutable month/weekday lists,
1472
1472
  and rank matching; the validated request clones custom entries so command
1473
1473
  execution has no dependency on mutable UI state. The browser adapters own a
1474
- bounded user-list registry and never writes preferences into controlled
1475
- workbook content. Without a store it remains scoped to the mounted session.
1474
+ bounded user-list registry and never write preferences into controlled workbook
1475
+ content. A dedicated manager stages create, edit, delete, and reorder operations
1476
+ against stable list identities, then publishes one normalized preference set.
1477
+ Built-ins remain read-only, no-op confirmation skips the storage boundary,
1478
+ edited active keys receive the replacement sequence, and deleted active keys
1479
+ fall back to ascending value order. Without a store the registry remains scoped
1480
+ to the mounted session.
1476
1481
  The optional typed `SpreadsheetSortCustomListStore` synchronously loads and
1477
1482
  saves canonical lists through a host-selected local backend. The provided
1478
1483
  `LocalStorageSpreadsheetSortCustomListStore` accepts an injected Storage
1479
1484
  object, uses a versioned key, and validates, deduplicates, and caps reads at 32
1480
- lists. Corrupt reads fail closed; a rejected write visibly downgrades the new
1481
- list to session scope. A stored list precedes an identical request-carried
1485
+ lists. Corrupt reads fail closed; a rejected write visibly downgrades the
1486
+ complete changed preference set to session scope. A stored list precedes an identical request-carried
1482
1487
  session copy so persistence identity remains stable. The separate
1483
1488
  `spreadsheet-sort-appearance` model snapshots direct native styles plus
1484
1489
  calculated conditional-format output, preserves no-fill/automatic identities,
@@ -1497,9 +1502,9 @@ One accepted operation crosses the Fortune boundary as one range write and one
1497
1502
  controlled Undo record. Table, worksheet-AutoFilter, hyperlink-map,
1498
1503
  imported-formula-metadata, and border-sidecar intersections fail closed because
1499
1504
  those models still own coordinates outside the cell matrix. Moving large sorts,
1500
- large aggregate/rank scans, remaining advanced filter predicates, full custom-list preference
1501
- management, and structural sidecar reconciliation into the Worker/WASM kernel
1502
- remains Stage 3 work.
1505
+ large aggregate/rank scans, remaining advanced filter predicates, and
1506
+ structural sidecar reconciliation into the Worker/WASM kernel remains Stage 3
1507
+ work.
1503
1508
  Direct host focus of the native grid now enters the same bounded focus observer
1504
1509
  used by ribbon commands. If a controlled workbook update replaces the focused
1505
1510
  Fortune overlay, focus transfers to its connected replacement; deliberate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.37.2",
3
+ "version": "0.37.3",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",