@adecore/ui 0.39.0 → 0.40.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 (207) hide show
  1. package/dist/AccentSwatches.js +2 -3
  2. package/dist/Banner.js +1 -6
  3. package/dist/ChoiceCards.js +0 -5
  4. package/dist/ColorSwatch.js +1 -4
  5. package/dist/ColorWheelPicker.js +3 -6
  6. package/dist/DockShell.js +3 -5
  7. package/dist/EmptyState.js +0 -1
  8. package/dist/ErrorBoundary.js +1 -5
  9. package/dist/Field.js +2 -6
  10. package/dist/FileIcon.js +2 -4
  11. package/dist/Icon.js +1 -2
  12. package/dist/IconButton.js +1 -1
  13. package/dist/IconPicker.js +3 -7
  14. package/dist/Input.js +1 -2
  15. package/dist/Kbd.js +1 -3
  16. package/dist/LazyBoundary.js +2 -3
  17. package/dist/Meter.js +0 -1
  18. package/dist/MiddleTruncate.js +2 -6
  19. package/dist/PanelEmpty.js +0 -1
  20. package/dist/PanelHeader.js +1 -4
  21. package/dist/Pill.js +1 -6
  22. package/dist/ProjectSwitcher.js +1 -1
  23. package/dist/PromptDialog.js +5 -8
  24. package/dist/RelativeTime.js +1 -1
  25. package/dist/SegmentBar.js +1 -2
  26. package/dist/Segmented.js +2 -5
  27. package/dist/Select.js +1 -4
  28. package/dist/ShortcutHints.js +2 -3
  29. package/dist/Slider.js +0 -1
  30. package/dist/SlidingColumn.js +2 -4
  31. package/dist/SplitButton.js +1 -5
  32. package/dist/TextMenu.js +1 -5
  33. package/dist/Tile.js +0 -4
  34. package/dist/Toasts.js +1 -9
  35. package/dist/Tooltip.js +1 -1
  36. package/dist/UIProvider.js +0 -5
  37. package/dist/Waveform.js +4 -2
  38. package/dist/Wipe.js +1 -6
  39. package/dist/ZoomControls.js +0 -1
  40. package/dist/clipboard.js +2 -5
  41. package/dist/combobox/parts.js +3 -9
  42. package/dist/command-palette/parts.js +5 -14
  43. package/dist/dialog/parts.js +5 -10
  44. package/dist/dialog-layer.js +2 -3
  45. package/dist/error-boundary.js +4 -20
  46. package/dist/file-icon.js +4 -9
  47. package/dist/file-tree/empty-lines.js +2 -8
  48. package/dist/file-tree/use-empty-lines.js +1 -5
  49. package/dist/file-tree/use-slots.js +6 -10
  50. package/dist/format/datetime.js +32 -29
  51. package/dist/format/duration.js +2 -10
  52. package/dist/format/locale.js +19 -39
  53. package/dist/format/number.js +9 -22
  54. package/dist/format/regions.js +4 -7
  55. package/dist/icon-picker.js +5 -14
  56. package/dist/key-value-list/parts.js +1 -5
  57. package/dist/lazy.js +4 -19
  58. package/dist/locales.js +2 -6
  59. package/dist/menu/parts.js +61 -133
  60. package/dist/middle-truncate.js +2 -3
  61. package/dist/placement.js +0 -1
  62. package/dist/popover/parts.js +1 -5
  63. package/dist/popup-layer.js +2 -4
  64. package/dist/prefetch.js +2 -5
  65. package/dist/selection.js +1 -5
  66. package/dist/settings/MasterDetail.js +1 -7
  67. package/dist/settings/SettingsDialog.js +1 -2
  68. package/dist/settings/SettingsRow.js +2 -5
  69. package/dist/settings/SettingsSection.js +1 -1
  70. package/dist/shortcut-hints.js +1 -5
  71. package/dist/shortcut.js +1 -2
  72. package/dist/tabs/overflow.js +1 -6
  73. package/dist/tabs/parts.js +69 -95
  74. package/dist/testing/dedupe.js +5 -7
  75. package/dist/tint.js +3 -5
  76. package/dist/toast-store.js +2 -10
  77. package/dist/tree/use-tree-shift.js +2 -6
  78. package/dist/useColumnResize.js +9 -10
  79. package/dist/useNow.js +2 -9
  80. package/dist/workspace/DropPreview.js +1 -5
  81. package/dist/workspace/preview.js +4 -9
  82. package/package.json +9 -9
  83. package/src/AccentSwatches.tsx +9 -3
  84. package/src/Banner.tsx +5 -11
  85. package/src/Button.tsx +4 -0
  86. package/src/Checkbox.tsx +3 -0
  87. package/src/ChoiceCards.tsx +12 -11
  88. package/src/CloseButton.tsx +1 -0
  89. package/src/ColorSwatch.tsx +7 -8
  90. package/src/ColorWheelPicker.tsx +28 -12
  91. package/src/ColumnResizeHandle.tsx +1 -0
  92. package/src/CopyButton.tsx +6 -0
  93. package/src/DisabledReason.tsx +1 -0
  94. package/src/DockShell.tsx +11 -10
  95. package/src/EmptyState.tsx +10 -6
  96. package/src/ErrorBoundary.tsx +13 -10
  97. package/src/Field.tsx +11 -13
  98. package/src/FileIcon.tsx +4 -4
  99. package/src/Icon.tsx +2 -4
  100. package/src/IconButton.tsx +14 -5
  101. package/src/IconPicker.tsx +30 -15
  102. package/src/Input.tsx +11 -9
  103. package/src/Kbd.tsx +8 -7
  104. package/src/LazyBoundary.tsx +3 -3
  105. package/src/Meter.tsx +7 -2
  106. package/src/MiddleTruncate.tsx +15 -6
  107. package/src/PanelEmpty.tsx +9 -7
  108. package/src/PanelHeader.tsx +2 -5
  109. package/src/Pill.tsx +7 -8
  110. package/src/ProjectSwitcher.tsx +16 -6
  111. package/src/PromptDialog.tsx +31 -26
  112. package/src/RelativeTime.tsx +6 -1
  113. package/src/SegmentBar.tsx +22 -11
  114. package/src/Segmented.tsx +16 -11
  115. package/src/Select.tsx +7 -4
  116. package/src/SelectSearch.tsx +3 -0
  117. package/src/ShortcutHints.tsx +24 -3
  118. package/src/Skeleton.tsx +1 -0
  119. package/src/Slider.tsx +4 -1
  120. package/src/SlidingColumn.tsx +18 -14
  121. package/src/Spinner.tsx +1 -0
  122. package/src/SplitButton.tsx +8 -8
  123. package/src/Stepper.tsx +6 -0
  124. package/src/Switch.tsx +2 -0
  125. package/src/TextMenu.tsx +2 -5
  126. package/src/Tile.tsx +2 -6
  127. package/src/Toasts.tsx +8 -12
  128. package/src/Tooltip.tsx +10 -5
  129. package/src/UIProvider.tsx +7 -7
  130. package/src/Waveform.tsx +41 -12
  131. package/src/Wipe.tsx +5 -6
  132. package/src/ZoomControls.tsx +5 -2
  133. package/src/clipboard.ts +4 -5
  134. package/src/combobox/parts.tsx +18 -14
  135. package/src/command-palette/context.ts +2 -0
  136. package/src/command-palette/index.parts.ts +1 -0
  137. package/src/command-palette/parts.tsx +20 -14
  138. package/src/command-palette/virtual.tsx +21 -0
  139. package/src/dialog/parts.tsx +10 -13
  140. package/src/dialog-layer.ts +5 -3
  141. package/src/error-boundary.ts +7 -20
  142. package/src/file-icon.ts +8 -9
  143. package/src/file-tree/empty-lines.ts +24 -8
  144. package/src/file-tree/helpers.ts +34 -0
  145. package/src/file-tree/highlight.ts +19 -0
  146. package/src/file-tree/index.parts.ts +1 -0
  147. package/src/file-tree/parts.tsx +41 -5
  148. package/src/file-tree/use-empty-lines.ts +22 -5
  149. package/src/file-tree/use-file-tree-shift.tsx +10 -0
  150. package/src/file-tree/use-file-tree.ts +2 -0
  151. package/src/file-tree/use-slots.ts +77 -12
  152. package/src/format/datetime.ts +42 -17
  153. package/src/format/duration.ts +28 -10
  154. package/src/format/index.ts +3 -0
  155. package/src/format/locale.ts +33 -42
  156. package/src/format/number.ts +21 -27
  157. package/src/format/regions.ts +4 -7
  158. package/src/icon-picker.ts +23 -16
  159. package/src/index.ts +8 -0
  160. package/src/key-value-list/parts.tsx +1 -5
  161. package/src/lazy-landings.ts +2 -0
  162. package/src/lazy.tsx +23 -19
  163. package/src/locales.ts +2 -6
  164. package/src/match.ts +14 -0
  165. package/src/menu/parts.tsx +30 -55
  166. package/src/middle-truncate.ts +12 -3
  167. package/src/modality.ts +4 -0
  168. package/src/placement.ts +2 -1
  169. package/src/platform.ts +2 -0
  170. package/src/popover/parts.tsx +3 -5
  171. package/src/popup-layer.ts +6 -4
  172. package/src/prefetch.ts +18 -5
  173. package/src/project-switcher-row.tsx +2 -0
  174. package/src/segmented-keys.ts +6 -0
  175. package/src/select-shared.ts +2 -0
  176. package/src/selection.ts +5 -5
  177. package/src/settings/ConfirmDialog.tsx +3 -0
  178. package/src/settings/MasterDetail.tsx +3 -9
  179. package/src/settings/SettingsDialog.tsx +27 -9
  180. package/src/settings/SettingsRow.tsx +16 -10
  181. package/src/settings/SettingsSection.tsx +10 -5
  182. package/src/settings/index.ts +3 -0
  183. package/src/settings/target.ts +1 -0
  184. package/src/shortcut-hints.ts +10 -5
  185. package/src/shortcut.ts +26 -6
  186. package/src/tabs/overflow.ts +8 -6
  187. package/src/tabs/parts.tsx +50 -26
  188. package/src/testing/dedupe.ts +27 -7
  189. package/src/testing/fake-source.ts +1 -0
  190. package/src/tint.ts +3 -5
  191. package/src/toast-store.ts +20 -14
  192. package/src/tree/parts.tsx +8 -0
  193. package/src/tree/shift.ts +6 -0
  194. package/src/tree/use-tree-shift.tsx +23 -7
  195. package/src/useAsyncAction.ts +8 -0
  196. package/src/useColumnResize.ts +24 -15
  197. package/src/useContentSize.ts +5 -0
  198. package/src/useMeasuredWidth.ts +1 -0
  199. package/src/useNow.ts +6 -11
  200. package/src/workspace/DropPreview.tsx +3 -5
  201. package/src/workspace/SplitView.tsx +109 -0
  202. package/src/workspace/TabStrip.tsx +42 -0
  203. package/src/workspace/Workspace.tsx +5 -0
  204. package/src/workspace/context.ts +1 -0
  205. package/src/workspace/geometry.ts +49 -0
  206. package/src/workspace/model.ts +72 -0
  207. package/src/workspace/preview.ts +15 -15
