@casualoffice/sheets 0.17.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.
Files changed (52) hide show
  1. package/dist/chrome.cjs +3339 -244
  2. package/dist/chrome.cjs.map +1 -1
  3. package/dist/chrome.js +3298 -203
  4. package/dist/chrome.js.map +1 -1
  5. package/dist/embed/embed-runtime.js +203 -157
  6. package/dist/index.cjs +5971 -1572
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +5961 -1548
  9. package/dist/index.js.map +1 -1
  10. package/dist/sheets.cjs +4937 -538
  11. package/dist/sheets.cjs.map +1 -1
  12. package/dist/sheets.js +4945 -532
  13. package/dist/sheets.js.map +1 -1
  14. package/package.json +8 -7
  15. package/src/charts/ChartContextMenu.tsx +264 -0
  16. package/src/charts/ChartLayer.tsx +333 -0
  17. package/src/charts/ChartOverlay.tsx +293 -0
  18. package/src/charts/ChartsPanel.tsx +211 -0
  19. package/src/charts/FormatChartDialog.tsx +419 -0
  20. package/src/charts/InsertChartDialog.tsx +478 -0
  21. package/src/charts/build-option.ts +476 -0
  22. package/src/charts/charts-context.tsx +205 -0
  23. package/src/charts/echarts-init.ts +58 -0
  24. package/src/charts/hit-test.ts +130 -0
  25. package/src/charts/insert-chart.ts +106 -0
  26. package/src/charts/naming.ts +38 -0
  27. package/src/charts/render-to-png.ts +117 -0
  28. package/src/charts/resources.ts +108 -0
  29. package/src/charts/types.ts +239 -0
  30. package/src/charts/univer-dom.ts +102 -0
  31. package/src/chrome/CommentsPanel.tsx +427 -0
  32. package/src/chrome/ConditionalFormattingDialog.tsx +534 -0
  33. package/src/chrome/CustomSortDialog.tsx +357 -0
  34. package/src/chrome/DataValidationDialog.tsx +536 -0
  35. package/src/chrome/DeleteCellsDialog.tsx +183 -0
  36. package/src/chrome/GoalSeekDialog.tsx +370 -0
  37. package/src/chrome/HistoryPanel.tsx +319 -0
  38. package/src/chrome/InsertCellsDialog.tsx +185 -0
  39. package/src/chrome/InsertChartDialog.tsx +490 -0
  40. package/src/chrome/InsertFunctionDialog.tsx +493 -0
  41. package/src/chrome/InsertPivotDialog.tsx +488 -0
  42. package/src/chrome/InsertSparklineDialog.tsx +344 -0
  43. package/src/chrome/NameManagerDialog.tsx +378 -0
  44. package/src/chrome/PanelHost.tsx +55 -0
  45. package/src/chrome/PanelRail.tsx +90 -0
  46. package/src/chrome/PasteSpecialDialog.tsx +286 -0
  47. package/src/chrome/PivotFieldsPanel.tsx +1052 -0
  48. package/src/chrome/TablesPanel.tsx +301 -0
  49. package/src/chrome/dialog-context.tsx +24 -0
  50. package/src/chrome/panel-context.tsx +55 -0
  51. package/src/chrome/panel-registry.ts +48 -0
  52. package/src/sheets/CasualSheets.tsx +60 -34
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Copyright 2026 Casual Office
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * NameManagerDialog — the SDK chrome's built-in Name Manager (defined names).
19
+ *
20
+ * Mirrors the Data Validation / Format Cells exemplars: it reaches the workbook
21
+ * off the FUniver facade and drives the workbook's defined-name API to list,
22
+ * add, edit, and delete named ranges. Grounded in
23
+ * `@univerjs/sheets/lib/types/facade/f-workbook.d.ts`:
24
+ * - `getDefinedNames(): FDefinedName[]` (L571)
25
+ * - `getDefinedName(name): FDefinedName | null` (L559)
26
+ * - `insertDefinedName(name, formulaOrRefString): FWorkbook` (L639)
27
+ * - `deleteDefinedName(name): boolean` (L651)
28
+ * - `updateDefinedNameBuilder(param): void` (L626)
29
+ * and `f-defined-name.d.ts`: `FDefinedName.getName()` (L221) /
30
+ * `getFormulaOrRefString()` (L265) / `toBuilder()` (L385); the builder's
31
+ * `setName` (L44) / `setRef` (L74) / `build()` (L175). The "add from selection"
32
+ * ref comes from `FRange.getA1Notation(true)` (sheet-qualified, e.g.
33
+ * `Sheet1!A1:B2` — `f-range.d.ts` L1220), which is exactly the shape
34
+ * `insertDefinedName`'s `formulaOrRefString` accepts.
35
+ *
36
+ * Univer's defined-name API is unscoped by name (no id needed here): to "edit" a
37
+ * name we rebuild it from `getDefinedName(oldName).toBuilder()`, preserving its
38
+ * identity while updating name + ref, then `updateDefinedNameBuilder`.
39
+ *
40
+ * Mounted by `<DialogHost>` when `openDialog('name-manager')` is called and no
41
+ * host override is registered.
42
+ */
43
+
44
+ import { useCallback, useMemo, useState, type CSSProperties } from 'react';
45
+ import type { DialogComponentProps } from './extensions';
46
+ import type { CasualSheetsAPI } from '../sheets/api';
47
+ import { Dialog } from './Dialog';
48
+ import {
49
+ DIALOG_BTN_PRIMARY_STYLE,
50
+ DIALOG_BTN_SECONDARY_STYLE,
51
+ DIALOG_FIELD_STYLE,
52
+ DIALOG_INPUT_STYLE,
53
+ DIALOG_LABEL_STYLE,
54
+ } from './dialog-styles';
55
+
56
+ /** Minimal shape of the FDefinedNameBuilder chain we use (grounded in f-defined-name.ts). */
57
+ interface FDefinedNameBuilderLike {
58
+ setName(name: string): FDefinedNameBuilderLike;
59
+ setRef(a1Notation: string): FDefinedNameBuilderLike;
60
+ build(): unknown;
61
+ }
62
+
63
+ /** Minimal shape of an FDefinedName we read (grounded in f-defined-name.ts). */
64
+ interface FDefinedNameLike {
65
+ getName(): string;
66
+ getFormulaOrRefString(): string;
67
+ toBuilder(): FDefinedNameBuilderLike;
68
+ }
69
+
70
+ /** Minimal shape of the FWorkbook defined-name surface (grounded in f-workbook.d.ts). */
71
+ interface FWorkbookLike {
72
+ getDefinedNames(): FDefinedNameLike[];
73
+ getDefinedName(name: string): FDefinedNameLike | null;
74
+ insertDefinedName(name: string, formulaOrRefString: string): unknown;
75
+ deleteDefinedName(name: string): boolean;
76
+ updateDefinedNameBuilder(param: unknown): void;
77
+ getActiveSheet(): {
78
+ getActiveRange(): { getA1Notation(withSheet?: boolean): string } | null;
79
+ } | null;
80
+ }
81
+
82
+ interface NameRow {
83
+ name: string;
84
+ refersTo: string;
85
+ }
86
+
87
+ /** Names must start with a letter/underscore and avoid A1-like tokens; keep it
88
+ * loose but block the common mistakes (spaces, leading digit, empty). */
89
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_.]*$/;
90
+
91
+ function workbook(api: CasualSheetsAPI): FWorkbookLike | null {
92
+ return (api.univer.getActiveWorkbook() as unknown as FWorkbookLike | null) ?? null;
93
+ }
94
+
95
+ function readRows(api: CasualSheetsAPI): NameRow[] {
96
+ const wb = workbook(api);
97
+ if (!wb) return [];
98
+ return wb.getDefinedNames().map((dn) => ({
99
+ name: dn.getName(),
100
+ refersTo: dn.getFormulaOrRefString(),
101
+ }));
102
+ }
103
+
104
+ /** Sheet-qualified A1 of the current selection, e.g. `Sheet1!A1:B2`, or ''. */
105
+ function selectionRef(api: CasualSheetsAPI): string {
106
+ const range = workbook(api)?.getActiveSheet()?.getActiveRange();
107
+ return range?.getA1Notation(true) ?? '';
108
+ }
109
+
110
+ const ROW_STYLE: CSSProperties = {
111
+ display: 'grid',
112
+ gridTemplateColumns: '1fr 1.3fr auto',
113
+ alignItems: 'center',
114
+ gap: 8,
115
+ padding: '6px 8px',
116
+ borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
117
+ };
118
+
119
+ const LIST_STYLE: CSSProperties = {
120
+ border: '1px solid var(--cs-chrome-border, #cdd3db)',
121
+ borderRadius: 6,
122
+ maxHeight: 200,
123
+ overflow: 'auto',
124
+ marginBottom: 16,
125
+ };
126
+
127
+ const EMPTY_STYLE: CSSProperties = {
128
+ padding: '16px 8px',
129
+ fontSize: 13,
130
+ color: 'var(--cs-chrome-muted, #605e5c)',
131
+ textAlign: 'center',
132
+ };
133
+
134
+ const REF_CELL_STYLE: CSSProperties = {
135
+ fontSize: 12,
136
+ color: 'var(--cs-chrome-muted, #605e5c)',
137
+ overflow: 'hidden',
138
+ textOverflow: 'ellipsis',
139
+ whiteSpace: 'nowrap',
140
+ };
141
+
142
+ const ROW_ACTIONS_STYLE: CSSProperties = {
143
+ display: 'flex',
144
+ gap: 4,
145
+ };
146
+
147
+ const SMALL_BTN_STYLE: CSSProperties = {
148
+ ...DIALOG_BTN_SECONDARY_STYLE,
149
+ height: 24,
150
+ padding: '0 8px',
151
+ fontSize: 12,
152
+ };
153
+
154
+ const SECTION_TITLE_STYLE: CSSProperties = {
155
+ fontSize: 13,
156
+ fontWeight: 600,
157
+ color: 'var(--cs-chrome-fg, #201f1e)',
158
+ margin: '0 0 8px',
159
+ };
160
+
161
+ const ERROR_STYLE: CSSProperties = {
162
+ fontSize: 12,
163
+ color: 'var(--cs-chrome-danger, #b91c1c)',
164
+ marginBottom: 8,
165
+ };
166
+
167
+ const TWO_COL_STYLE: CSSProperties = {
168
+ display: 'grid',
169
+ gridTemplateColumns: '1fr 1.3fr',
170
+ gap: 8,
171
+ };
172
+
173
+ export function NameManagerDialog({ api, onClose }: DialogComponentProps) {
174
+ const [rows, setRows] = useState<NameRow[]>(() => readRows(api));
175
+ // When editing, this holds the ORIGINAL name of the row being edited (used to
176
+ // locate + update it). null means we're adding a new name.
177
+ const [editingOriginal, setEditingOriginal] = useState<string | null>(null);
178
+ const [nameInput, setNameInput] = useState('');
179
+ const [refInput, setRefInput] = useState('');
180
+ const [error, setError] = useState<string | null>(null);
181
+
182
+ const initialSelRef = useMemo(() => selectionRef(api), [api]);
183
+
184
+ const refresh = useCallback(() => setRows(readRows(api)), [api]);
185
+
186
+ const resetForm = useCallback(() => {
187
+ setEditingOriginal(null);
188
+ setNameInput('');
189
+ setRefInput('');
190
+ setError(null);
191
+ }, []);
192
+
193
+ const startEdit = (row: NameRow) => {
194
+ setEditingOriginal(row.name);
195
+ setNameInput(row.name);
196
+ setRefInput(row.refersTo);
197
+ setError(null);
198
+ };
199
+
200
+ const useSelection = () => {
201
+ const ref = selectionRef(api);
202
+ if (ref) setRefInput(ref);
203
+ };
204
+
205
+ const remove = (row: NameRow) => {
206
+ const wb = workbook(api);
207
+ if (!wb) return;
208
+ wb.deleteDefinedName(row.name);
209
+ if (editingOriginal === row.name) resetForm();
210
+ refresh();
211
+ };
212
+
213
+ const submit = () => {
214
+ const wb = workbook(api);
215
+ if (!wb) {
216
+ setError('No active workbook.');
217
+ return;
218
+ }
219
+ const name = nameInput.trim();
220
+ const ref = refInput.trim();
221
+ if (!name) {
222
+ setError('Enter a name.');
223
+ return;
224
+ }
225
+ if (!NAME_RE.test(name)) {
226
+ setError('Names must start with a letter or underscore and contain no spaces.');
227
+ return;
228
+ }
229
+ if (!ref) {
230
+ setError('Enter a range or formula (e.g. Sheet1!A1:B2).');
231
+ return;
232
+ }
233
+
234
+ // Duplicate-name guard: allow keeping the same name while editing, but block
235
+ // colliding with a different existing name.
236
+ const collides = rows.some(
237
+ (r) => r.name.toLowerCase() === name.toLowerCase() && r.name !== editingOriginal,
238
+ );
239
+ if (collides) {
240
+ setError(`A defined name "${name}" already exists.`);
241
+ return;
242
+ }
243
+
244
+ if (editingOriginal === null) {
245
+ // Add.
246
+ wb.insertDefinedName(name, ref);
247
+ } else {
248
+ // Edit — rebuild from the existing name to preserve identity/scope, then
249
+ // update. If the row vanished (e.g. deleted elsewhere) fall back to insert.
250
+ const existing = wb.getDefinedName(editingOriginal);
251
+ if (existing) {
252
+ const param = existing.toBuilder().setName(name).setRef(ref).build();
253
+ wb.updateDefinedNameBuilder(param);
254
+ } else {
255
+ wb.insertDefinedName(name, ref);
256
+ }
257
+ }
258
+
259
+ refresh();
260
+ resetForm();
261
+ };
262
+
263
+ const isEditing = editingOriginal !== null;
264
+
265
+ return (
266
+ <Dialog
267
+ title="Name manager"
268
+ onClose={onClose}
269
+ width={480}
270
+ data-testid="cs-name-manager-dialog"
271
+ footer={
272
+ <>
273
+ <span style={{ flex: 1 }} />
274
+ <button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
275
+ Close
276
+ </button>
277
+ </>
278
+ }
279
+ >
280
+ <h3 style={SECTION_TITLE_STYLE}>Defined names</h3>
281
+ <div style={LIST_STYLE} data-testid="cs-name-manager-list">
282
+ {rows.length === 0 ? (
283
+ <div style={EMPTY_STYLE} data-testid="cs-name-manager-empty">
284
+ No defined names yet. Add one below.
285
+ </div>
286
+ ) : (
287
+ rows.map((row) => (
288
+ <div key={row.name} style={ROW_STYLE} data-testid="cs-name-manager-row">
289
+ <span style={{ fontWeight: 500 }}>{row.name}</span>
290
+ <span style={REF_CELL_STYLE} title={row.refersTo}>
291
+ {row.refersTo}
292
+ </span>
293
+ <span style={ROW_ACTIONS_STYLE}>
294
+ <button
295
+ type="button"
296
+ style={SMALL_BTN_STYLE}
297
+ data-testid="cs-name-manager-edit"
298
+ onClick={() => startEdit(row)}
299
+ >
300
+ Edit
301
+ </button>
302
+ <button
303
+ type="button"
304
+ style={SMALL_BTN_STYLE}
305
+ data-testid="cs-name-manager-delete"
306
+ onClick={() => remove(row)}
307
+ >
308
+ Delete
309
+ </button>
310
+ </span>
311
+ </div>
312
+ ))
313
+ )}
314
+ </div>
315
+
316
+ <h3 style={SECTION_TITLE_STYLE}>{isEditing ? 'Edit name' : 'Add a name'}</h3>
317
+
318
+ {error && (
319
+ <div style={ERROR_STYLE} data-testid="cs-name-manager-error">
320
+ {error}
321
+ </div>
322
+ )}
323
+
324
+ <div style={TWO_COL_STYLE}>
325
+ <label style={DIALOG_FIELD_STYLE}>
326
+ <span style={DIALOG_LABEL_STYLE}>Name</span>
327
+ <input
328
+ style={DIALOG_INPUT_STYLE}
329
+ data-testid="cs-name-manager-name"
330
+ value={nameInput}
331
+ placeholder="MyRange"
332
+ onChange={(e) => setNameInput(e.target.value)}
333
+ />
334
+ </label>
335
+ <label style={DIALOG_FIELD_STYLE}>
336
+ <span style={DIALOG_LABEL_STYLE}>Refers to</span>
337
+ <input
338
+ style={DIALOG_INPUT_STYLE}
339
+ data-testid="cs-name-manager-ref"
340
+ value={refInput}
341
+ placeholder={initialSelRef || 'Sheet1!A1:B2'}
342
+ onChange={(e) => setRefInput(e.target.value)}
343
+ />
344
+ </label>
345
+ </div>
346
+
347
+ <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
348
+ <button
349
+ type="button"
350
+ style={DIALOG_BTN_SECONDARY_STYLE}
351
+ data-testid="cs-name-manager-use-selection"
352
+ onClick={useSelection}
353
+ >
354
+ Use selection
355
+ </button>
356
+ <span style={{ flex: 1 }} />
357
+ {isEditing && (
358
+ <button
359
+ type="button"
360
+ style={DIALOG_BTN_SECONDARY_STYLE}
361
+ data-testid="cs-name-manager-cancel-edit"
362
+ onClick={resetForm}
363
+ >
364
+ Cancel
365
+ </button>
366
+ )}
367
+ <button
368
+ type="button"
369
+ style={DIALOG_BTN_PRIMARY_STYLE}
370
+ data-testid="cs-name-manager-submit"
371
+ onClick={submit}
372
+ >
373
+ {isEditing ? 'Update' : 'Add'}
374
+ </button>
375
+ </div>
376
+ </Dialog>
377
+ );
378
+ }
@@ -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
+ * Renders the currently-open side panel's body (built-in or host-supplied),
9
+ * to the left of the rail. Returns null when no panel is open so the grid keeps
10
+ * the full width. Each panel body receives `{ api, onClose }`.
11
+ */
12
+ import { Suspense, type CSSProperties } from 'react';
13
+
14
+ import type { CasualSheetsAPI } from '../sheets/api';
15
+ import type { ChromeExtensions } from './extensions';
16
+ import { usePanels } from './panel-context';
17
+ import { BUILT_IN_PANELS } from './panel-registry';
18
+
19
+ const asideStyle: CSSProperties = {
20
+ flex: '0 0 auto',
21
+ width: 300,
22
+ minWidth: 0,
23
+ height: '100%',
24
+ overflow: 'auto',
25
+ borderLeft: '1px solid var(--cs-chrome-border, #edeff3)',
26
+ background: 'var(--cs-chrome-input-bg, #ffffff)',
27
+ color: 'var(--cs-chrome-fg, #201f1e)',
28
+ };
29
+
30
+ export function PanelHost({
31
+ api,
32
+ extensions,
33
+ }: {
34
+ api: CasualSheetsAPI | null;
35
+ extensions?: ChromeExtensions;
36
+ }) {
37
+ const panels = usePanels();
38
+ const openId = panels.openPanelId;
39
+ if (!openId || !api) return null;
40
+
41
+ const builtIn = BUILT_IN_PANELS.find((p) => p.id === openId);
42
+ const host = extensions?.panels?.find((p) => p.id === openId);
43
+ const Body = builtIn?.component ?? host?.component;
44
+ if (!Body) return null;
45
+
46
+ return (
47
+ <aside style={asideStyle} data-testid={`cs-panel-${openId}`}>
48
+ {/* Panels may be lazy (e.g. Charts pulls echarts) — Suspense keeps the
49
+ rest of the chrome painted while the chunk loads. */}
50
+ <Suspense fallback={null}>
51
+ <Body api={api} onClose={panels.close} />
52
+ </Suspense>
53
+ </aside>
54
+ );
55
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Copyright 2026 Casual Office
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License").
5
+ */
6
+
7
+ /**
8
+ * Right-edge vertical rail of panel-toggle buttons (mirrors the standalone
9
+ * app's PanelRail, but registry-driven and self-contained in the SDK). Built-in
10
+ * panels come first, then any host `extensions.panels`. Clicking a button
11
+ * toggles that panel through the shared panel store; the open panel's body is
12
+ * rendered to the left by PanelHost.
13
+ */
14
+ import type { CSSProperties } from 'react';
15
+
16
+ import type { ChromeExtensions } from './extensions';
17
+ import { Icon } from './Icon';
18
+ import { usePanels } from './panel-context';
19
+ import { BUILT_IN_PANELS } from './panel-registry';
20
+
21
+ const railStyle: CSSProperties = {
22
+ display: 'flex',
23
+ flexDirection: 'column',
24
+ alignItems: 'center',
25
+ gap: 2,
26
+ padding: '6px 4px',
27
+ flex: '0 0 auto',
28
+ borderLeft: '1px solid var(--cs-chrome-border, #edeff3)',
29
+ background: 'var(--cs-chrome-bg, #eef1f5)',
30
+ };
31
+
32
+ export interface RailEntry {
33
+ id: string;
34
+ label: string;
35
+ icon: string;
36
+ }
37
+
38
+ /** Merge built-in panels with host-supplied ones into a single rail list. */
39
+ export function railEntries(extensions?: ChromeExtensions): RailEntry[] {
40
+ const built = BUILT_IN_PANELS.map((p) => ({ id: p.id, label: p.label, icon: p.icon }));
41
+ const host = (extensions?.panels ?? []).map((p) => ({
42
+ id: p.id,
43
+ label: p.title,
44
+ icon: p.railIcon,
45
+ }));
46
+ return [...built, ...host];
47
+ }
48
+
49
+ export function PanelRail({ extensions }: { extensions?: ChromeExtensions }) {
50
+ const panels = usePanels();
51
+ const entries = railEntries(extensions);
52
+ if (entries.length === 0) return null;
53
+
54
+ return (
55
+ <aside style={railStyle} data-testid="cs-panel-rail" aria-label="Panels" role="toolbar">
56
+ {entries.map((e) => {
57
+ const pressed = panels.openPanelId === e.id;
58
+ return (
59
+ <button
60
+ key={e.id}
61
+ type="button"
62
+ data-testid={`cs-panel-rail-${e.id}`}
63
+ aria-pressed={pressed}
64
+ aria-label={pressed ? `Hide ${e.label}` : e.label}
65
+ title={pressed ? `Hide ${e.label}` : e.label}
66
+ onClick={() => panels.toggle(e.id)}
67
+ style={{
68
+ display: 'flex',
69
+ alignItems: 'center',
70
+ justifyContent: 'center',
71
+ width: 30,
72
+ height: 30,
73
+ border: 'none',
74
+ borderRadius: 6,
75
+ cursor: 'pointer',
76
+ color: pressed
77
+ ? 'var(--cs-chrome-active-fg, #0e7490)'
78
+ : 'var(--cs-chrome-fg, #201f1e)',
79
+ background: pressed
80
+ ? 'var(--cs-chrome-active, rgba(14,116,144,0.11))'
81
+ : 'transparent',
82
+ }}
83
+ >
84
+ <Icon name={e.icon} size={18} />
85
+ </button>
86
+ );
87
+ })}
88
+ </aside>
89
+ );
90
+ }