@casualoffice/sheets 0.9.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/LICENSE +200 -0
- package/dist/embed/embed-runtime.js +537 -0
- package/dist/embed/embed.html +29 -0
- package/dist/embed/parser.worker.js +48474 -0
- package/dist/embed.cjs +225 -0
- package/dist/embed.cjs.map +1 -0
- package/dist/embed.d.cts +100 -0
- package/dist/embed.d.ts +100 -0
- package/dist/embed.js +204 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.cjs +1549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1530 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.worker.cjs +48469 -0
- package/dist/parser.worker.cjs.map +1 -0
- package/dist/parser.worker.js +48474 -0
- package/dist/parser.worker.js.map +1 -0
- package/dist/protocol--KyBQUjU.d.cts +171 -0
- package/dist/protocol-cEzy7S0i.d.ts +171 -0
- package/dist/sheets.cjs +677 -0
- package/dist/sheets.cjs.map +1 -0
- package/dist/sheets.d.cts +177 -0
- package/dist/sheets.d.ts +177 -0
- package/dist/sheets.js +658 -0
- package/dist/sheets.js.map +1 -0
- package/dist/signing.cjs +706 -0
- package/dist/signing.cjs.map +1 -0
- package/dist/signing.d.cts +141 -0
- package/dist/signing.d.ts +141 -0
- package/dist/signing.js +683 -0
- package/dist/signing.js.map +1 -0
- package/dist/styles.cjs +10 -0
- package/dist/styles.cjs.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.js +8 -0
- package/dist/styles.js.map +1 -0
- package/dist/types-s_O0u6Cg.d.cts +90 -0
- package/dist/types-s_O0u6Cg.d.ts +90 -0
- package/dist/univer.cjs +220 -0
- package/dist/univer.cjs.map +1 -0
- package/dist/univer.d.cts +60 -0
- package/dist/univer.d.ts +60 -0
- package/dist/univer.js +187 -0
- package/dist/univer.js.map +1 -0
- package/dist/xlsx.cjs +3388 -0
- package/dist/xlsx.cjs.map +1 -0
- package/dist/xlsx.d.cts +383 -0
- package/dist/xlsx.d.ts +383 -0
- package/dist/xlsx.js +3383 -0
- package/dist/xlsx.js.map +1 -0
- package/package.json +293 -0
- package/src/embed/EmbedHostTransport.ts +226 -0
- package/src/embed/EmbedTransport.ts +323 -0
- package/src/embed/EmbedTransport.unit.test.ts +161 -0
- package/src/embed/index.ts +40 -0
- package/src/embed/protocol.ts +258 -0
- package/src/embed-runtime/embed.html +29 -0
- package/src/embed-runtime/index.tsx +440 -0
- package/src/index.ts +16 -0
- package/src/sheets/CasualSheets.tsx +319 -0
- package/src/sheets/CasualSheetsIframe.tsx +220 -0
- package/src/sheets/api.ts +108 -0
- package/src/sheets/index.ts +11 -0
- package/src/signing/SigningPane.tsx +374 -0
- package/src/signing/SigningProvider.tsx +126 -0
- package/src/signing/captures.tsx +316 -0
- package/src/signing/controller.ts +151 -0
- package/src/signing/controller.unit.test.ts +133 -0
- package/src/signing/index.ts +44 -0
- package/src/signing/types.ts +89 -0
- package/src/styles.ts +16 -0
- package/src/univer/index.ts +17 -0
- package/src/univer/lazy-plugins.ts +280 -0
- package/src/xlsx/_perf.ts +14 -0
- package/src/xlsx/_snapshot-constants.ts +15 -0
- package/src/xlsx/comments-resource.ts +209 -0
- package/src/xlsx/constants.ts +9 -0
- package/src/xlsx/data-validation-resource.ts +219 -0
- package/src/xlsx/import.ts +35 -0
- package/src/xlsx/index.ts +40 -0
- package/src/xlsx/page-setup-resource.ts +205 -0
- package/src/xlsx/parse-impl.ts +418 -0
- package/src/xlsx/parse-in-worker.ts +82 -0
- package/src/xlsx/parser.worker.ts +39 -0
- package/src/xlsx/passthrough-resource.ts +175 -0
- package/src/xlsx/pivot-passthrough.ts +359 -0
- package/src/xlsx/style-mapping.ts +171 -0
- package/src/xlsx/tables-resource.ts +211 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CasualSheets — minimal React wrapper around Univer Sheets.
|
|
3
|
+
*
|
|
4
|
+
* Boots Univer with the eager plugin set (render + formula engine +
|
|
5
|
+
* UI + docs + sheets + sheets-ui + sheets-formula + numfmt), mounts a
|
|
6
|
+
* single workbook unit from `initialData`, and hands the host the
|
|
7
|
+
* `CasualSheetsAPI` imperative ref via `onReady` (raw FUniver facade
|
|
8
|
+
* available at `api.univer`).
|
|
9
|
+
*
|
|
10
|
+
* Feature plugins (conditional formatting, data validation, drawings,
|
|
11
|
+
* sort, filter, hyperlinks, tables, comments, find/replace) load lazily
|
|
12
|
+
* by default (`lazyPlugins`): eagerly before mount for whatever the
|
|
13
|
+
* snapshot already uses, idle-loaded otherwise. Pass `lazyPlugins={false}`
|
|
14
|
+
* for the minimal editor.
|
|
15
|
+
*
|
|
16
|
+
* 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
|
+
* - Snapshot swap (this component mounts a snapshot once; change
|
|
22
|
+
* the React `key` to remount with a fresh snapshot).
|
|
23
|
+
* - Paste-merge hooks, dev helpers, zoom-shortcut overrides,
|
|
24
|
+
* facade extensions — all app concerns.
|
|
25
|
+
*
|
|
26
|
+
* Styles: host must import `@casualoffice/sheets/styles.css`
|
|
27
|
+
* (or the per-plugin CSS) once at app boot. Tree-shaking strips the
|
|
28
|
+
* styles from this entry if the host doesn't reach the styles export.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useEffect, useRef, type CSSProperties } from 'react';
|
|
32
|
+
import {
|
|
33
|
+
ICommandService,
|
|
34
|
+
LocaleType,
|
|
35
|
+
LogLevel,
|
|
36
|
+
ThemeService,
|
|
37
|
+
Univer,
|
|
38
|
+
UniverInstanceType,
|
|
39
|
+
type ICommandInfo,
|
|
40
|
+
type IExecutionOptions,
|
|
41
|
+
type IWorkbookData,
|
|
42
|
+
type ILocales,
|
|
43
|
+
} from '@univerjs/core';
|
|
44
|
+
import { FUniver } from '@univerjs/core/facade';
|
|
45
|
+
import { defaultTheme } from '@univerjs/themes';
|
|
46
|
+
|
|
47
|
+
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
|
48
|
+
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
|
49
|
+
import { UniverUIPlugin } from '@univerjs/ui';
|
|
50
|
+
import { UniverDocsPlugin } from '@univerjs/docs';
|
|
51
|
+
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
|
52
|
+
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
|
53
|
+
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
|
54
|
+
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
|
55
|
+
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
|
|
56
|
+
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
|
|
57
|
+
import { UniverSheetsNumfmtUIPlugin } from '@univerjs/sheets-numfmt-ui';
|
|
58
|
+
|
|
59
|
+
import { createCasualSheetsAPI, type CasualSheetsAPI } from './api';
|
|
60
|
+
import { eagerLoadForSnapshot, idleLoadAll, setUniverForLazyLoad } from '../univer/lazy-plugins';
|
|
61
|
+
|
|
62
|
+
export interface CasualSheetsProps {
|
|
63
|
+
/** Workbook snapshot to mount. Read once on initial mount; change
|
|
64
|
+
* the React `key` on this component to remount with a new
|
|
65
|
+
* workbook. */
|
|
66
|
+
initialData: IWorkbookData;
|
|
67
|
+
/** Called after the workbook unit is created. Hands back the
|
|
68
|
+
* `CasualSheetsAPI` imperative ref — the SDK's stable integration
|
|
69
|
+
* surface (snapshot I/O, xlsx import, selection, command dispatch).
|
|
70
|
+
* The raw FUniver facade is on `api.univer` as the escape hatch. */
|
|
71
|
+
onReady?: (api: CasualSheetsAPI) => void;
|
|
72
|
+
/** Debounced stream of workbook snapshots, emitted after edits
|
|
73
|
+
* settle. This is the "host persists it" half of the Excalidraw
|
|
74
|
+
* model — the editor stays storage-unaware and the host writes the
|
|
75
|
+
* snapshot wherever it likes (localStorage, server, …). Driven by
|
|
76
|
+
* Univer's mutation hook (`onMutationExecutedForCollab`), not UI
|
|
77
|
+
* events, so it captures every edit including programmatic ones.
|
|
78
|
+
* May fire for background/structural mutations too; treat each call
|
|
79
|
+
* as "current state, persist if you care". */
|
|
80
|
+
onChange?: (snapshot: IWorkbookData) => void;
|
|
81
|
+
/** Debounce window for `onChange`, in ms. Default 400. */
|
|
82
|
+
onChangeDebounceMs?: number;
|
|
83
|
+
/** Lazy-load the feature plugins (conditional formatting, data
|
|
84
|
+
* validation, hyperlinks, notes, tables, comments, drawings, sort,
|
|
85
|
+
* filter, find/replace). Default `true`: plugins whose data is in
|
|
86
|
+
* `initialData` load eagerly before mount (so nothing is dropped on
|
|
87
|
+
* open), the rest idle-load after first paint. Set `false` for the
|
|
88
|
+
* minimal editor (render + formula + numfmt only) — the embed-iframe
|
|
89
|
+
* build does this to stay a single self-contained bundle. */
|
|
90
|
+
lazyPlugins?: boolean;
|
|
91
|
+
/** Locale identifier. Defaults to `LocaleType.EN_US`. */
|
|
92
|
+
locale?: LocaleType;
|
|
93
|
+
/** Locale string bundle. Optional — Univer's default English
|
|
94
|
+
* strings load if omitted. */
|
|
95
|
+
locales?: ILocales;
|
|
96
|
+
/** Univer log level. Defaults to `LogLevel.WARN`. */
|
|
97
|
+
logLevel?: LogLevel;
|
|
98
|
+
/** Univer chrome toggles. Defaults: header / toolbar / footer off,
|
|
99
|
+
* context menu on — matches Casual Sheets' embedded shape. */
|
|
100
|
+
ui?: {
|
|
101
|
+
header?: boolean;
|
|
102
|
+
toolbar?: boolean;
|
|
103
|
+
footer?: boolean;
|
|
104
|
+
contextMenu?: boolean;
|
|
105
|
+
};
|
|
106
|
+
/** Override the Univer theme object (colour palette). Defaults to
|
|
107
|
+
* Univer's `defaultTheme`. Distinct from `appearance` (light/dark). */
|
|
108
|
+
theme?: typeof defaultTheme;
|
|
109
|
+
/** Light or dark mode. Reactive — flipping it re-themes the live
|
|
110
|
+
* editor via `ThemeService.setDarkMode` (canvas colours, notifications,
|
|
111
|
+
* and Univer's own `univer-dark` class). Defaults to light.
|
|
112
|
+
* Note: Univer's Workbench applies the `univer-dark` class to the
|
|
113
|
+
* document root (`<html>`) itself, so dark mode is page-global by
|
|
114
|
+
* Univer's design — a host that embeds the editor inside a light page
|
|
115
|
+
* should scope the editor or accept the global dark CSS. */
|
|
116
|
+
appearance?: 'light' | 'dark';
|
|
117
|
+
/** Container style. Default fills the parent. */
|
|
118
|
+
style?: CSSProperties;
|
|
119
|
+
/** Container className for additional styling hooks. */
|
|
120
|
+
className?: string;
|
|
121
|
+
/** Optional test id for the host container. */
|
|
122
|
+
testId?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const DEFAULT_STYLE: CSSProperties = {
|
|
126
|
+
width: '100%',
|
|
127
|
+
height: '100%',
|
|
128
|
+
position: 'relative',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const DEFAULT_UI = {
|
|
132
|
+
header: false,
|
|
133
|
+
toolbar: false,
|
|
134
|
+
footer: false,
|
|
135
|
+
contextMenu: true,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export function CasualSheets({
|
|
139
|
+
initialData,
|
|
140
|
+
onReady,
|
|
141
|
+
onChange,
|
|
142
|
+
onChangeDebounceMs = 400,
|
|
143
|
+
lazyPlugins = true,
|
|
144
|
+
locale = LocaleType.EN_US,
|
|
145
|
+
locales,
|
|
146
|
+
logLevel = LogLevel.WARN,
|
|
147
|
+
ui,
|
|
148
|
+
theme = defaultTheme,
|
|
149
|
+
appearance = 'light',
|
|
150
|
+
style,
|
|
151
|
+
className,
|
|
152
|
+
testId = 'casual-sheets',
|
|
153
|
+
}: CasualSheetsProps) {
|
|
154
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
155
|
+
// Keep the latest onChange callable without re-subscribing (the effect
|
|
156
|
+
// mounts once). The subscription itself is only wired when onChange was
|
|
157
|
+
// present at mount.
|
|
158
|
+
const onChangeRef = useRef(onChange);
|
|
159
|
+
onChangeRef.current = onChange;
|
|
160
|
+
const hasOnChange = useRef(!!onChange).current;
|
|
161
|
+
// The live FUniver facade, captured at mount so the reactive appearance
|
|
162
|
+
// effect can reach Univer's ThemeService without re-running boot.
|
|
163
|
+
const apiRef = useRef<CasualSheetsAPI | null>(null);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
const container = hostRef.current;
|
|
167
|
+
if (!container) return;
|
|
168
|
+
|
|
169
|
+
const univer = new Univer({
|
|
170
|
+
theme,
|
|
171
|
+
locale,
|
|
172
|
+
locales,
|
|
173
|
+
logLevel,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const uiOpts = { ...DEFAULT_UI, ...ui, container };
|
|
177
|
+
|
|
178
|
+
univer.registerPlugin(UniverRenderEnginePlugin);
|
|
179
|
+
// Formula compute runs on the MAIN THREAD (no RPC worker). The library build
|
|
180
|
+
// externalises @univerjs (see tsup.config.ts), so the host provides a single
|
|
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);
|
|
202
|
+
|
|
203
|
+
let cancelled = false;
|
|
204
|
+
let changeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
205
|
+
let changeSub: { dispose: () => void } | undefined;
|
|
206
|
+
|
|
207
|
+
void (async () => {
|
|
208
|
+
// Eager-load any feature plugin whose data already lives in initialData
|
|
209
|
+
// (CF rules, tables, hyperlinks, …) BEFORE createUnit — Univer's resource
|
|
210
|
+
// manager silently drops keys for plugins that aren't registered when it
|
|
211
|
+
// reads the snapshot. Skipped entirely when lazyPlugins is false.
|
|
212
|
+
if (lazyPlugins) {
|
|
213
|
+
await eagerLoadForSnapshot(univer, initialData);
|
|
214
|
+
if (cancelled) return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
univer.createUnit(UniverInstanceType.UNIVER_SHEET, initialData);
|
|
218
|
+
|
|
219
|
+
const api = createCasualSheetsAPI(FUniver.newAPI(univer));
|
|
220
|
+
apiRef.current = api;
|
|
221
|
+
// Apply the initial appearance now that the editor exists (the reactive
|
|
222
|
+
// effect below also runs on mount, but apiRef may not be set yet when it
|
|
223
|
+
// first fires — this guarantees dark mode from the first paint).
|
|
224
|
+
applyAppearance(api, container, appearance);
|
|
225
|
+
onReady?.(api);
|
|
226
|
+
|
|
227
|
+
// Debounced snapshot stream → onChange. Subscribed AFTER createUnit so the
|
|
228
|
+
// initial unit-creation mutations don't fire a spurious first emit. Uses the
|
|
229
|
+
// mutation hook (CLAUDE.md hard rule), never UI events.
|
|
230
|
+
if (hasOnChange) {
|
|
231
|
+
const injector = (api.univer as unknown as { _injector?: { get(t: unknown): unknown } })
|
|
232
|
+
._injector;
|
|
233
|
+
const cmdSvc = injector?.get(ICommandService) as
|
|
234
|
+
| {
|
|
235
|
+
onMutationExecutedForCollab: (
|
|
236
|
+
l: (info: ICommandInfo, options?: IExecutionOptions) => void,
|
|
237
|
+
) => { dispose: () => void };
|
|
238
|
+
}
|
|
239
|
+
| undefined;
|
|
240
|
+
changeSub = cmdSvc?.onMutationExecutedForCollab(() => {
|
|
241
|
+
if (changeTimer) clearTimeout(changeTimer);
|
|
242
|
+
changeTimer = setTimeout(() => {
|
|
243
|
+
const snap = api.getSnapshot();
|
|
244
|
+
if (snap) onChangeRef.current?.(snap);
|
|
245
|
+
}, onChangeDebounceMs);
|
|
246
|
+
});
|
|
247
|
+
// If we unmounted during the eager-load await, cleanup already ran with
|
|
248
|
+
// changeSub still undefined — dispose this late subscription.
|
|
249
|
+
if (cancelled) changeSub?.dispose();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Idle-load the remaining feature plugins so Insert / Data / Format actions
|
|
253
|
+
// are ready when the user reaches them.
|
|
254
|
+
if (lazyPlugins) idleLoadAll(univer);
|
|
255
|
+
})();
|
|
256
|
+
|
|
257
|
+
return () => {
|
|
258
|
+
cancelled = true;
|
|
259
|
+
if (changeTimer) clearTimeout(changeTimer);
|
|
260
|
+
changeSub?.dispose();
|
|
261
|
+
apiRef.current = null;
|
|
262
|
+
if (lazyPlugins) setUniverForLazyLoad(null);
|
|
263
|
+
// Defer disposal off the React render phase — Univer owns its
|
|
264
|
+
// own React root, and a synchronous unmount mid-render warns
|
|
265
|
+
// and leaves the canvas detached.
|
|
266
|
+
const toDispose = univer;
|
|
267
|
+
queueMicrotask(() => toDispose.dispose());
|
|
268
|
+
};
|
|
269
|
+
// initialData is intentionally NOT in the dep array — the wrapper
|
|
270
|
+
// mounts the snapshot once. Hosts that need to swap workbooks
|
|
271
|
+
// change the React `key` to force a remount.
|
|
272
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
275
|
+
// Reactive appearance. Runs after the boot effect (apiRef populated on first
|
|
276
|
+
// mount) and re-runs whenever `appearance` flips, re-theming the live editor.
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const api = apiRef.current;
|
|
279
|
+
const container = hostRef.current;
|
|
280
|
+
if (!api || !container) return;
|
|
281
|
+
applyAppearance(api, container, appearance);
|
|
282
|
+
}, [appearance]);
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
ref={hostRef}
|
|
287
|
+
style={{ ...DEFAULT_STYLE, ...style }}
|
|
288
|
+
className={className}
|
|
289
|
+
data-testid={testId}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Apply light/dark to a live editor. `ThemeService.setDarkMode` is the source of
|
|
296
|
+
* truth — it flips the canvas colours, the internals that subscribe to
|
|
297
|
+
* `darkMode$` (notifications, message containers), AND Univer's Workbench toggles
|
|
298
|
+
* the `univer-dark` class on the document root for its compiled dark CSS. We also
|
|
299
|
+
* mirror the class onto the editor container as race-insurance (the Workbench
|
|
300
|
+
* effect can land a frame after ours). Mirrors the app's ThemeBridge.
|
|
301
|
+
*/
|
|
302
|
+
function applyAppearance(
|
|
303
|
+
api: CasualSheetsAPI,
|
|
304
|
+
container: HTMLElement,
|
|
305
|
+
appearance: 'light' | 'dark',
|
|
306
|
+
): void {
|
|
307
|
+
const dark = appearance === 'dark';
|
|
308
|
+
container.classList.toggle('univer-dark', dark);
|
|
309
|
+
try {
|
|
310
|
+
const injector = (api.univer as unknown as { _injector?: { get(t: unknown): unknown } })
|
|
311
|
+
._injector;
|
|
312
|
+
const themeService = injector?.get(ThemeService) as
|
|
313
|
+
| { setDarkMode(b: boolean): void; darkMode: boolean }
|
|
314
|
+
| undefined;
|
|
315
|
+
if (themeService && themeService.darkMode !== dark) themeService.setDarkMode(dark);
|
|
316
|
+
} catch {
|
|
317
|
+
/* ThemeService unavailable — the class toggle still themes visible chrome */
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CasualSheetsIframe — the iframe-mounting variant of `<CasualSheets>`.
|
|
3
|
+
* Sheet sibling of `@casualoffice/docs`'s `<CasualEditorIframe>`;
|
|
4
|
+
* see doc 16 in the parent repo.
|
|
5
|
+
*
|
|
6
|
+
* Differs from CasualEditorIframe in one place: the embed URL params
|
|
7
|
+
* carry `app=sheet` and the embed-runtime inside the iframe knows to
|
|
8
|
+
* convert raw xlsx bytes into an `IWorkbookData` snapshot via
|
|
9
|
+
* `xlsxToWorkbookData` before mounting `<CasualSheets>`.
|
|
10
|
+
*
|
|
11
|
+
* Public surface intentionally identical to CasualSheets. v0.6 will
|
|
12
|
+
* rename CasualSheetsIframe → CasualSheets and the existing direct-
|
|
13
|
+
* mount component → CasualSheetsDirect.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
forwardRef,
|
|
18
|
+
useCallback,
|
|
19
|
+
useEffect,
|
|
20
|
+
useRef,
|
|
21
|
+
type CSSProperties,
|
|
22
|
+
type MutableRefObject,
|
|
23
|
+
} from 'react';
|
|
24
|
+
|
|
25
|
+
import { EmbedHostTransport } from '../embed/EmbedHostTransport';
|
|
26
|
+
import type {
|
|
27
|
+
CasualErrorData,
|
|
28
|
+
CommandExecuteData,
|
|
29
|
+
LoadResponseData,
|
|
30
|
+
SaveResponseData,
|
|
31
|
+
SelectionChangedData,
|
|
32
|
+
SelectionFormatStateData,
|
|
33
|
+
TelemetryEventData,
|
|
34
|
+
} from '../embed/protocol';
|
|
35
|
+
|
|
36
|
+
/** What the host-side load/save handlers consume + return. The wrapper
|
|
37
|
+
* binds these to the host's FileSource via simple adapters. */
|
|
38
|
+
export interface HostFileBridge {
|
|
39
|
+
open(docId: string): Promise<{ bytes: ArrayBuffer; name: string; etag?: string }>;
|
|
40
|
+
save?(docId: string, bytes: ArrayBuffer, opts?: { etag?: string }): Promise<{ etag: string }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CasualSheetsIframeRef {
|
|
44
|
+
setViewMode(mode: 'preview' | 'editor'): void;
|
|
45
|
+
iframe(): HTMLIFrameElement | null;
|
|
46
|
+
/** Dispatch a formatting / navigation command (bold, italic, undo, …)
|
|
47
|
+
* against the iframe's active selection. v0.6+. */
|
|
48
|
+
executeCommand(command: CommandExecuteData['command']): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CasualSheetsIframeProps {
|
|
52
|
+
/** Host-side bytes bridge. The wrapper round-trips load / save to the
|
|
53
|
+
* iframe through postMessage; bytes never live in the iframe's origin
|
|
54
|
+
* except in-memory while the workbook is open. */
|
|
55
|
+
fileSource: HostFileBridge;
|
|
56
|
+
docId: string;
|
|
57
|
+
/** Default `editor`. Live changes push casual.command.set.viewmode. */
|
|
58
|
+
viewMode?: 'preview' | 'editor';
|
|
59
|
+
/** Default `/embed/sheets`. Consumer copies the SDK's
|
|
60
|
+
* `dist/embed/{embed.html, embed-runtime.js, embed-runtime.css}`
|
|
61
|
+
* to this path. */
|
|
62
|
+
embedBasePath?: string;
|
|
63
|
+
onSelectionChanged?: (data: SelectionChangedData) => void;
|
|
64
|
+
/** Fires when the active cell's format flags change (bold, italic,
|
|
65
|
+
* …). Drive's custom toolbar reflects this state in the button
|
|
66
|
+
* "pressed" indicators. v0.6+. */
|
|
67
|
+
onSelectionFormatState?: (data: SelectionFormatStateData) => void;
|
|
68
|
+
onTelemetry?: (data: TelemetryEventData) => void;
|
|
69
|
+
onError?: (data: CasualErrorData) => void;
|
|
70
|
+
style?: CSSProperties;
|
|
71
|
+
className?: string;
|
|
72
|
+
testId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const DEFAULT_STYLE: CSSProperties = {
|
|
76
|
+
width: '100%',
|
|
77
|
+
height: '100%',
|
|
78
|
+
border: 'none',
|
|
79
|
+
display: 'block',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const CasualSheetsIframe = forwardRef<CasualSheetsIframeRef, CasualSheetsIframeProps>(
|
|
83
|
+
function CasualSheetsIframe(props, ref) {
|
|
84
|
+
const {
|
|
85
|
+
fileSource,
|
|
86
|
+
docId,
|
|
87
|
+
viewMode = 'editor',
|
|
88
|
+
embedBasePath = '/embed/sheets',
|
|
89
|
+
onSelectionChanged,
|
|
90
|
+
onSelectionFormatState,
|
|
91
|
+
onTelemetry,
|
|
92
|
+
onError,
|
|
93
|
+
style,
|
|
94
|
+
className,
|
|
95
|
+
testId = 'casual-sheets-iframe',
|
|
96
|
+
} = props;
|
|
97
|
+
|
|
98
|
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
99
|
+
const transportRef = useRef<EmbedHostTransport | null>(null);
|
|
100
|
+
const fileSourceRef = useRef(fileSource);
|
|
101
|
+
fileSourceRef.current = fileSource;
|
|
102
|
+
|
|
103
|
+
const onLoad = useCallback(async (req: { docId: string }): Promise<LoadResponseData> => {
|
|
104
|
+
try {
|
|
105
|
+
const { bytes, name, etag } = await fileSourceRef.current.open(req.docId);
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
bytes,
|
|
109
|
+
fileName: name,
|
|
110
|
+
...(etag !== undefined ? { etag } : {}),
|
|
111
|
+
};
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
code: 'open_failed',
|
|
116
|
+
message: err instanceof Error ? err.message : String(err),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const onSave = useCallback(
|
|
122
|
+
async (req: {
|
|
123
|
+
docId: string;
|
|
124
|
+
bytes: ArrayBuffer;
|
|
125
|
+
baseEtag?: string;
|
|
126
|
+
}): Promise<SaveResponseData> => {
|
|
127
|
+
try {
|
|
128
|
+
if (!fileSourceRef.current.save) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
code: 'save_unsupported',
|
|
132
|
+
message: 'host fileSource does not implement save',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const opts = req.baseEtag !== undefined ? { etag: req.baseEtag } : undefined;
|
|
136
|
+
const { etag } = await fileSourceRef.current.save(req.docId, req.bytes, opts);
|
|
137
|
+
return { ok: true, etag };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
code: 'save_failed',
|
|
142
|
+
message: err instanceof Error ? err.message : String(err),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const onIframeLoad = useCallback(() => {
|
|
150
|
+
const iframe = iframeRef.current;
|
|
151
|
+
if (!iframe?.contentWindow) return;
|
|
152
|
+
transportRef.current?.destroy();
|
|
153
|
+
const transport = new EmbedHostTransport({
|
|
154
|
+
app: 'sheet',
|
|
155
|
+
iframeWindow: iframe.contentWindow,
|
|
156
|
+
embedOrigin: window.location.origin,
|
|
157
|
+
});
|
|
158
|
+
transport.on({
|
|
159
|
+
onLoadRequest: onLoad,
|
|
160
|
+
onSaveRequest: onSave,
|
|
161
|
+
...(onSelectionChanged ? { onSelectionChanged } : {}),
|
|
162
|
+
...(onSelectionFormatState ? { onSelectionFormatState } : {}),
|
|
163
|
+
...(onTelemetry ? { onTelemetry } : {}),
|
|
164
|
+
...(onError ? { onError } : {}),
|
|
165
|
+
onEditorReady: () => {
|
|
166
|
+
transport.sendHostHello({ capabilities: ['load', 'save'] });
|
|
167
|
+
transport.sendSetViewMode({ viewMode });
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
transportRef.current = transport;
|
|
171
|
+
}, [
|
|
172
|
+
onLoad,
|
|
173
|
+
onSave,
|
|
174
|
+
onSelectionChanged,
|
|
175
|
+
onSelectionFormatState,
|
|
176
|
+
onTelemetry,
|
|
177
|
+
onError,
|
|
178
|
+
viewMode,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
transportRef.current?.sendSetViewMode({ viewMode });
|
|
183
|
+
}, [viewMode]);
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
return () => {
|
|
187
|
+
transportRef.current?.destroy();
|
|
188
|
+
transportRef.current = null;
|
|
189
|
+
};
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
if (ref) {
|
|
193
|
+
const apiRef = ref as MutableRefObject<CasualSheetsIframeRef | null>;
|
|
194
|
+
apiRef.current = {
|
|
195
|
+
setViewMode: (mode) => transportRef.current?.sendSetViewMode({ viewMode: mode }),
|
|
196
|
+
iframe: () => iframeRef.current,
|
|
197
|
+
executeCommand: (command) => transportRef.current?.sendCommandExecute({ command }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const url =
|
|
202
|
+
`${embedBasePath}/embed.html` +
|
|
203
|
+
`?app=sheet` +
|
|
204
|
+
`&docId=${encodeURIComponent(docId)}` +
|
|
205
|
+
`&viewMode=${viewMode}`;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<iframe
|
|
209
|
+
ref={iframeRef}
|
|
210
|
+
src={url}
|
|
211
|
+
onLoad={onIframeLoad}
|
|
212
|
+
title="Casual Sheets"
|
|
213
|
+
sandbox="allow-scripts allow-same-origin allow-downloads allow-modals"
|
|
214
|
+
style={{ ...DEFAULT_STYLE, ...style }}
|
|
215
|
+
className={className}
|
|
216
|
+
data-testid={testId}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CasualSheetsAPI — the imperative ref handed to a host via `<CasualSheets onReady>`.
|
|
3
|
+
*
|
|
4
|
+
* This is the SDK's stable integration surface (Excalidraw's model: props +
|
|
5
|
+
* imperative ref). Hosts drive the editor through these methods rather than
|
|
6
|
+
* reaching into Univer directly; `api.univer` is the documented escape hatch and
|
|
7
|
+
* is explicitly NOT covered by semver — everything else here is.
|
|
8
|
+
*
|
|
9
|
+
* Scope of THIS batch (SDK restructure, Phase 1 step 3):
|
|
10
|
+
* getSnapshot / loadSnapshot / getSelection / executeCommand / univer
|
|
11
|
+
*
|
|
12
|
+
* Deferred to later, clearly-scoped batches (kept off the type until they work,
|
|
13
|
+
* so the surface never advertises a method that throws):
|
|
14
|
+
* - importXlsx / exportXlsx — the xlsx I/O batch. importXlsx must NOT be a
|
|
15
|
+
* plain `import('../xlsx')` here: the main tsup config is `splitting:false`,
|
|
16
|
+
* so a dynamic import gets inlined and balloons the editor entry from ~11KB
|
|
17
|
+
* to ~200KB of parser code for hosts that never open a file. The xlsx-I/O
|
|
18
|
+
* batch wires it as its own chunk (and lifts the export converter out of
|
|
19
|
+
* apps/web — the SDK xlsx module is import-only today).
|
|
20
|
+
* - attachCollab — belongs to the storage/collab adapter phase (Phase 2);
|
|
21
|
+
* the editor ships collab-unaware until then.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Side-effect import: registers the Sheets FUniver mixins
|
|
25
|
+
// (getActiveWorkbook / createWorkbook / FWorkbook.save / FRange.getRange / …)
|
|
26
|
+
// onto the core FUniver facade — both the runtime methods AND the TypeScript
|
|
27
|
+
// type augmentation this file relies on. Without it, FUniver is the bare core
|
|
28
|
+
// facade and these methods exist neither at type-check nor at runtime.
|
|
29
|
+
import '@univerjs/sheets/facade';
|
|
30
|
+
import { ThemeService } from '@univerjs/core';
|
|
31
|
+
import type { FUniver } from '@univerjs/core/facade';
|
|
32
|
+
import type { IRange, IWorkbookData } from '@univerjs/core';
|
|
33
|
+
|
|
34
|
+
/** The active selection, as a sheet-scoped range. */
|
|
35
|
+
export interface RangeRef {
|
|
36
|
+
/** Workbook unit id the selection belongs to. */
|
|
37
|
+
unitId: string;
|
|
38
|
+
/** Worksheet (sub-unit) id the selection belongs to. */
|
|
39
|
+
sheetId: string;
|
|
40
|
+
/** `{ startRow, startColumn, endRow, endColumn }`. */
|
|
41
|
+
range: IRange;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CasualSheetsAPI {
|
|
45
|
+
/** Current workbook as an `IWorkbookData` snapshot. `null` before the unit
|
|
46
|
+
* is created (shouldn't happen after `onReady`, but typed defensively). */
|
|
47
|
+
getSnapshot(): IWorkbookData | null;
|
|
48
|
+
/** Replace the workbook with a new snapshot. Disposes the current unit and
|
|
49
|
+
* mounts `data` as a fresh one. */
|
|
50
|
+
loadSnapshot(data: IWorkbookData): void;
|
|
51
|
+
/** The active selection, or `null` when there is none. */
|
|
52
|
+
getSelection(): RangeRef | null;
|
|
53
|
+
/** Dispatch a Univer command by id. Resolves to the command's boolean
|
|
54
|
+
* result. */
|
|
55
|
+
executeCommand(id: string, params?: object): Promise<boolean>;
|
|
56
|
+
/** Imperative light/dark switch — the API equivalent of the reactive
|
|
57
|
+
* `appearance` prop. Flips Univer's `ThemeService.setDarkMode` (canvas
|
|
58
|
+
* colours + the `univer-dark` class Univer applies to the document root). */
|
|
59
|
+
setTheme(appearance: 'light' | 'dark'): void;
|
|
60
|
+
/** The FUniver facade — documented escape hatch, NOT covered by semver. */
|
|
61
|
+
univer: FUniver;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the imperative API over a live FUniver facade. The wrapper holds no
|
|
66
|
+
* state of its own — every call reads the current active workbook, so it stays
|
|
67
|
+
* correct across `loadSnapshot` swaps without the host re-acquiring the ref.
|
|
68
|
+
*/
|
|
69
|
+
export function createCasualSheetsAPI(univerAPI: FUniver): CasualSheetsAPI {
|
|
70
|
+
return {
|
|
71
|
+
univer: univerAPI,
|
|
72
|
+
|
|
73
|
+
getSnapshot() {
|
|
74
|
+
return univerAPI.getActiveWorkbook()?.save() ?? null;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
loadSnapshot(data) {
|
|
78
|
+
const current = univerAPI.getActiveWorkbook();
|
|
79
|
+
if (current) univerAPI.disposeUnit(current.getId());
|
|
80
|
+
univerAPI.createWorkbook(data);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
getSelection() {
|
|
84
|
+
const wb = univerAPI.getActiveWorkbook();
|
|
85
|
+
const range = wb?.getActiveRange();
|
|
86
|
+
if (!wb || !range) return null;
|
|
87
|
+
return {
|
|
88
|
+
unitId: wb.getId(),
|
|
89
|
+
sheetId: wb.getActiveSheet().getSheetId(),
|
|
90
|
+
range: range.getRange(),
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
executeCommand(id, params) {
|
|
95
|
+
return univerAPI.executeCommand(id, params) as Promise<boolean>;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
setTheme(appearance) {
|
|
99
|
+
const dark = appearance === 'dark';
|
|
100
|
+
const injector = (univerAPI as unknown as { _injector?: { get(t: unknown): unknown } })
|
|
101
|
+
._injector;
|
|
102
|
+
const themeService = injector?.get(ThemeService) as
|
|
103
|
+
| { setDarkMode(b: boolean): void; darkMode: boolean }
|
|
104
|
+
| undefined;
|
|
105
|
+
if (themeService && themeService.darkMode !== dark) themeService.setDarkMode(dark);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sheets surface — React wrappers around Univer Sheets.
|
|
3
|
+
*/
|
|
4
|
+
export { CasualSheets, type CasualSheetsProps } from './CasualSheets';
|
|
5
|
+
export { createCasualSheetsAPI, type CasualSheetsAPI, type RangeRef } from './api';
|
|
6
|
+
export {
|
|
7
|
+
CasualSheetsIframe,
|
|
8
|
+
type CasualSheetsIframeProps,
|
|
9
|
+
type CasualSheetsIframeRef,
|
|
10
|
+
type HostFileBridge,
|
|
11
|
+
} from './CasualSheetsIframe';
|