@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SheetTabs — worksheet tab strip for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* The fundamental multi-sheet control the built-in chrome was missing: switch
|
|
5
|
+
* sheets (click), add a sheet (+), rename (double-click), and delete (context
|
|
6
|
+
* menu). Drives the editor through the FUniver facade only — `insertSheet` /
|
|
7
|
+
* `setActiveSheet` / `deleteSheet` / `FWorksheet.setName` — and stays live by
|
|
8
|
+
* subscribing to the sheet-lifecycle facade events (plus the mutation-level
|
|
9
|
+
* fallback so collab/replay-driven changes refresh too).
|
|
10
|
+
*
|
|
11
|
+
* Scope of this batch: switch / add / rename / delete. Drag-reorder, tab
|
|
12
|
+
* colour, hide/unhide, and duplicate are richer host features deferred to
|
|
13
|
+
* later parity batches (the reference app's SheetTabs has them).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useState, type CSSProperties, type KeyboardEvent } from 'react';
|
|
17
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
18
|
+
|
|
19
|
+
interface SheetTab {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
hidden: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SheetsSnapshot {
|
|
26
|
+
tabs: SheetTab[];
|
|
27
|
+
activeId: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const EMPTY: SheetsSnapshot = { tabs: [], activeId: null };
|
|
31
|
+
|
|
32
|
+
/** Sheet-list mutations that must refresh the strip even when they arrive via
|
|
33
|
+
* the collab bridge's mutation-only replay (which bypasses facade events). */
|
|
34
|
+
const SHEET_LIST_MUTATIONS = new Set([
|
|
35
|
+
'sheet.mutation.insert-sheet',
|
|
36
|
+
'sheet.mutation.remove-sheet',
|
|
37
|
+
'sheet.mutation.set-worksheet-name',
|
|
38
|
+
'sheet.mutation.set-worksheet-order',
|
|
39
|
+
'sheet.mutation.set-worksheet-hidden',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
type AnyFacade = any;
|
|
44
|
+
|
|
45
|
+
function readSheets(api: CasualSheetsAPI): SheetsSnapshot {
|
|
46
|
+
const wb = api.univer.getActiveWorkbook();
|
|
47
|
+
if (!wb) return EMPTY;
|
|
48
|
+
const tabs = wb.getSheets().map((s) => ({
|
|
49
|
+
id: s.getSheetId(),
|
|
50
|
+
name: s.getSheetName(),
|
|
51
|
+
hidden: (s as AnyFacade).isSheetHidden?.() === true,
|
|
52
|
+
}));
|
|
53
|
+
return { tabs, activeId: wb.getActiveSheet()?.getSheetId() ?? null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const BAR_STYLE: CSSProperties = {
|
|
57
|
+
display: 'flex',
|
|
58
|
+
alignItems: 'stretch',
|
|
59
|
+
gap: 2,
|
|
60
|
+
height: 30,
|
|
61
|
+
flex: '0 0 auto',
|
|
62
|
+
padding: '0 6px',
|
|
63
|
+
borderTop: '1px solid var(--cs-chrome-border, rgba(0,0,0,0.12))',
|
|
64
|
+
background: 'var(--cs-chrome-bg, #f8f9fa)',
|
|
65
|
+
font: 'inherit',
|
|
66
|
+
fontSize: 13,
|
|
67
|
+
overflowX: 'auto',
|
|
68
|
+
overflowY: 'hidden',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const ADD_BTN_STYLE: CSSProperties = {
|
|
72
|
+
flex: '0 0 auto',
|
|
73
|
+
width: 28,
|
|
74
|
+
border: 'none',
|
|
75
|
+
background: 'transparent',
|
|
76
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
77
|
+
cursor: 'pointer',
|
|
78
|
+
fontSize: 18,
|
|
79
|
+
lineHeight: 1,
|
|
80
|
+
borderRadius: 4,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const RENAME_INPUT_STYLE: CSSProperties = {
|
|
84
|
+
height: 22,
|
|
85
|
+
alignSelf: 'center',
|
|
86
|
+
padding: '0 6px',
|
|
87
|
+
border: '1px solid var(--cs-chrome-active-fg, #0e7490)',
|
|
88
|
+
borderRadius: 4,
|
|
89
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
90
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
91
|
+
font: 'inherit',
|
|
92
|
+
fontSize: 13,
|
|
93
|
+
width: 120,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const MENU_STYLE: CSSProperties = {
|
|
97
|
+
position: 'fixed',
|
|
98
|
+
zIndex: 1000,
|
|
99
|
+
minWidth: 140,
|
|
100
|
+
padding: 4,
|
|
101
|
+
border: '1px solid var(--cs-chrome-border, #e6e9ee)',
|
|
102
|
+
borderRadius: 8,
|
|
103
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
104
|
+
boxShadow: '0 6px 20px rgba(0,0,0,0.16)',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const MENU_ITEM_STYLE: CSSProperties = {
|
|
108
|
+
display: 'block',
|
|
109
|
+
width: '100%',
|
|
110
|
+
textAlign: 'left',
|
|
111
|
+
height: 28,
|
|
112
|
+
padding: '0 10px',
|
|
113
|
+
border: 'none',
|
|
114
|
+
borderRadius: 6,
|
|
115
|
+
background: 'transparent',
|
|
116
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
117
|
+
font: 'inherit',
|
|
118
|
+
fontSize: 13,
|
|
119
|
+
cursor: 'pointer',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export interface SheetTabsProps {
|
|
123
|
+
/** Live API, or `null` until the editor is ready. */
|
|
124
|
+
api: CasualSheetsAPI | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function SheetTabs({ api }: SheetTabsProps) {
|
|
128
|
+
const [{ tabs, activeId }, setSnapshot] = useState<SheetsSnapshot>(EMPTY);
|
|
129
|
+
// Inline-rename target sheet id (null = not renaming) + its draft text.
|
|
130
|
+
const [renaming, setRenaming] = useState<string | null>(null);
|
|
131
|
+
const [draft, setDraft] = useState('');
|
|
132
|
+
// Right-click context menu: anchor + target sheet id.
|
|
133
|
+
const [menu, setMenu] = useState<{ x: number; y: number; sheetId: string } | null>(null);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!api) return;
|
|
137
|
+
const refresh = () => setSnapshot(readSheets(api));
|
|
138
|
+
refresh();
|
|
139
|
+
const f = api.univer as AnyFacade;
|
|
140
|
+
const subs: Array<{ dispose(): void }> = [];
|
|
141
|
+
try {
|
|
142
|
+
subs.push(
|
|
143
|
+
f.addEvent(f.Event.SheetCreated, refresh),
|
|
144
|
+
f.addEvent(f.Event.SheetDeleted, refresh),
|
|
145
|
+
f.addEvent(f.Event.SheetNameChanged, refresh),
|
|
146
|
+
f.addEvent(f.Event.ActiveSheetChanged, refresh),
|
|
147
|
+
f.addEvent(f.Event.SheetMoved, refresh),
|
|
148
|
+
// Mutation-level fallback — catches collab/replay-driven changes that
|
|
149
|
+
// don't emit the higher-level facade events.
|
|
150
|
+
f.addEvent(f.Event.CommandExecuted, (e: { id?: string }) => {
|
|
151
|
+
if (e?.id && SHEET_LIST_MUTATIONS.has(e.id)) refresh();
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
// Whole-unit swaps (loadSnapshot / File→Open) don't fire SheetCreated;
|
|
155
|
+
// refresh a tick after the new unit is wired in.
|
|
156
|
+
if (typeof f.onUniverSheetCreated === 'function') {
|
|
157
|
+
subs.push(f.onUniverSheetCreated(() => queueMicrotask(refresh)));
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
/* facade event surface differs — the initial read still populated tabs */
|
|
161
|
+
}
|
|
162
|
+
return () => {
|
|
163
|
+
for (const s of subs) s?.dispose?.();
|
|
164
|
+
};
|
|
165
|
+
}, [api]);
|
|
166
|
+
|
|
167
|
+
// Close the context menu on any outside interaction.
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!menu) return;
|
|
170
|
+
const close = () => setMenu(null);
|
|
171
|
+
window.addEventListener('pointerdown', close);
|
|
172
|
+
window.addEventListener('keydown', close);
|
|
173
|
+
return () => {
|
|
174
|
+
window.removeEventListener('pointerdown', close);
|
|
175
|
+
window.removeEventListener('keydown', close);
|
|
176
|
+
};
|
|
177
|
+
}, [menu]);
|
|
178
|
+
|
|
179
|
+
if (!api) {
|
|
180
|
+
return <div style={BAR_STYLE} data-testid="casual-sheets-tabs" />;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const visible = tabs.filter((t) => !t.hidden);
|
|
184
|
+
const wb = () => api.univer.getActiveWorkbook();
|
|
185
|
+
|
|
186
|
+
const switchTo = (id: string) => {
|
|
187
|
+
if (id === activeId) return;
|
|
188
|
+
wb()?.setActiveSheet(id);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const addSheet = () => {
|
|
192
|
+
const created = (wb() as AnyFacade)?.insertSheet?.();
|
|
193
|
+
created?.activate?.();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const startRename = (tab: SheetTab) => {
|
|
197
|
+
setMenu(null);
|
|
198
|
+
setRenaming(tab.id);
|
|
199
|
+
setDraft(tab.name);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const commitRename = () => {
|
|
203
|
+
const id = renaming;
|
|
204
|
+
setRenaming(null);
|
|
205
|
+
if (!id) return;
|
|
206
|
+
const name = draft.trim();
|
|
207
|
+
if (!name) return;
|
|
208
|
+
const sheet = (wb() as AnyFacade)?.getSheetBySheetId?.(id);
|
|
209
|
+
if (sheet && sheet.getSheetName?.() !== name) sheet.setName?.(name);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const deleteSheet = (id: string) => {
|
|
213
|
+
setMenu(null);
|
|
214
|
+
// Excel keeps at least one visible sheet — never delete the last one.
|
|
215
|
+
if (visible.length <= 1) return;
|
|
216
|
+
(wb() as AnyFacade)?.deleteSheet?.(id);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const onRenameKey = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
220
|
+
if (e.key === 'Enter') {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
commitRename();
|
|
223
|
+
} else if (e.key === 'Escape') {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
setRenaming(null);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div style={BAR_STYLE} data-testid="casual-sheets-tabs" role="tablist">
|
|
231
|
+
{visible.map((tab) => {
|
|
232
|
+
const active = tab.id === activeId;
|
|
233
|
+
if (renaming === tab.id) {
|
|
234
|
+
return (
|
|
235
|
+
<input
|
|
236
|
+
key={tab.id}
|
|
237
|
+
autoFocus
|
|
238
|
+
style={RENAME_INPUT_STYLE}
|
|
239
|
+
value={draft}
|
|
240
|
+
data-testid="cs-tab-rename-input"
|
|
241
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
242
|
+
onKeyDown={onRenameKey}
|
|
243
|
+
onBlur={commitRename}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return (
|
|
248
|
+
<button
|
|
249
|
+
key={tab.id}
|
|
250
|
+
type="button"
|
|
251
|
+
role="tab"
|
|
252
|
+
aria-selected={active}
|
|
253
|
+
data-testid={`cs-tab-${tab.id}`}
|
|
254
|
+
data-active={active || undefined}
|
|
255
|
+
title={tab.name}
|
|
256
|
+
style={{
|
|
257
|
+
flex: '0 0 auto',
|
|
258
|
+
maxWidth: 160,
|
|
259
|
+
padding: '0 12px',
|
|
260
|
+
border: 'none',
|
|
261
|
+
borderTop: active
|
|
262
|
+
? '2px solid var(--cs-chrome-active-fg, #0e7490)'
|
|
263
|
+
: '2px solid transparent',
|
|
264
|
+
background: active ? 'var(--cs-chrome-input-bg, #fff)' : 'transparent',
|
|
265
|
+
color: active
|
|
266
|
+
? 'var(--cs-chrome-active-fg, #0e7490)'
|
|
267
|
+
: 'var(--cs-chrome-fg, #201f1e)',
|
|
268
|
+
fontWeight: active ? 600 : 400,
|
|
269
|
+
font: 'inherit',
|
|
270
|
+
fontSize: 13,
|
|
271
|
+
cursor: 'pointer',
|
|
272
|
+
whiteSpace: 'nowrap',
|
|
273
|
+
overflow: 'hidden',
|
|
274
|
+
textOverflow: 'ellipsis',
|
|
275
|
+
}}
|
|
276
|
+
// mousedown keeps the grid from stealing focus mid-switch.
|
|
277
|
+
onMouseDown={(e) => {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
switchTo(tab.id);
|
|
280
|
+
}}
|
|
281
|
+
onDoubleClick={() => startRename(tab)}
|
|
282
|
+
onContextMenu={(e) => {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
setMenu({ x: e.clientX, y: e.clientY, sheetId: tab.id });
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{tab.name}
|
|
288
|
+
</button>
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
style={ADD_BTN_STYLE}
|
|
294
|
+
aria-label="Add sheet"
|
|
295
|
+
title="Add sheet"
|
|
296
|
+
data-testid="cs-tab-add"
|
|
297
|
+
onMouseDown={(e) => {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
addSheet();
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
+
|
|
303
|
+
</button>
|
|
304
|
+
|
|
305
|
+
{menu && (
|
|
306
|
+
<div
|
|
307
|
+
style={{ ...MENU_STYLE, left: menu.x, top: menu.y }}
|
|
308
|
+
data-testid="cs-tab-menu"
|
|
309
|
+
role="menu"
|
|
310
|
+
// Keep clicks inside the menu from closing it via the window handler.
|
|
311
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
312
|
+
>
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
role="menuitem"
|
|
316
|
+
style={MENU_ITEM_STYLE}
|
|
317
|
+
data-testid="cs-tab-menu-rename"
|
|
318
|
+
onMouseDown={(e) => {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
const tab = tabs.find((t) => t.id === menu.sheetId);
|
|
321
|
+
if (tab) startRename(tab);
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
Rename
|
|
325
|
+
</button>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
role="menuitem"
|
|
329
|
+
style={{
|
|
330
|
+
...MENU_ITEM_STYLE,
|
|
331
|
+
opacity: visible.length <= 1 ? 0.5 : 1,
|
|
332
|
+
cursor: visible.length <= 1 ? 'not-allowed' : 'pointer',
|
|
333
|
+
}}
|
|
334
|
+
data-testid="cs-tab-menu-delete"
|
|
335
|
+
onMouseDown={(e) => {
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
deleteSheet(menu.sheetId);
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
Delete
|
|
341
|
+
</button>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusBar — built-in status bar for `<CasualSheets chrome>`.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained: reads the active selection through `CasualSheetsAPI` and shows
|
|
5
|
+
* Excel-style aggregates over it (Average / Count / Numerical Count / Min / Max
|
|
6
|
+
* / Sum), plus a zoom control on the right (− / level / +, click the level to
|
|
7
|
+
* reset to 100%). Drives the editor only through the facade + commands.
|
|
8
|
+
*
|
|
9
|
+
* Count = non-empty cells (any type); Numerical Count = numeric cells; the
|
|
10
|
+
* numeric aggregates (Average/Min/Max/Sum) run over the numeric cells only —
|
|
11
|
+
* matching Excel's status-bar semantics.
|
|
12
|
+
*
|
|
13
|
+
* Zoom dispatches the OPERATION `sheet.operation.set-zoom-ratio` (not the
|
|
14
|
+
* `set-zoom-ratio` / `change-zoom-ratio` commands, which bail when Univer's
|
|
15
|
+
* always-present formula-bar editor unit reports visible). The operation's
|
|
16
|
+
* render controller registers in sheets-ui's `onRendered` lifecycle — i.e. a
|
|
17
|
+
* frame or two after `onReady` — so the buttons just no-op until the grid has
|
|
18
|
+
* painted; in practice the user can't click that fast.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useState, type CSSProperties } from 'react';
|
|
22
|
+
import { ICommandService } from '@univerjs/core';
|
|
23
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
24
|
+
|
|
25
|
+
interface Stats {
|
|
26
|
+
/** Non-empty cells (any type). */
|
|
27
|
+
count: number;
|
|
28
|
+
/** Numeric cells. */
|
|
29
|
+
numCount: number;
|
|
30
|
+
sum: number;
|
|
31
|
+
avg: number;
|
|
32
|
+
min: number;
|
|
33
|
+
max: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readStats(api: CasualSheetsAPI): Stats | null {
|
|
37
|
+
const sel = api.getSelection();
|
|
38
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
39
|
+
if (!sel || !sheet) return null;
|
|
40
|
+
const { startRow, startColumn, endRow, endColumn } = sel.range;
|
|
41
|
+
// Single cell → nothing to aggregate (matches Excel).
|
|
42
|
+
if (startRow === endRow && startColumn === endColumn) return null;
|
|
43
|
+
const values = sheet.getRange(sel.range).getValues?.() as unknown[][] | undefined;
|
|
44
|
+
if (!values) return null;
|
|
45
|
+
let count = 0;
|
|
46
|
+
let numCount = 0;
|
|
47
|
+
let sum = 0;
|
|
48
|
+
let min = Infinity;
|
|
49
|
+
let max = -Infinity;
|
|
50
|
+
for (const row of values) {
|
|
51
|
+
for (const v of row) {
|
|
52
|
+
if (v == null || v === '') continue;
|
|
53
|
+
count += 1;
|
|
54
|
+
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
55
|
+
numCount += 1;
|
|
56
|
+
sum += v;
|
|
57
|
+
if (v < min) min = v;
|
|
58
|
+
if (v > max) max = v;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (count === 0) return null;
|
|
63
|
+
return { count, numCount, sum, avg: numCount ? sum / numCount : 0, min, max };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Current zoom as a fraction (1 = 100%). Reads the live worksheet config via
|
|
67
|
+
* the facade's `getSheet()` escape hatch; falls back to 1. */
|
|
68
|
+
function readZoom(api: CasualSheetsAPI): number {
|
|
69
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
const ws = (sheet as any)?.getSheet?.();
|
|
72
|
+
const z = ws?.getConfig?.().zoomRatio;
|
|
73
|
+
return typeof z === 'number' && z > 0 ? z : 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Trim float noise without locking to a fixed precision.
|
|
77
|
+
function fmt(n: number): string {
|
|
78
|
+
return Number(n.toFixed(10)).toLocaleString();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const BAR_STYLE: CSSProperties = {
|
|
82
|
+
display: 'flex',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'flex-end',
|
|
85
|
+
gap: 16,
|
|
86
|
+
height: 24,
|
|
87
|
+
padding: '0 12px',
|
|
88
|
+
borderTop: '1px solid var(--cs-chrome-border, rgba(0,0,0,0.12))',
|
|
89
|
+
background: 'var(--cs-chrome-bg, #f8f9fa)',
|
|
90
|
+
color: 'var(--cs-chrome-muted, #4b5563)',
|
|
91
|
+
flex: '0 0 auto',
|
|
92
|
+
font: 'inherit',
|
|
93
|
+
fontSize: 12,
|
|
94
|
+
userSelect: 'none',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ZOOM_GROUP_STYLE: CSSProperties = {
|
|
98
|
+
display: 'flex',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
gap: 4,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const ZOOM_BTN_STYLE: CSSProperties = {
|
|
104
|
+
width: 20,
|
|
105
|
+
height: 18,
|
|
106
|
+
border: 'none',
|
|
107
|
+
borderRadius: 4,
|
|
108
|
+
background: 'transparent',
|
|
109
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
110
|
+
cursor: 'pointer',
|
|
111
|
+
fontSize: 14,
|
|
112
|
+
lineHeight: 1,
|
|
113
|
+
padding: 0,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const ZOOM_LEVEL_STYLE: CSSProperties = {
|
|
117
|
+
minWidth: 40,
|
|
118
|
+
height: 18,
|
|
119
|
+
border: 'none',
|
|
120
|
+
borderRadius: 4,
|
|
121
|
+
background: 'transparent',
|
|
122
|
+
color: 'var(--cs-chrome-muted, #4b5563)',
|
|
123
|
+
cursor: 'pointer',
|
|
124
|
+
font: 'inherit',
|
|
125
|
+
fontSize: 12,
|
|
126
|
+
textAlign: 'center',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export interface StatusBarProps {
|
|
130
|
+
api: CasualSheetsAPI | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function StatusBar({ api }: StatusBarProps) {
|
|
134
|
+
const [stats, setStats] = useState<Stats | null>(null);
|
|
135
|
+
const [zoom, setZoom] = useState(1);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!api) return;
|
|
139
|
+
const refresh = () => {
|
|
140
|
+
setStats(readStats(api));
|
|
141
|
+
setZoom(readZoom(api));
|
|
142
|
+
};
|
|
143
|
+
refresh();
|
|
144
|
+
const injector = (api.univer as unknown as { _injector?: { get(t: unknown): unknown } })
|
|
145
|
+
._injector;
|
|
146
|
+
const cmd = injector?.get(ICommandService) as
|
|
147
|
+
| { onCommandExecuted: (cb: () => void) => { dispose: () => void } }
|
|
148
|
+
| undefined;
|
|
149
|
+
const sub = cmd?.onCommandExecuted(() => refresh());
|
|
150
|
+
return () => sub?.dispose();
|
|
151
|
+
}, [api]);
|
|
152
|
+
|
|
153
|
+
// Drive the zoom OPERATION directly — see file header for why not the commands.
|
|
154
|
+
const setZoomRatio = (ratio: number) => {
|
|
155
|
+
const wb = api?.univer.getActiveWorkbook();
|
|
156
|
+
const sheet = wb?.getActiveSheet();
|
|
157
|
+
if (!wb || !sheet) return;
|
|
158
|
+
const clamped = Math.min(4, Math.max(0.1, ratio));
|
|
159
|
+
void api?.executeCommand('sheet.operation.set-zoom-ratio', {
|
|
160
|
+
unitId: wb.getId(),
|
|
161
|
+
subUnitId: sheet.getSheetId(),
|
|
162
|
+
zoomRatio: clamped,
|
|
163
|
+
});
|
|
164
|
+
// Optimistic local update — the onCommandExecuted refresh will reconcile.
|
|
165
|
+
setZoom(clamped);
|
|
166
|
+
};
|
|
167
|
+
const zoomBy = (delta: number) => setZoomRatio(Math.round((zoom + delta) * 100) / 100);
|
|
168
|
+
const resetZoom = () => setZoomRatio(1);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div style={BAR_STYLE} data-testid="casual-sheets-status-bar">
|
|
172
|
+
{stats && (
|
|
173
|
+
<>
|
|
174
|
+
{stats.numCount > 0 && <span data-stat="average">Average: {fmt(stats.avg)}</span>}
|
|
175
|
+
<span data-stat="count">Count: {stats.count}</span>
|
|
176
|
+
{stats.numCount > 0 && (
|
|
177
|
+
<>
|
|
178
|
+
<span data-stat="num-count">Numerical Count: {stats.numCount}</span>
|
|
179
|
+
<span data-stat="min">Min: {fmt(stats.min)}</span>
|
|
180
|
+
<span data-stat="max">Max: {fmt(stats.max)}</span>
|
|
181
|
+
<span data-stat="sum">Sum: {fmt(stats.sum)}</span>
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
<span style={ZOOM_GROUP_STYLE} data-testid="cs-zoom">
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
style={ZOOM_BTN_STYLE}
|
|
190
|
+
aria-label="Zoom out"
|
|
191
|
+
title="Zoom out"
|
|
192
|
+
data-testid="cs-zoom-out"
|
|
193
|
+
disabled={!api}
|
|
194
|
+
onMouseDown={(e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
zoomBy(-0.1);
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
−
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
style={ZOOM_LEVEL_STYLE}
|
|
204
|
+
aria-label="Reset zoom to 100%"
|
|
205
|
+
title="Reset zoom to 100%"
|
|
206
|
+
data-testid="cs-zoom-level"
|
|
207
|
+
disabled={!api}
|
|
208
|
+
onMouseDown={(e) => {
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
resetZoom();
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
{Math.round(zoom * 100)}%
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
style={ZOOM_BTN_STYLE}
|
|
218
|
+
aria-label="Zoom in"
|
|
219
|
+
title="Zoom in"
|
|
220
|
+
data-testid="cs-zoom-in"
|
|
221
|
+
disabled={!api}
|
|
222
|
+
onMouseDown={(e) => {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
zoomBy(0.1);
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
+
|
|
228
|
+
</button>
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|