@textui/widgets 0.1.0 → 0.2.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 (55) hide show
  1. package/README.md +17 -15
  2. package/dist/control/checkbox.d.ts +2 -0
  3. package/dist/control/checkbox.d.ts.map +1 -1
  4. package/dist/control/checkbox.js +2 -2
  5. package/dist/control/radio-group.d.ts +2 -0
  6. package/dist/control/radio-group.d.ts.map +1 -1
  7. package/dist/control/radio-group.js +2 -2
  8. package/dist/control/text-area.d.ts +15 -2
  9. package/dist/control/text-area.d.ts.map +1 -1
  10. package/dist/control/text-area.js +402 -22
  11. package/dist/control/text-input.d.ts.map +1 -1
  12. package/dist/control/text-input.js +40 -0
  13. package/dist/data/feed.d.ts +14 -0
  14. package/dist/data/feed.d.ts.map +1 -1
  15. package/dist/data/feed.js +28 -2
  16. package/dist/data/list.d.ts +60 -7
  17. package/dist/data/list.d.ts.map +1 -1
  18. package/dist/data/list.js +66 -10
  19. package/dist/data/markdown-view.d.ts.map +1 -1
  20. package/dist/data/markdown-view.js +49 -0
  21. package/dist/display/color-text.d.ts +158 -0
  22. package/dist/display/color-text.d.ts.map +1 -0
  23. package/dist/display/color-text.js +246 -0
  24. package/dist/display/index.d.ts +1 -0
  25. package/dist/display/index.d.ts.map +1 -1
  26. package/dist/display/index.js +3 -0
  27. package/dist/layout/divider.d.ts +6 -1
  28. package/dist/layout/divider.d.ts.map +1 -1
  29. package/dist/layout/divider.js +5 -5
  30. package/dist/layout/scroll-view.d.ts +10 -0
  31. package/dist/layout/scroll-view.d.ts.map +1 -1
  32. package/dist/layout/scroll-view.js +2 -2
  33. package/dist/navigation/menu.d.ts +24 -0
  34. package/dist/navigation/menu.d.ts.map +1 -1
  35. package/dist/navigation/menu.js +58 -15
  36. package/dist/overlay/command-palette.d.ts +32 -1
  37. package/dist/overlay/command-palette.d.ts.map +1 -1
  38. package/dist/overlay/command-palette.js +84 -10
  39. package/dist/shells/workbench-shell.d.ts.map +1 -1
  40. package/dist/shells/workbench-shell.js +16 -2
  41. package/package.json +8 -8
  42. package/src/control/checkbox.ts +4 -2
  43. package/src/control/radio-group.ts +4 -2
  44. package/src/control/text-area.ts +394 -31
  45. package/src/control/text-input.ts +38 -1
  46. package/src/data/feed.ts +44 -1
  47. package/src/data/list.ts +125 -18
  48. package/src/data/markdown-view.ts +56 -0
  49. package/src/display/color-text.ts +386 -0
  50. package/src/display/index.ts +3 -0
  51. package/src/layout/divider.ts +15 -7
  52. package/src/layout/scroll-view.ts +12 -2
  53. package/src/navigation/menu.ts +89 -14
  54. package/src/overlay/command-palette.ts +125 -11
  55. package/src/shells/workbench-shell.ts +16 -3
@@ -1,4 +1,4 @@
1
- import { defineComponent, h, useEffect, useFocusScope, useInput, useRuntime, useState, useTheme, } from '@textui/core';
1
+ import { defineComponent, h, stringWidth, useEffect, useFocusScope, useInput, useRuntime, useState, useTheme, } from '@textui/core';
2
2
  import { TextInput } from '../control/index.js';
3
3
  import { Menu } from '../navigation/index.js';
4
4
  import { hint } from './shared.js';
