@casualoffice/sheets 0.18.0 → 0.20.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 (61) hide show
  1. package/dist/{api-DJhuCjhn.d.cts → api-cyO4EA4_.d.cts} +1 -1
  2. package/dist/{api-DJhuCjhn.d.ts → api-cyO4EA4_.d.ts} +1 -1
  3. package/dist/{attachCollab-BmCRT4V_.d.ts → attachCollab-BT4fVzxP.d.ts} +1 -1
  4. package/dist/{attachCollab-DIUQCCrq.d.cts → attachCollab-DHjb5XYK.d.cts} +1 -1
  5. package/dist/chrome.cjs +60 -3
  6. package/dist/chrome.cjs.map +1 -1
  7. package/dist/chrome.d.cts +3 -3
  8. package/dist/chrome.d.ts +3 -3
  9. package/dist/chrome.js +60 -3
  10. package/dist/chrome.js.map +1 -1
  11. package/dist/collab.d.cts +2 -2
  12. package/dist/collab.d.ts +2 -2
  13. package/dist/embed/embed-runtime.js +205 -161
  14. package/dist/{extensions-DZnqTEN2.d.ts → extensions-Cni7mbEm.d.ts} +1 -1
  15. package/dist/{extensions-FFqdLpk_.d.cts → extensions-DMmPnIYB.d.cts} +1 -1
  16. package/dist/index.cjs +6424 -1559
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +6409 -1530
  21. package/dist/index.js.map +1 -1
  22. package/dist/sheets.cjs +5426 -561
  23. package/dist/sheets.cjs.map +1 -1
  24. package/dist/sheets.d.cts +5 -5
  25. package/dist/sheets.d.ts +5 -5
  26. package/dist/sheets.js +5415 -536
  27. package/dist/sheets.js.map +1 -1
  28. package/package.json +8 -7
  29. package/src/charts/ChartContextMenu.tsx +264 -0
  30. package/src/charts/ChartLayer.tsx +333 -0
  31. package/src/charts/ChartOverlay.tsx +293 -0
  32. package/src/charts/ChartsPanel.tsx +232 -0
  33. package/src/charts/FormatChartDialog.tsx +419 -0
  34. package/src/charts/InsertChartDialog.tsx +478 -0
  35. package/src/charts/build-option.ts +476 -0
  36. package/src/charts/charts-context.tsx +205 -0
  37. package/src/charts/echarts-init.ts +58 -0
  38. package/src/charts/hit-test.ts +130 -0
  39. package/src/charts/insert-chart.ts +106 -0
  40. package/src/charts/naming.ts +38 -0
  41. package/src/charts/render-to-png.ts +117 -0
  42. package/src/charts/resources.ts +108 -0
  43. package/src/charts/types.ts +239 -0
  44. package/src/charts/univer-dom.ts +102 -0
  45. package/src/chrome/CommentsPanel.tsx +418 -0
  46. package/src/chrome/HistoryPanel.tsx +304 -0
  47. package/src/chrome/InsertPivotDialog.tsx +74 -2
  48. package/src/chrome/PanelHost.tsx +56 -0
  49. package/src/chrome/PanelRail.tsx +90 -0
  50. package/src/chrome/PivotFieldsPanel.tsx +1021 -0
  51. package/src/chrome/TablesPanel.tsx +283 -0
  52. package/src/chrome/Toolbar.tsx +7 -1
  53. package/src/chrome/panel-context.tsx +55 -0
  54. package/src/chrome/panel-registry.ts +48 -0
  55. package/src/chrome/panel-shell.tsx +151 -0
  56. package/src/pivots/apply.ts +139 -0
  57. package/src/pivots/compute.ts +604 -0
  58. package/src/pivots/fields-model.ts +267 -0
  59. package/src/pivots/types.ts +137 -0
  60. package/src/sheets/CasualSheets.tsx +61 -35
  61. package/src/sheets/api.ts +37 -2
