@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,357 @@
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
+ * CustomSortDialog — the SDK chrome's built-in "Sort range" modal.
19
+ *
20
+ * Multi-level sort of the active A1 selection: the user adds one or more sort
21
+ * levels (a column + ascending/descending), plus a "data has header row" toggle
22
+ * that keeps the header in place and labels the column pickers with the header
23
+ * text. Applied through the `@univerjs/sheets-sort` facade — the
24
+ * `FRange.sort(SortColumnSpec | SortColumnSpec[])` extension, where
25
+ * `SortColumnSpec = { column: number; ascending: boolean } | number` and
26
+ * `column` is 0-based **relative to the sorted range's first column**
27
+ * (verified in `@univerjs/sheets-sort/lib/types/facade/f-range.d.ts` L17-49).
28
+ *
29
+ * The facade `sort()` has no first-class "has header" option, so the header row
30
+ * is honoured by sorting a sub-range that starts one row below the selection's
31
+ * top (`FWorksheet.getRange(row, col, numRows, numCols)`, verified in
32
+ * `@univerjs/sheets/lib/types/facade/f-worksheet.d.ts` L258) — the header row is
33
+ * simply excluded from the sorted band, so it stays put.
34
+ *
35
+ * Mounted by `<DialogHost>` when `openDialog('custom-sort')` is called and no
36
+ * host override is registered.
37
+ */
38
+
39
+ import { useMemo, useState, type CSSProperties } from 'react';
40
+ // Side-effect import: installs `FRange.sort()` (and the sort mixin's TS
41
+ // augmentation of `@univerjs/sheets/facade`) on the facade prototype. The sort
42
+ // plugin registers the command but does NOT import this facade module, so
43
+ // without it `range.sort(...)` is undefined at runtime. Mirrors the
44
+ // `sheets-data-validation/facade` side-effect import in DataValidationDialog.
45
+ import '@univerjs/sheets-sort/facade';
46
+ import type { DialogComponentProps } from './extensions';
47
+ import type { CasualSheetsAPI } from '../sheets/api';
48
+ import { Dialog } from './Dialog';
49
+ import {
50
+ DIALOG_BTN_PRIMARY_STYLE,
51
+ DIALOG_BTN_SECONDARY_STYLE,
52
+ DIALOG_FIELD_STYLE,
53
+ DIALOG_INPUT_STYLE,
54
+ DIALOG_LABEL_STYLE,
55
+ } from './dialog-styles';
56
+
57
+ /** One sort level: a range-relative column offset + direction. */
58
+ interface SortLevel {
59
+ /** 0-based offset from the range's first column. */
60
+ column: number;
61
+ ascending: boolean;
62
+ }
63
+
64
+ /** The `SortColumnSpec` object shape from the sheets-sort facade. */
65
+ type SortColumnSpec = { column: number; ascending: boolean };
66
+
67
+ /** The active FRange, or null when there is no selection. */
68
+ function activeRange(api: CasualSheetsAPI) {
69
+ return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
70
+ }
71
+
72
+ /** Minimal shape we lean on off the live FRange (grounded in sheets/facade). */
73
+ interface RangeGeom {
74
+ getRow(): number;
75
+ getColumn(): number;
76
+ getWidth(): number;
77
+ getHeight(): number;
78
+ getA1Notation?: () => string;
79
+ getValues?: () => Array<Array<unknown>>;
80
+ }
81
+
82
+ /** Convert a 0-based absolute column index to a spreadsheet letter (A, B, …, AA). */
83
+ function columnLetter(index: number): string {
84
+ let n = index;
85
+ let label = '';
86
+ do {
87
+ label = String.fromCharCode(65 + (n % 26)) + label;
88
+ n = Math.floor(n / 26) - 1;
89
+ } while (n >= 0);
90
+ return label;
91
+ }
92
+
93
+ /** Read geometry + optional header labels off the active range. */
94
+ interface RangeInfo {
95
+ /** First (absolute) row of the selection. */
96
+ startRow: number;
97
+ /** First (absolute) column of the selection. */
98
+ startColumn: number;
99
+ /** Number of columns in the selection. */
100
+ width: number;
101
+ /** Number of rows in the selection. */
102
+ height: number;
103
+ /** A1 label for the header note, e.g. "A1:C10". */
104
+ a1: string | null;
105
+ /** Values of the first row (header candidates), if readable. */
106
+ firstRow: Array<unknown> | null;
107
+ }
108
+
109
+ function readRangeInfo(api: CasualSheetsAPI): RangeInfo | null {
110
+ const range = activeRange(api) as unknown as RangeGeom | null;
111
+ if (!range) return null;
112
+ const values = range.getValues?.();
113
+ return {
114
+ startRow: range.getRow(),
115
+ startColumn: range.getColumn(),
116
+ width: range.getWidth(),
117
+ height: range.getHeight(),
118
+ a1: range.getA1Notation?.() ?? null,
119
+ firstRow: values && values.length > 0 ? values[0] : null,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Apply the multi-level sort via the sheets-sort facade. Returns false when
125
+ * there's no selection or nothing sensible to sort.
126
+ */
127
+ function applySort(
128
+ api: CasualSheetsAPI,
129
+ info: RangeInfo,
130
+ levels: SortLevel[],
131
+ hasHeader: boolean,
132
+ ): boolean {
133
+ const worksheet = api.univer.getActiveWorkbook()?.getActiveSheet();
134
+ if (!worksheet) return false;
135
+
136
+ // When the data has a header row, exclude it from the sorted band so it stays
137
+ // pinned at the top. Otherwise sort the whole selection.
138
+ const bandStartRow = hasHeader ? info.startRow + 1 : info.startRow;
139
+ const bandHeight = hasHeader ? info.height - 1 : info.height;
140
+ if (bandHeight <= 0) return false;
141
+
142
+ // getRange(row, column, numRows, numColumns) — sheets/facade f-worksheet.d.ts L258.
143
+ const band = (
144
+ worksheet as unknown as {
145
+ getRange: (row: number, col: number, numRows: number, numCols: number) => unknown;
146
+ }
147
+ ).getRange(bandStartRow, info.startColumn, bandHeight, info.width);
148
+
149
+ // Clamp each level's column offset to the range and drop invalid ones.
150
+ const specs: SortColumnSpec[] = levels
151
+ .filter((l) => l.column >= 0 && l.column < info.width)
152
+ .map((l) => ({ column: l.column, ascending: l.ascending }));
153
+ if (specs.length === 0) return false;
154
+
155
+ // FRange.sort(SortColumnSpec[]) — sheets-sort/facade f-range.d.ts L46. Column
156
+ // indices are relative to the (sub-)range's first column, which is exactly the
157
+ // 0-based offset the pickers produce.
158
+ (band as { sort: (c: SortColumnSpec | SortColumnSpec[]) => unknown }).sort(specs);
159
+ return true;
160
+ }
161
+
162
+ const RANGE_NOTE_STYLE: CSSProperties = {
163
+ fontSize: 12,
164
+ color: 'var(--cs-chrome-muted, #605e5c)',
165
+ marginBottom: 12,
166
+ };
167
+
168
+ const CHECK_STYLE: CSSProperties = {
169
+ display: 'flex',
170
+ alignItems: 'center',
171
+ gap: 6,
172
+ marginBottom: 12,
173
+ cursor: 'pointer',
174
+ };
175
+
176
+ const LEVEL_ROW_STYLE: CSSProperties = {
177
+ display: 'grid',
178
+ gridTemplateColumns: 'auto 1fr 130px 28px',
179
+ alignItems: 'center',
180
+ gap: 8,
181
+ marginBottom: 8,
182
+ };
183
+
184
+ const LEVEL_PREFIX_STYLE: CSSProperties = {
185
+ fontSize: 12,
186
+ color: 'var(--cs-chrome-muted, #605e5c)',
187
+ whiteSpace: 'nowrap',
188
+ };
189
+
190
+ const REMOVE_BTN_STYLE: CSSProperties = {
191
+ width: 28,
192
+ height: 30,
193
+ display: 'inline-flex',
194
+ alignItems: 'center',
195
+ justifyContent: 'center',
196
+ border: '1px solid var(--cs-chrome-border, #cdd3db)',
197
+ borderRadius: 6,
198
+ background: 'var(--cs-chrome-input-bg, #fff)',
199
+ color: 'var(--cs-chrome-muted, #605e5c)',
200
+ cursor: 'pointer',
201
+ padding: 0,
202
+ fontSize: 16,
203
+ lineHeight: 1,
204
+ };
205
+
206
+ const ADD_BTN_STYLE: CSSProperties = {
207
+ ...DIALOG_BTN_SECONDARY_STYLE,
208
+ marginTop: 4,
209
+ };
210
+
211
+ export function CustomSortDialog({ api, onClose }: DialogComponentProps) {
212
+ // Read the selection once when the dialog mounts.
213
+ const info = useMemo(() => readRangeInfo(api), [api]);
214
+ const hasSelection = info !== null;
215
+ const width = info?.width ?? 1;
216
+
217
+ const [hasHeader, setHasHeader] = useState(false);
218
+ const [levels, setLevels] = useState<SortLevel[]>([{ column: 0, ascending: true }]);
219
+
220
+ // Column option labels: header text when "has header" is on and text exists,
221
+ // otherwise the absolute spreadsheet column letter.
222
+ const columnOptions = useMemo(() => {
223
+ const startCol = info?.startColumn ?? 0;
224
+ return Array.from({ length: width }, (_, offset) => {
225
+ const headerCell = hasHeader ? info?.firstRow?.[offset] : undefined;
226
+ const headerText =
227
+ headerCell !== undefined && headerCell !== null && String(headerCell).trim() !== ''
228
+ ? String(headerCell)
229
+ : null;
230
+ const letter = columnLetter(startCol + offset);
231
+ return {
232
+ value: offset,
233
+ label: headerText ? `${headerText} (${letter})` : `Column ${letter}`,
234
+ };
235
+ });
236
+ }, [width, hasHeader, info]);
237
+
238
+ const updateLevel = (index: number, patch: Partial<SortLevel>) =>
239
+ setLevels((prev) => prev.map((l, i) => (i === index ? { ...l, ...patch } : l)));
240
+
241
+ const addLevel = () => {
242
+ // Default the new level to the first column not already used, else column 0.
243
+ const used = new Set(levels.map((l) => l.column));
244
+ let next = 0;
245
+ for (let c = 0; c < width; c += 1) {
246
+ if (!used.has(c)) {
247
+ next = c;
248
+ break;
249
+ }
250
+ }
251
+ setLevels((prev) => [...prev, { column: next, ascending: true }]);
252
+ };
253
+
254
+ const removeLevel = (index: number) =>
255
+ setLevels((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index)));
256
+
257
+ const apply = () => {
258
+ if (info && applySort(api, info, levels, hasHeader)) onClose();
259
+ };
260
+
261
+ const canApply = hasSelection && levels.length > 0;
262
+
263
+ return (
264
+ <Dialog
265
+ title="Sort range"
266
+ onClose={onClose}
267
+ width={480}
268
+ data-testid="cs-custom-sort-dialog"
269
+ footer={
270
+ <>
271
+ <button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
272
+ Cancel
273
+ </button>
274
+ <button
275
+ type="button"
276
+ style={DIALOG_BTN_PRIMARY_STYLE}
277
+ data-testid="cs-custom-sort-apply"
278
+ disabled={!canApply}
279
+ onClick={apply}
280
+ >
281
+ Sort
282
+ </button>
283
+ </>
284
+ }
285
+ >
286
+ {hasSelection ? (
287
+ <div style={RANGE_NOTE_STYLE} data-testid="cs-custom-sort-range">
288
+ Sort <strong>{info?.a1 ?? 'the current selection'}</strong>
289
+ </div>
290
+ ) : (
291
+ <div style={RANGE_NOTE_STYLE} data-testid="cs-custom-sort-no-selection">
292
+ Select the range you want to sort first, then reopen this dialog.
293
+ </div>
294
+ )}
295
+
296
+ <label style={CHECK_STYLE} data-testid="cs-custom-sort-has-header-label">
297
+ <input
298
+ type="checkbox"
299
+ data-testid="cs-custom-sort-has-header"
300
+ checked={hasHeader}
301
+ onChange={(e) => setHasHeader(e.target.checked)}
302
+ />
303
+ <span>Data has header row</span>
304
+ </label>
305
+
306
+ <div style={DIALOG_FIELD_STYLE}>
307
+ <span style={DIALOG_LABEL_STYLE}>Sort by</span>
308
+ {levels.map((level, index) => (
309
+ <div key={index} style={LEVEL_ROW_STYLE} data-testid={`cs-custom-sort-level-${index}`}>
310
+ <span style={LEVEL_PREFIX_STYLE}>{index === 0 ? 'Sort by' : 'then by'}</span>
311
+ <select
312
+ style={DIALOG_INPUT_STYLE}
313
+ data-testid={`cs-custom-sort-column-${index}`}
314
+ value={level.column}
315
+ onChange={(e) => updateLevel(index, { column: Number(e.target.value) })}
316
+ >
317
+ {columnOptions.map((opt) => (
318
+ <option key={opt.value} value={opt.value}>
319
+ {opt.label}
320
+ </option>
321
+ ))}
322
+ </select>
323
+ <select
324
+ style={DIALOG_INPUT_STYLE}
325
+ data-testid={`cs-custom-sort-order-${index}`}
326
+ value={level.ascending ? 'asc' : 'desc'}
327
+ onChange={(e) => updateLevel(index, { ascending: e.target.value === 'asc' })}
328
+ >
329
+ <option value="asc">A → Z (ascending)</option>
330
+ <option value="desc">Z → A (descending)</option>
331
+ </select>
332
+ <button
333
+ type="button"
334
+ style={REMOVE_BTN_STYLE}
335
+ data-testid={`cs-custom-sort-remove-${index}`}
336
+ aria-label="Remove sort level"
337
+ disabled={levels.length <= 1}
338
+ onClick={() => removeLevel(index)}
339
+ >
340
+
341
+ </button>
342
+ </div>
343
+ ))}
344
+
345
+ <button
346
+ type="button"
347
+ style={ADD_BTN_STYLE}
348
+ data-testid="cs-custom-sort-add-level"
349
+ disabled={levels.length >= width}
350
+ onClick={addLevel}
351
+ >
352
+ Add another sort level
353
+ </button>
354
+ </div>
355
+ </Dialog>
356
+ );
357
+ }