@textui/widgets 0.8.0 → 0.9.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/control/text-input.d.ts.map +1 -1
- package/dist/control/text-input.js +6 -0
- package/dist/display/marquee.d.ts.map +1 -1
- package/dist/display/marquee.js +16 -3
- package/dist/navigation/menu.d.ts +9 -0
- package/dist/navigation/menu.d.ts.map +1 -1
- package/dist/navigation/menu.js +3 -3
- package/dist/overlay/command-palette.d.ts.map +1 -1
- package/dist/overlay/command-palette.js +18 -6
- package/dist/shells/workbench-shell.d.ts.map +1 -1
- package/dist/shells/workbench-shell.js +5 -1
- package/package.json +2 -2
- package/src/control/text-input.ts +6 -0
- package/src/display/marquee.ts +15 -3
- package/src/navigation/menu.ts +12 -3
- package/src/overlay/command-palette.ts +13 -2
- package/src/shells/workbench-shell.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/control/text-input.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/control/text-input.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,gEA0KpB,CAAC"}
|
|
@@ -165,5 +165,11 @@ export const TextInput = defineComponent('TextInput', (props) => {
|
|
|
165
165
|
: sliceColumns(shown, start, field),
|
|
166
166
|
fg: empty ? 'subtle' : disabled ? 'disabled' : undefined,
|
|
167
167
|
flex: 1,
|
|
168
|
+
// The name a theme styles this field's text under. Without it a theme
|
|
169
|
+
// could colour a list row and not the field a person types in - and the
|
|
170
|
+
// palette's search box, which is this component, could not be coloured
|
|
171
|
+
// at all. The placeholder and the disabled case state their own `fg`, so
|
|
172
|
+
// they keep the colours they had.
|
|
173
|
+
styleAs: 'TextInput',
|
|
168
174
|
}));
|
|
169
175
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marquee.d.ts","sourceRoot":"","sources":["../../src/display/marquee.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAa9C,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"marquee.d.ts","sourceRoot":"","sources":["../../src/display/marquee.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAa9C,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,8DAoDlB,CAAC"}
|
package/dist/display/marquee.js
CHANGED
|
@@ -29,11 +29,24 @@ export const Marquee = defineComponent('Marquee', (props) => {
|
|
|
29
29
|
// at rather than at the rate it asked for.
|
|
30
30
|
const [frame, setFrame] = useState(0);
|
|
31
31
|
const seen = useRef(0);
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// `null` until the first tick of a run, rather than 0.
|
|
33
|
+
//
|
|
34
|
+
// The ticker counts from the application's first frame, not from this
|
|
35
|
+
// component's, so a marquee that starts at frame 0 and subtracts a `from` of
|
|
36
|
+
// 0 begins at whatever number the application happens to be on - and a
|
|
37
|
+
// label scrolled to a cell nothing had travelled to yet is what opening a
|
|
38
|
+
// menu looked like. The effect below resets this, but it runs *after* the
|
|
39
|
+
// first tick, so the first tick has to be the one that starts the count.
|
|
40
|
+
const from = useRef(null);
|
|
41
|
+
useTicker((at) => {
|
|
42
|
+
seen.current = at;
|
|
43
|
+
if (from.current === null)
|
|
44
|
+
from.current = at;
|
|
45
|
+
setFrame(at - from.current);
|
|
46
|
+
}, { fps, enabled: sliding });
|
|
34
47
|
// Back to the start whenever it stops or the words change, so the row you
|
|
35
48
|
// leave is the row you saw when you arrived at it.
|
|
36
|
-
useEffect(() => { from.current =
|
|
49
|
+
useEffect(() => { from.current = null; setFrame(0); }, [sliding, content]);
|
|
37
50
|
if (!sliding)
|
|
38
51
|
return h('text', { content, truncate: 'end', ...rest });
|
|
39
52
|
const perCell = Math.max(1, Math.round(fps / speed));
|
|
@@ -54,6 +54,15 @@ export interface MenuProps extends BoxProps {
|
|
|
54
54
|
* only follows.
|
|
55
55
|
*/
|
|
56
56
|
interactive?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* What to say when nothing is selectable.
|
|
59
|
+
*
|
|
60
|
+
* A palette answers for an empty list itself: it knows *why* it is empty, and
|
|
61
|
+
* its own row can say which kind of nothing this is - nothing to choose,
|
|
62
|
+
* still asking, no match. Given one of its own it turns this off rather than
|
|
63
|
+
* putting both lines on the screen.
|
|
64
|
+
*/
|
|
65
|
+
noMatch?: string | false;
|
|
57
66
|
}
|
|
58
67
|
/** The name a theme styles a menu row under. See `LIST`. */
|
|
59
68
|
export declare const MENU = "Menu";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../src/navigation/menu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAK9D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,QAAQ,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../src/navigation/menu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAK9D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,QAAQ,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC1B;AAED,4DAA4D;AAC5D,eAAO,MAAM,IAAI,SAAS,CAAC;AAE3B,eAAO,MAAM,IAAI,2DA8If,CAAC"}
|
package/dist/navigation/menu.js
CHANGED
|
@@ -6,7 +6,7 @@ export const MENU = 'Menu';
|
|
|
6
6
|
export const Menu = defineComponent('Menu', (props) => {
|
|
7
7
|
const theme = useTheme();
|
|
8
8
|
const i18n = useI18n();
|
|
9
|
-
const { items, onSelect, visibleRows, activeId, autoFocus, interactive = true, descriptions = 'inline', ...rest } = props;
|
|
9
|
+
const { items, onSelect, visibleRows, activeId, autoFocus, interactive = true, descriptions = 'inline', noMatch, ...rest } = props;
|
|
10
10
|
const focus = useFocus({ autoFocus, disabled: !interactive });
|
|
11
11
|
const selectable = items.filter((i) => !i.disabled);
|
|
12
12
|
const [internalHighlight, setHighlight] = useState(Math.max(0, items.findIndex((i) => i.id === activeId)));
|
|
@@ -123,8 +123,8 @@ export const Menu = defineComponent('Menu', (props) => {
|
|
|
123
123
|
return item.separatorBefore
|
|
124
124
|
? [h('box', { key: `${item.id}-sep`, height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }), row]
|
|
125
125
|
: [row];
|
|
126
|
-
}), selectable.length === 0
|
|
127
|
-
? h('text', { content: ` ${i18n.t('textui.menu.noMatch', undefined, 'no matches')}`, fg: 'subtle' })
|
|
126
|
+
}), selectable.length === 0 && noMatch !== false
|
|
127
|
+
? h('text', { content: ` ${noMatch ?? i18n.t('textui.menu.noMatch', undefined, 'no matches')}`, fg: 'subtle' })
|
|
128
128
|
: null);
|
|
129
129
|
});
|
|
130
130
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../src/overlay/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAkB/F,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../src/overlay/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAkB/F,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD,eAAO,MAAM,cAAc,qEAifzB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,EAC1B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,GAAG,SAAS,CAKrB;AA2DD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAehG"}
|
|
@@ -323,11 +323,16 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
323
323
|
// It is the one line in the panel that wraps rather than truncates,
|
|
324
324
|
// because it is the one place a sentence can be read whole.
|
|
325
325
|
const chosen = pending ? offered[index] : undefined;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
// Nothing selectable is nothing to describe: the line under the list would
|
|
327
|
+
// otherwise keep the last answer's sentence, or repeat the question the row
|
|
328
|
+
// above has already asked.
|
|
329
|
+
const detail = rows.length === 0
|
|
330
|
+
? ''
|
|
331
|
+
: pending
|
|
332
|
+
? chosen?.description
|
|
333
|
+
?? pending.arg.description
|
|
334
|
+
?? i18n.t('textui.palette.needs', { command: pending.command.title, arg: pending.arg.name }, '{command} needs a {arg}')
|
|
335
|
+
: highlighted?.description ?? highlighted?.id ?? '';
|
|
331
336
|
/*
|
|
332
337
|
* How wide the panel wants to be.
|
|
333
338
|
*
|
|
@@ -419,6 +424,11 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
419
424
|
descriptions: pending?.arg.descriptions ?? descriptions,
|
|
420
425
|
interactive: false,
|
|
421
426
|
activeId: rows[index],
|
|
427
|
+
// When the answer is a fixed set, the row above already says which kind
|
|
428
|
+
// of nothing this is and this would be the second line. Searching the
|
|
429
|
+
// command list has no such row, and there the menu's own line is the one
|
|
430
|
+
// that says it.
|
|
431
|
+
noMatch: pending ? false : undefined,
|
|
422
432
|
onSelect: (id) => {
|
|
423
433
|
const at = rows.indexOf(id);
|
|
424
434
|
if (at >= 0)
|
|
@@ -428,7 +438,9 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
428
438
|
}), h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }),
|
|
429
439
|
// What the highlighted row actually is, and how to move around. A palette
|
|
430
440
|
// that shows only titles makes you run something to find out what it does.
|
|
431
|
-
h('box', { direction: 'row', gap: 1, align: 'start' }, h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }),
|
|
441
|
+
h('box', { direction: 'row', gap: 1, align: 'start' }, h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }),
|
|
442
|
+
// A zero beside an empty line is a number with nothing to count.
|
|
443
|
+
rows.length > 0 ? h('text', { content: `${rows.length}`, fg: 'subtle' }) : null), h('box', { direction: 'row', gap: 1 }, h('text', {
|
|
432
444
|
content: pending
|
|
433
445
|
? (pending.arg.choices === undefined
|
|
434
446
|
? hint(theme, [
|
|
@@ -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,iFAuEzB,CAAC"}
|
|
@@ -25,7 +25,11 @@ export const WorkbenchShell = defineComponent('WorkbenchShell', (props) => {
|
|
|
25
25
|
height: '100%',
|
|
26
26
|
bg: 'canvas',
|
|
27
27
|
fg: 'text',
|
|
28
|
-
|
|
28
|
+
// The frame is chrome, not text. A box that states an `fg` draws its
|
|
29
|
+
// border in it - which is right for `<box fg="danger" border="single">`
|
|
30
|
+
// and wrong here, where the `fg` is the application's text colour: it made
|
|
31
|
+
// every screen's outermost edge as bright as the words inside it.
|
|
32
|
+
border: { style: theme.border, color: 'border' },
|
|
29
33
|
...props,
|
|
30
34
|
}, h(SurfaceArea, { surface: 'header' }), h('box', { direction: 'row', flex: 1 }, h(SurfaceArea, { surface: 'rail' }), showSidebar
|
|
31
35
|
? h('box', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textui/widgets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The TextUI component catalog - layout, display, controls, data, overlays and charts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test": "vitest run"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@textui/core": "^0.
|
|
49
|
+
"@textui/core": "^0.9.0"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
@@ -182,6 +182,12 @@ export const TextInput = defineComponent<TextInputProps>('TextInput', (props) =>
|
|
|
182
182
|
: sliceColumns(shown, start, field),
|
|
183
183
|
fg: empty ? 'subtle' : disabled ? 'disabled' : undefined,
|
|
184
184
|
flex: 1,
|
|
185
|
+
// The name a theme styles this field's text under. Without it a theme
|
|
186
|
+
// could colour a list row and not the field a person types in - and the
|
|
187
|
+
// palette's search box, which is this component, could not be coloured
|
|
188
|
+
// at all. The placeholder and the disabled case state their own `fg`, so
|
|
189
|
+
// they keep the colours they had.
|
|
190
|
+
styleAs: 'TextInput',
|
|
185
191
|
}),
|
|
186
192
|
);
|
|
187
193
|
});
|
package/src/display/marquee.ts
CHANGED
|
@@ -60,11 +60,23 @@ export const Marquee = defineComponent<MarqueeProps>('Marquee', (props) => {
|
|
|
60
60
|
// at rather than at the rate it asked for.
|
|
61
61
|
const [frame, setFrame] = useState(0);
|
|
62
62
|
const seen = useRef(0);
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// `null` until the first tick of a run, rather than 0.
|
|
64
|
+
//
|
|
65
|
+
// The ticker counts from the application's first frame, not from this
|
|
66
|
+
// component's, so a marquee that starts at frame 0 and subtracts a `from` of
|
|
67
|
+
// 0 begins at whatever number the application happens to be on - and a
|
|
68
|
+
// label scrolled to a cell nothing had travelled to yet is what opening a
|
|
69
|
+
// menu looked like. The effect below resets this, but it runs *after* the
|
|
70
|
+
// first tick, so the first tick has to be the one that starts the count.
|
|
71
|
+
const from = useRef<number | null>(null);
|
|
72
|
+
useTicker((at) => {
|
|
73
|
+
seen.current = at;
|
|
74
|
+
if (from.current === null) from.current = at;
|
|
75
|
+
setFrame(at - from.current);
|
|
76
|
+
}, { fps, enabled: sliding });
|
|
65
77
|
// Back to the start whenever it stops or the words change, so the row you
|
|
66
78
|
// leave is the row you saw when you arrived at it.
|
|
67
|
-
useEffect(() => { from.current =
|
|
79
|
+
useEffect(() => { from.current = null; setFrame(0); }, [sliding, content]);
|
|
68
80
|
|
|
69
81
|
if (!sliding) return h('text', { content, truncate: 'end', ...rest });
|
|
70
82
|
|
package/src/navigation/menu.ts
CHANGED
|
@@ -59,6 +59,15 @@ export interface MenuProps extends BoxProps {
|
|
|
59
59
|
* only follows.
|
|
60
60
|
*/
|
|
61
61
|
interactive?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* What to say when nothing is selectable.
|
|
64
|
+
*
|
|
65
|
+
* A palette answers for an empty list itself: it knows *why* it is empty, and
|
|
66
|
+
* its own row can say which kind of nothing this is - nothing to choose,
|
|
67
|
+
* still asking, no match. Given one of its own it turns this off rather than
|
|
68
|
+
* putting both lines on the screen.
|
|
69
|
+
*/
|
|
70
|
+
noMatch?: string | false;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
/** The name a theme styles a menu row under. See `LIST`. */
|
|
@@ -69,7 +78,7 @@ export const Menu = defineComponent<MenuProps>('Menu', (props) => {
|
|
|
69
78
|
const i18n = useI18n();
|
|
70
79
|
const {
|
|
71
80
|
items, onSelect, visibleRows, activeId, autoFocus, interactive = true,
|
|
72
|
-
descriptions = 'inline', ...rest
|
|
81
|
+
descriptions = 'inline', noMatch, ...rest
|
|
73
82
|
} = props;
|
|
74
83
|
const focus = useFocus({ autoFocus, disabled: !interactive });
|
|
75
84
|
const selectable = items.filter((i) => !i.disabled);
|
|
@@ -202,8 +211,8 @@ export const Menu = defineComponent<MenuProps>('Menu', (props) => {
|
|
|
202
211
|
? [h('box', { key: `${item.id}-sep`, height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }), row]
|
|
203
212
|
: [row];
|
|
204
213
|
}),
|
|
205
|
-
selectable.length === 0
|
|
206
|
-
? h('text', { content: ` ${i18n.t('textui.menu.noMatch', undefined, 'no matches')}`, fg: 'subtle' })
|
|
214
|
+
selectable.length === 0 && noMatch !== false
|
|
215
|
+
? h('text', { content: ` ${noMatch ?? i18n.t('textui.menu.noMatch', undefined, 'no matches')}`, fg: 'subtle' })
|
|
207
216
|
: null,
|
|
208
217
|
);
|
|
209
218
|
});
|
|
@@ -439,7 +439,12 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
439
439
|
// It is the one line in the panel that wraps rather than truncates,
|
|
440
440
|
// because it is the one place a sentence can be read whole.
|
|
441
441
|
const chosen = pending ? offered[index] : undefined;
|
|
442
|
-
|
|
442
|
+
// Nothing selectable is nothing to describe: the line under the list would
|
|
443
|
+
// otherwise keep the last answer's sentence, or repeat the question the row
|
|
444
|
+
// above has already asked.
|
|
445
|
+
const detail = rows.length === 0
|
|
446
|
+
? ''
|
|
447
|
+
: pending
|
|
443
448
|
? chosen?.description
|
|
444
449
|
?? pending.arg.description
|
|
445
450
|
?? i18n.t(
|
|
@@ -549,6 +554,11 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
549
554
|
descriptions: pending?.arg.descriptions ?? descriptions,
|
|
550
555
|
interactive: false,
|
|
551
556
|
activeId: rows[index],
|
|
557
|
+
// When the answer is a fixed set, the row above already says which kind
|
|
558
|
+
// of nothing this is and this would be the second line. Searching the
|
|
559
|
+
// command list has no such row, and there the menu's own line is the one
|
|
560
|
+
// that says it.
|
|
561
|
+
noMatch: pending ? false : undefined,
|
|
552
562
|
onSelect: (id: string) => {
|
|
553
563
|
const at = rows.indexOf(id);
|
|
554
564
|
if (at >= 0) setHighlight(at);
|
|
@@ -560,7 +570,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
560
570
|
// that shows only titles makes you run something to find out what it does.
|
|
561
571
|
h('box', { direction: 'row', gap: 1, align: 'start' },
|
|
562
572
|
h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }),
|
|
563
|
-
|
|
573
|
+
// A zero beside an empty line is a number with nothing to count.
|
|
574
|
+
rows.length > 0 ? h('text', { content: `${rows.length}`, fg: 'subtle' }) : null),
|
|
564
575
|
h('box', { direction: 'row', gap: 1 },
|
|
565
576
|
h('text', {
|
|
566
577
|
content: pending
|
|
@@ -28,7 +28,11 @@ export const WorkbenchShell = defineComponent<ShellProps>('WorkbenchShell', (pro
|
|
|
28
28
|
height: '100%',
|
|
29
29
|
bg: 'canvas',
|
|
30
30
|
fg: 'text',
|
|
31
|
-
|
|
31
|
+
// The frame is chrome, not text. A box that states an `fg` draws its
|
|
32
|
+
// border in it - which is right for `<box fg="danger" border="single">`
|
|
33
|
+
// and wrong here, where the `fg` is the application's text colour: it made
|
|
34
|
+
// every screen's outermost edge as bright as the words inside it.
|
|
35
|
+
border: { style: theme.border, color: 'border' },
|
|
32
36
|
...props,
|
|
33
37
|
},
|
|
34
38
|
h(SurfaceArea, { surface: 'header' }),
|