@casualoffice/sheets 0.18.0 → 0.19.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/embed/embed-runtime.js +205 -161
- package/dist/index.cjs +5971 -1572
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5961 -1548
- package/dist/index.js.map +1 -1
- package/dist/sheets.cjs +4937 -538
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.js +4945 -532
- package/dist/sheets.js.map +1 -1
- package/package.json +8 -7
- package/src/charts/ChartContextMenu.tsx +264 -0
- package/src/charts/ChartLayer.tsx +333 -0
- package/src/charts/ChartOverlay.tsx +293 -0
- package/src/charts/ChartsPanel.tsx +211 -0
- package/src/charts/FormatChartDialog.tsx +419 -0
- package/src/charts/InsertChartDialog.tsx +478 -0
- package/src/charts/build-option.ts +476 -0
- package/src/charts/charts-context.tsx +205 -0
- package/src/charts/echarts-init.ts +58 -0
- package/src/charts/hit-test.ts +130 -0
- package/src/charts/insert-chart.ts +106 -0
- package/src/charts/naming.ts +38 -0
- package/src/charts/render-to-png.ts +117 -0
- package/src/charts/resources.ts +108 -0
- package/src/charts/types.ts +239 -0
- package/src/charts/univer-dom.ts +102 -0
- package/src/chrome/CommentsPanel.tsx +427 -0
- package/src/chrome/HistoryPanel.tsx +319 -0
- package/src/chrome/PanelHost.tsx +55 -0
- package/src/chrome/PanelRail.tsx +90 -0
- package/src/chrome/PivotFieldsPanel.tsx +1052 -0
- package/src/chrome/TablesPanel.tsx +301 -0
- package/src/chrome/panel-context.tsx +55 -0
- package/src/chrome/panel-registry.ts +48 -0
- package/src/sheets/CasualSheets.tsx +60 -34
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License").
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tables side panel: lists the formatted tables on the active sheet with
|
|
9
|
+
* inline rename, theme swatches, and delete. Ported from the standalone app's
|
|
10
|
+
* TablesPanel — the app-only `useUI`/`useBusy` couplings are replaced by the
|
|
11
|
+
* panel host's `onClose`, and the Univer facade is reached through `api.univer`.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useMemo, useState, type CSSProperties } from 'react';
|
|
14
|
+
|
|
15
|
+
import { ensurePluginByName } from '../univer';
|
|
16
|
+
import type { PanelComponentProps } from './extensions';
|
|
17
|
+
import { Icon } from './Icon';
|
|
18
|
+
|
|
19
|
+
// Univer's six built-in table themes, surfaced as swatches.
|
|
20
|
+
const TABLE_THEMES = [
|
|
21
|
+
{ id: 'table-default-0', label: 'Indigo', swatch: '#6280F9' },
|
|
22
|
+
{ id: 'table-default-1', label: 'Teal', swatch: '#16BDCA' },
|
|
23
|
+
{ id: 'table-default-2', label: 'Green', swatch: '#31C48D' },
|
|
24
|
+
{ id: 'table-default-3', label: 'Purple', swatch: '#AC94FA' },
|
|
25
|
+
{ id: 'table-default-4', label: 'Pink', swatch: '#F17EBB' },
|
|
26
|
+
{ id: 'table-default-5', label: 'Red', swatch: '#F98080' },
|
|
27
|
+
] as const;
|
|
28
|
+
type TableThemeId = (typeof TABLE_THEMES)[number]['id'];
|
|
29
|
+
|
|
30
|
+
interface TableRange {
|
|
31
|
+
startRow: number;
|
|
32
|
+
startColumn: number;
|
|
33
|
+
endRow: number;
|
|
34
|
+
endColumn: number;
|
|
35
|
+
}
|
|
36
|
+
interface RawTable {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
subUnitId: string;
|
|
40
|
+
range: TableRange;
|
|
41
|
+
}
|
|
42
|
+
interface TableInfo extends RawTable {
|
|
43
|
+
styleId: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function colLetters(col: number): string {
|
|
47
|
+
let n = col + 1;
|
|
48
|
+
let out = '';
|
|
49
|
+
while (n > 0) {
|
|
50
|
+
const r = (n - 1) % 26;
|
|
51
|
+
out = String.fromCharCode(65 + r) + out;
|
|
52
|
+
n = Math.floor((n - 1) / 26);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function toA1(r: TableRange): string {
|
|
57
|
+
const start = `${colLetters(r.startColumn)}${r.startRow + 1}`;
|
|
58
|
+
const end = `${colLetters(r.endColumn)}${r.endRow + 1}`;
|
|
59
|
+
return start === end ? start : `${start}:${end}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const REFRESH_CMDS = new Set([
|
|
63
|
+
'sheet.command.add-table',
|
|
64
|
+
'sheet.command.delete-table',
|
|
65
|
+
'sheet.command.set-table-config',
|
|
66
|
+
'sheet.mutation.add-table',
|
|
67
|
+
'sheet.mutation.set-table-config',
|
|
68
|
+
'sheet.mutation.delete-table',
|
|
69
|
+
'sheet.operation.set-worksheet-activate',
|
|
70
|
+
'doc.command-replace-snapshot',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
74
|
+
function readTables(univer: any, sheetId: string | null): TableInfo[] {
|
|
75
|
+
try {
|
|
76
|
+
const wb = univer.getActiveWorkbook?.();
|
|
77
|
+
if (!wb) return [];
|
|
78
|
+
const all: RawTable[] = wb.getTableList?.() ?? [];
|
|
79
|
+
return all
|
|
80
|
+
.filter((t) => (sheetId ? t.subUnitId === sheetId : true))
|
|
81
|
+
.map((t) => ({ ...t, styleId: 'table-default-0' }));
|
|
82
|
+
} catch {
|
|
83
|
+
// Table plugin still registering — the CommandExecuted subscription
|
|
84
|
+
// recomputes once it's ready.
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const header: CSSProperties = {
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap: 8,
|
|
93
|
+
padding: '10px 12px',
|
|
94
|
+
borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function TablesPanel({ api, onClose }: PanelComponentProps) {
|
|
98
|
+
const univer = (api as any).univer;
|
|
99
|
+
const [tables, setTables] = useState<TableInfo[]>([]);
|
|
100
|
+
const [renaming, setRenaming] = useState<{ id: string; draft: string } | null>(null);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!univer) return;
|
|
104
|
+
let cancelled = false;
|
|
105
|
+
let disp: { dispose?: () => void } | undefined;
|
|
106
|
+
const compute = () => {
|
|
107
|
+
if (cancelled) return;
|
|
108
|
+
const sheetId = univer.getActiveWorkbook?.()?.getActiveSheet?.()?.getSheetId?.() ?? null;
|
|
109
|
+
setTables(readTables(univer, sheetId));
|
|
110
|
+
};
|
|
111
|
+
// The table plugin is lazy-loaded — `getTableList()` resolves SheetTableService
|
|
112
|
+
// and throws if the plugin isn't registered yet, so ensure it before reading.
|
|
113
|
+
void ensurePluginByName('table').then(() => {
|
|
114
|
+
if (cancelled) return;
|
|
115
|
+
compute();
|
|
116
|
+
disp = univer.addEvent(univer.Event.CommandExecuted, (e: { id?: string }) => {
|
|
117
|
+
if (e.id && REFRESH_CMDS.has(e.id)) compute();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return () => {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
disp?.dispose?.();
|
|
123
|
+
};
|
|
124
|
+
}, [univer]);
|
|
125
|
+
|
|
126
|
+
const themesById = useMemo(() => {
|
|
127
|
+
const m = new Map<string, (typeof TABLE_THEMES)[number]>();
|
|
128
|
+
for (const t of TABLE_THEMES) m.set(t.id, t);
|
|
129
|
+
return m;
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const empty = tables.length === 0;
|
|
133
|
+
|
|
134
|
+
const onRenameCommit = async (id: string, currentName: string) => {
|
|
135
|
+
if (!renaming || renaming.id !== id) return;
|
|
136
|
+
const next = renaming.draft.trim();
|
|
137
|
+
setRenaming(null);
|
|
138
|
+
if (!next || next === currentName) return;
|
|
139
|
+
const sheet = univer.getActiveWorkbook?.()?.getActiveSheet?.();
|
|
140
|
+
if (!sheet) return;
|
|
141
|
+
await ensurePluginByName('table');
|
|
142
|
+
const ok = await Promise.resolve(sheet.setTableName?.(id, next));
|
|
143
|
+
if (ok === false) console.warn(`[tables] rename rejected: "${next}"`);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const onPickTheme = async (id: string, themeId: TableThemeId) => {
|
|
147
|
+
const wb = univer.getActiveWorkbook?.();
|
|
148
|
+
if (!wb) return;
|
|
149
|
+
await ensurePluginByName('table');
|
|
150
|
+
univer.executeCommand('sheet.command.set-table-config', {
|
|
151
|
+
unitId: wb.getId(),
|
|
152
|
+
tableId: id,
|
|
153
|
+
theme: themeId,
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const onDelete = async (id: string) => {
|
|
158
|
+
const wb = univer.getActiveWorkbook?.();
|
|
159
|
+
if (!wb) return;
|
|
160
|
+
await ensurePluginByName('table');
|
|
161
|
+
wb.removeTable?.(id);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
data-testid="cs-tables-panel"
|
|
167
|
+
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
|
168
|
+
>
|
|
169
|
+
<header style={header}>
|
|
170
|
+
<Icon name="table" size={18} />
|
|
171
|
+
<span style={{ fontWeight: 600, flex: 1 }}>Tables</span>
|
|
172
|
+
{!empty && <span style={{ opacity: 0.6, fontSize: 12 }}>{tables.length}</span>}
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
aria-label="Close tables panel"
|
|
176
|
+
onClick={onClose}
|
|
177
|
+
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
178
|
+
>
|
|
179
|
+
<Icon name="close" size={18} />
|
|
180
|
+
</button>
|
|
181
|
+
</header>
|
|
182
|
+
|
|
183
|
+
<div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
|
|
184
|
+
{empty ? (
|
|
185
|
+
<div
|
|
186
|
+
data-testid="cs-tables-panel-empty"
|
|
187
|
+
style={{ textAlign: 'center', opacity: 0.75, padding: '24px 8px' }}
|
|
188
|
+
>
|
|
189
|
+
<Icon name="table_rows" size={40} style={{ opacity: 0.4 }} />
|
|
190
|
+
<div style={{ fontWeight: 600, marginTop: 8 }}>No tables on this sheet</div>
|
|
191
|
+
<div style={{ fontSize: 13, marginTop: 4 }}>
|
|
192
|
+
Select your data, then use <strong>Insert → Table</strong> from the menu.
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
) : (
|
|
196
|
+
<ul
|
|
197
|
+
style={{
|
|
198
|
+
listStyle: 'none',
|
|
199
|
+
margin: 0,
|
|
200
|
+
padding: 0,
|
|
201
|
+
display: 'flex',
|
|
202
|
+
flexDirection: 'column',
|
|
203
|
+
gap: 8,
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
{tables.map((t) => {
|
|
207
|
+
const isRenaming = renaming?.id === t.id;
|
|
208
|
+
const theme = themesById.get(t.styleId);
|
|
209
|
+
return (
|
|
210
|
+
<li
|
|
211
|
+
key={t.id}
|
|
212
|
+
data-testid={`cs-tables-panel-row-${t.id}`}
|
|
213
|
+
style={{
|
|
214
|
+
border: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
215
|
+
borderRadius: 8,
|
|
216
|
+
padding: 10,
|
|
217
|
+
display: 'flex',
|
|
218
|
+
flexDirection: 'column',
|
|
219
|
+
gap: 6,
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
223
|
+
{isRenaming ? (
|
|
224
|
+
<input
|
|
225
|
+
autoFocus
|
|
226
|
+
value={renaming.draft}
|
|
227
|
+
onChange={(e) => setRenaming({ id: t.id, draft: e.target.value })}
|
|
228
|
+
onBlur={() => onRenameCommit(t.id, t.name)}
|
|
229
|
+
onKeyDown={(e) => {
|
|
230
|
+
if (e.key === 'Enter') onRenameCommit(t.id, t.name);
|
|
231
|
+
if (e.key === 'Escape') setRenaming(null);
|
|
232
|
+
}}
|
|
233
|
+
style={{ flex: 1, font: 'inherit', padding: '2px 4px' }}
|
|
234
|
+
/>
|
|
235
|
+
) : (
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
onClick={() => setRenaming({ id: t.id, draft: t.name })}
|
|
239
|
+
title="Click to rename"
|
|
240
|
+
style={{
|
|
241
|
+
flex: 1,
|
|
242
|
+
textAlign: 'left',
|
|
243
|
+
border: 'none',
|
|
244
|
+
background: 'transparent',
|
|
245
|
+
cursor: 'pointer',
|
|
246
|
+
font: 'inherit',
|
|
247
|
+
fontWeight: 600,
|
|
248
|
+
color: 'inherit',
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{t.name}
|
|
252
|
+
</button>
|
|
253
|
+
)}
|
|
254
|
+
<span style={{ opacity: 0.6, fontSize: 12 }}>{toA1(t.range)}</span>
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
aria-label={`Delete table ${t.name}`}
|
|
258
|
+
title="Delete table"
|
|
259
|
+
onClick={() => onDelete(t.id)}
|
|
260
|
+
style={{
|
|
261
|
+
border: 'none',
|
|
262
|
+
background: 'transparent',
|
|
263
|
+
cursor: 'pointer',
|
|
264
|
+
color: 'inherit',
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<Icon name="delete" size={16} />
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
<div role="group" aria-label="Table theme" style={{ display: 'flex', gap: 4 }}>
|
|
271
|
+
{TABLE_THEMES.map((opt) => (
|
|
272
|
+
<button
|
|
273
|
+
key={opt.id}
|
|
274
|
+
type="button"
|
|
275
|
+
title={opt.label}
|
|
276
|
+
aria-label={opt.label}
|
|
277
|
+
aria-pressed={theme?.id === opt.id}
|
|
278
|
+
onClick={() => onPickTheme(t.id, opt.id)}
|
|
279
|
+
style={{
|
|
280
|
+
width: 18,
|
|
281
|
+
height: 18,
|
|
282
|
+
borderRadius: 4,
|
|
283
|
+
cursor: 'pointer',
|
|
284
|
+
background: opt.swatch,
|
|
285
|
+
border:
|
|
286
|
+
theme?.id === opt.id
|
|
287
|
+
? '2px solid var(--cs-chrome-active-fg, #0e7490)'
|
|
288
|
+
: '1px solid rgba(0,0,0,0.15)',
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
</li>
|
|
294
|
+
);
|
|
295
|
+
})}
|
|
296
|
+
</ul>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License").
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Panel store for the SDK chrome's side-panel rail (Tables/Charts/Comments/…).
|
|
9
|
+
*
|
|
10
|
+
* A single `openPanelId` is the whole state — that IS the mutex: opening one
|
|
11
|
+
* panel closes any other, exactly like the standalone app's PanelMutex, but
|
|
12
|
+
* without the app-only `ui-context`. The rail toggles panels through here and
|
|
13
|
+
* the panel host renders whichever id is open. Kept deliberately tiny so the
|
|
14
|
+
* chrome stays a thin shell around Univer.
|
|
15
|
+
*/
|
|
16
|
+
import { createContext, useContext, useMemo, useState, type ReactNode } from 'react';
|
|
17
|
+
|
|
18
|
+
export interface PanelStore {
|
|
19
|
+
/** The id of the currently-open panel, or null when the rail is all-closed. */
|
|
20
|
+
openPanelId: string | null;
|
|
21
|
+
/** Open `id`, replacing whatever was open (mutex). */
|
|
22
|
+
open: (id: string) => void;
|
|
23
|
+
/** Close the open panel. */
|
|
24
|
+
close: () => void;
|
|
25
|
+
/** Toggle `id`: open it if closed, close it if it's the open one. */
|
|
26
|
+
toggle: (id: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PanelContext = createContext<PanelStore | null>(null);
|
|
30
|
+
|
|
31
|
+
export function PanelProvider({ children }: { children: ReactNode }) {
|
|
32
|
+
const [openPanelId, setOpenPanelId] = useState<string | null>(null);
|
|
33
|
+
const store = useMemo<PanelStore>(
|
|
34
|
+
() => ({
|
|
35
|
+
openPanelId,
|
|
36
|
+
open: (id) => setOpenPanelId(id),
|
|
37
|
+
close: () => setOpenPanelId(null),
|
|
38
|
+
toggle: (id) => setOpenPanelId((cur) => (cur === id ? null : id)),
|
|
39
|
+
}),
|
|
40
|
+
[openPanelId],
|
|
41
|
+
);
|
|
42
|
+
return <PanelContext.Provider value={store}>{children}</PanelContext.Provider>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read the panel store. Returns a no-op store when used outside a provider. */
|
|
46
|
+
export function usePanels(): PanelStore {
|
|
47
|
+
return (
|
|
48
|
+
useContext(PanelContext) ?? {
|
|
49
|
+
openPanelId: null,
|
|
50
|
+
open: () => {},
|
|
51
|
+
close: () => {},
|
|
52
|
+
toggle: () => {},
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License").
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Built-in side panels the SDK chrome ships (mirrors `BUILT_IN_DIALOGS`). Each
|
|
9
|
+
* entry gets a rail button + a body rendered by the panel host. Hosts can add
|
|
10
|
+
* more via `extensions.panels` (see extensions.ts) — those are merged after
|
|
11
|
+
* these on the rail.
|
|
12
|
+
*/
|
|
13
|
+
import { lazy, type ComponentType } from 'react';
|
|
14
|
+
|
|
15
|
+
import type { PanelComponentProps } from './extensions';
|
|
16
|
+
import { TablesPanel } from './TablesPanel';
|
|
17
|
+
import { PivotFieldsPanel } from './PivotFieldsPanel';
|
|
18
|
+
import { CommentsPanel } from './CommentsPanel';
|
|
19
|
+
import { HistoryPanel } from './HistoryPanel';
|
|
20
|
+
// Charts panel pulls echarts — lazy so it (and echarts) only load when the
|
|
21
|
+
// panel is first opened. PanelHost renders panels inside a Suspense boundary.
|
|
22
|
+
const ChartsPanel = lazy(() =>
|
|
23
|
+
import('../charts/ChartsPanel').then((m) => ({ default: m.ChartsPanel })),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface BuiltInPanel {
|
|
27
|
+
/** Stable id (rail `data-testid` `cs-panel-rail-<id>`, mutex key). */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Rail button tooltip / aria-label. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Material Symbols icon name for the rail button. */
|
|
32
|
+
icon: string;
|
|
33
|
+
/** The panel body; receives `{ api, onClose }`. */
|
|
34
|
+
component: ComponentType<PanelComponentProps>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const BUILT_IN_PANELS: BuiltInPanel[] = [
|
|
38
|
+
{ id: 'tables', label: 'Tables', icon: 'table', component: TablesPanel },
|
|
39
|
+
{
|
|
40
|
+
id: 'pivot',
|
|
41
|
+
label: 'PivotTable Fields',
|
|
42
|
+
icon: 'pivot_table_chart',
|
|
43
|
+
component: PivotFieldsPanel,
|
|
44
|
+
},
|
|
45
|
+
{ id: 'charts', label: 'Charts', icon: 'analytics', component: ChartsPanel },
|
|
46
|
+
{ id: 'comments', label: 'Comments', icon: 'forum', component: CommentsPanel },
|
|
47
|
+
{ id: 'history', label: 'History', icon: 'history', component: HistoryPanel },
|
|
48
|
+
];
|
|
@@ -106,6 +106,16 @@ import {
|
|
|
106
106
|
import type { ChromeExtensions } from '../chrome/extensions';
|
|
107
107
|
import type { DialogKind } from '../chrome/dialog-context';
|
|
108
108
|
import { AiPanelSurface, type SheetsAiConfig } from '../ai/AiPanelSurface';
|
|
109
|
+
import { PanelProvider } from '../chrome/panel-context';
|
|
110
|
+
import { PanelRail } from '../chrome/PanelRail';
|
|
111
|
+
import { PanelHost } from '../chrome/PanelHost';
|
|
112
|
+
// Charts: the provider is echarts-free (resources/types only) so it imports
|
|
113
|
+
// eagerly; the overlay pulls echarts, so it's lazy — echarts becomes its own
|
|
114
|
+
// async chunk, loaded only when a chart actually renders.
|
|
115
|
+
import { ChartsProvider } from '../charts/charts-context';
|
|
116
|
+
const ChartLayer = lazy(() =>
|
|
117
|
+
import('../charts/ChartLayer').then((m) => ({ default: m.ChartLayer })),
|
|
118
|
+
);
|
|
109
119
|
// Chrome is lazy-loaded from the `@casualoffice/sheets/chrome` subpath (NOT a
|
|
110
120
|
// relative import — that would inline under this build's splitting:false). The
|
|
111
121
|
// subpath is externalised in tsup, so the consumer's bundler code-splits it and
|
|
@@ -713,47 +723,63 @@ export function CasualSheets({
|
|
|
713
723
|
} as CSSProperties;
|
|
714
724
|
|
|
715
725
|
return (
|
|
716
|
-
<
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
726
|
+
<PanelProvider>
|
|
727
|
+
<div
|
|
728
|
+
className={className}
|
|
729
|
+
data-testid={testId}
|
|
730
|
+
data-theme={dark ? 'dark' : 'light'}
|
|
731
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
732
|
+
style={{
|
|
733
|
+
...DEFAULT_STYLE,
|
|
734
|
+
...chromeVars,
|
|
735
|
+
...style,
|
|
736
|
+
display: 'flex',
|
|
737
|
+
flexDirection: 'column',
|
|
738
|
+
}}
|
|
739
|
+
>
|
|
740
|
+
{/* Bars appear once their lazy chunk loads (a tick after first paint); the
|
|
730
741
|
grid host is OUTSIDE Suspense so Univer mounts immediately. */}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
742
|
+
<Suspense fallback={null}>
|
|
743
|
+
<ChromeTop
|
|
744
|
+
api={chromeApi}
|
|
745
|
+
features={features}
|
|
746
|
+
onDialogRequest={onDialogRequest}
|
|
747
|
+
hostOwnedDialogs={hostOwnedDialogs}
|
|
748
|
+
extensions={extensions}
|
|
749
|
+
/>
|
|
750
|
+
</Suspense>
|
|
751
|
+
{/* Grid + side panels + rail share a flex row so the built-in chrome
|
|
752
|
+
(top/bottom bars) still spans the full width. The row shape is stable
|
|
753
|
+
(grid host never remounts); PanelHost/AI aside only add/remove
|
|
754
|
+
siblings, and the rail is always present. */}
|
|
743
755
|
<div style={{ flex: '1 1 auto', minHeight: 0, display: 'flex', flexDirection: 'row' }}>
|
|
744
756
|
<div
|
|
745
757
|
ref={hostRef}
|
|
758
|
+
data-testid="univer-host"
|
|
746
759
|
style={{ flex: '1 1 auto', minWidth: 0, minHeight: 0, position: 'relative' }}
|
|
747
760
|
/>
|
|
748
|
-
|
|
761
|
+
{/* Charts context + overlay live once the api is ready (it arrives
|
|
762
|
+
after Univer boots into the grid host above, so it can't gate the
|
|
763
|
+
host). ChartLayer portals onto the `univer-host` marker; the
|
|
764
|
+
Charts panel (inside PanelHost) reads the same context. */}
|
|
765
|
+
{chromeApi ? (
|
|
766
|
+
<ChartsProvider api={chromeApi}>
|
|
767
|
+
<Suspense fallback={null}>
|
|
768
|
+
<ChartLayer />
|
|
769
|
+
</Suspense>
|
|
770
|
+
<PanelHost api={chromeApi} extensions={extensions} />
|
|
771
|
+
</ChartsProvider>
|
|
772
|
+
) : (
|
|
773
|
+
<PanelHost api={chromeApi} extensions={extensions} />
|
|
774
|
+
)}
|
|
775
|
+
{hasAi && <AiPanelSurface config={ai} api={aiApi} />}
|
|
776
|
+
<PanelRail extensions={extensions} />
|
|
749
777
|
</div>
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
</Suspense>
|
|
756
|
-
</div>
|
|
778
|
+
<Suspense fallback={null}>
|
|
779
|
+
<ChromeBottom api={chromeApi} />
|
|
780
|
+
</Suspense>
|
|
781
|
+
</div>
|
|
782
|
+
</PanelProvider>
|
|
757
783
|
);
|
|
758
784
|
}
|
|
759
785
|
|