@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.
@@ -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,gEAoKpB,CAAC"}
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,8DAwClB,CAAC"}
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"}
@@ -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
- const from = useRef(0);
33
- useTicker((at) => { seen.current = at; setFrame(at - from.current); }, { fps, enabled: sliding });
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 = seen.current; setFrame(0); }, [sliding, content]);
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;CACvB;AAED,4DAA4D;AAC5D,eAAO,MAAM,IAAI,SAAS,CAAC;AAE3B,eAAO,MAAM,IAAI,2DA8If,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"}
@@ -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,qEAsezB,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"}
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
- const detail = pending
327
- ? chosen?.description
328
- ?? pending.arg.description
329
- ?? i18n.t('textui.palette.needs', { command: pending.command.title, arg: pending.arg.name }, '{command} needs a {arg}')
330
- : highlighted?.description ?? highlighted?.id ?? '';
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' }), h('text', { content: `${rows.length}`, fg: 'subtle' })), h('box', { direction: 'row', gap: 1 }, h('text', {
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,iFAmEzB,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
- border: theme.border,
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.8.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.8.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
  });
@@ -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
- const from = useRef(0);
64
- useTicker((at) => { seen.current = at; setFrame(at - from.current); }, { fps, enabled: sliding });
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 = seen.current; setFrame(0); }, [sliding, content]);
79
+ useEffect(() => { from.current = null; setFrame(0); }, [sliding, content]);
68
80
 
69
81
  if (!sliding) return h('text', { content, truncate: 'end', ...rest });
70
82
 
@@ -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
- const detail = pending
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
- h('text', { content: `${rows.length}`, fg: 'subtle' })),
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
- border: theme.border,
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' }),