@textui/widgets 0.6.1 → 0.8.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 (120) 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/control/text-area.d.ts +2 -0
  8. package/dist/control/text-area.d.ts.map +1 -1
  9. package/dist/control/text-area.js +16 -8
  10. package/dist/data/code-viewer.d.ts +2 -0
  11. package/dist/data/code-viewer.d.ts.map +1 -1
  12. package/dist/data/code-viewer.js +9 -1
  13. package/dist/data/index.d.ts +9 -0
  14. package/dist/data/index.d.ts.map +1 -1
  15. package/dist/data/index.js +11 -4
  16. package/dist/data/list.d.ts +9 -0
  17. package/dist/data/list.d.ts.map +1 -1
  18. package/dist/data/list.js +23 -5
  19. package/dist/data/log-viewer.d.ts.map +1 -1
  20. package/dist/data/log-viewer.js +7 -2
  21. package/dist/data/markdown-view.d.ts.map +1 -1
  22. package/dist/data/markdown-view.js +6 -2
  23. package/dist/data/pagination.d.ts.map +1 -1
  24. package/dist/data/pagination.js +6 -2
  25. package/dist/data/tree.d.ts +2 -0
  26. package/dist/data/tree.d.ts.map +1 -1
  27. package/dist/data/tree.js +8 -2
  28. package/dist/display/card.d.ts.map +1 -1
  29. package/dist/display/card.js +12 -0
  30. package/dist/display/color-text.d.ts +13 -1
  31. package/dist/display/color-text.d.ts.map +1 -1
  32. package/dist/display/color-text.js +28 -6
  33. package/dist/display/error-state.d.ts.map +1 -1
  34. package/dist/display/error-state.js +4 -3
  35. package/dist/display/font-text.d.ts +72 -0
  36. package/dist/display/font-text.d.ts.map +1 -0
  37. package/dist/display/font-text.js +35 -0
  38. package/dist/display/fonts.d.ts +138 -0
  39. package/dist/display/fonts.d.ts.map +1 -0
  40. package/dist/display/fonts.js +893 -0
  41. package/dist/display/gard.d.ts +26 -0
  42. package/dist/display/gard.d.ts.map +1 -0
  43. package/dist/display/gard.js +479 -0
  44. package/dist/display/index.d.ts +3 -0
  45. package/dist/display/index.d.ts.map +1 -1
  46. package/dist/display/index.js +7 -0
  47. package/dist/display/key-value.d.ts +25 -5
  48. package/dist/display/key-value.d.ts.map +1 -1
  49. package/dist/display/key-value.js +13 -3
  50. package/dist/display/pagga.d.ts +29 -0
  51. package/dist/display/pagga.d.ts.map +1 -0
  52. package/dist/display/pagga.js +101 -0
  53. package/dist/display/pattern.d.ts +117 -0
  54. package/dist/display/pattern.d.ts.map +1 -0
  55. package/dist/display/pattern.js +257 -0
  56. package/dist/display/spinner.d.ts +2 -0
  57. package/dist/display/spinner.d.ts.map +1 -1
  58. package/dist/display/spinner.js +3 -2
  59. package/dist/form/danger-zone.d.ts.map +1 -1
  60. package/dist/form/danger-zone.js +7 -3
  61. package/dist/form/form-actions.d.ts.map +1 -1
  62. package/dist/form/form-actions.js +3 -2
  63. package/dist/navigation/menu.d.ts +2 -0
  64. package/dist/navigation/menu.d.ts.map +1 -1
  65. package/dist/navigation/menu.js +22 -8
  66. package/dist/navigation/tabs.d.ts +2 -0
  67. package/dist/navigation/tabs.d.ts.map +1 -1
  68. package/dist/navigation/tabs.js +12 -2
  69. package/dist/overlay/command-palette.d.ts.map +1 -1
  70. package/dist/overlay/command-palette.js +55 -15
  71. package/dist/overlay/dialog.d.ts +11 -0
  72. package/dist/overlay/dialog.d.ts.map +1 -1
  73. package/dist/overlay/dialog.js +24 -1
  74. package/dist/overlay/index.d.ts.map +1 -1
  75. package/dist/overlay/index.js +11 -19
  76. package/dist/overlay/path-picker.d.ts.map +1 -1
  77. package/dist/overlay/path-picker.js +19 -11
  78. package/dist/overlay/prompt-dialog.d.ts.map +1 -1
  79. package/dist/overlay/prompt-dialog.js +5 -4
  80. package/dist/panel/index.d.ts.map +1 -1
  81. package/dist/panel/index.js +8 -7
  82. package/dist/panel/resource-panel.d.ts.map +1 -1
  83. package/dist/panel/resource-panel.js +9 -5
  84. package/dist/surface/mount-view.d.ts.map +1 -1
  85. package/dist/surface/mount-view.js +3 -2
  86. package/package.json +2 -2
  87. package/src/chart/line-chart.ts +3 -2
  88. package/src/control/select.ts +4 -2
  89. package/src/control/switch.ts +9 -2
  90. package/src/control/text-area.ts +18 -7
  91. package/src/data/code-viewer.ts +10 -1
  92. package/src/data/index.ts +12 -3
  93. package/src/data/list.ts +24 -4
  94. package/src/data/log-viewer.ts +7 -1
  95. package/src/data/markdown-view.ts +10 -1
  96. package/src/data/pagination.ts +6 -2
  97. package/src/data/tree.ts +9 -2
  98. package/src/display/card.ts +12 -0
  99. package/src/display/color-text.ts +33 -10
  100. package/src/display/error-state.ts +4 -3
  101. package/src/display/font-text.tsx +94 -0
  102. package/src/display/fonts.ts +990 -0
  103. package/src/display/gard.ts +475 -0
  104. package/src/display/index.ts +7 -0
  105. package/src/display/key-value.ts +44 -6
  106. package/src/display/pagga.ts +102 -0
  107. package/src/display/pattern.tsx +427 -0
  108. package/src/display/spinner.ts +5 -2
  109. package/src/form/danger-zone.ts +7 -3
  110. package/src/form/form-actions.ts +5 -2
  111. package/src/navigation/menu.ts +23 -8
  112. package/src/navigation/tabs.ts +13 -2
  113. package/src/overlay/command-palette.ts +66 -14
  114. package/src/overlay/dialog.ts +37 -1
  115. package/src/overlay/index.ts +11 -19
  116. package/src/overlay/path-picker.ts +23 -10
  117. package/src/overlay/prompt-dialog.ts +5 -4
  118. package/src/panel/index.ts +9 -7
  119. package/src/panel/resource-panel.ts +9 -4
  120. package/src/surface/mount-view.ts +3 -2
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps, SemanticVariant } from '@textui/core';
2
- import { defineComponent, h, stringWidth, useFocus, useInput, useState, useTheme } from '@textui/core';
2
+ import { defineComponent, h, stringWidth, useFocus, useI18n, useInput, useState, useTheme } from '@textui/core';
3
3
  import { Marquee } from '../display/index.js';
