@casualoffice/sheets 0.13.0 → 0.14.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/chrome.cjs +1568 -283
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.d.cts +35 -2
- package/dist/chrome.d.ts +35 -2
- package/dist/chrome.js +1566 -291
- package/dist/chrome.js.map +1 -1
- package/dist/embed/embed-runtime.js +131 -131
- package/dist/embed.cjs +3 -3
- package/dist/embed.cjs.map +1 -1
- package/dist/embed.js +3 -3
- package/dist/embed.js.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chrome/MenuBar.tsx +1246 -104
- package/src/chrome/Toolbar.tsx +546 -216
- package/src/embed/EmbedTransport.ts +3 -3
- package/src/embed-runtime/index.tsx +56 -8
package/src/chrome/Toolbar.tsx
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Toolbar — the rich built-in formatting toolbar for `<CasualSheets chrome>`.
|
|
3
3
|
*
|
|
4
|
-
* Drives the editor purely through `CasualSheetsAPI.executeCommand
|
|
5
|
-
* Material Symbols icons + design-system tokens. No
|
|
6
|
-
* row (the host frames the editor with its own bar).
|
|
7
|
-
* dividers, Office-style
|
|
4
|
+
* Drives the editor purely through `CasualSheetsAPI.executeCommand` + the FUniver
|
|
5
|
+
* facade (`api.univer`), with Material Symbols icons + design-system tokens. No
|
|
6
|
+
* app context, no title/logo row (the host frames the editor with its own bar).
|
|
7
|
+
* Commands grouped with dividers, Office-style — mirrors the app shell's
|
|
8
|
+
* `apps/web/src/shell/Toolbar.tsx` + `RibbonControls.tsx` group/order/icons,
|
|
9
|
+
* replicating `home-tab-actions.ts` logic inline against `api` / `api.univer`.
|
|
8
10
|
*
|
|
9
|
-
* Covered
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Covered (parity with the app's Home tab):
|
|
12
|
+
* history (undo/redo) · clipboard (paste/cut/copy/paste-values) · format
|
|
13
|
+
* painter · font family/size + grow/shrink · bold/italic/underline/strike ·
|
|
14
|
+
* text + fill colour (ColorPicker) · borders (BordersPicker) · horizontal +
|
|
15
|
+
* vertical align · wrap · merge/unmerge · number-format dropdown +
|
|
16
|
+
* currency/percent/decimals · AutoSum (AutoSumPicker) · clear formatting.
|
|
17
|
+
*
|
|
18
|
+
* Reflects the active cell — toggles light up, dropdowns show the current font /
|
|
19
|
+
* number format — via a command-execution subscription.
|
|
20
|
+
*
|
|
21
|
+
* Dialog-only controls (Format Cells, Insert Chart, PivotTable) have no SDK
|
|
22
|
+
* dialog: when the host passes `onDialogRequest` they render and call it so the
|
|
23
|
+
* host can render its own; without it, they are omitted entirely (no fake
|
|
24
|
+
* dialog). Each group/control can also be hidden via the `features` flag map.
|
|
14
25
|
*/
|
|
15
26
|
|
|
16
27
|
import { useEffect, useState, type CSSProperties } from 'react';
|
|
@@ -32,6 +43,7 @@ interface ActiveStyle {
|
|
|
32
43
|
tb: number; // WrapStrategy of the active cell (0 = unset, 3 = wrap)
|
|
33
44
|
ff: string;
|
|
34
45
|
fs: number;
|
|
46
|
+
nf: string; // number-format pattern ('' = General)
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
const NO_STYLE: ActiveStyle = {
|
|
@@ -44,6 +56,7 @@ const NO_STYLE: ActiveStyle = {
|
|
|
44
56
|
tb: 0,
|
|
45
57
|
ff: '',
|
|
46
58
|
fs: 0,
|
|
59
|
+
nf: '',
|
|
47
60
|
};
|
|
48
61
|
|
|
49
62
|
function readActiveStyle(api: CasualSheetsAPI): ActiveStyle {
|
|
@@ -63,11 +76,63 @@ function readActiveStyle(api: CasualSheetsAPI): ActiveStyle {
|
|
|
63
76
|
tb: typeof s?.tb === 'number' ? s.tb : 0,
|
|
64
77
|
ff: typeof s?.ff === 'string' ? s.ff : '',
|
|
65
78
|
fs: typeof s?.fs === 'number' ? s.fs : 0,
|
|
79
|
+
nf: typeof s?.n?.pattern === 'string' ? s.n.pattern : '',
|
|
66
80
|
};
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
const FONT_FAMILIES = [
|
|
70
|
-
|
|
83
|
+
const FONT_FAMILIES = [
|
|
84
|
+
'Calibri',
|
|
85
|
+
'Arial',
|
|
86
|
+
'Helvetica',
|
|
87
|
+
'Inter',
|
|
88
|
+
'Times New Roman',
|
|
89
|
+
'Georgia',
|
|
90
|
+
'Verdana',
|
|
91
|
+
'Courier New',
|
|
92
|
+
'JetBrains Mono',
|
|
93
|
+
];
|
|
94
|
+
const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Number-format presets, mirroring the app's `home-tab-actions`
|
|
98
|
+
* `NUMBER_FORMAT_PATTERNS`. The dropdown applies a pattern via the facade
|
|
99
|
+
* (`FRange.setNumberFormat`, a runtime numfmt-facade extension) so it lands as a
|
|
100
|
+
* normal numfmt mutation — same path the app uses.
|
|
101
|
+
*/
|
|
102
|
+
const NUMBER_FORMAT_PATTERNS = {
|
|
103
|
+
general: '',
|
|
104
|
+
integer: '#,##0',
|
|
105
|
+
number: '#,##0.00',
|
|
106
|
+
currency: '"$"#,##0.00',
|
|
107
|
+
accounting: '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)',
|
|
108
|
+
percent: '0.00%',
|
|
109
|
+
date: 'yyyy-mm-dd',
|
|
110
|
+
time: 'hh:mm:ss',
|
|
111
|
+
scientific: '0.00E+00',
|
|
112
|
+
text: '@',
|
|
113
|
+
} as const;
|
|
114
|
+
|
|
115
|
+
type NumberFormatKey = keyof typeof NUMBER_FORMAT_PATTERNS;
|
|
116
|
+
|
|
117
|
+
const NUMBER_FORMAT_OPTIONS: { value: NumberFormatKey; label: string }[] = [
|
|
118
|
+
{ value: 'general', label: 'General' },
|
|
119
|
+
{ value: 'integer', label: 'Number (no decimals)' },
|
|
120
|
+
{ value: 'number', label: 'Number (2 decimals)' },
|
|
121
|
+
{ value: 'currency', label: 'Currency' },
|
|
122
|
+
{ value: 'accounting', label: 'Accounting' },
|
|
123
|
+
{ value: 'percent', label: 'Percent' },
|
|
124
|
+
{ value: 'date', label: 'Date (yyyy-mm-dd)' },
|
|
125
|
+
{ value: 'time', label: 'Time (hh:mm:ss)' },
|
|
126
|
+
{ value: 'scientific', label: 'Scientific' },
|
|
127
|
+
{ value: 'text', label: 'Text' },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
function detectFormatKey(pattern: string): NumberFormatKey {
|
|
131
|
+
for (const k of Object.keys(NUMBER_FORMAT_PATTERNS) as NumberFormatKey[]) {
|
|
132
|
+
if (NUMBER_FORMAT_PATTERNS[k] === pattern) return k;
|
|
133
|
+
}
|
|
134
|
+
return 'general';
|
|
135
|
+
}
|
|
71
136
|
|
|
72
137
|
interface ToolbarAction {
|
|
73
138
|
id: string;
|
|
@@ -75,138 +140,208 @@ interface ToolbarAction {
|
|
|
75
140
|
command: string;
|
|
76
141
|
icon: string;
|
|
77
142
|
params?: object;
|
|
143
|
+
/** Feature key gating this action; defaults to the owning group's key. */
|
|
144
|
+
feature?: string;
|
|
78
145
|
}
|
|
79
146
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
label: '
|
|
99
|
-
command: '
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
id: '
|
|
112
|
-
label: '
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Each entry is a feature-gated group. The `feature` key lets a host hide the
|
|
149
|
+
* whole group (and per-action `feature` keys gate individual buttons). Order +
|
|
150
|
+
* icons mirror the app shell's Home tab.
|
|
151
|
+
*
|
|
152
|
+
* HorizontalAlign enum (@univerjs/core): LEFT=1, CENTER=2, RIGHT=3.
|
|
153
|
+
* VerticalAlign enum (@univerjs/core): TOP=1, MIDDLE=2, BOTTOM=3.
|
|
154
|
+
* WrapStrategy enum (@univerjs/core): OVERFLOW=1, CLIP=2, WRAP=3.
|
|
155
|
+
*
|
|
156
|
+
* Clipboard command ids: `univer.command.{paste,cut,copy}` (matches
|
|
157
|
+
* `home-tab-actions.ts`). Paste values-only uses the base paste command with a
|
|
158
|
+
* predefined hook `value` (`special-paste-value`), same as the app's
|
|
159
|
+
* `pasteSpecial`.
|
|
160
|
+
*/
|
|
161
|
+
const GROUPS: { feature: string; actions: ToolbarAction[] }[] = [
|
|
162
|
+
{
|
|
163
|
+
feature: 'history',
|
|
164
|
+
actions: [
|
|
165
|
+
{ id: 'undo', label: 'Undo (Ctrl+Z)', command: 'univer.command.undo', icon: 'undo' },
|
|
166
|
+
{ id: 'redo', label: 'Redo (Ctrl+Y)', command: 'univer.command.redo', icon: 'redo' },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
feature: 'clipboard',
|
|
171
|
+
actions: [
|
|
172
|
+
{
|
|
173
|
+
id: 'paste',
|
|
174
|
+
label: 'Paste (Ctrl+V)',
|
|
175
|
+
command: 'univer.command.paste',
|
|
176
|
+
icon: 'content_paste',
|
|
177
|
+
},
|
|
178
|
+
{ id: 'cut', label: 'Cut (Ctrl+X)', command: 'univer.command.cut', icon: 'content_cut' },
|
|
179
|
+
{ id: 'copy', label: 'Copy (Ctrl+C)', command: 'univer.command.copy', icon: 'content_copy' },
|
|
180
|
+
{
|
|
181
|
+
id: 'paste-values',
|
|
182
|
+
label: 'Paste values only',
|
|
183
|
+
command: 'univer.command.paste',
|
|
184
|
+
icon: 'content_paste_go',
|
|
185
|
+
params: { value: 'special-paste-value' },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
feature: 'format-painter',
|
|
191
|
+
actions: [
|
|
192
|
+
{
|
|
193
|
+
id: 'format-painter',
|
|
194
|
+
label: 'Format Painter',
|
|
195
|
+
command: 'sheet.command.set-once-format-painter',
|
|
196
|
+
icon: 'format_paint',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
feature: 'font-style',
|
|
202
|
+
actions: [
|
|
203
|
+
{
|
|
204
|
+
id: 'bold',
|
|
205
|
+
label: 'Bold (Ctrl+B)',
|
|
206
|
+
command: 'sheet.command.set-range-bold',
|
|
207
|
+
icon: 'format_bold',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'italic',
|
|
211
|
+
label: 'Italic (Ctrl+I)',
|
|
212
|
+
command: 'sheet.command.set-range-italic',
|
|
213
|
+
icon: 'format_italic',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'underline',
|
|
217
|
+
label: 'Underline (Ctrl+U)',
|
|
218
|
+
command: 'sheet.command.set-range-underline',
|
|
219
|
+
icon: 'format_underlined',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'strikethrough',
|
|
223
|
+
label: 'Strikethrough',
|
|
224
|
+
command: 'sheet.command.set-range-stroke',
|
|
225
|
+
icon: 'format_strikethrough',
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
feature: 'alignment',
|
|
231
|
+
actions: [
|
|
232
|
+
{
|
|
233
|
+
id: 'align-left',
|
|
234
|
+
label: 'Align left',
|
|
235
|
+
command: 'sheet.command.set-horizontal-text-align',
|
|
236
|
+
icon: 'format_align_left',
|
|
237
|
+
params: { value: 1 },
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'align-center',
|
|
241
|
+
label: 'Align center',
|
|
242
|
+
command: 'sheet.command.set-horizontal-text-align',
|
|
243
|
+
icon: 'format_align_center',
|
|
244
|
+
params: { value: 2 },
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'align-right',
|
|
248
|
+
label: 'Align right',
|
|
249
|
+
command: 'sheet.command.set-horizontal-text-align',
|
|
250
|
+
icon: 'format_align_right',
|
|
251
|
+
params: { value: 3 },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'wrap-text',
|
|
255
|
+
label: 'Wrap text',
|
|
256
|
+
command: 'sheet.command.set-text-wrap',
|
|
257
|
+
icon: 'wrap_text',
|
|
258
|
+
params: { value: 3 },
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
feature: 'alignment',
|
|
264
|
+
actions: [
|
|
265
|
+
{
|
|
266
|
+
id: 'align-top',
|
|
267
|
+
label: 'Align top',
|
|
268
|
+
command: 'sheet.command.set-vertical-text-align',
|
|
269
|
+
icon: 'vertical_align_top',
|
|
270
|
+
params: { value: 1 },
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'align-middle',
|
|
274
|
+
label: 'Align middle',
|
|
275
|
+
command: 'sheet.command.set-vertical-text-align',
|
|
276
|
+
icon: 'vertical_align_center',
|
|
277
|
+
params: { value: 2 },
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'align-bottom',
|
|
281
|
+
label: 'Align bottom',
|
|
282
|
+
command: 'sheet.command.set-vertical-text-align',
|
|
283
|
+
icon: 'vertical_align_bottom',
|
|
284
|
+
params: { value: 3 },
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
feature: 'merge',
|
|
290
|
+
actions: [
|
|
291
|
+
{
|
|
292
|
+
id: 'merge',
|
|
293
|
+
label: 'Merge cells',
|
|
294
|
+
command: 'sheet.command.add-worksheet-merge-all',
|
|
295
|
+
icon: 'cell_merge',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'unmerge',
|
|
299
|
+
label: 'Unmerge cells',
|
|
300
|
+
command: 'sheet.command.remove-worksheet-merge',
|
|
301
|
+
icon: 'splitscreen_vertical_add',
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
feature: 'number',
|
|
307
|
+
actions: [
|
|
308
|
+
{
|
|
309
|
+
id: 'currency',
|
|
310
|
+
label: 'Currency format',
|
|
311
|
+
command: 'sheet.command.numfmt.set.currency',
|
|
312
|
+
icon: 'attach_money',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: 'percent',
|
|
316
|
+
label: 'Percent format',
|
|
317
|
+
command: 'sheet.command.numfmt.set.percent',
|
|
318
|
+
icon: 'percent',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'decimal-increase',
|
|
322
|
+
label: 'Increase decimals',
|
|
323
|
+
command: 'sheet.command.numfmt.add.decimal.command',
|
|
324
|
+
icon: 'decimal_increase',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'decimal-decrease',
|
|
328
|
+
label: 'Decrease decimals',
|
|
329
|
+
command: 'sheet.command.numfmt.subtract.decimal.command',
|
|
330
|
+
icon: 'decimal_decrease',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
feature: 'clear-format',
|
|
336
|
+
actions: [
|
|
337
|
+
{
|
|
338
|
+
id: 'clear-format',
|
|
339
|
+
label: 'Clear formatting',
|
|
340
|
+
command: 'sheet.command.clear-selection-format',
|
|
341
|
+
icon: 'format_clear',
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
210
345
|
];
|
|
211
346
|
|
|
212
347
|
const BAR_STYLE: CSSProperties = {
|
|
@@ -218,6 +353,7 @@ const BAR_STYLE: CSSProperties = {
|
|
|
218
353
|
background: 'var(--cs-chrome-bg, #eef1f5)',
|
|
219
354
|
flex: '0 0 auto',
|
|
220
355
|
userSelect: 'none',
|
|
356
|
+
flexWrap: 'wrap',
|
|
221
357
|
};
|
|
222
358
|
|
|
223
359
|
const BTN_STYLE: CSSProperties = {
|
|
@@ -257,6 +393,25 @@ const SELECT_STYLE: CSSProperties = {
|
|
|
257
393
|
export interface ToolbarProps {
|
|
258
394
|
/** Live API, or `null` until the editor is ready. */
|
|
259
395
|
api: CasualSheetsAPI | null;
|
|
396
|
+
/**
|
|
397
|
+
* Per-control / per-group feature flags. A key set to `false` hides that
|
|
398
|
+
* control or group; omitted keys default to enabled. Keys:
|
|
399
|
+
* history · clipboard · format-painter · font · font-style · color ·
|
|
400
|
+
* borders · alignment · merge · number · autosum · clear-format ·
|
|
401
|
+
* format-cells · insert-chart · pivot-table
|
|
402
|
+
*/
|
|
403
|
+
features?: Record<string, boolean>;
|
|
404
|
+
/**
|
|
405
|
+
* Host hook for dialogs the SDK has no built-in UI for (Format Cells, Insert
|
|
406
|
+
* Chart, PivotTable). When provided, those controls render and call this with
|
|
407
|
+
* a `kind`; when omitted, they are not rendered (no fake dialog).
|
|
408
|
+
*/
|
|
409
|
+
onDialogRequest?: (kind: string, context?: unknown) => void;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** A feature is on unless explicitly set to `false`. */
|
|
413
|
+
function enabled(features: Record<string, boolean> | undefined, key: string): boolean {
|
|
414
|
+
return features?.[key] !== false;
|
|
260
415
|
}
|
|
261
416
|
|
|
262
417
|
// Which actions reflect an active state, keyed off the active cell's style.
|
|
@@ -289,7 +444,7 @@ function isActive(id: string, s: ActiveStyle): boolean {
|
|
|
289
444
|
}
|
|
290
445
|
}
|
|
291
446
|
|
|
292
|
-
export function Toolbar({ api }: ToolbarProps) {
|
|
447
|
+
export function Toolbar({ api, features, onDialogRequest }: ToolbarProps) {
|
|
293
448
|
const [active, setActive] = useState<ActiveStyle>(NO_STYLE);
|
|
294
449
|
|
|
295
450
|
useEffect(() => {
|
|
@@ -313,89 +468,264 @@ export function Toolbar({ api }: ToolbarProps) {
|
|
|
313
468
|
|
|
314
469
|
const dispatch = (command: string, params?: object) => void api?.executeCommand(command, params);
|
|
315
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Apply a font size via the dedicated command (matches the app's
|
|
473
|
+
* `set-range-fontsize`). Used by the size dropdown + grow/shrink.
|
|
474
|
+
*/
|
|
475
|
+
const applyFontSize = (size: number) => {
|
|
476
|
+
if (!Number.isFinite(size) || size <= 0) return;
|
|
477
|
+
dispatch('sheet.command.set-range-fontsize', { value: size });
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Grow / shrink the active cell's font size by `delta`, mirroring the app's
|
|
482
|
+
* `adjustFontSize` (read current off the active cell, fall back to 11, clamp
|
|
483
|
+
* to [6, 72]). Reads the size we already track in `active`.
|
|
484
|
+
*/
|
|
485
|
+
const adjustFontSize = (delta: number) => {
|
|
486
|
+
const current = active.fs && active.fs > 0 ? active.fs : 11;
|
|
487
|
+
const next = Math.max(6, Math.min(72, current + delta));
|
|
488
|
+
if (next === current) return;
|
|
489
|
+
applyFontSize(next);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Apply a number-format pattern via the FUniver facade — `FRange.setNumberFormat`
|
|
494
|
+
* is a runtime numfmt-facade extension (added via `FUniver.extend()`), so a
|
|
495
|
+
* runtime cast is the cleanest way to reach it. Same approach as the app's
|
|
496
|
+
* `home-tab-actions.setNumberFormat`.
|
|
497
|
+
*/
|
|
498
|
+
const applyNumberFormat = (pattern: string) => {
|
|
499
|
+
const sheet = api?.univer.getActiveWorkbook()?.getActiveSheet();
|
|
500
|
+
const range = (sheet as unknown as { getActiveRange?: () => unknown })?.getActiveRange?.() as
|
|
501
|
+
| { setNumberFormat?: (p: string) => unknown }
|
|
502
|
+
| undefined;
|
|
503
|
+
range?.setNumberFormat?.(pattern);
|
|
504
|
+
};
|
|
505
|
+
|
|
316
506
|
// Reflect current font in the dropdowns; surface a non-listed value too.
|
|
317
|
-
const familyValue = active.ff || '
|
|
507
|
+
const familyValue = active.ff || 'Calibri';
|
|
318
508
|
const familyOptions =
|
|
319
509
|
active.ff && !FONT_FAMILIES.includes(active.ff) ? [active.ff, ...FONT_FAMILIES] : FONT_FAMILIES;
|
|
320
510
|
const sizeValue = active.fs || 11;
|
|
321
511
|
const sizeOptions =
|
|
322
512
|
active.fs && !FONT_SIZES.includes(active.fs) ? [active.fs, ...FONT_SIZES] : FONT_SIZES;
|
|
513
|
+
const numberFormatValue = detectFormatKey(active.nf);
|
|
514
|
+
|
|
515
|
+
const renderActionButton = (a: ToolbarAction) => {
|
|
516
|
+
const on = isActive(a.id, active);
|
|
517
|
+
const baseBg = on ? 'var(--cs-chrome-active, #e6f3f7)' : 'transparent';
|
|
518
|
+
return (
|
|
519
|
+
<button
|
|
520
|
+
key={a.id}
|
|
521
|
+
type="button"
|
|
522
|
+
title={a.label}
|
|
523
|
+
aria-label={a.label}
|
|
524
|
+
aria-pressed={on}
|
|
525
|
+
data-action={a.id}
|
|
526
|
+
data-testid={`cs-${a.id}`}
|
|
527
|
+
data-active={on ? 'true' : undefined}
|
|
528
|
+
disabled={!api}
|
|
529
|
+
style={{
|
|
530
|
+
...BTN_STYLE,
|
|
531
|
+
background: baseBg,
|
|
532
|
+
color: on ? 'var(--cs-chrome-active-fg, #0e7490)' : BTN_STYLE.color,
|
|
533
|
+
}}
|
|
534
|
+
// mousedown (not click) so the grid's selection isn't lost first.
|
|
535
|
+
onMouseDown={(e) => {
|
|
536
|
+
e.preventDefault();
|
|
537
|
+
dispatch(a.command, a.params);
|
|
538
|
+
}}
|
|
539
|
+
onMouseEnter={(e) => {
|
|
540
|
+
if (!on) e.currentTarget.style.background = 'var(--cs-chrome-hover, rgba(0,0,0,0.06))';
|
|
541
|
+
}}
|
|
542
|
+
onMouseLeave={(e) => {
|
|
543
|
+
e.currentTarget.style.background = baseBg;
|
|
544
|
+
}}
|
|
545
|
+
>
|
|
546
|
+
<Icon name={a.icon} size={20} />
|
|
547
|
+
</button>
|
|
548
|
+
);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// A plain (non-command) icon button — used for grow/shrink + dialog requests.
|
|
552
|
+
const renderIconButton = (
|
|
553
|
+
id: string,
|
|
554
|
+
label: string,
|
|
555
|
+
icon: string,
|
|
556
|
+
onPress: () => void,
|
|
557
|
+
testid: string,
|
|
558
|
+
) => (
|
|
559
|
+
<button
|
|
560
|
+
key={id}
|
|
561
|
+
type="button"
|
|
562
|
+
title={label}
|
|
563
|
+
aria-label={label}
|
|
564
|
+
data-action={id}
|
|
565
|
+
data-testid={testid}
|
|
566
|
+
disabled={!api}
|
|
567
|
+
style={BTN_STYLE}
|
|
568
|
+
onMouseDown={(e) => {
|
|
569
|
+
e.preventDefault();
|
|
570
|
+
onPress();
|
|
571
|
+
}}
|
|
572
|
+
onMouseEnter={(e) => {
|
|
573
|
+
e.currentTarget.style.background = 'var(--cs-chrome-hover, rgba(0,0,0,0.06))';
|
|
574
|
+
}}
|
|
575
|
+
onMouseLeave={(e) => {
|
|
576
|
+
e.currentTarget.style.background = 'transparent';
|
|
577
|
+
}}
|
|
578
|
+
>
|
|
579
|
+
<Icon name={icon} size={20} />
|
|
580
|
+
</button>
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
// Render the static command groups, skipping any whose feature is off and any
|
|
584
|
+
// group left empty after per-action filtering. A leading divider separates a
|
|
585
|
+
// group from whatever rendered before it.
|
|
586
|
+
let firstGroupRendered = false;
|
|
587
|
+
const groupNodes = GROUPS.map((group, gi) => {
|
|
588
|
+
if (!enabled(features, group.feature)) return null;
|
|
589
|
+
const visible = group.actions.filter((a) => enabled(features, a.feature ?? group.feature));
|
|
590
|
+
if (visible.length === 0) return null;
|
|
591
|
+
const showDivider = firstGroupRendered;
|
|
592
|
+
firstGroupRendered = true;
|
|
593
|
+
return (
|
|
594
|
+
<span key={gi} style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
595
|
+
{showDivider && <span style={DIVIDER_STYLE} aria-hidden />}
|
|
596
|
+
{visible.map(renderActionButton)}
|
|
597
|
+
</span>
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const showFont = enabled(features, 'font');
|
|
602
|
+
const showColor = enabled(features, 'color');
|
|
603
|
+
const showBorders = enabled(features, 'borders');
|
|
604
|
+
const showAutoSum = enabled(features, 'autosum');
|
|
605
|
+
const showNumberDropdown = enabled(features, 'number');
|
|
606
|
+
|
|
607
|
+
// Dialog-only controls: render ONLY when the host gave us a way to open them.
|
|
608
|
+
const showFormatCells = enabled(features, 'format-cells') && !!onDialogRequest;
|
|
609
|
+
const showInsertChart = enabled(features, 'insert-chart') && !!onDialogRequest;
|
|
610
|
+
const showPivotTable = enabled(features, 'pivot-table') && !!onDialogRequest;
|
|
611
|
+
const showDialogGroup = showFormatCells || showInsertChart || showPivotTable;
|
|
323
612
|
|
|
324
613
|
return (
|
|
325
614
|
<div style={BAR_STYLE} data-testid="casual-sheets-toolbar" role="toolbar" aria-label="Editor">
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
615
|
+
{showFont && (
|
|
616
|
+
<>
|
|
617
|
+
<select
|
|
618
|
+
aria-label="Font family"
|
|
619
|
+
data-testid="cs-font-family"
|
|
620
|
+
style={{ ...SELECT_STYLE, width: 130 }}
|
|
621
|
+
value={familyValue}
|
|
622
|
+
disabled={!api}
|
|
623
|
+
onChange={(e) =>
|
|
624
|
+
dispatch('sheet.command.set-range-font-family', { value: e.target.value })
|
|
625
|
+
}
|
|
626
|
+
>
|
|
627
|
+
{familyOptions.map((f) => (
|
|
628
|
+
<option key={f} value={f}>
|
|
629
|
+
{f}
|
|
630
|
+
</option>
|
|
631
|
+
))}
|
|
632
|
+
</select>
|
|
633
|
+
<select
|
|
634
|
+
aria-label="Font size"
|
|
635
|
+
data-testid="cs-font-size"
|
|
636
|
+
style={{ ...SELECT_STYLE, width: 56, marginLeft: 4 }}
|
|
637
|
+
value={sizeValue}
|
|
638
|
+
disabled={!api}
|
|
639
|
+
onChange={(e) => applyFontSize(Number(e.target.value))}
|
|
640
|
+
>
|
|
641
|
+
{sizeOptions.map((s) => (
|
|
642
|
+
<option key={s} value={s}>
|
|
643
|
+
{s}
|
|
644
|
+
</option>
|
|
645
|
+
))}
|
|
646
|
+
</select>
|
|
647
|
+
{renderIconButton(
|
|
648
|
+
'font-size-up',
|
|
649
|
+
'Increase font size',
|
|
650
|
+
'text_increase',
|
|
651
|
+
() => adjustFontSize(+1),
|
|
652
|
+
'cs-font-size-up',
|
|
653
|
+
)}
|
|
654
|
+
{renderIconButton(
|
|
655
|
+
'font-size-down',
|
|
656
|
+
'Decrease font size',
|
|
657
|
+
'text_decrease',
|
|
658
|
+
() => adjustFontSize(-1),
|
|
659
|
+
'cs-font-size-down',
|
|
660
|
+
)}
|
|
661
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
662
|
+
</>
|
|
663
|
+
)}
|
|
664
|
+
|
|
665
|
+
{groupNodes}
|
|
666
|
+
|
|
667
|
+
{(showColor || showBorders) && <span style={DIVIDER_STYLE} aria-hidden />}
|
|
668
|
+
{showColor && <ColorPicker api={api} />}
|
|
669
|
+
{showBorders && <BordersPicker api={api} />}
|
|
670
|
+
|
|
671
|
+
{showNumberDropdown && (
|
|
672
|
+
<>
|
|
673
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
674
|
+
<select
|
|
675
|
+
aria-label="Number format"
|
|
676
|
+
data-testid="cs-number-format"
|
|
677
|
+
style={{ ...SELECT_STYLE, width: 124 }}
|
|
678
|
+
value={numberFormatValue}
|
|
679
|
+
disabled={!api}
|
|
680
|
+
onChange={(e) =>
|
|
681
|
+
applyNumberFormat(NUMBER_FORMAT_PATTERNS[e.target.value as NumberFormatKey])
|
|
682
|
+
}
|
|
683
|
+
>
|
|
684
|
+
{NUMBER_FORMAT_OPTIONS.map((o) => (
|
|
685
|
+
<option key={o.value} value={o.value}>
|
|
686
|
+
{o.label}
|
|
687
|
+
</option>
|
|
688
|
+
))}
|
|
689
|
+
</select>
|
|
690
|
+
</>
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
{showAutoSum && (
|
|
694
|
+
<>
|
|
695
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
696
|
+
<AutoSumPicker api={api} />
|
|
697
|
+
</>
|
|
698
|
+
)}
|
|
699
|
+
|
|
700
|
+
{showDialogGroup && (
|
|
701
|
+
<>
|
|
702
|
+
<span style={DIVIDER_STYLE} aria-hidden />
|
|
703
|
+
{showFormatCells &&
|
|
704
|
+
renderIconButton(
|
|
705
|
+
'format-cells',
|
|
706
|
+
'Format Cells…',
|
|
707
|
+
'format_shapes',
|
|
708
|
+
() => onDialogRequest?.('format-cells'),
|
|
709
|
+
'cs-format-cells',
|
|
710
|
+
)}
|
|
711
|
+
{showInsertChart &&
|
|
712
|
+
renderIconButton(
|
|
713
|
+
'insert-chart',
|
|
714
|
+
'Insert chart',
|
|
715
|
+
'bar_chart',
|
|
716
|
+
() => onDialogRequest?.('insert-chart'),
|
|
717
|
+
'cs-insert-chart',
|
|
718
|
+
)}
|
|
719
|
+
{showPivotTable &&
|
|
720
|
+
renderIconButton(
|
|
721
|
+
'pivot-table',
|
|
722
|
+
'Insert PivotTable',
|
|
723
|
+
'pivot_table_chart',
|
|
724
|
+
() => onDialogRequest?.('pivot-table'),
|
|
725
|
+
'cs-pivot-table',
|
|
726
|
+
)}
|
|
727
|
+
</>
|
|
728
|
+
)}
|
|
399
729
|
</div>
|
|
400
730
|
);
|
|
401
731
|
}
|