@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,286 @@
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
+ * PasteSpecialDialog — the SDK chrome's built-in Paste Special modal.
19
+ *
20
+ * Copies the DataValidationDialog/FormatCellsDialog structure: reads the active
21
+ * A1 selection off the FUniver facade, gathers a mode via a small form, and
22
+ * applies through real Univer commands / facade calls (no app context).
23
+ *
24
+ * Modes → mechanism (all grounded in the INSTALLED @univerjs/sheets-ui build):
25
+ * - values → `sheet.command.paste` with `{ value: 'special-paste-value' }`
26
+ * - formats → `sheet.command.paste` with `{ value: 'special-paste-format' }`
27
+ * - formulas → `sheet.command.paste` with `{ value: 'special-paste-formula' }`
28
+ *
29
+ * The special-paste hook names live in `PREDEFINED_HOOK_NAME_PASTE`
30
+ * (sheets-ui/lib/es/index.js: SPECIAL_PASTE_VALUE/FORMAT/FORMULA), and the
31
+ * dedicated `SheetPasteValueCommand`/`SheetPasteFormatCommand` handlers just
32
+ * forward `SheetPasteCommand.id` (= 'sheet.command.paste', name verified in
33
+ * the same build) with those `{ value }` params. We dispatch the base
34
+ * `sheet.command.paste` with the matching hook value so all three modes go
35
+ * through one real, installed command — including 'formulas', which has no
36
+ * dedicated `sheet.command.paste-*` id but IS a valid paste hook.
37
+ *
38
+ * - transpose → in-place matrix flip via the sheets facade:
39
+ * `FRange.getValues()` → transpose → `getRange(row, col, cols, rows)` →
40
+ * `.setValues()`. There is NO transpose paste command anywhere in the
41
+ * installed sheets/sheets-ui build (grep 'transpose' returns nothing), so
42
+ * rather than stub it we transpose the CURRENT selection in place using the
43
+ * real `getValues`/`getRange`/`setValues` facade methods (verified in
44
+ * @univerjs/sheets/facade f-range.d.ts + f-worksheet.d.ts). See the
45
+ * `limitations` note: this transposes the selection itself, not clipboard
46
+ * contents (the facade exposes no clipboard-transpose primitive).
47
+ *
48
+ * Mounted by `<DialogHost>` when `openDialog('paste-special')` is called and no
49
+ * host override is registered.
50
+ */
51
+
52
+ import { useMemo, useState, type CSSProperties } from 'react';
53
+ // Side-effect import: installs the sheets-ui facade extensions and registers
54
+ // the clipboard/paste commands on the FUniver facade so
55
+ // `executeCommand('sheet.command.paste', …)` resolves at runtime. Mirrors the
56
+ // `@univerjs/sheets/facade` side-effect import the SDK's api.ts already does for
57
+ // the core sheets mixins.
58
+ import '@univerjs/sheets-ui/facade';
59
+ import type { DialogComponentProps } from './extensions';
60
+ import type { CasualSheetsAPI } from '../sheets/api';
61
+ import { Dialog } from './Dialog';
62
+ import {
63
+ DIALOG_BTN_PRIMARY_STYLE,
64
+ DIALOG_BTN_SECONDARY_STYLE,
65
+ DIALOG_FIELD_STYLE,
66
+ DIALOG_LABEL_STYLE,
67
+ } from './dialog-styles';
68
+
69
+ /** Paste-special modes this dialog offers. */
70
+ type PasteMode = 'values' | 'formats' | 'formulas' | 'transpose';
71
+
72
+ const MODE_OPTIONS: Array<{ value: PasteMode; label: string; hint: string }> = [
73
+ {
74
+ value: 'values',
75
+ label: 'Values only',
76
+ hint: 'Paste cell values, dropping formatting and formulas.',
77
+ },
78
+ {
79
+ value: 'formats',
80
+ label: 'Formats only',
81
+ hint: 'Paste number formats, fonts, borders and fills — no values.',
82
+ },
83
+ {
84
+ value: 'formulas',
85
+ label: 'Formulas only',
86
+ hint: 'Paste formulas, adjusting relative references.',
87
+ },
88
+ {
89
+ value: 'transpose',
90
+ label: 'Transpose',
91
+ hint: 'Flip the current selection — rows become columns and columns become rows, in place.',
92
+ },
93
+ ];
94
+
95
+ /** Paste hook names from sheets-ui `PREDEFINED_HOOK_NAME_PASTE`. */
96
+ const PASTE_HOOK: Record<'values' | 'formats' | 'formulas', string> = {
97
+ values: 'special-paste-value',
98
+ formats: 'special-paste-format',
99
+ formulas: 'special-paste-formula',
100
+ };
101
+
102
+ /** The base sheets paste command; the special modes are `{ value: <hook> }`. */
103
+ const SHEET_PASTE_COMMAND = 'sheet.command.paste';
104
+
105
+ /** Minimal shape of the FRange we lean on for the transpose path. */
106
+ interface TransposableRange {
107
+ getRow(): number;
108
+ getColumn(): number;
109
+ getWidth(): number;
110
+ getHeight(): number;
111
+ getValues(): unknown[][];
112
+ getA1Notation?(): string;
113
+ }
114
+
115
+ /** The active FRange, or null when there is no selection. */
116
+ function activeRange(api: CasualSheetsAPI) {
117
+ return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
118
+ }
119
+
120
+ /**
121
+ * Transpose the current selection in place using the sheets facade: read the
122
+ * value matrix, flip it, then write it back into a range whose rows/columns are
123
+ * swapped, anchored at the same top-left cell. Returns false when there is no
124
+ * selection or nothing to transpose.
125
+ */
126
+ function transposeSelection(api: CasualSheetsAPI): boolean {
127
+ const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
128
+ const range = sheet?.getActiveRange() as unknown as TransposableRange | null;
129
+ if (!sheet || !range) return false;
130
+
131
+ const rows = range.getHeight();
132
+ const cols = range.getWidth();
133
+ if (rows <= 0 || cols <= 0) return false;
134
+
135
+ const values = range.getValues();
136
+ const transposed: unknown[][] = [];
137
+ for (let c = 0; c < cols; c++) {
138
+ const newRow: unknown[] = [];
139
+ for (let r = 0; r < rows; r++) {
140
+ newRow.push(values[r]?.[c] ?? null);
141
+ }
142
+ transposed.push(newRow);
143
+ }
144
+
145
+ // New range: same anchor, swapped dimensions (cols rows × rows columns).
146
+ const target = sheet.getRange(range.getRow(), range.getColumn(), cols, rows) as unknown as {
147
+ setValues: (v: unknown[][]) => unknown;
148
+ };
149
+ target.setValues(transposed);
150
+ return true;
151
+ }
152
+
153
+ /**
154
+ * Apply the chosen paste-special mode. Values/formats/formulas dispatch the real
155
+ * `sheet.command.paste` with the matching special-paste hook; transpose flips the
156
+ * selection in place. Returns a promise that resolves to whether it ran.
157
+ */
158
+ async function applyPasteSpecial(api: CasualSheetsAPI, mode: PasteMode): Promise<boolean> {
159
+ if (activeRange(api) === null) return false;
160
+
161
+ if (mode === 'transpose') {
162
+ return transposeSelection(api);
163
+ }
164
+
165
+ return api.executeCommand(SHEET_PASTE_COMMAND, { value: PASTE_HOOK[mode] });
166
+ }
167
+
168
+ const MODE_RADIO_ROW_STYLE: CSSProperties = {
169
+ display: 'flex',
170
+ alignItems: 'flex-start',
171
+ gap: 8,
172
+ padding: '8px 10px',
173
+ border: '1px solid var(--cs-chrome-border, #cdd3db)',
174
+ borderRadius: 6,
175
+ marginBottom: 8,
176
+ cursor: 'pointer',
177
+ };
178
+
179
+ const MODE_RADIO_ROW_ACTIVE_STYLE: CSSProperties = {
180
+ ...MODE_RADIO_ROW_STYLE,
181
+ borderColor: 'var(--cs-chrome-active-fg, #0e7490)',
182
+ background: 'var(--cs-chrome-active-bg, rgba(14, 116, 144, 0.06))',
183
+ };
184
+
185
+ const MODE_LABEL_STYLE: CSSProperties = {
186
+ fontSize: 13,
187
+ fontWeight: 500,
188
+ color: 'var(--cs-chrome-fg, #201f1e)',
189
+ };
190
+
191
+ const MODE_HINT_STYLE: CSSProperties = {
192
+ fontSize: 12,
193
+ color: 'var(--cs-chrome-muted, #605e5c)',
194
+ marginTop: 2,
195
+ lineHeight: 1.35,
196
+ };
197
+
198
+ const RANGE_NOTE_STYLE: CSSProperties = {
199
+ fontSize: 12,
200
+ color: 'var(--cs-chrome-muted, #605e5c)',
201
+ marginBottom: 12,
202
+ };
203
+
204
+ export function PasteSpecialDialog({ api, onClose }: DialogComponentProps) {
205
+ const [mode, setMode] = useState<PasteMode>('values');
206
+
207
+ // Read the selection once for the header hint (getA1Notation is verified on
208
+ // the sheets facade FRange — f-range.d.ts).
209
+ const rangeLabel = useMemo(() => {
210
+ const fRange = activeRange(api) as unknown as { getA1Notation?: () => string } | null;
211
+ return fRange?.getA1Notation?.() ?? null;
212
+ }, [api]);
213
+
214
+ const hasSelection = activeRange(api) !== null;
215
+
216
+ const apply = () => {
217
+ void applyPasteSpecial(api, mode).then((ok) => {
218
+ if (ok) onClose();
219
+ });
220
+ };
221
+
222
+ return (
223
+ <Dialog
224
+ title="Paste special"
225
+ onClose={onClose}
226
+ width={440}
227
+ data-testid="cs-paste-special-dialog"
228
+ footer={
229
+ <>
230
+ <button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
231
+ Cancel
232
+ </button>
233
+ <button
234
+ type="button"
235
+ style={DIALOG_BTN_PRIMARY_STYLE}
236
+ data-testid="cs-paste-special-apply"
237
+ disabled={!hasSelection}
238
+ onClick={apply}
239
+ >
240
+ Paste
241
+ </button>
242
+ </>
243
+ }
244
+ >
245
+ {hasSelection ? (
246
+ <div style={RANGE_NOTE_STYLE} data-testid="cs-paste-special-range">
247
+ {mode === 'transpose' ? 'Transposes' : 'Pastes into'}{' '}
248
+ <strong>{rangeLabel ?? 'the current selection'}</strong>
249
+ </div>
250
+ ) : (
251
+ <div style={RANGE_NOTE_STYLE} data-testid="cs-paste-special-no-selection">
252
+ Select the destination cell(s) first, then reopen this dialog.
253
+ </div>
254
+ )}
255
+
256
+ <div style={DIALOG_FIELD_STYLE}>
257
+ <span style={DIALOG_LABEL_STYLE}>Paste</span>
258
+ <div role="radiogroup" aria-label="Paste special mode">
259
+ {MODE_OPTIONS.map((opt) => {
260
+ const active = mode === opt.value;
261
+ return (
262
+ <label
263
+ key={opt.value}
264
+ style={active ? MODE_RADIO_ROW_ACTIVE_STYLE : MODE_RADIO_ROW_STYLE}
265
+ data-testid={`cs-paste-special-mode-${opt.value}`}
266
+ >
267
+ <input
268
+ type="radio"
269
+ name="cs-paste-special-mode"
270
+ value={opt.value}
271
+ checked={active}
272
+ onChange={() => setMode(opt.value)}
273
+ style={{ marginTop: 2 }}
274
+ />
275
+ <span>
276
+ <span style={MODE_LABEL_STYLE}>{opt.label}</span>
277
+ <span style={MODE_HINT_STYLE}>{opt.hint}</span>
278
+ </span>
279
+ </label>
280
+ );
281
+ })}
282
+ </div>
283
+ </div>
284
+ </Dialog>
285
+ );
286
+ }