@@ -19,7 +19,7 @@ import { hint } from './shared.js';
19
19
  export const CommandPalette = defineComponent('CommandPalette', (props) => {
20
20
  const theme = useTheme();
21
21
  const runtime = useRuntime();
22
- const { commands, placeholder, onRun, onClose, execute = true, grouped = true, visibleRows = 8, width = 60, openAt, ...rest } = props;
22
+ const { commands, placeholder, onRun, onClose, execute = true, grouped = true, visibleRows = 8, width, maxWidth = 60, openAt, descriptions = 'inline', ...rest } = props;
23
23
  const [query, setQuery] = useState('');
24
24
  const [highlight, setHighlight] = useState(0);
25
25
  /** The command being asked about, when the palette has drilled in. */
@@ -71,16 +71,28 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
71
71
  : matches.map((command, i) => ({
72
72
  id: command.id,
73
73
  label: command.title,
74
- // `badge` when the row has state to report, the category otherwise.
75
- // The icon stays put either way: it is what the row *is*.
76
- description: command.badge ?? command.category,
74
+ // What this row does, or the state it is reporting. The category is
75
+ // not here: it names the *group*, so it is said once above it.
76
+ description: command.badge ?? command.description,
77
77
  icon: command.icon,
78
78
  // A row may stand for a command registered under another id, and the
79
79
  // key a person would press belongs to that one.
80
80
  shortcut: command.shortcut ?? app?.keybindings.forCommand(command.id)[0],
81
81
  // A chevron, from `Menu`, for anything that will ask a question.
82
82
  children: argumentOf(command) ? [] : undefined,
83
- separatorBefore: grouped && i > 0 && matches[i - 1].category !== command.category,
83
+ // The heading goes on the first row of each group, including the
84
+ // first - a group with no name over it is the one the reader has to
85
+ // work out from the rows in it.
86
+ //
87
+ // Sorted matches interleave the categories, so a query turns the
88
+ // headings off rather than repeating them: with the rows in relevance
89
+ // order, "Screens" over a single row is noise, and the group it claims
90
+ // to start is one row long.
91
+ ...(grouped && query.trim() === ''
92
+ && (i === 0 || matches[i - 1].category !== command.category)
93
+ && command.category
94
+ ? { sectionBefore: command.category }
95
+ : {}),
84
96
  }));
85
97
  const back = () => {
86
98
  // `null` is "never mind": whatever the preview did gets undone by whoever
@@ -132,18 +144,40 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
132
144
  const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
133
145
  setPending({ command, arg, collected });
134
146
  setQuery('');
135
- setHighlight(0);
147
+ /*
148
+ * Open on the answer that is already in force.
149
+ *
150
+ * A question about a setting is nearly always asked in order to change it
151
+ * *from* something, and the row that something is on is where the reader
152
+ * is looking. Starting at the top instead says the first option is the
153
+ * current one, which is wrong on every list where it is not - and it
154
+ * costs an extra press to get back to where you began.
155
+ *
156
+ * `default` is the argument's own word for it, and the same one the row
157
+ * labelled "default" already used.
158
+ */
159
+ const startAt = (list) => {
160
+ const at = list.findIndex((choice) => choice.value === arg.default);
161
+ return at < 0 ? 0 : at;
162
+ };
136
163
  if (Array.isArray(resolved)) {
137
164
  setAsking(false);
138
- setChoices(resolved.map(asChoice));
165
+ const list = resolved.map(asChoice);
166
+ setChoices(list);
167
+ setHighlight(startAt(list));
139
168
  return;
140
169
  }
141
170
  setChoices([]);
171
+ setHighlight(0);
142
172
  setAsking(true);
143
173
  // Answered either way: a `choices` function that rejects leaves the panel
144
174
  // saying "nothing to choose", which is true of what it can offer.
145
175
  void resolved
146
- .then((list) => setChoices(list.map(asChoice)))
176
+ .then((list) => {
177
+ const choices = list.map(asChoice);
178
+ setChoices(choices);
179
+ setHighlight(startAt(choices));
180
+ })
147
181
  .catch(() => setChoices([]))
148
182
  .finally(() => setAsking(false));
149
183
  };
@@ -263,12 +297,29 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
263
297
  ? chosen?.description
264
298
  ?? pending.arg.description ?? `${pending.command.title} needs a ${pending.arg.name}`
265
299
  : highlighted?.description ?? highlighted?.id ?? '';
300
+ /*
301
+ * How wide the panel wants to be.
302
+ *
303
+ * A menu sized to a constant is a menu that is too wide for a list of
304
+ * one-word answers and too narrow for a list of sentences, and it is the
305
+ * same menu either way. So it asks for what it holds - the widest row, plus
306
+ * what the row draws around it - and takes `maxWidth` when that is more than
307
+ * there is any point having.
308
+ *
309
+ * `minWidth` keeps the search field, the hint row and the crumb from being
310
+ * the things that decide it: a question with two short answers still needs
311
+ * somewhere to type and a line saying what the keys do.
312
+ */
313
+ const content = Math.max(...items.map((item) => rowWidth(item, descriptions)), ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]));
266
314
  return h('box', {
267
315
  role: 'dialog',
268
316
  label: 'Commands',
269
317
  border: theme.border,
270
318
  bg: 'overlay',
271
- width,
319
+ // A stated width is a width. Left off, it fits what it holds.
320
+ ...(width !== undefined
321
+ ? { width }
322
+ : { minWidth: Math.min(28, maxWidth), maxWidth, width: Math.min(content, maxWidth) }),
272
323
  direction: 'column',
273
324
  // A border is a gutter as well as a line. Without one - `paper` sets
274
325
  // `border: 'none'` - the rows run flush to the panel edge and the last
@@ -316,6 +367,9 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
316
367
  }), h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }), h(Menu, {
317
368
  items,
318
369
  visibleRows,
370
+ // The argument gets the last word: only it knows whether its answers are
371
+ // told apart by a word or by a sentence.
372
+ descriptions: pending?.arg.descriptions ?? descriptions,
319
373
  interactive: false,
320
374
  activeId: rows[index],
321
375
  onSelect: (id) => {
@@ -413,3 +467,23 @@ function subsequenceScore(haystack, needle) {
413
467
  }
414
468
  return score;
415
469
  }
470
+ /**
471
+ * The cells one row would like, drawn the way this menu draws it.
472
+ *
473
+ * Mirrors `Menu`'s own layout rather than guessing: the cursor's column and
474
+ * the gap after it, the icon when there is one, the label, and then either the
475
+ * description beside it or a line of its own under it. A description on its
476
+ * own line does not widen the row past its own indent, which is why `below`
477
+ * is the layout a long sentence wants.
478
+ */
479
+ function rowWidth(item, descriptions) {
480
+ // The marker and its gap; a switch column when the menu has one.
481
+ const lead = 2 + (item.checked !== undefined ? 2 : 0)
482
+ + (item.icon ? stringWidth(item.icon) + 1 : 0);
483
+ const label = stringWidth(item.label);
484
+ const trail = (item.shortcut ? stringWidth(item.shortcut) + 1 : 0) + (item.children ? 2 : 0);
485
+ const description = item.description ? stringWidth(item.description) : 0;
486
+ return descriptions === 'below'
487
+ ? Math.max(lead + label + trail, lead + description)
488
+ : lead + label + (description > 0 ? description + 2 : 0) + trail;
489
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"workbench-shell.d.ts","sourceRoot":"","sources":["../../src/shells/workbench-shell.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,iFAgDzB,CAAC"}
1
+ {"version":3,"file":"workbench-shell.d.ts","sourceRoot":"","sources":["../../src/shells/workbench-shell.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,iFA6DzB,CAAC"}
@@ -17,6 +17,8 @@ export const WorkbenchShell = defineComponent('WorkbenchShell', (props) => {
17
17
  const sidebar = useSurfaceMounted('sidebar');
18
18
  const aside = useSurfaceMounted('aside');
19
19
  const narrow = size.width < 90;
20
+ const showSidebar = Boolean(sidebar) && !sidebarCollapsed && !narrow;
21
+ const showAside = Boolean(aside) && asideVisible && !narrow;
20
22
  return h('box', {
21
23
  direction: 'column',
22
24
  width: '100%',
@@ -25,14 +27,26 @@ export const WorkbenchShell = defineComponent('WorkbenchShell', (props) => {
25
27
  fg: 'text',
26
28
  border: theme.border,
27
29
  ...props,
28
- }, h(SurfaceArea, { surface: 'header' }), h('box', { direction: 'row', flex: 1 }, h(SurfaceArea, { surface: 'rail' }), sidebar && !sidebarCollapsed && !narrow
30
+ }, h(SurfaceArea, { surface: 'header' }), h('box', { direction: 'row', flex: 1 }, h(SurfaceArea, { surface: 'rail' }), showSidebar
29
31
  ? h('box', {
30
32
  width: 24,
31
33
  border: { style: theme.border, sides: { right: true } },
32
34
  direction: 'column',
33
35
  padding: { left: 1 },
34
36
  }, h(SurfaceArea, { surface: 'sidebar', flex: 1 }))
35
- : null, h('box', { flex: 1, direction: 'column', padding: { left: 1 } }, h(SurfaceArea, { surface: 'main', flex: 1 }), h(SurfaceArea, { surface: 'panel' })), aside && asideVisible && !narrow
37
+ : null,
38
+ // A gutter separates main from the pane beside it, so it belongs on the
39
+ // sides that have one. Applied unconditionally it insets every screen by
40
+ // a cell on the left and nothing on the right - hidden under a theme
41
+ // that draws a frame, and plainly lopsided under one that does not.
42
+ h('box', {
43
+ flex: 1,
44
+ direction: 'column',
45
+ padding: {
46
+ ...(showSidebar ? { left: 1 } : {}),
47
+ ...(showAside ? { right: 1 } : {}),
48
+ },
49
+ }, h(SurfaceArea, { surface: 'main', flex: 1 }), h(SurfaceArea, { surface: 'panel' })), showAside
36
50
  ? h('box', {
37
51
  width: 30,
38
52
  border: { style: theme.border, sides: { left: true } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textui/widgets",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The TextUI component catalog - layout, display, controls, data, overlays and charts",
5
5
  "keywords": [
6
6
  "terminal",
@@ -40,15 +40,15 @@
40
40
  "import": "./dist/index.js"
41
41
  }
42
42
  },
43
- "dependencies": {
44
- "@textui/core": "^0.1.0"
45
- },
46
- "publishConfig": {
47
- "access": "public"
48
- },
49
43
  "scripts": {
50
44
  "build": "tsc -p tsconfig.json",
51
45
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
52
46
  "test": "vitest run"
47
+ },
48
+ "dependencies": {
49
+ "@textui/core": "^0.2.0"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
53
  }
54
- }
54
+ }
@@ -7,12 +7,14 @@ export interface CheckboxProps extends BoxProps {
7
7
  /** Neither checked nor unchecked - a parent of mixed children. */
8
8
  indeterminate?: boolean;
9
9
  onChange?(checked: boolean): void;
10
+ /** Take the keyboard on arrival. */
11
+ autoFocus?: boolean;
10
12
  }
11
13
 
12
14
  export const Checkbox = defineComponent<CheckboxProps>('Checkbox', (props) => {
13
15
  const theme = useTheme();
14
- const { label, checked = false, indeterminate, onChange, disabled, ...rest } = props;
15
- const focus = useFocus({ disabled });
16
+ const { label, checked = false, indeterminate, onChange, disabled, autoFocus, ...rest } = props;
17
+ const focus = useFocus({ disabled, autoFocus });
16
18
 
17
19
  useInput(
18
20
  (event) => {
@@ -15,12 +15,14 @@ export interface RadioGroupProps extends BoxProps {
15
15
  label?: string;
16
16
  /** Lay the options out in a row instead of a column. */
17
17
  inline?: boolean;
18
+ /** Take the keyboard on arrival. */
19
+ autoFocus?: boolean;
18
20
  }
19
21
 
20
22
  export const RadioGroup = defineComponent<RadioGroupProps>('RadioGroup', (props) => {
21
23
  const theme = useTheme();
22
- const { options, value, onChange, label, inline, disabled, ...rest } = props;
23
- const focus = useFocus({ disabled });
24
+ const { options, value, onChange, label, inline, disabled, autoFocus, ...rest } = props;
25
+ const focus = useFocus({ disabled, autoFocus });
24
26
  const index = Math.max(0, options.findIndex((o) => o.value === value));
25
27
 
26
28
  useInput(