@casualoffice/sheets 0.12.0 → 0.14.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.
@@ -235,13 +235,13 @@ export class EmbedTransport {
235
235
  await this.handlers.onHostHello?.(env.data as HostHelloData);
236
236
  this.sendReady();
237
237
  return;
238
- case 'casual.command.setReadOnly':
238
+ case 'casual.command.set.readonly':
239
239
  await this.handlers.onCommandSetReadOnly?.(env.data as CommandSetReadOnlyData);
240
240
  return;
241
- case 'casual.command.setTheme':
241
+ case 'casual.command.set.theme':
242
242
  await this.handlers.onCommandSetTheme?.(env.data as CommandSetThemeData);
243
243
  return;
244
- case 'casual.command.setLocale':
244
+ case 'casual.command.set.locale':
245
245
  await this.handlers.onCommandSetLocale?.(env.data as CommandSetLocaleData);
246
246
  return;
247
247
  case 'casual.command.set.viewmode':
@@ -32,6 +32,7 @@ interface EmbedUrlConfig {
32
32
  app: CasualApp;
33
33
  docId: string;
34
34
  viewMode: 'preview' | 'editor';
35
+ theme: 'light' | 'dark' | 'system';
35
36
  }
36
37
 
37
38
  function parseUrlConfig(search: string): EmbedUrlConfig {
@@ -40,7 +41,22 @@ function parseUrlConfig(search: string): EmbedUrlConfig {
40
41
  const docId = params.get('docId') ?? '';
41
42
  const viewModeParam = params.get('viewMode');
42
43
  const viewMode: 'preview' | 'editor' = viewModeParam === 'editor' ? 'editor' : 'preview';
43
- return { app, docId, viewMode };
44
+ // The host passes its theme so the embedded editor matches light/dark.
45
+ // Default `system` follows the OS/host colour-scheme; the host can also
46
+ // push live changes over `casual.command.set.theme`.
47
+ const themeParam = params.get('theme');
48
+ const theme: 'light' | 'dark' | 'system' =
49
+ themeParam === 'dark' ? 'dark' : themeParam === 'light' ? 'light' : 'system';
50
+ return { app, docId, viewMode, theme };
51
+ }
52
+
53
+ /** Resolve `system` against the iframe's media query; `light`/`dark` pass through. */
54
+ function resolveAppearance(theme: 'light' | 'dark' | 'system'): 'light' | 'dark' {
55
+ if (theme === 'light' || theme === 'dark') return theme;
56
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
57
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
58
+ }
59
+ return 'light';
44
60
  }
45
61
 
46
62
  export interface MountEmbeddedOptions {
@@ -112,7 +128,12 @@ export function mountEmbedded(opts: MountEmbeddedOptions): void {
112
128
 
113
129
  const reactRoot = createRoot(opts.root);
114
130
  reactRoot.render(
115
- <EmbeddedSheets transport={transport} docId={config.docId} initialViewMode={config.viewMode} />,
131
+ <EmbeddedSheets
132
+ transport={transport}
133
+ docId={config.docId}
134
+ initialViewMode={config.viewMode}
135
+ initialTheme={config.theme}
136
+ />,
116
137
  );
117
138
  }
118
139
 
@@ -123,31 +144,53 @@ function EmbeddedSheets({
123
144
  transport,
124
145
  docId,
125
146
  initialViewMode,
147
+ initialTheme,
126
148
  }: {
127
149
  transport: EmbedTransport;
128
150
  docId: string;
129
151
  initialViewMode: 'preview' | 'editor';
152
+ initialTheme: 'light' | 'dark' | 'system';
130
153
  }) {
131
154
  const [data, setData] = useState<IWorkbookData | null>(null);
132
155
  const [errorMsg, setErrorMsg] = useState<string | null>(null);
133
156
 
157
+ // Theme: track the host's choice (light/dark/system) and resolve `system`
158
+ // live against the iframe's prefers-color-scheme. The host pushes changes
159
+ // over `casual.command.set.theme`; passed to <CasualSheets appearance>,
160
+ // which flips Univer's `univer-dark` class + ThemeService so the canvas,
161
+ // headers, gridlines and chrome all match.
162
+ const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(initialTheme);
163
+ const [appearance, setAppearance] = useState<'light' | 'dark'>(() =>
164
+ resolveAppearance(initialTheme),
165
+ );
166
+ useEffect(() => {
167
+ setAppearance(resolveAppearance(theme));
168
+ if (theme !== 'system' || typeof window === 'undefined' || !window.matchMedia) return;
169
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
170
+ const onChange = () => setAppearance(mq.matches ? 'dark' : 'light');
171
+ mq.addEventListener('change', onChange);
172
+ return () => mq.removeEventListener('change', onChange);
173
+ }, [theme]);
174
+
134
175
  const [viewMode, setViewMode] = useState<'preview' | 'editor'>(initialViewMode);
135
- // Editor mode flips `header: true` so Univer's formula bar (A1 cell
136
- // ref, fx button) and menubar render at the top of the iframe —
137
- // visually distinct from preview's canvas-only surface. `toolbar` and
138
- // `footer` stay off: Univer's ribbon and sheet-tabs slot resolve
139
- // services (IRPCChannelService, sheet-drawing) at construction that
140
- // the SDK doesn't bundle a worker for, and turning them on lights up
141
- // `[redi]: Cannot find "Kb" registered by any injector` and the
142
- // canvas never paints. Cells stay editable in editor mode via direct
143
- // keyboard input on the focused cell.
144
- const ui =
145
- viewMode === 'editor'
146
- ? { header: true, toolbar: false, footer: false, contextMenu: true }
147
- : { header: false, toolbar: false, footer: false, contextMenu: true };
176
+ // Editor mode renders the SDK's OWN React chrome (`chrome="full"`): the
177
+ // menu bar (Edit/Insert/Format/Data/View), the rich formatting toolbar,
178
+ // the formula bar, sheet tabs and status bar — the full native editor, so
179
+ // hosts embed a complete spreadsheet and only frame/brand it (they do NOT
180
+ // hand-roll a toolbar). We use the SDK chrome, NOT Univer's built-in `ui`
181
+ // toolbar/footer: Univer's ribbon + sheet-tabs slots resolve services
182
+ // (IRPCChannelService, sheet-drawing) the single-file embed doesn't bundle,
183
+ // which throws `[redi]: Cannot find registered by any injector`. The SDK
184
+ // chrome is plain React over the facade, so it has no such dependency.
185
+ //
186
+ // Preview = `chrome="none"`: bare canvas, made read-only by the veto in
187
+ // onReady. Univer's own chrome stays off in both modes.
188
+ const chrome: 'full' | 'none' = viewMode === 'editor' ? 'full' : 'none';
189
+ const ui = { header: false, toolbar: false, footer: false, contextMenu: true };
148
190
 
149
191
  useEffect(() => {
150
192
  transport.on({
193
+ onCommandSetTheme: ({ theme: next }) => setTheme(next),
151
194
  onCommandSetViewMode: ({ viewMode: next }) => setViewMode(next),
152
195
  });
153
196
  }, [transport]);
@@ -225,6 +268,8 @@ function EmbeddedSheets({
225
268
  <CasualSheets
226
269
  key={viewMode}
227
270
  initialData={data}
271
+ chrome={chrome}
272
+ appearance={appearance}
228
273
  ui={ui}
229
274
  // Seed the en-US string bundle. Without `locales`, Univer's
230
275
  // LocaleService throws "Locale not initialized" and the workbench
@@ -233,12 +278,17 @@ function EmbeddedSheets({
233
278
  // this from the host's `locales` prop; the iframe has no host to
234
279
  // pass one, so the self-contained runtime bundles a minimal set.
235
280
  locales={EMBED_LOCALES}
236
- // The embed runtime is bundled as ONE self-contained file (tsup
237
- // `noExternal: /.*/`). Lazy plugins would emit per-feature dynamic
238
- // chunks into dist/embed/, breaking that single-file deployment, so the
239
- // iframe ships the minimal editor. Hosts that need feature plugins use
240
- // the React `<CasualSheets>` component directly (lazyPlugins defaults on).
241
- lazyPlugins={false}
281
+ // Enable the FULL feature set (tables, sort, filter, conditional
282
+ // formatting, data validation, drawing, hyperlinks, notes, comments,
283
+ // find/replace) so the embed matches the real app. The tsup embed
284
+ // config is `splitting: false` + `noExternal: /.*/`, so the lazy
285
+ // loader's dynamic `import()`s are INLINED into the single
286
+ // embed-runtime.js rather than emitted as separate chunks — the
287
+ // single-file deployment is preserved. With lazyPlugins on, the loader
288
+ // also eager-loads any feature whose data is already in the snapshot
289
+ // (so opening a file with tables/CF never silently drops it) and
290
+ // idle-loads the rest, so the toolbar/menu feature actions resolve.
291
+ lazyPlugins={true}
242
292
  // Phase 2 save/exit contract — the iframe surface of the same
243
293
  // `onSave` / `onExit` hooks the React component exposes. The editor
244
294
  // never persists; it hands the snapshot to the host over postMessage