@casualoffice/sheets 0.9.0 → 0.10.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 (67) hide show
  1. package/dist/api-BI2VLYQ6.d.cts +62 -0
  2. package/dist/api-BI2VLYQ6.d.ts +62 -0
  3. package/dist/chrome.cjs +2569 -0
  4. package/dist/chrome.cjs.map +1 -0
  5. package/dist/chrome.d.cts +96 -0
  6. package/dist/chrome.d.ts +96 -0
  7. package/dist/chrome.js +2556 -0
  8. package/dist/chrome.js.map +1 -0
  9. package/dist/collab.cjs +770 -0
  10. package/dist/collab.cjs.map +1 -0
  11. package/dist/collab.d.cts +248 -0
  12. package/dist/collab.d.ts +248 -0
  13. package/dist/collab.js +737 -0
  14. package/dist/collab.js.map +1 -0
  15. package/dist/embed/embed-runtime.js +128 -128
  16. package/dist/embed.cjs +166 -0
  17. package/dist/embed.cjs.map +1 -1
  18. package/dist/embed.d.cts +78 -3
  19. package/dist/embed.d.ts +78 -3
  20. package/dist/embed.js +166 -0
  21. package/dist/embed.js.map +1 -1
  22. package/dist/index.cjs +262 -165
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +4 -3
  25. package/dist/index.d.ts +4 -3
  26. package/dist/index.js +271 -168
  27. package/dist/index.js.map +1 -1
  28. package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
  29. package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
  30. package/dist/sheets.cjs +102 -16
  31. package/dist/sheets.cjs.map +1 -1
  32. package/dist/sheets.d.cts +49 -63
  33. package/dist/sheets.d.ts +49 -63
  34. package/dist/sheets.js +111 -19
  35. package/dist/sheets.js.map +1 -1
  36. package/package.json +28 -3
  37. package/src/chrome/AutoSumPicker.tsx +176 -0
  38. package/src/chrome/BordersPicker.tsx +171 -0
  39. package/src/chrome/ChromeBottom.tsx +18 -0
  40. package/src/chrome/ChromeTop.tsx +21 -0
  41. package/src/chrome/ColorPicker.tsx +220 -0
  42. package/src/chrome/FindReplace.tsx +370 -0
  43. package/src/chrome/FormulaBar.tsx +378 -0
  44. package/src/chrome/Icon.tsx +43 -0
  45. package/src/chrome/MenuBar.tsx +336 -0
  46. package/src/chrome/NameBox.tsx +347 -0
  47. package/src/chrome/SheetTabs.tsx +346 -0
  48. package/src/chrome/StatusBar.tsx +232 -0
  49. package/src/chrome/Toolbar.tsx +401 -0
  50. package/src/chrome/fonts.ts +42 -0
  51. package/src/chrome/index.ts +24 -0
  52. package/src/collab/attachCollab.ts +151 -0
  53. package/src/collab/bridge-helpers.ts +97 -0
  54. package/src/collab/bridge.ts +885 -0
  55. package/src/collab/bridge.unit.test.ts +160 -0
  56. package/src/collab/index.ts +38 -0
  57. package/src/collab/replay-retry.ts +137 -0
  58. package/src/collab/replay-retry.unit.test.ts +223 -0
  59. package/src/collab/ws-url.ts +20 -0
  60. package/src/collab/ws-url.unit.test.ts +35 -0
  61. package/src/embed/EmbedHostTransport.ts +16 -1
  62. package/src/embed/EmbedTransport.ts +16 -0
  63. package/src/embed/EmbedTransport.unit.test.ts +88 -2
  64. package/src/embed/index.ts +7 -0
  65. package/src/embed/protocol.ts +34 -0
  66. package/src/embed-runtime/index.tsx +20 -0
  67. package/src/sheets/CasualSheets.tsx +204 -33
