@a3s-lab/office 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,13 +13,13 @@
13
13
 
14
14
  <p align="center">
15
15
  <a href="https://github.com/A3S-Lab/Office/actions/workflows/ci.yml"><img alt="CI status" src="https://img.shields.io/github/actions/workflow/status/A3S-Lab/Office/ci.yml?branch=main&amp;style=flat-square&amp;label=CI"></a>
16
- <a href="https://a3s-lab.github.io/Office/"><img alt="Open the live Playground" src="https://img.shields.io/badge/Live_Playground-open-2f6fed?style=flat-square"></a>
16
+ <a href="https://a3s-lab.github.io/Office/playground/"><img alt="Open the live Playground" src="https://img.shields.io/badge/Live_Playground-open-2f6fed?style=flat-square"></a>
17
17
  <a href="#project-status"><img alt="Project status: pre-1.0" src="https://img.shields.io/badge/status-pre--1.0-7a5bd6?style=flat-square"></a>
18
18
  <a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-159469?style=flat-square"></a>
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
- <a href="https://a3s-lab.github.io/Office/">Playground</a> ·
22
+ <a href="https://a3s-lab.github.io/Office/playground/">Playground</a> ·
23
23
  <a href="https://a3s-lab.github.io/Office/docs/">Documentation</a> ·
24
24
  <a href="#quick-start">Quick start</a> ·
25
25
  <a href="#editor-capability-comparison">Editors</a> ·
@@ -56,7 +56,7 @@ workflows, and a separate Rust automation plane.
56
56
 
57
57
  ## Latest on `main`
58
58
 
59
- The `0.34.0` release plus the current `main` branch expose these capabilities
59
+ The `0.35.0` release plus the current `main` branch expose these capabilities
60
60
  as normal user-facing Playground templates, with matching implementation
61
61
  detail in the documentation:
62
62
 
