@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.
Files changed (92) hide show
  1. package/LICENSE +200 -0
  2. package/dist/embed/embed-runtime.js +537 -0
  3. package/dist/embed/embed.html +29 -0
  4. package/dist/embed/parser.worker.js +48474 -0
  5. package/dist/embed.cjs +225 -0
  6. package/dist/embed.cjs.map +1 -0
  7. package/dist/embed.d.cts +100 -0
  8. package/dist/embed.d.ts +100 -0
  9. package/dist/embed.js +204 -0
  10. package/dist/embed.js.map +1 -0
  11. package/dist/index.cjs +1549 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +9 -0
  14. package/dist/index.d.ts +9 -0
  15. package/dist/index.js +1530 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/parser.worker.cjs +48469 -0
  18. package/dist/parser.worker.cjs.map +1 -0
  19. package/dist/parser.worker.js +48474 -0
  20. package/dist/parser.worker.js.map +1 -0
  21. package/dist/protocol--KyBQUjU.d.cts +171 -0
  22. package/dist/protocol-cEzy7S0i.d.ts +171 -0
  23. package/dist/sheets.cjs +677 -0
  24. package/dist/sheets.cjs.map +1 -0
  25. package/dist/sheets.d.cts +177 -0
  26. package/dist/sheets.d.ts +177 -0
  27. package/dist/sheets.js +658 -0
  28. package/dist/sheets.js.map +1 -0
  29. package/dist/signing.cjs +706 -0
  30. package/dist/signing.cjs.map +1 -0
  31. package/dist/signing.d.cts +141 -0
  32. package/dist/signing.d.ts +141 -0
  33. package/dist/signing.js +683 -0
  34. package/dist/signing.js.map +1 -0
  35. package/dist/styles.cjs +10 -0
  36. package/dist/styles.cjs.map +1 -0
  37. package/dist/styles.d.cts +2 -0
  38. package/dist/styles.d.ts +2 -0
  39. package/dist/styles.js +8 -0
  40. package/dist/styles.js.map +1 -0
  41. package/dist/types-s_O0u6Cg.d.cts +90 -0
  42. package/dist/types-s_O0u6Cg.d.ts +90 -0
  43. package/dist/univer.cjs +220 -0
  44. package/dist/univer.cjs.map +1 -0
  45. package/dist/univer.d.cts +60 -0
  46. package/dist/univer.d.ts +60 -0
  47. package/dist/univer.js +187 -0
  48. package/dist/univer.js.map +1 -0
  49. package/dist/xlsx.cjs +3388 -0
  50. package/dist/xlsx.cjs.map +1 -0
  51. package/dist/xlsx.d.cts +383 -0
  52. package/dist/xlsx.d.ts +383 -0
  53. package/dist/xlsx.js +3383 -0
  54. package/dist/xlsx.js.map +1 -0
  55. package/package.json +293 -0
  56. package/src/embed/EmbedHostTransport.ts +226 -0
  57. package/src/embed/EmbedTransport.ts +323 -0
  58. package/src/embed/EmbedTransport.unit.test.ts +161 -0
  59. package/src/embed/index.ts +40 -0
  60. package/src/embed/protocol.ts +258 -0
  61. package/src/embed-runtime/embed.html +29 -0
  62. package/src/embed-runtime/index.tsx +440 -0
  63. package/src/index.ts +16 -0
  64. package/src/sheets/CasualSheets.tsx +319 -0
  65. package/src/sheets/CasualSheetsIframe.tsx +220 -0
  66. package/src/sheets/api.ts +108 -0
  67. package/src/sheets/index.ts +11 -0
  68. package/src/signing/SigningPane.tsx +374 -0
  69. package/src/signing/SigningProvider.tsx +126 -0
  70. package/src/signing/captures.tsx +316 -0
  71. package/src/signing/controller.ts +151 -0
  72. package/src/signing/controller.unit.test.ts +133 -0
  73. package/src/signing/index.ts +44 -0
  74. package/src/signing/types.ts +89 -0
  75. package/src/styles.ts +16 -0
  76. package/src/univer/index.ts +17 -0
  77. package/src/univer/lazy-plugins.ts +280 -0
  78. package/src/xlsx/_perf.ts +14 -0
  79. package/src/xlsx/_snapshot-constants.ts +15 -0
  80. package/src/xlsx/comments-resource.ts +209 -0
  81. package/src/xlsx/constants.ts +9 -0
  82. package/src/xlsx/data-validation-resource.ts +219 -0
  83. package/src/xlsx/import.ts +35 -0
  84. package/src/xlsx/index.ts +40 -0
  85. package/src/xlsx/page-setup-resource.ts +205 -0
  86. package/src/xlsx/parse-impl.ts +418 -0
  87. package/src/xlsx/parse-in-worker.ts +82 -0
  88. package/src/xlsx/parser.worker.ts +39 -0
  89. package/src/xlsx/passthrough-resource.ts +175 -0
  90. package/src/xlsx/pivot-passthrough.ts +359 -0
  91. package/src/xlsx/style-mapping.ts +171 -0
  92. 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';