@@ -0,0 +1,478 @@
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
+ import { useMemo, useRef, useState, type ReactElement } from 'react';
18
+ import { Dialog } from '../chrome/Dialog';
19
+ import type { FUniver } from '@univerjs/core/facade';
20
+ import { CHART_FAMILY_OF, CHART_TYPE_LABEL, type ChartFamily, type ChartType } from './types';
21
+
22
+ type Props = {
23
+ api: FUniver;
24
+ /** A1 reference to seed the source-range input. Pulled from the active
25
+ * selection by the caller so the dialog opens already filled in. */
26
+ defaultSourceA1: string;
27
+ /** Pre-selected chart type. Defaults to `'column'` (Excel's
28
+ * Insert > Chart default — Clustered Column). Set to the chart's
29
+ * current type when used for "Change chart type". */
30
+ initialType?: ChartType;
31
+ /** Dialog title — defaults to "Insert chart". */
32
+ title?: string;
33
+ /** Primary action label — defaults to "Insert". */
34
+ confirmLabel?: string;
35
+ onCancel: () => void;
36
+ onConfirm: (args: {
37
+ source: { startRow: number; endRow: number; startColumn: number; endColumn: number };
38
+ type: ChartType;
39
+ }) => void;
40
+ };
41
+
42
+ type FamilyDef = {
43
+ id: ChartFamily;
44
+ label: string;
45
+ icon: string;
46
+ subtypes: { id: ChartType; label: string }[];
47
+ };
48
+
49
+ /**
50
+ * Excel chart catalog, family + subtype hierarchy. Order mirrors
51
+ * Excel's Insert > Chart panel so muscle memory transfers.
52
+ */
53
+ const FAMILIES: FamilyDef[] = [
54
+ {
55
+ id: 'column',
56
+ label: 'Column',
57
+ icon: 'bar_chart',
58
+ subtypes: [
59
+ { id: 'column', label: CHART_TYPE_LABEL.column },
60
+ { id: 'column-stacked', label: CHART_TYPE_LABEL['column-stacked'] },
61
+ { id: 'column-stacked-100', label: CHART_TYPE_LABEL['column-stacked-100'] },
62
+ ],
63
+ },
64
+ {
65
+ id: 'bar',
66
+ label: 'Bar',
67
+ icon: 'align_horizontal_left',
68
+ subtypes: [
69
+ { id: 'bar', label: CHART_TYPE_LABEL.bar },
70
+ { id: 'bar-stacked', label: CHART_TYPE_LABEL['bar-stacked'] },
71
+ { id: 'bar-stacked-100', label: CHART_TYPE_LABEL['bar-stacked-100'] },
72
+ ],
73
+ },
74
+ {
75
+ id: 'line',
76
+ label: 'Line',
77
+ icon: 'show_chart',
78
+ subtypes: [
79
+ { id: 'line', label: CHART_TYPE_LABEL.line },
80
+ { id: 'line-stacked', label: CHART_TYPE_LABEL['line-stacked'] },
81
+ ],
82
+ },
83
+ {
84
+ id: 'area',
85
+ label: 'Area',
86
+ icon: 'area_chart',
87
+ subtypes: [
88
+ { id: 'area', label: CHART_TYPE_LABEL.area },
89
+ { id: 'area-stacked', label: CHART_TYPE_LABEL['area-stacked'] },
90
+ ],
91
+ },
92
+ {
93
+ id: 'pie',
94
+ label: 'Pie',
95
+ icon: 'pie_chart',
96
+ subtypes: [
97
+ { id: 'pie', label: CHART_TYPE_LABEL.pie },
98
+ { id: 'doughnut', label: CHART_TYPE_LABEL.doughnut },
99
+ ],
100
+ },
101
+ {
102
+ id: 'scatter',
103
+ label: 'Scatter',
104
+ icon: 'scatter_plot',
105
+ subtypes: [{ id: 'scatter', label: CHART_TYPE_LABEL.scatter }],
106
+ },
107
+ ];
108
+
109
+ /**
110
+ * Excel-style "Insert chart" dialog. Two-panel layout:
111
+ *
112
+ * - Left: family list (Column / Bar / Line / Area / Pie / Scatter).
113
+ * Clicking switches the visible subtypes.
114
+ * - Right top: subtype thumbnails with hover-to-preview labels.
115
+ * - Right bottom: source-range input, pre-filled from the active
116
+ * selection so the typical flow is two clicks.
117
+ *
118
+ * Range parsing leans on `sheet.getRange(a1)` — same A1 parser as the
119
+ * Name Box — so anything the formula bar accepts works here too
120
+ * (B5, A1:C4, B:B). Below 2 rows × 2 cols we refuse with an inline
121
+ * hint instead of producing an empty chart.
122
+ */
123
+ export function InsertChartDialog({
124
+ api,
125
+ defaultSourceA1,
126
+ initialType = 'column',
127
+ title = 'Insert chart',
128
+ confirmLabel = 'Insert',
129
+ onCancel,
130
+ onConfirm,
131
+ }: Props) {
132
+ const [type, setType] = useState<ChartType>(initialType);
133
+ const [family, setFamily] = useState<ChartFamily>(CHART_FAMILY_OF[initialType]);
134
+ const [sourceA1, setSourceA1] = useState(defaultSourceA1);
135
+ const [error, setError] = useState<string | null>(null);
136
+ const inputRef = useRef<HTMLInputElement>(null);
137
+
138
+ const activeSheet = useMemo(() => {
139
+ const wb = api.getActiveWorkbook();
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ return wb?.getActiveSheet() as any;
142
+ }, [api]);
143
+
144
+ const currentFamily = FAMILIES.find((f) => f.id === family) ?? FAMILIES[0];
145
+
146
+ const pickFamily = (id: ChartFamily) => {
147
+ setFamily(id);
148
+ const fam = FAMILIES.find((f) => f.id === id);
149
+ if (fam && !fam.subtypes.some((s) => s.id === type)) {
150
+ // Switched family — pick its first subtype.
151
+ setType(fam.subtypes[0].id);
152
+ }
153
+ };
154
+
155
+ const confirm = () => {
156
+ const trimmed = sourceA1.trim();
157
+ if (!trimmed) {
158
+ setError('Pick a source range.');
159
+ inputRef.current?.focus();
160
+ return;
161
+ }
162
+ let range: { startRow: number; endRow: number; startColumn: number; endColumn: number } | null =
163
+ null;
164
+ try {
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
+ const r = activeSheet?.getRange(trimmed) as any;
167
+ const raw = r?.getRange?.();
168
+ if (raw) {
169
+ range = {
170
+ startRow: raw.startRow,
171
+ endRow: raw.endRow,
172
+ startColumn: raw.startColumn,
173
+ endColumn: raw.endColumn,
174
+ };
175
+ }
176
+ } catch {
177
+ /* fall through */
178
+ }
179
+ if (!range) {
180
+ setError("That doesn't look like a valid range — try A1:C4.");
181
+ inputRef.current?.focus();
182
+ return;
183
+ }
184
+ const rows = range.endRow - range.startRow + 1;
185
+ const cols = range.endColumn - range.startColumn + 1;
186
+ if (rows < 2 || cols < 2) {
187
+ setError(
188
+ 'Source needs at least a header row + a data row, and a label column + a value column.',
189
+ );
190
+ inputRef.current?.focus();
191
+ return;
192
+ }
193
+ onConfirm({ source: range, type });
194
+ };
195
+
196
+ return (
197
+ <Dialog
198
+ title={title}
199
+ onClose={onCancel}
200
+ data-testid="insert-chart-dialog"
201
+ footer={
202
+ <>
203
+ <button
204
+ type="button"
205
+ className="btn-secondary"
206
+ data-testid="insert-chart-cancel"
207
+ onClick={onCancel}
208
+ >
209
+ Cancel
210
+ </button>
211
+ <button
212
+ type="button"
213
+ className="btn-primary"
214
+ data-testid="insert-chart-confirm"
215
+ onClick={confirm}
216
+ >
217
+ {confirmLabel}
218
+ </button>
219
+ </>
220
+ }
221
+ >
222
+ <div className="insert-chart">
223
+ <div className="insert-chart__catalog">
224
+ <ul
225
+ className="insert-chart__families"
226
+ role="tablist"
227
+ aria-label="Chart family"
228
+ data-testid="insert-chart-families"
229
+ >
230
+ {FAMILIES.map((f) => (
231
+ <li key={f.id}>
232
+ <button
233
+ type="button"
234
+ role="tab"
235
+ aria-selected={family === f.id}
236
+ className={`insert-chart__family${family === f.id ? ' insert-chart__family--active' : ''}`}
237
+ data-testid={`insert-chart-family-${f.id}`}
238
+ onClick={() => pickFamily(f.id)}
239
+ >
240
+ <span className="material-symbols-outlined" aria-hidden="true">
241
+ {f.icon}
242
+ </span>
243
+ <span>{f.label}</span>
244
+ </button>
245
+ </li>
246
+ ))}
247
+ </ul>
248
+ <div className="insert-chart__subtypes" role="radiogroup" aria-label="Chart subtype">
249
+ {currentFamily.subtypes.map((s) => (
250
+ <label
251
+ key={s.id}
252
+ className={`insert-chart__subtype${type === s.id ? ' insert-chart__subtype--active' : ''}`}
253
+ data-testid={`insert-chart-type-${s.id}`}
254
+ title={s.label}
255
+ >
256
+ <input
257
+ type="radio"
258
+ name="chart-subtype"
259
+ value={s.id}
260
+ checked={type === s.id}
261
+ onChange={() => setType(s.id)}
262
+ />
263
+ <SubtypeThumbnail type={s.id} />
264
+ <span className="insert-chart__subtype-label">{s.label}</span>
265
+ </label>
266
+ ))}
267
+ </div>
268
+ </div>
269
+
270
+ <fieldset className="insert-chart__group">
271
+ <legend className="insert-chart__legend">Source data</legend>
272
+ {/* Error elevates ABOVE the input so the user's eye lands
273
+ on it before the field they're correcting. The previous
274
+ "small red text below the input" layout was easy to
275
+ miss (audit finding 1.2); a proper banner with icon +
276
+ role=alert + aria-live=assertive makes it impossible
277
+ to skip. Screen readers announce on appearance. */}
278
+ {error && (
279
+ <div
280
+ className="insert-chart__error"
281
+ data-testid="insert-chart-error"
282
+ role="alert"
283
+ aria-live="assertive"
284
+ >
285
+ <span className="insert-chart__error-icon" aria-hidden="true">
286
+ !
287
+ </span>
288
+ <span>{error}</span>
289
+ </div>
290
+ )}
291
+ <input
292
+ ref={inputRef}
293
+ type="text"
294
+ className={`insert-chart__range${error ? ' insert-chart__range--error' : ''}`}
295
+ data-testid="insert-chart-range"
296
+ value={sourceA1}
297
+ spellCheck={false}
298
+ aria-invalid={Boolean(error)}
299
+ aria-describedby={error ? 'insert-chart-error-desc' : undefined}
300
+ onChange={(e) => {
301
+ setSourceA1(e.target.value);
302
+ if (error) setError(null);
303
+ }}
304
+ onKeyDown={(e) => {
305
+ if (e.key === 'Enter') {
306
+ e.preventDefault();
307
+ confirm();
308
+ }
309
+ }}
310
+ placeholder="A1:C4"
311
+ />
312
+ <p className="insert-chart__hint" id="insert-chart-error-desc">
313
+ First row is used as series labels and the first column as category labels — same
314
+ convention as Excel's Insert &gt; Chart.
315
+ </p>
316
+ </fieldset>
317
+ </div>
318
+ </Dialog>
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Inline SVG thumbnail for each subtype. ECharts canvases would be
324
+ * too heavy to mount one per option; small static SVGs read at a
325
+ * glance and match the Excel preview tiles' role.
326
+ */
327
+ function SubtypeThumbnail({ type }: { type: ChartType }) {
328
+ return (
329
+ <svg
330
+ className="insert-chart__thumb"
331
+ viewBox="0 0 60 40"
332
+ width={60}
333
+ height={40}
334
+ aria-hidden="true"
335
+ >
336
+ {renderThumbBody(type)}
337
+ </svg>
338
+ );
339
+ }
340
+
341
+ function renderThumbBody(type: ChartType): ReactElement {
342
+ const stroke = 'currentColor';
343
+ switch (type) {
344
+ case 'column':
345
+ return (
346
+ <g fill={stroke}>
347
+ <rect x="6" y="20" width="6" height="14" />
348
+ <rect x="16" y="12" width="6" height="22" />
349
+ <rect x="26" y="8" width="6" height="26" />
350
+ <rect x="36" y="16" width="6" height="18" />
351
+ <rect x="46" y="22" width="6" height="12" />
352
+ </g>
353
+ );
354
+ case 'column-stacked':
355
+ return (
356
+ <g>
357
+ <rect x="10" y="22" width="10" height="12" fill={stroke} />
358
+ <rect x="10" y="12" width="10" height="10" fill={stroke} opacity="0.6" />
359
+ <rect x="26" y="16" width="10" height="18" fill={stroke} />
360
+ <rect x="26" y="8" width="10" height="8" fill={stroke} opacity="0.6" />
361
+ <rect x="42" y="20" width="10" height="14" fill={stroke} />
362
+ <rect x="42" y="10" width="10" height="10" fill={stroke} opacity="0.6" />
363
+ </g>
364
+ );
365
+ case 'column-stacked-100':
366
+ return (
367
+ <g>
368
+ <rect x="10" y="20" width="10" height="14" fill={stroke} />
369
+ <rect x="10" y="6" width="10" height="14" fill={stroke} opacity="0.6" />
370
+ <rect x="26" y="14" width="10" height="20" fill={stroke} />
371
+ <rect x="26" y="6" width="10" height="8" fill={stroke} opacity="0.6" />
372
+ <rect x="42" y="24" width="10" height="10" fill={stroke} />
373
+ <rect x="42" y="6" width="10" height="18" fill={stroke} opacity="0.6" />
374
+ </g>
375
+ );
376
+ case 'bar':
377
+ return (
378
+ <g fill={stroke}>
379
+ <rect x="6" y="6" width="22" height="5" />
380
+ <rect x="6" y="14" width="36" height="5" />
381
+ <rect x="6" y="22" width="48" height="5" />
382
+ <rect x="6" y="30" width="28" height="5" />
383
+ </g>
384
+ );
385
+ case 'bar-stacked':
386
+ return (
387
+ <g>
388
+ <rect x="6" y="10" width="20" height="6" fill={stroke} />
389
+ <rect x="26" y="10" width="16" height="6" fill={stroke} opacity="0.6" />
390
+ <rect x="6" y="20" width="14" height="6" fill={stroke} />
391
+ <rect x="20" y="20" width="28" height="6" fill={stroke} opacity="0.6" />
392
+ <rect x="6" y="30" width="30" height="6" fill={stroke} />
393
+ <rect x="36" y="30" width="14" height="6" fill={stroke} opacity="0.6" />
394
+ </g>
395
+ );
396
+ case 'bar-stacked-100':
397
+ return (
398
+ <g>
399
+ <rect x="6" y="10" width="28" height="6" fill={stroke} />
400
+ <rect x="34" y="10" width="20" height="6" fill={stroke} opacity="0.6" />
401
+ <rect x="6" y="20" width="14" height="6" fill={stroke} />
402
+ <rect x="20" y="20" width="34" height="6" fill={stroke} opacity="0.6" />
403
+ <rect x="6" y="30" width="40" height="6" fill={stroke} />
404
+ <rect x="46" y="30" width="8" height="6" fill={stroke} opacity="0.6" />
405
+ </g>
406
+ );
407
+ case 'line':
408
+ return (
409
+ <g fill="none" stroke={stroke} strokeWidth="2">
410
+ <polyline points="6,30 18,18 30,24 42,10 54,16" />
411
+ <circle cx="6" cy="30" r="2" fill={stroke} />
412
+ <circle cx="18" cy="18" r="2" fill={stroke} />
413
+ <circle cx="30" cy="24" r="2" fill={stroke} />
414
+ <circle cx="42" cy="10" r="2" fill={stroke} />
415
+ <circle cx="54" cy="16" r="2" fill={stroke} />
416
+ </g>
417
+ );
418
+ case 'line-stacked':
419
+ return (
420
+ <g fill="none" strokeWidth="2">
421
+ <polyline points="6,32 18,28 30,30 42,24 54,26" stroke={stroke} />
422
+ <polyline points="6,20 18,12 30,18 42,8 54,14" stroke={stroke} opacity="0.6" />
423
+ </g>
424
+ );
425
+ case 'area':
426
+ return (
427
+ <g>
428
+ <polygon points="6,34 6,24 18,14 30,18 42,8 54,12 54,34" fill={stroke} opacity="0.4" />
429
+ <polyline
430
+ points="6,24 18,14 30,18 42,8 54,12"
431
+ fill="none"
432
+ stroke={stroke}
433
+ strokeWidth="2"
434
+ />
435
+ </g>
436
+ );
437
+ case 'area-stacked':
438
+ return (
439
+ <g>
440
+ <polygon points="6,34 6,24 18,18 30,22 42,16 54,20 54,34" fill={stroke} opacity="0.6" />
441
+ <polygon
442
+ points="6,24 18,18 30,22 42,16 54,20 54,12 42,6 30,12 18,8 6,14"
443
+ fill={stroke}
444
+ opacity="0.3"
445
+ />
446
+ </g>
447
+ );
448
+ case 'pie':
449
+ return (
450
+ <g>
451
+ <circle cx="30" cy="20" r="14" fill={stroke} opacity="0.4" />
452
+ <path d="M30 20 L30 6 A14 14 0 0 1 42 24 Z" fill={stroke} />
453
+ </g>
454
+ );
455
+ case 'doughnut':
456
+ return (
457
+ <g>
458
+ <circle cx="30" cy="20" r="14" fill={stroke} opacity="0.4" />
459
+ <path d="M30 20 L30 6 A14 14 0 0 1 42 24 Z" fill={stroke} />
460
+ <circle cx="30" cy="20" r="7" fill="white" />
461
+ </g>
462
+ );
463
+ case 'scatter':
464
+ return (
465
+ <g fill={stroke}>
466
+ <circle cx="10" cy="30" r="2" />
467
+ <circle cx="18" cy="22" r="2" />
468
+ <circle cx="24" cy="26" r="2" />
469
+ <circle cx="30" cy="14" r="2" />
470
+ <circle cx="36" cy="20" r="2" />
471
+ <circle cx="42" cy="10" r="2" />
472
+ <circle cx="48" cy="16" r="2" />
473
+ <circle cx="14" cy="14" r="2" />
474
+ <circle cx="50" cy="28" r="2" />
475
+ </g>
476
+ );
477
+ }
478
+ }