4
4
  import { TONE } from '../tone.js';
5
5
 
@@ -61,8 +61,12 @@ export interface MenuProps extends BoxProps {
61
61
  interactive?: boolean;
62
62
  }
63
63
 
64
+ /** The name a theme styles a menu row under. See `LIST`. */
65
+ export const MENU = 'Menu';
66
+
64
67
  export const Menu = defineComponent<MenuProps>('Menu', (props) => {
65
68
  const theme = useTheme();
69
+ const i18n = useI18n();
66
70
  const {
67
71
  items, onSelect, visibleRows, activeId, autoFocus, interactive = true,
68
72
  descriptions = 'inline', ...rest
@@ -125,15 +129,20 @@ export const Menu = defineComponent<MenuProps>('Menu', (props) => {
125
129
  // The description yields first, and by a lot. It is the elaboration;
126
130
  // the label is the thing being chosen, and a row reading "Accept ed…"
127
131
  // beside a full sentence has given up the wrong half.
132
+ //
133
+ // On the row under the cursor the foreground is `undefined`, not a
134
+ // token: what is written on a selection is the theme's, and a hard
135
+ // `onSelected` here would be a second answer to the same question that
136
+ // a theme restating `Menu.focused.fg` could not reach.
128
137
  item.description && !below
129
138
  ? h(Marquee, {
130
139
  content: item.description,
131
140
  active,
132
- fg: active ? 'inverted' : 'muted',
141
+ fg: active ? undefined : 'muted',
133
142
  shrink: 8,
134
143
  })
135
144
  : null,
136
- item.shortcut ? h('text', { content: item.shortcut, fg: active ? 'inverted' : 'subtle' }) : null,
145
+ item.shortcut ? h('text', { content: item.shortcut, fg: active ? undefined : 'subtle' }) : null,
137
146
  // Present but empty means "this opens something, contents unknown" -
138
147
  // which is what the command palette knows about an argument whose
139
148
  // choices it has not resolved yet.
@@ -144,12 +153,18 @@ export const Menu = defineComponent<MenuProps>('Menu', (props) => {
144
153
  key: item.id,
145
154
  role: 'menuitem',
146
155
  label: item.label,
156
+ styleAs: MENU,
147
157
  selected: active,
158
+ // The keyboard is the menu's, so the row is told - which is what lets
159
+ // `Menu.selected` be the dimmer fill a menu shows when something else
160
+ // has taken the keyboard, and `Menu.focused` the one it shows while
161
+ // the reader is moving through it.
162
+ focused: active && focus.focused,
148
163
  direction: 'column',
149
164
  // One background over both lines: a highlight that stopped after the
150
- // label would split the row it is highlighting in two.
151
- bg: active ? 'selected' : undefined,
152
- fg: item.disabled ? 'disabled' : active ? 'inverted' : item.tone ? TONE[item.tone] : undefined,
165
+ // label would split the row it is highlighting in two. What it is
166
+ // filled with is the theme's to say.
167
+ fg: item.disabled ? 'disabled' : active ? undefined : item.tone ? TONE[item.tone] : undefined,
153
168
  onClick: () => { if (!item.disabled) onSelect?.(item.id, item); },
154
169
  },
155
170
  head,
@@ -165,7 +180,7 @@ export const Menu = defineComponent<MenuProps>('Menu', (props) => {
165
180
  h(Marquee, {
166
181
  content: item.description as string,
167
182
  active,
168
- fg: active ? 'inverted' : 'muted',
183
+ fg: active ? undefined : 'muted',
169
184
  flex: 1,
170
185
  }))
171
186
  : null,
@@ -188,7 +203,7 @@ export const Menu = defineComponent<MenuProps>('Menu', (props) => {
188
203
  : [row];
189
204
  }),
190
205
  selectable.length === 0
191
- ? h('text', { content: ' no matches', fg: 'subtle' })
206
+ ? h('text', { content: ` ${i18n.t('textui.menu.noMatch', undefined, 'no matches')}`, fg: 'subtle' })
192
207
  : null,
193
208
  );
194
209
  });
@@ -21,6 +21,9 @@ export interface TabsProps extends BoxProps {
21
21
  autoFocus?: boolean;
22
22
  }
23
23
 
24
+ /** The name a theme styles a tab under. See `LIST`. */
25
+ export const TABS = 'Tabs';
26
+
24
27
  export const Tabs = defineComponent<TabsProps>('Tabs', (props) => {
25
28
  const { items, activeId, onChange, variant = 'underline', separator, autoFocus, ...rest } = props;
26
29
  const focus = useFocus({ autoFocus });
@@ -46,12 +49,20 @@ export const Tabs = defineComponent<TabsProps>('Tabs', (props) => {
46
49
  key: item.id,
47
50
  role: 'tab',
48
51
  label: item.label,
52
+ styleAs: TABS,
53
+ // Carried so the theme can tell a filled tab from an unfilled one: a
54
+ // solid tab is `Tabs.solid.selected`, an underline one is not filled
55
+ // at all. See `Tabs` in the built-in themes.
56
+ variant,
57
+ // A tab is open, not selected, and it says so with one state. There is
58
+ // no second name for it because there is nothing to dim: the document
59
+ // a tab names is open whether or not the strip has the keyboard, and
60
+ // dimming the tab on losing focus would claim none is.
49
61
  selected: active,
50
62
  direction: 'row',
51
63
  gap: 1,
52
64
  padding: variant === 'solid' ? [0, 1] : 0,
53
- bg: variant === 'solid' && active ? 'selected' : undefined,
54
- fg: item.disabled ? 'disabled' : active ? (variant === 'solid' ? 'inverted' : 'accent') : 'muted',
65
+ fg: item.disabled ? 'disabled' : active ? (variant === 'solid' ? undefined : 'accent') : 'muted',
55
66
  bold: active,
56
67
  underline: variant === 'underline' && active,
57
68
  onClick: () => { if (!item.disabled) onChange?.(item.id); },
@@ -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,
@@ -100,6 +101,8 @@ interface Pending {
100
101
 
101
102
  export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalette', (props) => {
102
103
  const theme = useTheme();
104
+ const i18n = useI18n();
105
+ const ellipsis = theme.glyphs.ellipsis;
103
106
  const runtime = useRuntime();
104
107
  const {
105
108
  commands, placeholder, onRun, onClose, execute = true,
@@ -131,7 +134,9 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
131
134
  const given = typeof commands === 'function' ? commands() : commands;
132
135
  const all = given ?? app?.commands.list({ slot: 'palette', enabledOnly: true }) ?? [];
133
136
 
134
- const matches = pending ? [] : filterCommands(all, query);
137
+ const matches = pending
138
+ ? []
139
+ : grouped && query.trim() === '' ? byCategory(all) : filterCommands(all, query);
135
140
  const offered = pending ? filterChoices(choices, query) : [];
136
141
  const rows = pending ? offered.map((choice) => choice.value) : matches.map((c) => c.id);
137
142
  const index = Math.max(0, Math.min(highlight, rows.length - 1));
@@ -146,7 +151,9 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
146
151
  // the only thing the palette itself knows about a value.
147
152
  ...(choice.description
148
153
  ? { description: choice.description }
149
- : pending.arg.default === choice.value ? { description: 'default' } : {}),
154
+ : pending.arg.default === choice.value
155
+ ? { description: i18n.t('textui.palette.default', undefined, 'default') }
156
+ : {}),
150
157
  }))
151
158
  // One row, saying which kind of nothing this is. A disabled item rather
152
159
  // than no items: the panel keeps its height, so the answer arriving does
@@ -154,8 +161,10 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
154
161
  : [{
155
162
  id: '',
156
163
  label: asking
157
- ? `Asking${theme.glyphs.ellipsis}`
158
- : (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')),
159
168
  disabled: true,
160
169
  }])
161
170
  : matches.map((command, i) => ({
@@ -415,7 +424,12 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
415
424
  );
416
425
 
417
426
  const highlighted = pending ? undefined : matches[index];
418
- 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');
419
433
  // What the highlighted row is, in full.
420
434
  //
421
435
  // The rows themselves have to fit, so a choice's own sentence is truncated
@@ -427,7 +441,12 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
427
441
  const chosen = pending ? offered[index] : undefined;
428
442
  const detail = pending
429
443
  ? chosen?.description
430
- ?? 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
+ )
431
450
  : highlighted?.description ?? highlighted?.id ?? '';
432
451
 
433
452
  /*
@@ -457,7 +476,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
457
476
 
458
477
  return h('box', {
459
478
  role: 'dialog',
460
- label: 'Commands',
479
+ label: i18n.t('textui.palette.label', undefined, 'Commands'),
461
480
  border: theme.border,
462
481
  bg: 'overlay',
463
482
  // A stated width is a width. Left off, it fits what it holds.
@@ -473,8 +492,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
473
492
  // where you are; "commands › id" is what the parameter happens to be
474
493
  // called, which is the author's business rather than the reader's.
475
494
  title: pending
476
- ? ` commands ${theme.glyphs.breadcrumb} ${pending.command.title} `
477
- : ' commands ',
495
+ ? ` ${i18n.t('textui.palette.title', undefined, 'commands')} ${theme.glyphs.breadcrumb} ${pending.command.title} `
496
+ : ` ${i18n.t('textui.palette.title', undefined, 'commands')} `,
478
497
  ...rest,
479
498
  },
480
499
  h(TextInput, {
@@ -511,8 +530,12 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
511
530
  ? pending.arg.description ?? `${pending.command.title}${theme.glyphs.ellipsis}`
512
531
  : offered.some((choice) => choice.description) && pending.arg.description
513
532
  ? pending.arg.description
514
- : `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`)
515
- : (placeholder ?? `Type a command${theme.glyphs.ellipsis}`),
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}')),
516
539
  search: true,
517
540
  autoFocus: true,
518
541
  border: 'none',
@@ -542,11 +565,22 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
542
565
  h('text', {
543
566
  content: pending
544
567
  ? (pending.arg.choices === undefined
545
- ? 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
+ ])
546
573
  // Nothing to choose is nothing to press enter on, and offering it
547
574
  // is how a panel reads as broken rather than as empty.
548
- : hint(theme, rows.length > 0 ? [move, 'enter choose', 'esc back'] : ['esc back']))
549
- : 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
+ ]),
550
584
  fg: 'subtle',
551
585
  truncate: 'end',
552
586
  })),
@@ -618,6 +652,24 @@ function filterChoices(choices: ArgChoice[], query: string): ArgChoice[] {
618
652
  .toLowerCase().includes(q));
619
653
  }
620
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
+
621
673
  export function filterCommands(commands: CommandDefinition[], query: string): CommandDefinition[] {
622
674
  const q = query.trim().toLowerCase();
623
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