@casualoffice/sheets 0.9.0 → 0.10.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/dist/api-BI2VLYQ6.d.cts +62 -0
- package/dist/api-BI2VLYQ6.d.ts +62 -0
- package/dist/chrome.cjs +2569 -0
- package/dist/chrome.cjs.map +1 -0
- package/dist/chrome.d.cts +96 -0
- package/dist/chrome.d.ts +96 -0
- package/dist/chrome.js +2556 -0
- package/dist/chrome.js.map +1 -0
- package/dist/collab.cjs +770 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.cts +248 -0
- package/dist/collab.d.ts +248 -0
- package/dist/collab.js +737 -0
- package/dist/collab.js.map +1 -0
- package/dist/embed/embed-runtime.js +128 -128
- package/dist/embed.cjs +166 -0
- package/dist/embed.cjs.map +1 -1
- package/dist/embed.d.cts +78 -3
- package/dist/embed.d.ts +78 -3
- package/dist/embed.js +166 -0
- package/dist/embed.js.map +1 -1
- package/dist/index.cjs +262 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +271 -168
- package/dist/index.js.map +1 -1
- package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
- package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
- package/dist/sheets.cjs +102 -16
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.d.cts +49 -63
- package/dist/sheets.d.ts +49 -63
- package/dist/sheets.js +111 -19
- package/dist/sheets.js.map +1 -1
- package/package.json +28 -3
- package/src/chrome/AutoSumPicker.tsx +176 -0
- package/src/chrome/BordersPicker.tsx +171 -0
- package/src/chrome/ChromeBottom.tsx +18 -0
- package/src/chrome/ChromeTop.tsx +21 -0
- package/src/chrome/ColorPicker.tsx +220 -0
- package/src/chrome/FindReplace.tsx +370 -0
- package/src/chrome/FormulaBar.tsx +378 -0
- package/src/chrome/Icon.tsx +43 -0
- package/src/chrome/MenuBar.tsx +336 -0
- package/src/chrome/NameBox.tsx +347 -0
- package/src/chrome/SheetTabs.tsx +346 -0
- package/src/chrome/StatusBar.tsx +232 -0
- package/src/chrome/Toolbar.tsx +401 -0
- package/src/chrome/fonts.ts +42 -0
- package/src/chrome/index.ts +24 -0
- package/src/collab/attachCollab.ts +151 -0
- package/src/collab/bridge-helpers.ts +97 -0
- package/src/collab/bridge.ts +885 -0
- package/src/collab/bridge.unit.test.ts +160 -0
- package/src/collab/index.ts +38 -0
- package/src/collab/replay-retry.ts +137 -0
- package/src/collab/replay-retry.unit.test.ts +223 -0
- package/src/collab/ws-url.ts +20 -0
- package/src/collab/ws-url.unit.test.ts +35 -0
- package/src/embed/EmbedHostTransport.ts +16 -1
- package/src/embed/EmbedTransport.ts +16 -0
- package/src/embed/EmbedTransport.unit.test.ts +88 -2
- package/src/embed/index.ts +7 -0
- package/src/embed/protocol.ts +34 -0
- package/src/embed-runtime/index.tsx +20 -0
- package/src/sheets/CasualSheets.tsx +204 -33
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { strict as assert } from 'node:assert';
|
|
8
8
|
import { test } from 'node:test';
|
|
9
9
|
|
|
10
|
-
import { EmbedTransport, isCasualEnvelope } from './';
|
|
11
|
-
import type { CasualEnvelope } from './';
|
|
10
|
+
import { EmbedTransport, EmbedHostTransport, isCasualEnvelope } from './';
|
|
11
|
+
import type { CasualEnvelope, SaveNotifyData, ExitData } from './';
|
|
12
12
|
|
|
13
13
|
function fakeHostWindow() {
|
|
14
14
|
let handler: ((ev: unknown) => void) | null = null;
|
|
@@ -135,6 +135,92 @@ test('sendHello emits an editor.hello envelope', () => {
|
|
|
135
135
|
assert.deepEqual((env.data as { capabilities: string[] }).capabilities, ['save', 'load']);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test('sendSaveNotify emits a fire-and-forget save.notify with the snapshot', () => {
|
|
139
|
+
const host = fakeHostWindow();
|
|
140
|
+
const parent = fakeParent();
|
|
141
|
+
const transport = new EmbedTransport({
|
|
142
|
+
app: 'sheet',
|
|
143
|
+
hostOrigin: 'https://drive.example',
|
|
144
|
+
version: '1.0.0',
|
|
145
|
+
commit: 'abc',
|
|
146
|
+
capabilities: [],
|
|
147
|
+
parentWindow: parent,
|
|
148
|
+
hostWindow: host,
|
|
149
|
+
});
|
|
150
|
+
const snapshot = { id: 'wb1', sheets: {} };
|
|
151
|
+
transport.sendSaveNotify({ snapshot, reason: 'shortcut' });
|
|
152
|
+
assert.equal(parent.sent.length, 1);
|
|
153
|
+
const env = parent.sent[0].msg as CasualEnvelope;
|
|
154
|
+
assert.equal(env.type, 'casual.save.notify');
|
|
155
|
+
assert.equal(env.app, 'sheet');
|
|
156
|
+
// No id — it's a notification, not a request awaiting a response.
|
|
157
|
+
assert.equal(env.id, undefined);
|
|
158
|
+
assert.equal((env.data as { reason: string }).reason, 'shortcut');
|
|
159
|
+
assert.deepEqual((env.data as { snapshot: unknown }).snapshot, snapshot);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('sendExit emits an exit envelope carrying the final snapshot', () => {
|
|
163
|
+
const host = fakeHostWindow();
|
|
164
|
+
const parent = fakeParent();
|
|
165
|
+
const transport = new EmbedTransport({
|
|
166
|
+
app: 'sheet',
|
|
167
|
+
hostOrigin: 'https://drive.example',
|
|
168
|
+
version: '1.0.0',
|
|
169
|
+
commit: 'abc',
|
|
170
|
+
capabilities: [],
|
|
171
|
+
parentWindow: parent,
|
|
172
|
+
hostWindow: host,
|
|
173
|
+
});
|
|
174
|
+
const snapshot = { id: 'wb1', sheets: { s1: {} } };
|
|
175
|
+
transport.sendExit({ snapshot });
|
|
176
|
+
assert.equal(parent.sent.length, 1);
|
|
177
|
+
const env = parent.sent[0].msg as CasualEnvelope;
|
|
178
|
+
assert.equal(env.type, 'casual.exit');
|
|
179
|
+
assert.equal(env.id, undefined);
|
|
180
|
+
assert.deepEqual((env.data as { snapshot: unknown }).snapshot, snapshot);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('host transport dispatches save.notify + exit to its handlers', async () => {
|
|
184
|
+
const host = fakeHostWindow();
|
|
185
|
+
// The host transport gates on ev.source === iframeWindow; use a sentinel.
|
|
186
|
+
const iframeWindow = { postMessage() {} } as unknown as Window;
|
|
187
|
+
const hostTransport = new EmbedHostTransport({
|
|
188
|
+
app: 'sheet',
|
|
189
|
+
iframeWindow,
|
|
190
|
+
embedOrigin: 'https://embed.example',
|
|
191
|
+
hostWindow: host,
|
|
192
|
+
});
|
|
193
|
+
let saved: SaveNotifyData | null = null;
|
|
194
|
+
let exited: ExitData | null = null;
|
|
195
|
+
hostTransport.on({
|
|
196
|
+
onSaveNotify: (d) => {
|
|
197
|
+
saved = d;
|
|
198
|
+
},
|
|
199
|
+
onExit: (d) => {
|
|
200
|
+
exited = d;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
host.fire({
|
|
204
|
+
origin: 'https://embed.example',
|
|
205
|
+
source: iframeWindow,
|
|
206
|
+
data: {
|
|
207
|
+
type: 'casual.save.notify',
|
|
208
|
+
app: 'sheet',
|
|
209
|
+
v: 1,
|
|
210
|
+
data: { snapshot: { id: 'wb1' }, reason: 'host' },
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
host.fire({
|
|
214
|
+
origin: 'https://embed.example',
|
|
215
|
+
source: iframeWindow,
|
|
216
|
+
data: { type: 'casual.exit', app: 'sheet', v: 1, data: { snapshot: { id: 'wb1' } } },
|
|
217
|
+
});
|
|
218
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
219
|
+
assert.equal(saved!.reason, 'host');
|
|
220
|
+
assert.deepEqual(saved!.snapshot, { id: 'wb1' });
|
|
221
|
+
assert.deepEqual(exited!.snapshot, { id: 'wb1' });
|
|
222
|
+
});
|
|
223
|
+
|
|
138
224
|
test('destroy detaches the listener', () => {
|
|
139
225
|
const host = fakeHostWindow();
|
|
140
226
|
const transport = new EmbedTransport({
|
package/src/embed/index.ts
CHANGED
|
@@ -12,6 +12,11 @@ export {
|
|
|
12
12
|
type EmbedTransportHandlers,
|
|
13
13
|
type EmbedTransportOptions,
|
|
14
14
|
} from './EmbedTransport';
|
|
15
|
+
export {
|
|
16
|
+
EmbedHostTransport,
|
|
17
|
+
type EmbedHostHandlers,
|
|
18
|
+
type EmbedHostTransportOptions,
|
|
19
|
+
} from './EmbedHostTransport';
|
|
15
20
|
export {
|
|
16
21
|
isCasualEnvelope,
|
|
17
22
|
type CasualApp,
|
|
@@ -26,6 +31,8 @@ export {
|
|
|
26
31
|
type SaveResponseData,
|
|
27
32
|
type SaveResponseDataOk,
|
|
28
33
|
type SaveResponseDataErr,
|
|
34
|
+
type SaveNotifyData,
|
|
35
|
+
type ExitData,
|
|
29
36
|
type SelectionChangedData,
|
|
30
37
|
type TelemetryEventData,
|
|
31
38
|
type LockLostData,
|
package/src/embed/protocol.ts
CHANGED
|
@@ -87,6 +87,40 @@ export interface SaveResponseDataErr {
|
|
|
87
87
|
|
|
88
88
|
export type SaveResponseData = SaveResponseDataOk | SaveResponseDataErr;
|
|
89
89
|
|
|
90
|
+
// ---------------------------------------------------------------
|
|
91
|
+
// Save / exit notifications (editor → host, fire-and-forget)
|
|
92
|
+
// ---------------------------------------------------------------
|
|
93
|
+
//
|
|
94
|
+
// The lightweight counterpart to the bytes-carrying load/save *request*
|
|
95
|
+
// pair above. These mirror the SDK's React `onSave` / `onExit` hooks one
|
|
96
|
+
// for one — the "one shape, two surfaces" save/exit contract — so a host
|
|
97
|
+
// that frames the iframe gets the same persistence signals a host that
|
|
98
|
+
// renders `<CasualSheets>` directly does. Fire-and-forget: the editor
|
|
99
|
+
// never owns storage; the host decides what to do with the snapshot.
|
|
100
|
+
//
|
|
101
|
+
// `casual.save.request` (above) stays the WOPI-style path for hosts that
|
|
102
|
+
// want xlsx bytes + etag round-trips; these notifications are the simpler
|
|
103
|
+
// "here's the current state, persist it however you like" path.
|
|
104
|
+
|
|
105
|
+
/** Editor → host: the user explicitly asked to save — Ctrl/Cmd+S inside
|
|
106
|
+
* the iframe, or the host's `casual.command.save`. Carries the full
|
|
107
|
+
* editor snapshot as JSON (sheet app: Univer's `IWorkbookData`). Mirror
|
|
108
|
+
* of the React `onSave` hook. v0.9+. */
|
|
109
|
+
export interface SaveNotifyData {
|
|
110
|
+
/** App-specific snapshot JSON. Sheet: `IWorkbookData`. */
|
|
111
|
+
snapshot: unknown;
|
|
112
|
+
/** What triggered the save: the in-editor shortcut or a host command. */
|
|
113
|
+
reason: 'shortcut' | 'host';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Editor → host: the editor is unmounting / navigating away. Carries
|
|
117
|
+
* the final snapshot so the host can persist on exit. Mirror of the
|
|
118
|
+
* React `onExit` hook. v0.9+. */
|
|
119
|
+
export interface ExitData {
|
|
120
|
+
/** App-specific snapshot JSON. Sheet: `IWorkbookData`. */
|
|
121
|
+
snapshot: unknown;
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
// ---------------------------------------------------------------
|
|
91
125
|
// Selection + telemetry + lock (editor → host notifications)
|
|
92
126
|
// ---------------------------------------------------------------
|
|
@@ -230,6 +230,14 @@ function EmbeddedSheets({
|
|
|
230
230
|
// iframe ships the minimal editor. Hosts that need feature plugins use
|
|
231
231
|
// the React `<CasualSheets>` component directly (lazyPlugins defaults on).
|
|
232
232
|
lazyPlugins={false}
|
|
233
|
+
// Phase 2 save/exit contract — the iframe surface of the same
|
|
234
|
+
// `onSave` / `onExit` hooks the React component exposes. The editor
|
|
235
|
+
// never persists; it hands the snapshot to the host over postMessage
|
|
236
|
+
// and the host decides what to do with it (WOPI, Drive, localStorage
|
|
237
|
+
// demo, …). `onSave` fires on Ctrl/Cmd+S inside the iframe; `onExit`
|
|
238
|
+
// on unmount.
|
|
239
|
+
onSave={(snapshot) => transport.sendSaveNotify({ snapshot, reason: 'shortcut' })}
|
|
240
|
+
onExit={(snapshot) => transport.sendExit({ snapshot })}
|
|
233
241
|
onReady={(api) => {
|
|
234
242
|
// Wire host → editor command.execute (Drive's custom toolbar
|
|
235
243
|
// calls this for bold / italic / undo / …). Maps the small
|
|
@@ -246,6 +254,7 @@ function EmbeddedSheets({
|
|
|
246
254
|
// a different module. The runtime behaviour is fine.
|
|
247
255
|
const apiAny = api as unknown as {
|
|
248
256
|
executeCommand(id: string, params?: object): Promise<unknown>;
|
|
257
|
+
getSnapshot(): unknown;
|
|
249
258
|
univer: { getActiveWorkbook(): { getActiveSheet(): SheetLike | null } | null };
|
|
250
259
|
};
|
|
251
260
|
transport.on({
|
|
@@ -259,6 +268,17 @@ function EmbeddedSheets({
|
|
|
259
268
|
/* swallow — bad command id from a stale host shouldn't crash the iframe */
|
|
260
269
|
}
|
|
261
270
|
},
|
|
271
|
+
// Host clicked its own "Save" button (host-rendered chrome above
|
|
272
|
+
// the iframe). Snapshot now and emit the same save notification
|
|
273
|
+
// the in-iframe Ctrl/Cmd+S path does — only the `reason` differs.
|
|
274
|
+
onCommandSave: () => {
|
|
275
|
+
try {
|
|
276
|
+
const snapshot = apiAny.getSnapshot();
|
|
277
|
+
if (snapshot) transport.sendSaveNotify({ snapshot, reason: 'host' });
|
|
278
|
+
} catch {
|
|
279
|
+
/* snapshot unavailable during boot — ignore */
|
|
280
|
+
}
|
|
281
|
+
},
|
|
262
282
|
});
|
|
263
283
|
|
|
264
284
|
// Editor → host: emit format state on a coarse interval. The
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* snapshot already uses, idle-loaded otherwise. Pass `lazyPlugins={false}`
|
|
14
14
|
* for the minimal editor.
|
|
15
15
|
*
|
|
16
|
+
* Formula compute runs on the main thread by default; pass `formula={{ worker }}`
|
|
17
|
+
* to move it off-thread (the SDK then wires `UniverRPCMainThreadPlugin` to the
|
|
18
|
+
* host's worker — see the `formula` prop).
|
|
19
|
+
*
|
|
16
20
|
* Intentionally NOT included (host can layer on top via FUniver):
|
|
17
|
-
* - Formula compute via Web Worker — `notExecuteFormula: false`
|
|
18
|
-
* is the default; the formula engine runs on the main thread.
|
|
19
|
-
* Host wires `UniverRPCMainThreadPlugin` + a worker URL itself
|
|
20
|
-
* if it wants the off-main path.
|
|
21
21
|
* - Snapshot swap (this component mounts a snapshot once; change
|
|
22
22
|
* the React `key` to remount with a fresh snapshot).
|
|
23
23
|
* - Paste-merge hooks, dev helpers, zoom-shortcut overrides,
|
|
@@ -28,7 +28,15 @@
|
|
|
28
28
|
* styles from this entry if the host doesn't reach the styles export.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
lazy,
|
|
33
|
+
Suspense,
|
|
34
|
+
useEffect,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
type CSSProperties,
|
|
38
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
39
|
+
} from 'react';
|
|
32
40
|
import {
|
|
33
41
|
ICommandService,
|
|
34
42
|
LocaleType,
|
|
@@ -51,13 +59,27 @@ import { UniverDocsPlugin } from '@univerjs/docs';
|
|
|
51
59
|
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
|
52
60
|
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
|
53
61
|
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
|
54
|
-
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
|
62
|
+
import { UniverSheetsFormulaPlugin, CalculationMode } from '@univerjs/sheets-formula';
|
|
55
63
|
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
|
|
56
64
|
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
|
|
57
65
|
import { UniverSheetsNumfmtUIPlugin } from '@univerjs/sheets-numfmt-ui';
|
|
66
|
+
// Type-only — erased at build, so `@univerjs/rpc` stays a runtime-optional peer
|
|
67
|
+
// (loaded via dynamic import only when a formula worker is passed).
|
|
68
|
+
import type { UniverRPCMainThreadPlugin as RpcMainThreadPluginType } from '@univerjs/rpc';
|
|
58
69
|
|
|
59
70
|
import { createCasualSheetsAPI, type CasualSheetsAPI } from './api';
|
|
60
71
|
import { eagerLoadForSnapshot, idleLoadAll, setUniverForLazyLoad } from '../univer/lazy-plugins';
|
|
72
|
+
// Chrome is lazy-loaded from the `@casualoffice/sheets/chrome` subpath (NOT a
|
|
73
|
+
// relative import — that would inline under this build's splitting:false). The
|
|
74
|
+
// subpath is externalised in tsup, so the consumer's bundler code-splits it and
|
|
75
|
+
// `chrome="none"` hosts (the default + the apps/web reference host) never load
|
|
76
|
+
// the chrome chunk.
|
|
77
|
+
const ChromeTop = lazy(() =>
|
|
78
|
+
import('@casualoffice/sheets/chrome').then((m) => ({ default: m.ChromeTop })),
|
|
79
|
+
);
|
|
80
|
+
const ChromeBottom = lazy(() =>
|
|
81
|
+
import('@casualoffice/sheets/chrome').then((m) => ({ default: m.ChromeBottom })),
|
|
82
|
+
);
|
|
61
83
|
|
|
62
84
|
export interface CasualSheetsProps {
|
|
63
85
|
/** Workbook snapshot to mount. Read once on initial mount; change
|
|
@@ -80,6 +102,13 @@ export interface CasualSheetsProps {
|
|
|
80
102
|
onChange?: (snapshot: IWorkbookData) => void;
|
|
81
103
|
/** Debounce window for `onChange`, in ms. Default 400. */
|
|
82
104
|
onChangeDebounceMs?: number;
|
|
105
|
+
/** Explicit save — fired when the user presses Ctrl/Cmd+S inside the editor
|
|
106
|
+
* (the browser's save dialog is suppressed). The host persists the snapshot.
|
|
107
|
+
* Part of the "host owns storage" contract: the SDK never writes a store. */
|
|
108
|
+
onSave?: (snapshot: IWorkbookData) => void;
|
|
109
|
+
/** Fired once when the editor unmounts, with the final snapshot — the host's
|
|
110
|
+
* last chance to persist before the workbook is disposed. */
|
|
111
|
+
onExit?: (snapshot: IWorkbookData) => void;
|
|
83
112
|
/** Lazy-load the feature plugins (conditional formatting, data
|
|
84
113
|
* validation, hyperlinks, notes, tables, comments, drawings, sort,
|
|
85
114
|
* filter, find/replace). Default `true`: plugins whose data is in
|
|
@@ -88,6 +117,29 @@ export interface CasualSheetsProps {
|
|
|
88
117
|
* minimal editor (render + formula + numfmt only) — the embed-iframe
|
|
89
118
|
* build does this to stay a single self-contained bundle. */
|
|
90
119
|
lazyPlugins?: boolean;
|
|
120
|
+
/** Escape hatch fired after the SDK registers its built-in plugins but BEFORE
|
|
121
|
+
* the workbook unit is created — the host can `univer.registerPlugin(...)`
|
|
122
|
+
* additional plugins here (e.g. an off-main formula worker via
|
|
123
|
+
* `UniverRPCMainThreadPlugin`, crosshair-highlight, zen-editor). Anything
|
|
124
|
+
* registered after `createUnit` would miss the unit's plugin-init pass, so
|
|
125
|
+
* register-time extras must go through this hook. Power hosts (the reference
|
|
126
|
+
* app) use it to share the SDK editor core while keeping their extra plugins;
|
|
127
|
+
* most integrators never need it. NOT covered by semver — it hands you the
|
|
128
|
+
* raw `Univer` instance. */
|
|
129
|
+
onBeforeCreateUnit?: (univer: Univer) => void;
|
|
130
|
+
/** Off-main formula compute. By default the formula engine runs on the MAIN
|
|
131
|
+
* thread (fine for typical sheets, zero host setup). Provide a Web Worker (or
|
|
132
|
+
* its URL) to move compute off-thread so paste / sort / fill on large
|
|
133
|
+
* workbooks don't freeze the UI: the SDK then registers the formula plugins
|
|
134
|
+
* with `notExecuteFormula` and wires `UniverRPCMainThreadPlugin` to your
|
|
135
|
+
* worker. The host owns the worker (the SDK never bundles one — that's brittle
|
|
136
|
+
* across bundlers) and must have `@univerjs/rpc` installed. The worker script
|
|
137
|
+
* is the standard Univer formula worker (see the reference app's
|
|
138
|
+
* `apps/web/src/univer/formula-worker.ts`). */
|
|
139
|
+
formula?: {
|
|
140
|
+
/** A constructed `Worker`, or a URL/string the RPC plugin loads. */
|
|
141
|
+
worker?: Worker | string;
|
|
142
|
+
};
|
|
91
143
|
/** Locale identifier. Defaults to `LocaleType.EN_US`. */
|
|
92
144
|
locale?: LocaleType;
|
|
93
145
|
/** Locale string bundle. Optional — Univer's default English
|
|
@@ -114,6 +166,18 @@ export interface CasualSheetsProps {
|
|
|
114
166
|
* Univer's design — a host that embeds the editor inside a light page
|
|
115
167
|
* should scope the editor or accept the global dark CSS. */
|
|
116
168
|
appearance?: 'light' | 'dark';
|
|
169
|
+
/** Office chrome rendered around the grid:
|
|
170
|
+
* - `'none'` (default): bare grid — the host supplies its own chrome.
|
|
171
|
+
* - `'minimal'` / `'full'`: the built-in Office shell — a menu bar
|
|
172
|
+
* (Edit/Insert/Format/Data/View), a formatting toolbar (font family/size,
|
|
173
|
+
* bold/italic/underline/strike, text & fill colour, borders, h/v align,
|
|
174
|
+
* wrap, merge, number formats, clear format, AutoSum), a formula bar with a
|
|
175
|
+
* name box + function autocomplete, a worksheet tab strip (switch/add/
|
|
176
|
+
* rename/delete), and a status bar (Average/Count/Numerical Count/Min/Max/
|
|
177
|
+
* Sum + zoom). All driven through the facade, themed via `--cs-chrome-*`
|
|
178
|
+
* (light/dark). `'minimal'` and `'full'` currently render the same shell;
|
|
179
|
+
* `'full'` is where richer panels (find/replace, charts, …) will land. */
|
|
180
|
+
chrome?: 'none' | 'minimal' | 'full';
|
|
117
181
|
/** Container style. Default fills the parent. */
|
|
118
182
|
style?: CSSProperties;
|
|
119
183
|
/** Container className for additional styling hooks. */
|
|
@@ -140,13 +204,18 @@ export function CasualSheets({
|
|
|
140
204
|
onReady,
|
|
141
205
|
onChange,
|
|
142
206
|
onChangeDebounceMs = 400,
|
|
207
|
+
onSave,
|
|
208
|
+
onExit,
|
|
143
209
|
lazyPlugins = true,
|
|
210
|
+
onBeforeCreateUnit,
|
|
211
|
+
formula,
|
|
144
212
|
locale = LocaleType.EN_US,
|
|
145
213
|
locales,
|
|
146
214
|
logLevel = LogLevel.WARN,
|
|
147
215
|
ui,
|
|
148
216
|
theme = defaultTheme,
|
|
149
217
|
appearance = 'light',
|
|
218
|
+
chrome = 'none',
|
|
150
219
|
style,
|
|
151
220
|
className,
|
|
152
221
|
testId = 'casual-sheets',
|
|
@@ -158,9 +227,20 @@ export function CasualSheets({
|
|
|
158
227
|
const onChangeRef = useRef(onChange);
|
|
159
228
|
onChangeRef.current = onChange;
|
|
160
229
|
const hasOnChange = useRef(!!onChange).current;
|
|
230
|
+
// Latest save/exit callbacks, called via refs so they fire without re-running
|
|
231
|
+
// the boot effect. onExit is read in cleanup; onSave on the Ctrl/Cmd+S handler.
|
|
232
|
+
const onSaveRef = useRef(onSave);
|
|
233
|
+
onSaveRef.current = onSave;
|
|
234
|
+
const onExitRef = useRef(onExit);
|
|
235
|
+
onExitRef.current = onExit;
|
|
161
236
|
// The live FUniver facade, captured at mount so the reactive appearance
|
|
162
237
|
// effect can reach Univer's ThemeService without re-running boot.
|
|
163
238
|
const apiRef = useRef<CasualSheetsAPI | null>(null);
|
|
239
|
+
// The live API as state, so the built-in chrome (FormulaBar) re-renders and
|
|
240
|
+
// subscribes once the editor is ready. Only set when chrome is shown — the
|
|
241
|
+
// bare-grid path never triggers this re-render. A single post-mount setState
|
|
242
|
+
// doesn't disturb the grid (Univer owns its canvas outside React).
|
|
243
|
+
const [chromeApi, setChromeApi] = useState<CasualSheetsAPI | null>(null);
|
|
164
244
|
|
|
165
245
|
useEffect(() => {
|
|
166
246
|
const container = hostRef.current;
|
|
@@ -175,36 +255,56 @@ export function CasualSheets({
|
|
|
175
255
|
|
|
176
256
|
const uiOpts = { ...DEFAULT_UI, ...ui, container };
|
|
177
257
|
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
// redi/@univerjs copy — the duplicate-redi that previously made the formula
|
|
182
|
-
// plugins fail (and which the SDK worked around by dropping them) is gone.
|
|
183
|
-
// apps/web offloads compute to a worker for perf on huge sheets, but a worker
|
|
184
|
-
// shipped inside a published package is brittle to bundle in arbitrary hosts;
|
|
185
|
-
// main-thread keeps the SDK editor self-contained. SDK restructure Batch 2.
|
|
186
|
-
univer.registerPlugin(UniverFormulaEnginePlugin);
|
|
187
|
-
univer.registerPlugin(UniverUIPlugin, uiOpts);
|
|
188
|
-
univer.registerPlugin(UniverDocsPlugin);
|
|
189
|
-
univer.registerPlugin(UniverDocsUIPlugin);
|
|
190
|
-
univer.registerPlugin(UniverSheetsPlugin);
|
|
191
|
-
univer.registerPlugin(UniverSheetsUIPlugin);
|
|
192
|
-
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
|
193
|
-
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
|
194
|
-
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
|
195
|
-
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
|
196
|
-
|
|
197
|
-
// Register the lazy-loader's holder so the eager/idle loaders can reach this
|
|
198
|
-
// univer. CasualSheets uses its own (bundled) copy of the loader — the
|
|
199
|
-
// exported `@casualoffice/sheets/univer` is @internal and only the host app's
|
|
200
|
-
// legacy UniverSheet consumes it, so there's no cross-instance state to share.
|
|
201
|
-
if (lazyPlugins) setUniverForLazyLoad(univer);
|
|
258
|
+
// `formula.worker` → off-main compute. Default = main thread (fine for
|
|
259
|
+
// typical sheets, zero host setup).
|
|
260
|
+
const offMain = !!formula?.worker;
|
|
202
261
|
|
|
203
262
|
let cancelled = false;
|
|
204
263
|
let changeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
205
264
|
let changeSub: { dispose: () => void } | undefined;
|
|
206
265
|
|
|
207
266
|
void (async () => {
|
|
267
|
+
// Plugin registration runs here (not synchronously) so the OPTIONAL RPC
|
|
268
|
+
// transport can be `await import`ed FIRST and registered in its correct
|
|
269
|
+
// slot — right after the formula engine, before sheets. Registering it out
|
|
270
|
+
// of order (or after createUnit) leaves the formula engine's worker channel
|
|
271
|
+
// unwired → cells stay 0. Dynamic import keeps `@univerjs/rpc` a true
|
|
272
|
+
// optional peer (only loaded when a worker is passed).
|
|
273
|
+
let RPCMainThreadPlugin: typeof RpcMainThreadPluginType | null = null;
|
|
274
|
+
if (offMain && formula?.worker) {
|
|
275
|
+
RPCMainThreadPlugin = (await import('@univerjs/rpc')).UniverRPCMainThreadPlugin;
|
|
276
|
+
if (cancelled) return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
univer.registerPlugin(UniverRenderEnginePlugin);
|
|
280
|
+
univer.registerPlugin(
|
|
281
|
+
UniverFormulaEnginePlugin,
|
|
282
|
+
offMain ? { notExecuteFormula: true } : undefined,
|
|
283
|
+
);
|
|
284
|
+
if (RPCMainThreadPlugin && formula?.worker) {
|
|
285
|
+
univer.registerPlugin(RPCMainThreadPlugin, { workerURL: formula.worker });
|
|
286
|
+
}
|
|
287
|
+
univer.registerPlugin(UniverUIPlugin, uiOpts);
|
|
288
|
+
univer.registerPlugin(UniverDocsPlugin);
|
|
289
|
+
univer.registerPlugin(UniverDocsUIPlugin);
|
|
290
|
+
univer.registerPlugin(UniverSheetsPlugin, offMain ? { notExecuteFormula: true } : undefined);
|
|
291
|
+
univer.registerPlugin(UniverSheetsUIPlugin);
|
|
292
|
+
univer.registerPlugin(
|
|
293
|
+
UniverSheetsFormulaPlugin,
|
|
294
|
+
offMain
|
|
295
|
+
? { notExecuteFormula: true, initialFormulaComputing: CalculationMode.NO_CALCULATION }
|
|
296
|
+
: undefined,
|
|
297
|
+
);
|
|
298
|
+
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
|
299
|
+
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
|
300
|
+
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
|
301
|
+
|
|
302
|
+
// Lazy-loader holder (the loader is @internal so a relative import shares
|
|
303
|
+
// no cross-instance state) + host plugin escape hatch — both before
|
|
304
|
+
// createUnit.
|
|
305
|
+
if (lazyPlugins) setUniverForLazyLoad(univer);
|
|
306
|
+
onBeforeCreateUnit?.(univer);
|
|
307
|
+
|
|
208
308
|
// Eager-load any feature plugin whose data already lives in initialData
|
|
209
309
|
// (CF rules, tables, hyperlinks, …) BEFORE createUnit — Univer's resource
|
|
210
310
|
// manager silently drops keys for plugins that aren't registered when it
|
|
@@ -218,6 +318,9 @@ export function CasualSheets({
|
|
|
218
318
|
|
|
219
319
|
const api = createCasualSheetsAPI(FUniver.newAPI(univer));
|
|
220
320
|
apiRef.current = api;
|
|
321
|
+
// Hand the live API to the built-in chrome (FormulaBar subscribes to it).
|
|
322
|
+
// Only when chrome is shown, so bare-grid consumers never re-render.
|
|
323
|
+
if (!cancelled && chrome !== 'none') setChromeApi(api);
|
|
221
324
|
// Apply the initial appearance now that the editor exists (the reactive
|
|
222
325
|
// effect below also runs on mount, but apiRef may not be set yet when it
|
|
223
326
|
// first fires — this guarantees dark mode from the first paint).
|
|
@@ -258,7 +361,14 @@ export function CasualSheets({
|
|
|
258
361
|
cancelled = true;
|
|
259
362
|
if (changeTimer) clearTimeout(changeTimer);
|
|
260
363
|
changeSub?.dispose();
|
|
364
|
+
// Last-chance persist: emit the final snapshot before the workbook is
|
|
365
|
+
// disposed (disposal is deferred via microtask below, so it's still alive).
|
|
366
|
+
if (onExitRef.current) {
|
|
367
|
+
const snap = apiRef.current?.getSnapshot();
|
|
368
|
+
if (snap) onExitRef.current(snap);
|
|
369
|
+
}
|
|
261
370
|
apiRef.current = null;
|
|
371
|
+
setChromeApi(null);
|
|
262
372
|
if (lazyPlugins) setUniverForLazyLoad(null);
|
|
263
373
|
// Defer disposal off the React render phase — Univer owns its
|
|
264
374
|
// own React root, and a synchronous unmount mid-render warns
|
|
@@ -281,13 +391,74 @@ export function CasualSheets({
|
|
|
281
391
|
applyAppearance(api, container, appearance);
|
|
282
392
|
}, [appearance]);
|
|
283
393
|
|
|
394
|
+
// Ctrl/Cmd+S anywhere in the editor → onSave (suppress the browser dialog).
|
|
395
|
+
// Capture phase so we beat Univer's own key handling on the canvas.
|
|
396
|
+
const onKeyDownCapture = (e: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
397
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === 's' || e.key === 'S')) {
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
const snap = apiRef.current?.getSnapshot();
|
|
400
|
+
if (snap) onSaveRef.current?.(snap);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// chrome="none" (default) keeps the exact bare-grid shape existing consumers
|
|
405
|
+
// rely on (embed-runtime, hosts that bring their own shell). Any other level
|
|
406
|
+
// wraps the grid in a flex column with the built-in chrome above it; the grid
|
|
407
|
+
// container (hostRef, where Univer mounts) fills the remaining space.
|
|
408
|
+
if (chrome === 'none') {
|
|
409
|
+
return (
|
|
410
|
+
<div
|
|
411
|
+
ref={hostRef}
|
|
412
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
413
|
+
style={{ ...DEFAULT_STYLE, ...style }}
|
|
414
|
+
className={className}
|
|
415
|
+
data-testid={testId}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The built-in chrome components read their colours from `--cs-chrome-*` CSS
|
|
421
|
+
// vars (with light fallbacks). Set them on the wrapper so the toolbar / formula
|
|
422
|
+
// bar / status bar flip with `appearance` — reactive via the prop. (Hosts can
|
|
423
|
+
// still override any of these vars themselves.)
|
|
424
|
+
// Values from @schnsrw/design-system tokens (surface-strip / text / border)
|
|
425
|
+
// — adopted directly (not as a package dep) so the chrome matches the suite.
|
|
426
|
+
const dark = appearance === 'dark';
|
|
427
|
+
const chromeVars = {
|
|
428
|
+
'--cs-chrome-bg': dark ? '#2a2e35' : '#eef1f5',
|
|
429
|
+
'--cs-chrome-fg': dark ? '#e6e6e6' : '#201f1e',
|
|
430
|
+
'--cs-chrome-muted': dark ? '#b0b3ba' : '#605e5c',
|
|
431
|
+
'--cs-chrome-border': dark ? '#32363d' : '#e6e9ee',
|
|
432
|
+
'--cs-chrome-input-bg': dark ? '#23262c' : '#ffffff',
|
|
433
|
+
'--cs-chrome-hover': dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)',
|
|
434
|
+
// accent (design-system #0e7490) for active toggle states.
|
|
435
|
+
'--cs-chrome-active': dark ? 'rgba(21,151,186,0.22)' : '#e6f3f7',
|
|
436
|
+
'--cs-chrome-active-fg': dark ? '#7fd3e6' : '#0e7490',
|
|
437
|
+
} as CSSProperties;
|
|
438
|
+
|
|
284
439
|
return (
|
|
285
440
|
<div
|
|
286
|
-
ref={hostRef}
|
|
287
|
-
style={{ ...DEFAULT_STYLE, ...style }}
|
|
288
441
|
className={className}
|
|
289
442
|
data-testid={testId}
|
|
290
|
-
|
|
443
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
444
|
+
style={{
|
|
445
|
+
...DEFAULT_STYLE,
|
|
446
|
+
...chromeVars,
|
|
447
|
+
...style,
|
|
448
|
+
display: 'flex',
|
|
449
|
+
flexDirection: 'column',
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
{/* Bars appear once their lazy chunk loads (a tick after first paint); the
|
|
453
|
+
grid host is OUTSIDE Suspense so Univer mounts immediately. */}
|
|
454
|
+
<Suspense fallback={null}>
|
|
455
|
+
<ChromeTop api={chromeApi} />
|
|
456
|
+
</Suspense>
|
|
457
|
+
<div ref={hostRef} style={{ flex: '1 1 auto', minHeight: 0, position: 'relative' }} />
|
|
458
|
+
<Suspense fallback={null}>
|
|
459
|
+
<ChromeBottom api={chromeApi} />
|
|
460
|
+
</Suspense>
|
|
461
|
+
</div>
|
|
291
462
|
);
|
|
292
463
|
}
|
|
293
464
|
|