@opengeni/react 0.48.0 → 0.57.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 +138 -2
- package/dist/artifacts-document.d.ts +3 -0
- package/dist/artifacts-document.js +17 -0
- package/dist/artifacts-document.js.map +1 -0
- package/dist/artifacts-presentation.d.ts +3 -0
- package/dist/artifacts-presentation.js +17 -0
- package/dist/artifacts-presentation.js.map +1 -0
- package/dist/artifacts-spreadsheet.d.ts +3 -0
- package/dist/artifacts-spreadsheet.js +17 -0
- package/dist/artifacts-spreadsheet.js.map +1 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +199 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
- package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
- package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/chunk-7WCLAEFV.js +2828 -0
- package/dist/chunk-7WCLAEFV.js.map +1 -0
- package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
- package/dist/chunk-IGC6A2ZI.js.map +1 -0
- package/dist/chunk-JVYQRRC4.js +53 -0
- package/dist/chunk-JVYQRRC4.js.map +1 -0
- package/dist/chunk-KCGXPP5V.js +157 -0
- package/dist/chunk-KCGXPP5V.js.map +1 -0
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +10 -2
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +7 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/types.d.ts +1 -1
- package/dist/components/approval-surface.d.ts +2 -2
- package/dist/components/artifacts/artifact-surface.d.ts +18 -0
- package/dist/components/artifacts/document-editor.d.ts +172 -0
- package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
- package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
- package/dist/components/artifacts/editable-document.d.ts +24 -0
- package/dist/components/artifacts/editable-presentation.d.ts +24 -0
- package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
- package/dist/components/artifacts/index.d.ts +9 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
- package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +30 -0
- package/dist/components/chat-composer.d.ts +8 -8
- package/dist/components/code-editor.d.ts +25 -3
- package/dist/components/command-palette.d.ts +2 -2
- package/dist/components/composer-transcription-control.d.ts +2 -2
- package/dist/components/composer.d.ts +17 -17
- package/dist/components/computer-viewer.d.ts +24 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +50 -22
- package/dist/components/diff-view.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +1 -1
- package/dist/components/enrollment-device-flow.d.ts +1 -1
- package/dist/components/file-browser.d.ts +2 -2
- package/dist/components/fleet-tile.d.ts +1 -1
- package/dist/components/generated-video-player.d.ts +10 -0
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +2 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/interactive-workbench-viewers.d.ts +2 -0
- package/dist/components/machine-card.d.ts +2 -2
- package/dist/components/machine-dock-bar.d.ts +3 -3
- package/dist/components/machine-health-pill.d.ts +2 -2
- package/dist/components/machine-input-display.d.ts +1 -1
- package/dist/components/machine-metrics.d.ts +2 -2
- package/dist/components/machine-status-pill.d.ts +4 -4
- package/dist/components/machines/health.d.ts +1 -1
- package/dist/components/machines/machine-detail.d.ts +3 -3
- package/dist/components/machines/metric-history-chart.d.ts +1 -1
- package/dist/components/machines/metric-sparkline.d.ts +2 -2
- package/dist/components/machines/series.d.ts +2 -2
- package/dist/components/machines-dashboard.d.ts +2 -2
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +12 -5
- package/dist/components/model-picker.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +7 -7
- package/dist/components/pierre-diff.d.ts +1 -1
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/queue-draft-policy.d.ts +1 -1
- package/dist/components/queue-surface-implementation.d.ts +3 -3
- package/dist/components/queue-surface-state.d.ts +4 -4
- package/dist/components/queue-surface.d.ts +5 -5
- package/dist/components/sandbox-files.d.ts +3 -3
- package/dist/components/sandbox-terminal.d.ts +24 -4
- package/dist/components/sandbox-workspace.d.ts +28 -9
- package/dist/components/session-chrome.d.ts +4 -4
- package/dist/components/session-status.d.ts +2 -2
- package/dist/components/timeline-annotation-selection.d.ts +9 -0
- package/dist/components/timeline-annotations-dialog.d.ts +16 -0
- package/dist/components/timeline-annotations.d.ts +13 -0
- package/dist/components/tooltip.d.ts +4 -4
- package/dist/components/user-message-body.d.ts +2 -2
- package/dist/components/workbench-changes.d.ts +2 -2
- package/dist/components/workspace-dock.d.ts +1 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +1 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -0
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-available-models.d.ts +2 -2
- package/dist/hooks/use-billing-usage.d.ts +1 -1
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +30 -0
- package/dist/hooks/use-browser-session.d.ts +29 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -0
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-codex-accounts.d.ts +2 -2
- package/dist/hooks/use-composer.d.ts +36 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
- package/dist/hooks/use-computer-session.d.ts +29 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +5 -1
- package/dist/hooks/use-environments.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +1 -1
- package/dist/hooks/use-goal.d.ts +2 -2
- package/dist/hooks/use-human-input.d.ts +2 -2
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
- package/dist/hooks/use-machine-chip.d.ts +2 -2
- package/dist/hooks/use-machines.d.ts +4 -4
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-packs.d.ts +7 -2
- package/dist/hooks/use-rigs.d.ts +1 -1
- package/dist/hooks/use-sandbox-files.d.ts +6 -4
- package/dist/hooks/use-sandbox-git.d.ts +6 -5
- package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
- package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
- package/dist/hooks/use-session-capabilities.d.ts +5 -4
- package/dist/hooks/use-session-control.d.ts +1 -1
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/hooks/use-session-lineage.d.ts +1 -1
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +2 -2
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-slash-commands.d.ts +1 -1
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +11 -2
- package/dist/hooks/use-turn-queue.d.ts +2 -2
- package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
- package/dist/hooks/use-voice-input.d.ts +1 -1
- package/dist/hooks/use-workspace-capture.d.ts +1 -1
- package/dist/hooks/use-workspace-edit.d.ts +1 -1
- package/dist/hooks/use-workspace-sessions.d.ts +1 -1
- package/dist/hooks/use-workspaces.d.ts +1 -1
- package/dist/index.d.ts +162 -155
- package/dist/index.js +1822 -1703
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +33 -0
- package/dist/interaction.js +550 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +24 -1
- package/dist/lib/sandbox-liveness.d.ts +9 -0
- package/dist/lib/terminal-capability.d.ts +5 -0
- package/dist/lib/xterm-theme.d.ts +1 -1
- package/dist/machines.d.ts +30 -30
- package/dist/machines.js +2 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +6 -5
- package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
- package/dist/realtime/dropdown-menu.d.ts +8 -8
- package/dist/realtime/realtime-control.d.ts +8 -8
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +38 -11
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +28 -2
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +5 -4
- package/dist/session.d.ts +28 -28
- package/dist/session.js +10 -10
- package/dist/timeline/activity-rail.d.ts +5 -3
- package/dist/timeline/disclosure-context.d.ts +1 -1
- package/dist/timeline/fleet-decision-projection.d.ts +1 -1
- package/dist/timeline/fleet-decision-row.d.ts +3 -3
- package/dist/timeline/index.d.ts +19 -19
- package/dist/timeline/parsers.d.ts +4 -1
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +8 -2
- package/dist/timeline/screenshot-lightbox.d.ts +2 -2
- package/dist/timeline/shared.d.ts +21 -10
- package/dist/timeline/tool-diff.d.ts +2 -2
- package/dist/timeline/tool-renderers.d.ts +1 -1
- package/dist/timeline/turn-summary.d.ts +3 -3
- package/dist/timeline/types.d.ts +44 -3
- package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
- package/package.json +36 -7
- package/src/artifacts-document.ts +31 -0
- package/src/artifacts-presentation.ts +36 -0
- package/src/artifacts-spreadsheet.ts +21 -0
- package/src/artifacts.ts +89 -0
- package/src/client.ts +157 -1
- package/src/clipboard.ts +2 -0
- package/src/components/artifacts/artifact-surface.tsx +93 -0
- package/src/components/artifacts/document-editor.tsx +2255 -0
- package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
- package/src/components/artifacts/editable-document.tsx +657 -0
- package/src/components/artifacts/editable-presentation.tsx +1020 -0
- package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
- package/src/components/artifacts/index.ts +104 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +2702 -0
- package/src/components/chat-composer.tsx +13 -0
- package/src/components/code-editor.tsx +89 -30
- package/src/components/composer-transcription-control.tsx +10 -6
- package/src/components/computer-viewer.tsx +1636 -0
- package/src/components/desktop-viewer.tsx +215 -74
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/message-timeline.tsx +289 -32
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/queue-surface-implementation.tsx +116 -60
- package/src/components/sandbox-files.tsx +2 -2
- package/src/components/sandbox-terminal.tsx +128 -46
- package/src/components/sandbox-workspace.tsx +256 -90
- package/src/components/session-chrome.tsx +53 -13
- package/src/components/timeline-annotation-selection.tsx +221 -0
- package/src/components/timeline-annotations-dialog.tsx +213 -0
- package/src/components/timeline-annotations.tsx +84 -0
- package/src/components/workbench-changes.tsx +188 -31
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +154 -0
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +425 -0
- package/src/hooks/use-browser-identities.ts +266 -0
- package/src/hooks/use-browser-session.ts +543 -0
- package/src/hooks/use-browser-sessions.ts +286 -0
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +434 -0
- package/src/hooks/use-computer-session.ts +462 -0
- package/src/hooks/use-computer-sessions.ts +251 -0
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +15 -3
- package/src/hooks/use-machines.ts +33 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +98 -55
- package/src/hooks/use-sandbox-git.ts +93 -38
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +59 -46
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +359 -47
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +18 -0
- package/src/interaction.ts +101 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +47 -4
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +34 -6
- package/src/session-context.ts +156 -1
- package/src/timeline/activity-rail.tsx +25 -3
- package/src/timeline/index.ts +3 -0
- package/src/timeline/parsers.ts +14 -1
- package/src/timeline/projection.ts +269 -14
- package/src/timeline/registry.ts +13 -1
- package/src/timeline/screenshot-lightbox.tsx +19 -9
- package/src/timeline/shared.tsx +56 -8
- package/src/timeline/tool-renderers.tsx +303 -50
- package/src/timeline/types.ts +57 -3
- package/styles/compiled.css +312 -21
- package/styles/index.css +15 -0
- package/styles/responsive.css +13 -1
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FWXGSNXT.js.map +0 -1
- package/dist/chunk-LHLYWG5R.js.map +0 -1
- package/dist/chunk-O4VXOBGF.js.map +0 -1
- package/dist/chunk-OL6QVXGF.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-XCNTXWBB.js.map +0 -1
- package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
- package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
|
@@ -0,0 +1,2828 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArtifactSurface,
|
|
3
|
+
EditableArtifactMessage,
|
|
4
|
+
editableArtifactAccessRevoked,
|
|
5
|
+
editableArtifactStatusLabel,
|
|
6
|
+
useEditableArtifactView
|
|
7
|
+
} from "./chunk-KCGXPP5V.js";
|
|
8
|
+
import {
|
|
9
|
+
cn
|
|
10
|
+
} from "./chunk-22KP6LR5.js";
|
|
11
|
+
|
|
12
|
+
// src/components/artifacts/spreadsheet-grid.tsx
|
|
13
|
+
import { PlusIcon } from "lucide-react";
|
|
14
|
+
import {
|
|
15
|
+
useCallback,
|
|
16
|
+
useEffect,
|
|
17
|
+
useId,
|
|
18
|
+
useLayoutEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState
|
|
22
|
+
} from "react";
|
|
23
|
+
|
|
24
|
+
// src/components/artifacts/spreadsheet-canvas.ts
|
|
25
|
+
var TILE_SIZE = 256;
|
|
26
|
+
var MAX_CACHE_PHYSICAL_PIXELS = 32e6;
|
|
27
|
+
var MAX_CACHE_TILES = 96;
|
|
28
|
+
var MAX_DEVICE_PIXEL_RATIO = 4;
|
|
29
|
+
var MAX_AXIS_MARKS_PER_PAINT = 8192;
|
|
30
|
+
var MAX_CANVAS_EDGE = 8192;
|
|
31
|
+
var MAX_CANVAS_PIXELS = 16e6;
|
|
32
|
+
var MAX_TILES_PER_PAINT = 512;
|
|
33
|
+
var SparseSpreadsheetCellIndex = class {
|
|
34
|
+
size;
|
|
35
|
+
rows;
|
|
36
|
+
constructor(entries) {
|
|
37
|
+
const sorted = [...entries].sort((left, right) => left.row - right.row || left.col - right.col);
|
|
38
|
+
this.size = sorted.length;
|
|
39
|
+
this.rows = [];
|
|
40
|
+
for (const entry of sorted) {
|
|
41
|
+
const previous = this.rows.at(-1);
|
|
42
|
+
if (previous?.row === entry.row) previous.cells.push(entry);
|
|
43
|
+
else this.rows.push({ row: entry.row, cells: [entry] });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
forEachInRange(rowStart, rowEnd, columnStart, columnEnd, visit) {
|
|
47
|
+
let rowIndex = lowerBound(this.rows, rowStart, (row) => row.row);
|
|
48
|
+
while (rowIndex < this.rows.length) {
|
|
49
|
+
const row = this.rows[rowIndex];
|
|
50
|
+
if (row.row >= rowEnd) break;
|
|
51
|
+
let cellIndex = lowerBound(row.cells, columnStart, (cell) => cell.col);
|
|
52
|
+
while (cellIndex < row.cells.length) {
|
|
53
|
+
const cell = row.cells[cellIndex];
|
|
54
|
+
if (cell.col >= columnEnd) break;
|
|
55
|
+
visit(cell);
|
|
56
|
+
cellIndex += 1;
|
|
57
|
+
}
|
|
58
|
+
rowIndex += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function lowerBound(values, target, valueAt) {
|
|
63
|
+
let low = 0;
|
|
64
|
+
let high = values.length;
|
|
65
|
+
while (low < high) {
|
|
66
|
+
const middle = low + high >>> 1;
|
|
67
|
+
if (valueAt(values[middle]) < target) low = middle + 1;
|
|
68
|
+
else high = middle;
|
|
69
|
+
}
|
|
70
|
+
return low;
|
|
71
|
+
}
|
|
72
|
+
function coverageContains(coverage, query) {
|
|
73
|
+
return coverage === void 0 || coverage.rowStart <= query.rowStart && coverage.rowEnd >= query.rowEnd && coverage.columnStart <= query.columnStart && coverage.columnEnd >= query.columnEnd;
|
|
74
|
+
}
|
|
75
|
+
function intersectCoverage(query, coverage) {
|
|
76
|
+
if (coverage === void 0) return query;
|
|
77
|
+
const intersection = {
|
|
78
|
+
rowStart: Math.max(query.rowStart, coverage.rowStart),
|
|
79
|
+
rowEnd: Math.min(query.rowEnd, coverage.rowEnd),
|
|
80
|
+
columnStart: Math.max(query.columnStart, coverage.columnStart),
|
|
81
|
+
columnEnd: Math.min(query.columnEnd, coverage.columnEnd)
|
|
82
|
+
};
|
|
83
|
+
return intersection.rowStart < intersection.rowEnd && intersection.columnStart < intersection.columnEnd ? intersection : null;
|
|
84
|
+
}
|
|
85
|
+
function spreadsheetCanvasDevicePixelRatio(value) {
|
|
86
|
+
if (!Number.isFinite(value) || value <= 0) return 1;
|
|
87
|
+
return Math.max(1, Math.min(MAX_DEVICE_PIXEL_RATIO, value));
|
|
88
|
+
}
|
|
89
|
+
function spreadsheetCanvasBackingScale(width, height, devicePixelRatio) {
|
|
90
|
+
const safeWidth = finiteExtent(width);
|
|
91
|
+
const safeHeight = finiteExtent(height);
|
|
92
|
+
const requested = spreadsheetCanvasDevicePixelRatio(devicePixelRatio);
|
|
93
|
+
return Math.max(
|
|
94
|
+
Number.EPSILON,
|
|
95
|
+
Math.min(
|
|
96
|
+
requested,
|
|
97
|
+
MAX_CANVAS_EDGE / safeWidth,
|
|
98
|
+
MAX_CANVAS_EDGE / safeHeight,
|
|
99
|
+
Math.sqrt(MAX_CANVAS_PIXELS / (safeWidth * safeHeight))
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
var SpreadsheetCanvasRenderer = class {
|
|
104
|
+
tiles = /* @__PURE__ */ new Map();
|
|
105
|
+
generation = "";
|
|
106
|
+
clock = 0;
|
|
107
|
+
dispose() {
|
|
108
|
+
this.tiles.clear();
|
|
109
|
+
this.generation = "";
|
|
110
|
+
}
|
|
111
|
+
paint(input) {
|
|
112
|
+
const context = safeCanvasContext(input.canvas);
|
|
113
|
+
if (!context) return null;
|
|
114
|
+
const width = finiteExtent(input.viewportWidth);
|
|
115
|
+
const height = finiteExtent(input.viewportHeight);
|
|
116
|
+
const dpr = spreadsheetCanvasBackingScale(width, height, input.devicePixelRatio);
|
|
117
|
+
const pixelWidth = Math.max(1, Math.min(MAX_CANVAS_EDGE, Math.round(width * dpr)));
|
|
118
|
+
const pixelHeight = Math.max(1, Math.min(MAX_CANVAS_EDGE, Math.round(height * dpr)));
|
|
119
|
+
if (input.canvas.width !== pixelWidth) input.canvas.width = pixelWidth;
|
|
120
|
+
if (input.canvas.height !== pixelHeight) input.canvas.height = pixelHeight;
|
|
121
|
+
input.canvas.style.width = `${width}px`;
|
|
122
|
+
input.canvas.style.height = `${height}px`;
|
|
123
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
124
|
+
context.clearRect(0, 0, width, height);
|
|
125
|
+
const generation = [
|
|
126
|
+
input.projection.sheetId,
|
|
127
|
+
input.projection.generationId ?? "<none>",
|
|
128
|
+
input.projection.revision,
|
|
129
|
+
input.dimensionRevision,
|
|
130
|
+
dpr,
|
|
131
|
+
input.theme.signature
|
|
132
|
+
].join("/");
|
|
133
|
+
if (generation !== this.generation) {
|
|
134
|
+
this.tiles.clear();
|
|
135
|
+
this.generation = generation;
|
|
136
|
+
}
|
|
137
|
+
setFill(context, input.theme.background, "#ffffff");
|
|
138
|
+
context.fillRect(0, 0, width, height);
|
|
139
|
+
const bodyLeft = input.rowHeaderWidth;
|
|
140
|
+
const bodyTop = input.columnHeaderHeight;
|
|
141
|
+
const visibleLeft = clamp(input.logicalScrollLeft, 0, input.columns.total);
|
|
142
|
+
const visibleTop = clamp(input.logicalScrollTop, 0, input.rows.total);
|
|
143
|
+
const visibleRight = clamp(
|
|
144
|
+
input.logicalScrollLeft + width - bodyLeft,
|
|
145
|
+
visibleLeft,
|
|
146
|
+
input.columns.total
|
|
147
|
+
);
|
|
148
|
+
const visibleBottom = clamp(
|
|
149
|
+
input.logicalScrollTop + height - bodyTop,
|
|
150
|
+
visibleTop,
|
|
151
|
+
input.rows.total
|
|
152
|
+
);
|
|
153
|
+
let paintedTiles = 0;
|
|
154
|
+
let reusedTiles = 0;
|
|
155
|
+
let uncachedTiles = 0;
|
|
156
|
+
context.save();
|
|
157
|
+
context.beginPath();
|
|
158
|
+
context.rect(bodyLeft, bodyTop, Math.max(0, width - bodyLeft), Math.max(0, height - bodyTop));
|
|
159
|
+
context.clip();
|
|
160
|
+
const firstTileColumn = Math.floor(visibleLeft / TILE_SIZE);
|
|
161
|
+
const lastTileColumn = Math.floor(
|
|
162
|
+
Math.max(visibleLeft, visibleRight - Number.EPSILON) / TILE_SIZE
|
|
163
|
+
);
|
|
164
|
+
const firstTileRow = Math.floor(visibleTop / TILE_SIZE);
|
|
165
|
+
const lastTileRow = Math.floor(
|
|
166
|
+
Math.max(visibleTop, visibleBottom - Number.EPSILON) / TILE_SIZE
|
|
167
|
+
);
|
|
168
|
+
let visitedTiles = 0;
|
|
169
|
+
tileLoop: for (let tileRow = firstTileRow; tileRow <= lastTileRow; tileRow += 1) {
|
|
170
|
+
for (let tileColumn = firstTileColumn; tileColumn <= lastTileColumn; tileColumn += 1) {
|
|
171
|
+
if (visitedTiles >= MAX_TILES_PER_PAINT) break tileLoop;
|
|
172
|
+
visitedTiles += 1;
|
|
173
|
+
const key = `${tileColumn}:${tileRow}`;
|
|
174
|
+
let tile = this.tiles.get(key);
|
|
175
|
+
if (!tile) {
|
|
176
|
+
const painted = this.paintTile(input, tileColumn, tileRow, dpr);
|
|
177
|
+
tile = {
|
|
178
|
+
canvas: painted.canvas,
|
|
179
|
+
lastUsed: ++this.clock
|
|
180
|
+
};
|
|
181
|
+
if (painted.cacheable) this.tiles.set(key, tile);
|
|
182
|
+
else uncachedTiles += 1;
|
|
183
|
+
paintedTiles += 1;
|
|
184
|
+
} else {
|
|
185
|
+
tile.lastUsed = ++this.clock;
|
|
186
|
+
reusedTiles += 1;
|
|
187
|
+
}
|
|
188
|
+
const logicalX = tileColumn * TILE_SIZE;
|
|
189
|
+
const logicalY = tileRow * TILE_SIZE;
|
|
190
|
+
context.drawImage(
|
|
191
|
+
tile.canvas,
|
|
192
|
+
bodyLeft + logicalX - input.logicalScrollLeft,
|
|
193
|
+
bodyTop + logicalY - input.logicalScrollTop,
|
|
194
|
+
TILE_SIZE,
|
|
195
|
+
TILE_SIZE
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
paintSelection(context, input);
|
|
200
|
+
paintVisibleGrid(context, input, visibleLeft, visibleTop, visibleRight, visibleBottom);
|
|
201
|
+
context.restore();
|
|
202
|
+
paintHeaders(
|
|
203
|
+
context,
|
|
204
|
+
input,
|
|
205
|
+
width,
|
|
206
|
+
height,
|
|
207
|
+
visibleLeft,
|
|
208
|
+
visibleTop,
|
|
209
|
+
visibleRight,
|
|
210
|
+
visibleBottom
|
|
211
|
+
);
|
|
212
|
+
this.evictTiles(dpr);
|
|
213
|
+
return {
|
|
214
|
+
cacheSize: this.tiles.size,
|
|
215
|
+
paintedTiles,
|
|
216
|
+
reusedTiles,
|
|
217
|
+
uncachedTiles,
|
|
218
|
+
devicePixelRatio: dpr
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
paintTile(input, tileColumn, tileRow, dpr) {
|
|
222
|
+
const tile = input.canvas.ownerDocument.createElement("canvas");
|
|
223
|
+
tile.width = Math.max(1, Math.round(TILE_SIZE * dpr));
|
|
224
|
+
tile.height = Math.max(1, Math.round(TILE_SIZE * dpr));
|
|
225
|
+
const context = safeCanvasContext(tile);
|
|
226
|
+
if (!context) return { canvas: tile, cacheable: false };
|
|
227
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
228
|
+
setFill(context, input.theme.background, "#ffffff");
|
|
229
|
+
context.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
|
|
230
|
+
const tileLeft = tileColumn * TILE_SIZE;
|
|
231
|
+
const tileTop = tileRow * TILE_SIZE;
|
|
232
|
+
const tileRight = Math.min(input.columns.total, tileLeft + TILE_SIZE);
|
|
233
|
+
const tileBottom = Math.min(input.rows.total, tileTop + TILE_SIZE);
|
|
234
|
+
const firstColumn = input.columns.indexAtOffset(tileLeft);
|
|
235
|
+
const lastColumn = input.columns.indexAtOffset(Math.max(tileLeft, tileRight - Number.EPSILON));
|
|
236
|
+
const firstRow = input.rows.indexAtOffset(tileTop);
|
|
237
|
+
const lastRow = input.rows.indexAtOffset(Math.max(tileTop, tileBottom - Number.EPSILON));
|
|
238
|
+
const query = {
|
|
239
|
+
rowStart: firstRow,
|
|
240
|
+
rowEnd: Math.min(input.rows.count, lastRow + 1),
|
|
241
|
+
columnStart: firstColumn,
|
|
242
|
+
columnEnd: Math.min(input.columns.count, lastColumn + 1)
|
|
243
|
+
};
|
|
244
|
+
const coveredQuery = intersectCoverage(query, input.projection.coverage);
|
|
245
|
+
const cacheable = coverageContains(input.projection.coverage, query);
|
|
246
|
+
const cells = [];
|
|
247
|
+
if (coveredQuery) {
|
|
248
|
+
input.projection.cells.forEachInRange(
|
|
249
|
+
coveredQuery.rowStart,
|
|
250
|
+
coveredQuery.rowEnd,
|
|
251
|
+
coveredQuery.columnStart,
|
|
252
|
+
coveredQuery.columnEnd,
|
|
253
|
+
(cell) => cells.push(cell)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
for (const cell of cells) {
|
|
257
|
+
const fill = cell.format.fill;
|
|
258
|
+
if (!fill) continue;
|
|
259
|
+
const x = input.columns.offsetAt(cell.col) - tileLeft;
|
|
260
|
+
const y = input.rows.offsetAt(cell.row) - tileTop;
|
|
261
|
+
const width = input.columns.sizeAt(cell.col);
|
|
262
|
+
const height = input.rows.sizeAt(cell.row);
|
|
263
|
+
setFill(context, fill, input.theme.background);
|
|
264
|
+
context.fillRect(x, y, width, height);
|
|
265
|
+
}
|
|
266
|
+
paintTileGrid(context, input, tileLeft, tileTop, tileRight, tileBottom);
|
|
267
|
+
for (const cell of cells) {
|
|
268
|
+
const value = input.projection.valueAt(cell);
|
|
269
|
+
const text = displayValue(value);
|
|
270
|
+
if (!text) continue;
|
|
271
|
+
paintCellText(context, input, cell, text, tileLeft, tileTop);
|
|
272
|
+
}
|
|
273
|
+
return { canvas: tile, cacheable };
|
|
274
|
+
}
|
|
275
|
+
evictTiles(dpr) {
|
|
276
|
+
const pixelsPerTile = Math.max(1, Math.round(TILE_SIZE * dpr)) ** 2;
|
|
277
|
+
const maximum = Math.max(
|
|
278
|
+
4,
|
|
279
|
+
Math.min(MAX_CACHE_TILES, Math.floor(MAX_CACHE_PHYSICAL_PIXELS / pixelsPerTile))
|
|
280
|
+
);
|
|
281
|
+
if (this.tiles.size <= maximum) return;
|
|
282
|
+
const oldest = [...this.tiles.entries()].sort(
|
|
283
|
+
(left, right) => left[1].lastUsed - right[1].lastUsed
|
|
284
|
+
);
|
|
285
|
+
for (let index = 0; index < oldest.length - maximum; index += 1) {
|
|
286
|
+
this.tiles.delete(oldest[index][0]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
function finiteExtent(value) {
|
|
291
|
+
if (!Number.isFinite(value) || value <= 0) return 1;
|
|
292
|
+
return Math.max(1, Math.floor(value));
|
|
293
|
+
}
|
|
294
|
+
function safeCanvasContext(canvas) {
|
|
295
|
+
try {
|
|
296
|
+
return canvas.getContext("2d", { alpha: false });
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function setFill(context, value, fallback) {
|
|
302
|
+
context.fillStyle = fallback;
|
|
303
|
+
try {
|
|
304
|
+
context.fillStyle = value;
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function setStroke(context, value, fallback) {
|
|
309
|
+
context.strokeStyle = fallback;
|
|
310
|
+
try {
|
|
311
|
+
context.strokeStyle = value;
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function paintTileGrid(context, input, tileLeft, tileTop, tileRight, tileBottom) {
|
|
316
|
+
setFill(context, input.theme.border, "#d1d5db");
|
|
317
|
+
visitAxisBoundaries(input.columns, tileLeft, tileRight, (offset) => {
|
|
318
|
+
context.fillRect(offset - tileLeft - 1, 0, 1, TILE_SIZE);
|
|
319
|
+
});
|
|
320
|
+
visitAxisBoundaries(input.rows, tileTop, tileBottom, (offset) => {
|
|
321
|
+
context.fillRect(0, offset - tileTop - 1, TILE_SIZE, 1);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function paintVisibleGrid(context, input, visibleLeft, visibleTop, visibleRight, visibleBottom) {
|
|
325
|
+
setFill(context, input.theme.border, "#d1d5db");
|
|
326
|
+
visitAxisBoundaries(input.columns, visibleLeft, visibleRight, (offset) => {
|
|
327
|
+
const x = input.rowHeaderWidth + offset - input.logicalScrollLeft;
|
|
328
|
+
context.fillRect(x - 1, input.columnHeaderHeight, 1, input.viewportHeight);
|
|
329
|
+
});
|
|
330
|
+
visitAxisBoundaries(input.rows, visibleTop, visibleBottom, (offset) => {
|
|
331
|
+
const y = input.columnHeaderHeight + offset - input.logicalScrollTop;
|
|
332
|
+
context.fillRect(input.rowHeaderWidth, y - 1, input.viewportWidth, 1);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function visitAxisBoundaries(axis, start, end, visit) {
|
|
336
|
+
if (axis.count <= 0 || end < start) return;
|
|
337
|
+
let index = axis.indexAtOffset(start);
|
|
338
|
+
let visits = 0;
|
|
339
|
+
while (index < axis.count && visits < MAX_AXIS_MARKS_PER_PAINT) {
|
|
340
|
+
const boundary = axis.offsetAt(index + 1);
|
|
341
|
+
if (boundary > end + 1) break;
|
|
342
|
+
if (boundary >= start) visit(boundary);
|
|
343
|
+
index += 1;
|
|
344
|
+
visits += 1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function paintSelection(context, input) {
|
|
348
|
+
const { selection } = input;
|
|
349
|
+
const left = input.rowHeaderWidth + input.columns.offsetAt(selection.left) - input.logicalScrollLeft;
|
|
350
|
+
const right = input.rowHeaderWidth + input.columns.offsetAt(selection.right + 1) - input.logicalScrollLeft;
|
|
351
|
+
const top = input.columnHeaderHeight + input.rows.offsetAt(selection.top) - input.logicalScrollTop;
|
|
352
|
+
const bottom = input.columnHeaderHeight + input.rows.offsetAt(selection.bottom + 1) - input.logicalScrollTop;
|
|
353
|
+
context.save();
|
|
354
|
+
setFill(context, input.theme.accent, "#3b82f6");
|
|
355
|
+
context.globalAlpha = 0.1;
|
|
356
|
+
context.fillRect(left, top, right - left, bottom - top);
|
|
357
|
+
context.restore();
|
|
358
|
+
const activeLeft = input.rowHeaderWidth + input.columns.offsetAt(selection.focusColumn) - input.logicalScrollLeft;
|
|
359
|
+
const activeTop = input.columnHeaderHeight + input.rows.offsetAt(selection.focusRow) - input.logicalScrollTop;
|
|
360
|
+
const activeWidth = input.columns.sizeAt(selection.focusColumn);
|
|
361
|
+
const activeHeight = input.rows.sizeAt(selection.focusRow);
|
|
362
|
+
context.save();
|
|
363
|
+
setStroke(context, input.theme.accent, "#3b82f6");
|
|
364
|
+
context.lineWidth = 2;
|
|
365
|
+
context.strokeRect(
|
|
366
|
+
activeLeft + 1,
|
|
367
|
+
activeTop + 1,
|
|
368
|
+
Math.max(0, activeWidth - 2),
|
|
369
|
+
Math.max(0, activeHeight - 2)
|
|
370
|
+
);
|
|
371
|
+
context.restore();
|
|
372
|
+
}
|
|
373
|
+
function paintHeaders(context, input, width, height, visibleLeft, visibleTop, visibleRight, visibleBottom) {
|
|
374
|
+
setFill(context, input.theme.headerBackground, "#f3f4f6");
|
|
375
|
+
context.fillRect(0, 0, width, input.columnHeaderHeight);
|
|
376
|
+
context.fillRect(0, input.columnHeaderHeight, input.rowHeaderWidth, height);
|
|
377
|
+
setFill(context, input.theme.border, "#d1d5db");
|
|
378
|
+
context.fillRect(0, input.columnHeaderHeight - 1, width, 1);
|
|
379
|
+
context.fillRect(input.rowHeaderWidth - 1, 0, 1, height);
|
|
380
|
+
context.save();
|
|
381
|
+
context.beginPath();
|
|
382
|
+
context.rect(
|
|
383
|
+
input.rowHeaderWidth,
|
|
384
|
+
0,
|
|
385
|
+
Math.max(0, width - input.rowHeaderWidth),
|
|
386
|
+
input.columnHeaderHeight
|
|
387
|
+
);
|
|
388
|
+
context.clip();
|
|
389
|
+
setFill(context, input.theme.mutedForeground, "#6b7280");
|
|
390
|
+
context.font = `600 ${input.theme.headerFontSize}px ${input.theme.fontFamily}`;
|
|
391
|
+
context.textAlign = "center";
|
|
392
|
+
context.textBaseline = "middle";
|
|
393
|
+
visitAxisItems(input.columns, visibleLeft, visibleRight, (column, offset, size) => {
|
|
394
|
+
const x = input.rowHeaderWidth + offset - input.logicalScrollLeft;
|
|
395
|
+
setFill(context, input.theme.border, "#d1d5db");
|
|
396
|
+
context.fillRect(x + size - 1, 0, 1, input.columnHeaderHeight);
|
|
397
|
+
if (size >= 8) {
|
|
398
|
+
setFill(context, input.theme.mutedForeground, "#6b7280");
|
|
399
|
+
context.fillText(columnName(column), x + size / 2, input.columnHeaderHeight / 2);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
context.restore();
|
|
403
|
+
context.save();
|
|
404
|
+
context.beginPath();
|
|
405
|
+
context.rect(
|
|
406
|
+
0,
|
|
407
|
+
input.columnHeaderHeight,
|
|
408
|
+
input.rowHeaderWidth,
|
|
409
|
+
Math.max(0, height - input.columnHeaderHeight)
|
|
410
|
+
);
|
|
411
|
+
context.clip();
|
|
412
|
+
setFill(context, input.theme.mutedForeground, "#6b7280");
|
|
413
|
+
context.font = `400 ${input.theme.headerFontSize}px ${input.theme.fontFamily}`;
|
|
414
|
+
context.textAlign = "center";
|
|
415
|
+
context.textBaseline = "middle";
|
|
416
|
+
visitAxisItems(input.rows, visibleTop, visibleBottom, (row, offset, size) => {
|
|
417
|
+
const y = input.columnHeaderHeight + offset - input.logicalScrollTop;
|
|
418
|
+
setFill(context, input.theme.border, "#d1d5db");
|
|
419
|
+
context.fillRect(0, y + size - 1, input.rowHeaderWidth, 1);
|
|
420
|
+
if (size >= 8) {
|
|
421
|
+
setFill(context, input.theme.mutedForeground, "#6b7280");
|
|
422
|
+
context.fillText(String(row + 1), input.rowHeaderWidth / 2, y + size / 2);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
context.restore();
|
|
426
|
+
}
|
|
427
|
+
function visitAxisItems(axis, start, end, visit) {
|
|
428
|
+
if (axis.count <= 0 || end < start) return;
|
|
429
|
+
let index = axis.indexAtOffset(start);
|
|
430
|
+
let visits = 0;
|
|
431
|
+
while (index < axis.count && visits < MAX_AXIS_MARKS_PER_PAINT) {
|
|
432
|
+
const offset = axis.offsetAt(index);
|
|
433
|
+
if (offset > end) break;
|
|
434
|
+
visit(index, offset, axis.sizeAt(index));
|
|
435
|
+
index += 1;
|
|
436
|
+
visits += 1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function paintCellText(context, input, cell, text, tileLeft, tileTop) {
|
|
440
|
+
const format = cell.format;
|
|
441
|
+
const x = input.columns.offsetAt(cell.col) - tileLeft;
|
|
442
|
+
const y = input.rows.offsetAt(cell.row) - tileTop;
|
|
443
|
+
const width = input.columns.sizeAt(cell.col);
|
|
444
|
+
const height = input.rows.sizeAt(cell.row);
|
|
445
|
+
if (width <= 2 || height <= 2) return;
|
|
446
|
+
const fontSize = finitePositive(format.font?.size, input.theme.fontSize);
|
|
447
|
+
const fontFamily = format.font?.name || input.theme.fontFamily;
|
|
448
|
+
const weight = format.font?.bold ? 600 : 400;
|
|
449
|
+
const italic = format.font?.italic ? "italic " : "";
|
|
450
|
+
context.save();
|
|
451
|
+
context.beginPath();
|
|
452
|
+
context.rect(x + 1, y + 1, Math.max(0, width - 2), Math.max(0, height - 2));
|
|
453
|
+
context.clip();
|
|
454
|
+
context.font = `${italic}${weight} ${fontSize}px ${fontFamily}`;
|
|
455
|
+
setFill(
|
|
456
|
+
context,
|
|
457
|
+
format.font?.color ?? (text.startsWith("#") ? input.theme.error : input.theme.foreground),
|
|
458
|
+
input.theme.foreground
|
|
459
|
+
);
|
|
460
|
+
const alignment = format.horizontalAlignment;
|
|
461
|
+
context.textAlign = alignment === "center" ? "center" : alignment === "right" ? "right" : "left";
|
|
462
|
+
context.textBaseline = "alphabetic";
|
|
463
|
+
const padding = 6;
|
|
464
|
+
const textX = alignment === "center" ? x + width / 2 : alignment === "right" ? x + width - padding : x + padding;
|
|
465
|
+
const lineHeight = fontSize * 1.5;
|
|
466
|
+
const availableWidth = Math.max(0, width - padding * 2);
|
|
467
|
+
const lines = format.wrapText ? wrapText(context, text, availableWidth, Math.max(1, Math.floor(height / lineHeight))) : [ellipsize(context, text.replaceAll(/\r?\n/g, " "), availableWidth)];
|
|
468
|
+
const blockHeight = lines.length * lineHeight;
|
|
469
|
+
const vertical = format.verticalAlignment;
|
|
470
|
+
const firstBaseline = vertical === "top" ? y + Math.min(height, padding + fontSize) : vertical === "bottom" ? y + height - padding - blockHeight + fontSize : y + (height - blockHeight) / 2 + fontSize;
|
|
471
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
472
|
+
const line = lines[index];
|
|
473
|
+
const baseline = firstBaseline + index * lineHeight;
|
|
474
|
+
context.fillText(line, textX, baseline);
|
|
475
|
+
if (format.font?.underline && line) {
|
|
476
|
+
const metrics = context.measureText(line);
|
|
477
|
+
const textWidth = metrics.width;
|
|
478
|
+
const startX = alignment === "center" ? textX - textWidth / 2 : alignment === "right" ? textX - textWidth : textX;
|
|
479
|
+
setFill(context, format.font.color ?? input.theme.foreground, input.theme.foreground);
|
|
480
|
+
context.fillRect(startX, baseline + 1, textWidth, Math.max(1, fontSize / 14));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
context.restore();
|
|
484
|
+
}
|
|
485
|
+
function wrapText(context, input, maximumWidth, maximumLines) {
|
|
486
|
+
if (maximumWidth <= 0) return [""];
|
|
487
|
+
const output = [];
|
|
488
|
+
for (const paragraph of input.split(/\r?\n/)) {
|
|
489
|
+
const words = paragraph.split(/\s+/).filter(Boolean);
|
|
490
|
+
if (words.length === 0) output.push("");
|
|
491
|
+
let line = "";
|
|
492
|
+
for (const word of words) {
|
|
493
|
+
const next = line ? `${line} ${word}` : word;
|
|
494
|
+
if (!line || context.measureText(next).width <= maximumWidth) line = next;
|
|
495
|
+
else {
|
|
496
|
+
output.push(ellipsize(context, line, maximumWidth));
|
|
497
|
+
line = word;
|
|
498
|
+
}
|
|
499
|
+
if (output.length >= maximumLines) break;
|
|
500
|
+
}
|
|
501
|
+
if (output.length < maximumLines && line) output.push(ellipsize(context, line, maximumWidth));
|
|
502
|
+
if (output.length >= maximumLines) break;
|
|
503
|
+
}
|
|
504
|
+
return output.slice(0, maximumLines);
|
|
505
|
+
}
|
|
506
|
+
function ellipsize(context, input, maximumWidth) {
|
|
507
|
+
if (!input || maximumWidth <= 0) return "";
|
|
508
|
+
if (context.measureText(input).width <= maximumWidth) return input;
|
|
509
|
+
const suffix = "\u2026";
|
|
510
|
+
const suffixWidth = context.measureText(suffix).width;
|
|
511
|
+
if (suffixWidth > maximumWidth) return "";
|
|
512
|
+
let low = 0;
|
|
513
|
+
let high = input.length;
|
|
514
|
+
while (low < high) {
|
|
515
|
+
const middle = Math.ceil((low + high) / 2);
|
|
516
|
+
if (context.measureText(`${input.slice(0, middle)}${suffix}`).width <= maximumWidth)
|
|
517
|
+
low = middle;
|
|
518
|
+
else high = middle - 1;
|
|
519
|
+
}
|
|
520
|
+
return `${input.slice(0, low)}${suffix}`;
|
|
521
|
+
}
|
|
522
|
+
function finitePositive(value, fallback) {
|
|
523
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
524
|
+
}
|
|
525
|
+
function displayValue(value) {
|
|
526
|
+
if (value === null || value === void 0) return "";
|
|
527
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
|
|
528
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
529
|
+
return String(value);
|
|
530
|
+
}
|
|
531
|
+
function columnName(index) {
|
|
532
|
+
let value = index + 1;
|
|
533
|
+
let result = "";
|
|
534
|
+
while (value > 0) {
|
|
535
|
+
const remainder = (value - 1) % 26;
|
|
536
|
+
result = String.fromCharCode(65 + remainder) + result;
|
|
537
|
+
value = Math.floor((value - 1) / 26);
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
function clamp(value, minimum, maximum) {
|
|
542
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/components/artifacts/spreadsheet-grid.tsx
|
|
546
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
547
|
+
var MAX_ROWS = 1048576;
|
|
548
|
+
var MAX_COLUMNS = 16384;
|
|
549
|
+
var ROW_HEADER_WIDTH = 48;
|
|
550
|
+
var COLUMN_HEADER_HEIGHT = 28;
|
|
551
|
+
var FALLBACK_VIEWPORT_WIDTH = 880;
|
|
552
|
+
var FALLBACK_VIEWPORT_HEIGHT = 480;
|
|
553
|
+
var MAX_PHYSICAL_SCROLL_SPAN = 8e6;
|
|
554
|
+
var MIN_PHYSICAL_ITEM_SPAN = 4;
|
|
555
|
+
var MAX_MOUNTED_AXIS_ITEMS = 512;
|
|
556
|
+
var MAX_MOUNTED_GRID_CELLS = 2048;
|
|
557
|
+
var MAX_CLIPBOARD_INPUT_BYTES = 4 * 1024 * 1024;
|
|
558
|
+
var MAX_CLIPBOARD_CELLS = 65536;
|
|
559
|
+
var useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
560
|
+
function boundedInteger(value, fallback, max) {
|
|
561
|
+
if (value === void 0) return fallback;
|
|
562
|
+
if (!Number.isFinite(value)) return fallback;
|
|
563
|
+
return Math.min(max, Math.max(1, Math.floor(value)));
|
|
564
|
+
}
|
|
565
|
+
function buildAxis(count, defaultSize, entries) {
|
|
566
|
+
const normalizedDefault = Number.isFinite(defaultSize) && defaultSize > 0 ? defaultSize : 1;
|
|
567
|
+
const sorted = [...entries].filter(
|
|
568
|
+
([index, size]) => Number.isInteger(index) && index >= 0 && index < count && Number.isFinite(size) && size > 0
|
|
569
|
+
).sort(([left], [right]) => left - right);
|
|
570
|
+
const overrides = [];
|
|
571
|
+
let cumulativeDelta = 0;
|
|
572
|
+
for (const [index, size] of sorted) {
|
|
573
|
+
const previous = overrides.at(-1);
|
|
574
|
+
if (previous?.index === index) {
|
|
575
|
+
cumulativeDelta -= previous.size - normalizedDefault;
|
|
576
|
+
overrides.pop();
|
|
577
|
+
}
|
|
578
|
+
if (size === normalizedDefault) continue;
|
|
579
|
+
const start = index * normalizedDefault + cumulativeDelta;
|
|
580
|
+
cumulativeDelta += size - normalizedDefault;
|
|
581
|
+
overrides.push({
|
|
582
|
+
index,
|
|
583
|
+
size,
|
|
584
|
+
start,
|
|
585
|
+
end: start + size,
|
|
586
|
+
cumulativeDelta
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
count,
|
|
591
|
+
defaultSize: normalizedDefault,
|
|
592
|
+
overrides,
|
|
593
|
+
total: count * normalizedDefault + cumulativeDelta
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function lowerBoundOverrideIndex(axis, index) {
|
|
597
|
+
let low = 0;
|
|
598
|
+
let high = axis.overrides.length;
|
|
599
|
+
while (low < high) {
|
|
600
|
+
const middle = low + high >>> 1;
|
|
601
|
+
if (axis.overrides[middle].index < index) low = middle + 1;
|
|
602
|
+
else high = middle;
|
|
603
|
+
}
|
|
604
|
+
return low;
|
|
605
|
+
}
|
|
606
|
+
function axisOffsetAt(axis, index) {
|
|
607
|
+
if (index <= 0) return 0;
|
|
608
|
+
if (index >= axis.count) return axis.total;
|
|
609
|
+
const insertion = lowerBoundOverrideIndex(axis, index);
|
|
610
|
+
const delta = insertion > 0 ? axis.overrides[insertion - 1].cumulativeDelta : 0;
|
|
611
|
+
return index * axis.defaultSize + delta;
|
|
612
|
+
}
|
|
613
|
+
function axisSizeAt(axis, index) {
|
|
614
|
+
const candidate = axis.overrides[lowerBoundOverrideIndex(axis, index)];
|
|
615
|
+
return candidate?.index === index ? candidate.size : axis.defaultSize;
|
|
616
|
+
}
|
|
617
|
+
function upperBoundOverrideStart(axis, offset) {
|
|
618
|
+
let low = 0;
|
|
619
|
+
let high = axis.overrides.length;
|
|
620
|
+
while (low < high) {
|
|
621
|
+
const middle = low + high >>> 1;
|
|
622
|
+
if (axis.overrides[middle].start <= offset) low = middle + 1;
|
|
623
|
+
else high = middle;
|
|
624
|
+
}
|
|
625
|
+
return low;
|
|
626
|
+
}
|
|
627
|
+
function indexAtOffset(axis, offset) {
|
|
628
|
+
if (axis.count <= 1) return 0;
|
|
629
|
+
const target = Math.max(0, Math.min(axis.total - 1, offset));
|
|
630
|
+
const previousIndex = upperBoundOverrideStart(axis, target) - 1;
|
|
631
|
+
if (previousIndex < 0) {
|
|
632
|
+
return Math.min(
|
|
633
|
+
(axis.overrides[0]?.index ?? axis.count) - 1,
|
|
634
|
+
Math.floor(target / axis.defaultSize)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const previous = axis.overrides[previousIndex];
|
|
638
|
+
if (target < previous.end) return previous.index;
|
|
639
|
+
const nextOverrideIndex = axis.overrides[previousIndex + 1]?.index ?? axis.count;
|
|
640
|
+
return Math.min(
|
|
641
|
+
axis.count - 1,
|
|
642
|
+
nextOverrideIndex - 1,
|
|
643
|
+
previous.index + 1 + Math.floor((target - previous.end) / axis.defaultSize)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
function visibleInterval(axis, startOffset, viewportSize, overscan) {
|
|
647
|
+
const start = Math.max(0, indexAtOffset(axis, startOffset) - overscan);
|
|
648
|
+
const end = Math.min(
|
|
649
|
+
axis.count,
|
|
650
|
+
indexAtOffset(axis, startOffset + Math.max(1, viewportSize)) + overscan + 1
|
|
651
|
+
);
|
|
652
|
+
return { start, end };
|
|
653
|
+
}
|
|
654
|
+
function boundVisibleWindows(rows, columns, focus) {
|
|
655
|
+
let rowCount = Math.min(MAX_MOUNTED_AXIS_ITEMS, rows.end - rows.start);
|
|
656
|
+
let columnCount = Math.min(MAX_MOUNTED_AXIS_ITEMS, columns.end - columns.start);
|
|
657
|
+
if (rowCount * columnCount > MAX_MOUNTED_GRID_CELLS) {
|
|
658
|
+
const balancedLimit = Math.floor(Math.sqrt(MAX_MOUNTED_GRID_CELLS));
|
|
659
|
+
if (rowCount > balancedLimit && columnCount > balancedLimit) {
|
|
660
|
+
rowCount = balancedLimit;
|
|
661
|
+
columnCount = balancedLimit;
|
|
662
|
+
} else if (rowCount >= columnCount) {
|
|
663
|
+
rowCount = Math.max(1, Math.floor(MAX_MOUNTED_GRID_CELLS / columnCount));
|
|
664
|
+
} else {
|
|
665
|
+
columnCount = Math.max(1, Math.floor(MAX_MOUNTED_GRID_CELLS / rowCount));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
rows: centeredInterval(rows, rowCount, focus.row),
|
|
670
|
+
columns: centeredInterval(columns, columnCount, focus.col)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function centeredInterval(interval, count, focus) {
|
|
674
|
+
if (count >= interval.end - interval.start) return interval;
|
|
675
|
+
if (focus < interval.start || focus >= interval.end) {
|
|
676
|
+
return { start: interval.start, end: interval.start + count };
|
|
677
|
+
}
|
|
678
|
+
const start = Math.max(
|
|
679
|
+
interval.start,
|
|
680
|
+
Math.min(interval.end - count, focus - Math.floor(count / 2))
|
|
681
|
+
);
|
|
682
|
+
return { start, end: start + count };
|
|
683
|
+
}
|
|
684
|
+
function indexesBetween(start, end) {
|
|
685
|
+
return Array.from({ length: end - start }, (_, index) => start + index);
|
|
686
|
+
}
|
|
687
|
+
function rawBoundaryAt(projection, index) {
|
|
688
|
+
if (index <= 0) return 0;
|
|
689
|
+
if (index >= projection.axis.count) {
|
|
690
|
+
return projection.axis.count * projection.baseItemSpan + projection.proportionalSpan;
|
|
691
|
+
}
|
|
692
|
+
return index * projection.baseItemSpan + axisOffsetAt(projection.axis, index) / projection.axis.total * projection.proportionalSpan;
|
|
693
|
+
}
|
|
694
|
+
function rawOffsetAtLogical(projection, offset) {
|
|
695
|
+
if (offset <= 0) return 0;
|
|
696
|
+
if (offset >= projection.axis.total) {
|
|
697
|
+
return rawBoundaryAt(projection, projection.axis.count);
|
|
698
|
+
}
|
|
699
|
+
const index = indexAtOffset(projection.axis, offset);
|
|
700
|
+
const logicalStart = axisOffsetAt(projection.axis, index);
|
|
701
|
+
const logicalSize = axisSizeAt(projection.axis, index);
|
|
702
|
+
const fraction = Math.max(0, Math.min(1, (offset - logicalStart) / logicalSize));
|
|
703
|
+
const rawStart = rawBoundaryAt(projection, index);
|
|
704
|
+
const rawEnd = rawBoundaryAt(projection, index + 1);
|
|
705
|
+
return rawStart + fraction * (rawEnd - rawStart);
|
|
706
|
+
}
|
|
707
|
+
function logicalOffsetAtRaw(projection, rawOffset) {
|
|
708
|
+
const target = Math.max(0, Math.min(projection.rawMax, rawOffset));
|
|
709
|
+
let low = 0;
|
|
710
|
+
let high = projection.axis.count;
|
|
711
|
+
while (low < high) {
|
|
712
|
+
const middle = Math.ceil((low + high) / 2);
|
|
713
|
+
if (rawBoundaryAt(projection, middle) <= target) low = middle;
|
|
714
|
+
else high = middle - 1;
|
|
715
|
+
}
|
|
716
|
+
const index = Math.min(projection.axis.count - 1, low);
|
|
717
|
+
const rawStart = rawBoundaryAt(projection, index);
|
|
718
|
+
const rawEnd = rawBoundaryAt(projection, index + 1);
|
|
719
|
+
const fraction = rawEnd === rawStart ? 0 : (target - rawStart) / (rawEnd - rawStart);
|
|
720
|
+
return Math.min(
|
|
721
|
+
projection.logicalMax,
|
|
722
|
+
axisOffsetAt(projection.axis, index) + fraction * axisSizeAt(projection.axis, index)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
function buildScrollProjection(axis, headerSize, viewportSize) {
|
|
726
|
+
const logicalTotal = headerSize + axis.total;
|
|
727
|
+
const physicalTotal = Math.min(logicalTotal, MAX_PHYSICAL_SCROLL_SPAN);
|
|
728
|
+
const logicalMax = Math.max(0, logicalTotal - viewportSize);
|
|
729
|
+
const physicalMax = Math.max(0, physicalTotal - viewportSize);
|
|
730
|
+
const mode = physicalTotal === logicalTotal ? "identity" : "indexed";
|
|
731
|
+
const physicalAxisCapacity = Math.max(0, physicalTotal - headerSize);
|
|
732
|
+
const baseItemSpan = mode === "indexed" ? Math.min(MIN_PHYSICAL_ITEM_SPAN, physicalAxisCapacity / axis.count) : 0;
|
|
733
|
+
const projection = {
|
|
734
|
+
axis,
|
|
735
|
+
mode,
|
|
736
|
+
physicalTotal,
|
|
737
|
+
logicalMax,
|
|
738
|
+
physicalMax,
|
|
739
|
+
baseItemSpan,
|
|
740
|
+
proportionalSpan: Math.max(0, physicalAxisCapacity - baseItemSpan * axis.count),
|
|
741
|
+
rawMax: 0
|
|
742
|
+
};
|
|
743
|
+
projection.rawMax = rawOffsetAtLogical(projection, logicalMax);
|
|
744
|
+
return projection;
|
|
745
|
+
}
|
|
746
|
+
function clampPhysicalScroll(offset, projection) {
|
|
747
|
+
return Math.max(0, Math.min(projection.physicalMax, offset));
|
|
748
|
+
}
|
|
749
|
+
function physicalToLogicalScroll(offset, projection) {
|
|
750
|
+
if (projection.logicalMax === 0 || projection.physicalMax === 0) return 0;
|
|
751
|
+
if (projection.mode === "indexed") {
|
|
752
|
+
return logicalOffsetAtRaw(
|
|
753
|
+
projection,
|
|
754
|
+
clampPhysicalScroll(offset, projection) / projection.physicalMax * projection.rawMax
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return clampPhysicalScroll(offset, projection) / projection.physicalMax * projection.logicalMax;
|
|
758
|
+
}
|
|
759
|
+
function logicalToPhysicalScroll(offset, projection) {
|
|
760
|
+
if (projection.logicalMax === 0 || projection.physicalMax === 0) return 0;
|
|
761
|
+
const logical = Math.max(0, Math.min(projection.logicalMax, offset));
|
|
762
|
+
if (projection.mode === "indexed") {
|
|
763
|
+
return rawOffsetAtLogical(projection, logical) / projection.rawMax * projection.physicalMax;
|
|
764
|
+
}
|
|
765
|
+
return logical / projection.logicalMax * projection.physicalMax;
|
|
766
|
+
}
|
|
767
|
+
function columnName2(index) {
|
|
768
|
+
let value = index + 1;
|
|
769
|
+
let result = "";
|
|
770
|
+
while (value > 0) {
|
|
771
|
+
const remainder = (value - 1) % 26;
|
|
772
|
+
result = String.fromCharCode(65 + remainder) + result;
|
|
773
|
+
value = Math.floor((value - 1) / 26);
|
|
774
|
+
}
|
|
775
|
+
return result;
|
|
776
|
+
}
|
|
777
|
+
function cellName(cell) {
|
|
778
|
+
return `${columnName2(cell.col)}${cell.row + 1}`;
|
|
779
|
+
}
|
|
780
|
+
function normalizeSelection(selection) {
|
|
781
|
+
return {
|
|
782
|
+
top: Math.min(selection.anchor.row, selection.focus.row),
|
|
783
|
+
bottom: Math.max(selection.anchor.row, selection.focus.row),
|
|
784
|
+
left: Math.min(selection.anchor.col, selection.focus.col),
|
|
785
|
+
right: Math.max(selection.anchor.col, selection.focus.col)
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function selectionName(selection) {
|
|
789
|
+
const normalized = normalizeSelection(selection);
|
|
790
|
+
const first = cellName({ row: normalized.top, col: normalized.left });
|
|
791
|
+
const last = cellName({ row: normalized.bottom, col: normalized.right });
|
|
792
|
+
return first === last ? first : `${first}:${last}`;
|
|
793
|
+
}
|
|
794
|
+
function isCellSelected(selection, row, col) {
|
|
795
|
+
const normalized = normalizeSelection(selection);
|
|
796
|
+
return row >= normalized.top && row <= normalized.bottom && col >= normalized.left && col <= normalized.right;
|
|
797
|
+
}
|
|
798
|
+
function displayValue2(value) {
|
|
799
|
+
if (value === null || value === void 0) return "";
|
|
800
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
|
|
801
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
802
|
+
return String(value);
|
|
803
|
+
}
|
|
804
|
+
function parseCellInput(input) {
|
|
805
|
+
if (input === "") return null;
|
|
806
|
+
const normalized = input.trim();
|
|
807
|
+
if (/^(?:true|false)$/i.test(normalized)) return normalized.toLowerCase() === "true";
|
|
808
|
+
if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
|
|
809
|
+
const number = Number(normalized);
|
|
810
|
+
if (Number.isFinite(number)) return number;
|
|
811
|
+
}
|
|
812
|
+
return input;
|
|
813
|
+
}
|
|
814
|
+
function parseSpreadsheetClipboard(input) {
|
|
815
|
+
if (new TextEncoder().encode(input).byteLength > MAX_CLIPBOARD_INPUT_BYTES) {
|
|
816
|
+
throw new Error("Clipboard data is too large to paste");
|
|
817
|
+
}
|
|
818
|
+
const records = [[]];
|
|
819
|
+
let field = "";
|
|
820
|
+
let quoted = false;
|
|
821
|
+
let endedWithRecordSeparator = false;
|
|
822
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
823
|
+
const character = input[index];
|
|
824
|
+
if (quoted) {
|
|
825
|
+
if (character === '"') {
|
|
826
|
+
if (input[index + 1] === '"') {
|
|
827
|
+
field += '"';
|
|
828
|
+
index += 1;
|
|
829
|
+
} else {
|
|
830
|
+
quoted = false;
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
field += character;
|
|
834
|
+
}
|
|
835
|
+
endedWithRecordSeparator = false;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (character === '"' && field.length === 0) {
|
|
839
|
+
quoted = true;
|
|
840
|
+
endedWithRecordSeparator = false;
|
|
841
|
+
} else if (character === " ") {
|
|
842
|
+
records.at(-1).push(field);
|
|
843
|
+
field = "";
|
|
844
|
+
endedWithRecordSeparator = false;
|
|
845
|
+
} else if (character === "\r" || character === "\n") {
|
|
846
|
+
records.at(-1).push(field);
|
|
847
|
+
field = "";
|
|
848
|
+
records.push([]);
|
|
849
|
+
if (character === "\r" && input[index + 1] === "\n") index += 1;
|
|
850
|
+
endedWithRecordSeparator = true;
|
|
851
|
+
} else {
|
|
852
|
+
field += character;
|
|
853
|
+
endedWithRecordSeparator = false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (quoted) throw new Error("Clipboard data contains an unterminated quoted cell");
|
|
857
|
+
if (endedWithRecordSeparator) records.pop();
|
|
858
|
+
else records.at(-1).push(field);
|
|
859
|
+
if (records.length === 0) records.push([""]);
|
|
860
|
+
const columns = Math.max(1, ...records.map((record) => record.length));
|
|
861
|
+
const cellCount = records.length * columns;
|
|
862
|
+
if (cellCount > MAX_CLIPBOARD_CELLS) {
|
|
863
|
+
throw new Error("Clipboard range is too large to paste");
|
|
864
|
+
}
|
|
865
|
+
const inputs = records.flatMap((record) => [
|
|
866
|
+
...record,
|
|
867
|
+
...Array.from({ length: columns - record.length }, () => "")
|
|
868
|
+
]);
|
|
869
|
+
return Object.freeze({ rows: records.length, columns, inputs: Object.freeze(inputs) });
|
|
870
|
+
}
|
|
871
|
+
function escapeSpreadsheetClipboardField(input) {
|
|
872
|
+
return /["\t\r\n]/u.test(input) ? `"${input.replaceAll('"', '""')}"` : input;
|
|
873
|
+
}
|
|
874
|
+
function cellStyle(format) {
|
|
875
|
+
return {
|
|
876
|
+
background: format.fill,
|
|
877
|
+
color: format.font?.color,
|
|
878
|
+
fontFamily: format.font?.name,
|
|
879
|
+
fontSize: format.font?.size,
|
|
880
|
+
fontWeight: format.font?.bold ? 600 : void 0,
|
|
881
|
+
fontStyle: format.font?.italic ? "italic" : void 0,
|
|
882
|
+
textDecoration: format.font?.underline ? "underline" : void 0,
|
|
883
|
+
textAlign: format.horizontalAlignment,
|
|
884
|
+
justifyContent: format.horizontalAlignment === "center" ? "center" : format.horizontalAlignment === "right" ? "flex-end" : "flex-start",
|
|
885
|
+
alignItems: format.verticalAlignment === "top" ? "flex-start" : format.verticalAlignment === "bottom" ? "flex-end" : "center",
|
|
886
|
+
whiteSpace: format.wrapText ? "normal" : "nowrap"
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function canvasAxis(axis) {
|
|
890
|
+
return {
|
|
891
|
+
count: axis.count,
|
|
892
|
+
total: axis.total,
|
|
893
|
+
offsetAt: (index) => axisOffsetAt(axis, index),
|
|
894
|
+
sizeAt: (index) => axisSizeAt(axis, index),
|
|
895
|
+
indexAtOffset: (offset) => indexAtOffset(axis, offset)
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function computedToken(style, name, fallback) {
|
|
899
|
+
return style.getPropertyValue(name).trim() || fallback;
|
|
900
|
+
}
|
|
901
|
+
function computedPixels(style, name, fallback) {
|
|
902
|
+
const value = Number.parseFloat(style.getPropertyValue(name));
|
|
903
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
904
|
+
}
|
|
905
|
+
function spreadsheetCanvasTheme(element) {
|
|
906
|
+
const style = getComputedStyle(element);
|
|
907
|
+
const background = style.backgroundColor || computedToken(style, "--og-color-surface-1", "#ffffff");
|
|
908
|
+
const headerBackground = computedToken(style, "--og-color-surface-2", "#f3f4f6");
|
|
909
|
+
const border = computedToken(style, "--og-color-border", "#d1d5db");
|
|
910
|
+
const foreground = style.color || computedToken(style, "--og-color-fg", "#111827");
|
|
911
|
+
const mutedForeground = computedToken(style, "--og-color-fg-muted", "#6b7280");
|
|
912
|
+
const accent = computedToken(style, "--og-color-accent", "#3b82f6");
|
|
913
|
+
const error = computedToken(style, "--og-color-status-failed", "#dc2626");
|
|
914
|
+
const fontFamily = style.fontFamily || computedToken(
|
|
915
|
+
style,
|
|
916
|
+
"--og-font-sans",
|
|
917
|
+
'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
918
|
+
);
|
|
919
|
+
const fontSize = Number.parseFloat(style.fontSize) || computedPixels(style, "--og-font-size-sm", 12);
|
|
920
|
+
const headerFontSize = computedPixels(style, "--og-font-size-xs", 11);
|
|
921
|
+
return {
|
|
922
|
+
background,
|
|
923
|
+
headerBackground,
|
|
924
|
+
border,
|
|
925
|
+
foreground,
|
|
926
|
+
mutedForeground,
|
|
927
|
+
accent,
|
|
928
|
+
error,
|
|
929
|
+
fontFamily,
|
|
930
|
+
fontSize,
|
|
931
|
+
headerFontSize,
|
|
932
|
+
signature: [
|
|
933
|
+
background,
|
|
934
|
+
headerBackground,
|
|
935
|
+
border,
|
|
936
|
+
foreground,
|
|
937
|
+
mutedForeground,
|
|
938
|
+
accent,
|
|
939
|
+
error,
|
|
940
|
+
fontFamily,
|
|
941
|
+
fontSize,
|
|
942
|
+
headerFontSize
|
|
943
|
+
].join("")
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function useWorkbookViewState(workbook) {
|
|
947
|
+
const [view, setView] = useState(() => ({
|
|
948
|
+
revision: workbook.revision,
|
|
949
|
+
dimensionRevision: 0
|
|
950
|
+
}));
|
|
951
|
+
useEffect(() => {
|
|
952
|
+
setView(
|
|
953
|
+
(current) => current.revision === workbook.revision ? current : { revision: workbook.revision, dimensionRevision: current.dimensionRevision + 1 }
|
|
954
|
+
);
|
|
955
|
+
return workbook.onChange((change) => {
|
|
956
|
+
setView((current) => ({
|
|
957
|
+
revision: change.revision,
|
|
958
|
+
dimensionRevision: change.reason === "dimension" || change.reason === "structure" ? current.dimensionRevision + 1 : current.dimensionRevision
|
|
959
|
+
}));
|
|
960
|
+
});
|
|
961
|
+
}, [workbook]);
|
|
962
|
+
return view;
|
|
963
|
+
}
|
|
964
|
+
function nextAvailableSheetName(workbook) {
|
|
965
|
+
let index = workbook.worksheets.items.length + 1;
|
|
966
|
+
while (workbook.worksheets.items.some((sheet) => sheet.name === `Sheet${index}`)) index += 1;
|
|
967
|
+
return `Sheet${index}`;
|
|
968
|
+
}
|
|
969
|
+
function SpreadsheetArtifactSurface({
|
|
970
|
+
workbook,
|
|
971
|
+
title = "Workbook",
|
|
972
|
+
subtitle,
|
|
973
|
+
actions,
|
|
974
|
+
initialSheetId,
|
|
975
|
+
allowAddSheet = true,
|
|
976
|
+
readOnly = false,
|
|
977
|
+
...gridProps
|
|
978
|
+
}) {
|
|
979
|
+
const { revision } = useWorkbookViewState(workbook);
|
|
980
|
+
const availableSheets = workbook.worksheets.items;
|
|
981
|
+
const initial = (initialSheetId ? availableSheets.find((sheet) => sheet.id === initialSheetId) : void 0) ?? (() => {
|
|
982
|
+
try {
|
|
983
|
+
return workbook.worksheets.getActiveWorksheet();
|
|
984
|
+
} catch {
|
|
985
|
+
return availableSheets[0];
|
|
986
|
+
}
|
|
987
|
+
})();
|
|
988
|
+
const [activeSheetId, setActiveSheetId] = useState(initial?.id ?? null);
|
|
989
|
+
const activeSheet = availableSheets.find((sheet) => sheet.id === activeSheetId) ?? availableSheets[0] ?? null;
|
|
990
|
+
useEffect(() => {
|
|
991
|
+
if (!activeSheet && activeSheetId !== null) setActiveSheetId(null);
|
|
992
|
+
else if (activeSheet && activeSheet.id !== activeSheetId) setActiveSheetId(activeSheet.id);
|
|
993
|
+
}, [activeSheet, activeSheetId, revision]);
|
|
994
|
+
const selectSheet = useCallback(
|
|
995
|
+
(sheet) => {
|
|
996
|
+
workbook.worksheets.setActiveWorksheet(sheet);
|
|
997
|
+
setActiveSheetId(sheet.id);
|
|
998
|
+
},
|
|
999
|
+
[workbook]
|
|
1000
|
+
);
|
|
1001
|
+
const addSheet = useCallback(() => {
|
|
1002
|
+
const sheet = workbook.worksheets.add(nextAvailableSheetName(workbook));
|
|
1003
|
+
selectSheet(sheet);
|
|
1004
|
+
}, [selectSheet, workbook]);
|
|
1005
|
+
const footer = /* @__PURE__ */ jsxs(
|
|
1006
|
+
"div",
|
|
1007
|
+
{
|
|
1008
|
+
className: "flex min-h-9 items-center gap-1 overflow-x-auto px-2",
|
|
1009
|
+
role: "tablist",
|
|
1010
|
+
"aria-label": "Worksheets",
|
|
1011
|
+
children: [
|
|
1012
|
+
availableSheets.map((sheet) => /* @__PURE__ */ jsx(
|
|
1013
|
+
"button",
|
|
1014
|
+
{
|
|
1015
|
+
type: "button",
|
|
1016
|
+
role: "tab",
|
|
1017
|
+
"aria-selected": sheet.id === activeSheet?.id,
|
|
1018
|
+
onClick: () => selectSheet(sheet),
|
|
1019
|
+
className: cn(
|
|
1020
|
+
"h-7 shrink-0 rounded-og-sm px-2.5 text-og-sm transition-colors",
|
|
1021
|
+
sheet.id === activeSheet?.id ? "bg-og-surface-3 font-medium text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg"
|
|
1022
|
+
),
|
|
1023
|
+
children: sheet.name
|
|
1024
|
+
},
|
|
1025
|
+
sheet.id
|
|
1026
|
+
)),
|
|
1027
|
+
!readOnly && allowAddSheet ? /* @__PURE__ */ jsx(
|
|
1028
|
+
"button",
|
|
1029
|
+
{
|
|
1030
|
+
type: "button",
|
|
1031
|
+
onClick: addSheet,
|
|
1032
|
+
"aria-label": "Add worksheet",
|
|
1033
|
+
className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg [&>svg]:size-3.5",
|
|
1034
|
+
children: /* @__PURE__ */ jsx(PlusIcon, {})
|
|
1035
|
+
}
|
|
1036
|
+
) : null
|
|
1037
|
+
]
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
return /* @__PURE__ */ jsx(
|
|
1041
|
+
ArtifactSurface,
|
|
1042
|
+
{
|
|
1043
|
+
modality: "spreadsheet",
|
|
1044
|
+
title,
|
|
1045
|
+
subtitle: subtitle ?? (activeSheet ? `${availableSheets.length} sheet${availableSheets.length === 1 ? "" : "s"}` : "Empty workbook"),
|
|
1046
|
+
actions,
|
|
1047
|
+
footer,
|
|
1048
|
+
children: activeSheet ? /* @__PURE__ */ jsx(
|
|
1049
|
+
SpreadsheetGrid,
|
|
1050
|
+
{
|
|
1051
|
+
...gridProps,
|
|
1052
|
+
workbook,
|
|
1053
|
+
worksheet: activeSheet,
|
|
1054
|
+
readOnly
|
|
1055
|
+
},
|
|
1056
|
+
activeSheet.id
|
|
1057
|
+
) : /* @__PURE__ */ jsx(
|
|
1058
|
+
"div",
|
|
1059
|
+
{
|
|
1060
|
+
className: "grid h-full place-items-center bg-og-bg p-6 text-center",
|
|
1061
|
+
style: { minHeight: 224 },
|
|
1062
|
+
children: /* @__PURE__ */ jsxs("div", { children: [
|
|
1063
|
+
/* @__PURE__ */ jsx("p", { className: "text-og-base font-medium text-og-fg", children: "This workbook has no worksheets." }),
|
|
1064
|
+
!readOnly && allowAddSheet ? /* @__PURE__ */ jsx(
|
|
1065
|
+
"button",
|
|
1066
|
+
{
|
|
1067
|
+
type: "button",
|
|
1068
|
+
onClick: addSheet,
|
|
1069
|
+
className: "mt-3 rounded-og-sm bg-og-accent-deep px-3 py-1.5 text-og-sm font-medium text-og-accent-fg",
|
|
1070
|
+
children: "Add worksheet"
|
|
1071
|
+
}
|
|
1072
|
+
) : null
|
|
1073
|
+
] })
|
|
1074
|
+
}
|
|
1075
|
+
)
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
function SpreadsheetGrid({
|
|
1080
|
+
workbook,
|
|
1081
|
+
worksheet,
|
|
1082
|
+
readOnly = false,
|
|
1083
|
+
rowCount: requestedRowCount,
|
|
1084
|
+
columnCount: requestedColumnCount,
|
|
1085
|
+
overscanRows = 3,
|
|
1086
|
+
overscanColumns = 2,
|
|
1087
|
+
onSelectionChange,
|
|
1088
|
+
onCommit,
|
|
1089
|
+
onViewportChange,
|
|
1090
|
+
className
|
|
1091
|
+
}) {
|
|
1092
|
+
const { revision, dimensionRevision } = useWorkbookViewState(workbook);
|
|
1093
|
+
const boundedProjection = requestedRowCount !== void 0 || requestedColumnCount !== void 0;
|
|
1094
|
+
const usedRange = useMemo(() => {
|
|
1095
|
+
void revision;
|
|
1096
|
+
return boundedProjection ? worksheet.usedRangeAddress() : null;
|
|
1097
|
+
}, [boundedProjection, revision, worksheet]);
|
|
1098
|
+
const rowCount = boundedInteger(
|
|
1099
|
+
Math.max(
|
|
1100
|
+
requestedRowCount ?? MAX_ROWS,
|
|
1101
|
+
(usedRange?.row ?? 0) + (usedRange?.rowCount ?? 0) + 32
|
|
1102
|
+
),
|
|
1103
|
+
MAX_ROWS,
|
|
1104
|
+
MAX_ROWS
|
|
1105
|
+
);
|
|
1106
|
+
const columnCount = boundedInteger(
|
|
1107
|
+
Math.max(
|
|
1108
|
+
requestedColumnCount ?? MAX_COLUMNS,
|
|
1109
|
+
(usedRange?.col ?? 0) + (usedRange?.colCount ?? 0) + 8
|
|
1110
|
+
),
|
|
1111
|
+
MAX_COLUMNS,
|
|
1112
|
+
MAX_COLUMNS
|
|
1113
|
+
);
|
|
1114
|
+
const rowHeights = useMemo(() => {
|
|
1115
|
+
void dimensionRevision;
|
|
1116
|
+
return [...worksheet.rowHeightEntries()];
|
|
1117
|
+
}, [dimensionRevision, worksheet]);
|
|
1118
|
+
const columnWidths = useMemo(() => {
|
|
1119
|
+
void dimensionRevision;
|
|
1120
|
+
return [...worksheet.columnWidthEntries()];
|
|
1121
|
+
}, [dimensionRevision, worksheet]);
|
|
1122
|
+
const projection = useMemo(() => {
|
|
1123
|
+
void dimensionRevision;
|
|
1124
|
+
const cells = new SparseSpreadsheetCellIndex(
|
|
1125
|
+
function* projectedCells() {
|
|
1126
|
+
for (const { row, col, data } of worksheet.cellEntries()) {
|
|
1127
|
+
yield {
|
|
1128
|
+
row,
|
|
1129
|
+
col,
|
|
1130
|
+
value: data.value,
|
|
1131
|
+
formula: data.formula,
|
|
1132
|
+
format: data.format
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}()
|
|
1136
|
+
);
|
|
1137
|
+
return {
|
|
1138
|
+
sheetId: worksheet.id,
|
|
1139
|
+
sheetName: worksheet.name,
|
|
1140
|
+
generationId: worksheet.id,
|
|
1141
|
+
revision,
|
|
1142
|
+
dimensionRevision,
|
|
1143
|
+
rowCount,
|
|
1144
|
+
columnCount,
|
|
1145
|
+
defaultRowHeight: worksheet.defaultRowHeight,
|
|
1146
|
+
defaultColumnWidth: worksheet.defaultColumnWidth,
|
|
1147
|
+
rowHeights,
|
|
1148
|
+
columnWidths,
|
|
1149
|
+
usedRange,
|
|
1150
|
+
cells,
|
|
1151
|
+
valueAt: (cell) => cell.formula ? workbook.valueAt(worksheet, { row: cell.row, col: cell.col }) : cell.value,
|
|
1152
|
+
readCell: (row, col) => {
|
|
1153
|
+
const data = worksheet.cellData(row, col);
|
|
1154
|
+
return {
|
|
1155
|
+
value: data.formula ? workbook.valueAt(worksheet, { row, col }) : data.value,
|
|
1156
|
+
input: data.formula ?? displayValue2(data.value),
|
|
1157
|
+
format: data.format
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
}, [
|
|
1162
|
+
columnCount,
|
|
1163
|
+
columnWidths,
|
|
1164
|
+
dimensionRevision,
|
|
1165
|
+
revision,
|
|
1166
|
+
rowCount,
|
|
1167
|
+
rowHeights,
|
|
1168
|
+
usedRange,
|
|
1169
|
+
workbook,
|
|
1170
|
+
worksheet
|
|
1171
|
+
]);
|
|
1172
|
+
const commit = useCallback(
|
|
1173
|
+
(next) => {
|
|
1174
|
+
const target = worksheet.getCell(next.cell.row, next.cell.col);
|
|
1175
|
+
if (next.kind === "formula") target.formulas = [[next.input]];
|
|
1176
|
+
else target.values = [[parseCellInput(next.input)]];
|
|
1177
|
+
onCommit?.(next);
|
|
1178
|
+
},
|
|
1179
|
+
[onCommit, worksheet]
|
|
1180
|
+
);
|
|
1181
|
+
const commitRange = useCallback(
|
|
1182
|
+
(next) => {
|
|
1183
|
+
if (next.rows * next.columns !== next.inputs.length) {
|
|
1184
|
+
throw new Error("Spreadsheet paste shape is invalid");
|
|
1185
|
+
}
|
|
1186
|
+
workbook.transact(() => {
|
|
1187
|
+
for (let index = 0; index < next.inputs.length; index += 1) {
|
|
1188
|
+
const input = next.inputs[index];
|
|
1189
|
+
const row = next.anchor.row + Math.floor(index / next.columns);
|
|
1190
|
+
const col = next.anchor.col + index % next.columns;
|
|
1191
|
+
worksheet.setCell(
|
|
1192
|
+
row,
|
|
1193
|
+
col,
|
|
1194
|
+
input.startsWith("=") ? { formula: input, value: null } : { formula: null, value: parseCellInput(input) },
|
|
1195
|
+
"content"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
},
|
|
1200
|
+
[workbook, worksheet]
|
|
1201
|
+
);
|
|
1202
|
+
const clear = useCallback(
|
|
1203
|
+
(next) => {
|
|
1204
|
+
const normalized = normalizeSelection(next);
|
|
1205
|
+
worksheet.getRangeByIndexes(
|
|
1206
|
+
normalized.top,
|
|
1207
|
+
normalized.left,
|
|
1208
|
+
normalized.bottom - normalized.top + 1,
|
|
1209
|
+
normalized.right - normalized.left + 1
|
|
1210
|
+
).clear({ applyTo: "contents" });
|
|
1211
|
+
},
|
|
1212
|
+
[worksheet]
|
|
1213
|
+
);
|
|
1214
|
+
return /* @__PURE__ */ jsx(
|
|
1215
|
+
SpreadsheetProjectionGrid,
|
|
1216
|
+
{
|
|
1217
|
+
projection,
|
|
1218
|
+
readOnly,
|
|
1219
|
+
overscanRows,
|
|
1220
|
+
overscanColumns,
|
|
1221
|
+
onSelectionChange,
|
|
1222
|
+
commit,
|
|
1223
|
+
commitRange,
|
|
1224
|
+
clear,
|
|
1225
|
+
onViewportChange,
|
|
1226
|
+
className
|
|
1227
|
+
}
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
function SpreadsheetProjectionGrid({
|
|
1231
|
+
projection,
|
|
1232
|
+
readOnly = false,
|
|
1233
|
+
overscanRows = 3,
|
|
1234
|
+
overscanColumns = 2,
|
|
1235
|
+
onSelectionChange,
|
|
1236
|
+
commit,
|
|
1237
|
+
commitRange,
|
|
1238
|
+
clear,
|
|
1239
|
+
onCommandError,
|
|
1240
|
+
onViewportChange,
|
|
1241
|
+
className
|
|
1242
|
+
}) {
|
|
1243
|
+
const { revision, rowCount, columnCount } = projection;
|
|
1244
|
+
const dimensionRevision = projection.dimensionRevision ?? 0;
|
|
1245
|
+
const editable = !readOnly && commit !== void 0;
|
|
1246
|
+
const rows = useMemo(() => {
|
|
1247
|
+
void dimensionRevision;
|
|
1248
|
+
return buildAxis(rowCount, projection.defaultRowHeight ?? 24, projection.rowHeights ?? []);
|
|
1249
|
+
}, [dimensionRevision, projection.defaultRowHeight, projection.rowHeights, rowCount]);
|
|
1250
|
+
const columns = useMemo(() => {
|
|
1251
|
+
void dimensionRevision;
|
|
1252
|
+
return buildAxis(
|
|
1253
|
+
columnCount,
|
|
1254
|
+
projection.defaultColumnWidth ?? 96,
|
|
1255
|
+
projection.columnWidths ?? []
|
|
1256
|
+
);
|
|
1257
|
+
}, [columnCount, dimensionRevision, projection.columnWidths, projection.defaultColumnWidth]);
|
|
1258
|
+
const canvasRows = useMemo(() => canvasAxis(rows), [rows]);
|
|
1259
|
+
const canvasColumns = useMemo(() => canvasAxis(columns), [columns]);
|
|
1260
|
+
const [selection, setSelection] = useState(() => ({
|
|
1261
|
+
sheetId: projection.sheetId,
|
|
1262
|
+
anchor: { row: 0, col: 0 },
|
|
1263
|
+
focus: { row: 0, col: 0 }
|
|
1264
|
+
}));
|
|
1265
|
+
const [editMode, setEditMode] = useState(null);
|
|
1266
|
+
const [draft, setDraft] = useState(
|
|
1267
|
+
() => projection.readCell(selection.focus.row, selection.focus.col)?.input ?? ""
|
|
1268
|
+
);
|
|
1269
|
+
const [pendingCommands, setPendingCommands] = useState(0);
|
|
1270
|
+
const [commandFailure, setCommandFailure] = useState(null);
|
|
1271
|
+
const [viewport, setViewport] = useState({
|
|
1272
|
+
width: FALLBACK_VIEWPORT_WIDTH,
|
|
1273
|
+
height: FALLBACK_VIEWPORT_HEIGHT,
|
|
1274
|
+
scrollLeft: 0,
|
|
1275
|
+
scrollTop: 0,
|
|
1276
|
+
devicePixelRatio: 1
|
|
1277
|
+
});
|
|
1278
|
+
const [canvasReady, setCanvasReady] = useState(false);
|
|
1279
|
+
const [canvasEpoch, setCanvasEpoch] = useState(0);
|
|
1280
|
+
const viewportRef = useRef(null);
|
|
1281
|
+
const canvasRef = useRef(null);
|
|
1282
|
+
const editorRef = useRef(null);
|
|
1283
|
+
const selectionRef = useRef(selection);
|
|
1284
|
+
const selectingRef = useRef(null);
|
|
1285
|
+
const committingRef = useRef(false);
|
|
1286
|
+
const composingRef = useRef(false);
|
|
1287
|
+
const commitAfterCompositionRef = useRef(false);
|
|
1288
|
+
const selectDraftOnFocusRef = useRef(true);
|
|
1289
|
+
const initialDraftRef = useRef(draft);
|
|
1290
|
+
const optimisticInputRef = useRef(null);
|
|
1291
|
+
const mountedRef = useRef(true);
|
|
1292
|
+
const commandScopeRef = useRef(0);
|
|
1293
|
+
const latestCommandRef = useRef(0);
|
|
1294
|
+
const canvasRendererRef = useRef(null);
|
|
1295
|
+
if (!canvasRendererRef.current) canvasRendererRef.current = new SpreadsheetCanvasRenderer();
|
|
1296
|
+
const reactId = useId().replaceAll(":", "");
|
|
1297
|
+
const updateSelection = useCallback(
|
|
1298
|
+
(next) => {
|
|
1299
|
+
selectionRef.current = next;
|
|
1300
|
+
setSelection(next);
|
|
1301
|
+
onSelectionChange?.(next);
|
|
1302
|
+
},
|
|
1303
|
+
[onSelectionChange]
|
|
1304
|
+
);
|
|
1305
|
+
useEffect(() => {
|
|
1306
|
+
if (selection.sheetId === projection.sheetId) return;
|
|
1307
|
+
commandScopeRef.current += 1;
|
|
1308
|
+
latestCommandRef.current += 1;
|
|
1309
|
+
const next = {
|
|
1310
|
+
sheetId: projection.sheetId,
|
|
1311
|
+
anchor: { row: 0, col: 0 },
|
|
1312
|
+
focus: { row: 0, col: 0 }
|
|
1313
|
+
};
|
|
1314
|
+
optimisticInputRef.current = null;
|
|
1315
|
+
setPendingCommands(0);
|
|
1316
|
+
setCommandFailure(null);
|
|
1317
|
+
setEditMode(null);
|
|
1318
|
+
updateSelection(next);
|
|
1319
|
+
}, [projection.sheetId, selection.sheetId, updateSelection]);
|
|
1320
|
+
useEffect(() => {
|
|
1321
|
+
mountedRef.current = true;
|
|
1322
|
+
return () => {
|
|
1323
|
+
mountedRef.current = false;
|
|
1324
|
+
commandScopeRef.current += 1;
|
|
1325
|
+
latestCommandRef.current += 1;
|
|
1326
|
+
};
|
|
1327
|
+
}, []);
|
|
1328
|
+
useEffect(() => {
|
|
1329
|
+
if (editMode !== null) return;
|
|
1330
|
+
const optimistic = optimisticInputRef.current;
|
|
1331
|
+
if (optimistic?.sheetId === projection.sheetId && optimistic.row === selection.focus.row && optimistic.col === selection.focus.col && optimistic.baseRevision === revision) {
|
|
1332
|
+
setDraft(optimistic.input);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (optimistic && optimistic.baseRevision !== revision) optimisticInputRef.current = null;
|
|
1336
|
+
setDraft(projection.readCell(selection.focus.row, selection.focus.col)?.input ?? "");
|
|
1337
|
+
}, [editMode, projection, revision, selection.focus.col, selection.focus.row]);
|
|
1338
|
+
useClientLayoutEffect(() => {
|
|
1339
|
+
const element = viewportRef.current;
|
|
1340
|
+
if (!element) return;
|
|
1341
|
+
const measure = () => {
|
|
1342
|
+
setViewport((current) => ({
|
|
1343
|
+
...current,
|
|
1344
|
+
width: element.clientWidth || FALLBACK_VIEWPORT_WIDTH,
|
|
1345
|
+
height: element.clientHeight || FALLBACK_VIEWPORT_HEIGHT,
|
|
1346
|
+
devicePixelRatio: spreadsheetCanvasDevicePixelRatio(globalThis.devicePixelRatio || 1)
|
|
1347
|
+
}));
|
|
1348
|
+
};
|
|
1349
|
+
measure();
|
|
1350
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
|
|
1351
|
+
observer?.observe(element);
|
|
1352
|
+
globalThis.addEventListener?.("resize", measure);
|
|
1353
|
+
return () => {
|
|
1354
|
+
observer?.disconnect();
|
|
1355
|
+
globalThis.removeEventListener?.("resize", measure);
|
|
1356
|
+
};
|
|
1357
|
+
}, []);
|
|
1358
|
+
useEffect(() => {
|
|
1359
|
+
return () => canvasRendererRef.current?.dispose();
|
|
1360
|
+
}, []);
|
|
1361
|
+
useEffect(() => {
|
|
1362
|
+
if (typeof document === "undefined") return;
|
|
1363
|
+
let active = true;
|
|
1364
|
+
void document.fonts?.ready.then(() => {
|
|
1365
|
+
if (active) setCanvasEpoch((value) => value + 1);
|
|
1366
|
+
});
|
|
1367
|
+
if (typeof MutationObserver === "undefined") {
|
|
1368
|
+
return () => {
|
|
1369
|
+
active = false;
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const observer = new MutationObserver(() => setCanvasEpoch((value) => value + 1));
|
|
1373
|
+
let ancestor = viewportRef.current;
|
|
1374
|
+
while (ancestor) {
|
|
1375
|
+
observer.observe(ancestor, {
|
|
1376
|
+
attributes: true,
|
|
1377
|
+
attributeFilter: ["class", "style", "data-og-theme", "data-og-density"]
|
|
1378
|
+
});
|
|
1379
|
+
ancestor = ancestor.parentElement;
|
|
1380
|
+
}
|
|
1381
|
+
return () => {
|
|
1382
|
+
active = false;
|
|
1383
|
+
observer.disconnect();
|
|
1384
|
+
};
|
|
1385
|
+
}, []);
|
|
1386
|
+
useEffect(() => {
|
|
1387
|
+
if (editMode !== "cell") return;
|
|
1388
|
+
const editor = editorRef.current;
|
|
1389
|
+
editor?.focus();
|
|
1390
|
+
if (!editor) return;
|
|
1391
|
+
if (selectDraftOnFocusRef.current) editor.select();
|
|
1392
|
+
else editor.setSelectionRange(editor.value.length, editor.value.length);
|
|
1393
|
+
}, [editMode]);
|
|
1394
|
+
const ensureVisible = useCallback(
|
|
1395
|
+
(cell) => {
|
|
1396
|
+
const element = viewportRef.current;
|
|
1397
|
+
if (!element) return;
|
|
1398
|
+
const viewportWidth = element.clientWidth || FALLBACK_VIEWPORT_WIDTH;
|
|
1399
|
+
const viewportHeight = element.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
|
|
1400
|
+
const horizontalProjection2 = buildScrollProjection(columns, ROW_HEADER_WIDTH, viewportWidth);
|
|
1401
|
+
const verticalProjection2 = buildScrollProjection(rows, COLUMN_HEADER_HEIGHT, viewportHeight);
|
|
1402
|
+
const left = ROW_HEADER_WIDTH + axisOffsetAt(columns, cell.col);
|
|
1403
|
+
const right = left + axisSizeAt(columns, cell.col);
|
|
1404
|
+
const top = COLUMN_HEADER_HEIGHT + axisOffsetAt(rows, cell.row);
|
|
1405
|
+
const bottom = top + axisSizeAt(rows, cell.row);
|
|
1406
|
+
const logicalScrollLeft2 = physicalToLogicalScroll(element.scrollLeft, horizontalProjection2);
|
|
1407
|
+
const logicalScrollTop2 = physicalToLogicalScroll(element.scrollTop, verticalProjection2);
|
|
1408
|
+
const viewportLeft = logicalScrollLeft2 + ROW_HEADER_WIDTH;
|
|
1409
|
+
const viewportRight = logicalScrollLeft2 + viewportWidth;
|
|
1410
|
+
const viewportTop = logicalScrollTop2 + COLUMN_HEADER_HEIGHT;
|
|
1411
|
+
const viewportBottom = logicalScrollTop2 + viewportHeight;
|
|
1412
|
+
if (left < viewportLeft) {
|
|
1413
|
+
element.scrollLeft = logicalToPhysicalScroll(
|
|
1414
|
+
Math.max(0, left - ROW_HEADER_WIDTH),
|
|
1415
|
+
horizontalProjection2
|
|
1416
|
+
);
|
|
1417
|
+
} else if (right > viewportRight) {
|
|
1418
|
+
element.scrollLeft = logicalToPhysicalScroll(right - viewportWidth, horizontalProjection2);
|
|
1419
|
+
}
|
|
1420
|
+
if (top < viewportTop) {
|
|
1421
|
+
element.scrollTop = logicalToPhysicalScroll(
|
|
1422
|
+
Math.max(0, top - COLUMN_HEADER_HEIGHT),
|
|
1423
|
+
verticalProjection2
|
|
1424
|
+
);
|
|
1425
|
+
} else if (bottom > viewportBottom) {
|
|
1426
|
+
element.scrollTop = logicalToPhysicalScroll(bottom - viewportHeight, verticalProjection2);
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
[columns, rows]
|
|
1430
|
+
);
|
|
1431
|
+
const moveSelection = useCallback(
|
|
1432
|
+
(rowDelta, colDelta, extend) => {
|
|
1433
|
+
const focus = {
|
|
1434
|
+
row: Math.max(0, Math.min(rowCount - 1, selection.focus.row + rowDelta)),
|
|
1435
|
+
col: Math.max(0, Math.min(columnCount - 1, selection.focus.col + colDelta))
|
|
1436
|
+
};
|
|
1437
|
+
const next = {
|
|
1438
|
+
sheetId: projection.sheetId,
|
|
1439
|
+
anchor: extend ? selection.anchor : focus,
|
|
1440
|
+
focus
|
|
1441
|
+
};
|
|
1442
|
+
updateSelection(next);
|
|
1443
|
+
ensureVisible(focus);
|
|
1444
|
+
},
|
|
1445
|
+
[columnCount, ensureVisible, projection.sheetId, rowCount, selection, updateSelection]
|
|
1446
|
+
);
|
|
1447
|
+
const runCommand = useCallback(
|
|
1448
|
+
(invoke, retry, rollback) => {
|
|
1449
|
+
const commandId = latestCommandRef.current + 1;
|
|
1450
|
+
latestCommandRef.current = commandId;
|
|
1451
|
+
const scope = commandScopeRef.current;
|
|
1452
|
+
setCommandFailure(null);
|
|
1453
|
+
const fail = (cause) => {
|
|
1454
|
+
if (!mountedRef.current || commandScopeRef.current !== scope || latestCommandRef.current !== commandId) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1458
|
+
rollback();
|
|
1459
|
+
setCommandFailure({ message: error.message || "Spreadsheet change failed", retry });
|
|
1460
|
+
onCommandError?.(error);
|
|
1461
|
+
};
|
|
1462
|
+
let result;
|
|
1463
|
+
try {
|
|
1464
|
+
result = invoke();
|
|
1465
|
+
} catch (cause) {
|
|
1466
|
+
fail(cause);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (!result || typeof result.then !== "function") return;
|
|
1470
|
+
setPendingCommands((current) => current + 1);
|
|
1471
|
+
void Promise.resolve(result).then(
|
|
1472
|
+
() => {
|
|
1473
|
+
if (!mountedRef.current || commandScopeRef.current !== scope) return;
|
|
1474
|
+
setPendingCommands((current) => Math.max(0, current - 1));
|
|
1475
|
+
},
|
|
1476
|
+
(cause) => {
|
|
1477
|
+
if (!mountedRef.current || commandScopeRef.current !== scope) return;
|
|
1478
|
+
setPendingCommands((current) => Math.max(0, current - 1));
|
|
1479
|
+
fail(cause);
|
|
1480
|
+
}
|
|
1481
|
+
);
|
|
1482
|
+
},
|
|
1483
|
+
[onCommandError]
|
|
1484
|
+
);
|
|
1485
|
+
const reportCommandFailure = useCallback(
|
|
1486
|
+
(cause) => {
|
|
1487
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1488
|
+
setCommandFailure({ message: error.message || "Spreadsheet action failed" });
|
|
1489
|
+
onCommandError?.(error);
|
|
1490
|
+
},
|
|
1491
|
+
[onCommandError]
|
|
1492
|
+
);
|
|
1493
|
+
const commitDraft = useCallback(
|
|
1494
|
+
(input = draft) => {
|
|
1495
|
+
if (committingRef.current) return;
|
|
1496
|
+
committingRef.current = true;
|
|
1497
|
+
if (!editable || !commit) {
|
|
1498
|
+
setEditMode(null);
|
|
1499
|
+
queueMicrotask(() => {
|
|
1500
|
+
committingRef.current = false;
|
|
1501
|
+
});
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (input === initialDraftRef.current) {
|
|
1505
|
+
setEditMode(null);
|
|
1506
|
+
viewportRef.current?.focus();
|
|
1507
|
+
queueMicrotask(() => {
|
|
1508
|
+
committingRef.current = false;
|
|
1509
|
+
});
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const next = {
|
|
1513
|
+
sheetId: projection.sheetId,
|
|
1514
|
+
cell: { ...selection.focus },
|
|
1515
|
+
input,
|
|
1516
|
+
kind: input.startsWith("=") ? "formula" : "value"
|
|
1517
|
+
};
|
|
1518
|
+
const execute = () => {
|
|
1519
|
+
optimisticInputRef.current = {
|
|
1520
|
+
sheetId: projection.sheetId,
|
|
1521
|
+
row: selection.focus.row,
|
|
1522
|
+
col: selection.focus.col,
|
|
1523
|
+
input,
|
|
1524
|
+
baseRevision: revision
|
|
1525
|
+
};
|
|
1526
|
+
setDraft(input);
|
|
1527
|
+
runCommand(
|
|
1528
|
+
() => commit(next),
|
|
1529
|
+
execute,
|
|
1530
|
+
() => {
|
|
1531
|
+
optimisticInputRef.current = null;
|
|
1532
|
+
const current = selectionRef.current.focus;
|
|
1533
|
+
setDraft(projection.readCell(current.row, current.col)?.input ?? "");
|
|
1534
|
+
}
|
|
1535
|
+
);
|
|
1536
|
+
};
|
|
1537
|
+
execute();
|
|
1538
|
+
initialDraftRef.current = input;
|
|
1539
|
+
setEditMode(null);
|
|
1540
|
+
viewportRef.current?.focus();
|
|
1541
|
+
queueMicrotask(() => {
|
|
1542
|
+
committingRef.current = false;
|
|
1543
|
+
});
|
|
1544
|
+
},
|
|
1545
|
+
[commit, draft, editable, projection, revision, runCommand, selection.focus]
|
|
1546
|
+
);
|
|
1547
|
+
const clearSelection = useCallback(() => {
|
|
1548
|
+
if (!editable || !clear) return;
|
|
1549
|
+
const current = selection;
|
|
1550
|
+
const execute = () => runCommand(
|
|
1551
|
+
() => clear(current),
|
|
1552
|
+
execute,
|
|
1553
|
+
() => {
|
|
1554
|
+
}
|
|
1555
|
+
);
|
|
1556
|
+
execute();
|
|
1557
|
+
}, [clear, editable, runCommand, selection]);
|
|
1558
|
+
const pasteClipboard = useCallback(
|
|
1559
|
+
(event) => {
|
|
1560
|
+
if (!editable || editMode !== null) return;
|
|
1561
|
+
const clipboardText = event.clipboardData.getData("text/plain");
|
|
1562
|
+
if (!clipboardText && !event.clipboardData.types.includes("text/plain")) return;
|
|
1563
|
+
event.preventDefault();
|
|
1564
|
+
const execute = () => {
|
|
1565
|
+
const previousSelection = selectionRef.current;
|
|
1566
|
+
const invoke = () => {
|
|
1567
|
+
const parsed = parseSpreadsheetClipboard(clipboardText);
|
|
1568
|
+
const anchor = previousSelection.focus;
|
|
1569
|
+
if (anchor.row + parsed.rows > rowCount || anchor.col + parsed.columns > columnCount) {
|
|
1570
|
+
throw new Error("Clipboard range does not fit in this worksheet");
|
|
1571
|
+
}
|
|
1572
|
+
if (!commitRange && parsed.inputs.length !== 1) {
|
|
1573
|
+
throw new Error("This spreadsheet adapter does not support rectangular paste");
|
|
1574
|
+
}
|
|
1575
|
+
const bottomRight = {
|
|
1576
|
+
row: anchor.row + parsed.rows - 1,
|
|
1577
|
+
col: anchor.col + parsed.columns - 1
|
|
1578
|
+
};
|
|
1579
|
+
updateSelection({
|
|
1580
|
+
sheetId: projection.sheetId,
|
|
1581
|
+
anchor: { ...anchor },
|
|
1582
|
+
focus: bottomRight
|
|
1583
|
+
});
|
|
1584
|
+
ensureVisible(bottomRight);
|
|
1585
|
+
if (commitRange) {
|
|
1586
|
+
return commitRange({
|
|
1587
|
+
sheetId: projection.sheetId,
|
|
1588
|
+
anchor: { ...anchor },
|
|
1589
|
+
rows: parsed.rows,
|
|
1590
|
+
columns: parsed.columns,
|
|
1591
|
+
inputs: parsed.inputs
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
const input = parsed.inputs[0];
|
|
1595
|
+
return commit({
|
|
1596
|
+
sheetId: projection.sheetId,
|
|
1597
|
+
cell: { ...anchor },
|
|
1598
|
+
input,
|
|
1599
|
+
kind: input.startsWith("=") ? "formula" : "value"
|
|
1600
|
+
});
|
|
1601
|
+
};
|
|
1602
|
+
runCommand(invoke, execute, () => updateSelection(previousSelection));
|
|
1603
|
+
};
|
|
1604
|
+
execute();
|
|
1605
|
+
},
|
|
1606
|
+
[
|
|
1607
|
+
columnCount,
|
|
1608
|
+
commit,
|
|
1609
|
+
commitRange,
|
|
1610
|
+
editMode,
|
|
1611
|
+
editable,
|
|
1612
|
+
ensureVisible,
|
|
1613
|
+
projection.sheetId,
|
|
1614
|
+
rowCount,
|
|
1615
|
+
runCommand,
|
|
1616
|
+
updateSelection
|
|
1617
|
+
]
|
|
1618
|
+
);
|
|
1619
|
+
const copySelection = useCallback(
|
|
1620
|
+
(event) => {
|
|
1621
|
+
if (editMode !== null) return;
|
|
1622
|
+
const selected = normalizeSelection(selectionRef.current);
|
|
1623
|
+
const rowsToCopy = selected.bottom - selected.top + 1;
|
|
1624
|
+
const columnsToCopy = selected.right - selected.left + 1;
|
|
1625
|
+
if (rowsToCopy * columnsToCopy > MAX_CLIPBOARD_CELLS) {
|
|
1626
|
+
event.preventDefault();
|
|
1627
|
+
reportCommandFailure(new Error("Selected range is too large to copy"));
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const coverage = projection.coverage;
|
|
1631
|
+
if (coverage && (selected.top < coverage.rowStart || selected.bottom >= coverage.rowEnd || selected.left < coverage.columnStart || selected.right >= coverage.columnEnd)) {
|
|
1632
|
+
event.preventDefault();
|
|
1633
|
+
reportCommandFailure(new Error("Selected cells are still loading; copy again when ready"));
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const output = [];
|
|
1637
|
+
for (let row = selected.top; row <= selected.bottom; row += 1) {
|
|
1638
|
+
const fields = [];
|
|
1639
|
+
for (let col = selected.left; col <= selected.right; col += 1) {
|
|
1640
|
+
fields.push(escapeSpreadsheetClipboardField(projection.readCell(row, col)?.input ?? ""));
|
|
1641
|
+
}
|
|
1642
|
+
output.push(fields.join(" "));
|
|
1643
|
+
}
|
|
1644
|
+
event.clipboardData.setData("text/plain", output.join("\r\n"));
|
|
1645
|
+
event.preventDefault();
|
|
1646
|
+
setCommandFailure(null);
|
|
1647
|
+
},
|
|
1648
|
+
[editMode, projection, reportCommandFailure]
|
|
1649
|
+
);
|
|
1650
|
+
const beginEditing = useCallback(
|
|
1651
|
+
(initial, cell = selectionRef.current.focus) => {
|
|
1652
|
+
if (!editable) return;
|
|
1653
|
+
ensureVisible(cell);
|
|
1654
|
+
const current = projection.readCell(cell.row, cell.col)?.input ?? "";
|
|
1655
|
+
initialDraftRef.current = current;
|
|
1656
|
+
selectDraftOnFocusRef.current = initial === void 0;
|
|
1657
|
+
setDraft(initial ?? current);
|
|
1658
|
+
setEditMode("cell");
|
|
1659
|
+
},
|
|
1660
|
+
[editable, ensureVisible, projection]
|
|
1661
|
+
);
|
|
1662
|
+
const onGridKeyDown = useCallback(
|
|
1663
|
+
(event) => {
|
|
1664
|
+
if (editMode !== null) return;
|
|
1665
|
+
if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
|
|
1666
|
+
switch (event.key) {
|
|
1667
|
+
case "ArrowUp":
|
|
1668
|
+
event.preventDefault();
|
|
1669
|
+
moveSelection(-1, 0, event.shiftKey);
|
|
1670
|
+
return;
|
|
1671
|
+
case "ArrowDown":
|
|
1672
|
+
event.preventDefault();
|
|
1673
|
+
moveSelection(1, 0, event.shiftKey);
|
|
1674
|
+
return;
|
|
1675
|
+
case "ArrowLeft":
|
|
1676
|
+
event.preventDefault();
|
|
1677
|
+
moveSelection(0, -1, event.shiftKey);
|
|
1678
|
+
return;
|
|
1679
|
+
case "ArrowRight":
|
|
1680
|
+
event.preventDefault();
|
|
1681
|
+
moveSelection(0, 1, event.shiftKey);
|
|
1682
|
+
return;
|
|
1683
|
+
case "Tab":
|
|
1684
|
+
event.preventDefault();
|
|
1685
|
+
moveSelection(0, event.shiftKey ? -1 : 1, false);
|
|
1686
|
+
return;
|
|
1687
|
+
case "Enter":
|
|
1688
|
+
case "F2":
|
|
1689
|
+
event.preventDefault();
|
|
1690
|
+
beginEditing();
|
|
1691
|
+
return;
|
|
1692
|
+
case "Backspace":
|
|
1693
|
+
case "Delete":
|
|
1694
|
+
event.preventDefault();
|
|
1695
|
+
clearSelection();
|
|
1696
|
+
return;
|
|
1697
|
+
case "Home": {
|
|
1698
|
+
event.preventDefault();
|
|
1699
|
+
const target = event.metaKey || event.ctrlKey ? { row: 0, col: 0 } : { row: selection.focus.row, col: 0 };
|
|
1700
|
+
updateSelection({ sheetId: projection.sheetId, anchor: target, focus: target });
|
|
1701
|
+
ensureVisible(target);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
case "End": {
|
|
1705
|
+
event.preventDefault();
|
|
1706
|
+
const used = projection.usedRange;
|
|
1707
|
+
const target = event.metaKey || event.ctrlKey ? {
|
|
1708
|
+
row: Math.min(rowCount - 1, (used?.row ?? 0) + (used?.rowCount ?? 1) - 1),
|
|
1709
|
+
col: Math.min(columnCount - 1, (used?.col ?? 0) + (used?.colCount ?? 1) - 1)
|
|
1710
|
+
} : { row: selection.focus.row, col: columnCount - 1 };
|
|
1711
|
+
updateSelection({ sheetId: projection.sheetId, anchor: target, focus: target });
|
|
1712
|
+
ensureVisible(target);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (editable && !event.metaKey && !event.ctrlKey && !event.altKey && event.key.length === 1) {
|
|
1717
|
+
event.preventDefault();
|
|
1718
|
+
beginEditing(event.key);
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
[
|
|
1722
|
+
beginEditing,
|
|
1723
|
+
clearSelection,
|
|
1724
|
+
columnCount,
|
|
1725
|
+
editMode,
|
|
1726
|
+
ensureVisible,
|
|
1727
|
+
moveSelection,
|
|
1728
|
+
editable,
|
|
1729
|
+
rowCount,
|
|
1730
|
+
selection.focus.row,
|
|
1731
|
+
updateSelection,
|
|
1732
|
+
projection.sheetId,
|
|
1733
|
+
projection.usedRange
|
|
1734
|
+
]
|
|
1735
|
+
);
|
|
1736
|
+
const onEditorKeyDown = useCallback(
|
|
1737
|
+
(event) => {
|
|
1738
|
+
if (composingRef.current || event.nativeEvent.isComposing) return;
|
|
1739
|
+
if (event.key === "Enter") {
|
|
1740
|
+
event.preventDefault();
|
|
1741
|
+
commitDraft();
|
|
1742
|
+
} else if (event.key === "Escape") {
|
|
1743
|
+
event.preventDefault();
|
|
1744
|
+
committingRef.current = true;
|
|
1745
|
+
setDraft(projection.readCell(selection.focus.row, selection.focus.col)?.input ?? "");
|
|
1746
|
+
setEditMode(null);
|
|
1747
|
+
viewportRef.current?.focus();
|
|
1748
|
+
queueMicrotask(() => {
|
|
1749
|
+
committingRef.current = false;
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
[commitDraft, projection, selection.focus.col, selection.focus.row]
|
|
1754
|
+
);
|
|
1755
|
+
const onEditorCompositionStart = useCallback(() => {
|
|
1756
|
+
composingRef.current = true;
|
|
1757
|
+
}, []);
|
|
1758
|
+
const onEditorCompositionEnd = useCallback(
|
|
1759
|
+
(input) => {
|
|
1760
|
+
composingRef.current = false;
|
|
1761
|
+
if (!commitAfterCompositionRef.current) return;
|
|
1762
|
+
commitAfterCompositionRef.current = false;
|
|
1763
|
+
commitDraft(input);
|
|
1764
|
+
},
|
|
1765
|
+
[commitDraft]
|
|
1766
|
+
);
|
|
1767
|
+
const commitEditorOnBlur = useCallback(
|
|
1768
|
+
(mode, input) => {
|
|
1769
|
+
if (editMode !== mode) return;
|
|
1770
|
+
if (composingRef.current) {
|
|
1771
|
+
commitAfterCompositionRef.current = true;
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
commitDraft(input);
|
|
1775
|
+
},
|
|
1776
|
+
[commitDraft, editMode]
|
|
1777
|
+
);
|
|
1778
|
+
const selectCell = useCallback(
|
|
1779
|
+
(cell, extend) => {
|
|
1780
|
+
const current = selectionRef.current;
|
|
1781
|
+
const next = {
|
|
1782
|
+
sheetId: projection.sheetId,
|
|
1783
|
+
anchor: extend ? current.anchor : cell,
|
|
1784
|
+
focus: cell
|
|
1785
|
+
};
|
|
1786
|
+
updateSelection(next);
|
|
1787
|
+
setEditMode(null);
|
|
1788
|
+
viewportRef.current?.focus();
|
|
1789
|
+
},
|
|
1790
|
+
[projection.sheetId, updateSelection]
|
|
1791
|
+
);
|
|
1792
|
+
const hitTestCell = useCallback(
|
|
1793
|
+
(clientX, clientY) => {
|
|
1794
|
+
const element = viewportRef.current;
|
|
1795
|
+
if (!element) return null;
|
|
1796
|
+
const bounds = element.getBoundingClientRect();
|
|
1797
|
+
const viewportX = clientX - bounds.left;
|
|
1798
|
+
const viewportY = clientY - bounds.top;
|
|
1799
|
+
const viewportWidth = element.clientWidth || FALLBACK_VIEWPORT_WIDTH;
|
|
1800
|
+
const viewportHeight = element.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
|
|
1801
|
+
if (viewportX < ROW_HEADER_WIDTH || viewportY < COLUMN_HEADER_HEIGHT || viewportX >= viewportWidth || viewportY >= viewportHeight) {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
const horizontalProjection2 = buildScrollProjection(columns, ROW_HEADER_WIDTH, viewportWidth);
|
|
1805
|
+
const verticalProjection2 = buildScrollProjection(rows, COLUMN_HEADER_HEIGHT, viewportHeight);
|
|
1806
|
+
const logicalX = physicalToLogicalScroll(element.scrollLeft, horizontalProjection2) + viewportX - ROW_HEADER_WIDTH;
|
|
1807
|
+
const logicalY = physicalToLogicalScroll(element.scrollTop, verticalProjection2) + viewportY - COLUMN_HEADER_HEIGHT;
|
|
1808
|
+
if (logicalX < 0 || logicalY < 0 || logicalX >= columns.total || logicalY >= rows.total) {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
row: Math.min(rowCount - 1, indexAtOffset(rows, logicalY)),
|
|
1813
|
+
col: Math.min(columnCount - 1, indexAtOffset(columns, logicalX))
|
|
1814
|
+
};
|
|
1815
|
+
},
|
|
1816
|
+
[columnCount, columns, rowCount, rows]
|
|
1817
|
+
);
|
|
1818
|
+
const onGridPointerDown = useCallback(
|
|
1819
|
+
(event) => {
|
|
1820
|
+
if (event.button !== 0) return;
|
|
1821
|
+
const cell = hitTestCell(event.clientX, event.clientY);
|
|
1822
|
+
if (!cell) return;
|
|
1823
|
+
event.preventDefault();
|
|
1824
|
+
const current = selectionRef.current;
|
|
1825
|
+
const anchor = event.shiftKey ? current.anchor : cell;
|
|
1826
|
+
selectingRef.current = { pointerId: event.pointerId, anchor };
|
|
1827
|
+
try {
|
|
1828
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
updateSelection({ sheetId: projection.sheetId, anchor, focus: cell });
|
|
1832
|
+
setEditMode(null);
|
|
1833
|
+
event.currentTarget.focus();
|
|
1834
|
+
},
|
|
1835
|
+
[hitTestCell, projection.sheetId, updateSelection]
|
|
1836
|
+
);
|
|
1837
|
+
const onGridPointerMove = useCallback(
|
|
1838
|
+
(event) => {
|
|
1839
|
+
const selecting = selectingRef.current;
|
|
1840
|
+
if (!selecting || selecting.pointerId !== event.pointerId) return;
|
|
1841
|
+
const cell = hitTestCell(event.clientX, event.clientY);
|
|
1842
|
+
if (!cell) return;
|
|
1843
|
+
const current = selectionRef.current;
|
|
1844
|
+
if (current.focus.row === cell.row && current.focus.col === cell.col) return;
|
|
1845
|
+
updateSelection({ sheetId: projection.sheetId, anchor: selecting.anchor, focus: cell });
|
|
1846
|
+
},
|
|
1847
|
+
[hitTestCell, projection.sheetId, updateSelection]
|
|
1848
|
+
);
|
|
1849
|
+
const stopPointerSelection = useCallback((pointerId) => {
|
|
1850
|
+
if (pointerId !== void 0 && selectingRef.current?.pointerId !== pointerId) return;
|
|
1851
|
+
selectingRef.current = null;
|
|
1852
|
+
}, []);
|
|
1853
|
+
useEffect(() => {
|
|
1854
|
+
if (typeof window === "undefined") return;
|
|
1855
|
+
const stopSelecting = () => stopPointerSelection();
|
|
1856
|
+
window.addEventListener("pointerup", stopSelecting);
|
|
1857
|
+
return () => window.removeEventListener("pointerup", stopSelecting);
|
|
1858
|
+
}, [stopPointerSelection]);
|
|
1859
|
+
const horizontalProjection = buildScrollProjection(columns, ROW_HEADER_WIDTH, viewport.width);
|
|
1860
|
+
const verticalProjection = buildScrollProjection(rows, COLUMN_HEADER_HEIGHT, viewport.height);
|
|
1861
|
+
const physicalScrollLeft = clampPhysicalScroll(viewport.scrollLeft, horizontalProjection);
|
|
1862
|
+
const physicalScrollTop = clampPhysicalScroll(viewport.scrollTop, verticalProjection);
|
|
1863
|
+
const logicalScrollLeft = physicalToLogicalScroll(viewport.scrollLeft, horizontalProjection);
|
|
1864
|
+
const logicalScrollTop = physicalToLogicalScroll(viewport.scrollTop, verticalProjection);
|
|
1865
|
+
const visibleRowWindow = visibleInterval(
|
|
1866
|
+
rows,
|
|
1867
|
+
logicalScrollTop,
|
|
1868
|
+
Math.max(1, viewport.height - COLUMN_HEADER_HEIGHT),
|
|
1869
|
+
0
|
|
1870
|
+
);
|
|
1871
|
+
const visibleColumnWindow = visibleInterval(
|
|
1872
|
+
columns,
|
|
1873
|
+
logicalScrollLeft,
|
|
1874
|
+
Math.max(1, viewport.width - ROW_HEADER_WIDTH),
|
|
1875
|
+
0
|
|
1876
|
+
);
|
|
1877
|
+
const projectedRowWindow = visibleInterval(
|
|
1878
|
+
rows,
|
|
1879
|
+
logicalScrollTop,
|
|
1880
|
+
Math.max(1, viewport.height - COLUMN_HEADER_HEIGHT),
|
|
1881
|
+
Math.max(0, overscanRows)
|
|
1882
|
+
);
|
|
1883
|
+
const projectedColumnWindow = visibleInterval(
|
|
1884
|
+
columns,
|
|
1885
|
+
logicalScrollLeft,
|
|
1886
|
+
Math.max(1, viewport.width - ROW_HEADER_WIDTH),
|
|
1887
|
+
Math.max(0, overscanColumns)
|
|
1888
|
+
);
|
|
1889
|
+
const { rows: rowWindow, columns: columnWindow } = boundVisibleWindows(
|
|
1890
|
+
projectedRowWindow,
|
|
1891
|
+
projectedColumnWindow,
|
|
1892
|
+
selection.focus
|
|
1893
|
+
);
|
|
1894
|
+
const visibleRows = indexesBetween(rowWindow.start, rowWindow.end);
|
|
1895
|
+
const visibleColumns = indexesBetween(columnWindow.start, columnWindow.end);
|
|
1896
|
+
const rowLayout = visibleRows.map((row) => ({
|
|
1897
|
+
row,
|
|
1898
|
+
top: physicalScrollTop + COLUMN_HEADER_HEIGHT + axisOffsetAt(rows, row) - logicalScrollTop,
|
|
1899
|
+
height: axisSizeAt(rows, row)
|
|
1900
|
+
}));
|
|
1901
|
+
const columnLayout = visibleColumns.map((col) => ({
|
|
1902
|
+
col,
|
|
1903
|
+
left: physicalScrollLeft + ROW_HEADER_WIDTH + axisOffsetAt(columns, col) - logicalScrollLeft,
|
|
1904
|
+
width: axisSizeAt(columns, col)
|
|
1905
|
+
}));
|
|
1906
|
+
const activeCellVisible = selection.focus.row >= rowWindow.start && selection.focus.row < rowWindow.end && selection.focus.col >= columnWindow.start && selection.focus.col < columnWindow.end;
|
|
1907
|
+
const activeCellId = `${reactId}-cell-${selection.focus.row}-${selection.focus.col}`;
|
|
1908
|
+
const hiddenActiveValue = activeCellVisible ? null : projection.readCell(selection.focus.row, selection.focus.col)?.value;
|
|
1909
|
+
useEffect(() => {
|
|
1910
|
+
if (!onViewportChange) return;
|
|
1911
|
+
const next = {
|
|
1912
|
+
sheetId: projection.sheetId,
|
|
1913
|
+
rowStart: visibleRowWindow.start,
|
|
1914
|
+
rowEnd: visibleRowWindow.end,
|
|
1915
|
+
columnStart: visibleColumnWindow.start,
|
|
1916
|
+
columnEnd: visibleColumnWindow.end,
|
|
1917
|
+
overscanRowStart: projectedRowWindow.start,
|
|
1918
|
+
overscanRowEnd: projectedRowWindow.end,
|
|
1919
|
+
overscanColumnStart: projectedColumnWindow.start,
|
|
1920
|
+
overscanColumnEnd: projectedColumnWindow.end,
|
|
1921
|
+
logicalScrollLeft,
|
|
1922
|
+
logicalScrollTop,
|
|
1923
|
+
viewportWidth: viewport.width,
|
|
1924
|
+
viewportHeight: viewport.height
|
|
1925
|
+
};
|
|
1926
|
+
let active = true;
|
|
1927
|
+
const notify = () => {
|
|
1928
|
+
if (active) onViewportChange(next);
|
|
1929
|
+
};
|
|
1930
|
+
if (typeof globalThis.requestAnimationFrame === "function") {
|
|
1931
|
+
const frame = globalThis.requestAnimationFrame(notify);
|
|
1932
|
+
return () => {
|
|
1933
|
+
active = false;
|
|
1934
|
+
globalThis.cancelAnimationFrame?.(frame);
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
queueMicrotask(notify);
|
|
1938
|
+
return () => {
|
|
1939
|
+
active = false;
|
|
1940
|
+
};
|
|
1941
|
+
}, [
|
|
1942
|
+
logicalScrollLeft,
|
|
1943
|
+
logicalScrollTop,
|
|
1944
|
+
onViewportChange,
|
|
1945
|
+
projectedColumnWindow.end,
|
|
1946
|
+
projectedColumnWindow.start,
|
|
1947
|
+
projectedRowWindow.end,
|
|
1948
|
+
projectedRowWindow.start,
|
|
1949
|
+
viewport.height,
|
|
1950
|
+
viewport.width,
|
|
1951
|
+
visibleColumnWindow.end,
|
|
1952
|
+
visibleColumnWindow.start,
|
|
1953
|
+
visibleRowWindow.end,
|
|
1954
|
+
visibleRowWindow.start,
|
|
1955
|
+
projection.sheetId
|
|
1956
|
+
]);
|
|
1957
|
+
useClientLayoutEffect(() => {
|
|
1958
|
+
void canvasEpoch;
|
|
1959
|
+
const canvas = canvasRef.current;
|
|
1960
|
+
const viewportElement = viewportRef.current;
|
|
1961
|
+
const renderer = canvasRendererRef.current;
|
|
1962
|
+
if (!canvas || !viewportElement || !renderer) return;
|
|
1963
|
+
const normalized = normalizeSelection(selection);
|
|
1964
|
+
const theme = spreadsheetCanvasTheme(viewportElement);
|
|
1965
|
+
const paintStartedAt = performance.now();
|
|
1966
|
+
const stats = renderer.paint({
|
|
1967
|
+
canvas,
|
|
1968
|
+
projection,
|
|
1969
|
+
rows: canvasRows,
|
|
1970
|
+
columns: canvasColumns,
|
|
1971
|
+
selection: {
|
|
1972
|
+
...normalized,
|
|
1973
|
+
focusRow: selection.focus.row,
|
|
1974
|
+
focusColumn: selection.focus.col
|
|
1975
|
+
},
|
|
1976
|
+
logicalScrollLeft,
|
|
1977
|
+
logicalScrollTop,
|
|
1978
|
+
viewportWidth: viewport.width,
|
|
1979
|
+
viewportHeight: viewport.height,
|
|
1980
|
+
rowHeaderWidth: ROW_HEADER_WIDTH,
|
|
1981
|
+
columnHeaderHeight: COLUMN_HEADER_HEIGHT,
|
|
1982
|
+
dimensionRevision,
|
|
1983
|
+
devicePixelRatio: viewport.devicePixelRatio,
|
|
1984
|
+
theme
|
|
1985
|
+
});
|
|
1986
|
+
const paintDurationMs = performance.now() - paintStartedAt;
|
|
1987
|
+
if (!stats) {
|
|
1988
|
+
if (canvasReady) setCanvasReady(false);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
canvas.dataset.ogTileCacheSize = String(stats.cacheSize);
|
|
1992
|
+
canvas.dataset.ogPaintedTiles = String(stats.paintedTiles);
|
|
1993
|
+
canvas.dataset.ogReusedTiles = String(stats.reusedTiles);
|
|
1994
|
+
canvas.dataset.ogUncachedTiles = String(stats.uncachedTiles);
|
|
1995
|
+
canvas.dataset.ogDevicePixelRatio = String(stats.devicePixelRatio);
|
|
1996
|
+
canvas.dataset.ogLogicalScrollLeft = String(logicalScrollLeft);
|
|
1997
|
+
canvas.dataset.ogLogicalScrollTop = String(logicalScrollTop);
|
|
1998
|
+
canvas.dataset.ogPaintDurationMs = String(paintDurationMs);
|
|
1999
|
+
if (!canvasReady) setCanvasReady(true);
|
|
2000
|
+
}, [
|
|
2001
|
+
canvasColumns,
|
|
2002
|
+
canvasEpoch,
|
|
2003
|
+
canvasReady,
|
|
2004
|
+
canvasRows,
|
|
2005
|
+
dimensionRevision,
|
|
2006
|
+
logicalScrollLeft,
|
|
2007
|
+
logicalScrollTop,
|
|
2008
|
+
projection,
|
|
2009
|
+
revision,
|
|
2010
|
+
selection,
|
|
2011
|
+
viewport.devicePixelRatio,
|
|
2012
|
+
viewport.height,
|
|
2013
|
+
viewport.width
|
|
2014
|
+
]);
|
|
2015
|
+
return /* @__PURE__ */ jsxs(
|
|
2016
|
+
"div",
|
|
2017
|
+
{
|
|
2018
|
+
className: cn("relative flex h-full min-w-0 flex-col bg-og-bg", className),
|
|
2019
|
+
"data-og-command-state": commandFailure ? "error" : pendingCommands > 0 ? "pending" : "idle",
|
|
2020
|
+
style: { minHeight: 256 },
|
|
2021
|
+
children: [
|
|
2022
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-9 shrink-0 items-center border-b border-og-border bg-og-surface-1", children: [
|
|
2023
|
+
/* @__PURE__ */ jsx(
|
|
2024
|
+
"output",
|
|
2025
|
+
{
|
|
2026
|
+
"aria-label": "Selected range",
|
|
2027
|
+
className: "shrink-0 border-r border-og-border px-2 font-mono text-og-xs text-og-fg-muted",
|
|
2028
|
+
style: { width: 96 },
|
|
2029
|
+
children: selectionName(selection)
|
|
2030
|
+
}
|
|
2031
|
+
),
|
|
2032
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "px-2 font-og-mono text-og-sm italic text-og-fg-subtle", children: "fx" }),
|
|
2033
|
+
/* @__PURE__ */ jsx(
|
|
2034
|
+
"input",
|
|
2035
|
+
{
|
|
2036
|
+
"aria-label": "Formula or value",
|
|
2037
|
+
readOnly: !editable,
|
|
2038
|
+
"aria-invalid": commandFailure ? "true" : void 0,
|
|
2039
|
+
value: draft,
|
|
2040
|
+
onFocus: () => {
|
|
2041
|
+
if (editable) {
|
|
2042
|
+
const current = projection.readCell(selection.focus.row, selection.focus.col)?.input ?? "";
|
|
2043
|
+
initialDraftRef.current = current;
|
|
2044
|
+
setDraft(current);
|
|
2045
|
+
setEditMode("formula");
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
2048
|
+
onInput: (event) => setDraft(event.currentTarget.value),
|
|
2049
|
+
onKeyDown: onEditorKeyDown,
|
|
2050
|
+
onCompositionStart: onEditorCompositionStart,
|
|
2051
|
+
onCompositionEnd: (event) => onEditorCompositionEnd(event.currentTarget.value),
|
|
2052
|
+
onBlur: (event) => commitEditorOnBlur("formula", event.currentTarget.value),
|
|
2053
|
+
className: "h-8 min-w-0 flex-1 bg-transparent px-1 font-mono text-og-sm text-og-fg outline-hidden placeholder:text-og-fg-subtle"
|
|
2054
|
+
}
|
|
2055
|
+
)
|
|
2056
|
+
] }),
|
|
2057
|
+
/* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: commandFailure?.message ?? (pendingCommands > 0 ? "Saving spreadsheet changes" : "") }),
|
|
2058
|
+
commandFailure ? /* @__PURE__ */ jsxs(
|
|
2059
|
+
"div",
|
|
2060
|
+
{
|
|
2061
|
+
role: "alert",
|
|
2062
|
+
className: "absolute bottom-2 left-2 z-50 flex items-center gap-2 rounded-og-sm border border-og-status-failed/30 bg-og-surface-1/95 px-2 py-1 text-og-xs text-og-status-failed shadow-og-sm",
|
|
2063
|
+
style: { maxWidth: "calc(100% - 1rem)" },
|
|
2064
|
+
children: [
|
|
2065
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: commandFailure.message }),
|
|
2066
|
+
commandFailure.retry ? /* @__PURE__ */ jsx(
|
|
2067
|
+
"button",
|
|
2068
|
+
{
|
|
2069
|
+
type: "button",
|
|
2070
|
+
onClick: commandFailure.retry,
|
|
2071
|
+
className: "shrink-0 rounded-og-xs px-1.5 py-0.5 font-medium outline-hidden hover:bg-og-surface-3 focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
2072
|
+
children: "Retry"
|
|
2073
|
+
}
|
|
2074
|
+
) : null
|
|
2075
|
+
]
|
|
2076
|
+
}
|
|
2077
|
+
) : null,
|
|
2078
|
+
/* @__PURE__ */ jsx(
|
|
2079
|
+
"div",
|
|
2080
|
+
{
|
|
2081
|
+
ref: viewportRef,
|
|
2082
|
+
role: "grid",
|
|
2083
|
+
tabIndex: 0,
|
|
2084
|
+
"aria-label": `${projection.sheetName} spreadsheet`,
|
|
2085
|
+
"aria-rowcount": rowCount,
|
|
2086
|
+
"aria-colcount": columnCount,
|
|
2087
|
+
"aria-multiselectable": "true",
|
|
2088
|
+
"aria-readonly": !editable,
|
|
2089
|
+
"aria-busy": pendingCommands > 0 ? "true" : void 0,
|
|
2090
|
+
"aria-activedescendant": activeCellId,
|
|
2091
|
+
onKeyDown: onGridKeyDown,
|
|
2092
|
+
onCopy: copySelection,
|
|
2093
|
+
onPaste: pasteClipboard,
|
|
2094
|
+
onCompositionEnd: (event) => {
|
|
2095
|
+
if (editMode === null && editable && event.data) beginEditing(event.data);
|
|
2096
|
+
},
|
|
2097
|
+
onPointerDown: onGridPointerDown,
|
|
2098
|
+
onPointerMove: onGridPointerMove,
|
|
2099
|
+
onPointerUp: (event) => stopPointerSelection(event.pointerId),
|
|
2100
|
+
onPointerCancel: (event) => stopPointerSelection(event.pointerId),
|
|
2101
|
+
onDoubleClick: (event) => {
|
|
2102
|
+
const cell = hitTestCell(event.clientX, event.clientY);
|
|
2103
|
+
if (!cell) return;
|
|
2104
|
+
selectCell(cell, event.shiftKey);
|
|
2105
|
+
beginEditing(void 0, cell);
|
|
2106
|
+
},
|
|
2107
|
+
onScroll: (event) => {
|
|
2108
|
+
const element = event.currentTarget;
|
|
2109
|
+
setViewport((current) => ({
|
|
2110
|
+
...current,
|
|
2111
|
+
scrollLeft: element.scrollLeft,
|
|
2112
|
+
scrollTop: element.scrollTop
|
|
2113
|
+
}));
|
|
2114
|
+
},
|
|
2115
|
+
className: "relative min-h-0 min-w-0 flex-1 overflow-auto overscroll-contain bg-og-surface-1 text-og-sm outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
|
|
2116
|
+
children: /* @__PURE__ */ jsxs(
|
|
2117
|
+
"div",
|
|
2118
|
+
{
|
|
2119
|
+
role: "rowgroup",
|
|
2120
|
+
className: "relative",
|
|
2121
|
+
style: {
|
|
2122
|
+
width: horizontalProjection.physicalTotal,
|
|
2123
|
+
height: verticalProjection.physicalTotal
|
|
2124
|
+
},
|
|
2125
|
+
children: [
|
|
2126
|
+
/* @__PURE__ */ jsx(
|
|
2127
|
+
"canvas",
|
|
2128
|
+
{
|
|
2129
|
+
ref: canvasRef,
|
|
2130
|
+
"aria-hidden": "true",
|
|
2131
|
+
"data-og-spreadsheet-canvas": "retained-tiles",
|
|
2132
|
+
className: "pointer-events-none absolute z-0 block",
|
|
2133
|
+
style: {
|
|
2134
|
+
left: physicalScrollLeft,
|
|
2135
|
+
top: physicalScrollTop,
|
|
2136
|
+
width: viewport.width,
|
|
2137
|
+
height: viewport.height
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
),
|
|
2141
|
+
/* @__PURE__ */ jsx(
|
|
2142
|
+
"div",
|
|
2143
|
+
{
|
|
2144
|
+
"aria-hidden": true,
|
|
2145
|
+
className: "sticky left-0 top-0 z-40 border-b border-r border-og-border bg-og-surface-2",
|
|
2146
|
+
style: {
|
|
2147
|
+
width: ROW_HEADER_WIDTH,
|
|
2148
|
+
height: COLUMN_HEADER_HEIGHT,
|
|
2149
|
+
...canvasReady ? { background: "transparent", borderColor: "transparent" } : void 0
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
),
|
|
2153
|
+
/* @__PURE__ */ jsx("div", { role: "row", className: "contents", children: columnLayout.map(({ col, left, width }) => /* @__PURE__ */ jsx(
|
|
2154
|
+
"div",
|
|
2155
|
+
{
|
|
2156
|
+
role: "columnheader",
|
|
2157
|
+
"aria-colindex": col + 1,
|
|
2158
|
+
className: "sticky top-0 z-30 grid select-none place-items-center overflow-hidden border-b border-r border-og-border bg-og-surface-2 text-og-xs font-medium text-og-fg-muted",
|
|
2159
|
+
style: {
|
|
2160
|
+
position: "absolute",
|
|
2161
|
+
left,
|
|
2162
|
+
top: physicalScrollTop,
|
|
2163
|
+
width,
|
|
2164
|
+
height: COLUMN_HEADER_HEIGHT,
|
|
2165
|
+
...canvasReady ? {
|
|
2166
|
+
background: "transparent",
|
|
2167
|
+
borderColor: "transparent",
|
|
2168
|
+
color: "transparent"
|
|
2169
|
+
} : void 0
|
|
2170
|
+
},
|
|
2171
|
+
children: columnName2(col)
|
|
2172
|
+
},
|
|
2173
|
+
`header-${col}`
|
|
2174
|
+
)) }),
|
|
2175
|
+
rowLayout.map(({ row, top, height }) => {
|
|
2176
|
+
return /* @__PURE__ */ jsxs(
|
|
2177
|
+
"div",
|
|
2178
|
+
{
|
|
2179
|
+
role: "row",
|
|
2180
|
+
"aria-rowindex": row + 1,
|
|
2181
|
+
className: "absolute left-0",
|
|
2182
|
+
style: {
|
|
2183
|
+
top,
|
|
2184
|
+
width: horizontalProjection.physicalTotal,
|
|
2185
|
+
height
|
|
2186
|
+
},
|
|
2187
|
+
children: [
|
|
2188
|
+
/* @__PURE__ */ jsx(
|
|
2189
|
+
"div",
|
|
2190
|
+
{
|
|
2191
|
+
role: "rowheader",
|
|
2192
|
+
className: "sticky left-0 z-20 grid h-full select-none place-items-center border-b border-r border-og-border bg-og-surface-2 text-og-xs text-og-fg-muted",
|
|
2193
|
+
style: {
|
|
2194
|
+
width: ROW_HEADER_WIDTH,
|
|
2195
|
+
...canvasReady ? {
|
|
2196
|
+
background: "transparent",
|
|
2197
|
+
borderColor: "transparent",
|
|
2198
|
+
color: "transparent"
|
|
2199
|
+
} : void 0
|
|
2200
|
+
},
|
|
2201
|
+
children: row + 1
|
|
2202
|
+
}
|
|
2203
|
+
),
|
|
2204
|
+
columnLayout.map(({ col, left, width }) => {
|
|
2205
|
+
const active = selection.focus.row === row && selection.focus.col === col;
|
|
2206
|
+
const selected = isCellSelected(selection, row, col);
|
|
2207
|
+
const data = projection.readCell(row, col) ?? {
|
|
2208
|
+
value: null,
|
|
2209
|
+
input: "",
|
|
2210
|
+
format: {}
|
|
2211
|
+
};
|
|
2212
|
+
const value = data.value;
|
|
2213
|
+
const address = cellName({ row, col });
|
|
2214
|
+
const error = typeof value === "string" && value.startsWith("#");
|
|
2215
|
+
return /* @__PURE__ */ jsx(
|
|
2216
|
+
"div",
|
|
2217
|
+
{
|
|
2218
|
+
id: `${reactId}-cell-${row}-${col}`,
|
|
2219
|
+
role: "gridcell",
|
|
2220
|
+
"aria-colindex": col + 1,
|
|
2221
|
+
"aria-selected": selected,
|
|
2222
|
+
"aria-label": `${address}${value === null ? "" : `, ${displayValue2(value)}`}`,
|
|
2223
|
+
"data-og-cell": address,
|
|
2224
|
+
className: cn(
|
|
2225
|
+
"pointer-events-none absolute top-0 flex h-full min-w-0 overflow-hidden border-b border-r border-og-border px-1.5 text-og-fg",
|
|
2226
|
+
!canvasReady && selected && !active && "bg-og-accent/10",
|
|
2227
|
+
!canvasReady && active && "z-10 bg-og-accent/10 ring-2 ring-inset ring-og-accent",
|
|
2228
|
+
!canvasReady && error && "text-og-status-failed"
|
|
2229
|
+
),
|
|
2230
|
+
style: {
|
|
2231
|
+
left,
|
|
2232
|
+
width,
|
|
2233
|
+
...cellStyle(data.format),
|
|
2234
|
+
...canvasReady ? {
|
|
2235
|
+
background: "transparent",
|
|
2236
|
+
borderColor: "transparent",
|
|
2237
|
+
color: "transparent"
|
|
2238
|
+
} : void 0
|
|
2239
|
+
},
|
|
2240
|
+
children: active && editMode === "cell" ? /* @__PURE__ */ jsx(
|
|
2241
|
+
"input",
|
|
2242
|
+
{
|
|
2243
|
+
ref: editorRef,
|
|
2244
|
+
"aria-label": `Edit ${address}`,
|
|
2245
|
+
value: draft,
|
|
2246
|
+
onInput: (event) => setDraft(event.currentTarget.value),
|
|
2247
|
+
onKeyDown: onEditorKeyDown,
|
|
2248
|
+
onCompositionStart: onEditorCompositionStart,
|
|
2249
|
+
onCompositionEnd: (event) => onEditorCompositionEnd(event.currentTarget.value),
|
|
2250
|
+
onBlur: (event) => commitEditorOnBlur("cell", event.currentTarget.value),
|
|
2251
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
2252
|
+
className: "pointer-events-auto absolute inset-0 size-full bg-og-surface-1 px-1.5 font-mono text-og-sm text-og-fg outline-hidden ring-2 ring-inset ring-og-accent"
|
|
2253
|
+
}
|
|
2254
|
+
) : /* @__PURE__ */ jsx("span", { className: "block min-w-0 overflow-hidden text-ellipsis", children: displayValue2(value) })
|
|
2255
|
+
},
|
|
2256
|
+
col
|
|
2257
|
+
);
|
|
2258
|
+
})
|
|
2259
|
+
]
|
|
2260
|
+
},
|
|
2261
|
+
`row-${row}`
|
|
2262
|
+
);
|
|
2263
|
+
}),
|
|
2264
|
+
!activeCellVisible ? /* @__PURE__ */ jsx("div", { role: "row", "aria-rowindex": selection.focus.row + 1, className: "sr-only", children: /* @__PURE__ */ jsx(
|
|
2265
|
+
"div",
|
|
2266
|
+
{
|
|
2267
|
+
id: activeCellId,
|
|
2268
|
+
role: "gridcell",
|
|
2269
|
+
"aria-colindex": selection.focus.col + 1,
|
|
2270
|
+
"aria-selected": "true",
|
|
2271
|
+
"aria-label": `${cellName(selection.focus)}${hiddenActiveValue == null ? "" : `, ${displayValue2(hiddenActiveValue)}`}`
|
|
2272
|
+
}
|
|
2273
|
+
) }) : null
|
|
2274
|
+
]
|
|
2275
|
+
}
|
|
2276
|
+
)
|
|
2277
|
+
}
|
|
2278
|
+
)
|
|
2279
|
+
]
|
|
2280
|
+
}
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/components/artifacts/editable-spreadsheet.tsx
|
|
2285
|
+
import {
|
|
2286
|
+
SPREADSHEET_ARTIFACT_COMMAND_VERSION,
|
|
2287
|
+
editableArtifactStableId,
|
|
2288
|
+
spreadsheetSheetId
|
|
2289
|
+
} from "@opengeni/sdk/editable-artifacts";
|
|
2290
|
+
import { PlusIcon as PlusIcon2 } from "lucide-react";
|
|
2291
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
2292
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2293
|
+
var EXCEL_MAX_ROWS = 1048576;
|
|
2294
|
+
var EXCEL_MAX_COLUMNS = 16384;
|
|
2295
|
+
var INITIAL_VIEWPORT_ROWS = 64;
|
|
2296
|
+
var INITIAL_VIEWPORT_COLUMNS = 32;
|
|
2297
|
+
var MAX_INTERACTIVE_QUERY_CELLS = 65536;
|
|
2298
|
+
var MAX_INTERACTIVE_QUERY_BYTES = 8 * 1024 * 1024;
|
|
2299
|
+
var EMPTY_FORMAT = Object.freeze({});
|
|
2300
|
+
var EMPTY_SHEETS = [];
|
|
2301
|
+
function EditableSpreadsheetGrid({
|
|
2302
|
+
session,
|
|
2303
|
+
sheet,
|
|
2304
|
+
metadataRevision,
|
|
2305
|
+
readOnly = false,
|
|
2306
|
+
rowCount: requestedRowCount,
|
|
2307
|
+
columnCount: requestedColumnCount,
|
|
2308
|
+
overscanRows = 3,
|
|
2309
|
+
overscanColumns = 2,
|
|
2310
|
+
onSelectionChange,
|
|
2311
|
+
onCommit,
|
|
2312
|
+
onCommandError,
|
|
2313
|
+
onViewportChange,
|
|
2314
|
+
className
|
|
2315
|
+
}) {
|
|
2316
|
+
const rowCount = boundedSheetCount(requestedRowCount, EXCEL_MAX_ROWS, sheet.usedBounds?.endRow);
|
|
2317
|
+
const columnCount = boundedSheetCount(
|
|
2318
|
+
requestedColumnCount,
|
|
2319
|
+
EXCEL_MAX_COLUMNS,
|
|
2320
|
+
sheet.usedBounds?.endColumn
|
|
2321
|
+
);
|
|
2322
|
+
const [state, setState] = useState2(() => ({
|
|
2323
|
+
query: initialViewportQuery(sheet.sheetId, rowCount, columnCount),
|
|
2324
|
+
projection: null,
|
|
2325
|
+
error: null
|
|
2326
|
+
}));
|
|
2327
|
+
const activeCellRef = useRef2({ row: 0, column: 0 });
|
|
2328
|
+
useEffect2(() => {
|
|
2329
|
+
setState({
|
|
2330
|
+
query: initialViewportQuery(sheet.sheetId, rowCount, columnCount),
|
|
2331
|
+
projection: null,
|
|
2332
|
+
error: null
|
|
2333
|
+
});
|
|
2334
|
+
activeCellRef.current = { row: 0, column: 0 };
|
|
2335
|
+
}, [columnCount, rowCount, session, sheet.sheetId]);
|
|
2336
|
+
useEffect2(() => {
|
|
2337
|
+
const query = state.query;
|
|
2338
|
+
return session.subscribeSpreadsheetViewport(
|
|
2339
|
+
query,
|
|
2340
|
+
(projection2) => {
|
|
2341
|
+
if (!sameViewport(projection2, query)) return;
|
|
2342
|
+
if (sheet.generationId !== null && projection2.generationId !== sheet.generationId) {
|
|
2343
|
+
setState(
|
|
2344
|
+
(current) => sameViewportQuery(current.query, query) ? {
|
|
2345
|
+
...current,
|
|
2346
|
+
projection: null,
|
|
2347
|
+
error: new Error("Spreadsheet generation changed; refreshing metadata")
|
|
2348
|
+
} : current
|
|
2349
|
+
);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
setState(
|
|
2353
|
+
(current) => sameViewportQuery(current.query, query) ? { query, projection: projection2, error: null } : current
|
|
2354
|
+
);
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
onError(error) {
|
|
2358
|
+
setState(
|
|
2359
|
+
(current) => sameViewportQuery(current.query, query) ? { ...current, error } : current
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
);
|
|
2364
|
+
}, [session, sheet.generationId, state.query]);
|
|
2365
|
+
const projection = useMemo2(
|
|
2366
|
+
() => projectSdkViewport(
|
|
2367
|
+
sheet,
|
|
2368
|
+
metadataRevision,
|
|
2369
|
+
state.query,
|
|
2370
|
+
state.projection,
|
|
2371
|
+
rowCount,
|
|
2372
|
+
columnCount
|
|
2373
|
+
),
|
|
2374
|
+
[columnCount, metadataRevision, rowCount, sheet, state.projection, state.query]
|
|
2375
|
+
);
|
|
2376
|
+
const generation = useMemo2(() => sheetGeneration(sheet), [sheet]);
|
|
2377
|
+
const editable = !readOnly && generation !== null;
|
|
2378
|
+
const handleCommit = useCallback2(
|
|
2379
|
+
async (commit) => {
|
|
2380
|
+
if (!generation) throw new Error("This sheet generation is not writable yet");
|
|
2381
|
+
const input = spreadsheetCellInput(commit.input, commit.kind);
|
|
2382
|
+
await session.applySpreadsheetCommands({
|
|
2383
|
+
version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
|
|
2384
|
+
commands: [
|
|
2385
|
+
{
|
|
2386
|
+
kind: "cells.set",
|
|
2387
|
+
sheet: generation,
|
|
2388
|
+
anchor: { row: commit.cell.row, column: commit.cell.col },
|
|
2389
|
+
rows: 1,
|
|
2390
|
+
columns: 1,
|
|
2391
|
+
cells: [input]
|
|
2392
|
+
}
|
|
2393
|
+
]
|
|
2394
|
+
});
|
|
2395
|
+
onCommit?.(commit);
|
|
2396
|
+
},
|
|
2397
|
+
[generation, onCommit, session]
|
|
2398
|
+
);
|
|
2399
|
+
const handleClear = useCallback2(
|
|
2400
|
+
async (selection) => {
|
|
2401
|
+
if (!generation) throw new Error("This sheet generation is not writable yet");
|
|
2402
|
+
const top = Math.min(selection.anchor.row, selection.focus.row);
|
|
2403
|
+
const bottom = Math.max(selection.anchor.row, selection.focus.row);
|
|
2404
|
+
const left = Math.min(selection.anchor.col, selection.focus.col);
|
|
2405
|
+
const right = Math.max(selection.anchor.col, selection.focus.col);
|
|
2406
|
+
await session.applySpreadsheetCommands({
|
|
2407
|
+
version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
|
|
2408
|
+
commands: [
|
|
2409
|
+
{
|
|
2410
|
+
kind: "range.clear",
|
|
2411
|
+
sheet: generation,
|
|
2412
|
+
range: {
|
|
2413
|
+
start: { row: top, column: left },
|
|
2414
|
+
end: { row: bottom, column: right }
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
]
|
|
2418
|
+
});
|
|
2419
|
+
},
|
|
2420
|
+
[generation, session]
|
|
2421
|
+
);
|
|
2422
|
+
const handleCommitRange = useCallback2(
|
|
2423
|
+
async (commit) => {
|
|
2424
|
+
if (!generation) throw new Error("This sheet generation is not writable yet");
|
|
2425
|
+
await session.applySpreadsheetCommands({
|
|
2426
|
+
version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
|
|
2427
|
+
commands: [
|
|
2428
|
+
{
|
|
2429
|
+
kind: "cells.set",
|
|
2430
|
+
sheet: generation,
|
|
2431
|
+
anchor: { row: commit.anchor.row, column: commit.anchor.col },
|
|
2432
|
+
rows: commit.rows,
|
|
2433
|
+
columns: commit.columns,
|
|
2434
|
+
cells: commit.inputs.map(
|
|
2435
|
+
(input) => spreadsheetCellInput(input, input.startsWith("=") ? "formula" : "value")
|
|
2436
|
+
)
|
|
2437
|
+
}
|
|
2438
|
+
]
|
|
2439
|
+
});
|
|
2440
|
+
},
|
|
2441
|
+
[generation, session]
|
|
2442
|
+
);
|
|
2443
|
+
const handleSelection = useCallback2(
|
|
2444
|
+
(selection) => {
|
|
2445
|
+
activeCellRef.current = {
|
|
2446
|
+
row: selection.focus.row,
|
|
2447
|
+
column: selection.focus.col
|
|
2448
|
+
};
|
|
2449
|
+
setState((current) => {
|
|
2450
|
+
if (queryContains(current.query, selection.focus.row, selection.focus.col)) return current;
|
|
2451
|
+
return {
|
|
2452
|
+
query: boundedViewportQuery(
|
|
2453
|
+
sheet.sheetId,
|
|
2454
|
+
selection.focus.row,
|
|
2455
|
+
selection.focus.row + 1,
|
|
2456
|
+
selection.focus.col,
|
|
2457
|
+
selection.focus.col + 1,
|
|
2458
|
+
rowCount,
|
|
2459
|
+
columnCount,
|
|
2460
|
+
activeCellRef.current
|
|
2461
|
+
),
|
|
2462
|
+
projection: current.projection,
|
|
2463
|
+
error: null
|
|
2464
|
+
};
|
|
2465
|
+
});
|
|
2466
|
+
onSelectionChange?.(selection);
|
|
2467
|
+
},
|
|
2468
|
+
[columnCount, onSelectionChange, rowCount, sheet.sheetId]
|
|
2469
|
+
);
|
|
2470
|
+
const handleViewport = useCallback2(
|
|
2471
|
+
(viewport) => {
|
|
2472
|
+
const next = boundedViewportQuery(
|
|
2473
|
+
sheet.sheetId,
|
|
2474
|
+
viewport.overscanRowStart,
|
|
2475
|
+
viewport.overscanRowEnd,
|
|
2476
|
+
viewport.overscanColumnStart,
|
|
2477
|
+
viewport.overscanColumnEnd,
|
|
2478
|
+
rowCount,
|
|
2479
|
+
columnCount,
|
|
2480
|
+
activeCellRef.current
|
|
2481
|
+
);
|
|
2482
|
+
setState(
|
|
2483
|
+
(current) => sameViewportQuery(current.query, next) ? current : { query: next, projection: current.projection, error: null }
|
|
2484
|
+
);
|
|
2485
|
+
onViewportChange?.(viewport);
|
|
2486
|
+
},
|
|
2487
|
+
[columnCount, onViewportChange, rowCount, sheet.sheetId]
|
|
2488
|
+
);
|
|
2489
|
+
return /* @__PURE__ */ jsxs2("div", { className: cn("relative h-full min-h-0", className), children: [
|
|
2490
|
+
/* @__PURE__ */ jsx2(
|
|
2491
|
+
SpreadsheetProjectionGrid,
|
|
2492
|
+
{
|
|
2493
|
+
projection,
|
|
2494
|
+
readOnly: !editable,
|
|
2495
|
+
overscanRows,
|
|
2496
|
+
overscanColumns,
|
|
2497
|
+
onSelectionChange: handleSelection,
|
|
2498
|
+
commit: editable ? handleCommit : void 0,
|
|
2499
|
+
commitRange: editable ? handleCommitRange : void 0,
|
|
2500
|
+
clear: editable ? handleClear : void 0,
|
|
2501
|
+
onCommandError,
|
|
2502
|
+
onViewportChange: handleViewport
|
|
2503
|
+
}
|
|
2504
|
+
),
|
|
2505
|
+
state.error ? /* @__PURE__ */ jsx2(
|
|
2506
|
+
"output",
|
|
2507
|
+
{
|
|
2508
|
+
role: "status",
|
|
2509
|
+
className: "pointer-events-none absolute bottom-2 left-2 z-50 rounded-og-sm border border-og-status-failed/30 bg-og-surface-1/95 px-2 py-1 text-og-xs text-og-status-failed shadow-og-sm",
|
|
2510
|
+
style: { maxWidth: "min(28rem, calc(100% - 1rem))" },
|
|
2511
|
+
children: state.error.message
|
|
2512
|
+
}
|
|
2513
|
+
) : null
|
|
2514
|
+
] });
|
|
2515
|
+
}
|
|
2516
|
+
function EditableSpreadsheetArtifactSurface({
|
|
2517
|
+
session,
|
|
2518
|
+
title = "Workbook",
|
|
2519
|
+
subtitle,
|
|
2520
|
+
actions,
|
|
2521
|
+
initialSheetId,
|
|
2522
|
+
allowAddSheet = true,
|
|
2523
|
+
readOnly = false,
|
|
2524
|
+
...gridProps
|
|
2525
|
+
}) {
|
|
2526
|
+
const { metadata, error: metadataError } = useSpreadsheetMetadata(session);
|
|
2527
|
+
const view = useEditableArtifactView(session);
|
|
2528
|
+
const [activeSheetId, setActiveSheetId] = useState2(initialSheetId ?? null);
|
|
2529
|
+
const [creatingSheet, setCreatingSheet] = useState2(false);
|
|
2530
|
+
const [surfaceError, setSurfaceError] = useState2(null);
|
|
2531
|
+
const sheets = metadata?.sheets ?? EMPTY_SHEETS;
|
|
2532
|
+
const activeSheet = sheets.find((sheet) => sheet.sheetId === activeSheetId) ?? (initialSheetId ? sheets.find((sheet) => sheet.sheetId === initialSheetId) : void 0) ?? sheets[0] ?? null;
|
|
2533
|
+
const writable = !readOnly && view.writable;
|
|
2534
|
+
const accessRevoked = editableArtifactAccessRevoked(view);
|
|
2535
|
+
const addSheet = useCallback2(async () => {
|
|
2536
|
+
if (!writable || creatingSheet) return;
|
|
2537
|
+
setCreatingSheet(true);
|
|
2538
|
+
setSurfaceError(null);
|
|
2539
|
+
try {
|
|
2540
|
+
const after = activeSheet ? sheetGeneration(activeSheet) : null;
|
|
2541
|
+
const created = await session.createSpreadsheetSheet({
|
|
2542
|
+
name: nextAvailableSheetName2(sheets),
|
|
2543
|
+
after
|
|
2544
|
+
});
|
|
2545
|
+
setActiveSheetId(created.sheetId);
|
|
2546
|
+
} catch (cause) {
|
|
2547
|
+
setSurfaceError(asError(cause));
|
|
2548
|
+
} finally {
|
|
2549
|
+
setCreatingSheet(false);
|
|
2550
|
+
}
|
|
2551
|
+
}, [activeSheet, creatingSheet, session, sheets, writable]);
|
|
2552
|
+
const footer = /* @__PURE__ */ jsxs2(
|
|
2553
|
+
"div",
|
|
2554
|
+
{
|
|
2555
|
+
className: "flex min-h-9 items-center gap-1 overflow-x-auto px-2",
|
|
2556
|
+
role: "tablist",
|
|
2557
|
+
"aria-label": "Worksheets",
|
|
2558
|
+
children: [
|
|
2559
|
+
sheets.map((sheet) => /* @__PURE__ */ jsx2(
|
|
2560
|
+
"button",
|
|
2561
|
+
{
|
|
2562
|
+
type: "button",
|
|
2563
|
+
role: "tab",
|
|
2564
|
+
"aria-selected": sheet.sheetId === activeSheet?.sheetId,
|
|
2565
|
+
onClick: () => setActiveSheetId(sheet.sheetId),
|
|
2566
|
+
className: cn(
|
|
2567
|
+
"h-7 shrink-0 rounded-og-sm px-2.5 text-og-sm outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
2568
|
+
sheet.sheetId === activeSheet?.sheetId ? "bg-og-surface-3 font-medium text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3 hover:text-og-fg"
|
|
2569
|
+
),
|
|
2570
|
+
children: sheet.name
|
|
2571
|
+
},
|
|
2572
|
+
`${sheet.sheetId}:${sheet.generationId ?? "pending"}`
|
|
2573
|
+
)),
|
|
2574
|
+
writable && allowAddSheet ? /* @__PURE__ */ jsx2(
|
|
2575
|
+
"button",
|
|
2576
|
+
{
|
|
2577
|
+
type: "button",
|
|
2578
|
+
onClick: () => void addSheet(),
|
|
2579
|
+
disabled: creatingSheet,
|
|
2580
|
+
"aria-label": creatingSheet ? "Adding worksheet" : "Add worksheet",
|
|
2581
|
+
className: "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted outline-hidden hover:bg-og-surface-3 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50 [&>svg]:size-3.5",
|
|
2582
|
+
children: /* @__PURE__ */ jsx2(PlusIcon2, {})
|
|
2583
|
+
}
|
|
2584
|
+
) : null
|
|
2585
|
+
]
|
|
2586
|
+
}
|
|
2587
|
+
);
|
|
2588
|
+
const error = surfaceError ?? metadataError;
|
|
2589
|
+
return /* @__PURE__ */ jsx2(
|
|
2590
|
+
ArtifactSurface,
|
|
2591
|
+
{
|
|
2592
|
+
modality: "spreadsheet",
|
|
2593
|
+
title,
|
|
2594
|
+
subtitle: subtitle ?? (accessRevoked ? editableArtifactStatusLabel(view) : metadata ? `${sheets.length} sheet${sheets.length === 1 ? "" : "s"}` : editableArtifactStatusLabel(view)),
|
|
2595
|
+
actions,
|
|
2596
|
+
footer: accessRevoked ? void 0 : footer,
|
|
2597
|
+
busy: !accessRevoked && !metadata && !error,
|
|
2598
|
+
children: accessRevoked ? /* @__PURE__ */ jsx2(
|
|
2599
|
+
EditableArtifactMessage,
|
|
2600
|
+
{
|
|
2601
|
+
title: "Access changed",
|
|
2602
|
+
detail: editableArtifactStatusLabel(view)
|
|
2603
|
+
}
|
|
2604
|
+
) : activeSheet && metadata ? /* @__PURE__ */ jsx2(
|
|
2605
|
+
EditableSpreadsheetGrid,
|
|
2606
|
+
{
|
|
2607
|
+
...gridProps,
|
|
2608
|
+
session,
|
|
2609
|
+
sheet: activeSheet,
|
|
2610
|
+
metadataRevision: metadata.revision,
|
|
2611
|
+
readOnly: !writable
|
|
2612
|
+
},
|
|
2613
|
+
`${activeSheet.sheetId}:${activeSheet.generationId ?? "pending"}`
|
|
2614
|
+
) : error ? /* @__PURE__ */ jsx2(EditableArtifactMessage, { title: "Could not open this workbook", detail: error.message }) : metadata ? /* @__PURE__ */ jsx2("div", { className: "grid h-full place-items-center bg-og-bg p-6 text-center", children: /* @__PURE__ */ jsxs2("div", { children: [
|
|
2615
|
+
/* @__PURE__ */ jsx2("p", { className: "text-og-base font-medium text-og-fg", children: "This workbook has no worksheets." }),
|
|
2616
|
+
writable && allowAddSheet ? /* @__PURE__ */ jsx2(
|
|
2617
|
+
"button",
|
|
2618
|
+
{
|
|
2619
|
+
type: "button",
|
|
2620
|
+
onClick: () => void addSheet(),
|
|
2621
|
+
disabled: creatingSheet,
|
|
2622
|
+
className: "mt-3 rounded-og-sm bg-og-accent-deep px-3 py-1.5 text-og-sm font-medium text-og-accent-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50",
|
|
2623
|
+
children: creatingSheet ? "Adding\u2026" : "Add worksheet"
|
|
2624
|
+
}
|
|
2625
|
+
) : null
|
|
2626
|
+
] }) }) : /* @__PURE__ */ jsx2(
|
|
2627
|
+
EditableArtifactMessage,
|
|
2628
|
+
{
|
|
2629
|
+
title: "Opening workbook",
|
|
2630
|
+
detail: editableArtifactStatusLabel(view)
|
|
2631
|
+
}
|
|
2632
|
+
)
|
|
2633
|
+
}
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
function useSpreadsheetMetadata(session) {
|
|
2637
|
+
const [state, setState] = useState2({ metadata: null, error: null });
|
|
2638
|
+
useEffect2(
|
|
2639
|
+
() => session.subscribeSpreadsheetMetadata({}, (metadata) => setState({ metadata, error: null }), {
|
|
2640
|
+
onError: (error) => setState((current) => ({ ...current, error }))
|
|
2641
|
+
}),
|
|
2642
|
+
[session]
|
|
2643
|
+
);
|
|
2644
|
+
return state;
|
|
2645
|
+
}
|
|
2646
|
+
function projectSdkViewport(sheet, metadataRevision, query, viewport, rowCount, columnCount) {
|
|
2647
|
+
const current = viewport && sameViewport(viewport, query) ? viewport : null;
|
|
2648
|
+
const projectedCells = (current?.cells ?? []).map((cell) => ({
|
|
2649
|
+
row: cell.row,
|
|
2650
|
+
col: cell.column,
|
|
2651
|
+
value: projectedCellValue(cell.value),
|
|
2652
|
+
formula: cell.formula,
|
|
2653
|
+
format: EMPTY_FORMAT
|
|
2654
|
+
}));
|
|
2655
|
+
const cells = new SparseSpreadsheetCellIndex(projectedCells);
|
|
2656
|
+
const byCoordinate = new Map(
|
|
2657
|
+
projectedCells.map((cell) => [`${cell.row}:${cell.col}`, cell])
|
|
2658
|
+
);
|
|
2659
|
+
const used = sheet.usedBounds;
|
|
2660
|
+
return {
|
|
2661
|
+
sheetId: sheet.sheetId,
|
|
2662
|
+
sheetName: sheet.name,
|
|
2663
|
+
generationId: current?.generationId ?? sheet.generationId,
|
|
2664
|
+
revision: `${metadataRevision}:${current?.revision ?? "loading"}`,
|
|
2665
|
+
dimensionRevision: metadataRevision.toString(),
|
|
2666
|
+
rowCount,
|
|
2667
|
+
columnCount,
|
|
2668
|
+
usedRange: used ? {
|
|
2669
|
+
row: used.startRow,
|
|
2670
|
+
col: used.startColumn,
|
|
2671
|
+
rowCount: used.endRow - used.startRow + 1,
|
|
2672
|
+
colCount: used.endColumn - used.startColumn + 1
|
|
2673
|
+
} : null,
|
|
2674
|
+
coverage: current ? {
|
|
2675
|
+
rowStart: current.startRow,
|
|
2676
|
+
rowEnd: current.startRow + current.rowCount,
|
|
2677
|
+
columnStart: current.startColumn,
|
|
2678
|
+
columnEnd: current.startColumn + current.columnCount
|
|
2679
|
+
} : {
|
|
2680
|
+
rowStart: query.startRow,
|
|
2681
|
+
rowEnd: query.startRow,
|
|
2682
|
+
columnStart: query.startColumn,
|
|
2683
|
+
columnEnd: query.startColumn
|
|
2684
|
+
},
|
|
2685
|
+
cells,
|
|
2686
|
+
valueAt: (cell) => cell.value,
|
|
2687
|
+
readCell: (row, column) => {
|
|
2688
|
+
const cell = byCoordinate.get(`${row}:${column}`);
|
|
2689
|
+
return cell ? {
|
|
2690
|
+
value: cell.value,
|
|
2691
|
+
input: cell.formula ?? displayValue3(cell.value),
|
|
2692
|
+
format: cell.format
|
|
2693
|
+
} : null;
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function initialViewportQuery(sheetId, rowCount, columnCount) {
|
|
2698
|
+
return {
|
|
2699
|
+
sheetId,
|
|
2700
|
+
startRow: 0,
|
|
2701
|
+
startColumn: 0,
|
|
2702
|
+
rowCount: Math.min(rowCount, INITIAL_VIEWPORT_ROWS),
|
|
2703
|
+
columnCount: Math.min(columnCount, INITIAL_VIEWPORT_COLUMNS),
|
|
2704
|
+
maxCells: MAX_INTERACTIVE_QUERY_CELLS,
|
|
2705
|
+
maxBytes: MAX_INTERACTIVE_QUERY_BYTES
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
function boundedViewportQuery(sheetId, desiredRowStart, desiredRowEnd, desiredColumnStart, desiredColumnEnd, totalRows, totalColumns, focus) {
|
|
2709
|
+
let rowStart = clampInteger(desiredRowStart, 0, totalRows - 1);
|
|
2710
|
+
let rowEnd = clampInteger(desiredRowEnd, rowStart + 1, totalRows);
|
|
2711
|
+
let columnStart = clampInteger(desiredColumnStart, 0, totalColumns - 1);
|
|
2712
|
+
let columnEnd = clampInteger(desiredColumnEnd, columnStart + 1, totalColumns);
|
|
2713
|
+
const desiredRows = rowEnd - rowStart;
|
|
2714
|
+
const desiredColumns = columnEnd - columnStart;
|
|
2715
|
+
if (desiredRows * desiredColumns > MAX_INTERACTIVE_QUERY_CELLS) {
|
|
2716
|
+
const columns = Math.min(
|
|
2717
|
+
desiredColumns,
|
|
2718
|
+
Math.max(1, Math.floor(Math.sqrt(MAX_INTERACTIVE_QUERY_CELLS * 2)))
|
|
2719
|
+
);
|
|
2720
|
+
const rows = Math.max(1, Math.floor(MAX_INTERACTIVE_QUERY_CELLS / columns));
|
|
2721
|
+
columnStart = clampWindowStart(columnStart, columnEnd, focus.column, columns, totalColumns);
|
|
2722
|
+
columnEnd = Math.min(totalColumns, columnStart + columns);
|
|
2723
|
+
rowStart = clampWindowStart(rowStart, rowEnd, focus.row, rows, totalRows);
|
|
2724
|
+
rowEnd = Math.min(totalRows, rowStart + rows);
|
|
2725
|
+
}
|
|
2726
|
+
return {
|
|
2727
|
+
sheetId,
|
|
2728
|
+
startRow: rowStart,
|
|
2729
|
+
startColumn: columnStart,
|
|
2730
|
+
rowCount: rowEnd - rowStart,
|
|
2731
|
+
columnCount: columnEnd - columnStart,
|
|
2732
|
+
maxCells: MAX_INTERACTIVE_QUERY_CELLS,
|
|
2733
|
+
maxBytes: MAX_INTERACTIVE_QUERY_BYTES
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
function clampWindowStart(desiredStart, desiredEnd, focus, size, total) {
|
|
2737
|
+
if (desiredEnd - desiredStart <= size) return Math.min(desiredStart, Math.max(0, total - size));
|
|
2738
|
+
const centered = focus - Math.floor(size / 2);
|
|
2739
|
+
return Math.max(desiredStart, Math.min(desiredEnd - size, centered));
|
|
2740
|
+
}
|
|
2741
|
+
function queryContains(query, row, column) {
|
|
2742
|
+
return row >= query.startRow && row < query.startRow + query.rowCount && column >= query.startColumn && column < query.startColumn + query.columnCount;
|
|
2743
|
+
}
|
|
2744
|
+
function sameViewport(projection, query) {
|
|
2745
|
+
return projection.sheetId === query.sheetId && projection.startRow === query.startRow && projection.startColumn === query.startColumn && projection.rowCount === query.rowCount && projection.columnCount === query.columnCount;
|
|
2746
|
+
}
|
|
2747
|
+
function sameViewportQuery(left, right) {
|
|
2748
|
+
return left.sheetId === right.sheetId && left.startRow === right.startRow && left.startColumn === right.startColumn && left.rowCount === right.rowCount && left.columnCount === right.columnCount && left.maxCells === right.maxCells && left.maxBytes === right.maxBytes;
|
|
2749
|
+
}
|
|
2750
|
+
function sheetGeneration(sheet) {
|
|
2751
|
+
if (sheet.generationId === null) return null;
|
|
2752
|
+
return {
|
|
2753
|
+
kind: "generation",
|
|
2754
|
+
sheetId: spreadsheetSheetId(sheet.sheetId),
|
|
2755
|
+
creationOperationId: editableArtifactStableId(sheet.generationId)
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function spreadsheetCellInput(input, kind) {
|
|
2759
|
+
if (kind === "formula") return { formula: input, cached: null };
|
|
2760
|
+
if (input === "") return null;
|
|
2761
|
+
const normalized = input.trim();
|
|
2762
|
+
if (/^(?:true|false)$/i.test(normalized)) return normalized.toLowerCase() === "true";
|
|
2763
|
+
if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
|
|
2764
|
+
const value = Number(normalized);
|
|
2765
|
+
if (Number.isFinite(value)) return value;
|
|
2766
|
+
}
|
|
2767
|
+
return input;
|
|
2768
|
+
}
|
|
2769
|
+
function projectedCellValue(value) {
|
|
2770
|
+
switch (value.kind) {
|
|
2771
|
+
case "empty":
|
|
2772
|
+
return null;
|
|
2773
|
+
case "boolean":
|
|
2774
|
+
case "number":
|
|
2775
|
+
case "text":
|
|
2776
|
+
return value.value;
|
|
2777
|
+
case "date":
|
|
2778
|
+
return new Date(value.value);
|
|
2779
|
+
case "error":
|
|
2780
|
+
return spreadsheetErrorText(value.value);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
function spreadsheetErrorText(value) {
|
|
2784
|
+
if (typeof value === "object") return value.custom;
|
|
2785
|
+
return {
|
|
2786
|
+
null: "#NULL!",
|
|
2787
|
+
divide_by_zero: "#DIV/0!",
|
|
2788
|
+
value: "#VALUE!",
|
|
2789
|
+
reference: "#REF!",
|
|
2790
|
+
name: "#NAME?",
|
|
2791
|
+
number: "#NUM!",
|
|
2792
|
+
not_available: "#N/A",
|
|
2793
|
+
spill: "#SPILL!",
|
|
2794
|
+
calculation: "#CALC!"
|
|
2795
|
+
}[value];
|
|
2796
|
+
}
|
|
2797
|
+
function boundedSheetCount(requested, maximum, usedEnd) {
|
|
2798
|
+
const requestedCount = requested === void 0 || !Number.isFinite(requested) ? maximum : Math.max(1, Math.min(maximum, Math.floor(requested)));
|
|
2799
|
+
const usedCount = usedEnd === void 0 || !Number.isSafeInteger(usedEnd) ? 1 : Math.max(1, Math.min(maximum, usedEnd + 1));
|
|
2800
|
+
return Math.max(requestedCount, usedCount);
|
|
2801
|
+
}
|
|
2802
|
+
function displayValue3(value) {
|
|
2803
|
+
if (value === null || value === void 0) return "";
|
|
2804
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toISOString();
|
|
2805
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
2806
|
+
return String(value);
|
|
2807
|
+
}
|
|
2808
|
+
function nextAvailableSheetName2(sheets) {
|
|
2809
|
+
let index = sheets.length + 1;
|
|
2810
|
+
while (sheets.some((sheet) => sheet.name === `Sheet${index}`)) index += 1;
|
|
2811
|
+
return `Sheet${index}`;
|
|
2812
|
+
}
|
|
2813
|
+
function clampInteger(value, minimum, maximum) {
|
|
2814
|
+
if (!Number.isFinite(value)) return minimum;
|
|
2815
|
+
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
2816
|
+
}
|
|
2817
|
+
function asError(cause) {
|
|
2818
|
+
return cause instanceof Error ? cause : new Error("Spreadsheet change failed");
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
export {
|
|
2822
|
+
SpreadsheetArtifactSurface,
|
|
2823
|
+
SpreadsheetGrid,
|
|
2824
|
+
SpreadsheetProjectionGrid,
|
|
2825
|
+
EditableSpreadsheetGrid,
|
|
2826
|
+
EditableSpreadsheetArtifactSurface
|
|
2827
|
+
};
|
|
2828
|
+
//# sourceMappingURL=chunk-7WCLAEFV.js.map
|