@textui/widgets 0.5.0 → 0.7.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.
Files changed (63) hide show
  1. package/dist/chart/line-chart.d.ts.map +1 -1
  2. package/dist/chart/line-chart.js +3 -2
  3. package/dist/control/select.d.ts.map +1 -1
  4. package/dist/control/select.js +4 -2
  5. package/dist/control/switch.d.ts.map +1 -1
  6. package/dist/control/switch.js +7 -2
  7. package/dist/data/index.d.ts.map +1 -1
  8. package/dist/data/index.js +3 -2
  9. package/dist/data/list.d.ts.map +1 -1
  10. package/dist/data/list.js +3 -2
  11. package/dist/data/log-viewer.d.ts.map +1 -1
  12. package/dist/data/log-viewer.js +7 -2
  13. package/dist/data/markdown-view.d.ts.map +1 -1
  14. package/dist/data/markdown-view.js +3 -2
  15. package/dist/data/pagination.d.ts.map +1 -1
  16. package/dist/data/pagination.js +3 -2
  17. package/dist/display/error-state.d.ts.map +1 -1
  18. package/dist/display/error-state.js +4 -3
  19. package/dist/form/danger-zone.d.ts.map +1 -1
  20. package/dist/form/danger-zone.js +7 -3
  21. package/dist/form/form-actions.d.ts.map +1 -1
  22. package/dist/form/form-actions.js +3 -2
  23. package/dist/navigation/menu.d.ts.map +1 -1
  24. package/dist/navigation/menu.js +3 -2
  25. package/dist/overlay/command-palette.d.ts +0 -14
  26. package/dist/overlay/command-palette.d.ts.map +1 -1
  27. package/dist/overlay/command-palette.js +127 -38
  28. package/dist/overlay/dialog.d.ts +11 -0
  29. package/dist/overlay/dialog.d.ts.map +1 -1
  30. package/dist/overlay/dialog.js +24 -1
  31. package/dist/overlay/index.d.ts.map +1 -1
  32. package/dist/overlay/index.js +11 -19
  33. package/dist/overlay/path-picker.d.ts.map +1 -1
  34. package/dist/overlay/path-picker.js +19 -11
  35. package/dist/overlay/prompt-dialog.d.ts.map +1 -1
  36. package/dist/overlay/prompt-dialog.js +5 -4
  37. package/dist/panel/index.d.ts.map +1 -1
  38. package/dist/panel/index.js +8 -7
  39. package/dist/panel/resource-panel.d.ts.map +1 -1
  40. package/dist/panel/resource-panel.js +9 -5
  41. package/dist/surface/mount-view.d.ts.map +1 -1
  42. package/dist/surface/mount-view.js +3 -2
  43. package/package.json +2 -2
  44. package/src/chart/line-chart.ts +3 -2
  45. package/src/control/select.ts +4 -2
  46. package/src/control/switch.ts +9 -2
  47. package/src/data/index.ts +3 -1
  48. package/src/data/list.ts +3 -1
  49. package/src/data/log-viewer.ts +7 -1
  50. package/src/data/markdown-view.ts +7 -1
  51. package/src/data/pagination.ts +3 -2
  52. package/src/display/error-state.ts +4 -3
  53. package/src/form/danger-zone.ts +7 -3
  54. package/src/form/form-actions.ts +5 -2
  55. package/src/navigation/menu.ts +3 -2
  56. package/src/overlay/command-palette.ts +152 -29
  57. package/src/overlay/dialog.ts +37 -1
  58. package/src/overlay/index.ts +11 -19
  59. package/src/overlay/path-picker.ts +23 -10
  60. package/src/overlay/prompt-dialog.ts +5 -4
  61. package/src/panel/index.ts +9 -7
  62. package/src/panel/resource-panel.ts +9 -4
  63. package/src/surface/mount-view.ts +3 -2
