@textui/widgets 0.1.0 → 0.2.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/README.md +17 -15
- package/dist/control/checkbox.d.ts +2 -0
- package/dist/control/checkbox.d.ts.map +1 -1
- package/dist/control/checkbox.js +2 -2
- package/dist/control/radio-group.d.ts +2 -0
- package/dist/control/radio-group.d.ts.map +1 -1
- package/dist/control/radio-group.js +2 -2
- package/dist/control/text-area.d.ts +15 -2
- package/dist/control/text-area.d.ts.map +1 -1
- package/dist/control/text-area.js +402 -22
- package/dist/control/text-input.d.ts.map +1 -1
- package/dist/control/text-input.js +40 -0
- package/dist/data/feed.d.ts +14 -0
- package/dist/data/feed.d.ts.map +1 -1
- package/dist/data/feed.js +28 -2
- package/dist/data/list.d.ts +60 -7
- package/dist/data/list.d.ts.map +1 -1
- package/dist/data/list.js +66 -10
- package/dist/data/markdown-view.d.ts.map +1 -1
- package/dist/data/markdown-view.js +49 -0
- package/dist/display/color-text.d.ts +158 -0
- package/dist/display/color-text.d.ts.map +1 -0
- package/dist/display/color-text.js +246 -0
- package/dist/display/index.d.ts +1 -0
- package/dist/display/index.d.ts.map +1 -1
- package/dist/display/index.js +3 -0
- package/dist/layout/divider.d.ts +6 -1
- package/dist/layout/divider.d.ts.map +1 -1
- package/dist/layout/divider.js +5 -5
- package/dist/layout/scroll-view.d.ts +10 -0
- package/dist/layout/scroll-view.d.ts.map +1 -1
- package/dist/layout/scroll-view.js +2 -2
- package/dist/navigation/menu.d.ts +24 -0
- package/dist/navigation/menu.d.ts.map +1 -1
- package/dist/navigation/menu.js +58 -15
- package/dist/overlay/command-palette.d.ts +32 -1
- package/dist/overlay/command-palette.d.ts.map +1 -1
- package/dist/overlay/command-palette.js +84 -10
- package/dist/shells/workbench-shell.d.ts.map +1 -1
- package/dist/shells/workbench-shell.js +16 -2
- package/package.json +8 -8
- package/src/control/checkbox.ts +4 -2
- package/src/control/radio-group.ts +4 -2
- package/src/control/text-area.ts +394 -31
- package/src/control/text-input.ts +38 -1
- package/src/data/feed.ts +44 -1
- package/src/data/list.ts +125 -18
- package/src/data/markdown-view.ts +56 -0
- package/src/display/color-text.ts +386 -0
- package/src/display/index.ts +3 -0
- package/src/layout/divider.ts +15 -7
- package/src/layout/scroll-view.ts +12 -2
- package/src/navigation/menu.ts +89 -14
- package/src/overlay/command-palette.ts +125 -11
- package/src/shells/workbench-shell.ts +16 -3
|
@@ -2,6 +2,7 @@ import type { ArgChoice, ArgSpec, BoxProps, CommandDefinition, TextUIApp } from
|
|
|
2
2
|
import {
|
|
3
3
|
defineComponent,
|
|
4
4
|
h,
|
|
5
|
+
stringWidth,
|
|
5
6
|
useEffect,
|
|
6
7
|
useFocusScope,
|
|
7
8
|
useInput,
|
|
@@ -29,10 +30,41 @@ export interface CommandPaletteProps extends BoxProps {
|
|
|
29
30
|
onClose?(): void;
|
|
30
31
|
/** Off makes this a picker: it reports the choice and runs nothing. */
|
|
31
32
|
execute?: boolean;
|
|
32
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Group the list by `category`, with the category named above each group.
|
|
35
|
+
*
|
|
36
|
+
* Only while nothing is typed. A query sorts by relevance, which interleaves
|
|
37
|
+
* the categories - and a heading over one row is not a group.
|
|
38
|
+
*/
|
|
33
39
|
grouped?: boolean;
|
|
34
40
|
visibleRows?: number;
|
|
41
|
+
/**
|
|
42
|
+
* A fixed width, in cells.
|
|
43
|
+
*
|
|
44
|
+
* Left off, the panel is as wide as its widest row and no wider than
|
|
45
|
+
* `maxWidth` - which is what a list of five short answers wants, and what a
|
|
46
|
+
* list of five sentences needs. A number here is a number: the panel is that
|
|
47
|
+
* wide whether the rows fill it or overflow it.
|
|
48
|
+
*/
|
|
35
49
|
width?: number;
|
|
50
|
+
/**
|
|
51
|
+
* The widest the panel may grow when `width` is left off. 60 by default.
|
|
52
|
+
*
|
|
53
|
+
* There is always a limit: a description is prose, and prose has no width it
|
|
54
|
+
* stops at. Past this the rows truncate, and the row under the cursor slides
|
|
55
|
+
* what it truncated.
|
|
56
|
+
*/
|
|
57
|
+
maxWidth?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Where a row's description goes. `inline` right-aligns it beside the label;
|
|
60
|
+
* `below` gives it a line of its own.
|
|
61
|
+
*
|
|
62
|
+
* `below` for a question whose answers differ by a sentence rather than by a
|
|
63
|
+
* word - four approval modes named in two words each are told apart by the
|
|
64
|
+
* line under them, and inline that line is the half that gets truncated.
|
|
65
|
+
* Every row costs two lines, so `visibleRows` buys half as many.
|
|
66
|
+
*/
|
|
67
|
+
descriptions?: 'inline' | 'below';
|
|
36
68
|
/**
|
|
37
69
|
* Open already drilled into this command's choices.
|
|
38
70
|
*
|
|
@@ -62,7 +94,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
62
94
|
const runtime = useRuntime();
|
|
63
95
|
const {
|
|
64
96
|
commands, placeholder, onRun, onClose, execute = true,
|
|
65
|
-
grouped = true, visibleRows = 8, width = 60, openAt,
|
|
97
|
+
grouped = true, visibleRows = 8, width, maxWidth = 60, openAt,
|
|
98
|
+
descriptions = 'inline', ...rest
|
|
66
99
|
} = props;
|
|
67
100
|
|
|
68
101
|
const [query, setQuery] = useState('');
|
|
@@ -124,17 +157,28 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
124
157
|
: matches.map((command, i) => ({
|
|
125
158
|
id: command.id,
|
|
126
159
|
label: command.title,
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
description: command.badge ?? command.
|
|
160
|
+
// What this row does, or the state it is reporting. The category is
|
|
161
|
+
// not here: it names the *group*, so it is said once above it.
|
|
162
|
+
description: command.badge ?? command.description,
|
|
130
163
|
icon: command.icon,
|
|
131
164
|
// A row may stand for a command registered under another id, and the
|
|
132
165
|
// key a person would press belongs to that one.
|
|
133
166
|
shortcut: command.shortcut ?? app?.keybindings.forCommand(command.id)[0],
|
|
134
167
|
// A chevron, from `Menu`, for anything that will ask a question.
|
|
135
168
|
children: argumentOf(command) ? [] : undefined,
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
// The heading goes on the first row of each group, including the
|
|
170
|
+
// first - a group with no name over it is the one the reader has to
|
|
171
|
+
// work out from the rows in it.
|
|
172
|
+
//
|
|
173
|
+
// Sorted matches interleave the categories, so a query turns the
|
|
174
|
+
// headings off rather than repeating them: with the rows in relevance
|
|
175
|
+
// order, "Screens" over a single row is noise, and the group it claims
|
|
176
|
+
// to start is one row long.
|
|
177
|
+
...(grouped && query.trim() === ''
|
|
178
|
+
&& (i === 0 || (matches[i - 1] as CommandDefinition).category !== command.category)
|
|
179
|
+
&& command.category
|
|
180
|
+
? { sectionBefore: command.category }
|
|
181
|
+
: {}),
|
|
138
182
|
}));
|
|
139
183
|
|
|
140
184
|
const back = (): void => {
|
|
@@ -193,18 +237,42 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
193
237
|
const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
|
|
194
238
|
setPending({ command, arg, collected });
|
|
195
239
|
setQuery('');
|
|
196
|
-
|
|
240
|
+
|
|
241
|
+
/*
|
|
242
|
+
* Open on the answer that is already in force.
|
|
243
|
+
*
|
|
244
|
+
* A question about a setting is nearly always asked in order to change it
|
|
245
|
+
* *from* something, and the row that something is on is where the reader
|
|
246
|
+
* is looking. Starting at the top instead says the first option is the
|
|
247
|
+
* current one, which is wrong on every list where it is not - and it
|
|
248
|
+
* costs an extra press to get back to where you began.
|
|
249
|
+
*
|
|
250
|
+
* `default` is the argument's own word for it, and the same one the row
|
|
251
|
+
* labelled "default" already used.
|
|
252
|
+
*/
|
|
253
|
+
const startAt = (list: ArgChoice[]): number => {
|
|
254
|
+
const at = list.findIndex((choice) => choice.value === arg.default);
|
|
255
|
+
return at < 0 ? 0 : at;
|
|
256
|
+
};
|
|
257
|
+
|
|
197
258
|
if (Array.isArray(resolved)) {
|
|
198
259
|
setAsking(false);
|
|
199
|
-
|
|
260
|
+
const list = resolved.map(asChoice);
|
|
261
|
+
setChoices(list);
|
|
262
|
+
setHighlight(startAt(list));
|
|
200
263
|
return;
|
|
201
264
|
}
|
|
202
265
|
setChoices([]);
|
|
266
|
+
setHighlight(0);
|
|
203
267
|
setAsking(true);
|
|
204
268
|
// Answered either way: a `choices` function that rejects leaves the panel
|
|
205
269
|
// saying "nothing to choose", which is true of what it can offer.
|
|
206
270
|
void resolved
|
|
207
|
-
.then((list) =>
|
|
271
|
+
.then((list) => {
|
|
272
|
+
const choices = list.map(asChoice);
|
|
273
|
+
setChoices(choices);
|
|
274
|
+
setHighlight(startAt(choices));
|
|
275
|
+
})
|
|
208
276
|
.catch(() => setChoices([]))
|
|
209
277
|
.finally(() => setAsking(false));
|
|
210
278
|
};
|
|
@@ -322,12 +390,33 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
322
390
|
?? pending.arg.description ?? `${pending.command.title} needs a ${pending.arg.name}`
|
|
323
391
|
: highlighted?.description ?? highlighted?.id ?? '';
|
|
324
392
|
|
|
393
|
+
/*
|
|
394
|
+
* How wide the panel wants to be.
|
|
395
|
+
*
|
|
396
|
+
* A menu sized to a constant is a menu that is too wide for a list of
|
|
397
|
+
* one-word answers and too narrow for a list of sentences, and it is the
|
|
398
|
+
* same menu either way. So it asks for what it holds - the widest row, plus
|
|
399
|
+
* what the row draws around it - and takes `maxWidth` when that is more than
|
|
400
|
+
* there is any point having.
|
|
401
|
+
*
|
|
402
|
+
* `minWidth` keeps the search field, the hint row and the crumb from being
|
|
403
|
+
* the things that decide it: a question with two short answers still needs
|
|
404
|
+
* somewhere to type and a line saying what the keys do.
|
|
405
|
+
*/
|
|
406
|
+
const content = Math.max(
|
|
407
|
+
...items.map((item) => rowWidth(item, descriptions)),
|
|
408
|
+
...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]),
|
|
409
|
+
);
|
|
410
|
+
|
|
325
411
|
return h('box', {
|
|
326
412
|
role: 'dialog',
|
|
327
413
|
label: 'Commands',
|
|
328
414
|
border: theme.border,
|
|
329
415
|
bg: 'overlay',
|
|
330
|
-
width,
|
|
416
|
+
// A stated width is a width. Left off, it fits what it holds.
|
|
417
|
+
...(width !== undefined
|
|
418
|
+
? { width }
|
|
419
|
+
: { minWidth: Math.min(28, maxWidth), maxWidth, width: Math.min(content, maxWidth) }),
|
|
331
420
|
direction: 'column',
|
|
332
421
|
// A border is a gutter as well as a line. Without one - `paper` sets
|
|
333
422
|
// `border: 'none'` - the rows run flush to the panel edge and the last
|
|
@@ -376,6 +465,9 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
376
465
|
h(Menu, {
|
|
377
466
|
items,
|
|
378
467
|
visibleRows,
|
|
468
|
+
// The argument gets the last word: only it knows whether its answers are
|
|
469
|
+
// told apart by a word or by a sentence.
|
|
470
|
+
descriptions: pending?.arg.descriptions ?? descriptions,
|
|
379
471
|
interactive: false,
|
|
380
472
|
activeId: rows[index],
|
|
381
473
|
onSelect: (id: string) => {
|
|
@@ -483,3 +575,25 @@ function subsequenceScore(haystack: string, needle: string): number {
|
|
|
483
575
|
}
|
|
484
576
|
return score;
|
|
485
577
|
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The cells one row would like, drawn the way this menu draws it.
|
|
581
|
+
*
|
|
582
|
+
* Mirrors `Menu`'s own layout rather than guessing: the cursor's column and
|
|
583
|
+
* the gap after it, the icon when there is one, the label, and then either the
|
|
584
|
+
* description beside it or a line of its own under it. A description on its
|
|
585
|
+
* own line does not widen the row past its own indent, which is why `below`
|
|
586
|
+
* is the layout a long sentence wants.
|
|
587
|
+
*/
|
|
588
|
+
function rowWidth(item: MenuItem, descriptions: 'inline' | 'below'): number {
|
|
589
|
+
// The marker and its gap; a switch column when the menu has one.
|
|
590
|
+
const lead = 2 + (item.checked !== undefined ? 2 : 0)
|
|
591
|
+
+ (item.icon ? stringWidth(item.icon) + 1 : 0);
|
|
592
|
+
const label = stringWidth(item.label);
|
|
593
|
+
const trail = (item.shortcut ? stringWidth(item.shortcut) + 1 : 0) + (item.children ? 2 : 0);
|
|
594
|
+
const description = item.description ? stringWidth(item.description) : 0;
|
|
595
|
+
|
|
596
|
+
return descriptions === 'below'
|
|
597
|
+
? Math.max(lead + label + trail, lead + description)
|
|
598
|
+
: lead + label + (description > 0 ? description + 2 : 0) + trail;
|
|
599
|
+
}
|
|
@@ -19,6 +19,8 @@ export const WorkbenchShell = defineComponent<ShellProps>('WorkbenchShell', (pro
|
|
|
19
19
|
const sidebar = useSurfaceMounted('sidebar');
|
|
20
20
|
const aside = useSurfaceMounted('aside');
|
|
21
21
|
const narrow = size.width < 90;
|
|
22
|
+
const showSidebar = Boolean(sidebar) && !sidebarCollapsed && !narrow;
|
|
23
|
+
const showAside = Boolean(aside) && asideVisible && !narrow;
|
|
22
24
|
|
|
23
25
|
return h('box', {
|
|
24
26
|
direction: 'column',
|
|
@@ -34,7 +36,7 @@ export const WorkbenchShell = defineComponent<ShellProps>('WorkbenchShell', (pro
|
|
|
34
36
|
h('box', { direction: 'row', flex: 1 },
|
|
35
37
|
h(SurfaceArea, { surface: 'rail' }),
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
showSidebar
|
|
38
40
|
? h('box', {
|
|
39
41
|
width: 24,
|
|
40
42
|
border: { style: theme.border, sides: { right: true } },
|
|
@@ -43,11 +45,22 @@ export const WorkbenchShell = defineComponent<ShellProps>('WorkbenchShell', (pro
|
|
|
43
45
|
}, h(SurfaceArea, { surface: 'sidebar', flex: 1 }))
|
|
44
46
|
: null,
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
// A gutter separates main from the pane beside it, so it belongs on the
|
|
49
|
+
// sides that have one. Applied unconditionally it insets every screen by
|
|
50
|
+
// a cell on the left and nothing on the right - hidden under a theme
|
|
51
|
+
// that draws a frame, and plainly lopsided under one that does not.
|
|
52
|
+
h('box', {
|
|
53
|
+
flex: 1,
|
|
54
|
+
direction: 'column',
|
|
55
|
+
padding: {
|
|
56
|
+
...(showSidebar ? { left: 1 } : {}),
|
|
57
|
+
...(showAside ? { right: 1 } : {}),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
47
60
|
h(SurfaceArea, { surface: 'main', flex: 1 }),
|
|
48
61
|
h(SurfaceArea, { surface: 'panel' })),
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
showAside
|
|
51
64
|
? h('box', {
|
|
52
65
|
width: 30,
|
|
53
66
|
border: { style: theme.border, sides: { left: true } },
|