@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, h, useEffect, useFocusScope, useInput, useRuntime, useState, useTheme, } from '@textui/core';
|
|
1
|
+
import { defineComponent, h, stringWidth, useEffect, useFocusScope, useInput, useRuntime, useState, useTheme, } from '@textui/core';
|
|
2
2
|
import { TextInput } from '../control/index.js';
|
|
3
3
|
import { Menu } from '../navigation/index.js';
|
|
4
4
|
import { hint } from './shared.js';
|
|
@@ -19,7 +19,7 @@ import { hint } from './shared.js';
|
|
|
19
19
|
export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
20
20
|
const theme = useTheme();
|
|
21
21
|
const runtime = useRuntime();
|
|
22
|
-
const { commands, placeholder, onRun, onClose, execute = true, grouped = true, visibleRows = 8, width = 60, openAt, ...rest } = props;
|
|
22
|
+
const { commands, placeholder, onRun, onClose, execute = true, grouped = true, visibleRows = 8, width, maxWidth = 60, openAt, descriptions = 'inline', ...rest } = props;
|
|
23
23
|
const [query, setQuery] = useState('');
|
|
24
24
|
const [highlight, setHighlight] = useState(0);
|
|
25
25
|
/** The command being asked about, when the palette has drilled in. */
|
|
@@ -71,16 +71,28 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
71
71
|
: matches.map((command, i) => ({
|
|
72
72
|
id: command.id,
|
|
73
73
|
label: command.title,
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
description: command.badge ?? command.
|
|
74
|
+
// What this row does, or the state it is reporting. The category is
|
|
75
|
+
// not here: it names the *group*, so it is said once above it.
|
|
76
|
+
description: command.badge ?? command.description,
|
|
77
77
|
icon: command.icon,
|
|
78
78
|
// A row may stand for a command registered under another id, and the
|
|
79
79
|
// key a person would press belongs to that one.
|
|
80
80
|
shortcut: command.shortcut ?? app?.keybindings.forCommand(command.id)[0],
|
|
81
81
|
// A chevron, from `Menu`, for anything that will ask a question.
|
|
82
82
|
children: argumentOf(command) ? [] : undefined,
|
|
83
|
-
|
|
83
|
+
// The heading goes on the first row of each group, including the
|
|
84
|
+
// first - a group with no name over it is the one the reader has to
|
|
85
|
+
// work out from the rows in it.
|
|
86
|
+
//
|
|
87
|
+
// Sorted matches interleave the categories, so a query turns the
|
|
88
|
+
// headings off rather than repeating them: with the rows in relevance
|
|
89
|
+
// order, "Screens" over a single row is noise, and the group it claims
|
|
90
|
+
// to start is one row long.
|
|
91
|
+
...(grouped && query.trim() === ''
|
|
92
|
+
&& (i === 0 || matches[i - 1].category !== command.category)
|
|
93
|
+
&& command.category
|
|
94
|
+
? { sectionBefore: command.category }
|
|
95
|
+
: {}),
|
|
84
96
|
}));
|
|
85
97
|
const back = () => {
|
|
86
98
|
// `null` is "never mind": whatever the preview did gets undone by whoever
|
|
@@ -132,18 +144,40 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
132
144
|
const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
|
|
133
145
|
setPending({ command, arg, collected });
|
|
134
146
|
setQuery('');
|
|
135
|
-
|
|
147
|
+
/*
|
|
148
|
+
* Open on the answer that is already in force.
|
|
149
|
+
*
|
|
150
|
+
* A question about a setting is nearly always asked in order to change it
|
|
151
|
+
* *from* something, and the row that something is on is where the reader
|
|
152
|
+
* is looking. Starting at the top instead says the first option is the
|
|
153
|
+
* current one, which is wrong on every list where it is not - and it
|
|
154
|
+
* costs an extra press to get back to where you began.
|
|
155
|
+
*
|
|
156
|
+
* `default` is the argument's own word for it, and the same one the row
|
|
157
|
+
* labelled "default" already used.
|
|
158
|
+
*/
|
|
159
|
+
const startAt = (list) => {
|
|
160
|
+
const at = list.findIndex((choice) => choice.value === arg.default);
|
|
161
|
+
return at < 0 ? 0 : at;
|
|
162
|
+
};
|
|
136
163
|
if (Array.isArray(resolved)) {
|
|
137
164
|
setAsking(false);
|
|
138
|
-
|
|
165
|
+
const list = resolved.map(asChoice);
|
|
166
|
+
setChoices(list);
|
|
167
|
+
setHighlight(startAt(list));
|
|
139
168
|
return;
|
|
140
169
|
}
|
|
141
170
|
setChoices([]);
|
|
171
|
+
setHighlight(0);
|
|
142
172
|
setAsking(true);
|
|
143
173
|
// Answered either way: a `choices` function that rejects leaves the panel
|
|
144
174
|
// saying "nothing to choose", which is true of what it can offer.
|
|
145
175
|
void resolved
|
|
146
|
-
.then((list) =>
|
|
176
|
+
.then((list) => {
|
|
177
|
+
const choices = list.map(asChoice);
|
|
178
|
+
setChoices(choices);
|
|
179
|
+
setHighlight(startAt(choices));
|
|
180
|
+
})
|
|
147
181
|
.catch(() => setChoices([]))
|
|
148
182
|
.finally(() => setAsking(false));
|
|
149
183
|
};
|
|
@@ -263,12 +297,29 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
263
297
|
? chosen?.description
|
|
264
298
|
?? pending.arg.description ?? `${pending.command.title} needs a ${pending.arg.name}`
|
|
265
299
|
: highlighted?.description ?? highlighted?.id ?? '';
|
|
300
|
+
/*
|
|
301
|
+
* How wide the panel wants to be.
|
|
302
|
+
*
|
|
303
|
+
* A menu sized to a constant is a menu that is too wide for a list of
|
|
304
|
+
* one-word answers and too narrow for a list of sentences, and it is the
|
|
305
|
+
* same menu either way. So it asks for what it holds - the widest row, plus
|
|
306
|
+
* what the row draws around it - and takes `maxWidth` when that is more than
|
|
307
|
+
* there is any point having.
|
|
308
|
+
*
|
|
309
|
+
* `minWidth` keeps the search field, the hint row and the crumb from being
|
|
310
|
+
* the things that decide it: a question with two short answers still needs
|
|
311
|
+
* somewhere to type and a line saying what the keys do.
|
|
312
|
+
*/
|
|
313
|
+
const content = Math.max(...items.map((item) => rowWidth(item, descriptions)), ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]));
|
|
266
314
|
return h('box', {
|
|
267
315
|
role: 'dialog',
|
|
268
316
|
label: 'Commands',
|
|
269
317
|
border: theme.border,
|
|
270
318
|
bg: 'overlay',
|
|
271
|
-
width,
|
|
319
|
+
// A stated width is a width. Left off, it fits what it holds.
|
|
320
|
+
...(width !== undefined
|
|
321
|
+
? { width }
|
|
322
|
+
: { minWidth: Math.min(28, maxWidth), maxWidth, width: Math.min(content, maxWidth) }),
|
|
272
323
|
direction: 'column',
|
|
273
324
|
// A border is a gutter as well as a line. Without one - `paper` sets
|
|
274
325
|
// `border: 'none'` - the rows run flush to the panel edge and the last
|
|
@@ -316,6 +367,9 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
316
367
|
}), h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }), h(Menu, {
|
|
317
368
|
items,
|
|
318
369
|
visibleRows,
|
|
370
|
+
// The argument gets the last word: only it knows whether its answers are
|
|
371
|
+
// told apart by a word or by a sentence.
|
|
372
|
+
descriptions: pending?.arg.descriptions ?? descriptions,
|
|
319
373
|
interactive: false,
|
|
320
374
|
activeId: rows[index],
|
|
321
375
|
onSelect: (id) => {
|
|
@@ -413,3 +467,23 @@ function subsequenceScore(haystack, needle) {
|
|
|
413
467
|
}
|
|
414
468
|
return score;
|
|
415
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* The cells one row would like, drawn the way this menu draws it.
|
|
472
|
+
*
|
|
473
|
+
* Mirrors `Menu`'s own layout rather than guessing: the cursor's column and
|
|
474
|
+
* the gap after it, the icon when there is one, the label, and then either the
|
|
475
|
+
* description beside it or a line of its own under it. A description on its
|
|
476
|
+
* own line does not widen the row past its own indent, which is why `below`
|
|
477
|
+
* is the layout a long sentence wants.
|
|
478
|
+
*/
|
|
479
|
+
function rowWidth(item, descriptions) {
|
|
480
|
+
// The marker and its gap; a switch column when the menu has one.
|
|
481
|
+
const lead = 2 + (item.checked !== undefined ? 2 : 0)
|
|
482
|
+
+ (item.icon ? stringWidth(item.icon) + 1 : 0);
|
|
483
|
+
const label = stringWidth(item.label);
|
|
484
|
+
const trail = (item.shortcut ? stringWidth(item.shortcut) + 1 : 0) + (item.children ? 2 : 0);
|
|
485
|
+
const description = item.description ? stringWidth(item.description) : 0;
|
|
486
|
+
return descriptions === 'below'
|
|
487
|
+
? Math.max(lead + label + trail, lead + description)
|
|
488
|
+
: lead + label + (description > 0 ? description + 2 : 0) + trail;
|
|
489
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workbench-shell.d.ts","sourceRoot":"","sources":["../../src/shells/workbench-shell.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"workbench-shell.d.ts","sourceRoot":"","sources":["../../src/shells/workbench-shell.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,iFA6DzB,CAAC"}
|
|
@@ -17,6 +17,8 @@ export const WorkbenchShell = defineComponent('WorkbenchShell', (props) => {
|
|
|
17
17
|
const sidebar = useSurfaceMounted('sidebar');
|
|
18
18
|
const aside = useSurfaceMounted('aside');
|
|
19
19
|
const narrow = size.width < 90;
|
|
20
|
+
const showSidebar = Boolean(sidebar) && !sidebarCollapsed && !narrow;
|
|
21
|
+
const showAside = Boolean(aside) && asideVisible && !narrow;
|
|
20
22
|
return h('box', {
|
|
21
23
|
direction: 'column',
|
|
22
24
|
width: '100%',
|
|
@@ -25,14 +27,26 @@ export const WorkbenchShell = defineComponent('WorkbenchShell', (props) => {
|
|
|
25
27
|
fg: 'text',
|
|
26
28
|
border: theme.border,
|
|
27
29
|
...props,
|
|
28
|
-
}, h(SurfaceArea, { surface: 'header' }), h('box', { direction: 'row', flex: 1 }, h(SurfaceArea, { surface: 'rail' }),
|
|
30
|
+
}, h(SurfaceArea, { surface: 'header' }), h('box', { direction: 'row', flex: 1 }, h(SurfaceArea, { surface: 'rail' }), showSidebar
|
|
29
31
|
? h('box', {
|
|
30
32
|
width: 24,
|
|
31
33
|
border: { style: theme.border, sides: { right: true } },
|
|
32
34
|
direction: 'column',
|
|
33
35
|
padding: { left: 1 },
|
|
34
36
|
}, h(SurfaceArea, { surface: 'sidebar', flex: 1 }))
|
|
35
|
-
: null,
|
|
37
|
+
: null,
|
|
38
|
+
// A gutter separates main from the pane beside it, so it belongs on the
|
|
39
|
+
// sides that have one. Applied unconditionally it insets every screen by
|
|
40
|
+
// a cell on the left and nothing on the right - hidden under a theme
|
|
41
|
+
// that draws a frame, and plainly lopsided under one that does not.
|
|
42
|
+
h('box', {
|
|
43
|
+
flex: 1,
|
|
44
|
+
direction: 'column',
|
|
45
|
+
padding: {
|
|
46
|
+
...(showSidebar ? { left: 1 } : {}),
|
|
47
|
+
...(showAside ? { right: 1 } : {}),
|
|
48
|
+
},
|
|
49
|
+
}, h(SurfaceArea, { surface: 'main', flex: 1 }), h(SurfaceArea, { surface: 'panel' })), showAside
|
|
36
50
|
? h('box', {
|
|
37
51
|
width: 30,
|
|
38
52
|
border: { style: theme.border, sides: { left: true } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textui/widgets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The TextUI component catalog - layout, display, controls, data, overlays and charts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -40,15 +40,15 @@
|
|
|
40
40
|
"import": "./dist/index.js"
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@textui/core": "^0.1.0"
|
|
45
|
-
},
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"access": "public"
|
|
48
|
-
},
|
|
49
43
|
"scripts": {
|
|
50
44
|
"build": "tsc -p tsconfig.json",
|
|
51
45
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
52
46
|
"test": "vitest run"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@textui/core": "^0.2.0"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
}
|
package/src/control/checkbox.ts
CHANGED
|
@@ -7,12 +7,14 @@ export interface CheckboxProps extends BoxProps {
|
|
|
7
7
|
/** Neither checked nor unchecked - a parent of mixed children. */
|
|
8
8
|
indeterminate?: boolean;
|
|
9
9
|
onChange?(checked: boolean): void;
|
|
10
|
+
/** Take the keyboard on arrival. */
|
|
11
|
+
autoFocus?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export const Checkbox = defineComponent<CheckboxProps>('Checkbox', (props) => {
|
|
13
15
|
const theme = useTheme();
|
|
14
|
-
const { label, checked = false, indeterminate, onChange, disabled, ...rest } = props;
|
|
15
|
-
const focus = useFocus({ disabled });
|
|
16
|
+
const { label, checked = false, indeterminate, onChange, disabled, autoFocus, ...rest } = props;
|
|
17
|
+
const focus = useFocus({ disabled, autoFocus });
|
|
16
18
|
|
|
17
19
|
useInput(
|
|
18
20
|
(event) => {
|
|
@@ -15,12 +15,14 @@ export interface RadioGroupProps extends BoxProps {
|
|
|
15
15
|
label?: string;
|
|
16
16
|
/** Lay the options out in a row instead of a column. */
|
|
17
17
|
inline?: boolean;
|
|
18
|
+
/** Take the keyboard on arrival. */
|
|
19
|
+
autoFocus?: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export const RadioGroup = defineComponent<RadioGroupProps>('RadioGroup', (props) => {
|
|
21
23
|
const theme = useTheme();
|
|
22
|
-
const { options, value, onChange, label, inline, disabled, ...rest } = props;
|
|
23
|
-
const focus = useFocus({ disabled });
|
|
24
|
+
const { options, value, onChange, label, inline, disabled, autoFocus, ...rest } = props;
|
|
25
|
+
const focus = useFocus({ disabled, autoFocus });
|
|
24
26
|
const index = Math.max(0, options.findIndex((o) => o.value === value));
|
|
25
27
|
|
|
26
28
|
useInput(
|