@@ -0,0 +1,370 @@
1
+ /**
2
+ * FindReplace — a custom find/replace dialog for `<CasualSheets chrome>`.
3
+ *
4
+ * Univer's own find-replace dialog doesn't render in the SDK's headless mount
5
+ * (it needs Univer's UI/overlay layer, which is dormant when header/toolbar/
6
+ * footer are off), so the chrome ships its own — driven purely through the
7
+ * facade: search reads the active sheet's `cellData` from `getSnapshot()`,
8
+ * navigation `activate()`s the matching cell, and replace `setValue()`s it.
9
+ *
10
+ * Opens on Ctrl/Cmd+F (find) or Ctrl/Cmd+H (replace) while the editor has focus;
11
+ * Escape closes. Self-managing — just mount it in the chrome with the live api.
12
+ */
13
+
14
+ import {
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ type CSSProperties,
20
+ type KeyboardEvent,
21
+ } from 'react';
22
+ import type { CasualSheetsAPI } from '../sheets/api';
23
+ import { Icon } from './Icon';
24
+
25
+ interface Match {
26
+ row: number;
27
+ col: number;
28
+ }
29
+
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ type AnySnapshot = any;
32
+
33
+ /** Collect matches on the active sheet, in row-major order. Searches each
34
+ * cell's value (`v`) and formula (`f`) text. */
35
+ function findMatches(api: CasualSheetsAPI, query: string, matchCase: boolean): Match[] {
36
+ if (!query) return [];
37
+ const snap = api.getSnapshot() as AnySnapshot;
38
+ const sheetId =
39
+ api.getSelection()?.sheetId ?? api.univer.getActiveWorkbook()?.getActiveSheet()?.getSheetId();
40
+ const sheet = sheetId ? snap?.sheets?.[sheetId] : undefined;
41
+ const cellData = sheet?.cellData as Record<string, Record<string, AnySnapshot>> | undefined;
42
+ if (!cellData) return [];
43
+ const needle = matchCase ? query : query.toLowerCase();
44
+ const out: Match[] = [];
45
+ for (const rk of Object.keys(cellData).sort((a, b) => Number(a) - Number(b))) {
46
+ const rowObj = cellData[rk];
47
+ if (!rowObj) continue;
48
+ for (const ck of Object.keys(rowObj).sort((a, b) => Number(a) - Number(b))) {
49
+ const cell = rowObj[ck];
50
+ if (!cell) continue;
51
+ const hay = `${cell.v ?? ''}${cell.f ? ` ${cell.f}` : ''}`;
52
+ const cmp = matchCase ? hay : hay.toLowerCase();
53
+ if (cmp.includes(needle)) out.push({ row: Number(rk), col: Number(ck) });
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ const WRAP_STYLE: CSSProperties = {
60
+ position: 'absolute',
61
+ top: 8,
62
+ right: 12,
63
+ zIndex: 1100,
64
+ width: 320,
65
+ padding: 10,
66
+ borderRadius: 8,
67
+ border: '1px solid var(--cs-chrome-border, #e6e9ee)',
68
+ background: 'var(--cs-chrome-input-bg, #fff)',
69
+ boxShadow: '0 8px 28px rgba(0,0,0,0.18)',
70
+ font: 'inherit',
71
+ fontSize: 13,
72
+ color: 'var(--cs-chrome-fg, #201f1e)',
73
+ };
74
+
75
+ const ROW_STYLE: CSSProperties = { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 };
76
+
77
+ const INPUT_STYLE: CSSProperties = {
78
+ flex: '1 1 auto',
79
+ height: 26,
80
+ padding: '0 8px',
81
+ border: '1px solid var(--cs-chrome-border, #cdd3db)',
82
+ borderRadius: 4,
83
+ background: 'var(--cs-chrome-input-bg, #fff)',
84
+ color: 'var(--cs-chrome-fg, #201f1e)',
85
+ font: 'inherit',
86
+ fontSize: 13,
87
+ boxSizing: 'border-box',
88
+ };
89
+
90
+ const BTN_STYLE: CSSProperties = {
91
+ height: 26,
92
+ padding: '0 10px',
93
+ border: '1px solid var(--cs-chrome-border, #cdd3db)',
94
+ borderRadius: 4,
95
+ background: 'var(--cs-chrome-bg, #f3f5f8)',
96
+ color: 'var(--cs-chrome-fg, #201f1e)',
97
+ font: 'inherit',
98
+ fontSize: 12,
99
+ cursor: 'pointer',
100
+ };
101
+
102
+ const ICON_BTN_STYLE: CSSProperties = {
103
+ width: 26,
104
+ height: 26,
105
+ display: 'inline-flex',
106
+ alignItems: 'center',
107
+ justifyContent: 'center',
108
+ border: 'none',
109
+ borderRadius: 4,
110
+ background: 'transparent',
111
+ color: 'var(--cs-chrome-fg, #201f1e)',
112
+ cursor: 'pointer',
113
+ padding: 0,
114
+ };
115
+
116
+ export interface FindReplaceProps {
117
+ /** Live API, or `null` until the editor is ready. */
118
+ api: CasualSheetsAPI | null;
119
+ }
120
+
121
+ export function FindReplace({ api }: FindReplaceProps) {
122
+ const [open, setOpen] = useState(false);
123
+ const [showReplace, setShowReplace] = useState(false);
124
+ const [query, setQuery] = useState('');
125
+ const [replaceText, setReplaceText] = useState('');
126
+ const [matchCase, setMatchCase] = useState(false);
127
+ const [idx, setIdx] = useState(0);
128
+ const inputRef = useRef<HTMLInputElement>(null);
129
+
130
+ // Open on Ctrl/Cmd+F (find) / Ctrl/Cmd+H (replace), close on Escape.
131
+ useEffect(() => {
132
+ const onKey = (e: globalThis.KeyboardEvent) => {
133
+ const mod = e.metaKey || e.ctrlKey;
134
+ if (mod && (e.key === 'f' || e.key === 'F')) {
135
+ e.preventDefault();
136
+ setShowReplace(false);
137
+ setOpen(true);
138
+ requestAnimationFrame(() => inputRef.current?.focus());
139
+ } else if (mod && (e.key === 'h' || e.key === 'H')) {
140
+ e.preventDefault();
141
+ setShowReplace(true);
142
+ setOpen(true);
143
+ requestAnimationFrame(() => inputRef.current?.focus());
144
+ }
145
+ };
146
+ document.addEventListener('keydown', onKey);
147
+ return () => document.removeEventListener('keydown', onKey);
148
+ }, []);
149
+
150
+ const matches = useMemo(
151
+ () => (api && open && query ? findMatches(api, query, matchCase) : []),
152
+ [api, open, query, matchCase],
153
+ );
154
+
155
+ // Keep idx in range as matches change.
156
+ useEffect(() => {
157
+ setIdx((i) => (matches.length === 0 ? 0 : Math.min(i, matches.length - 1)));
158
+ }, [matches.length]);
159
+
160
+ if (!open || !api) return null;
161
+
162
+ const go = (delta: number) => {
163
+ if (matches.length === 0) return;
164
+ const next = (idx + delta + matches.length) % matches.length;
165
+ setIdx(next);
166
+ activate(matches[next]);
167
+ };
168
+
169
+ const activate = (m: Match) => {
170
+ const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ (sheet?.getRange(m.row, m.col) as any)?.activate?.();
173
+ };
174
+
175
+ const replaceOne = () => {
176
+ if (matches.length === 0) return;
177
+ const m = matches[Math.min(idx, matches.length - 1)];
178
+ replaceAt(m);
179
+ // Matches recompute via the query memo on next render; advance stays put.
180
+ };
181
+
182
+ const replaceAt = (m: Match) => {
183
+ const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
184
+ if (!sheet) return;
185
+ const cur = String(sheet.getRange(m.row, m.col).getValue?.() ?? '');
186
+ const next = replaceInString(cur, query, replaceText, matchCase);
187
+ if (next !== cur) sheet.getRange(m.row, m.col).setValue(next);
188
+ };
189
+
190
+ const replaceAll = () => {
191
+ const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
192
+ if (!sheet) return;
193
+ for (const m of matches) {
194
+ const cur = String(sheet.getRange(m.row, m.col).getValue?.() ?? '');
195
+ const next = replaceInString(cur, query, replaceText, matchCase);
196
+ if (next !== cur) sheet.getRange(m.row, m.col).setValue(next);
197
+ }
198
+ };
199
+
200
+ const onInputKey = (e: KeyboardEvent<HTMLInputElement>) => {
201
+ if (e.key === 'Enter') {
202
+ e.preventDefault();
203
+ go(e.shiftKey ? -1 : 1);
204
+ } else if (e.key === 'Escape') {
205
+ e.preventDefault();
206
+ setOpen(false);
207
+ }
208
+ };
209
+
210
+ const count = matches.length;
211
+ const position = count === 0 ? 0 : Math.min(idx, count - 1) + 1;
212
+
213
+ return (
214
+ <div
215
+ style={WRAP_STYLE}
216
+ data-testid="cs-find-replace"
217
+ role="dialog"
218
+ aria-label="Find and replace"
219
+ >
220
+ <div style={ROW_STYLE}>
221
+ <input
222
+ ref={inputRef}
223
+ type="text"
224
+ aria-label="Find"
225
+ placeholder="Find"
226
+ data-testid="cs-find-input"
227
+ style={INPUT_STYLE}
228
+ value={query}
229
+ onChange={(e) => setQuery(e.target.value)}
230
+ onKeyDown={onInputKey}
231
+ />
232
+ <span
233
+ style={{ minWidth: 54, textAlign: 'center', color: 'var(--cs-chrome-muted, #6b7280)' }}
234
+ data-testid="cs-find-count"
235
+ >
236
+ {position}/{count}
237
+ </span>
238
+ <button
239
+ type="button"
240
+ style={ICON_BTN_STYLE}
241
+ aria-label="Previous match"
242
+ data-testid="cs-find-prev"
243
+ onMouseDown={(e) => {
244
+ e.preventDefault();
245
+ go(-1);
246
+ }}
247
+ >
248
+ <Icon name="keyboard_arrow_up" size={18} />
249
+ </button>
250
+ <button
251
+ type="button"
252
+ style={ICON_BTN_STYLE}
253
+ aria-label="Next match"
254
+ data-testid="cs-find-next"
255
+ onMouseDown={(e) => {
256
+ e.preventDefault();
257
+ go(1);
258
+ }}
259
+ >
260
+ <Icon name="keyboard_arrow_down" size={18} />
261
+ </button>
262
+ <button
263
+ type="button"
264
+ style={ICON_BTN_STYLE}
265
+ aria-label="Close"
266
+ data-testid="cs-find-close"
267
+ onMouseDown={(e) => {
268
+ e.preventDefault();
269
+ setOpen(false);
270
+ }}
271
+ >
272
+ <Icon name="close" size={18} />
273
+ </button>
274
+ </div>
275
+
276
+ {showReplace && (
277
+ <div style={ROW_STYLE}>
278
+ <input
279
+ type="text"
280
+ aria-label="Replace with"
281
+ placeholder="Replace with"
282
+ data-testid="cs-replace-input"
283
+ style={INPUT_STYLE}
284
+ value={replaceText}
285
+ onChange={(e) => setReplaceText(e.target.value)}
286
+ />
287
+ <button
288
+ type="button"
289
+ style={BTN_STYLE}
290
+ data-testid="cs-replace-one"
291
+ onMouseDown={(e) => {
292
+ e.preventDefault();
293
+ replaceOne();
294
+ }}
295
+ >
296
+ Replace
297
+ </button>
298
+ <button
299
+ type="button"
300
+ style={BTN_STYLE}
301
+ data-testid="cs-replace-all"
302
+ onMouseDown={(e) => {
303
+ e.preventDefault();
304
+ replaceAll();
305
+ }}
306
+ >
307
+ All
308
+ </button>
309
+ </div>
310
+ )}
311
+
312
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
313
+ <label style={{ display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
314
+ <input
315
+ type="checkbox"
316
+ data-testid="cs-find-match-case"
317
+ checked={matchCase}
318
+ onChange={(e) => setMatchCase(e.target.checked)}
319
+ />
320
+ Match case
321
+ </label>
322
+ {!showReplace && (
323
+ <button
324
+ type="button"
325
+ style={{
326
+ ...BTN_STYLE,
327
+ marginLeft: 'auto',
328
+ border: 'none',
329
+ background: 'transparent',
330
+ color: 'var(--cs-chrome-active-fg, #0e7490)',
331
+ }}
332
+ data-testid="cs-find-toggle-replace"
333
+ onMouseDown={(e) => {
334
+ e.preventDefault();
335
+ setShowReplace(true);
336
+ }}
337
+ >
338
+ Replace…
339
+ </button>
340
+ )}
341
+ </div>
342
+ </div>
343
+ );
344
+ }
345
+
346
+ /** Replace all occurrences of `query` in `text` (case-(in)sensitive). */
347
+ function replaceInString(
348
+ text: string,
349
+ query: string,
350
+ replacement: string,
351
+ matchCase: boolean,
352
+ ): string {
353
+ if (!query) return text;
354
+ if (matchCase) return text.split(query).join(replacement);
355
+ // Case-insensitive: walk and rebuild preserving non-matched casing.
356
+ const lower = text.toLowerCase();
357
+ const needle = query.toLowerCase();
358
+ let out = '';
359
+ let i = 0;
360
+ while (i < text.length) {
361
+ if (lower.startsWith(needle, i)) {
362
+ out += replacement;
363
+ i += needle.length;
364
+ } else {
365
+ out += text[i];
366
+ i += 1;
367
+ }
368
+ }
369
+ return out;
370
+ }