@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.
- package/dist/api-BI2VLYQ6.d.cts +62 -0
- package/dist/api-BI2VLYQ6.d.ts +62 -0
- package/dist/chrome.cjs +2569 -0
- package/dist/chrome.cjs.map +1 -0
- package/dist/chrome.d.cts +96 -0
- package/dist/chrome.d.ts +96 -0
- package/dist/chrome.js +2556 -0
- package/dist/chrome.js.map +1 -0
- package/dist/collab.cjs +770 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.cts +248 -0
- package/dist/collab.d.ts +248 -0
- package/dist/collab.js +737 -0
- package/dist/collab.js.map +1 -0
- package/dist/embed/embed-runtime.js +128 -128
- package/dist/embed.cjs +166 -0
- package/dist/embed.cjs.map +1 -1
- package/dist/embed.d.cts +78 -3
- package/dist/embed.d.ts +78 -3
- package/dist/embed.js +166 -0
- package/dist/embed.js.map +1 -1
- package/dist/index.cjs +262 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +271 -168
- package/dist/index.js.map +1 -1
- package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
- package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
- package/dist/sheets.cjs +102 -16
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.d.cts +49 -63
- package/dist/sheets.d.ts +49 -63
- package/dist/sheets.js +111 -19
- package/dist/sheets.js.map +1 -1
- package/package.json +28 -3
- package/src/chrome/AutoSumPicker.tsx +176 -0
- package/src/chrome/BordersPicker.tsx +171 -0
- package/src/chrome/ChromeBottom.tsx +18 -0
- package/src/chrome/ChromeTop.tsx +21 -0
- package/src/chrome/ColorPicker.tsx +220 -0
- package/src/chrome/FindReplace.tsx +370 -0
- package/src/chrome/FormulaBar.tsx +378 -0
- package/src/chrome/Icon.tsx +43 -0
- package/src/chrome/MenuBar.tsx +336 -0
- package/src/chrome/NameBox.tsx +347 -0
- package/src/chrome/SheetTabs.tsx +346 -0
- package/src/chrome/StatusBar.tsx +232 -0
- package/src/chrome/Toolbar.tsx +401 -0
- package/src/chrome/fonts.ts +42 -0
- package/src/chrome/index.ts +24 -0
- package/src/collab/attachCollab.ts +151 -0
- package/src/collab/bridge-helpers.ts +97 -0
- package/src/collab/bridge.ts +885 -0
- package/src/collab/bridge.unit.test.ts +160 -0
- package/src/collab/index.ts +38 -0
- package/src/collab/replay-retry.ts +137 -0
- package/src/collab/replay-retry.unit.test.ts +223 -0
- package/src/collab/ws-url.ts +20 -0
- package/src/collab/ws-url.unit.test.ts +35 -0
- package/src/embed/EmbedHostTransport.ts +16 -1
- package/src/embed/EmbedTransport.ts +16 -0
- package/src/embed/EmbedTransport.unit.test.ts +88 -2
- package/src/embed/index.ts +7 -0
- package/src/embed/protocol.ts +34 -0
- package/src/embed-runtime/index.tsx +20 -0
- package/src/sheets/CasualSheets.tsx +204 -33
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolbar — the rich built-in formatting toolbar for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* Drives the editor purely through `CasualSheetsAPI.executeCommand`, with
|
|
5
|
+
* Material Symbols icons + design-system tokens. No app context, no title/logo
|
|
6
|
+
* row (the host frames the editor with its own bar). Commands grouped with
|
|
7
|
+
* dividers, Office-style.
|
|
8
|
+
*
|
|
9
|
+
* Covered: font family/size · undo/redo · bold/italic/underline/strikethrough ·
|
|
10
|
+
* horizontal align · merge/unmerge · number formats (currency/percent/decimals).
|
|
11
|
+
* Reflects the active cell — toggles light up, dropdowns show the current font —
|
|
12
|
+
* via a command-execution subscription. Follow-up: text/fill colour pickers
|
|
13
|
+
* (need a swatch popover).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useState, type CSSProperties } from 'react';
|
|
17
|
+
import { ICommandService } from '@univerjs/core';
|
|
18
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
19
|
+
import { Icon } from './Icon';
|
|
20
|
+
import { ensureChromeFonts } from './fonts';
|
|
21
|
+
import { ColorPicker } from './ColorPicker';
|
|
22
|
+
import { BordersPicker } from './BordersPicker';
|
|
23
|
+
import { AutoSumPicker } from './AutoSumPicker';
|
|
24
|
+
|
|
25
|
+
interface ActiveStyle {
|
|
26
|
+
bold: boolean;
|
|
27
|
+
italic: boolean;
|
|
28
|
+
underline: boolean;
|
|
29
|
+
strike: boolean;
|
|
30
|
+
ht: number; // HorizontalAlign of the active cell (0 = unset)
|
|
31
|
+
vt: number; // VerticalAlign of the active cell (0 = unset)
|
|
32
|
+
tb: number; // WrapStrategy of the active cell (0 = unset, 3 = wrap)
|
|
33
|
+
ff: string;
|
|
34
|
+
fs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NO_STYLE: ActiveStyle = {
|
|
38
|
+
bold: false,
|
|
39
|
+
italic: false,
|
|
40
|
+
underline: false,
|
|
41
|
+
strike: false,
|
|
42
|
+
ht: 0,
|
|
43
|
+
vt: 0,
|
|
44
|
+
tb: 0,
|
|
45
|
+
ff: '',
|
|
46
|
+
fs: 0,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readActiveStyle(api: CasualSheetsAPI): ActiveStyle {
|
|
50
|
+
const sel = api.getSelection();
|
|
51
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
52
|
+
if (!sel || !sheet) return NO_STYLE;
|
|
53
|
+
const range = sheet.getRange(sel.range.startRow, sel.range.startColumn);
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
const s = (range.getCellStyleData?.() ?? null) as any;
|
|
56
|
+
return {
|
|
57
|
+
bold: s?.bl === 1,
|
|
58
|
+
italic: s?.it === 1,
|
|
59
|
+
underline: s?.ul?.s === 1,
|
|
60
|
+
strike: s?.st?.s === 1,
|
|
61
|
+
ht: typeof s?.ht === 'number' ? s.ht : 0,
|
|
62
|
+
vt: typeof s?.vt === 'number' ? s.vt : 0,
|
|
63
|
+
tb: typeof s?.tb === 'number' ? s.tb : 0,
|
|
64
|
+
ff: typeof s?.ff === 'string' ? s.ff : '',
|
|
65
|
+
fs: typeof s?.fs === 'number' ? s.fs : 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const FONT_FAMILIES = ['Arial', 'Calibri', 'Times New Roman', 'Courier New', 'Georgia', 'Verdana'];
|
|
70
|
+
const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 36, 48, 72];
|
|
71
|
+
|
|
72
|
+
interface ToolbarAction {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
command: string;
|
|
76
|
+
icon: string;
|
|
77
|
+
params?: object;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// HorizontalAlign enum (@univerjs/core): LEFT=1, CENTER=2, RIGHT=3.
|
|
81
|
+
// VerticalAlign enum (@univerjs/core): TOP=1, MIDDLE=2, BOTTOM=3.
|
|
82
|
+
// WrapStrategy enum (@univerjs/core): OVERFLOW=1, CLIP=2, WRAP=3.
|
|
83
|
+
const GROUPS: ToolbarAction[][] = [
|
|
84
|
+
[
|
|
85
|
+
{ id: 'undo', label: 'Undo', command: 'univer.command.undo', icon: 'undo' },
|
|
86
|
+
{ id: 'redo', label: 'Redo', command: 'univer.command.redo', icon: 'redo' },
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
{ id: 'bold', label: 'Bold', command: 'sheet.command.set-range-bold', icon: 'format_bold' },
|
|
90
|
+
{
|
|
91
|
+
id: 'italic',
|
|
92
|
+
label: 'Italic',
|
|
93
|
+
command: 'sheet.command.set-range-italic',
|
|
94
|
+
icon: 'format_italic',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'underline',
|
|
98
|
+
label: 'Underline',
|
|
99
|
+
command: 'sheet.command.set-range-underline',
|
|
100
|
+
icon: 'format_underlined',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'strikethrough',
|
|
104
|
+
label: 'Strikethrough',
|
|
105
|
+
command: 'sheet.command.set-range-stroke',
|
|
106
|
+
icon: 'format_strikethrough',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
id: 'align-left',
|
|
112
|
+
label: 'Align left',
|
|
113
|
+
command: 'sheet.command.set-horizontal-text-align',
|
|
114
|
+
icon: 'format_align_left',
|
|
115
|
+
params: { value: 1 },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'align-center',
|
|
119
|
+
label: 'Align center',
|
|
120
|
+
command: 'sheet.command.set-horizontal-text-align',
|
|
121
|
+
icon: 'format_align_center',
|
|
122
|
+
params: { value: 2 },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'align-right',
|
|
126
|
+
label: 'Align right',
|
|
127
|
+
command: 'sheet.command.set-horizontal-text-align',
|
|
128
|
+
icon: 'format_align_right',
|
|
129
|
+
params: { value: 3 },
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
{
|
|
134
|
+
id: 'align-top',
|
|
135
|
+
label: 'Align top',
|
|
136
|
+
command: 'sheet.command.set-vertical-text-align',
|
|
137
|
+
icon: 'vertical_align_top',
|
|
138
|
+
params: { value: 1 },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'align-middle',
|
|
142
|
+
label: 'Align middle',
|
|
143
|
+
command: 'sheet.command.set-vertical-text-align',
|
|
144
|
+
icon: 'vertical_align_center',
|
|
145
|
+
params: { value: 2 },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'align-bottom',
|
|
149
|
+
label: 'Align bottom',
|
|
150
|
+
command: 'sheet.command.set-vertical-text-align',
|
|
151
|
+
icon: 'vertical_align_bottom',
|
|
152
|
+
params: { value: 3 },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'wrap-text',
|
|
156
|
+
label: 'Wrap text',
|
|
157
|
+
command: 'sheet.command.set-text-wrap',
|
|
158
|
+
icon: 'wrap_text',
|
|
159
|
+
params: { value: 3 },
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
id: 'merge',
|
|
165
|
+
label: 'Merge cells',
|
|
166
|
+
command: 'sheet.command.add-worksheet-merge-all',
|
|
167
|
+
icon: 'cell_merge',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'unmerge',
|
|
171
|
+
label: 'Unmerge cells',
|
|
172
|
+
command: 'sheet.command.remove-worksheet-merge',
|
|
173
|
+
icon: 'splitscreen_vertical_add',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
{
|
|
178
|
+
id: 'currency',
|
|
179
|
+
label: 'Currency format',
|
|
180
|
+
command: 'sheet.command.numfmt.set.currency',
|
|
181
|
+
icon: 'attach_money',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'percent',
|
|
185
|
+
label: 'Percent format',
|
|
186
|
+
command: 'sheet.command.numfmt.set.percent',
|
|
187
|
+
icon: 'percent',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'decimal-increase',
|
|
191
|
+
label: 'Increase decimals',
|
|
192
|
+
command: 'sheet.command.numfmt.add.decimal.command',
|
|
193
|
+
icon: 'decimal_increase',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'decimal-decrease',
|
|
197
|
+
label: 'Decrease decimals',
|
|
198
|
+
command: 'sheet.command.numfmt.subtract.decimal.command',
|
|
199
|
+
icon: 'decimal_decrease',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
{
|
|
204
|
+
id: 'clear-format',
|
|
205
|
+
label: 'Clear formatting',
|
|
206
|
+
command: 'sheet.command.clear-selection-format',
|
|
207
|
+
icon: 'format_clear',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const BAR_STYLE: CSSProperties = {
|
|
213
|
+
display: 'flex',
|
|
214
|
+
alignItems: 'center',
|
|
215
|
+
gap: 2,
|
|
216
|
+
padding: '4px 8px',
|
|
217
|
+
borderBottom: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
218
|
+
background: 'var(--cs-chrome-bg, #eef1f5)',
|
|
219
|
+
flex: '0 0 auto',
|
|
220
|
+
userSelect: 'none',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const BTN_STYLE: CSSProperties = {
|
|
224
|
+
width: 30,
|
|
225
|
+
height: 30,
|
|
226
|
+
display: 'inline-flex',
|
|
227
|
+
alignItems: 'center',
|
|
228
|
+
justifyContent: 'center',
|
|
229
|
+
border: 'none',
|
|
230
|
+
borderRadius: 6,
|
|
231
|
+
background: 'transparent',
|
|
232
|
+
cursor: 'pointer',
|
|
233
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
234
|
+
padding: 0,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const DIVIDER_STYLE: CSSProperties = {
|
|
238
|
+
width: 1,
|
|
239
|
+
height: 18,
|
|
240
|
+
margin: '0 4px',
|
|
241
|
+
background: 'var(--cs-chrome-border, #cdd3db)',
|
|
242
|
+
flex: '0 0 auto',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const SELECT_STYLE: CSSProperties = {
|
|
246
|
+
height: 26,
|
|
247
|
+
border: '1px solid var(--cs-chrome-border, #cdd3db)',
|
|
248
|
+
borderRadius: 6,
|
|
249
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
250
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
251
|
+
font: 'inherit',
|
|
252
|
+
fontSize: 13,
|
|
253
|
+
padding: '0 4px',
|
|
254
|
+
cursor: 'pointer',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export interface ToolbarProps {
|
|
258
|
+
/** Live API, or `null` until the editor is ready. */
|
|
259
|
+
api: CasualSheetsAPI | null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Which actions reflect an active state, keyed off the active cell's style.
|
|
263
|
+
function isActive(id: string, s: ActiveStyle): boolean {
|
|
264
|
+
switch (id) {
|
|
265
|
+
case 'bold':
|
|
266
|
+
return s.bold;
|
|
267
|
+
case 'italic':
|
|
268
|
+
return s.italic;
|
|
269
|
+
case 'underline':
|
|
270
|
+
return s.underline;
|
|
271
|
+
case 'strikethrough':
|
|
272
|
+
return s.strike;
|
|
273
|
+
case 'align-left':
|
|
274
|
+
return s.ht === 1;
|
|
275
|
+
case 'align-center':
|
|
276
|
+
return s.ht === 2;
|
|
277
|
+
case 'align-right':
|
|
278
|
+
return s.ht === 3;
|
|
279
|
+
case 'align-top':
|
|
280
|
+
return s.vt === 1;
|
|
281
|
+
case 'align-middle':
|
|
282
|
+
return s.vt === 2;
|
|
283
|
+
case 'align-bottom':
|
|
284
|
+
return s.vt === 3;
|
|
285
|
+
case 'wrap-text':
|
|
286
|
+
return s.tb === 3;
|
|
287
|
+
default:
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function Toolbar({ api }: ToolbarProps) {
|
|
293
|
+
const [active, setActive] = useState<ActiveStyle>(NO_STYLE);
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
ensureChromeFonts();
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
// Reflect the active cell: subscribe to command activity (covers selection
|
|
300
|
+
// moves + style mutations) and re-read the style.
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (!api) return;
|
|
303
|
+
const refresh = () => setActive(readActiveStyle(api));
|
|
304
|
+
refresh();
|
|
305
|
+
const injector = (api.univer as unknown as { _injector?: { get(t: unknown): unknown } })
|
|
306
|
+
._injector;
|
|
307
|
+
const cmd = injector?.get(ICommandService) as
|
|
308
|
+
| { onCommandExecuted: (cb: () => void) => { dispose: () => void } }
|
|
309
|
+
| undefined;
|
|
310
|
+
const sub = cmd?.onCommandExecuted(() => refresh());
|
|
311
|
+
return () => sub?.dispose();
|
|
312
|
+
}, [api]);
|
|
313
|
+
|
|
314
|
+
const dispatch = (command: string, params?: object) => void api?.executeCommand(command, params);
|
|
315
|
+
|
|
316
|
+
// Reflect current font in the dropdowns; surface a non-listed value too.
|
|
317
|
+
const familyValue = active.ff || 'Arial';
|
|
318
|
+
const familyOptions =
|
|
319
|
+
active.ff && !FONT_FAMILIES.includes(active.ff) ? [active.ff, ...FONT_FAMILIES] : FONT_FAMILIES;
|
|
320
|
+
const sizeValue = active.fs || 11;
|
|
321
|
+
const sizeOptions =
|
|
322
|
+
active.fs && !FONT_SIZES.includes(active.fs) ? [active.fs, ...FONT_SIZES] : FONT_SIZES;
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div style={BAR_STYLE} data-testid="casual-sheets-toolbar" role="toolbar" aria-label="Editor">
|
|
326
|
+
<select
|
|
327
|
+
aria-label="Font family"
|
|
328
|
+
data-testid="cs-font-family"
|
|
329
|
+
style={{ ...SELECT_STYLE, width: 116 }}
|
|
330
|
+
value={familyValue}
|
|
331
|
+
onChange={(e) => dispatch('sheet.command.set-range-font-family', { value: e.target.value })}
|
|
332
|
+
>
|
|
333
|
+
{familyOptions.map((f) => (
|
|
334
|
+
<option key={f} value={f}>
|
|
335
|
+
{f}
|
|
336
|
+
</option>
|
|
337
|
+
))}
|
|
338
|
+
</select>
|
|
339
|
+
<select
|
|
340
|
+
aria-label="Font size"
|
|
341
|
+
data-testid="cs-font-size"
|
|
342
|
+
style={{ ...SELECT_STYLE, width: 56, marginLeft: 4 }}
|
|
343
|
+
value={sizeValue}
|
|
344
|
+
onChange={(e) =>
|
|
345
|
+
dispatch('sheet.command.set-range-fontsize', { value: Number(e.target.value) })
|
|
346
|
+
}
|
|
347
|
+
>
|
|
348
|
+
{sizeOptions.map((s) => (
|
|
349
|
+
<option key={s} value={s}>
|
|
350
|
+
{s}
|
|
351
|
+
</option>
|
|
352
|
+
))}
|
|
353
|
+
</select>
|
|
354
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
355
|
+
{GROUPS.map((group, gi) => (
|
|
356
|
+
<span key={gi} style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
357
|
+
{gi > 0 && <span style={DIVIDER_STYLE} aria-hidden />}
|
|
358
|
+
{group.map((a) => {
|
|
359
|
+
const on = isActive(a.id, active);
|
|
360
|
+
const baseBg = on ? 'var(--cs-chrome-active, #e6f3f7)' : 'transparent';
|
|
361
|
+
return (
|
|
362
|
+
<button
|
|
363
|
+
key={a.id}
|
|
364
|
+
type="button"
|
|
365
|
+
title={a.label}
|
|
366
|
+
aria-label={a.label}
|
|
367
|
+
aria-pressed={on}
|
|
368
|
+
data-action={a.id}
|
|
369
|
+
data-active={on ? 'true' : undefined}
|
|
370
|
+
style={{
|
|
371
|
+
...BTN_STYLE,
|
|
372
|
+
background: baseBg,
|
|
373
|
+
color: on ? 'var(--cs-chrome-active-fg, #0e7490)' : BTN_STYLE.color,
|
|
374
|
+
}}
|
|
375
|
+
// mousedown (not click) so the grid's selection isn't lost first.
|
|
376
|
+
onMouseDown={(e) => {
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
dispatch(a.command, a.params);
|
|
379
|
+
}}
|
|
380
|
+
onMouseEnter={(e) => {
|
|
381
|
+
if (!on)
|
|
382
|
+
e.currentTarget.style.background = 'var(--cs-chrome-hover, rgba(0,0,0,0.06))';
|
|
383
|
+
}}
|
|
384
|
+
onMouseLeave={(e) => {
|
|
385
|
+
e.currentTarget.style.background = baseBg;
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<Icon name={a.icon} size={20} />
|
|
389
|
+
</button>
|
|
390
|
+
);
|
|
391
|
+
})}
|
|
392
|
+
</span>
|
|
393
|
+
))}
|
|
394
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
395
|
+
<ColorPicker api={api} />
|
|
396
|
+
<BordersPicker api={api} />
|
|
397
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
398
|
+
<AutoSumPicker api={api} />
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures the chrome's webfonts (Inter for UI, Material Symbols Outlined for
|
|
3
|
+
* icons) are loaded. The design system standardises on these; the rich
|
|
4
|
+
* `chrome="full"` toolbar/menus render Material Symbols ligatures.
|
|
5
|
+
*
|
|
6
|
+
* Idempotent + safe to call repeatedly: it no-ops if the links already exist
|
|
7
|
+
* (e.g. a host that already loads them, like our own app). Injected lazily only
|
|
8
|
+
* when chrome is actually shown, so bare-grid SDK consumers pull nothing.
|
|
9
|
+
*
|
|
10
|
+
* Hosts with a strict CSP (no fonts.googleapis.com) can pre-load the fonts
|
|
11
|
+
* themselves or self-host them; this is best-effort, not required for the editor
|
|
12
|
+
* to function (icons fall back to the ligature text).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const LINKS: Array<{ id: string; href: string }> = [
|
|
16
|
+
{
|
|
17
|
+
id: 'cs-font-inter',
|
|
18
|
+
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'cs-font-material-symbols',
|
|
22
|
+
href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,300..700,0..1,-50..200&display=block',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
let done = false;
|
|
27
|
+
|
|
28
|
+
export function ensureChromeFonts(): void {
|
|
29
|
+
if (done || typeof document === 'undefined') return;
|
|
30
|
+
done = true;
|
|
31
|
+
const head = document.head;
|
|
32
|
+
for (const { id, href } of LINKS) {
|
|
33
|
+
if (document.getElementById(id)) continue;
|
|
34
|
+
// Skip if the host already loaded the same family by any <link>.
|
|
35
|
+
if (document.querySelector(`link[href*="${href.split('?')[0]}"]`)) continue;
|
|
36
|
+
const link = document.createElement('link');
|
|
37
|
+
link.id = id;
|
|
38
|
+
link.rel = 'stylesheet';
|
|
39
|
+
link.href = href;
|
|
40
|
+
head.appendChild(link);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome — the Office shell as slot components for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* First slice (SDK_MIGRATION_PIPELINE Phase 1 step 2): a minimal built-in
|
|
5
|
+
* `Toolbar`. The rich shell (FormulaBar, MenuBar, TitleBar, StatusBar) lifts
|
|
6
|
+
* from `apps/web/src/shell/` here in later slices, behind `chrome="full"`.
|
|
7
|
+
*/
|
|
8
|
+
export { Toolbar, type ToolbarProps } from './Toolbar';
|
|
9
|
+
export { FormulaBar, type FormulaBarProps } from './FormulaBar';
|
|
10
|
+
export { StatusBar, type StatusBarProps } from './StatusBar';
|
|
11
|
+
export { Icon, type IconProps } from './Icon';
|
|
12
|
+
export { ensureChromeFonts } from './fonts';
|
|
13
|
+
export { ColorPicker, type ColorPickerProps } from './ColorPicker';
|
|
14
|
+
export { BordersPicker, type BordersPickerProps } from './BordersPicker';
|
|
15
|
+
export { AutoSumPicker, type AutoSumPickerProps } from './AutoSumPicker';
|
|
16
|
+
export { MenuBar, type MenuBarProps } from './MenuBar';
|
|
17
|
+
export { NameBox, type NameBoxProps } from './NameBox';
|
|
18
|
+
export { SheetTabs, type SheetTabsProps } from './SheetTabs';
|
|
19
|
+
export { FindReplace, type FindReplaceProps } from './FindReplace';
|
|
20
|
+
// Lazy-loaded shells: chrome above (ChromeTop) and below (ChromeBottom) the grid,
|
|
21
|
+
// imported on demand by `<CasualSheets>` via the `@casualoffice/sheets/chrome`
|
|
22
|
+
// subpath so `chrome="none"` consumers don't bundle the chrome JS.
|
|
23
|
+
export { ChromeTop } from './ChromeTop';
|
|
24
|
+
export { ChromeBottom } from './ChromeBottom';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* attachCollab — opt-in real-time co-editing for `<CasualSheets>`.
|
|
3
|
+
*
|
|
4
|
+
* The editor ships **collab-unaware**: it boots, edits, and persists with no
|
|
5
|
+
* knowledge of rooms or peers. A host that wants co-editing calls
|
|
6
|
+
* `attachCollab(api, { room, server })` once after `onReady` and gets back a
|
|
7
|
+
* detach handle. That's the entire public surface — everything else (presence
|
|
8
|
+
* UI, password prompts, room preflight, reconnect banners) is the host's to
|
|
9
|
+
* build on top of the returned `provider` / `doc`.
|
|
10
|
+
*
|
|
11
|
+
* Transport: Yjs + Hocuspocus, exactly as the reference host (`apps/web`) uses
|
|
12
|
+
* it. The non-negotiable Univer hooks live in `./bridge` — it subscribes to
|
|
13
|
+
* `ICommandService.onMutationExecutedForCollab`, applies remote mutations with
|
|
14
|
+
* `IExecutionOptions.fromCollab` (echo-loop prevention), and guards
|
|
15
|
+
* `params.__splitChunk__`.
|
|
16
|
+
*
|
|
17
|
+
* Persistence note: Yjs/Hocuspocus is the **realtime transport only**. The
|
|
18
|
+
* authoritative document is still saved by the host via WOPI / its own backend
|
|
19
|
+
* (the save/exit event contract) — collab does not turn the SDK into a store.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as Y from 'yjs';
|
|
23
|
+
import { HocuspocusProvider, HocuspocusProviderWebsocket } from '@hocuspocus/provider';
|
|
24
|
+
import type { IWorkbookData } from '@univerjs/core';
|
|
25
|
+
import type { FUniver } from '@univerjs/core/facade';
|
|
26
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
27
|
+
import { startBridge, type BridgeHandle } from './bridge';
|
|
28
|
+
import { buildWsUrl } from './ws-url';
|
|
29
|
+
|
|
30
|
+
/** Either the SDK's imperative API (`onReady`) or the bare FUniver facade.
|
|
31
|
+
* Collab only needs the facade, so a host that holds the raw FUniver (the
|
|
32
|
+
* reference app does) can attach without constructing a CasualSheetsAPI. */
|
|
33
|
+
export type CollabAttachable = CasualSheetsAPI | FUniver;
|
|
34
|
+
|
|
35
|
+
/** Pull the FUniver facade out of whichever attachable form was passed.
|
|
36
|
+
* FUniver exposes `getActiveWorkbook` directly; CasualSheetsAPI wraps the
|
|
37
|
+
* facade on `.univer`. Discriminating on the method (not on `'univer' in …`)
|
|
38
|
+
* is unambiguous either way. */
|
|
39
|
+
function resolveFacade(api: CollabAttachable): FUniver {
|
|
40
|
+
const maybe = api as Partial<FUniver> & Partial<CasualSheetsAPI>;
|
|
41
|
+
return typeof maybe.getActiveWorkbook === 'function'
|
|
42
|
+
? (api as FUniver)
|
|
43
|
+
: (maybe.univer as FUniver);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `write` peers broadcast their edits; `view` peers only receive. The
|
|
47
|
+
* client-side gate is belt-and-braces — real enforcement is the server's
|
|
48
|
+
* `role` check on the WS upgrade. */
|
|
49
|
+
export type CollabRole = 'view' | 'write';
|
|
50
|
+
|
|
51
|
+
/** Coarse connection state mapped from Hocuspocus's provider status. */
|
|
52
|
+
export type CollabConnectionStatus = 'connecting' | 'live' | 'offline';
|
|
53
|
+
|
|
54
|
+
export interface AttachCollabOptions {
|
|
55
|
+
/** Room / document id. Becomes the Hocuspocus document name. */
|
|
56
|
+
room: string;
|
|
57
|
+
/** Base WebSocket URL of the collab server, e.g. `wss://host/yjs`. */
|
|
58
|
+
server: string;
|
|
59
|
+
/** Room password, if the server gates the room. Sent on the WS URL as
|
|
60
|
+
* `p=…` so the upgrade handler can validate before the protocol handshake. */
|
|
61
|
+
password?: string;
|
|
62
|
+
/** Auth token for the Hocuspocus handshake. The provider only sends its
|
|
63
|
+
* auth submessage when this is truthy; servers with an `onAuthenticate`
|
|
64
|
+
* hook keep the connection queued without it. Defaults to `'anon'` (the
|
|
65
|
+
* reference server's hook only reads the `role` query param). */
|
|
66
|
+
token?: string;
|
|
67
|
+
/** `view` joins read-only. Defaults to `'write'`. */
|
|
68
|
+
role?: CollabRole;
|
|
69
|
+
/**
|
|
70
|
+
* Called when a peer's compaction snapshot arrives — the host swaps the
|
|
71
|
+
* workbook (typically `api.loadSnapshot(wb)`). MAY return a promise; the
|
|
72
|
+
* bridge pauses op-log replay until it resolves so later mutations don't
|
|
73
|
+
* land on the pre-swap unit.
|
|
74
|
+
*/
|
|
75
|
+
onSnapshot?: (wb: IWorkbookData) => void | Promise<void>;
|
|
76
|
+
/** Connection-status transitions — drive a status pill / offline banner. */
|
|
77
|
+
onStatus?: (status: CollabConnectionStatus) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CollabHandle {
|
|
81
|
+
/** The underlying Yjs document — introspection / devtools / extra maps. */
|
|
82
|
+
readonly doc: Y.Doc;
|
|
83
|
+
/** The Hocuspocus provider — `awareness` for presence, `on('status')`, etc. */
|
|
84
|
+
readonly provider: HocuspocusProvider;
|
|
85
|
+
/** The mutation bridge — replay-failure diagnostics live here. */
|
|
86
|
+
readonly bridge: BridgeHandle;
|
|
87
|
+
/** Last known connection status. */
|
|
88
|
+
status(): CollabConnectionStatus;
|
|
89
|
+
/** Detach: tear down bridge + provider + doc. Idempotent. */
|
|
90
|
+
detach(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Attach real-time collab to a live editor. Call once after `onReady`.
|
|
95
|
+
* Returns a {@link CollabHandle}; call `.detach()` to leave the room
|
|
96
|
+
* (and always before the editor unmounts).
|
|
97
|
+
*/
|
|
98
|
+
export function attachCollab(api: CollabAttachable, opts: AttachCollabOptions): CollabHandle {
|
|
99
|
+
const facade = resolveFacade(api);
|
|
100
|
+
const role: CollabRole = opts.role ?? 'write';
|
|
101
|
+
|
|
102
|
+
const doc = new Y.Doc();
|
|
103
|
+
// Match the reference host: drop the reconnect timeout to 10 s so a dropped
|
|
104
|
+
// socket surfaces as `offline` quickly rather than after the 30 s default.
|
|
105
|
+
const ws = new HocuspocusProviderWebsocket({
|
|
106
|
+
url: buildWsUrl(opts.server, opts.room, role, opts.password),
|
|
107
|
+
messageReconnectTimeout: 10_000,
|
|
108
|
+
});
|
|
109
|
+
const provider = new HocuspocusProvider({
|
|
110
|
+
websocketProvider: ws,
|
|
111
|
+
name: opts.room,
|
|
112
|
+
document: doc,
|
|
113
|
+
// Truthy token so the handshake completes even when the server has an
|
|
114
|
+
// onAuthenticate hook (used for role enforcement). See option docs above.
|
|
115
|
+
token: opts.token ?? 'anon',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const bridge = startBridge(facade, doc, {
|
|
119
|
+
role,
|
|
120
|
+
awareness: provider.awareness ?? undefined,
|
|
121
|
+
onSnapshotReceived: opts.onSnapshot,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let current: CollabConnectionStatus = 'connecting';
|
|
125
|
+
const onStatus = (ev: { status: string }) => {
|
|
126
|
+
const next: CollabConnectionStatus =
|
|
127
|
+
ev.status === 'connected' ? 'live' : ev.status === 'connecting' ? 'connecting' : 'offline';
|
|
128
|
+
if (next === current) return;
|
|
129
|
+
current = next;
|
|
130
|
+
opts.onStatus?.(next);
|
|
131
|
+
};
|
|
132
|
+
provider.on('status', onStatus);
|
|
133
|
+
|
|
134
|
+
let detached = false;
|
|
135
|
+
const detach = () => {
|
|
136
|
+
if (detached) return;
|
|
137
|
+
detached = true;
|
|
138
|
+
provider.off('status', onStatus);
|
|
139
|
+
bridge.dispose();
|
|
140
|
+
provider.destroy();
|
|
141
|
+
doc.destroy();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
doc,
|
|
146
|
+
provider,
|
|
147
|
+
bridge,
|
|
148
|
+
status: () => current,
|
|
149
|
+
detach,
|
|
150
|
+
};
|
|
151
|
+
}
|