@casualoffice/sheets 0.17.0 → 0.19.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 +3339 -244
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +3298 -203
- package/dist/chrome.js.map +1 -1
- package/dist/embed/embed-runtime.js +203 -157
- package/dist/index.cjs +5971 -1572
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5961 -1548
- package/dist/index.js.map +1 -1
- package/dist/sheets.cjs +4937 -538
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.js +4945 -532
- package/dist/sheets.js.map +1 -1
- package/package.json +8 -7
- package/src/charts/ChartContextMenu.tsx +264 -0
- package/src/charts/ChartLayer.tsx +333 -0
- package/src/charts/ChartOverlay.tsx +293 -0
- package/src/charts/ChartsPanel.tsx +211 -0
- package/src/charts/FormatChartDialog.tsx +419 -0
- package/src/charts/InsertChartDialog.tsx +478 -0
- package/src/charts/build-option.ts +476 -0
- package/src/charts/charts-context.tsx +205 -0
- package/src/charts/echarts-init.ts +58 -0
- package/src/charts/hit-test.ts +130 -0
- package/src/charts/insert-chart.ts +106 -0
- package/src/charts/naming.ts +38 -0
- package/src/charts/render-to-png.ts +117 -0
- package/src/charts/resources.ts +108 -0
- package/src/charts/types.ts +239 -0
- package/src/charts/univer-dom.ts +102 -0
- package/src/chrome/CommentsPanel.tsx +427 -0
- package/src/chrome/ConditionalFormattingDialog.tsx +534 -0
- package/src/chrome/CustomSortDialog.tsx +357 -0
- package/src/chrome/DataValidationDialog.tsx +536 -0
- package/src/chrome/DeleteCellsDialog.tsx +183 -0
- package/src/chrome/GoalSeekDialog.tsx +370 -0
- package/src/chrome/HistoryPanel.tsx +319 -0
- package/src/chrome/InsertCellsDialog.tsx +185 -0
- package/src/chrome/InsertChartDialog.tsx +490 -0
- package/src/chrome/InsertFunctionDialog.tsx +493 -0
- package/src/chrome/InsertPivotDialog.tsx +488 -0
- package/src/chrome/InsertSparklineDialog.tsx +344 -0
- package/src/chrome/NameManagerDialog.tsx +378 -0
- package/src/chrome/PanelHost.tsx +55 -0
- package/src/chrome/PanelRail.tsx +90 -0
- package/src/chrome/PasteSpecialDialog.tsx +286 -0
- package/src/chrome/PivotFieldsPanel.tsx +1052 -0
- package/src/chrome/TablesPanel.tsx +301 -0
- package/src/chrome/dialog-context.tsx +24 -0
- package/src/chrome/panel-context.tsx +55 -0
- package/src/chrome/panel-registry.ts +48 -0
- package/src/sheets/CasualSheets.tsx +60 -34
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* NameManagerDialog — the SDK chrome's built-in Name Manager (defined names).
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the Data Validation / Format Cells exemplars: it reaches the workbook
|
|
21
|
+
* off the FUniver facade and drives the workbook's defined-name API to list,
|
|
22
|
+
* add, edit, and delete named ranges. Grounded in
|
|
23
|
+
* `@univerjs/sheets/lib/types/facade/f-workbook.d.ts`:
|
|
24
|
+
* - `getDefinedNames(): FDefinedName[]` (L571)
|
|
25
|
+
* - `getDefinedName(name): FDefinedName | null` (L559)
|
|
26
|
+
* - `insertDefinedName(name, formulaOrRefString): FWorkbook` (L639)
|
|
27
|
+
* - `deleteDefinedName(name): boolean` (L651)
|
|
28
|
+
* - `updateDefinedNameBuilder(param): void` (L626)
|
|
29
|
+
* and `f-defined-name.d.ts`: `FDefinedName.getName()` (L221) /
|
|
30
|
+
* `getFormulaOrRefString()` (L265) / `toBuilder()` (L385); the builder's
|
|
31
|
+
* `setName` (L44) / `setRef` (L74) / `build()` (L175). The "add from selection"
|
|
32
|
+
* ref comes from `FRange.getA1Notation(true)` (sheet-qualified, e.g.
|
|
33
|
+
* `Sheet1!A1:B2` — `f-range.d.ts` L1220), which is exactly the shape
|
|
34
|
+
* `insertDefinedName`'s `formulaOrRefString` accepts.
|
|
35
|
+
*
|
|
36
|
+
* Univer's defined-name API is unscoped by name (no id needed here): to "edit" a
|
|
37
|
+
* name we rebuild it from `getDefinedName(oldName).toBuilder()`, preserving its
|
|
38
|
+
* identity while updating name + ref, then `updateDefinedNameBuilder`.
|
|
39
|
+
*
|
|
40
|
+
* Mounted by `<DialogHost>` when `openDialog('name-manager')` is called and no
|
|
41
|
+
* host override is registered.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { useCallback, useMemo, useState, type CSSProperties } from 'react';
|
|
45
|
+
import type { DialogComponentProps } from './extensions';
|
|
46
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
47
|
+
import { Dialog } from './Dialog';
|
|
48
|
+
import {
|
|
49
|
+
DIALOG_BTN_PRIMARY_STYLE,
|
|
50
|
+
DIALOG_BTN_SECONDARY_STYLE,
|
|
51
|
+
DIALOG_FIELD_STYLE,
|
|
52
|
+
DIALOG_INPUT_STYLE,
|
|
53
|
+
DIALOG_LABEL_STYLE,
|
|
54
|
+
} from './dialog-styles';
|
|
55
|
+
|
|
56
|
+
/** Minimal shape of the FDefinedNameBuilder chain we use (grounded in f-defined-name.ts). */
|
|
57
|
+
interface FDefinedNameBuilderLike {
|
|
58
|
+
setName(name: string): FDefinedNameBuilderLike;
|
|
59
|
+
setRef(a1Notation: string): FDefinedNameBuilderLike;
|
|
60
|
+
build(): unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Minimal shape of an FDefinedName we read (grounded in f-defined-name.ts). */
|
|
64
|
+
interface FDefinedNameLike {
|
|
65
|
+
getName(): string;
|
|
66
|
+
getFormulaOrRefString(): string;
|
|
67
|
+
toBuilder(): FDefinedNameBuilderLike;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Minimal shape of the FWorkbook defined-name surface (grounded in f-workbook.d.ts). */
|
|
71
|
+
interface FWorkbookLike {
|
|
72
|
+
getDefinedNames(): FDefinedNameLike[];
|
|
73
|
+
getDefinedName(name: string): FDefinedNameLike | null;
|
|
74
|
+
insertDefinedName(name: string, formulaOrRefString: string): unknown;
|
|
75
|
+
deleteDefinedName(name: string): boolean;
|
|
76
|
+
updateDefinedNameBuilder(param: unknown): void;
|
|
77
|
+
getActiveSheet(): {
|
|
78
|
+
getActiveRange(): { getA1Notation(withSheet?: boolean): string } | null;
|
|
79
|
+
} | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface NameRow {
|
|
83
|
+
name: string;
|
|
84
|
+
refersTo: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Names must start with a letter/underscore and avoid A1-like tokens; keep it
|
|
88
|
+
* loose but block the common mistakes (spaces, leading digit, empty). */
|
|
89
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_.]*$/;
|
|
90
|
+
|
|
91
|
+
function workbook(api: CasualSheetsAPI): FWorkbookLike | null {
|
|
92
|
+
return (api.univer.getActiveWorkbook() as unknown as FWorkbookLike | null) ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readRows(api: CasualSheetsAPI): NameRow[] {
|
|
96
|
+
const wb = workbook(api);
|
|
97
|
+
if (!wb) return [];
|
|
98
|
+
return wb.getDefinedNames().map((dn) => ({
|
|
99
|
+
name: dn.getName(),
|
|
100
|
+
refersTo: dn.getFormulaOrRefString(),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Sheet-qualified A1 of the current selection, e.g. `Sheet1!A1:B2`, or ''. */
|
|
105
|
+
function selectionRef(api: CasualSheetsAPI): string {
|
|
106
|
+
const range = workbook(api)?.getActiveSheet()?.getActiveRange();
|
|
107
|
+
return range?.getA1Notation(true) ?? '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ROW_STYLE: CSSProperties = {
|
|
111
|
+
display: 'grid',
|
|
112
|
+
gridTemplateColumns: '1fr 1.3fr auto',
|
|
113
|
+
alignItems: 'center',
|
|
114
|
+
gap: 8,
|
|
115
|
+
padding: '6px 8px',
|
|
116
|
+
borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const LIST_STYLE: CSSProperties = {
|
|
120
|
+
border: '1px solid var(--cs-chrome-border, #cdd3db)',
|
|
121
|
+
borderRadius: 6,
|
|
122
|
+
maxHeight: 200,
|
|
123
|
+
overflow: 'auto',
|
|
124
|
+
marginBottom: 16,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const EMPTY_STYLE: CSSProperties = {
|
|
128
|
+
padding: '16px 8px',
|
|
129
|
+
fontSize: 13,
|
|
130
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
131
|
+
textAlign: 'center',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const REF_CELL_STYLE: CSSProperties = {
|
|
135
|
+
fontSize: 12,
|
|
136
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
137
|
+
overflow: 'hidden',
|
|
138
|
+
textOverflow: 'ellipsis',
|
|
139
|
+
whiteSpace: 'nowrap',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const ROW_ACTIONS_STYLE: CSSProperties = {
|
|
143
|
+
display: 'flex',
|
|
144
|
+
gap: 4,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const SMALL_BTN_STYLE: CSSProperties = {
|
|
148
|
+
...DIALOG_BTN_SECONDARY_STYLE,
|
|
149
|
+
height: 24,
|
|
150
|
+
padding: '0 8px',
|
|
151
|
+
fontSize: 12,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const SECTION_TITLE_STYLE: CSSProperties = {
|
|
155
|
+
fontSize: 13,
|
|
156
|
+
fontWeight: 600,
|
|
157
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
158
|
+
margin: '0 0 8px',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const ERROR_STYLE: CSSProperties = {
|
|
162
|
+
fontSize: 12,
|
|
163
|
+
color: 'var(--cs-chrome-danger, #b91c1c)',
|
|
164
|
+
marginBottom: 8,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const TWO_COL_STYLE: CSSProperties = {
|
|
168
|
+
display: 'grid',
|
|
169
|
+
gridTemplateColumns: '1fr 1.3fr',
|
|
170
|
+
gap: 8,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export function NameManagerDialog({ api, onClose }: DialogComponentProps) {
|
|
174
|
+
const [rows, setRows] = useState<NameRow[]>(() => readRows(api));
|
|
175
|
+
// When editing, this holds the ORIGINAL name of the row being edited (used to
|
|
176
|
+
// locate + update it). null means we're adding a new name.
|
|
177
|
+
const [editingOriginal, setEditingOriginal] = useState<string | null>(null);
|
|
178
|
+
const [nameInput, setNameInput] = useState('');
|
|
179
|
+
const [refInput, setRefInput] = useState('');
|
|
180
|
+
const [error, setError] = useState<string | null>(null);
|
|
181
|
+
|
|
182
|
+
const initialSelRef = useMemo(() => selectionRef(api), [api]);
|
|
183
|
+
|
|
184
|
+
const refresh = useCallback(() => setRows(readRows(api)), [api]);
|
|
185
|
+
|
|
186
|
+
const resetForm = useCallback(() => {
|
|
187
|
+
setEditingOriginal(null);
|
|
188
|
+
setNameInput('');
|
|
189
|
+
setRefInput('');
|
|
190
|
+
setError(null);
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
const startEdit = (row: NameRow) => {
|
|
194
|
+
setEditingOriginal(row.name);
|
|
195
|
+
setNameInput(row.name);
|
|
196
|
+
setRefInput(row.refersTo);
|
|
197
|
+
setError(null);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const useSelection = () => {
|
|
201
|
+
const ref = selectionRef(api);
|
|
202
|
+
if (ref) setRefInput(ref);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const remove = (row: NameRow) => {
|
|
206
|
+
const wb = workbook(api);
|
|
207
|
+
if (!wb) return;
|
|
208
|
+
wb.deleteDefinedName(row.name);
|
|
209
|
+
if (editingOriginal === row.name) resetForm();
|
|
210
|
+
refresh();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const submit = () => {
|
|
214
|
+
const wb = workbook(api);
|
|
215
|
+
if (!wb) {
|
|
216
|
+
setError('No active workbook.');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const name = nameInput.trim();
|
|
220
|
+
const ref = refInput.trim();
|
|
221
|
+
if (!name) {
|
|
222
|
+
setError('Enter a name.');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!NAME_RE.test(name)) {
|
|
226
|
+
setError('Names must start with a letter or underscore and contain no spaces.');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!ref) {
|
|
230
|
+
setError('Enter a range or formula (e.g. Sheet1!A1:B2).');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Duplicate-name guard: allow keeping the same name while editing, but block
|
|
235
|
+
// colliding with a different existing name.
|
|
236
|
+
const collides = rows.some(
|
|
237
|
+
(r) => r.name.toLowerCase() === name.toLowerCase() && r.name !== editingOriginal,
|
|
238
|
+
);
|
|
239
|
+
if (collides) {
|
|
240
|
+
setError(`A defined name "${name}" already exists.`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (editingOriginal === null) {
|
|
245
|
+
// Add.
|
|
246
|
+
wb.insertDefinedName(name, ref);
|
|
247
|
+
} else {
|
|
248
|
+
// Edit — rebuild from the existing name to preserve identity/scope, then
|
|
249
|
+
// update. If the row vanished (e.g. deleted elsewhere) fall back to insert.
|
|
250
|
+
const existing = wb.getDefinedName(editingOriginal);
|
|
251
|
+
if (existing) {
|
|
252
|
+
const param = existing.toBuilder().setName(name).setRef(ref).build();
|
|
253
|
+
wb.updateDefinedNameBuilder(param);
|
|
254
|
+
} else {
|
|
255
|
+
wb.insertDefinedName(name, ref);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
refresh();
|
|
260
|
+
resetForm();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const isEditing = editingOriginal !== null;
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Dialog
|
|
267
|
+
title="Name manager"
|
|
268
|
+
onClose={onClose}
|
|
269
|
+
width={480}
|
|
270
|
+
data-testid="cs-name-manager-dialog"
|
|
271
|
+
footer={
|
|
272
|
+
<>
|
|
273
|
+
<span style={{ flex: 1 }} />
|
|
274
|
+
<button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
|
|
275
|
+
Close
|
|
276
|
+
</button>
|
|
277
|
+
</>
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
<h3 style={SECTION_TITLE_STYLE}>Defined names</h3>
|
|
281
|
+
<div style={LIST_STYLE} data-testid="cs-name-manager-list">
|
|
282
|
+
{rows.length === 0 ? (
|
|
283
|
+
<div style={EMPTY_STYLE} data-testid="cs-name-manager-empty">
|
|
284
|
+
No defined names yet. Add one below.
|
|
285
|
+
</div>
|
|
286
|
+
) : (
|
|
287
|
+
rows.map((row) => (
|
|
288
|
+
<div key={row.name} style={ROW_STYLE} data-testid="cs-name-manager-row">
|
|
289
|
+
<span style={{ fontWeight: 500 }}>{row.name}</span>
|
|
290
|
+
<span style={REF_CELL_STYLE} title={row.refersTo}>
|
|
291
|
+
{row.refersTo}
|
|
292
|
+
</span>
|
|
293
|
+
<span style={ROW_ACTIONS_STYLE}>
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
style={SMALL_BTN_STYLE}
|
|
297
|
+
data-testid="cs-name-manager-edit"
|
|
298
|
+
onClick={() => startEdit(row)}
|
|
299
|
+
>
|
|
300
|
+
Edit
|
|
301
|
+
</button>
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
style={SMALL_BTN_STYLE}
|
|
305
|
+
data-testid="cs-name-manager-delete"
|
|
306
|
+
onClick={() => remove(row)}
|
|
307
|
+
>
|
|
308
|
+
Delete
|
|
309
|
+
</button>
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
312
|
+
))
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<h3 style={SECTION_TITLE_STYLE}>{isEditing ? 'Edit name' : 'Add a name'}</h3>
|
|
317
|
+
|
|
318
|
+
{error && (
|
|
319
|
+
<div style={ERROR_STYLE} data-testid="cs-name-manager-error">
|
|
320
|
+
{error}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
<div style={TWO_COL_STYLE}>
|
|
325
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
326
|
+
<span style={DIALOG_LABEL_STYLE}>Name</span>
|
|
327
|
+
<input
|
|
328
|
+
style={DIALOG_INPUT_STYLE}
|
|
329
|
+
data-testid="cs-name-manager-name"
|
|
330
|
+
value={nameInput}
|
|
331
|
+
placeholder="MyRange"
|
|
332
|
+
onChange={(e) => setNameInput(e.target.value)}
|
|
333
|
+
/>
|
|
334
|
+
</label>
|
|
335
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
336
|
+
<span style={DIALOG_LABEL_STYLE}>Refers to</span>
|
|
337
|
+
<input
|
|
338
|
+
style={DIALOG_INPUT_STYLE}
|
|
339
|
+
data-testid="cs-name-manager-ref"
|
|
340
|
+
value={refInput}
|
|
341
|
+
placeholder={initialSelRef || 'Sheet1!A1:B2'}
|
|
342
|
+
onChange={(e) => setRefInput(e.target.value)}
|
|
343
|
+
/>
|
|
344
|
+
</label>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
style={DIALOG_BTN_SECONDARY_STYLE}
|
|
351
|
+
data-testid="cs-name-manager-use-selection"
|
|
352
|
+
onClick={useSelection}
|
|
353
|
+
>
|
|
354
|
+
Use selection
|
|
355
|
+
</button>
|
|
356
|
+
<span style={{ flex: 1 }} />
|
|
357
|
+
{isEditing && (
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
style={DIALOG_BTN_SECONDARY_STYLE}
|
|
361
|
+
data-testid="cs-name-manager-cancel-edit"
|
|
362
|
+
onClick={resetForm}
|
|
363
|
+
>
|
|
364
|
+
Cancel
|
|
365
|
+
</button>
|
|
366
|
+
)}
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
style={DIALOG_BTN_PRIMARY_STYLE}
|
|
370
|
+
data-testid="cs-name-manager-submit"
|
|
371
|
+
onClick={submit}
|
|
372
|
+
>
|
|
373
|
+
{isEditing ? 'Update' : 'Add'}
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
</Dialog>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License").
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders the currently-open side panel's body (built-in or host-supplied),
|
|
9
|
+
* to the left of the rail. Returns null when no panel is open so the grid keeps
|
|
10
|
+
* the full width. Each panel body receives `{ api, onClose }`.
|
|
11
|
+
*/
|
|
12
|
+
import { Suspense, type CSSProperties } from 'react';
|
|
13
|
+
|
|
14
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
15
|
+
import type { ChromeExtensions } from './extensions';
|
|
16
|
+
import { usePanels } from './panel-context';
|
|
17
|
+
import { BUILT_IN_PANELS } from './panel-registry';
|
|
18
|
+
|
|
19
|
+
const asideStyle: CSSProperties = {
|
|
20
|
+
flex: '0 0 auto',
|
|
21
|
+
width: 300,
|
|
22
|
+
minWidth: 0,
|
|
23
|
+
height: '100%',
|
|
24
|
+
overflow: 'auto',
|
|
25
|
+
borderLeft: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
26
|
+
background: 'var(--cs-chrome-input-bg, #ffffff)',
|
|
27
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function PanelHost({
|
|
31
|
+
api,
|
|
32
|
+
extensions,
|
|
33
|
+
}: {
|
|
34
|
+
api: CasualSheetsAPI | null;
|
|
35
|
+
extensions?: ChromeExtensions;
|
|
36
|
+
}) {
|
|
37
|
+
const panels = usePanels();
|
|
38
|
+
const openId = panels.openPanelId;
|
|
39
|
+
if (!openId || !api) return null;
|
|
40
|
+
|
|
41
|
+
const builtIn = BUILT_IN_PANELS.find((p) => p.id === openId);
|
|
42
|
+
const host = extensions?.panels?.find((p) => p.id === openId);
|
|
43
|
+
const Body = builtIn?.component ?? host?.component;
|
|
44
|
+
if (!Body) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<aside style={asideStyle} data-testid={`cs-panel-${openId}`}>
|
|
48
|
+
{/* Panels may be lazy (e.g. Charts pulls echarts) — Suspense keeps the
|
|
49
|
+
rest of the chrome painted while the chunk loads. */}
|
|
50
|
+
<Suspense fallback={null}>
|
|
51
|
+
<Body api={api} onClose={panels.close} />
|
|
52
|
+
</Suspense>
|
|
53
|
+
</aside>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License").
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Right-edge vertical rail of panel-toggle buttons (mirrors the standalone
|
|
9
|
+
* app's PanelRail, but registry-driven and self-contained in the SDK). Built-in
|
|
10
|
+
* panels come first, then any host `extensions.panels`. Clicking a button
|
|
11
|
+
* toggles that panel through the shared panel store; the open panel's body is
|
|
12
|
+
* rendered to the left by PanelHost.
|
|
13
|
+
*/
|
|
14
|
+
import type { CSSProperties } from 'react';
|
|
15
|
+
|
|
16
|
+
import type { ChromeExtensions } from './extensions';
|
|
17
|
+
import { Icon } from './Icon';
|
|
18
|
+
import { usePanels } from './panel-context';
|
|
19
|
+
import { BUILT_IN_PANELS } from './panel-registry';
|
|
20
|
+
|
|
21
|
+
const railStyle: CSSProperties = {
|
|
22
|
+
display: 'flex',
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: 2,
|
|
26
|
+
padding: '6px 4px',
|
|
27
|
+
flex: '0 0 auto',
|
|
28
|
+
borderLeft: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
29
|
+
background: 'var(--cs-chrome-bg, #eef1f5)',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface RailEntry {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
icon: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Merge built-in panels with host-supplied ones into a single rail list. */
|
|
39
|
+
export function railEntries(extensions?: ChromeExtensions): RailEntry[] {
|
|
40
|
+
const built = BUILT_IN_PANELS.map((p) => ({ id: p.id, label: p.label, icon: p.icon }));
|
|
41
|
+
const host = (extensions?.panels ?? []).map((p) => ({
|
|
42
|
+
id: p.id,
|
|
43
|
+
label: p.title,
|
|
44
|
+
icon: p.railIcon,
|
|
45
|
+
}));
|
|
46
|
+
return [...built, ...host];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function PanelRail({ extensions }: { extensions?: ChromeExtensions }) {
|
|
50
|
+
const panels = usePanels();
|
|
51
|
+
const entries = railEntries(extensions);
|
|
52
|
+
if (entries.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<aside style={railStyle} data-testid="cs-panel-rail" aria-label="Panels" role="toolbar">
|
|
56
|
+
{entries.map((e) => {
|
|
57
|
+
const pressed = panels.openPanelId === e.id;
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
key={e.id}
|
|
61
|
+
type="button"
|
|
62
|
+
data-testid={`cs-panel-rail-${e.id}`}
|
|
63
|
+
aria-pressed={pressed}
|
|
64
|
+
aria-label={pressed ? `Hide ${e.label}` : e.label}
|
|
65
|
+
title={pressed ? `Hide ${e.label}` : e.label}
|
|
66
|
+
onClick={() => panels.toggle(e.id)}
|
|
67
|
+
style={{
|
|
68
|
+
display: 'flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
width: 30,
|
|
72
|
+
height: 30,
|
|
73
|
+
border: 'none',
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
color: pressed
|
|
77
|
+
? 'var(--cs-chrome-active-fg, #0e7490)'
|
|
78
|
+
: 'var(--cs-chrome-fg, #201f1e)',
|
|
79
|
+
background: pressed
|
|
80
|
+
? 'var(--cs-chrome-active, rgba(14,116,144,0.11))'
|
|
81
|
+
: 'transparent',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Icon name={e.icon} size={18} />
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</aside>
|
|
89
|
+
);
|
|
90
|
+
}
|