@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,427 @@
1
+ /**
2
+ * Copyright 2026 Casual Office
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License").
5
+ */
6
+
7
+ /**
8
+ * Comments side panel: lists the thread comments on the active sheet (active +
9
+ * resolved), navigates to a comment's cell on click, and resolves / reopens a
10
+ * thread inline. "Add comment" opens Univer's comment-on-cell modal (the same
11
+ * one as Review → New comment); the in-cell popup owns full reply threading.
12
+ *
13
+ * Ported from the standalone app's `shell/CommentsPanel`. The app-only collab
14
+ * stores it depended on — `collab/comment-authors` (avatar authorship),
15
+ * `collab/presence` (display name / initials) and `collab/comment-mentions`
16
+ * (@-mention "mentions you" badge) — DO NOT exist in the SDK and are dropped:
17
+ * the byline degrades to the in-band `personId` string carried on the comment
18
+ * model (no avatar, no color), and the mentions-me highlight is omitted. The
19
+ * `useUniverAPI()` / `useUI()` couplings are replaced by the `api` / `onClose`
20
+ * panel-host props, and the Univer facade is reached through `api.univer`.
21
+ */
22
+ import { useEffect, useMemo, useState, type CSSProperties } from 'react';
23
+ import { SheetsThreadCommentModel } from '@univerjs/sheets-thread-comment';
24
+
25
+ import { ensurePluginByName } from '../univer';
26
+ import type { PanelComponentProps } from './extensions';
27
+ import { Icon } from './Icon';
28
+
29
+ interface CommentRow {
30
+ id: string;
31
+ text: string;
32
+ ref: string;
33
+ author: string;
34
+ replies: number;
35
+ }
36
+
37
+ function cleanText(stream: string | undefined): string {
38
+ return (stream ?? '').replace(/[\r\n\t]+/g, ' ').trim() || '(empty comment)';
39
+ }
40
+
41
+ /**
42
+ * Active comments come straight off the facade (`getComments()` returns the
43
+ * cell-location index; resolved threads leave it, so they're read separately).
44
+ * Only root comments are listed — replies render under their root in the
45
+ * in-cell popup.
46
+ */
47
+ /* eslint-disable @typescript-eslint/no-explicit-any */
48
+ function readActive(univer: any): CommentRow[] {
49
+ const ws = univer?.getActiveWorkbook?.()?.getActiveSheet?.();
50
+ const all: any[] = ws?.getComments?.() ?? [];
51
+ const rows: CommentRow[] = [];
52
+ for (const c of all) {
53
+ try {
54
+ if (c.getIsRoot && !c.getIsRoot()) continue;
55
+ const data = c.getCommentData?.() ?? {};
56
+ let ref = '';
57
+ try {
58
+ ref = c.getRange?.()?.getA1Notation?.() ?? '';
59
+ } catch {
60
+ /* range no longer resolvable */
61
+ }
62
+ rows.push({
63
+ id: c.id ?? data.id ?? ref,
64
+ text: cleanText(data.text?.dataStream),
65
+ ref,
66
+ author: data.personId ?? '',
67
+ replies: c.getReplies?.()?.length ?? 0,
68
+ });
69
+ } catch {
70
+ /* skip malformed thread */
71
+ }
72
+ }
73
+ return rows;
74
+ }
75
+
76
+ /**
77
+ * Resolved comments have left the cell-location index, so `getComments()` no
78
+ * longer returns them. Read them straight from the model — the only path to a
79
+ * "resolved" view + reopen.
80
+ */
81
+ function readResolved(univer: any): CommentRow[] {
82
+ try {
83
+ const injector = (univer as { _injector?: { get(t: unknown): unknown } })?._injector;
84
+ const model = injector?.get?.(SheetsThreadCommentModel) as any;
85
+ const wb = univer?.getActiveWorkbook?.();
86
+ const ws = wb?.getActiveSheet?.();
87
+ if (!model?.getSubUnitAll || !wb || !ws) return [];
88
+ const unitId = wb.getId();
89
+ const subUnitId = ws.getSheetId?.() ?? ws.getId?.();
90
+ const all: any[] = model.getSubUnitAll(unitId, subUnitId) ?? [];
91
+ const rows: CommentRow[] = [];
92
+ for (const c of all) {
93
+ if (!c?.resolved) continue;
94
+ rows.push({
95
+ id: c.id,
96
+ text: cleanText(c.text?.dataStream),
97
+ ref: c.ref ?? '',
98
+ author: c.personId ?? '',
99
+ replies: (c.children ?? []).length,
100
+ });
101
+ }
102
+ return rows;
103
+ } catch {
104
+ return [];
105
+ }
106
+ }
107
+ /* eslint-enable @typescript-eslint/no-explicit-any */
108
+
109
+ const header: CSSProperties = {
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ gap: 8,
113
+ padding: '10px 12px',
114
+ borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
115
+ };
116
+
117
+ const rowCard: CSSProperties = {
118
+ border: '1px solid var(--cs-chrome-border, #edeff3)',
119
+ borderRadius: 8,
120
+ padding: 10,
121
+ display: 'flex',
122
+ alignItems: 'flex-start',
123
+ gap: 6,
124
+ };
125
+
126
+ const iconBtn: CSSProperties = {
127
+ border: 'none',
128
+ background: 'transparent',
129
+ cursor: 'pointer',
130
+ color: 'inherit',
131
+ padding: 0,
132
+ display: 'inline-flex',
133
+ alignItems: 'center',
134
+ };
135
+
136
+ function CommentCard({
137
+ row,
138
+ resolved,
139
+ onOpen,
140
+ onToggle,
141
+ }: {
142
+ row: CommentRow;
143
+ resolved: boolean;
144
+ onOpen: (r: CommentRow) => void;
145
+ onToggle: (id: string, value: boolean) => void;
146
+ }) {
147
+ return (
148
+ <li
149
+ data-testid={`cs-comments-panel-${resolved ? 'resolved-row' : 'row'}-${row.id}`}
150
+ style={{ ...rowCard, opacity: resolved ? 0.72 : 1 }}
151
+ >
152
+ <button
153
+ type="button"
154
+ onClick={() => onOpen(row)}
155
+ title={`Go to ${row.ref || 'comment'}`}
156
+ style={{
157
+ flex: 1,
158
+ minWidth: 0,
159
+ textAlign: 'left',
160
+ border: 'none',
161
+ background: 'transparent',
162
+ cursor: 'pointer',
163
+ color: 'inherit',
164
+ font: 'inherit',
165
+ display: 'flex',
166
+ flexDirection: 'column',
167
+ gap: 4,
168
+ }}
169
+ >
170
+ <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
171
+ <span style={{ fontWeight: 600 }}>{row.ref || 'Comment'}</span>
172
+ {row.author && <span style={{ opacity: 0.6, fontSize: 12 }}>{row.author}</span>}
173
+ {row.replies > 0 && (
174
+ <span
175
+ title={`${row.replies} replies`}
176
+ style={{
177
+ opacity: 0.6,
178
+ fontSize: 12,
179
+ display: 'inline-flex',
180
+ alignItems: 'center',
181
+ gap: 2,
182
+ }}
183
+ >
184
+ <Icon name="reply" size={13} />
185
+ {row.replies}
186
+ </span>
187
+ )}
188
+ </span>
189
+ <span
190
+ style={{
191
+ fontSize: 13,
192
+ overflow: 'hidden',
193
+ textOverflow: 'ellipsis',
194
+ display: '-webkit-box',
195
+ WebkitLineClamp: 2,
196
+ WebkitBoxOrient: 'vertical',
197
+ }}
198
+ >
199
+ {row.text}
200
+ </span>
201
+ </button>
202
+ <button
203
+ type="button"
204
+ data-testid={`cs-comments-panel-${resolved ? 'reopen' : 'resolve'}-${row.id}`}
205
+ aria-label={resolved ? 'Reopen comment' : 'Resolve comment'}
206
+ title={resolved ? 'Reopen comment' : 'Resolve comment'}
207
+ onClick={() => onToggle(row.id, !resolved)}
208
+ style={iconBtn}
209
+ >
210
+ <Icon name={resolved ? 'undo' : 'check_circle'} size={16} />
211
+ </button>
212
+ </li>
213
+ );
214
+ }
215
+
216
+ const REFRESH_CMDS = (id: string) =>
217
+ id.includes('comment') ||
218
+ id === 'sheet.operation.set-worksheet-activate' ||
219
+ id === 'doc.command-replace-snapshot';
220
+
221
+ export function CommentsPanel({ api, onClose }: PanelComponentProps) {
222
+ const univer = (api as { univer?: any }).univer; // eslint-disable-line @typescript-eslint/no-explicit-any
223
+ const [rows, setRows] = useState<CommentRow[]>([]);
224
+ const [resolved, setResolved] = useState<CommentRow[]>([]);
225
+ const [showResolved, setShowResolved] = useState(false);
226
+
227
+ useEffect(() => {
228
+ if (!univer) return;
229
+ let cancelled = false;
230
+ let disp: { dispose?: () => void } | undefined;
231
+ const read = () => {
232
+ if (cancelled) return;
233
+ setRows(readActive(univer));
234
+ setResolved(readResolved(univer));
235
+ };
236
+ // The thread-comment plugin is lazy — `getComments()` and
237
+ // SheetsThreadCommentModel only resolve once its group is registered.
238
+ void ensurePluginByName('threadComment').then(() => {
239
+ if (cancelled) return;
240
+ read();
241
+ disp = univer.addEvent(univer.Event.CommandExecuted, (e: { id?: string }) => {
242
+ if (e.id && REFRESH_CMDS(e.id)) read();
243
+ });
244
+ });
245
+ return () => {
246
+ cancelled = true;
247
+ disp?.dispose?.();
248
+ };
249
+ }, [univer]);
250
+
251
+ const empty = rows.length === 0 && resolved.length === 0;
252
+
253
+ const openThread = (r: CommentRow) => {
254
+ if (!univer || !r.ref) return;
255
+ try {
256
+ univer.getActiveWorkbook?.()?.getActiveSheet?.()?.getRange?.(r.ref)?.activate?.();
257
+ } catch {
258
+ /* range gone */
259
+ }
260
+ };
261
+
262
+ const setResolvedState = async (commentId: string, value: boolean) => {
263
+ if (!univer) return;
264
+ try {
265
+ const wb = univer.getActiveWorkbook?.();
266
+ const ws = wb?.getActiveSheet?.();
267
+ if (!wb || !ws) return;
268
+ const subUnitId = ws.getSheetId?.() ?? ws.getId?.();
269
+ await ensurePluginByName('threadComment');
270
+ univer.executeCommand('thread-comment.command.resolve-comment', {
271
+ unitId: wb.getId(),
272
+ subUnitId,
273
+ commentId,
274
+ resolved: value,
275
+ });
276
+ } catch {
277
+ /* command unavailable — plugin not loaded */
278
+ }
279
+ };
280
+
281
+ const addComment = async () => {
282
+ if (!univer) return;
283
+ await ensurePluginByName('threadComment');
284
+ univer.executeCommand('sheet.operation.show-comment-modal');
285
+ };
286
+
287
+ const handlers = useMemo(
288
+ () => ({ open: openThread, toggle: setResolvedState }),
289
+ // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ [univer],
291
+ );
292
+
293
+ return (
294
+ <div
295
+ data-testid="cs-comments-panel"
296
+ style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
297
+ >
298
+ <header style={header}>
299
+ <Icon name="forum" size={18} />
300
+ <span style={{ fontWeight: 600, flex: 1 }}>Comments</span>
301
+ {rows.length > 0 && <span style={{ opacity: 0.6, fontSize: 12 }}>{rows.length}</span>}
302
+ <button
303
+ type="button"
304
+ aria-label="Add comment"
305
+ title="Add comment"
306
+ onClick={addComment}
307
+ style={iconBtn}
308
+ >
309
+ <Icon name="add_comment" size={18} />
310
+ </button>
311
+ <button type="button" aria-label="Close comments panel" onClick={onClose} style={iconBtn}>
312
+ <Icon name="close" size={18} />
313
+ </button>
314
+ </header>
315
+
316
+ <div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
317
+ {empty ? (
318
+ <div
319
+ data-testid="cs-comments-panel-empty"
320
+ style={{ textAlign: 'center', opacity: 0.75, padding: '24px 8px' }}
321
+ >
322
+ <Icon name="forum" size={40} style={{ opacity: 0.4 }} />
323
+ <div style={{ fontWeight: 600, marginTop: 8 }}>No comments yet</div>
324
+ <div style={{ fontSize: 13, marginTop: 4 }}>
325
+ Select a cell and add a comment to start a discussion — or use{' '}
326
+ <strong>Review → New comment</strong>.
327
+ </div>
328
+ <button
329
+ type="button"
330
+ data-testid="cs-comments-panel-empty-cta"
331
+ disabled={!univer}
332
+ onClick={addComment}
333
+ style={{
334
+ marginTop: 12,
335
+ padding: '6px 14px',
336
+ borderRadius: 6,
337
+ border: 'none',
338
+ cursor: univer ? 'pointer' : 'default',
339
+ background: 'var(--cs-chrome-active-fg, #0e7490)',
340
+ color: '#fff',
341
+ font: 'inherit',
342
+ fontWeight: 600,
343
+ }}
344
+ >
345
+ Add comment
346
+ </button>
347
+ </div>
348
+ ) : (
349
+ <>
350
+ {rows.length > 0 && (
351
+ <ul
352
+ style={{
353
+ listStyle: 'none',
354
+ margin: 0,
355
+ padding: 0,
356
+ display: 'flex',
357
+ flexDirection: 'column',
358
+ gap: 8,
359
+ }}
360
+ >
361
+ {rows.map((r) => (
362
+ <CommentCard
363
+ key={r.id}
364
+ row={r}
365
+ resolved={false}
366
+ onOpen={handlers.open}
367
+ onToggle={handlers.toggle}
368
+ />
369
+ ))}
370
+ </ul>
371
+ )}
372
+
373
+ {resolved.length > 0 && (
374
+ <div style={{ marginTop: rows.length > 0 ? 12 : 0 }}>
375
+ <button
376
+ type="button"
377
+ data-testid="cs-comments-panel-resolved-toggle"
378
+ aria-expanded={showResolved}
379
+ onClick={() => setShowResolved((v) => !v)}
380
+ style={{
381
+ display: 'flex',
382
+ alignItems: 'center',
383
+ gap: 4,
384
+ width: '100%',
385
+ border: 'none',
386
+ background: 'transparent',
387
+ cursor: 'pointer',
388
+ color: 'inherit',
389
+ font: 'inherit',
390
+ fontWeight: 600,
391
+ padding: '4px 0',
392
+ }}
393
+ >
394
+ <Icon name={showResolved ? 'expand_more' : 'chevron_right'} size={16} />
395
+ Resolved
396
+ <span style={{ opacity: 0.6, fontSize: 12 }}>{resolved.length}</span>
397
+ </button>
398
+ {showResolved && (
399
+ <ul
400
+ style={{
401
+ listStyle: 'none',
402
+ margin: '8px 0 0',
403
+ padding: 0,
404
+ display: 'flex',
405
+ flexDirection: 'column',
406
+ gap: 8,
407
+ }}
408
+ >
409
+ {resolved.map((r) => (
410
+ <CommentCard
411
+ key={r.id}
412
+ row={r}
413
+ resolved
414
+ onOpen={handlers.open}
415
+ onToggle={handlers.toggle}
416
+ />
417
+ ))}
418
+ </ul>
419
+ )}
420
+ </div>
421
+ )}
422
+ </>
423
+ )}
424
+ </div>
425
+ </div>
426
+ );
427
+ }