@@ -5,6 +5,7 @@ import {
5
5
  stringWidth,
6
6
  useEffect,
7
7
  useFocusScope,
8
+ useI18n,
8
9
  useInput,
9
10
  useRuntime,
10
11
  useState,
@@ -89,8 +90,19 @@ export interface CommandPaletteProps extends BoxProps {
89
90
  * That is where sub-items come from - the command says what it needs and the
90
91
  * palette asks, rather than every caller inventing its own submenu.
91
92
  */
93
+
94
+ /** The question being asked: the command, the argument, and the answers given before it. */
95
+ interface Pending {
96
+ command: CommandDefinition;
97
+ arg: ArgSpec;
98
+ /** Answers given so far, for a command that asks for more than one. */
99
+ collected: Record<string, unknown>;
100
+ }
101
+
92
102
  export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalette', (props) => {
93
103
  const theme = useTheme();
104
+ const i18n = useI18n();
105
+ const ellipsis = theme.glyphs.ellipsis;
94
106
  const runtime = useRuntime();
95
107
  const {
96
108
  commands, placeholder, onRun, onClose, execute = true,
@@ -101,12 +113,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
101
113
  const [query, setQuery] = useState('');
102
114
  const [highlight, setHighlight] = useState(0);
103
115
  /** The command being asked about, when the palette has drilled in. */
104
- const [pending, setPending] = useState<{
105
- command: CommandDefinition;
106
- arg: ArgSpec;
107
- /** Answers given so far, for a command that asks for more than one. */
108
- collected: Record<string, unknown>;
109
- } | null>(null);
116
+ const [pending, setPending] = useState<Pending | null>(null);
110
117
  const [choices, setChoices] = useState<ArgChoice[]>([]);
111
118
  /**
112
119
  * Whether the answer to "what may I choose" is still on its way.
@@ -127,7 +134,9 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
127
134
  const given = typeof commands === 'function' ? commands() : commands;
128
135
  const all = given ?? app?.commands.list({ slot: 'palette', enabledOnly: true }) ?? [];
129
136
 
130
- const matches = pending ? [] : filterCommands(all, query);
137
+ const matches = pending
138
+ ? []
139
+ : grouped && query.trim() === '' ? byCategory(all) : filterCommands(all, query);
131
140
  const offered = pending ? filterChoices(choices, query) : [];
132
141
  const rows = pending ? offered.map((choice) => choice.value) : matches.map((c) => c.id);
133
142
  const index = Math.max(0, Math.min(highlight, rows.length - 1));
@@ -142,7 +151,9 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
142
151
  // the only thing the palette itself knows about a value.
143
152
  ...(choice.description
144
153
  ? { description: choice.description }
145
- : pending.arg.default === choice.value ? { description: 'default' } : {}),
154
+ : pending.arg.default === choice.value
155
+ ? { description: i18n.t('textui.palette.default', undefined, 'default') }
156
+ : {}),
146
157
  }))
147
158
  // One row, saying which kind of nothing this is. A disabled item rather
148
159
  // than no items: the panel keeps its height, so the answer arriving does
@@ -150,8 +161,10 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
150
161
  : [{
151
162
  id: '',
152
163
  label: asking
153
- ? `Asking${theme.glyphs.ellipsis}`
154
- : (query === '' ? 'Nothing to choose' : 'No match'),
164
+ ? i18n.t('textui.palette.asking', { ellipsis }, 'Asking{ellipsis}')
165
+ : (query === ''
166
+ ? i18n.t('textui.palette.nothingToChoose', undefined, 'Nothing to choose')
167
+ : i18n.t('textui.palette.noMatch', undefined, 'No match')),
155
168
  disabled: true,
156
169
  }])
157
170
  : matches.map((command, i) => ({
@@ -186,10 +199,30 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
186
199
  : {}),
187
200
  }));
188
201
 
202
+ /** The argument answered last, when this question came after one. */
203
+ const previousOf = (asked: Pending): ArgSpec | undefined => {
204
+ const last = Object.keys(asked.collected).at(-1);
205
+ return last === undefined ? undefined : (asked.command.args ?? []).find((arg) => arg.name === last);
206
+ };
207
+
208
+ /**
209
+ * One question back.
210
+ *
211
+ * After a first question the second is asked again from the start with
212
+ * the earlier answer withdrawn: a wrong provider is corrected by choosing
213
+ * another, not by closing the whole picker and opening it again. At the
214
+ * first question, the command list.
215
+ */
189
216
  const back = (): void => {
190
217
  // `null` is "never mind": whatever the preview did gets undone by whoever
191
218
  // did it, because only the command knows what it changed.
192
219
  pending?.arg.preview?.(null);
220
+ const previous = pending ? previousOf(pending) : undefined;
221
+ if (pending && previous) {
222
+ const { [previous.name]: _withdrawn, ...rest } = pending.collected;
223
+ drillInto(pending.command, previous, rest);
224
+ return;
225
+ }
193
226
  setPending(null);
194
227
  setChoices([]);
195
228
  setQuery('');
@@ -233,13 +266,20 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
233
266
  onRun?.(id, args);
234
267
  };
235
268
 
236
- /** Ask about one command. `choices` may be a function, and may be async. */
269
+ /**
270
+ * Ask about one command. `choices` may be a function, and may be async.
271
+ *
272
+ * A resolver is handed what was answered before this argument - and an
273
+ * argument that was not asked because its `default` stood was answered by
274
+ * that default. Left out, "the models of the chosen provider" would see no
275
+ * provider exactly when there was only one to choose from.
276
+ */
237
277
  const drillInto = (
238
278
  command: CommandDefinition,
239
279
  arg: ArgSpec,
240
280
  collected: Record<string, unknown> = {},
241
281
  ): void => {
242
- const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
282
+ const resolved = typeof arg.choices === 'function' ? arg.choices(answeredBefore(command, arg, collected)) : arg.choices ?? [];
243
283
  setPending({ command, arg, collected });
244
284
  setQuery('');
245
285
 
@@ -350,8 +390,10 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
350
390
  // anybody chose to be at: the caller drilled in on their behalf, and
351
391
  // the level underneath is a list of one. Backing out to it reads as
352
392
  // "escape did nothing", and the second escape - the one that would
353
- // close it - is spent leaving a screen nobody asked to see.
354
- if (pending && !openAt) { back(); return true; }
393
+ // close it - is spent leaving a screen nobody asked to see. A question
394
+ // that came after one is different: there the level underneath is the
395
+ // earlier question, which somebody did choose to be at.
396
+ if (pending && (!openAt || previousOf(pending) !== undefined)) { back(); return true; }
355
397
  // Whatever a highlighted choice previewed has to be put back on the
356
398
  // way out, and `null` is how the command is told to undo it - only it
357
399
  // knows what it changed. `back()` did that, so closing instead of
@@ -361,7 +403,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
361
403
  onClose?.();
362
404
  return true;
363
405
  }
364
- if (event.name === 'left' && pending && query === '' && !openAt) { back(); return true; }
406
+ if (event.name === 'left' && pending && query === '' && (!openAt || previousOf(pending) !== undefined)) { back(); return true; }
365
407
  // Wrapping, both ways. A list you can walk off the end of makes you
366
408
  // check where you are before every press; one that comes round means the
367
409
  // last item is one key from the first.
@@ -382,17 +424,29 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
382
424
  );
383
425
 
384
426
  const highlighted = pending ? undefined : matches[index];
385
- const move = `${theme.glyphs.arrowUp}${theme.glyphs.arrowDown} move`;
427
+ const move = i18n.t(
428
+ 'textui.palette.hint.move',
429
+ { arrows: `${theme.glyphs.arrowUp}${theme.glyphs.arrowDown}` },
430
+ '{arrows} move',
431
+ );
432
+ const escBack = i18n.t('textui.palette.hint.back', undefined, 'esc back');
386
433
  // What the highlighted row is, in full.
387
434
  //
388
435
  // The rows themselves have to fit, so a choice's own sentence is truncated
389
436
  // there - and a sentence about what a mode does is exactly the thing a
390
437
  // person needs whole. So the line under the list follows the highlight: the
391
438
  // choice's description while there is one, the question's otherwise.
439
+ // It is the one line in the panel that wraps rather than truncates,
440
+ // because it is the one place a sentence can be read whole.
392
441
  const chosen = pending ? offered[index] : undefined;
393
442
  const detail = pending
394
443
  ? chosen?.description
395
- ?? pending.arg.description ?? `${pending.command.title} needs a ${pending.arg.name}`
444
+ ?? pending.arg.description
445
+ ?? i18n.t(
446
+ 'textui.palette.needs',
447
+ { command: pending.command.title, arg: pending.arg.name },
448
+ '{command} needs a {arg}',
449
+ )
396
450
  : highlighted?.description ?? highlighted?.id ?? '';
397
451
 
398
452
  /*
@@ -411,11 +465,18 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
411
465
  const content = Math.max(
412
466
  ...items.map((item) => rowWidth(item, descriptions)),
413
467
  ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]),
468
+ // The sentences the line under the list may show, so a question with
469
+ // three one-word answers is not a panel 28 wide wrapping a sentence
470
+ // three times under them. Capped like everything else by `maxWidth`.
471
+ ...(pending
472
+ ? [pending.arg.description ?? '', ...offered.map((choice) => choice.description ?? '')]
473
+ .map((sentence) => (sentence === '' ? 0 : stringWidth(sentence) + 6))
474
+ : []),
414
475
  );
415
476
 
416
477
  return h('box', {
417
478
  role: 'dialog',
418
- label: 'Commands',
479
+ label: i18n.t('textui.palette.label', undefined, 'Commands'),
419
480
  border: theme.border,
420
481
  bg: 'overlay',
421
482
  // A stated width is a width. Left off, it fits what it holds.
@@ -431,8 +492,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
431
492
  // where you are; "commands › id" is what the parameter happens to be
432
493
  // called, which is the author's business rather than the reader's.
433
494
  title: pending
434
- ? ` commands ${theme.glyphs.breadcrumb} ${pending.command.title} `
435
- : ' commands ',
495
+ ? ` ${i18n.t('textui.palette.title', undefined, 'commands')} ${theme.glyphs.breadcrumb} ${pending.command.title} `
496
+ : ` ${i18n.t('textui.palette.title', undefined, 'commands')} `,
436
497
  ...rest,
437
498
  },
438
499
  h(TextInput, {
@@ -457,11 +518,24 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
457
518
  const command = matches[index];
458
519
  if (command && argumentOf(command)) choose();
459
520
  },
521
+ // A typed answer's field is the answer, so its placeholder is the
522
+ // question. A list's field is a filter, and the question goes where
523
+ // there is room to say it once: here, while the line under the list
524
+ // is busy saying what each answer means; under the list, whole and
525
+ // wrapped, when the answers have nothing to say for themselves and
526
+ // that line would otherwise repeat this one, cut to whatever width
527
+ // three short answers gave the panel.
460
528
  placeholder: pending
461
- ? (pending.arg.description ?? (pending.arg.choices === undefined
462
- ? `${pending.command.title}${theme.glyphs.ellipsis}`
463
- : `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`))
464
- : (placeholder ?? `Type a command${theme.glyphs.ellipsis}`),
529
+ ? (pending.arg.choices === undefined
530
+ ? pending.arg.description ?? `${pending.command.title}${theme.glyphs.ellipsis}`
531
+ : offered.some((choice) => choice.description) && pending.arg.description
532
+ ? pending.arg.description
533
+ : i18n.t(
534
+ 'textui.palette.choose',
535
+ { command: pending.command.title.toLowerCase(), ellipsis },
536
+ 'Choose {command}{ellipsis}',
537
+ ))
538
+ : (placeholder ?? i18n.t('textui.palette.placeholder', { ellipsis }, 'Type a command{ellipsis}')),
465
539
  search: true,
466
540
  autoFocus: true,
467
541
  border: 'none',
@@ -484,18 +558,29 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
484
558
  h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }),
485
559
  // What the highlighted row actually is, and how to move around. A palette
486
560
  // that shows only titles makes you run something to find out what it does.
487
- h('box', { direction: 'row', gap: 1 },
488
- h('text', { content: detail, fg: 'muted', flex: 1, truncate: 'end' }),
561
+ h('box', { direction: 'row', gap: 1, align: 'start' },
562
+ h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }),
489
563
  h('text', { content: `${rows.length}`, fg: 'subtle' })),
490
564
  h('box', { direction: 'row', gap: 1 },
491
565
  h('text', {
492
566
  content: pending
493
567
  ? (pending.arg.choices === undefined
494
- ? hint(theme, ['type it', 'enter confirm', 'esc back'])
568
+ ? hint(theme, [
569
+ i18n.t('textui.palette.hint.type', undefined, 'type it'),
570
+ i18n.t('textui.palette.hint.confirm', undefined, 'enter confirm'),
571
+ escBack,
572
+ ])
495
573
  // Nothing to choose is nothing to press enter on, and offering it
496
574
  // is how a panel reads as broken rather than as empty.
497
- : hint(theme, rows.length > 0 ? [move, 'enter choose', 'esc back'] : ['esc back']))
498
- : hint(theme, [move, 'enter run', `${theme.glyphs.chevronRight} sub-items`, 'esc close']),
575
+ : hint(theme, rows.length > 0
576
+ ? [move, i18n.t('textui.palette.hint.choose', undefined, 'enter choose'), escBack]
577
+ : [escBack]))
578
+ : hint(theme, [
579
+ move,
580
+ i18n.t('textui.palette.hint.run', undefined, 'enter run'),
581
+ i18n.t('textui.palette.hint.subItems', { glyph: theme.glyphs.chevronRight }, '{glyph} sub-items'),
582
+ i18n.t('textui.palette.hint.close', undefined, 'esc close'),
583
+ ]),
499
584
  fg: 'subtle',
500
585
  truncate: 'end',
501
586
  })),
@@ -528,6 +613,26 @@ export function argumentOf(
528
613
  );
529
614
  }
530
615
 
616
+ /**
617
+ * What a `choices` resolver is told: the answers given, under the defaults
618
+ * of the arguments before this one that stood in for a question.
619
+ *
620
+ * Only the ones ahead of it in the declaration. An argument after this one
621
+ * has not been reached, and its default is not yet an answer to anything.
622
+ */
623
+ function answeredBefore(
624
+ command: CommandDefinition,
625
+ arg: ArgSpec,
626
+ collected: Record<string, unknown>,
627
+ ): Record<string, unknown> {
628
+ const answered: Record<string, unknown> = {};
629
+ for (const spec of command.args ?? []) {
630
+ if (spec === arg) break;
631
+ if (spec.default !== undefined) answered[spec.name] = spec.default;
632
+ }
633
+ return { ...answered, ...collected };
634
+ }
635
+
531
636
  /** The short form and the long one, as one shape. */
532
637
  function asChoice(choice: string | ArgChoice): ArgChoice {
533
638
  return typeof choice === 'string' ? { value: choice } : choice;
@@ -547,6 +652,24 @@ function filterChoices(choices: ArgChoice[], query: string): ArgChoice[] {
547
652
  .toLowerCase().includes(q));
548
653
  }
549
654
 
655
+ /**
656
+ * Each category's commands together, the categories in the order they first
657
+ * appear and the commands in the order they came.
658
+ *
659
+ * Registration order is whatever order the code happened to run in, so a
660
+ * category registered in two places would be two groups with the same name
661
+ * over them.
662
+ */
663
+ function byCategory(commands: CommandDefinition[]): CommandDefinition[] {
664
+ const groups = new Map<string | undefined, CommandDefinition[]>();
665
+ for (const command of commands) {
666
+ const group = groups.get(command.category);
667
+ if (group) group.push(command);
668
+ else groups.set(command.category, [command]);
669
+ }
670
+ return [...groups.values()].flat();
671
+ }
672
+
550
673
  export function filterCommands(commands: CommandDefinition[], query: string): CommandDefinition[] {
551
674
  const q = query.trim().toLowerCase();
552
675
  if (q === '') return commands;
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps, SemanticVariant } from '@textui/core';
2
- import { defineComponent, h, useFocusScope, useInput, useTheme } from '@textui/core';
2
+ import { defineComponent, h, useFocusScope, useI18n, useInput, useTheme } from '@textui/core';
3
3
  import { Button } from '../control/index.js';
4
4
 
5
5
  export interface DialogProps extends BoxProps {
@@ -63,3 +63,39 @@ export const Dialog = defineComponent<DialogProps>('Dialog', (props) => {
63
63
  : null,
64
64
  );
65
65
  });
66
+
67
+ export interface ConfirmDialogProps extends BoxProps {
68
+ title?: string;
69
+ message: string;
70
+ confirmLabel?: string;
71
+ cancelLabel?: string;
72
+ tone?: SemanticVariant;
73
+ onConfirm?(): void;
74
+ onCancel?(): void;
75
+ }
76
+
77
+ /** Two-button dialog behind the `confirm` helper. */
78
+ export const ConfirmDialog = defineComponent<ConfirmDialogProps>('ConfirmDialog', (props) => {
79
+ const i18n = useI18n();
80
+ const { title, message, confirmLabel, cancelLabel, tone, onConfirm, onCancel, ...rest } = props;
81
+ return h(Dialog, {
82
+ title: title ?? i18n.t('textui.confirmDialog.title', undefined, 'Confirm'),
83
+ width: 50,
84
+ actions: [
85
+ {
86
+ id: 'confirm',
87
+ label: confirmLabel ?? i18n.t('textui.confirmDialog.confirm', undefined, 'Confirm'),
88
+ tone: tone ?? 'primary',
89
+ onPress: onConfirm,
90
+ },
91
+ {
92
+ id: 'cancel',
93
+ label: cancelLabel ?? i18n.t('textui.confirmDialog.cancel', undefined, 'Cancel'),
94
+ onPress: onCancel,
95
+ },
96
+ ],
97
+ ...rest,
98
+ },
99
+ h('text', { content: message, wrap: 'word' }),
100
+ );
101
+ });
@@ -7,7 +7,7 @@ import type {
7
7
  TextUIApp,
8
8
  } from '@textui/core';
9
9
  import { CommandPalette } from './command-palette.js';
10
- import { Dialog } from './dialog.js';
10
+ import { ConfirmDialog, Dialog } from './dialog.js';
11
11
  import { LayerScope } from './layer-scope.js';
12
12
  import { PathPicker } from './path-picker.js';
13
13
  import { PromptDialog } from './prompt-dialog.js';
@@ -68,23 +68,14 @@ export function confirm(
68
68
  dismissOnEscape: true,
69
69
  onClose: () => finish(false),
70
70
  node: {
71
- component: 'Dialog',
72
- title: options.title ?? 'Confirm',
73
- width: 50,
74
- children: { component: 'text', content: options.message, wrap: 'word' },
75
- actions: [
76
- {
77
- id: 'confirm',
78
- label: options.confirmLabel ?? 'Confirm',
79
- tone: options.tone ?? 'primary',
80
- onPress: () => finish(true),
81
- },
82
- {
83
- id: 'cancel',
84
- label: options.cancelLabel ?? 'Cancel',
85
- onPress: () => finish(false),
86
- },
87
- ],
71
+ component: 'ConfirmDialog',
72
+ title: options.title,
73
+ message: options.message,
74
+ confirmLabel: options.confirmLabel,
75
+ cancelLabel: options.cancelLabel,
76
+ tone: options.tone,
77
+ onConfirm: { handler: () => finish(true) },
78
+ onCancel: { handler: () => finish(false) },
88
79
  },
89
80
  });
90
81
  });