@@ -71,16 +71,17 @@ detail in the documentation:
71
71
  | Spreadsheet | Complete common Data Validation input, blank/dropdown, and Stop/Warning/Information error settings with native XLSX round trips | Playground **Latest capabilities → 数据验证** · [Spreadsheet reference](docs/latest/en/components/spreadsheet.mdx#data-validation) |
72
72
  | PDF | Insert, delete, rotate, reorder, extract, merge, and split pages through a dedicated Web Worker, with Blob-level Undo/Redo and independent binary reopen verification | Playground **Latest capabilities → 组织 PDF 页面** · [PDF reference](docs/latest/en/components/pdf.mdx#page-organization) |
73
73
 
74
- All eight entries are reachable from the first Playground viewport. The seven
75
- content templates also remain under **新建**; **组织 PDF 页面** opens the normal
76
- PDF file workflow because page organization must operate on host-provided source
77
- bytes. The highlighted strip keeps each capability discoverable without a deep
78
- documentation section or an `?e2e=` fixture.
74
+ All eight entries are reachable from the first Playground viewport. The
75
+ filterable capability gallery groups releases by editor, publishes the source
76
+ release on every card, and grows from one shared metadata list instead of a
77
+ fixed-width strip. The seven content templates also remain under **新建**;
78
+ **组织 PDF 页面** opens the normal PDF file workflow because page organization
79
+ must operate on host-provided source bytes.
79
80
 
80
81
  ## See it working
81
82
 
82
83
  The images below are committed visual-regression baselines from the real
83
- [Playground](https://a3s-lab.github.io/Office/), not conceptual mockups.
84
+ [Playground](https://a3s-lab.github.io/Office/playground/), not conceptual mockups.
84
85
 
85
86
  <p align="center">
86
87
  <a href="visual-tests/__snapshots__/linux/desktop-1280/document.png">
@@ -364,7 +365,7 @@ bun run playground
364
365
  ```
365
366
 
366
367
  Then open the local URL printed by the development server. For a zero-install
367
- tour, use the [live Playground](https://a3s-lab.github.io/Office/).
368
+ tour, use the [live Playground](https://a3s-lab.github.io/Office/playground/).
368
369
  Choose **体验格式修订**, open **审阅**, then **查看修订(2)** to inspect the
369
370
  independent Formatting and Paragraph Formatting cards and exercise accept or
370
371
  reject semantics.
@@ -1613,15 +1614,16 @@ committed visual contracts; see [CONTRIBUTING.md](CONTRIBUTING.md).
1613
1614
 
1614
1615
  ## Documentation
1615
1616
 
1616
- The published website keeps the interactive Playground at its root and builds
1617
- the searchable Rspress documentation center under `/docs/`. Simplified Chinese
1618
- is the stable default, English remains available from the language menu, and
1619
- the version menu switches between `latest` and frozen release documentation.
1620
- Search is scoped to the active language and version. Both surfaces use the
1621
- same deployment base, so preview, Pages, and fork deployments keep working
1622
- without hard-coded return URLs.
1617
+ The published website follows the A3S UI documentation model: the searchable
1618
+ Rspress documentation center owns the website root, while Playground is a
1619
+ primary top-navigation route under `/playground/`. Simplified Chinese is the
1620
+ stable default, English remains available from the language menu, and the
1621
+ version menu switches between `latest` and frozen release documentation.
1622
+ Search is scoped to the active language and version. Both surfaces derive
1623
+ their paths from one deployment base, so preview, Pages, and fork deployments
1624
+ keep working without hard-coded return URLs.
1623
1625
 
1624
- - [Live Playground](https://a3s-lab.github.io/Office/)
1626
+ - [Live Playground](https://a3s-lab.github.io/Office/playground/)
1625
1627
  - [Documentation center](https://a3s-lab.github.io/Office/docs/)
1626
1628
  - [A3S Office 0.34.0 documentation](https://a3s-lab.github.io/Office/docs/0.34.0/)
1627
1629
  - [A3S Office 0.33.0 documentation](https://a3s-lab.github.io/Office/docs/0.33.0/)
@@ -2425,6 +2425,9 @@ function PlaceholderButtons({ onAdd }) {
2425
2425
  });
2426
2426
  }
2427
2427
  const PRESENTATION_WORKSPACE_FOCUS_RETRY_FRAMES = 6;
2428
+ function presentationInitialEditingElementId(elements) {
2429
+ return elements.find((element)=>element.placeholder?.type === 'title' && presentationElementCanEditContent(element) && !element.text.trim() && !element.textRuns?.some((run)=>run.text.trim()))?.id ?? null;
2430
+ }
2428
2431
  function restorePresentationWorkspaceFocus(root, getState, focusOrigin = document.activeElement) {
2429
2432
  if (!root) return;
2430
2433
  const commandTrigger = focusOrigin;
@@ -4356,6 +4359,7 @@ function PresentationTextEditor({ autoFocus = false, element, onChange, onEditor
4356
4359
  const onSelectionChangeRef = useRef(onSelectionChange);
4357
4360
  const appliedSignatureRef = useRef(presentationTextElementSignature(element));
4358
4361
  const initialContentRef = useRef(presentationTextElementHtml(element));
4362
+ const initialFocusTargetRef = useRef("u" > typeof document && document.activeElement instanceof HTMLElement ? document.activeElement : null);
4359
4363
  elementRef.current = element;
4360
4364
  onChangeRef.current = onChange;
4361
4365
  onEditorChangeRef.current = onEditorChange;
@@ -4410,7 +4414,8 @@ function PresentationTextEditor({ autoFocus = false, element, onChange, onEditor
4410
4414
  useEffect(()=>{
4411
4415
  if (!autoFocus || !editor || editor.isDestroyed) return;
4412
4416
  const frame = window.requestAnimationFrame(()=>{
4413
- if (!editor.isDestroyed) editor.commands.focus('end');
4417
+ if (editor.isDestroyed || !presentationFocusOwnerIsUnchanged(initialFocusTargetRef.current)) return;
4418
+ editor.commands.focus('end');
4414
4419
  });
4415
4420
  return ()=>window.cancelAnimationFrame(frame);
4416
4421
  }, [
@@ -4438,6 +4443,12 @@ function PresentationTextEditor({ autoFocus = false, element, onChange, onEditor
4438
4443
  style: presentationTextBaseStyle(element)
4439
4444
  });
4440
4445
  }
4446
+ function presentationFocusOwnerIsUnchanged(initialFocusTarget) {
4447
+ if ("u" < typeof document) return true;
4448
+ const currentFocusTarget = document.activeElement;
4449
+ if (!currentFocusTarget || currentFocusTarget === document.body) return true;
4450
+ return !initialFocusTarget || currentFocusTarget === initialFocusTarget;
4451
+ }
4441
4452
  function createPresentationTextEditorExtensions(placeholder = '输入文字') {
4442
4453
  return [
4443
4454
  starter_kit.configure({
@@ -7881,9 +7892,12 @@ function usePresentationReviewCommands({ content, onChange, onClearSelection, on
7881
7892
  function activePresentationReviewInvoker() {
7882
7893
  return "u" > typeof document && document.activeElement instanceof HTMLElement ? document.activeElement : null;
7883
7894
  }
7884
- function usePresentationSelection(elements) {
7885
- const [selectedElementIds, setSelectedElementIds] = useState([]);
7886
- const [editingElementId, setEditingElementId] = useState(null);
7895
+ function usePresentationSelection(elements, initialEditingElementId = null) {
7896
+ const initialEditingElementExists = Boolean(initialEditingElementId && elements.some((element)=>element.id === initialEditingElementId));
7897
+ const [selectedElementIds, setSelectedElementIds] = useState(()=>initialEditingElementExists && initialEditingElementId ? selectionWithPendingIds(elements, [
7898
+ initialEditingElementId
7899
+ ]) : []);
7900
+ const [editingElementId, setEditingElementId] = useState(()=>initialEditingElementExists ? initialEditingElementId : null);
7887
7901
  const clear = useCallback(()=>{
7888
7902
  setSelectedElementIds([]);
7889
7903
  setEditingElementId(null);
@@ -8482,7 +8496,7 @@ function PresentationEditingSurface({ initialSlide, autoFocus = true, collaborat
8482
8496
  const selectedLayout = designContent.layouts?.find((layout)=>layout.id === selectedSlide?.layoutId) ?? designContent.layouts?.[0];
8483
8497
  const selectedMaster = designContent.masters?.find((master)=>master.id === selectedLayout?.masterId) ?? designContent.masters?.[0];
8484
8498
  const activeElements = 'layout' === designMode ? selectedLayout?.elements ?? [] : 'master' === designMode ? selectedMaster?.elements ?? [] : selectedSlide?.elements ?? [];
8485
- const selection = usePresentationSelection(activeElements);
8499
+ const selection = usePresentationSelection(activeElements, autoFocus ? presentationInitialEditingElementId(activeElements) : null);
8486
8500
  const presentationPresenceLocation = useMemo(()=>({
8487
8501
  kind: 'presentation',
8488
8502
  slideId: selectedSlide.id,
@@ -5315,6 +5315,70 @@ function spreadsheetCellFillRangeIsSafe(range) {
5315
5315
  function sameSpreadsheetCellRange(left, right) {
5316
5316
  return left.row[0] === right.row[0] && left.row[1] === right.row[1] && left.column[0] === right.column[0] && left.column[1] === right.column[1];
5317
5317
  }
5318
+ function canEditSpreadsheetSelection(context) {
5319
+ return Boolean(context.editable && context.workbook && context.targetSheetId);
5320
+ }
5321
+ function spreadsheetLiveCommandRange(context) {
5322
+ const selection = spreadsheetLiveCommandSelection(context);
5323
+ return spreadsheetSingleRange(selection ?? context.fallbackRange);
5324
+ }
5325
+ function spreadsheetLiveCommandSelection(context) {
5326
+ return spreadsheetLiveCommandSelections(context)?.at(-1);
5327
+ }
5328
+ function spreadsheetLiveCommandSelections(context) {
5329
+ const shadow = context.selectionRef?.current;
5330
+ if (shadow?.sheetId === context.targetSheetId) return [
5331
+ shadow.selection
5332
+ ];
5333
+ return context.workbook?.getSelection();
5334
+ }
5335
+ function acceptSpreadsheetSelectionChange(selectionRef, sheetId, selection) {
5336
+ const nextSelection = finiteSpreadsheetSelection(selection);
5337
+ const requested = selectionRef.requested;
5338
+ if (requested && (requested.sheetId !== sheetId || !sameSpreadsheetSelectionValue(requested.selection, nextSelection))) return null;
5339
+ const next = {
5340
+ sheetId,
5341
+ selection: nextSelection
5342
+ };
5343
+ selectionRef.current = next;
5344
+ return next;
5345
+ }
5346
+ function rememberSpreadsheetCommandSelection(context, selection) {
5347
+ if (!context.selectionRef || !context.targetSheetId) return;
5348
+ const next = {
5349
+ ...selection,
5350
+ row: [
5351
+ ...selection.row
5352
+ ],
5353
+ column: [
5354
+ ...selection.column
5355
+ ]
5356
+ };
5357
+ context.selectionRef.current = {
5358
+ sheetId: context.targetSheetId,
5359
+ selection: next
5360
+ };
5361
+ context.selectionRef.requested = {
5362
+ sheetId: context.targetSheetId,
5363
+ selection: {
5364
+ ...next,
5365
+ row: [
5366
+ ...next.row
5367
+ ],
5368
+ column: [
5369
+ ...next.column
5370
+ ]
5371
+ }
5372
+ };
5373
+ }
5374
+ function releaseSpreadsheetSelectionRequest(selectionRef) {
5375
+ selectionRef.requested = null;
5376
+ }
5377
+ function sameSpreadsheetSelectionValue(left, right) {
5378
+ const normalizedLeft = finiteSpreadsheetSelection(left);
5379
+ const normalizedRight = finiteSpreadsheetSelection(right);
5380
+ return normalizedLeft.row[0] === normalizedRight.row[0] && normalizedLeft.row[1] === normalizedRight.row[1] && normalizedLeft.column[0] === normalizedRight.column[0] && normalizedLeft.column[1] === normalizedRight.column[1] && normalizedLeft.row_focus === normalizedRight.row_focus && normalizedLeft.column_focus === normalizedRight.column_focus;
5381
+ }
5318
5382
  function createSpreadsheetCellFillExtension() {
5319
5383
  return createOfficeEditorExtension({
5320
5384
  name: 'spreadsheetCellFill',
@@ -5365,9 +5429,11 @@ function materializeSpreadsheetCellFillRows(sheet, plan) {
5365
5429
  }
5366
5430
  function spreadsheetSelectedCellFillPlan(context, direction) {
5367
5431
  if (!context.editable || !context.workbook || !context.targetSheetId || context.targetSheetId !== context.activeSheetId) return null;
5368
- const selections = context.workbook.getSelection();
5432
+ const selections = spreadsheetLiveCommandSelections(context);
5369
5433
  if (selections?.length !== 1) return null;
5370
- const plan = planSpreadsheetCellFill(selections[0], direction);
5434
+ const selection = selections[0];
5435
+ if (!selection) return null;
5436
+ const plan = planSpreadsheetCellFill(selection, direction);
5371
5437
  if (!plan) return null;
5372
5438
  const sheet = context.content.sheets.find((candidate)=>candidate.id === context.targetSheetId);
5373
5439
  return canApplySpreadsheetCellFill(sheet, plan) ? plan : null;
@@ -6323,13 +6389,6 @@ function applySelectedCellStyle(context, preset) {
6323
6389
  function liveSpreadsheetCellStyleRange(context) {
6324
6390
  return spreadsheetSingleRange(context.workbook?.getSelection()?.at(-1) ?? context.fallbackRange);
6325
6391
  }
6326
- function canEditSpreadsheetSelection(context) {
6327
- return Boolean(context.editable && context.workbook && context.targetSheetId);
6328
- }
6329
- function spreadsheetLiveCommandRange(context) {
6330
- const selection = context.workbook?.getSelection()?.at(-1);
6331
- return spreadsheetSingleRange(selection ?? context.fallbackRange);
6332
- }
6333
6392
  const MAX_SPREADSHEET_ROWS = 1048576;
6334
6393
  const spreadsheetPasteContentOptions = [
6335
6394
  {
@@ -8493,7 +8552,7 @@ function updateSpreadsheetSelection(context, storage, update) {
8493
8552
  if (!context.workbook || !context.targetSheetId) return false;
8494
8553
  const sheet = context.content.sheets.find((candidate)=>candidate.id === context.targetSheetId);
8495
8554
  if (!sheet) return false;
8496
- const liveSelection = context.workbook.getSelection()?.at(-1) ?? context.fallbackRange;
8555
+ const liveSelection = spreadsheetLiveCommandSelection(context) ?? context.fallbackRange;
8497
8556
  const rememberedFocus = storage.focus;
8498
8557
  const selection = rememberedFocus && spreadsheetSelectionContainsFocus(liveSelection, rememberedFocus) ? {
8499
8558
  ...liveSelection,
@@ -8507,6 +8566,7 @@ function updateSpreadsheetSelection(context, storage, update) {
8507
8566
  ], {
8508
8567
  id: context.targetSheetId
8509
8568
  });
8569
+ rememberSpreadsheetCommandSelection(context, next);
8510
8570
  storage.focus = {
8511
8571
  row: next.row_focus ?? next.row[1] ?? next.row[0] ?? 0,
8512
8572
  column: next.column_focus ?? next.column[1] ?? next.column[0] ?? 0
@@ -10010,6 +10070,16 @@ function pasteCells(context, values) {
10010
10070
  ], {
10011
10071
  id: context.targetSheetId
10012
10072
  });
10073
+ rememberSpreadsheetCommandSelection(context, {
10074
+ row: [
10075
+ ...range.row
10076
+ ],
10077
+ column: [
10078
+ ...range.column
10079
+ ],
10080
+ row_focus: range.row[0],
10081
+ column_focus: range.column[0]
10082
+ });
10013
10083
  } catch {}
10014
10084
  syncSpreadsheetFormulaBar(context, data[0][0]);
10015
10085
  return true;
@@ -25395,6 +25465,60 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
25395
25465
  const editorFocusOrigin = useOfficeEditorFocusOrigin();
25396
25466
  const workbookRef = useRef(null);
25397
25467
  const projectedWorkbookSheetsRef = useRef([]);
25468
+ useLayoutEffect(()=>{
25469
+ const container = spreadsheetCanvasRef.current;
25470
+ if (!container || preview) return;
25471
+ let pendingEditor = null;
25472
+ let pointerEnteredEditor = false;
25473
+ const moveEditorCaretToEnd = (editor)=>{
25474
+ if (document.activeElement !== editor || !editor.isConnected || !editor.textContent) return false;
25475
+ const selection = document.getSelection();
25476
+ if (!selection) return false;
25477
+ const range = document.createRange();
25478
+ range.selectNodeContents(editor);
25479
+ range.collapse(false);
25480
+ selection.removeAllRanges();
25481
+ selection.addRange(range);
25482
+ return true;
25483
+ };
25484
+ const handlePointerDown = (event)=>{
25485
+ pointerEnteredEditor = event.target instanceof Element && Boolean(event.target.closest('.luckysheet-cell-input'));
25486
+ };
25487
+ const handleFocusIn = (event)=>{
25488
+ const target = event.target;
25489
+ const editor = target instanceof Element ? target.closest('.luckysheet-cell-input') : null;
25490
+ if (!editor || pointerEnteredEditor) {
25491
+ pendingEditor = null;
25492
+ pointerEnteredEditor = false;
25493
+ return;
25494
+ }
25495
+ pendingEditor = editor;
25496
+ queueMicrotask(()=>{
25497
+ if (pendingEditor === editor && moveEditorCaretToEnd(editor)) pendingEditor = null;
25498
+ });
25499
+ };
25500
+ const observer = new MutationObserver(()=>{
25501
+ const editor = pendingEditor;
25502
+ if (!editor || document.activeElement !== editor) return;
25503
+ if (!editor.textContent) return;
25504
+ moveEditorCaretToEnd(editor);
25505
+ pendingEditor = null;
25506
+ });
25507
+ container.addEventListener('pointerdown', handlePointerDown, true);
25508
+ container.addEventListener('focusin', handleFocusIn, true);
25509
+ observer.observe(container, {
25510
+ characterData: true,
25511
+ childList: true,
25512
+ subtree: true
25513
+ });
25514
+ return ()=>{
25515
+ container.removeEventListener('pointerdown', handlePointerDown, true);
25516
+ container.removeEventListener('focusin', handleFocusIn, true);
25517
+ observer.disconnect();
25518
+ };
25519
+ }, [
25520
+ preview
25521
+ ]);
25398
25522
  const spreadsheetZoomRef = useRef(100);
25399
25523
  const [workbookInstance, setWorkbookInstance] = useState(null);
25400
25524
  const bindWorkbookInstance = useCallback((instance)=>{
@@ -25413,6 +25537,10 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
25413
25537
  const [panel, setPanel] = useState(null);
25414
25538
  const panelTriggerRef = useRef(null);
25415
25539
  const [selectionState, setSelectionState] = useState(null);
25540
+ const selectionStateRef = useRef({
25541
+ current: null,
25542
+ requested: null
25543
+ });
25416
25544
  const formatPainterSelectionHandlerRef = useRef((_sheetId, _selection)=>void 0);
25417
25545
  const [contextMenu, setContextMenu] = useState(null);
25418
25546
  const [formatCellsDialog, setFormatCellsDialog] = useState(null);
@@ -25588,15 +25716,16 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
25588
25716
  collaborationView?.activateSheet(id);
25589
25717
  if (previewRef.current) setPreviewActiveSheetId(id);
25590
25718
  else setNavigationActiveSheetId(id);
25719
+ selectionStateRef.current.current = null;
25720
+ releaseSpreadsheetSelectionRequest(selectionStateRef.current);
25591
25721
  setSelectionState(null);
25592
25722
  },
25593
25723
  afterSelectionChange: (sheetId, selection)=>{
25594
- collaborationView?.select(sheetId, selection);
25595
- setSelectionState({
25596
- sheetId,
25597
- selection
25598
- });
25599
- formatPainterSelectionHandlerRef.current(sheetId, selection);
25724
+ const next = acceptSpreadsheetSelectionChange(selectionStateRef.current, sheetId, selection);
25725
+ if (!next) return;
25726
+ collaborationView?.select(sheetId, next.selection);
25727
+ setSelectionState(next);
25728
+ formatPainterSelectionHandlerRef.current(sheetId, next.selection);
25600
25729
  },
25601
25730
  beforeUpdateCell: (row, column)=>{
25602
25731
  const sheet = contentRef.current.sheets.find((candidate)=>candidate.id === activeSheetIdRef.current);
@@ -25876,6 +26005,8 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
25876
26005
  if (!sheet) return false;
25877
26006
  activeSheetIdRef.current = sheetId;
25878
26007
  if (collaborationView && !collaborationView.activateSheet(sheetId)) return false;
26008
+ selectionStateRef.current.current = null;
26009
+ releaseSpreadsheetSelectionRequest(selectionStateRef.current);
25879
26010
  setSelectionState(null);
25880
26011
  if (previewRef.current) setPreviewActiveSheetId(sheetId);
25881
26012
  try {
@@ -25910,7 +26041,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
25910
26041
  targetRow: activeCell.row,
25911
26042
  targetColumn: activeCell.column
25912
26043
  });
25913
- setSelectionState({
26044
+ const nextSelection = {
25914
26045
  sheetId,
25915
26046
  selection: {
25916
26047
  row: [
@@ -25922,7 +26053,21 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
25922
26053
  row_focus: activeCell.row,
25923
26054
  column_focus: activeCell.column
25924
26055
  }
25925
- });
26056
+ };
26057
+ selectionStateRef.current.current = nextSelection;
26058
+ selectionStateRef.current.requested = {
26059
+ sheetId,
26060
+ selection: {
26061
+ ...nextSelection.selection,
26062
+ row: [
26063
+ ...nextSelection.selection.row
26064
+ ],
26065
+ column: [
26066
+ ...nextSelection.selection.column
26067
+ ]
26068
+ }
26069
+ };
26070
+ setSelectionState(nextSelection);
25926
26071
  } catch {
25927
26072
  return false;
25928
26073
  }
@@ -26043,6 +26188,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
26043
26188
  toggle: (attribute)=>richTextSelectionRef.current?.toggle(contentRef.current, acceptSpreadsheetCommandChange, attribute) ?? false
26044
26189
  },
26045
26190
  selection: selectionState,
26191
+ selectionRef: selectionStateRef.current,
26046
26192
  table: spreadsheetTable.commandPort,
26047
26193
  targetSheetGridSize,
26048
26194
  targetSheetId: toolbarSheetId,
@@ -26150,6 +26296,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
26150
26296
  if ('Escape' === event.key && isSpreadsheetCellEditingTarget(event.target)) requestAnimationFrame(()=>focusSpreadsheetGrid(spreadsheetCanvasRef.current));
26151
26297
  };
26152
26298
  const handleSpreadsheetKeyDownCapture = (event)=>{
26299
+ releaseSpreadsheetSelectionRequest(selectionStateRef.current);
26153
26300
  if ('Alt' === event.key && !event.repeat && event.target instanceof Element && event.target.closest('.fortune-sheet-overlay') && reserveAutoFilterAltKey()) {
26154
26301
  event.preventDefault();
26155
26302
  event.stopPropagation();
@@ -26158,6 +26305,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
26158
26305
  handleSpreadsheetEditingEscape(event);
26159
26306
  };
26160
26307
  const handleSpreadsheetPointerDownCapture = (event)=>{
26308
+ releaseSpreadsheetSelectionRequest(selectionStateRef.current);
26161
26309
  const controller = richTextSelectionRef.current;
26162
26310
  if (!controller) return;
26163
26311
  if (!isSpreadsheetRichTextFormatPointerTarget(event.target)) return void controller.clear();
@@ -1,4 +1,5 @@
1
1
  import type { PresentationEditorCommands } from './presentation-command-types';
2
+ import type { WorkSlideElement } from '../work-types';
2
3
  export interface PresentationObjectFocusState {
3
4
  editingElementId: string | null;
4
5
  selectedElementIds: readonly string[];
@@ -7,6 +8,7 @@ export interface PresentationWorkspaceFocusState extends PresentationObjectFocus
7
8
  selectedSlideId: string;
8
9
  viewMode: 'normal' | 'sorter';
9
10
  }
11
+ export declare function presentationInitialEditingElementId(elements: readonly WorkSlideElement[]): string | null;
10
12
  export declare function restorePresentationObjectFocus(container: HTMLElement | null, getState: () => PresentationObjectFocusState, focusOrigin?: Element | null): void;
11
13
  export declare function restorePresentationWorkspaceFocus(root: HTMLElement | null, getState: () => PresentationWorkspaceFocusState, focusOrigin?: Element | null): void;
12
14
  export declare function presentationWorkspaceFocusTarget(root: HTMLElement, state: PresentationWorkspaceFocusState): HTMLElement | undefined;
@@ -13,6 +13,13 @@ export interface PresentationTextEditorProps {
13
13
  onSelectionChange?: () => void;
14
14
  }
15
15
  export declare function PresentationTextEditor({ autoFocus, element, onChange, onEditorChange, onExitEditing, onSelectionChange, }: PresentationTextEditorProps): import("react").JSX.Element | null;
16
+ /**
17
+ * Keep delayed first-open focus from overriding a deliberate focus move that
18
+ * happened while the rich-text editor was being created. The document body is
19
+ * treated as an unfocused baseline so opening from a non-focusable surface can
20
+ * still enter the editor normally.
21
+ */
22
+ export declare function presentationFocusOwnerIsUnchanged(initialFocusTarget: HTMLElement | null): boolean;
16
23
  export declare function createPresentationTextEditorExtensions(placeholder?: string): Extensions;
17
24
  export declare function presentationTextElementHtml(element: WorkSlideElement): string;
18
25
  export declare function presentationTextValue(editor: Editor, element: WorkSlideElement): PresentationTextValue;
@@ -67,6 +67,15 @@ export interface SpreadsheetCommandSelection {
67
67
  sheetId: string;
68
68
  selection: Selection;
69
69
  }
70
+ /**
71
+ * Synchronous selection state shared by commands and the Fortune Sheet bridge.
72
+ * Fortune applies API calls through React state, so reading its imperative API
73
+ * twice in the same task can otherwise return the previous selection.
74
+ */
75
+ export interface SpreadsheetSelectionRef {
76
+ current: SpreadsheetCommandSelection | null;
77
+ requested: SpreadsheetCommandSelection | null;
78
+ }
70
79
  export type SpreadsheetStructureAxis = 'row' | 'column';
71
80
  export type SpreadsheetStructureInsertPosition = 'before' | 'after';
72
81
  export type SpreadsheetSortDirection = 'ascending' | 'descending';
@@ -238,6 +247,7 @@ export interface SpreadsheetCommandContext {
238
247
  onChange: (content: WorkSpreadsheetContent) => void;
239
248
  richTextFormat?: SpreadsheetRichTextFormatCommandPort | null;
240
249
  selection: SpreadsheetCommandSelection | null;
250
+ selectionRef?: SpreadsheetSelectionRef;
241
251
  table: SpreadsheetTableCommandPort;
242
252
  targetSheetGridSize?: SpreadsheetGridSize | null;
243
253
  targetSheetId: string;
@@ -1,3 +1,24 @@
1
- import type { SpreadsheetCommandContext, SpreadsheetCommandRange } from './spreadsheet-command-controller';
1
+ import type { SpreadsheetCommandContext, SpreadsheetCommandSelection, SpreadsheetCommandRange, SpreadsheetSelectionRef } from './spreadsheet-command-controller';
2
2
  export declare function canEditSpreadsheetSelection(context: SpreadsheetCommandContext): boolean;
3
3
  export declare function spreadsheetLiveCommandRange(context: SpreadsheetCommandContext): SpreadsheetCommandRange;
4
+ /**
5
+ * Returns the most recent selection known to the Office bridge. The ref is
6
+ * intentionally checked before Fortune's imperative API because that API is
7
+ * backed by React state and can lag behind a just-issued keyboard command.
8
+ */
9
+ export declare function spreadsheetLiveCommandSelection(context: SpreadsheetCommandContext): SpreadsheetCommandRange | undefined;
10
+ /**
11
+ * Returns all current selections while preserving the workbook's multi-range
12
+ * semantics. A synchronous command selection is represented as one range.
13
+ */
14
+ export declare function spreadsheetLiveCommandSelections(context: SpreadsheetCommandContext): SpreadsheetCommandRange[] | undefined;
15
+ /**
16
+ * Accept a Fortune selection callback unless it is older than a pending
17
+ * programmatic selection request. The returned value is the normalized state
18
+ * that should be published to React and collaboration consumers.
19
+ */
20
+ export declare function acceptSpreadsheetSelectionChange(selectionRef: SpreadsheetSelectionRef, sheetId: string, selection: SpreadsheetCommandRange): SpreadsheetCommandSelection | null;
21
+ export declare function rememberSpreadsheetCommandSelection(context: SpreadsheetCommandContext, selection: SpreadsheetCommandRange): void;
22
+ /** Clear a pending programmatic request when a fresh user intent arrives. */
23
+ export declare function releaseSpreadsheetSelectionRequest(selectionRef: SpreadsheetSelectionRef): void;
24
+ export declare function sameSpreadsheetSelectionValue(left: SpreadsheetCommandRange, right: SpreadsheetCommandRange): boolean;
@@ -8,4 +8,4 @@ export interface PresentationSelectionController {
8
8
  select: (elementId: string | null, additive?: boolean) => void;
9
9
  selectedElementIds: string[];
10
10
  }
11
- export declare function usePresentationSelection(elements: readonly WorkSlideElement[]): PresentationSelectionController;
11
+ export declare function usePresentationSelection(elements: readonly WorkSlideElement[], initialEditingElementId?: string | null): PresentationSelectionController;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",
@@ -72,8 +72,8 @@
72
72
  "build": "bun run kernel:build && rslib",
73
73
  "collaboration-server:typecheck": "tsc --noEmit -p examples/collaboration-server/tsconfig.json",
74
74
  "dev": "bun run kernel:build && rslib --watch",
75
- "docs:build": "rspress build -c website/rspress.config.ts && bun scripts/rewrite-docs-base.ts",
76
- "docs:dev": "rspress -c website/rspress.config.ts",
75
+ "docs:build": "bun scripts/build-docs.ts",
76
+ "docs:dev": "rspress -c website/rspress.docs.config.ts",
77
77
  "format": "biome format --write .",
78
78
  "format:check": "biome format .",
79
79
  "lint": "biome lint .",
@@ -82,9 +82,9 @@
82
82
  "kernel:test": "bun run kernel:build && bun scripts/smoke-office-kernel.ts",
83
83
  "playground": "bun run playground:assets && rsbuild dev --config playground/rsbuild.config.ts",
84
84
  "playground:assets": "bun run build && bash scripts/package-office-skill.sh",
85
- "playground:build": "bun run playground:assets && rsbuild build --config playground/rsbuild.config.ts && bun run docs:build",
85
+ "playground:build": "bun run playground:assets && bun run docs:build && rsbuild build --config playground/rsbuild.config.ts",
86
86
  "playground:bundle-check": "bun scripts/check-playground-bundle.ts",
87
- "playground:preview": "rsbuild preview --config playground/rsbuild.config.ts --host 127.0.0.1 --port 4175 --strict-port",
87
+ "playground:preview": "rspress preview -c website/rspress.config.ts --host 127.0.0.1 --port 4175",
88
88
  "playground:typecheck": "tsc --noEmit -p playground/tsconfig.json",
89
89
  "playground:visual": "playwright test --config playwright.config.ts",
90
90
  "playground:visual:document-emphasis": "playwright test visual-tests/document-emphasis.functional.spec.ts --config playwright.config.ts",
@@ -118,12 +118,12 @@
118
118
  "playground:visual:pdf-page-organization": "playwright test visual-tests/pdf-page-organization.functional.spec.ts --config playwright.config.ts",
119
119
  "playground:visual:update": "playwright test --config playwright.config.ts --update-snapshots",
120
120
  "performance:fixtures": "bun .a3s-test/performance/generate-fixtures.ts",
121
- "performance:large-document-edits": "bun .a3s-test/performance/generate-fixtures.ts --documents-only && bun .a3s-test/performance/benchmark-document-edit.ts http://127.0.0.1:4175/ 3",
122
- "performance:large-documents": "bun .a3s-test/performance/generate-fixtures.ts --documents-only && bun .a3s-test/performance/benchmark.ts --url http://127.0.0.1:4175/ --scenarios document-text,document-table --runs 3 --timeout-ms 180000",
123
- "performance:large-spreadsheet-edits": "bun .a3s-test/performance/generate-fixtures.ts --spreadsheets-only && bun .a3s-test/performance/benchmark-spreadsheet-edit.ts http://127.0.0.1:4175/ 3",
124
- "performance:large-spreadsheets": "bun .a3s-test/performance/generate-fixtures.ts --spreadsheets-only && bun .a3s-test/performance/benchmark.ts --url http://127.0.0.1:4175/ --scenarios spreadsheet --runs 5 --timeout-ms 180000",
125
- "performance:pdf": "bun .a3s-test/performance/generate-fixtures.ts --pdf-only && bun .a3s-test/performance/benchmark-pdf.ts --url http://127.0.0.1:4175/ --runs 3 --timeout-ms 120000",
126
- "performance:presentation": "bun .a3s-test/performance/generate-fixtures.ts --presentations-only && bun .a3s-test/performance/benchmark-presentation.ts --url http://127.0.0.1:4175/ --runs 3 --timeout-ms 120000 --modes default,content-visibility",
121
+ "performance:large-document-edits": "bun .a3s-test/performance/generate-fixtures.ts --documents-only && bun .a3s-test/performance/benchmark-document-edit.ts http://127.0.0.1:4175/playground/ 3",
122
+ "performance:large-documents": "bun .a3s-test/performance/generate-fixtures.ts --documents-only && bun .a3s-test/performance/benchmark.ts --url http://127.0.0.1:4175/playground/ --scenarios document-text,document-table --runs 3 --timeout-ms 180000",
123
+ "performance:large-spreadsheet-edits": "bun .a3s-test/performance/generate-fixtures.ts --spreadsheets-only && bun .a3s-test/performance/benchmark-spreadsheet-edit.ts http://127.0.0.1:4175/playground/ 3",
124
+ "performance:large-spreadsheets": "bun .a3s-test/performance/generate-fixtures.ts --spreadsheets-only && bun .a3s-test/performance/benchmark.ts --url http://127.0.0.1:4175/playground/ --scenarios spreadsheet --runs 5 --timeout-ms 180000",
125
+ "performance:pdf": "bun .a3s-test/performance/generate-fixtures.ts --pdf-only && bun .a3s-test/performance/benchmark-pdf.ts --url http://127.0.0.1:4175/playground/ --runs 3 --timeout-ms 120000",
126
+ "performance:presentation": "bun .a3s-test/performance/generate-fixtures.ts --presentations-only && bun .a3s-test/performance/benchmark-presentation.ts --url http://127.0.0.1:4175/playground/ --runs 3 --timeout-ms 120000 --modes default,content-visibility",
127
127
  "test": "rstest",
128
128
  "test:e2e": "bun run test:e2e:fixtures && a3s-test run tests/e2e/word-page-color.acl --json && a3s-test run tests/e2e/word-page-setup-phone.acl --json && a3s-test run tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test run tests/e2e/word-list-formatting-phone.acl --json && a3s-test run tests/e2e/word-font-picker-phone.acl --json && a3s-test run tests/e2e/word-comments-drawer.acl --json && a3s-test run tests/e2e/word-comment-windowing.acl --json && a3s-test run tests/e2e/word-citations-phone.acl --json && a3s-test run tests/e2e/word-navigation-search.acl --json && a3s-test run tests/e2e/word-navigation-windowing.acl --json && a3s-test run tests/e2e/word-find-replace-phone.acl --json && a3s-test run tests/e2e/word-page-navigation.acl --json && a3s-test run tests/e2e/word-page-windowing.acl --json && a3s-test run tests/e2e/word-ai-question.acl --json && a3s-test run tests/e2e/word-picture-insert.acl --json && a3s-test run tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test run tests/e2e/word-track-changes-phone.acl --json && a3s-test run tests/e2e/word-formatting-revision.acl --json && a3s-test run tests/e2e/word-paragraph-formatting-revision.acl --json && a3s-test run tests/e2e/word-document-comparison.acl --json && a3s-test run tests/e2e/word-review-conflict-phone.acl --json && a3s-test run tests/e2e/word-revision-windowing.acl --json && a3s-test run tests/e2e/word-table-phone.acl --json && a3s-test run tests/e2e/word-table-borders.acl --json && a3s-test run tests/e2e/word-theme-table.acl --json && a3s-test run tests/e2e/word-styled-table.acl --json && a3s-test run tests/e2e/word-multi-page-table.acl --json && a3s-test run tests/e2e/word-cross-reference-phone.acl --json && a3s-test run tests/e2e/word-bookmark-links-phone.acl --json && a3s-test run tests/e2e/word-notes-phone.acl --json && a3s-test run tests/e2e/word-fields-phone.acl --json && a3s-test run tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test run tests/e2e/spreadsheet-phone-find.acl --json && a3s-test run tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test run tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test run tests/e2e/spreadsheet-maximum-sparse.acl --json && a3s-test run tests/e2e/spreadsheet-cell-style.acl --json && a3s-test run tests/e2e/spreadsheet-paste-special.acl --json && a3s-test run tests/e2e/spreadsheet-hyperlink.acl --json && a3s-test run tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test run tests/e2e/presentation-presenter-view.acl --json && a3s-test run tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test run tests/e2e/presentation-phone-comments.acl --json && a3s-test run tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test run tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test run tests/e2e/markdown-phone-modes.acl --json && a3s-test run tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test run tests/e2e/office-docs-navigation.acl --json && a3s-test run tests/e2e/collaboration-playground-entry.acl --json && a3s-test run tests/e2e/collaboration-document-comments.acl --command-timeout-ms 120000 --json && a3s-test run tests/e2e/collaboration-document-suggestions.acl --command-timeout-ms 120000 --json && a3s-test run tests/e2e/collaboration-participants.acl --json && a3s-test run tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test run tests/e2e/collaboration-spreadsheet-cells.acl --json && a3s-test run tests/e2e/collaboration-presentation-elements.acl --json",
129
129
  "test:e2e:check": "bun run test:e2e:fixtures && a3s-test check tests/e2e/word-page-color.acl --json && a3s-test check tests/e2e/word-page-setup-phone.acl --json && a3s-test check tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test check tests/e2e/word-list-formatting-phone.acl --json && a3s-test check tests/e2e/word-font-picker-phone.acl --json && a3s-test check tests/e2e/word-comments-drawer.acl --json && a3s-test check tests/e2e/word-comment-windowing.acl --json && a3s-test check tests/e2e/word-citations-phone.acl --json && a3s-test check tests/e2e/word-navigation-search.acl --json && a3s-test check tests/e2e/word-navigation-windowing.acl --json && a3s-test check tests/e2e/word-find-replace-phone.acl --json && a3s-test check tests/e2e/word-page-navigation.acl --json && a3s-test check tests/e2e/word-page-windowing.acl --json && a3s-test check tests/e2e/word-ai-question.acl --json && a3s-test check tests/e2e/word-picture-insert.acl --json && a3s-test check tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test check tests/e2e/word-track-changes-phone.acl --json && a3s-test check tests/e2e/word-formatting-revision.acl --json && a3s-test check tests/e2e/word-paragraph-formatting-revision.acl --json && a3s-test check tests/e2e/word-document-comparison.acl --json && a3s-test check tests/e2e/word-review-conflict-phone.acl --json && a3s-test check tests/e2e/word-revision-windowing.acl --json && a3s-test check tests/e2e/word-table-phone.acl --json && a3s-test check tests/e2e/word-table-borders.acl --json && a3s-test check tests/e2e/word-theme-table.acl --json && a3s-test check tests/e2e/word-styled-table.acl --json && a3s-test check tests/e2e/word-multi-page-table.acl --json && a3s-test check tests/e2e/word-cross-reference-phone.acl --json && a3s-test check tests/e2e/word-bookmark-links-phone.acl --json && a3s-test check tests/e2e/word-notes-phone.acl --json && a3s-test check tests/e2e/word-fields-phone.acl --json && a3s-test check tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test check tests/e2e/spreadsheet-phone-find.acl --json && a3s-test check tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test check tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test check tests/e2e/spreadsheet-maximum-sparse.acl --json && a3s-test check tests/e2e/spreadsheet-cell-style.acl --json && a3s-test check tests/e2e/spreadsheet-paste-special.acl --json && a3s-test check tests/e2e/spreadsheet-hyperlink.acl --json && a3s-test check tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test check tests/e2e/presentation-presenter-view.acl --json && a3s-test check tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test check tests/e2e/presentation-phone-comments.acl --json && a3s-test check tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test check tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test check tests/e2e/markdown-phone-modes.acl --json && a3s-test check tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test check tests/e2e/office-docs-navigation.acl --json && a3s-test check tests/e2e/collaboration-playground-entry.acl --json && a3s-test check tests/e2e/collaboration-document-comments.acl --json && a3s-test check tests/e2e/collaboration-document-suggestions.acl --json && a3s-test check tests/e2e/collaboration-participants.acl --json && a3s-test check tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test check tests/e2e/collaboration-spreadsheet-cells.acl --json && a3s-test check tests/e2e/collaboration-presentation-elements.acl --json",
@@ -142,6 +142,8 @@
142
142
  "test:e2e:initial-focus:check": "a3s-test check tests/e2e/office-editor-initial-focus.acl --json",
143
143
  "test:e2e:docs": "a3s-test run tests/e2e/office-docs-navigation.acl --json",
144
144
  "test:e2e:docs:check": "a3s-test check tests/e2e/office-docs-navigation.acl --json",
145
+ "test:e2e:homepage": "A3S_TEST_SUITE=tests/e2e/homepage-editor-demo.acl bash scripts/run-a3s-test-web-gate.sh",
146
+ "test:e2e:homepage:check": "a3s-test check tests/e2e/homepage-editor-demo.acl --json",
145
147
  "test:e2e:latest-capabilities": "A3S_TEST_SUITE=tests/e2e/latest-capabilities-discoverability.acl bash scripts/run-a3s-test-web-gate.sh",
146
148
  "test:e2e:latest-capabilities:check": "a3s-test check tests/e2e/latest-capabilities-discoverability.acl --json",
147
149
  "test:e2e:collaboration-participants": "a3s-test run tests/e2e/collaboration-participants.acl --json",
@@ -307,15 +309,16 @@
307
309
  }
308
310
  },
309
311
  "devDependencies": {
312
+ "@a3s-lab/testkit": "0.6.2",
310
313
  "@biomejs/biome": "2.4.15",
311
314
  "@fontsource-variable/geist": "^5.3.0",
312
315
  "@fontsource-variable/geist-mono": "^5.3.0",
313
- "@rspress/core": "^2.0.17",
314
- "@rspress/shared": "2.0.19",
315
316
  "@playwright/test": "1.61.1",
316
317
  "@rsbuild/core": "^2.1.5",
317
318
  "@rsbuild/plugin-react": "^2.1.0",
318
319
  "@rslib/core": "^0.23.2",
320
+ "@rspress/core": "^2.0.17",
321
+ "@rspress/shared": "2.0.19",
319
322
  "@rstest/adapter-rslib": "^0.10.6",
320
323
  "@rstest/core": "^0.10.6",
321
324
  "@rstest/coverage-v8": "0.10.6",