@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,378 @@
1
+ /**
2
+ * FormulaBar — minimal built-in formula bar for `<CasualSheets chrome>`.
3
+ *
4
+ * The formula bar: a NameBox (active-cell ref + go-to navigation) and an editable
5
+ * input that shows the active cell's formula (or value) and commits edits through
6
+ * the facade — `=…` as a formula, a number as a number, else text. Typing a
7
+ * function name after `=` shows an autocomplete dropdown (arrows + Enter/Tab to
8
+ * complete, Escape to dismiss). Self-contained: drives the editor only via
9
+ * `CasualSheetsAPI`.
10
+ */
11
+
12
+ import {
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ type CSSProperties,
18
+ type KeyboardEvent,
19
+ } from 'react';
20
+ import { ICommandService } from '@univerjs/core';
21
+ import type { CasualSheetsAPI } from '../sheets/api';
22
+ import { NameBox } from './NameBox';
23
+
24
+ // Curated common spreadsheet functions for the autocomplete dropdown. (Univer
25
+ // ships ~530; this covers what people actually type without bundling them all.)
26
+ const FUNCTIONS = [
27
+ 'ABS',
28
+ 'AND',
29
+ 'AVERAGE',
30
+ 'AVERAGEIF',
31
+ 'AVERAGEIFS',
32
+ 'CEILING',
33
+ 'CHOOSE',
34
+ 'COLUMN',
35
+ 'COLUMNS',
36
+ 'CONCAT',
37
+ 'CONCATENATE',
38
+ 'COUNT',
39
+ 'COUNTA',
40
+ 'COUNTBLANK',
41
+ 'COUNTIF',
42
+ 'COUNTIFS',
43
+ 'DATE',
44
+ 'DATEDIF',
45
+ 'DAY',
46
+ 'EDATE',
47
+ 'EOMONTH',
48
+ 'EXACT',
49
+ 'FILTER',
50
+ 'FIND',
51
+ 'FLOOR',
52
+ 'HLOOKUP',
53
+ 'HOUR',
54
+ 'IF',
55
+ 'IFERROR',
56
+ 'IFNA',
57
+ 'IFS',
58
+ 'INDEX',
59
+ 'INDIRECT',
60
+ 'INT',
61
+ 'ISBLANK',
62
+ 'ISERROR',
63
+ 'ISNA',
64
+ 'ISNUMBER',
65
+ 'ISTEXT',
66
+ 'LEFT',
67
+ 'LEN',
68
+ 'LOOKUP',
69
+ 'LOWER',
70
+ 'MATCH',
71
+ 'MAX',
72
+ 'MAXIFS',
73
+ 'MEDIAN',
74
+ 'MID',
75
+ 'MIN',
76
+ 'MINIFS',
77
+ 'MINUTE',
78
+ 'MOD',
79
+ 'MONTH',
80
+ 'NETWORKDAYS',
81
+ 'NOT',
82
+ 'NOW',
83
+ 'OFFSET',
84
+ 'OR',
85
+ 'POWER',
86
+ 'PRODUCT',
87
+ 'PROPER',
88
+ 'RAND',
89
+ 'RANDBETWEEN',
90
+ 'RANK',
91
+ 'REPLACE',
92
+ 'REPT',
93
+ 'RIGHT',
94
+ 'ROUND',
95
+ 'ROUNDDOWN',
96
+ 'ROUNDUP',
97
+ 'ROW',
98
+ 'ROWS',
99
+ 'SEARCH',
100
+ 'SECOND',
101
+ 'SORT',
102
+ 'SQRT',
103
+ 'SUBSTITUTE',
104
+ 'SUM',
105
+ 'SUMIF',
106
+ 'SUMIFS',
107
+ 'SUMPRODUCT',
108
+ 'SWITCH',
109
+ 'TEXT',
110
+ 'TEXTJOIN',
111
+ 'TODAY',
112
+ 'TRANSPOSE',
113
+ 'TRIM',
114
+ 'UNIQUE',
115
+ 'UPPER',
116
+ 'VALUE',
117
+ 'VLOOKUP',
118
+ 'WEEKDAY',
119
+ 'WORKDAY',
120
+ 'XLOOKUP',
121
+ 'XMATCH',
122
+ 'YEAR',
123
+ ];
124
+
125
+ const TOKEN_RE = /([A-Za-z][A-Za-z0-9._]*)$/;
126
+
127
+ /** Function suggestions for the trailing identifier of a formula draft. */
128
+ function suggestFor(draft: string | null): string[] {
129
+ if (!draft || !draft.startsWith('=')) return [];
130
+ const m = TOKEN_RE.exec(draft);
131
+ if (!m) return [];
132
+ const prefix = m[1].toUpperCase();
133
+ return FUNCTIONS.filter((f) => f.startsWith(prefix) && f !== prefix).slice(0, 8);
134
+ }
135
+
136
+ /** A1 column letters from a 0-based column index (0→A, 26→AA). */
137
+ function colToLetters(col: number): string {
138
+ let s = '';
139
+ let n = col;
140
+ do {
141
+ s = String.fromCharCode(65 + (n % 26)) + s;
142
+ n = Math.floor(n / 26) - 1;
143
+ } while (n >= 0);
144
+ return s;
145
+ }
146
+
147
+ interface ActiveCell {
148
+ a1: string;
149
+ /** Formula text (`=…`) if the cell has one, else the raw value as a string. */
150
+ text: string;
151
+ }
152
+
153
+ function readActiveCell(api: CasualSheetsAPI): ActiveCell | null {
154
+ const sel = api.getSelection();
155
+ const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
156
+ if (!sel || !sheet) return null;
157
+ const { startRow, startColumn } = sel.range;
158
+ const range = sheet.getRange(startRow, startColumn);
159
+ const formula = range.getFormula?.() || '';
160
+ const raw = range.getValue?.();
161
+ return {
162
+ a1: colToLetters(startColumn) + (startRow + 1),
163
+ text: formula || (raw == null ? '' : String(raw)),
164
+ };
165
+ }
166
+
167
+ const BAR_STYLE: CSSProperties = {
168
+ display: 'flex',
169
+ alignItems: 'center',
170
+ gap: 6,
171
+ padding: '3px 6px',
172
+ borderBottom: '1px solid var(--cs-chrome-border, rgba(0,0,0,0.12))',
173
+ background: 'var(--cs-chrome-bg, #f8f9fa)',
174
+ flex: '0 0 auto',
175
+ font: 'inherit',
176
+ fontSize: 13,
177
+ };
178
+
179
+ const FX_STYLE: CSSProperties = {
180
+ flex: '0 0 auto',
181
+ fontStyle: 'italic',
182
+ color: 'var(--cs-chrome-muted, #6b7280)',
183
+ };
184
+
185
+ const INPUT_STYLE: CSSProperties = {
186
+ width: '100%',
187
+ height: 24,
188
+ padding: '0 8px',
189
+ border: '1px solid var(--cs-chrome-border, rgba(0,0,0,0.18))',
190
+ borderRadius: 4,
191
+ background: 'var(--cs-chrome-input-bg, #fff)',
192
+ color: 'var(--cs-chrome-fg, #1f2329)',
193
+ font: 'inherit',
194
+ fontSize: 13,
195
+ boxSizing: 'border-box',
196
+ };
197
+
198
+ const INPUT_WRAP_STYLE: CSSProperties = { position: 'relative', flex: '1 1 auto', minWidth: 0 };
199
+
200
+ const AC_STYLE: CSSProperties = {
201
+ position: 'absolute',
202
+ top: '100%',
203
+ left: 0,
204
+ marginTop: 2,
205
+ minWidth: 200,
206
+ maxHeight: 240,
207
+ overflowY: 'auto',
208
+ zIndex: 1000,
209
+ padding: 4,
210
+ border: '1px solid var(--cs-chrome-border, #e6e9ee)',
211
+ borderRadius: 8,
212
+ background: 'var(--cs-chrome-input-bg, #fff)',
213
+ boxShadow: '0 6px 20px rgba(0,0,0,0.16)',
214
+ };
215
+
216
+ const AC_ITEM_STYLE: CSSProperties = {
217
+ display: 'block',
218
+ width: '100%',
219
+ textAlign: 'left',
220
+ height: 26,
221
+ padding: '0 8px',
222
+ border: 'none',
223
+ borderRadius: 6,
224
+ background: 'transparent',
225
+ color: 'var(--cs-chrome-fg, #201f1e)',
226
+ font: 'inherit',
227
+ fontSize: 13,
228
+ cursor: 'pointer',
229
+ };
230
+
231
+ export interface FormulaBarProps {
232
+ /** Live API, or `null` until the editor is ready. */
233
+ api: CasualSheetsAPI | null;
234
+ }
235
+
236
+ export function FormulaBar({ api }: FormulaBarProps) {
237
+ const [cell, setCell] = useState<ActiveCell | null>(null);
238
+ // null = mirror the active cell; a string = the user is editing.
239
+ const [draft, setDraft] = useState<string | null>(null);
240
+ const draftRef = useRef<string | null>(null);
241
+ draftRef.current = draft;
242
+ // Autocomplete: active suggestion index + a dismissed flag (Escape closes the
243
+ // dropdown without cancelling the edit).
244
+ const [acIdx, setAcIdx] = useState(0);
245
+ const [acDismissed, setAcDismissed] = useState(false);
246
+
247
+ useEffect(() => {
248
+ if (!api) return;
249
+ const refresh = () => {
250
+ // Don't clobber an in-progress edit on unrelated command activity.
251
+ if (draftRef.current !== null) return;
252
+ setCell(readActiveCell(api));
253
+ };
254
+ refresh();
255
+ const injector = (api.univer as unknown as { _injector?: { get(t: unknown): unknown } })
256
+ ._injector;
257
+ const cmd = injector?.get(ICommandService) as
258
+ | { onCommandExecuted: (cb: () => void) => { dispose: () => void } }
259
+ | undefined;
260
+ const sub = cmd?.onCommandExecuted(() => refresh());
261
+ return () => sub?.dispose();
262
+ }, [api]);
263
+
264
+ const suggestions = useMemo(() => suggestFor(draft), [draft]);
265
+ const acOpen = !acDismissed && suggestions.length > 0;
266
+
267
+ const commit = (text: string) => {
268
+ setDraft(null);
269
+ setAcDismissed(false);
270
+ if (!api) return;
271
+ const sel = api.getSelection();
272
+ const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
273
+ if (!sel || !sheet) return;
274
+ const range = sheet.getRange(sel.range.startRow, sel.range.startColumn);
275
+ const t = text.trim();
276
+ if (t.startsWith('=')) range.setValue({ f: t });
277
+ else if (t !== '' && !Number.isNaN(Number(t))) range.setValue(Number(t));
278
+ else range.setValue(t);
279
+ };
280
+
281
+ const onType = (v: string) => {
282
+ setDraft(v);
283
+ setAcIdx(0);
284
+ setAcDismissed(false);
285
+ };
286
+
287
+ // Replace the trailing identifier with the chosen function + '('.
288
+ const complete = (fn: string) => {
289
+ const base = draft ?? '';
290
+ setDraft(base.replace(TOKEN_RE, `${fn}(`));
291
+ setAcIdx(0);
292
+ };
293
+
294
+ const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
295
+ if (acOpen) {
296
+ if (e.key === 'ArrowDown') {
297
+ e.preventDefault();
298
+ setAcIdx((i) => Math.min(i + 1, suggestions.length - 1));
299
+ return;
300
+ }
301
+ if (e.key === 'ArrowUp') {
302
+ e.preventDefault();
303
+ setAcIdx((i) => Math.max(i - 1, 0));
304
+ return;
305
+ }
306
+ if (e.key === 'Enter' || e.key === 'Tab') {
307
+ e.preventDefault();
308
+ complete(suggestions[acIdx] ?? suggestions[0]);
309
+ return;
310
+ }
311
+ if (e.key === 'Escape') {
312
+ e.preventDefault();
313
+ setAcDismissed(true);
314
+ return;
315
+ }
316
+ }
317
+ if (e.key === 'Enter') {
318
+ e.preventDefault();
319
+ commit((e.target as HTMLInputElement).value);
320
+ } else if (e.key === 'Escape') {
321
+ e.preventDefault();
322
+ setDraft(null);
323
+ (e.target as HTMLInputElement).blur();
324
+ }
325
+ };
326
+
327
+ const shown = draft ?? cell?.text ?? '';
328
+
329
+ return (
330
+ <div style={BAR_STYLE} data-testid="casual-sheets-formula-bar">
331
+ <NameBox api={api} />
332
+ <span style={FX_STYLE} aria-hidden>
333
+ fx
334
+ </span>
335
+ <div style={INPUT_WRAP_STYLE}>
336
+ <input
337
+ type="text"
338
+ aria-label="Formula bar"
339
+ data-testid="casual-sheets-formula-input"
340
+ style={INPUT_STYLE}
341
+ value={shown}
342
+ onChange={(e) => onType(e.target.value)}
343
+ onKeyDown={onKeyDown}
344
+ onBlur={(e) => {
345
+ if (draftRef.current !== null) commit(e.target.value);
346
+ }}
347
+ disabled={!api}
348
+ />
349
+ {acOpen && (
350
+ <div style={AC_STYLE} data-testid="cs-formula-suggestions" role="listbox">
351
+ {suggestions.map((fn, i) => (
352
+ <button
353
+ key={fn}
354
+ type="button"
355
+ role="option"
356
+ aria-selected={i === acIdx}
357
+ data-testid={`cs-formula-suggestion-${fn}`}
358
+ style={{
359
+ ...AC_ITEM_STYLE,
360
+ background: i === acIdx ? 'var(--cs-chrome-active, #e6f3f7)' : 'transparent',
361
+ color: i === acIdx ? 'var(--cs-chrome-active-fg, #0e7490)' : AC_ITEM_STYLE.color,
362
+ }}
363
+ // mousedown+preventDefault so the input keeps focus (no blur-commit).
364
+ onMouseDown={(e) => {
365
+ e.preventDefault();
366
+ complete(fn);
367
+ }}
368
+ onMouseEnter={() => setAcIdx(i)}
369
+ >
370
+ {fn}
371
+ </button>
372
+ ))}
373
+ </div>
374
+ )}
375
+ </div>
376
+ </div>
377
+ );
378
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Icon — a Material Symbols Outlined glyph for the chrome.
3
+ *
4
+ * Names are Material Symbols ids (https://fonts.google.com/icons), e.g.
5
+ * `format_bold`. Requires the Material Symbols font (loaded via
6
+ * `ensureChromeFonts`); if it isn't present the ligature text shows instead,
7
+ * which is harmless. Icon-only buttons must carry their own `aria-label`.
8
+ */
9
+
10
+ import type { CSSProperties } from 'react';
11
+
12
+ export interface IconProps {
13
+ name: string;
14
+ size?: number;
15
+ style?: CSSProperties;
16
+ }
17
+
18
+ export function Icon({ name, size = 20, style }: IconProps) {
19
+ return (
20
+ <span
21
+ className="material-symbols-outlined"
22
+ aria-hidden
23
+ style={{
24
+ fontFamily: "'Material Symbols Outlined'",
25
+ fontWeight: 'normal',
26
+ fontStyle: 'normal',
27
+ fontSize: size,
28
+ lineHeight: 1,
29
+ letterSpacing: 'normal',
30
+ textTransform: 'none',
31
+ display: 'inline-block',
32
+ whiteSpace: 'nowrap',
33
+ wordWrap: 'normal',
34
+ direction: 'ltr',
35
+ // Hint the variable-font axes so weight/optical-size are consistent.
36
+ fontVariationSettings: "'opsz' 20, 'wght' 400, 'FILL' 0, 'GRAD' 0",
37
+ ...style,
38
+ }}
39
+ >
40
+ {name}
41
+ </span>
42
+ );
43
+ }