@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,493 @@
|
|
|
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
|
+
* InsertFunctionDialog — the SDK chrome's built-in Insert Function picker.
|
|
19
|
+
*
|
|
20
|
+
* A searchable, category-grouped list of common spreadsheet functions. Picking a
|
|
21
|
+
* function seeds the active cell with `=NAME(` so the user can type arguments,
|
|
22
|
+
* then focuses the grid. Applying is grounded in the real Sheets FUniver facade:
|
|
23
|
+
* - `api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange()` gives
|
|
24
|
+
* the live FRange (see FormatCellsDialog `activeRange`);
|
|
25
|
+
* - `FRange.setValue('=NAME(')` writes the formula stub into the range's
|
|
26
|
+
* top-left cell (verified in `@univerjs/sheets/lib/types/facade/f-range.d.ts`
|
|
27
|
+
* L814 `setValue(value: CellValue | ICellData): FRange`);
|
|
28
|
+
* - `FRange.getA1Notation()` (f-range.d.ts L1220) labels the target cell;
|
|
29
|
+
* - `api.focus()` returns keyboard focus to the grid for argument entry.
|
|
30
|
+
*
|
|
31
|
+
* Note on the caret: the installed FRange facade has no "enter cell edit mode
|
|
32
|
+
* with the caret parked after the open-paren" method — `setValue` commits the
|
|
33
|
+
* cell content and Univer re-parses it. So on choose we write `=NAME(` and focus
|
|
34
|
+
* the grid; the user double-clicks / F2s to continue typing arguments. That's the
|
|
35
|
+
* closest real, installed behaviour (recorded in the dialog's limitations).
|
|
36
|
+
*
|
|
37
|
+
* Mounted by `<DialogHost>` when `openDialog('insert-function')` is called and no
|
|
38
|
+
* host override is registered.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { useMemo, useState, type CSSProperties } from 'react';
|
|
42
|
+
import type { DialogComponentProps } from './extensions';
|
|
43
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
44
|
+
import { Dialog } from './Dialog';
|
|
45
|
+
import {
|
|
46
|
+
DIALOG_BTN_PRIMARY_STYLE,
|
|
47
|
+
DIALOG_BTN_SECONDARY_STYLE,
|
|
48
|
+
DIALOG_INPUT_STYLE,
|
|
49
|
+
} from './dialog-styles';
|
|
50
|
+
|
|
51
|
+
/** A single function entry in the picker. */
|
|
52
|
+
interface FunctionEntry {
|
|
53
|
+
/** Canonical function name (upper-case), inserted verbatim. */
|
|
54
|
+
name: string;
|
|
55
|
+
/** Category bucket for grouping. */
|
|
56
|
+
category: string;
|
|
57
|
+
/** One-line description shown under the list / in the preview. */
|
|
58
|
+
description: string;
|
|
59
|
+
/** Argument-signature hint shown in the preview, e.g. `SUM(value1, [value2, …])`. */
|
|
60
|
+
syntax: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The common-function catalog. Deliberately a focused, Google-Sheets-style
|
|
65
|
+
* "common functions" set rather than the full library — this is a quick-insert
|
|
66
|
+
* picker, not a reference. Grouped by `category`.
|
|
67
|
+
*/
|
|
68
|
+
const FUNCTIONS: FunctionEntry[] = [
|
|
69
|
+
// Math
|
|
70
|
+
{
|
|
71
|
+
name: 'SUM',
|
|
72
|
+
category: 'Math',
|
|
73
|
+
description: 'Sum of a set of numbers.',
|
|
74
|
+
syntax: 'SUM(value1, [value2, …])',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'PRODUCT',
|
|
78
|
+
category: 'Math',
|
|
79
|
+
description: 'Product of a set of numbers.',
|
|
80
|
+
syntax: 'PRODUCT(factor1, [factor2, …])',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'ROUND',
|
|
84
|
+
category: 'Math',
|
|
85
|
+
description: 'Round a number to a given number of places.',
|
|
86
|
+
syntax: 'ROUND(value, [places])',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'ABS',
|
|
90
|
+
category: 'Math',
|
|
91
|
+
description: 'Absolute value of a number.',
|
|
92
|
+
syntax: 'ABS(value)',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'MOD',
|
|
96
|
+
category: 'Math',
|
|
97
|
+
description: 'Remainder after division.',
|
|
98
|
+
syntax: 'MOD(dividend, divisor)',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'POWER',
|
|
102
|
+
category: 'Math',
|
|
103
|
+
description: 'A number raised to a power.',
|
|
104
|
+
syntax: 'POWER(base, exponent)',
|
|
105
|
+
},
|
|
106
|
+
// Statistical
|
|
107
|
+
{
|
|
108
|
+
name: 'AVERAGE',
|
|
109
|
+
category: 'Statistical',
|
|
110
|
+
description: 'Arithmetic mean of a set of numbers.',
|
|
111
|
+
syntax: 'AVERAGE(value1, [value2, …])',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'COUNT',
|
|
115
|
+
category: 'Statistical',
|
|
116
|
+
description: 'Count of numeric values in a range.',
|
|
117
|
+
syntax: 'COUNT(value1, [value2, …])',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'COUNTA',
|
|
121
|
+
category: 'Statistical',
|
|
122
|
+
description: 'Count of non-empty values in a range.',
|
|
123
|
+
syntax: 'COUNTA(value1, [value2, …])',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'MAX',
|
|
127
|
+
category: 'Statistical',
|
|
128
|
+
description: 'Largest value in a set.',
|
|
129
|
+
syntax: 'MAX(value1, [value2, …])',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'MIN',
|
|
133
|
+
category: 'Statistical',
|
|
134
|
+
description: 'Smallest value in a set.',
|
|
135
|
+
syntax: 'MIN(value1, [value2, …])',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'MEDIAN',
|
|
139
|
+
category: 'Statistical',
|
|
140
|
+
description: 'Median of a set of numbers.',
|
|
141
|
+
syntax: 'MEDIAN(value1, [value2, …])',
|
|
142
|
+
},
|
|
143
|
+
// Logical
|
|
144
|
+
{
|
|
145
|
+
name: 'IF',
|
|
146
|
+
category: 'Logical',
|
|
147
|
+
description: 'Return one value if a condition is true, another if false.',
|
|
148
|
+
syntax: 'IF(condition, value_if_true, value_if_false)',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'IFS',
|
|
152
|
+
category: 'Logical',
|
|
153
|
+
description: 'Test multiple conditions, return the first match.',
|
|
154
|
+
syntax: 'IFS(condition1, value1, [condition2, value2, …])',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'AND',
|
|
158
|
+
category: 'Logical',
|
|
159
|
+
description: 'True when all arguments are true.',
|
|
160
|
+
syntax: 'AND(logical1, [logical2, …])',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'OR',
|
|
164
|
+
category: 'Logical',
|
|
165
|
+
description: 'True when any argument is true.',
|
|
166
|
+
syntax: 'OR(logical1, [logical2, …])',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'IFERROR',
|
|
170
|
+
category: 'Logical',
|
|
171
|
+
description: 'Return a fallback when a formula errors.',
|
|
172
|
+
syntax: 'IFERROR(value, value_if_error)',
|
|
173
|
+
},
|
|
174
|
+
// Lookup
|
|
175
|
+
{
|
|
176
|
+
name: 'VLOOKUP',
|
|
177
|
+
category: 'Lookup',
|
|
178
|
+
description: 'Search a column for a key, return a value from the same row.',
|
|
179
|
+
syntax: 'VLOOKUP(search_key, range, index, [is_sorted])',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'HLOOKUP',
|
|
183
|
+
category: 'Lookup',
|
|
184
|
+
description: 'Search a row for a key, return a value from the same column.',
|
|
185
|
+
syntax: 'HLOOKUP(search_key, range, index, [is_sorted])',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'INDEX',
|
|
189
|
+
category: 'Lookup',
|
|
190
|
+
description: 'Value at a given row/column offset in a range.',
|
|
191
|
+
syntax: 'INDEX(reference, [row], [column])',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'MATCH',
|
|
195
|
+
category: 'Lookup',
|
|
196
|
+
description: 'Position of a value within a range.',
|
|
197
|
+
syntax: 'MATCH(search_key, range, [search_type])',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'XLOOKUP',
|
|
201
|
+
category: 'Lookup',
|
|
202
|
+
description: 'Search a range and return a corresponding value.',
|
|
203
|
+
syntax: 'XLOOKUP(search_key, lookup_range, result_range)',
|
|
204
|
+
},
|
|
205
|
+
// Text
|
|
206
|
+
{
|
|
207
|
+
name: 'CONCATENATE',
|
|
208
|
+
category: 'Text',
|
|
209
|
+
description: 'Join strings end to end.',
|
|
210
|
+
syntax: 'CONCATENATE(string1, [string2, …])',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'LEFT',
|
|
214
|
+
category: 'Text',
|
|
215
|
+
description: 'Leftmost characters of a string.',
|
|
216
|
+
syntax: 'LEFT(string, [num_chars])',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'RIGHT',
|
|
220
|
+
category: 'Text',
|
|
221
|
+
description: 'Rightmost characters of a string.',
|
|
222
|
+
syntax: 'RIGHT(string, [num_chars])',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'MID',
|
|
226
|
+
category: 'Text',
|
|
227
|
+
description: 'Characters from the middle of a string.',
|
|
228
|
+
syntax: 'MID(string, start, length)',
|
|
229
|
+
},
|
|
230
|
+
{ name: 'LEN', category: 'Text', description: 'Length of a string.', syntax: 'LEN(string)' },
|
|
231
|
+
{
|
|
232
|
+
name: 'TRIM',
|
|
233
|
+
category: 'Text',
|
|
234
|
+
description: 'Remove leading/trailing/duplicate spaces.',
|
|
235
|
+
syntax: 'TRIM(string)',
|
|
236
|
+
},
|
|
237
|
+
// Date
|
|
238
|
+
{ name: 'TODAY', category: 'Date', description: "Today's date.", syntax: 'TODAY()' },
|
|
239
|
+
{ name: 'NOW', category: 'Date', description: 'Current date and time.', syntax: 'NOW()' },
|
|
240
|
+
{
|
|
241
|
+
name: 'DATE',
|
|
242
|
+
category: 'Date',
|
|
243
|
+
description: 'Build a date from year, month, day.',
|
|
244
|
+
syntax: 'DATE(year, month, day)',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'DATEDIF',
|
|
248
|
+
category: 'Date',
|
|
249
|
+
description: 'Difference between two dates in a chosen unit.',
|
|
250
|
+
syntax: 'DATEDIF(start_date, end_date, unit)',
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
/** Ordered category list, so groups render in a stable, sensible order. */
|
|
255
|
+
const CATEGORY_ORDER = ['Math', 'Statistical', 'Logical', 'Lookup', 'Text', 'Date'];
|
|
256
|
+
|
|
257
|
+
/** The active FRange, or null when there is no selection. */
|
|
258
|
+
function activeRange(api: CasualSheetsAPI) {
|
|
259
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Insert `=NAME(` into the top-left cell of the active range via the facade,
|
|
264
|
+
* then focus the grid for argument entry. Returns false when there's no
|
|
265
|
+
* selection to write into.
|
|
266
|
+
*/
|
|
267
|
+
function insertFunction(api: CasualSheetsAPI, fn: FunctionEntry): boolean {
|
|
268
|
+
const range = activeRange(api);
|
|
269
|
+
if (!range) return false;
|
|
270
|
+
|
|
271
|
+
// Zero-arg functions (TODAY / NOW) are complete on their own; for the rest,
|
|
272
|
+
// leave the open-paren so the user continues typing arguments.
|
|
273
|
+
const zeroArg = fn.syntax.endsWith('()');
|
|
274
|
+
const formula = zeroArg ? `=${fn.name}()` : `=${fn.name}(`;
|
|
275
|
+
|
|
276
|
+
// FRange.setValue writes into the range's top-left cell (f-range.d.ts L814).
|
|
277
|
+
range.setValue(formula);
|
|
278
|
+
|
|
279
|
+
// Return keyboard focus to the grid so the user can keep typing arguments.
|
|
280
|
+
api.focus();
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const SEARCH_STYLE: CSSProperties = {
|
|
285
|
+
...DIALOG_INPUT_STYLE,
|
|
286
|
+
width: '100%',
|
|
287
|
+
marginBottom: 12,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const RANGE_NOTE_STYLE: CSSProperties = {
|
|
291
|
+
fontSize: 12,
|
|
292
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
293
|
+
marginBottom: 12,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const LIST_STYLE: CSSProperties = {
|
|
297
|
+
maxHeight: 280,
|
|
298
|
+
overflow: 'auto',
|
|
299
|
+
border: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
300
|
+
borderRadius: 8,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const GROUP_HEADER_STYLE: CSSProperties = {
|
|
304
|
+
position: 'sticky',
|
|
305
|
+
top: 0,
|
|
306
|
+
padding: '6px 12px',
|
|
307
|
+
fontSize: 11,
|
|
308
|
+
fontWeight: 600,
|
|
309
|
+
letterSpacing: 0.4,
|
|
310
|
+
textTransform: 'uppercase',
|
|
311
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
312
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
313
|
+
borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
function itemStyle(selected: boolean): CSSProperties {
|
|
317
|
+
return {
|
|
318
|
+
display: 'block',
|
|
319
|
+
width: '100%',
|
|
320
|
+
textAlign: 'left',
|
|
321
|
+
padding: '8px 12px',
|
|
322
|
+
border: 'none',
|
|
323
|
+
borderBottom: '1px solid var(--cs-chrome-border, #f3f4f6)',
|
|
324
|
+
background: selected ? 'var(--cs-chrome-hover-bg, #eef6f8)' : 'transparent',
|
|
325
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
326
|
+
font: 'inherit',
|
|
327
|
+
fontSize: 13,
|
|
328
|
+
cursor: 'pointer',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const ITEM_NAME_STYLE: CSSProperties = {
|
|
333
|
+
fontWeight: 600,
|
|
334
|
+
fontFamily: 'var(--cs-chrome-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const ITEM_DESC_STYLE: CSSProperties = {
|
|
338
|
+
fontSize: 12,
|
|
339
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
340
|
+
marginTop: 2,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const PREVIEW_STYLE: CSSProperties = {
|
|
344
|
+
marginTop: 12,
|
|
345
|
+
padding: '10px 12px',
|
|
346
|
+
borderRadius: 8,
|
|
347
|
+
background: 'var(--cs-chrome-subtle-bg, #f6f8fa)',
|
|
348
|
+
fontSize: 12,
|
|
349
|
+
color: 'var(--cs-chrome-fg, #201f1e)',
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const PREVIEW_SYNTAX_STYLE: CSSProperties = {
|
|
353
|
+
fontFamily: 'var(--cs-chrome-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
354
|
+
fontWeight: 600,
|
|
355
|
+
marginBottom: 4,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export function InsertFunctionDialog({ api, onClose }: DialogComponentProps) {
|
|
359
|
+
const [query, setQuery] = useState('');
|
|
360
|
+
const [selectedName, setSelectedName] = useState<string | null>(null);
|
|
361
|
+
|
|
362
|
+
// Label the target cell once for the header hint.
|
|
363
|
+
const rangeLabel = useMemo(() => {
|
|
364
|
+
const fRange = activeRange(api) as unknown as { getA1Notation?: () => string } | null;
|
|
365
|
+
return fRange?.getA1Notation?.() ?? null;
|
|
366
|
+
}, [api]);
|
|
367
|
+
|
|
368
|
+
const hasSelection = activeRange(api) !== null;
|
|
369
|
+
|
|
370
|
+
// Filter by name / description / category, case-insensitive.
|
|
371
|
+
const filtered = useMemo(() => {
|
|
372
|
+
const q = query.trim().toLowerCase();
|
|
373
|
+
if (!q) return FUNCTIONS;
|
|
374
|
+
return FUNCTIONS.filter(
|
|
375
|
+
(fn) =>
|
|
376
|
+
fn.name.toLowerCase().includes(q) ||
|
|
377
|
+
fn.description.toLowerCase().includes(q) ||
|
|
378
|
+
fn.category.toLowerCase().includes(q),
|
|
379
|
+
);
|
|
380
|
+
}, [query]);
|
|
381
|
+
|
|
382
|
+
// Group the filtered set by category, preserving CATEGORY_ORDER.
|
|
383
|
+
const grouped = useMemo(() => {
|
|
384
|
+
const byCat = new Map<string, FunctionEntry[]>();
|
|
385
|
+
for (const fn of filtered) {
|
|
386
|
+
const list = byCat.get(fn.category);
|
|
387
|
+
if (list) list.push(fn);
|
|
388
|
+
else byCat.set(fn.category, [fn]);
|
|
389
|
+
}
|
|
390
|
+
return CATEGORY_ORDER.filter((c) => byCat.has(c)).map((c) => ({
|
|
391
|
+
category: c,
|
|
392
|
+
items: byCat.get(c)!,
|
|
393
|
+
}));
|
|
394
|
+
}, [filtered]);
|
|
395
|
+
|
|
396
|
+
const selected = useMemo(
|
|
397
|
+
() => FUNCTIONS.find((fn) => fn.name === selectedName) ?? null,
|
|
398
|
+
[selectedName],
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const choose = (fn: FunctionEntry) => {
|
|
402
|
+
if (insertFunction(api, fn)) onClose();
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const insertSelected = () => {
|
|
406
|
+
if (selected && insertFunction(api, selected)) onClose();
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<Dialog
|
|
411
|
+
title="Insert function"
|
|
412
|
+
onClose={onClose}
|
|
413
|
+
width={460}
|
|
414
|
+
data-testid="cs-insert-function-dialog"
|
|
415
|
+
footer={
|
|
416
|
+
<>
|
|
417
|
+
<button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
|
|
418
|
+
Cancel
|
|
419
|
+
</button>
|
|
420
|
+
<button
|
|
421
|
+
type="button"
|
|
422
|
+
style={DIALOG_BTN_PRIMARY_STYLE}
|
|
423
|
+
data-testid="cs-insert-function-insert"
|
|
424
|
+
disabled={!hasSelection || !selected}
|
|
425
|
+
onClick={insertSelected}
|
|
426
|
+
>
|
|
427
|
+
Insert
|
|
428
|
+
</button>
|
|
429
|
+
</>
|
|
430
|
+
}
|
|
431
|
+
>
|
|
432
|
+
{hasSelection ? (
|
|
433
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-function-range">
|
|
434
|
+
Inserts into <strong>{rangeLabel ?? 'the active cell'}</strong>
|
|
435
|
+
</div>
|
|
436
|
+
) : (
|
|
437
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-function-no-selection">
|
|
438
|
+
Select a cell first, then reopen this dialog.
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
<input
|
|
443
|
+
style={SEARCH_STYLE}
|
|
444
|
+
data-testid="cs-insert-function-search"
|
|
445
|
+
type="search"
|
|
446
|
+
placeholder="Search functions (name, category, or description)"
|
|
447
|
+
value={query}
|
|
448
|
+
autoFocus
|
|
449
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
450
|
+
/>
|
|
451
|
+
|
|
452
|
+
<div
|
|
453
|
+
style={LIST_STYLE}
|
|
454
|
+
role="listbox"
|
|
455
|
+
aria-label="Functions"
|
|
456
|
+
data-testid="cs-insert-function-list"
|
|
457
|
+
>
|
|
458
|
+
{grouped.length === 0 && (
|
|
459
|
+
<div style={{ padding: '12px', fontSize: 13, color: 'var(--cs-chrome-muted, #605e5c)' }}>
|
|
460
|
+
No functions match “{query}”.
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
{grouped.map((group) => (
|
|
464
|
+
<div key={group.category}>
|
|
465
|
+
<div style={GROUP_HEADER_STYLE}>{group.category}</div>
|
|
466
|
+
{group.items.map((fn) => (
|
|
467
|
+
<button
|
|
468
|
+
key={fn.name}
|
|
469
|
+
type="button"
|
|
470
|
+
role="option"
|
|
471
|
+
aria-selected={selectedName === fn.name}
|
|
472
|
+
data-testid={`cs-insert-function-item-${fn.name}`}
|
|
473
|
+
style={itemStyle(selectedName === fn.name)}
|
|
474
|
+
onClick={() => setSelectedName(fn.name)}
|
|
475
|
+
onDoubleClick={() => choose(fn)}
|
|
476
|
+
>
|
|
477
|
+
<div style={ITEM_NAME_STYLE}>{fn.name}</div>
|
|
478
|
+
<div style={ITEM_DESC_STYLE}>{fn.description}</div>
|
|
479
|
+
</button>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
))}
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{selected && (
|
|
486
|
+
<div style={PREVIEW_STYLE} data-testid="cs-insert-function-preview">
|
|
487
|
+
<div style={PREVIEW_SYNTAX_STYLE}>{selected.syntax}</div>
|
|
488
|
+
<div>{selected.description}</div>
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
</Dialog>
|
|
492
|
+
);
|
|
493
|
+
}
|