@@ -16,35 +16,41 @@ export type { IconPickerGroup } from './icon-picker.ts';
16
16
 
17
17
  /* A row of cells is 28 high with a gap of 4. Under a heading of 28 the last row keeps 8 below it; without headings the grid has 6 above as well. */
18
18
  const ROW_HEIGHT = 32;
19
+
19
20
  const HEADED_EXTRA = 32;
20
21
  const BARE_EXTRA = 10;
21
22
 
22
23
  export interface IconPickerProps {
23
- /* Every icon on offer by its name, in the order of the grid, or in groups under a heading each. The name is also its tooltip. */
24
+ /* By name, in the order of the grid, or in groups under a heading each. The name is also its tooltip. */
24
25
  icons: IconPickerIcons;
25
- /* The name of the icon as it stands, or null for none. */
26
+
26
27
  value: string | null;
27
28
  onValueChange(name: string): void;
28
- /* More words a search finds an icon by, beside its name and its group's label, by icon name. */
29
+
30
+ /* By icon name: more words a search finds it by, beside its name and its group's label. */
29
31
  keywords?: Readonly<Record<string, readonly string[]>>;
30
- /* A search field on top of the grid. On by default for groups, off for a flat set. */
32
+
33
+ /* On by default for groups, off for a flat set. */
31
34
  searchable?: boolean;
32
- /* The grid as a scroll area this many rows high, with room for a heading. Without it the grid is as tall as its icons. */
35
+
36
+ /* The grid as a scroll area this many rows high. Without it the grid is as tall as its icons. */
33
37
  rows?: number;
38
+
34
39
  disabled?: boolean;
35
- /* Clearing the icon. A surface that falls back to a default of its own offers that instead. */
36
40
  onClear?: () => void;
37
- /* The label above the trigger. A surface that also takes an image calls this something else, or the two read as one list. */
38
41
  label?: string;
39
- /* Leaves the label out of sight, for a row that names the control beside it; it still names the trigger. */
42
+
43
+ /* It still names the trigger. */
40
44
  hideLabel?: boolean;
45
+
41
46
  className?: string;
42
47
  ref?: Ref<HTMLDivElement>;
43
48
  }
