@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.
- package/dist/chart/line-chart.d.ts.map +1 -1
- package/dist/chart/line-chart.js +3 -2
- package/dist/control/select.d.ts.map +1 -1
- package/dist/control/select.js +4 -2
- package/dist/control/switch.d.ts.map +1 -1
- package/dist/control/switch.js +7 -2
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +3 -2
- package/dist/data/list.d.ts.map +1 -1
- package/dist/data/list.js +3 -2
- package/dist/data/log-viewer.d.ts.map +1 -1
- package/dist/data/log-viewer.js +7 -2
- package/dist/data/markdown-view.d.ts.map +1 -1
- package/dist/data/markdown-view.js +3 -2
- package/dist/data/pagination.d.ts.map +1 -1
- package/dist/data/pagination.js +3 -2
- package/dist/display/error-state.d.ts.map +1 -1
- package/dist/display/error-state.js +4 -3
- package/dist/form/danger-zone.d.ts.map +1 -1
- package/dist/form/danger-zone.js +7 -3
- package/dist/form/form-actions.d.ts.map +1 -1
- package/dist/form/form-actions.js +3 -2
- package/dist/navigation/menu.d.ts.map +1 -1
- package/dist/navigation/menu.js +3 -2
- package/dist/overlay/command-palette.d.ts +0 -14
- package/dist/overlay/command-palette.d.ts.map +1 -1
- package/dist/overlay/command-palette.js +127 -38
- package/dist/overlay/dialog.d.ts +11 -0
- package/dist/overlay/dialog.d.ts.map +1 -1
- package/dist/overlay/dialog.js +24 -1
- package/dist/overlay/index.d.ts.map +1 -1
- package/dist/overlay/index.js +11 -19
- package/dist/overlay/path-picker.d.ts.map +1 -1
- package/dist/overlay/path-picker.js +19 -11
- package/dist/overlay/prompt-dialog.d.ts.map +1 -1
- package/dist/overlay/prompt-dialog.js +5 -4
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +8 -7
- package/dist/panel/resource-panel.d.ts.map +1 -1
- package/dist/panel/resource-panel.js +9 -5
- package/dist/surface/mount-view.d.ts.map +1 -1
- package/dist/surface/mount-view.js +3 -2
- package/package.json +2 -2
- package/src/chart/line-chart.ts +3 -2
- package/src/control/select.ts +4 -2
- package/src/control/switch.ts +9 -2
- package/src/data/index.ts +3 -1
- package/src/data/list.ts +3 -1
- package/src/data/log-viewer.ts +7 -1
- package/src/data/markdown-view.ts +7 -1
- package/src/data/pagination.ts +3 -2
- package/src/display/error-state.ts +4 -3
- package/src/form/danger-zone.ts +7 -3
- package/src/form/form-actions.ts +5 -2
- package/src/navigation/menu.ts +3 -2
- package/src/overlay/command-palette.ts +152 -29
- package/src/overlay/dialog.ts +37 -1
- package/src/overlay/index.ts +11 -19
- package/src/overlay/path-picker.ts +23 -10
- package/src/overlay/prompt-dialog.ts +5 -4
- package/src/panel/index.ts +9 -7
- package/src/panel/resource-panel.ts +9 -4
- 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
|
|
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
|
|
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
|
-
?
|
|
154
|
-
: (query === ''
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
462
|
-
? `${pending.command.title}${theme.glyphs.ellipsis}`
|
|
463
|
-
:
|
|
464
|
-
|
|
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,
|
|
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, [
|
|
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
|
|
498
|
-
|
|
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;
|
package/src/overlay/dialog.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/overlay/index.ts
CHANGED
|
@@ -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: '
|
|
72
|
-
title: options.title
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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 =
|
|
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'
|
|
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 ??
|
|
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
|
-
?
|
|
199
|
-
: (q === ''
|
|
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'
|
|
208
|
-
|
|
209
|
-
|
|
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
|
},
|
package/src/panel/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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, {
|
|
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
|