@@ -120,7 +111,7 @@ export function prompt(
120
111
  onClose: () => finish(null),
121
112
  node: {
122
113
  component: 'PromptDialog',
123
- title: options.title ?? 'Input',
114
+ title: options.title,
124
115
  message: options.message,
125
116
  placeholder: options.placeholder,
126
117
  initialValue: options.initialValue,
@@ -222,6 +213,7 @@ export function toastPosition(anchor: ToastHostProps['anchor']): LayerPosition {
222
213
 
223
214
  export const OVERLAY_COMPONENTS: ComponentDefinition[] = [
224
215
  { component: 'Dialog', category: 'overlay', renderer: { kind: 'function', render: Dialog }, role: 'dialog', description: 'Modal box with actions; traps focus and restores it.' },
216
+ { component: 'ConfirmDialog', category: 'overlay', renderer: { kind: 'function', render: ConfirmDialog }, role: 'dialog', description: 'Two-button dialog behind the `confirm` helper.' },
225
217
  { component: 'PromptDialog', category: 'overlay', renderer: { kind: 'function', render: PromptDialog }, role: 'dialog', description: 'One-field dialog behind the `prompt` helper.' },
226
218
  { component: 'PathPicker', category: 'overlay', renderer: { kind: 'function', render: PathPicker }, role: 'dialog', description: 'Walk the resource tree and pick a file or a folder.' },
227
219
  { component: 'Tooltip', category: 'overlay', renderer: { kind: 'function', render: Tooltip }, role: 'tooltip', description: 'Small anchored hint.' },
@@ -4,6 +4,7 @@ import {
4
4
  h,
5
5
  nameOf,
6
6
  useEffect,
7
+ useI18n,
7
8
  useInput,
8
9
  useRuntime,
9
10
  useState,
@@ -41,6 +42,8 @@ export const PathPicker = defineComponent<PathPickerProps>('PathPicker', (props)
41
42
  width = 60, onPick, onCancel, ...rest
42
43
  } = props;
43
44
  const theme = useTheme();
45
+ const i18n = useI18n();
46
+ const ellipsis = theme.glyphs.ellipsis;
44
47
  const runtime = useRuntime();
45
48
  const app = runtime.app();
46
49
 
@@ -90,7 +93,7 @@ export const PathPicker = defineComponent<PathPickerProps>('PathPicker', (props)
90
93
  // child to press enter on and a hidden chord for it is a chord nobody
91
94
  // finds. It is first, since it is the answer most often wanted.
92
95
  ...(wants === 'directory'
93
- ? [{ id: '.', label: 'Use this folder', icon: theme.glyphs.check }]
96
+ ? [{ id: '.', label: i18n.t('textui.pathPicker.useFolder', undefined, 'Use this folder'), icon: theme.glyphs.check }]
94
97
  : []),
95
98
  ...(parent !== null ? [{ id: '..', label: '..', icon: theme.glyphs.chevronUp }] : []),
96
99
  ...shown.map((e) => ({
@@ -139,17 +142,23 @@ export const PathPicker = defineComponent<PathPickerProps>('PathPicker', (props)
139
142
  }, { global: true });
140
143
 
141
144
 
142
- const move = `${theme.glyphs.arrowUp}${theme.glyphs.arrowDown} move`;
145
+ const move = i18n.t(
146
+ 'textui.pathPicker.hint.move',
147
+ { arrows: `${theme.glyphs.arrowUp}${theme.glyphs.arrowDown}` },
148
+ '{arrows} move',
149
+ );
143
150
 
144
151
  return h('box', {
145
152
  role: 'dialog',
146
- label: title ?? 'Choose',
153
+ label: title ?? i18n.t('textui.pathPicker.label', undefined, 'Choose'),
147
154
  border: theme.border,
148
155
  bg: 'overlay',
149
156
  width,
150
157
  direction: 'column',
151
158
  ...(theme.border === 'none' ? { padding: { left: 1, right: 1 } } : {}),
152
- title: ` ${title ?? (wants === 'directory' ? 'choose a folder' : 'choose a file')} `,
159
+ title: ` ${title ?? (wants === 'directory'
160
+ ? i18n.t('textui.pathPicker.titleFolder', undefined, 'choose a folder')
161
+ : i18n.t('textui.pathPicker.titleFile', undefined, 'choose a file'))} `,
153
162
  ...rest,
154
163
  },
155
164
  // Where you are. A column of bare names with no header is a list you can
@@ -171,7 +180,7 @@ export const PathPicker = defineComponent<PathPickerProps>('PathPicker', (props)
171
180
  * what you want while editing one.
172
181
  */
173
182
  onEdge: (edge: 'start' | 'end') => { if (edge === 'start') up(); },
174
- placeholder: placeholder ?? `Filter${theme.glyphs.ellipsis}`,
183
+ placeholder: placeholder ?? i18n.t('textui.pathPicker.placeholder', { ellipsis }, 'Filter{ellipsis}'),
175
184
  search: true,
176
185
  autoFocus: true,
177
186
  border: 'none',
@@ -195,8 +204,10 @@ export const PathPicker = defineComponent<PathPickerProps>('PathPicker', (props)
195
204
  shown.length === 0
196
205
  ? h('text', {
197
206
  content: loading
198
- ? `Reading${theme.glyphs.ellipsis}`
199
- : (q === '' ? 'Nothing here.' : 'Nothing matches.'),
207
+ ? i18n.t('textui.pathPicker.reading', { ellipsis }, 'Reading{ellipsis}')
208
+ : (q === ''
209
+ ? i18n.t('textui.pathPicker.empty', undefined, 'Nothing here.')
210
+ : i18n.t('textui.pathPicker.noMatch', undefined, 'Nothing matches.')),
200
211
  fg: 'muted',
201
212
  })
202
213
  : null,
@@ -204,9 +215,11 @@ export const PathPicker = defineComponent<PathPickerProps>('PathPicker', (props)
204
215
  h('text', {
205
216
  content: hint(theme, [
206
217
  move,
207
- wants === 'directory' ? 'enter open' : 'enter choose',
208
- `${theme.glyphs.chevronLeft} up`,
209
- 'esc cancel',
218
+ wants === 'directory'
219
+ ? i18n.t('textui.pathPicker.hint.open', undefined, 'enter open')
220
+ : i18n.t('textui.pathPicker.hint.choose', undefined, 'enter choose'),
221
+ i18n.t('textui.pathPicker.hint.up', { glyph: theme.glyphs.chevronLeft }, '{glyph} up'),
222
+ i18n.t('textui.pathPicker.hint.cancel', undefined, 'esc cancel'),
210
223
  ]),
211
224
  fg: 'subtle',
212
225
  truncate: 'end',
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps } from '@textui/core';
2
- import { defineComponent, h, useState } from '@textui/core';
2
+ import { defineComponent, h, useI18n, useState } from '@textui/core';
3
3
  import { TextInput } from '../control/index.js';
4
4
  import { Dialog } from './dialog.js';
5
5
 
@@ -15,15 +15,16 @@ export interface PromptDialogProps extends BoxProps {
15
15
 
16
16
  export const PromptDialog = defineComponent<PromptDialogProps>('PromptDialog', (props) => {
17
17
  const { title, message, placeholder, initialValue = '', mask, onSubmit, onCancel, ...rest } = props;
18
+ const i18n = useI18n();
18
19
  const [value, setValue] = useState(initialValue);
19
20
 
20
21
  return h(Dialog, {
21
- title,
22
+ title: title ?? i18n.t('textui.promptDialog.title', undefined, 'Input'),
22
23
  width: 50,
23
24
  onClose: onCancel,
24
25
  actions: [
25
- { id: 'ok', label: 'OK', tone: 'primary', onPress: () => onSubmit?.(value) },
26
- { id: 'cancel', label: 'Cancel', onPress: onCancel },
26
+ { id: 'ok', label: i18n.t('textui.promptDialog.ok', undefined, 'OK'), tone: 'primary', onPress: () => onSubmit?.(value) },
27
+ { id: 'cancel', label: i18n.t('textui.promptDialog.cancel', undefined, 'Cancel'), onPress: onCancel },
27
28
  ],
28
29
  ...rest,
29
30
  },
@@ -244,18 +244,20 @@ export function panelCommands(app: TextUIApp): CommandDefinition[] {
244
244
  const targetUri = (args: Record<string, unknown>, id: string | null): string | null =>
245
245
  (args.uri as string | undefined) ?? (id === null ? null : panelUri(app.store, id));
246
246
 
247
+ const category = app.i18n.t('textui.panel.category', undefined, 'View');
248
+
247
249
  return [
248
250
  {
249
251
  id: 'panel.openWith',
250
- title: 'Open With…',
251
- category: 'View',
252
+ title: app.i18n.t('textui.panel.openWith', undefined, 'Open With…'),
253
+ category,
252
254
  slots: ['palette'],
253
255
  when: PANEL_PATH,
254
256
  args: [{
255
257
  name: 'renderer',
256
258
  type: 'string' as const,
257
259
  required: true,
258
- description: 'How to show this resource',
260
+ description: app.i18n.t('textui.panel.renderer', undefined, 'How to show this resource'),
259
261
  choices: async () => {
260
262
  const id = activePanel(app.store);
261
263
  return (await choicesFor(app, id === null ? null : panelUri(app.store, id)))
@@ -272,8 +274,8 @@ export function panelCommands(app: TextUIApp): CommandDefinition[] {
272
274
  },
273
275
  {
274
276
  id: 'panel.nextRenderer',
275
- title: 'Next View',
276
- category: 'View',
277
+ title: app.i18n.t('textui.panel.nextView', undefined, 'Next View'),
278
+ category,
277
279
  slots: ['palette'],
278
280
  when: PANEL_PATH,
279
281
  run: async (args: Record<string, unknown>, ctx: CommandContext) => {
@@ -291,8 +293,8 @@ export function panelCommands(app: TextUIApp): CommandDefinition[] {
291
293
  },
292
294
  {
293
295
  id: 'panel.toggleEdit',
294
- title: 'Edit / View',
295
- category: 'View',
296
+ title: app.i18n.t('textui.panel.toggleEdit', undefined, 'Edit / View'),
297
+ category,
296
298
  slots: ['palette'],
297
299
  when: PANEL_PATH,
298
300
  run: async (args: Record<string, unknown>, ctx: CommandContext) => {
@@ -12,6 +12,7 @@ import {
12
12
  h,
13
13
  useEffect,
14
14
  useFocusScope,
15
+ useI18n,
15
16
  useRef,
16
17
  useRuntime,
17
18
  useStoreValue,
@@ -144,6 +145,7 @@ export interface ResourcePanelProps extends BoxProps {
144
145
  */
145
146
  export const ResourcePanel = defineComponent<ResourcePanelProps>('ResourcePanel', (props) => {
146
147
  const runtime = useRuntime();
148
+ const i18n = useI18n();
147
149
  const {
148
150
  id: named, uri, renderer, mode, rendererProps, fallbackComponent, autoFocus,
149
151
  emptyTitle, ...rest
@@ -265,19 +267,22 @@ export const ResourcePanel = defineComponent<ResourcePanelProps>('ResourcePanel'
265
267
  if (claim > 0 && chosen) runtime.store.set(panelClaimPath(id), 0);
266
268
  }, [id, claim, chosen?.id ?? null]);
267
269
 
268
- if (!uri) return h(EmptyState, { title: emptyTitle ?? 'Nothing selected', ...rest });
270
+ if (!uri) return h(EmptyState, { title: emptyTitle ?? i18n.t('textui.resourcePanel.empty', undefined, 'Nothing selected'), ...rest });
269
271
  if (!app) return null;
270
272
  if (task.status === 'running' || task.status === 'idle') {
271
- return h(Spinner, { label: 'Loading…', ...rest });
273
+ return h(Spinner, { label: i18n.t('textui.resourcePanel.loading', undefined, 'Loading…'), ...rest });
272
274
  }
273
275
  if (task.status === 'error') return h(ErrorState, { error: task.error, ...rest });
274
- if (!resource) return h(EmptyState, { title: 'Not found', message: uri, ...rest });
276
+ if (!resource) return h(EmptyState, { title: i18n.t('textui.resourcePanel.notFound', undefined, 'Not found'), message: uri, ...rest });
275
277
 
276
278
  if (!chosen) {
277
279
  return h('box', { direction: 'column', flex: 1, id: scope, ...rest },
278
280
  fallbackComponent
279
281
  ? { component: fallbackComponent, resource, uri }
280
- : h(EmptyState, { title: 'No renderer', message: resource.kind }));
282
+ : h(EmptyState, {
283
+ title: i18n.t('textui.resourcePanel.noRenderer', undefined, 'No renderer'),
284
+ message: resource.kind,
285
+ }));
281
286
  }
282
287
 
283
288
  const node: ComponentNode = {
@@ -1,5 +1,5 @@
1
1
  import type { BindingPath, BoxProps, ComponentNode, Mount } from '@textui/core';
2
- import { defineComponent, h, useRuntime } from '@textui/core';
2
+ import { defineComponent, h, useI18n, useRuntime } from '@textui/core';
3
3
  import { EmptyState } from '../display/index.js';
4
4
 
5
5
  export interface MountViewProps extends BoxProps {
@@ -9,6 +9,7 @@ export interface MountViewProps extends BoxProps {
9
9
  /** Renders one mount's target - an inline node, or a node read from a path. */
10
10
  export const MountView = defineComponent<MountViewProps>('MountView', ({ mount, ...rest }) => {
11
11
  const runtime = useRuntime();
12
+ const i18n = useI18n();
12
13
 
13
14
  const target = mount.target;
14
15
  const node: ComponentNode | null =
@@ -17,7 +18,7 @@ export const MountView = defineComponent<MountViewProps>('MountView', ({ mount,
17
18
  : (runtime.store.get<ComponentNode>((target as { path: BindingPath }).path) ?? null);
18
19
 
19
20
  if (!node) {
20
- return h(EmptyState, { title: 'Nothing mounted', ...rest });
21
+ return h(EmptyState, { title: i18n.t('textui.mountView.empty', undefined, 'Nothing mounted'), ...rest });
21
22
  }
22
23
 
23
24
  // A mount's data context makes relative paths inside it resolve against the