44
49
 
45
- /* A heading is stuck while its group runs past the top of the scroll area, and only then draws a line under it. */
50
+ /* Only a stuck heading draws a line under it. */
46
51
  const markStuckHeadings = (area: HTMLElement): void => {
47
52
  const top = area.scrollTop;
53
+
48
54
  for (const heading of area.querySelectorAll<HTMLElement>('[data-icon-heading]')) {
49
55
  const group = heading.parentElement;
50
56
  const stuck = group !== null && top > 0 && group.offsetTop <= top && group.offsetTop + group.offsetHeight > top;
@@ -52,11 +58,7 @@ const markStuckHeadings = (area: HTMLElement): void => {
52
58
  }
53
59
  };
54
60
 
55
- /*
56
- * The mark a thing wears, one of a set of Lucide icons, behind a trigger drawn like a field. The
57
- * popover holds a search over the grid. The grid is one tab stop: arrows move the focus and only
58
- * Enter, Space or a click chooses, since a caller may save every choice at once.
59
- */
61
+ /* The grid is one tab stop: arrows move the focus and only Enter, Space or a click chooses, since a caller may save every choice at once. */
60
62
  export function IconPicker({
61
63
  icons,
62
64
  value,
@@ -168,7 +170,7 @@ interface IconPickerPanelProps {
168
170
  onClear: (() => void) | undefined;
169
171
  }
170
172
 
171
- /* What the popover holds. It mounts on every open, so a search and the focus start over each time. */
173
+ /* It mounts on every open, so a search and the focus start over each time. */
172
174
  function IconPickerPanel({ id, name, sections, headed, value, keywords, searchable, rows, disabled, onChoose, onClear }: IconPickerPanelProps) {
173
175
  const { t } = useTranslation('ui');
174
176
  const input = useRef<HTMLInputElement>(null);
@@ -176,6 +178,7 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
176
178
  const [query, setQuery] = useState('');
177
179
  const [focused, setFocused] = useState<string | null>(null);
178
180
  const shown = filterSections(sections, query, keywords);
181
+
179
182
  const tabStop = tabStopOf(
180
183
  shown.flatMap((section) => section.icons.map(([icon]) => icon)),
181
184
  focused,
@@ -186,15 +189,18 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
186
189
  useLayoutEffect(() => {
187
190
  const element = area.current;
188
191
  const chosen = element?.querySelector<HTMLElement>('[aria-checked="true"]');
192
+
189
193
  if (!element || !chosen || element.scrollHeight <= element.clientHeight) {
190
194
  return;
191
195
  }
196
+
192
197
  const heading = element.querySelector<HTMLElement>('[data-icon-heading]')?.offsetHeight ?? 0;
193
198
  element.scrollTop = Math.round(chosen.offsetTop - heading - (element.clientHeight - heading - chosen.offsetHeight) / 2);
194
199
  }, []);
195
200
 
196
201
  const search = (next: string): void => {
197
202
  setQuery(next);
203
+
198
204
  if (area.current) {
199
205
  area.current.scrollTop = 0;
200
206
  }
@@ -205,6 +211,7 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
205
211
  if (!escapeClearsSearch(event.key, query)) {
206
212
  return;
207
213
  }
214
+
208
215
  event.stopPropagation();
209
216
  event.preventDefault();
210
217
  search('');
@@ -215,19 +222,24 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
215
222
  if (event.metaKey || event.ctrlKey || event.altKey) {
216
223
  return;
217
224
  }
225
+
218
226
  const cells = [...event.currentTarget.querySelectorAll<HTMLElement>('[role="radio"]')];
219
227
  const from = cells.indexOf(event.target as HTMLElement);
228
+
220
229
  if (from === -1) {
221
230
  return;
222
231
  }
232
+
223
233
  const to = cellForKey(
224
234
  event.key,
225
235
  cells.map((cell) => cell.getBoundingClientRect()),
226
236
  from
227
237
  );
238
+
228
239
  if (to === null) {
229
240
  return;
230
241
  }
242
+
231
243
  event.preventDefault();
232
244
  cells[to]?.focus();
233
245
  };
@@ -305,10 +317,13 @@ function IconPickerPanel({ id, name, sections, headed, value, keywords, searchab
305
317
  ))}
306
318
  </div>
307
319
  );
320
+
308
321
  if (section.label === null) {
309
322
  return cells;
310
323
  }
324
+
311
325
  const headingId = `${id}-group-${index}`;
326
+
312
327
  return (
313
328
  <div key={section.id} role="group" aria-labelledby={headingId}>
314
329
  {/* A step lighter than the popover in the dark theme, where a darker band would read as a hole in it. */}
package/src/Input.tsx CHANGED
@@ -5,17 +5,18 @@ import { fieldControlProps, useFieldControl } from './field-context.ts';
5
5
  import { Icon } from './Icon.tsx';
6
6
 
7
7
  export interface InputProps extends Omit<ComponentProps<'input'>, 'size'> {
8
- /* `sm` is the height of a small button: a row of tools, or a name typed in place of the label it renames. */
8
+ /* `sm` is the height of a small button. */
9
9
  size?: 'md' | 'sm';
10
- /* A branch, a path or anything else read back letter by letter. */
10
+
11
11
  mono?: boolean;
12
- /* Drawn in front of the text, such as a magnifier on a search field. `className` then goes to the box around the icon and the input; `ref` stays on the input. */
12
+
13
+ /* `className` then goes to the box around the icon and the input; `ref` stays on the input. */
13
14
  icon?: LucideIcon;
14
- /* Drawn after the text inside the field, such as a count of matches and an `IconButton` that clears it. `className` then goes to the box, as with `icon`. */
15
+
16
+ /* Inside the field, after the text. `className` then goes to the box, as with `icon`. */
15
17
  end?: ReactNode;
16
18
  }
17
19
 
18
- /* The one text input: one border and one focus ring at the one height or the compact one. */
19
20
  export function Input({ size = 'md', mono = false, icon, end, className, id, ...props }: InputProps) {
20
21
  const field = useFieldControl();
21
22
  const small = size === 'sm';
@@ -50,19 +51,20 @@ export function Input({ size = 'md', mono = false, icon, end, className, id, ...
50
51
  }
51
52
 
52
53
  export interface TextAreaProps extends ComponentProps<'textarea'> {
53
- /* `sm` is the type and the padding of a small input: a commit message or a note in a narrow panel. */
54
+ /* `sm` is the type and the padding of a small input. */
54
55
  size?: 'md' | 'sm';
55
- /* `vertical` gives it a handle to drag it taller, for a draft of a few sentences. */
56
+
56
57
  resize?: 'none' | 'vertical';
57
- /* A command or anything else read back letter by letter. */
58
58
  mono?: boolean;
59
+
59
60
  /* `bare` drops the border, the ground and the focus outline, for a field inside a card that draws all three around it. */
60
61
  variant?: 'field' | 'bare';
61
62
  }
62
63
 
63
- /* A field of a few lines: a commit message, the body of a pull request, a note. At least 64px tall; `rows` makes it taller. */
64
+ /* At least 64px tall; `rows` makes it taller. */
64
65
  export function TextArea({ size = 'md', resize = 'none', mono = false, variant = 'field', className, id, ...props }: TextAreaProps) {
65
66
  const field = useFieldControl();
67
+
66
68
  return (
67
69
  <textarea
68
70
  {...fieldControlProps(field, id)}
package/src/Kbd.tsx CHANGED
@@ -6,7 +6,7 @@ import { formatShortcut, shortcutParts, type Shortcut } from './shortcut.ts';
6
6
  /* A shortcut chip on the surface it sits on: in a tooltip, in a palette's footer, in a line of help. */
7
7
  const INLINE_KBD = 'rounded-sm bg-surface-sunken px-[5px] py-px font-sans text-xs/[inherit] text-text-muted';
8
8
 
9
- /* One key, drawn as a cap. A settings page prints a shortcut key by key, so a gesture can stand beside it. */
9
+ /* A settings page prints a shortcut key by key, so a gesture can stand beside it. */
10
10
  const KEY_CAP = 'inline-grid place-items-center rounded-md border border-b-2 border-border-strong bg-surface-hover bg-clip-padding font-sans text-text';
11
11
 
12
12
  /* `sm` fits inside a small field, as the hint of the key that focuses it. */
@@ -15,17 +15,16 @@ const KEY_CAP_SIZES = { md: 'h-6 min-w-6 px-1.5 text-xs', sm: 'h-5 min-w-5 px-1
15
15
  export interface KbdProps {
16
16
  /* Printed the way this platform writes it. Without one, the children are printed as they are (`↑`, `Esc`). */
17
17
  shortcut?: Shortcut;
18
+
18
19
  children?: ReactNode;
19
- /*
20
- * Where the shortcut is printed. In a menu row the `.menu-item kbd` rule dresses it, as the
21
- * quiet hint at the end of the row; anywhere else it is a chip on the surface it sits on.
22
- */
20
+
21
+ /* In a menu row the `.menu-item kbd` rule dresses it; anywhere else it is a chip on the surface it sits on. */
23
22
  variant?: 'menu' | 'inline';
23
+
24
24
  className?: string;
25
25
  ref?: Ref<HTMLElement>;
26
26
  }
27
27
 
28
- /* A shortcut as this platform prints it: `⌘K` on macOS, `Ctrl+K` elsewhere. */
29
28
  export function Kbd({ shortcut, children, variant = 'menu', className, ref }: KbdProps) {
30
29
  return (
31
30
  <kbd ref={ref} className={clsx(variant === 'inline' && INLINE_KBD, className)}>
@@ -51,16 +50,18 @@ export function KeyCap({ children, size = 'md', className, ref }: KeyCapProps) {
51
50
 
52
51
  export interface KeysProps {
53
52
  shortcut: Shortcut;
53
+
54
54
  /* The pointer gesture the keys go with, such as "drag", as one more cap. */
55
55
  then?: string;
56
+
56
57
  size?: 'md' | 'sm';
57
58
  className?: string;
58
59
  ref?: Ref<HTMLSpanElement>;
59
60
  }
60
61
 
61
- /* A shortcut as one cap per key, `⌘` and `K`, plus the pointer gesture it goes with. */
62
62
  export function Keys({ shortcut, then, size = 'md', className, ref }: KeysProps) {
63
63
  const parts = [...shortcutParts(shortcut, isApplePlatform()), ...(then ? [then] : [])];
64
+
64
65
  return (
65
66
  <span ref={ref} className={clsx('flex items-center', size === 'sm' ? 'gap-0.5' : 'gap-1', className)}>
66
67
  {parts.map((part, index) => (
@@ -7,13 +7,13 @@ export interface LazyBoundaryProps {
7
7
  }
8
8
 
9
9
  /*
10
- * `Suspense` for what `lazyNamed` loads. React holds content that comes in as a retry back until
11
- * 300 ms after its fallback showed; this boundary draws again as an ordinary update once a module
12
- * lands, so a chunk that takes 20 ms shows after 20 ms.
10
+ * React holds content that comes in as a retry back until 300 ms after its fallback showed; this
11
+ * boundary draws again as an ordinary update once a module lands, so a 20 ms chunk shows after 20 ms.
13
12
  */
14
13
  export function LazyBoundary({ fallback = null, children }: LazyBoundaryProps) {
15
14
  // Compiled, the same boundary would come back and the landing would wait for React's own retry after all.
16
15
  'use no memo';
17
16
  useSyncExternalStore(subscribeLandings, readLandings, readLandings);
17
+
18
18
  return <Suspense fallback={fallback}>{children}</Suspense>;
19
19
  }
package/src/Meter.tsx CHANGED
@@ -4,19 +4,24 @@ import clsx from 'clsx';
4
4
  export interface MeterProps {
5
5
  /* In the unit of `min` and `max`, held to them. Null draws an empty track, for a level not measured yet. */
6
6
  value: number | null;
7
+
7
8
  min?: number;
8
9
  max?: number;
9
- /* Values drawn as lines across the track, such as a target or a limit. */
10
+
11
+ /* Drawn as lines across the track, such as a target or a limit. */
10
12
  marks?: readonly number[];
13
+
11
14
  label: string;
15
+
12
16
  /* The value in words for a screen reader, such as "-18.2 LUFS". Left out, it reads the number. */
13
17
  valueText?: string;
18
+
14
19
  /* Its width, which it fills. */
15
20
  className?: string;
21
+
16
22
  ref?: Ref<HTMLDivElement>;
17
23
  }
18
24
 
19
- /* A level against a scale: how full it is, and where a target or a limit sits on the same scale. */
20
25
  export function Meter({ value, min = 0, max = 100, marks = [], label, valueText, className, ref }: MeterProps) {
21
26
  const fractionOf = (level: number): number => (max > min ? Math.min(1, Math.max(0, (level - min) / (max - min))) : 0);
22
27
 
@@ -6,27 +6,33 @@ import { Tooltip } from './Tooltip.tsx';
6
6
 
7
7
  export interface MiddleTruncateProps {
8
8
  text: string;
9
+
9
10
  /* The whole text on hover and focus while it is cut. Off where the caller's own tooltip already says it. */
10
11
  tooltip?: boolean;
12
+
11
13
  className?: string;
12
14
  ref?: Ref<HTMLSpanElement>;
13
15
  }
14
16
 
15
17
  let canvas: HTMLCanvasElement | null = null;
16
18
 
17
- /* How wide a text is drawn in the face of `element`, or null where the page cannot tell (no canvas). */
19
+ /* Null where the page has no canvas. */
18
20
  function textMeasure(element: HTMLElement): ((text: string) => number) | null {
19
21
  const view = element.ownerDocument.defaultView;
20
22
  canvas ??= element.ownerDocument.createElement('canvas');
21
23
  const context = canvas.getContext('2d');
24
+
22
25
  if (context === null || view === null) {
23
26
  return null;
24
27
  }
28
+
25
29
  const style = view.getComputedStyle(element);
26
30
  context.font = style.font || `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
31
+
27
32
  if ('letterSpacing' in context) {
28
33
  context.letterSpacing = style.letterSpacing === 'normal' ? '0px' : style.letterSpacing;
29
34
  }
35
+
30
36
  return (text) => context.measureText(text).width;
31
37
  }
32
38
 
@@ -34,11 +40,7 @@ function sameCut(first: MiddleCut | null, second: MiddleCut | null): boolean {
34
40
  return first?.head === second?.head && first?.tail === second?.tail;
35
41
  }
36
42
 
37
- /*
38
- * A path or a branch name on one line that keeps its start and its end and cuts the middle. It asks
39
- * for the width of the whole text and measures what it gets. The part it cuts stays in the element
40
- * at no width, so selecting, copying and finding read the whole text.
41
- */
43
+ /* The part it cuts stays in the element at no width, so selecting, copying and finding read the whole text. */
42
44
  export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleTruncateProps) {
43
45
  const [element, setElement] = useState<HTMLSpanElement | null>(null);
44
46
  const [cut, setCut] = useState<MiddleCut | null>(null);
@@ -48,18 +50,23 @@ export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleT
48
50
  if (element === null) {
49
51
  return;
50
52
  }
53
+
51
54
  const update = (): void => {
52
55
  const measure = textMeasure(element);
53
56
  // Layout rounds what a canvas measures, so a text a fraction wider than its box still fits.
54
57
  const next = measure === null ? null : fitMiddle(text, element.getBoundingClientRect().width + 0.5, measure);
55
58
  setCut((current) => (sameCut(current, next) ? current : next));
56
59
  };
60
+
57
61
  update();
62
+
58
63
  if (typeof ResizeObserver === 'undefined') {
59
64
  return;
60
65
  }
66
+
61
67
  const observer = new ResizeObserver(update);
62
68
  observer.observe(element);
69
+
63
70
  return () => observer.disconnect();
64
71
  }, [element, text]);
65
72
 
@@ -76,9 +83,11 @@ export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleT
76
83
  )}
77
84
  </span>
78
85
  );
86
+
79
87
  // Only a cut text has more to say, and a long list of whole ones keeps no hover handlers.
80
88
  if (cut === null || !tooltip) {
81
89
  return content;
82
90
  }
91
+
83
92
  return <Tooltip label={text}>{content}</Tooltip>;
84
93
  }
@@ -6,23 +6,25 @@ import { EmptyState } from './EmptyState.tsx';
6
6
  export interface PanelEmptyProps {
7
7
  /* The panel's own header, kept above the sentence so a person can still act while nothing is listed. */
8
8
  header?: ReactNode;
9
+
9
10
  icon?: LucideIcon;
10
- /* A `Spinner` in place of the icon, for a panel that is still loading. */
11
+
12
+ /* A `Spinner` in place of the icon. */
11
13
  busy?: boolean;
14
+
12
15
  action?: ReactNode;
13
- /* The sunken ground, for a box that has none behind it yet (a card on a canvas) rather than a panel that does. */
16
+
17
+ /* For a box that has no ground behind it yet, such as a card on a canvas. */
14
18
  sunken?: boolean;
15
- /*
16
- * A panel is one track of a flex column and takes what is left of it; a card is a box with a
17
- * height of its own, and `grow` means nothing to the block that lays it out.
18
- */
19
+
20
+ /* A panel is one track of a flex column; a card is a box with a height of its own, where `grow` means nothing. */
19
21
  fill?: 'grow' | 'full';
22
+
20
23
  children: ReactNode;
21
24
  className?: string;
22
25
  ref?: Ref<HTMLDivElement>;
23
26
  }
24
27
 
25
- /* What a panel or a card shows while it holds nothing: a sentence, one icon, at most one button. */
26
28
  export function PanelEmpty({ header, icon, busy = false, action, sunken = false, fill = 'grow', children, className, ref }: PanelEmptyProps) {
27
29
  return (
28
30
  <div ref={ref} className={clsx('grid min-h-0 place-items-center', fill === 'full' ? 'h-full' : 'grow', sunken && 'bg-surface-sunken', className)}>
@@ -4,14 +4,11 @@ import { useRender } from '@base-ui/react/use-render';
4
4
  import { SectionLabel } from './SectionLabel.tsx';
5
5
 
6
6
  export type PanelHeaderProps = useRender.ComponentProps<'header'> & {
7
- /* The panel's name, as a section label first on the bar. Left out when the caller lays out the name itself. */
7
+ /* Left out when the caller lays out the name itself. */
8
8
  title?: ReactNode;
9
9
  };
10
10
 
11
- /*
12
- * The bar across the top of a side panel, as tall as a 48px toolbar beside it: the panel's name,
13
- * its own controls, and a close button last.
14
- */
11
+ /* As tall as a 48px toolbar beside it. */
15
12
  export function PanelHeader({ title, render, className, ref, children, ...props }: PanelHeaderProps) {
16
13
  return useRender({
17
14
  render,
package/src/Pill.tsx CHANGED
@@ -1,11 +1,7 @@
1
1
  import type { ReactNode, Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
 
4
- /*
5
- * What the label says about the thing it names, drawn as its ground. Every ground is an alpha over
6
- * whatever the pill stands on, the way a border is: in the dark theme it lifts a shade off the row
7
- * under it, in the light theme it sinks one. A solid sunken ground read as a hole on a dark row.
8
- */
4
+ /* Every ground is an alpha over whatever the pill stands on, the way a border is: a solid sunken ground read as a hole on a dark row. */
9
5
  const TONES = {
10
6
  muted: 'bg-text/7 text-text-muted',
11
7
  /* A step further off the ground, for a label that should stand out as a control. */
@@ -34,18 +30,19 @@ export interface PillProps {
34
30
  tone?: keyof typeof TONES;
35
31
  shape?: keyof typeof SHAPES;
36
32
  size?: 'md' | 'sm';
37
- /* A branch name or a count reads better in the monospace face. */
38
33
  mono?: boolean;
34
+
39
35
  /* With a handler the pill is a button; without one it is a label. */
40
36
  onClick?: () => void;
41
- /* Makes the button a toggle, for a switch of what a surface shows. */
37
+
38
+ /* Makes the button a toggle. */
42
39
  pressed?: boolean;
40
+
43
41
  disabled?: boolean;
44
42
  className?: string;
45
43
  ref?: Ref<HTMLElement>;
46
44
  }
47
45
 
48
- /* The small rounded label in a header, a sidebar row or a settings line: a count, a branch, a status. */
49
46
  export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'md', mono = false, onClick, pressed, disabled, className, ref }: PillProps) {
50
47
  // Never broken inside itself: in a row that wraps, a pill that does not fit moves to the next line whole.
51
48
  const shared = clsx(
@@ -56,6 +53,7 @@ export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'm
56
53
  mono && 'font-mono',
57
54
  className
58
55
  );
56
+
59
57
  if (!onClick) {
60
58
  return (
61
59
  <span ref={ref} className={shared}>
@@ -64,6 +62,7 @@ export function Pill({ icon, children, tone = 'muted', shape = 'pill', size = 'm
64
62
  </span>
65
63
  );
66
64
  }
65
+
67
66
  return (
68
67
  <button
69
68
  ref={ref as Ref<HTMLButtonElement>}
@@ -10,14 +10,19 @@ export interface ProjectSwitcherItem {
10
10
  id: string;
11
11
  name: string;
12
12
  icon?: ReactNode;
13
+
13
14
  /* The folder or other details, shown in a tooltip so the row stays on one line. */
14
15
  description?: ReactNode;
15
- /* A machine or workspace name at the end of the row. */
16
+
17
+ /* At the end of the row. */
16
18
  hint?: ReactNode;
19
+
17
20
  disabled?: boolean;
21
+
18
22
  /* A stale or disconnected project can still be selected to try opening it. */
19
23
  muted?: boolean;
20
- /* Menu items for the submenu beside this project's row. */
24
+
25
+ /* For the submenu beside this project's row. */
21
26
  actions?: ReactNode;
22
27
  }
23
28
 
@@ -26,14 +31,19 @@ export interface ProjectSwitcherProps<Item extends ProjectSwitcherItem = Project
26
31
  projects: readonly Item[];
27
32
  recentProjects?: readonly Item[];
28
33
  onSelect(project: Item, event: MouseEvent<HTMLElement>): void;
29
- /* Before the current project's icon, such as the machine it runs on. */
34
+
35
+ /* Before the current project's icon. */
30
36
  leading?: ReactNode;
31
- /* Menu items in a group of their own between the open projects and Recent projects. */
37
+
38
+ /* A group of their own between the open projects and Recent projects. */
32
39
  before?: ReactNode;
33
- /* Menu items in one group with Recent projects, such as opening a folder or creating a project. */
40
+
41
+ /* In one group with Recent projects. */
34
42
  children?: ReactNode;
43
+
35
44
  /* What the trigger reads while `current` is null; next to a current project, it comes before that name in the accessible name. */
36
45
  label?: string;
46
+
37
47
  disabled?: boolean;
38
48
  open?: boolean;
39
49
  defaultOpen?: boolean;
@@ -83,7 +93,7 @@ export function ProjectSwitcher<Item extends ProjectSwitcherItem>({
83
93
  );
84
94
  }
85
95
 
86
- /* The rows of the popup, apart from it so they render without an open menu. */
96
+ /* Apart from the popup, so the rows render without an open menu. */
87
97
  export function ProjectSwitcherGroups<Item extends ProjectSwitcherItem>({
88
98
  projects,
89
99
  recentProjects = [],
@@ -11,56 +11,59 @@ import { useAsyncAction } from './useAsyncAction.ts';
11
11
  export interface PromptDialogProps {
12
12
  open: boolean;
13
13
  title: string;
14
- /* The mark beside the title, for a dialog that names the thing it is about. */
15
14
  titleIcon?: LucideIcon;
16
15
  description?: ReactNode;
17
- /* A one line field the answer is typed in; without it the dialog is a confirm and nothing else. */
16
+
17
+ /* Without it the dialog is a confirm and nothing else. */
18
18
  field?: {
19
- /* Drawn above the field. Without one only a screen reader hears the name, from `ariaLabel`. */
19
+ /* Without one only a screen reader hears the name, from `ariaLabel`. */
20
20
  label?: string;
21
+
21
22
  ariaLabel?: string;
22
23
  initial?: string;
23
24
  placeholder?: string;
24
- /* A branch, a path or anything else read back letter by letter. */
25
25
  mono?: boolean;
26
26
  maxLength?: number;
27
27
  };
28
- /* A second, taller field under it, for the body of a pull request. */
28
+
29
+ /* A second, taller field under it. */
29
30
  area?: { label: string; initial?: string; placeholder?: string };
31
+
30
32
  confirmLabel: string;
31
- /* What the button says while the step runs, for one slow enough that a quiet button is not enough. */
32
33
  confirmBusyLabel?: string;
33
34
  confirmIcon?: LucideIcon;
34
35
  danger?: boolean;
35
- /* The caller runs the step somewhere else and says when it is busy; a step this dialog awaits says so itself. */
36
+
37
+ /* For a step the caller runs elsewhere; a step this dialog awaits says so itself. */
36
38
  busy?: boolean;
37
- /* Lets the field be confirmed empty, for a list that may be cleared or a name with a default. */
39
+
38
40
  allowEmpty?: boolean;
41
+
39
42
  /* A reason this question cannot be answered at all, beside the one an empty field is. */
40
43
  confirmDisabled?: boolean;
41
- /* A second way out next to the confirm, such as merging before removing. */
44
+
42
45
  secondary?: { label: string; onClick(): void };
43
- /* Over another dialog, so what is under it is dimmed a second time. */
44
46
  nested?: boolean;
45
- /* Whatever this question needs beyond a field, between the description and the buttons. */
47
+
48
+ /* Between the description and the buttons. */
46
49
  children?: ReactNode;
47
- /* Told what was typed. A rejection stays on screen as the reason and leaves the dialog open. */
50
+
51
+ /* A rejection stays on screen as the reason and leaves the dialog open. */
48
52
  onConfirm(value: string, body: string): void | Promise<unknown>;
49
- /* What the failure line reads when the rejection carries no sentence of its own. */
53
+
54
+ /* For a rejection that carries no sentence of its own. */
50
55
  fallbackMessage?: string;
51
- /* Only ever told `false`: Cancel, Escape or a click outside. The dialog never closes itself. */
56
+
57
+ /* Only ever told `false`: the dialog never closes itself. */
52
58
  onOpenChange(open: boolean): void;
53
- /* Extra on the popup. */
59
+
60
+ /* On the popup. */
54
61
  className?: string;
62
+
55
63
  ref?: Ref<HTMLDivElement>;
56
64
  }
57
65
 
58
- /*
59
- * The one dialog a question is asked in: a name to type, or a warning to agree with. Every confirm
60
- * and every rename shares it, so none of them grows a layout of its own. It never closes itself,
61
- * because what a confirm leads to is the caller's business: an action that asks a second question
62
- * would be shut out by a close of its own.
63
- */
66
+ /* It never closes itself: an action that asks a second question would be shut out by a close of its own. */
64
67
  export function PromptDialog({
65
68
  open,
66
69
  title,
@@ -85,20 +88,21 @@ export function PromptDialog({
85
88
  ref
86
89
  }: PromptDialogProps) {
87
90
  const { t } = useTranslation('ui');
88
- /* The dialog stays mounted between questions, so every opening starts from what it was handed.
89
- The token is what it was handed, so a new question resets the fields in the same render that
90
- shows it and not in a second one after. */
91
+ /* The dialog stays mounted between questions; a new question resets the fields in the render that shows it. */
91
92
  const token = open ? `${field?.initial ?? ''}\u0000${area?.initial ?? ''}` : '';
92
93
  const [draft, setDraft] = useState({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
93
94
  const step = useAsyncAction(fallbackMessage);
95
+
94
96
  if (draft.token !== token) {
95
97
  setDraft({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
96
98
  step.clear();
97
99
  }
98
- const value = draft.value;
99
- const body = draft.body;
100
+
101
+ const { value, body } = draft;
100
102
  const working = busy || step.busy;
103
+
101
104
  const setValue = (next: string): void => setDraft({ ...draft, value: next });
105
+
102
106
  const setBody = (next: string): void => setDraft({ ...draft, body: next });
103
107
 
104
108
  const submit = (): void => {
@@ -131,6 +135,7 @@ export function PromptDialog({
131
135
  onKeyDown={(event) => {
132
136
  // Whatever listens on the window has keys of its own, and a name may hold any of them.
133
137
  event.stopPropagation();
138
+
134
139
  if (event.key === 'Enter') {
135
140
  event.preventDefault();
136
141
  submit();