@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
@@ -10,19 +10,22 @@ const TICK_MS = 30_000;
10
10
  export interface RelativeTimeProps {
11
11
  /* `null`, `undefined` or an invalid date for a moment that is not known, drawn as a dash. */
12
12
  at: Date | number | null | undefined;
13
+
13
14
  /* Off inside a row that is the focus target itself: no tab stop of its own, and the row shows the exact time. */
14
15
  tooltip?: boolean;
16
+
15
17
  tooltipSide?: TooltipSide;
16
18
  className?: string;
17
19
  ref?: Ref<HTMLElement>;
18
20
  }
19
21
 
20
- /* A moment as its age, `3m ago`, counting on while it is on screen, with the exact time on hover and on keyboard focus. */
22
+ /* Counts on while it is on screen, with the exact time on hover and on keyboard focus. */
21
23
  export function RelativeTime({ at, tooltip = true, tooltipSide, className, ref }: RelativeTimeProps) {
22
24
  // The formatters read the locale from a module, not from an input, so a compiled render would keep its first words.
23
25
  'use no memo';
24
26
  useFormatLocale();
25
27
  const now = useNow(TICK_MS);
28
+
26
29
  if (at === null || at === undefined || Number.isNaN(new Date(at).getTime())) {
27
30
  return (
28
31
  <span ref={ref} className={className}>
@@ -30,6 +33,7 @@ export function RelativeTime({ at, tooltip = true, tooltipSide, className, ref }
30
33
  </span>
31
34
  );
32
35
  }
36
+
33
37
  if (!tooltip) {
34
38
  return (
35
39
  <time ref={ref as Ref<HTMLTimeElement>} dateTime={new Date(at).toISOString()} className={clsx('whitespace-nowrap', className)}>
@@ -37,6 +41,7 @@ export function RelativeTime({ at, tooltip = true, tooltipSide, className, ref }
37
41
  </time>
38
42
  );
39
43
  }
44
+
40
45
  return (
41
46
  <Tooltip label={formatDayClockWithYear(at)} side={tooltipSide}>
42
47
  {/* Text is no tab stop of its own, and a hint that only the pointer can open is lost to the keyboard. */}
@@ -7,34 +7,37 @@ import { useMeasuredWidth } from './useMeasuredWidth.ts';
7
7
  export interface SegmentBarPart {
8
8
  /* Its length, in the unit of the bar's `range`. */
9
9
  value: number;
10
- /* Where it starts, in the unit of the bar's `range`. Defaults to the end of the part before, or 0 for the first;
11
- a later start leaves the stretch in between empty. Parts stay in order. */
10
+
11
+ /* Defaults to the end of the part before, or 0 for the first; a later start leaves the stretch in between empty. Parts stay in order. */
12
12
  start?: number;
13
+
13
14
  /* Drawn in the part where there is room for it, and always read by a screen reader. */
14
15
  label?: string;
15
- /* Any CSS color, such as `var(--positive)`, in place of the theme's. It wins over the accent of `current`. */
16
+
17
+ /* Any CSS color. It wins over the accent of `current`. */
16
18
  color?: string;
17
- /* The part a person is at, such as the section under the playhead. */
19
+
18
20
  current?: boolean;
19
21
  }
20
22
 
21
23
  export interface SegmentBarProps {
22
24
  parts: readonly SegmentBarPart[];
23
- /* The stretch the bar shows, in the unit of the parts: the rest of a whole, or the visible part of a zoomed timeline.
24
- Defaults to 0 up to the end of the last part. */
25
+
26
+ /* Defaults to 0 up to the end of the last part. */
25
27
  range?: readonly [from: number, to: number];
26
- /* `md` draws each label in its part, `sm` is a thin bar that shows its labels only as a tooltip. */
28
+
29
+ /* `sm` is a thin bar that shows its labels only as a tooltip. */
27
30
  size?: 'sm' | 'md';
28
- /* Makes every part a button, which calls this with its index in `parts`. */
31
+
32
+ /* Makes every part a button. */
29
33
  onSelect?(index: number): void;
30
- /* The accessible name of the whole. */
34
+
31
35
  label?: string;
32
36
  className?: string;
33
37
  ref?: Ref<HTMLOListElement>;
34
38
  }
35
39
 
36
- /* A whole split into parts, each as wide as it lasts, so they compare at a glance. A part starts exactly at its
37
- place; the gap between two parts that touch comes out of the one before. */
40
+ /* A part starts exactly at its place; the gap between two parts that touch comes out of the one before. */
38
41
  export function SegmentBar({ parts, range, size = 'md', onSelect, label, className, ref }: SegmentBarProps) {
39
42
  const [measure, width] = useMeasuredWidth();
40
43
  const rootRef = mergeRefs(ref, measure);
@@ -42,11 +45,14 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
42
45
  const [cut, setCut] = useState<readonly boolean[]>([]);
43
46
 
44
47
  let end = 0;
48
+
45
49
  const placed = parts.map((part, index) => {
46
50
  const from = part.start ?? end;
47
51
  end = from + Math.max(0, part.value);
52
+
48
53
  return { part, index, from, to: end };
49
54
  });
55
+
50
56
  const [from, to] = range ?? [0, end];
51
57
  const span = to - from;
52
58
  const visible = span > 0 ? placed.filter((placement) => placement.to > from && placement.from < to) : [];
@@ -73,6 +79,7 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
73
79
  const next = visible[i + 1];
74
80
  // Less than a pixel apart they touch, whatever rounding the caller's starts carry.
75
81
  const touches = next !== undefined && ((next.from - partTo) / span) * width < 1;
82
+
76
83
  const fill = clsx(
77
84
  'flex size-full min-w-0 items-center',
78
85
  size === 'sm' ? 'rounded-full' : 'rounded-sm px-1.5 text-left text-xs',
@@ -81,6 +88,7 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
81
88
  !part.current && size === 'md' && 'text-text-muted',
82
89
  onSelect !== undefined && 'cursor-pointer hover:opacity-80'
83
90
  );
91
+
84
92
  const text = (
85
93
  <span
86
94
  ref={(element) => {
@@ -91,7 +99,9 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
91
99
  {part.label}
92
100
  </span>
93
101
  );
102
+
94
103
  const look = { className: fill, style: { backgroundColor: part.color }, 'aria-current': part.current ? true : undefined };
104
+
95
105
  const body =
96
106
  onSelect === undefined ? (
97
107
  <span {...look}>{text}</span>
@@ -100,6 +110,7 @@ export function SegmentBar({ parts, range, size = 'md', onSelect, label, classNa
100
110
  {text}
101
111
  </button>
102
112
  );
113
+
103
114
  return (
104
115
  <li
105
116
  key={index}
package/src/Segmented.tsx CHANGED
@@ -9,7 +9,8 @@ export interface SegmentedOption<T extends string> {
9
9
  id: T;
10
10
  label: string;
11
11
  icon?: LucideIcon;
12
- /* Dims the option and takes it out of the tab order and the arrow keys; the rest of the group stays usable. */
12
+
13
+ /* Takes the option out of the tab order and the arrow keys; the rest of the group stays usable. */
13
14
  disabled?: boolean;
14
15
  }
15
16
 
@@ -17,15 +18,18 @@ export interface SegmentedProps<T extends string> {
17
18
  value: T;
18
19
  onValueChange(id: T): void;
19
20
  options: readonly SegmentedOption<T>[];
20
- /* The accessible name of the group. */
21
21
  label: string;
22
22
  disabled?: boolean;
23
- /* `vertical` stacks the options in a column as wide as its container, for labels too long to stand side by side. */
23
+
24
+ /* `vertical` is as wide as its container, for labels too long to stand side by side. */
24
25
  orientation?: 'horizontal' | 'vertical';
25
- /* 32 pixels high at `md` and 28 at `sm`, the heights of `Input`, `Select` and `Button`. */
26
+
27
+ /* The heights of `Input`, `Select` and `Button`. */
26
28
  size?: 'md' | 'sm';
27
- /* Draws every option with an icon as a square, the label as its accessible name and its tooltip. One without an icon keeps its label. */
29
+
30
+ /* An option with an icon draws as a square, its label as its accessible name and tooltip. One without an icon keeps its label. */
28
31
  iconOnly?: boolean;
32
+
29
33
  className?: string;
30
34
  ref?: Ref<HTMLDivElement>;
31
35
  }
@@ -35,10 +39,7 @@ const SIZE = {
35
39
  sm: { track: 'rounded-md', row: 'min-h-7', option: 'min-h-6 rounded-sm px-2.5 py-0.5', square: 'size-6 rounded-sm', icon: 12 }
36
40
  } as const;
37
41
 
38
- /*
39
- * One of a few options side by side in one sunken track, the picked one lifted out of it. A radio
40
- * group: one tab stop, and the arrows pick and focus the next option, around the ends.
41
- */
42
+ /* A radio group: one tab stop, and the arrows pick and focus the next option, around the ends. */
42
43
  export function Segmented<T extends string>({
43
44
  value,
44
45
  onValueChange,
@@ -55,15 +56,17 @@ export function Segmented<T extends string>({
55
56
  const sizing = SIZE[size];
56
57
  const stop = segmentedStop(options, value);
57
58
 
58
- function move(event: KeyboardEvent<HTMLButtonElement>, index: number) {
59
+ const move = (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {
59
60
  const next = segmentedTarget(options, index, event.key);
61
+
60
62
  if (next === null) {
61
63
  return;
62
64
  }
65
+
63
66
  event.preventDefault();
64
67
  onValueChange(options[next]!.id);
65
68
  event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role="radio"]')[next]?.focus();
66
- }
69
+ };
67
70
 
68
71
  return (
69
72
  <div
@@ -80,6 +83,7 @@ export function Segmented<T extends string>({
80
83
  >
81
84
  {options.map((option, index) => {
82
85
  const square = iconOnly && option.icon !== undefined;
86
+
83
87
  const button = (
84
88
  <button
85
89
  key={option.id}
@@ -100,6 +104,7 @@ export function Segmented<T extends string>({
100
104
  {!square && option.label}
101
105
  </button>
102
106
  );
107
+
103
108
  return square ? (
104
109
  <Tooltip key={option.id} label={option.label} name>
105
110
  {button}
package/src/Select.tsx CHANGED
@@ -16,19 +16,25 @@ export interface SelectProps<T extends string> {
16
16
  value: T | null;
17
17
  onValueChange(value: T): void;
18
18
  items: SelectItem<T>[] | SelectGroup<T>[];
19
+
19
20
  /* The accessible name of the trigger; the row above it usually carries the visible one. */
20
21
  label: string;
22
+
21
23
  /* What the trigger shows while nothing is picked. */
22
24
  placeholder?: string;
25
+
23
26
  size?: 'sm' | 'md';
24
27
  variant?: 'outlined' | 'ghost';
25
28
  disabled?: boolean;
26
29
  align?: 'start' | 'end';
27
30
  truncateValue?: boolean;
31
+
28
32
  /* A search field above the options: `true` always, a number once the list has at least that many options. */
29
33
  searchable?: boolean | number;
34
+
30
35
  /* What the search field says while it is empty, and its accessible name. */
31
36
  searchPlaceholder?: string;
37
+
32
38
  className?: string;
33
39
  ref?: Ref<HTMLButtonElement>;
34
40
  }
@@ -41,10 +47,7 @@ function Row<T extends string>({ item, picked }: { item: SelectItem<T>; picked:
41
47
  );
42
48
  }
43
49
 
44
- /* The one select. A Base UI `Select` in the popup style the menus use, so a settings field and a
45
- picker in a toolbar read the same. Base UI brings the keyboard along (arrows,
46
- Home and End, typeahead, Enter, Escape), which a hand-rolled listbox would have to repeat.
47
- A long list can ask for a search field, which swaps in a combobox with the same trigger and rows. */
50
+ /* A long list can ask for a search field, which swaps in a combobox with the same trigger and rows. */
48
51
  export function Select<T extends string>(props: SelectProps<T>) {
49
52
  const { searchable, items } = props;
50
53
  const count = items.length > 0 && isGroup(items[0]!) ? (items as SelectGroup<T>[]).reduce((sum, group) => sum + group.items.length, 0) : items.length;
@@ -32,13 +32,16 @@ export function SelectSearch<T extends string>({
32
32
  const flat = groups ? groups.flatMap((group) => group.items) : (items as SelectItem<T>[]);
33
33
  const byValue = new Map(flat.map((item) => [item.value, item]));
34
34
  const selected = value === null ? null : (byValue.get(value) ?? null);
35
+
35
36
  const comboboxItems = groups
36
37
  ? groups.map((group) => ({ value: group.label, items: group.items.map((item) => item.value) }))
37
38
  : flat.map((item) => item.value);
39
+
38
40
  const searchLabel = searchPlaceholder ?? t('select.search');
39
41
 
40
42
  const renderRow = (itemValue: T) => {
41
43
  const item = byValue.get(itemValue)!;
44
+
42
45
  return (
43
46
  <Combobox.Item key={item.value} className={rowClassName(item)} value={item.value} disabled={item.disabled}>
44
47
  <RowContent item={item} picked={item.value === value} text={<span className="truncate">{item.label}</span>} />
@@ -15,35 +15,42 @@ const isSeen = (element: Element, rect: DOMRect): boolean => {
15
15
  if (rect.width === 0 || rect.height === 0 || rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth) {
16
16
  return false;
17
17
  }
18
+
18
19
  if (!element.checkVisibility({ opacityProperty: true, visibilityProperty: true })) {
19
20
  return false;
20
21
  }
22
+
21
23
  const hit = document.elementFromPoint((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2);
24
+
22
25
  return hit !== null && element.contains(hit);
23
26
  };
24
27
 
25
28
  const measureHints = (apple: boolean): PlacedHint[] => {
26
29
  const viewport = { width: window.innerWidth, height: window.innerHeight };
27
30
  const hints: PlacedHint[] = [];
31
+
28
32
  for (const [element, target] of shortcutHintTargets()) {
29
33
  const rect = element.getBoundingClientRect();
34
+
30
35
  if (!isSeen(element, rect)) {
31
36
  continue;
32
37
  }
38
+
33
39
  const placement = placeHint(rect, viewport);
40
+
34
41
  hints.push({
35
42
  id: target.id,
36
43
  text: formatShortcut(target.shortcut, apple),
37
44
  style: { left: placement.left, right: placement.right, top: placement.top, translate: `${placement.translateX} ${placement.translateY}` }
38
45
  });
39
46
  }
47
+
40
48
  return hints;
41
49
  };
42
50
 
43
51
  /*
44
- * Every visible button's shortcut, under the button, for as long as the modifier is held on its own.
45
- * Mounted once; the listener sits on the window in the capture phase so a terminal that keeps a key to
46
- * itself still takes the hints down. An embedded page (`<webview>`) has the keyboard to itself and shows none.
52
+ * The listener sits on the window in the capture phase so a terminal that keeps a key to itself still
53
+ * takes the hints down. An embedded page (`<webview>`) has the keyboard to itself and shows none.
47
54
  */
48
55
  export function ShortcutHints() {
49
56
  const [shown, setShown] = useState(false);
@@ -53,15 +60,20 @@ export function ShortcutHints() {
53
60
  useEffect(() => {
54
61
  const hold = new ModifierHold(isApplePlatform(), setShown);
55
62
  holdRef.current = hold;
63
+
56
64
  const onKeyDown = (e: KeyboardEvent): void => hold.keyDown(e);
65
+
57
66
  const onKeyUp = (e: KeyboardEvent): void => hold.keyUp(e);
67
+
58
68
  // A click or a zoom with the modifier down is a gesture, not someone looking for a shortcut.
59
69
  const cancel = (): void => hold.cancel();
70
+
60
71
  window.addEventListener('keydown', onKeyDown, true);
61
72
  window.addEventListener('keyup', onKeyUp, true);
62
73
  window.addEventListener('pointerdown', cancel, true);
63
74
  window.addEventListener('wheel', cancel, { capture: true, passive: true });
64
75
  window.addEventListener('blur', cancel);
76
+
65
77
  return () => {
66
78
  window.removeEventListener('keydown', onKeyDown, true);
67
79
  window.removeEventListener('keyup', onKeyUp, true);
@@ -77,30 +89,39 @@ export function ShortcutHints() {
77
89
  if (!shown) {
78
90
  return;
79
91
  }
92
+
80
93
  const apple = isApplePlatform();
81
94
  let frame = 0;
82
95
  let last = '';
96
+
83
97
  const measure = (): void => {
84
98
  // Focus that moved into a page takes the key-up with it, so the embedder never hears the modifier let go.
85
99
  if (document.activeElement?.tagName === 'WEBVIEW') {
86
100
  holdRef.current?.cancel();
101
+
87
102
  return;
88
103
  }
104
+
89
105
  const next = measureHints(apple);
90
106
  const signature = JSON.stringify(next);
107
+
91
108
  if (signature !== last) {
92
109
  last = signature;
93
110
  setHints(next);
94
111
  }
112
+
95
113
  frame = requestAnimationFrame(measure);
96
114
  };
115
+
97
116
  measure();
117
+
98
118
  return () => cancelAnimationFrame(frame);
99
119
  }, [shown]);
100
120
 
101
121
  if (!shown) {
102
122
  return null;
103
123
  }
124
+
104
125
  return createPortal(
105
126
  <div aria-hidden className="pointer-events-none fixed inset-0 z-(--z-shortcut-hints)">
106
127
  {hints.map((hint) => (
package/src/Skeleton.tsx CHANGED
@@ -4,6 +4,7 @@ import clsx from 'clsx';
4
4
  export interface SkeletonProps {
5
5
  /* The width, and a height other than the one line of 16px. */
6
6
  className?: string;
7
+
7
8
  ref?: Ref<HTMLSpanElement>;
8
9
  }
9
10
 
package/src/Slider.tsx CHANGED
@@ -10,18 +10,21 @@ export interface SliderProps {
10
10
  min: number;
11
11
  max: number;
12
12
  step?: number;
13
+
13
14
  /* Places after the separator, always shown. Defaults to the places `step` is written with. */
14
15
  decimals?: number;
16
+
15
17
  /* Printed right after the number, such as `px` or `%`. */
16
18
  unit?: string;
19
+
17
20
  /* Shown above the track and read as the slider's name. */
18
21
  label: string;
22
+
19
23
  disabled?: boolean;
20
24
  className?: string;
21
25
  ref?: Ref<HTMLDivElement>;
22
26
  }
23
27
 
24
- /* A number picked along a range: the label and the value on one line, the track under them. */
25
28
  export function Slider({ value, onValueChange, min, max, step = 1, decimals = placesOf(step), unit, label, disabled, className, ref }: SliderProps) {
26
29
  const labelId = useId();
27
30
  const reading = formatFixed(value, decimals);
@@ -9,48 +9,50 @@ const TRANSITION_MS = 200;
9
9
 
10
10
  export interface SlidingColumnProps {
11
11
  open: boolean;
12
+
12
13
  /* Already clamped by the caller, which knows what has to stay beside it (`clampColumnSize`). */
13
14
  width: number;
15
+
14
16
  /* Space before the surface, in pixels. Replaces the border and carries the resize handle. */
15
17
  gap?: number;
18
+
16
19
  bounds: { min: number; max(): number };
17
20
  onWidthChange(width: number): void;
18
- /*
19
- * The width lands without the motion, for a column whose place is being restored as the page
20
- * loads; only what a person does afterwards should animate.
21
- */
21
+
22
+ /* For a column whose place is restored as the page loads; only what a person does afterwards should animate. */
22
23
  instant?: boolean;
23
- /*
24
- * The column takes the keyboard itself, for a panel with shortcuts of its own. It is focusable
25
- * by script alone, so opening something in it hands it the keys without a click first.
26
- */
24
+
25
+ /* For a panel with shortcuts of its own. Focusable by script alone, so opening something in it hands it the keys without a click. */
27
26
  body?: {
28
27
  ref: RefObject<HTMLDivElement | null>;
29
28
  onKeyDown(event: ReactKeyboardEvent<HTMLDivElement>): void;
30
29
  };
30
+
31
31
  className?: string;
32
+
32
33
  /* The outer column, for bounds that measure what is beside it rather than the whole window. */
33
34
  ref?: Ref<HTMLElement>;
35
+
34
36
  children?: ReactNode;
35
37
  }
36
38
 
37
39
  /*
38
- * A panel that slides in and out along the right edge. The outer column is 0 wide and inert while
39
- * closed; the contents sit in an inner column of the stored width, so they do not reflow while it
40
- * moves, and they stay mounted until the slide is over, so a close plays out. The left edge is the
41
- * drag handle.
40
+ * The contents sit in an inner column of the stored width, so they do not reflow while the outer one
41
+ * moves, and stay mounted until the slide is over, so a close plays out.
42
42
  */
43
43
  export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, instant = false, body, className, ref, children }: SlidingColumnProps) {
44
44
  /* Closed and done animating. */
45
45
  const [settled, setSettled] = useState(!open);
46
- /* A width that lands without a transition fires no `transitionend`, so the motion it would have
47
- ended is over in the same commit that starts it. */
46
+
47
+ /* A width that lands without a transition fires no `transitionend`. */
48
48
  if (instant && settled !== !open) {
49
49
  setSettled(!open);
50
50
  }
51
+
51
52
  const present = open || !settled;
52
53
  const column = useRef<HTMLElement>(null);
53
54
  const spacing = Math.max(0, Math.round(gap));
55
+
54
56
  const { startResize } = useColumnResize(column, {
55
57
  min: bounds.min + spacing,
56
58
  max: () => bounds.max() + spacing,
@@ -63,8 +65,10 @@ export function SlidingColumn({ open, width, gap = 0, bounds, onWidthChange, ins
63
65
  if (open || settled) {
64
66
  return;
65
67
  }
68
+
66
69
  // Reduced motion and a hidden tab paint no width change, so no `transitionend` arrives.
67
70
  const timer = window.setTimeout(() => setSettled(true), TRANSITION_MS + 50);
71
+
68
72
  return () => {
69
73
  window.clearTimeout(timer);
70
74
  };
package/src/Spinner.tsx CHANGED
@@ -6,6 +6,7 @@ const DOTS = [0, 1, 2];
6
6
  export interface SpinnerProps extends Omit<ComponentProps<'span'>, 'children'> {
7
7
  /* In pixels, on the steps of an icon: 12, 14, 16 or 20. */
8
8
  size?: number;
9
+
9
10
  /* What runs, read by a screen reader. Left out where the words beside it already say so, which hides the spinner as an icon is hidden. */
10
11
  label?: string;
11
12
  }
@@ -10,28 +10,28 @@ import { Tooltip } from './Tooltip.tsx';
10
10
  export interface SplitButtonProps extends Omit<ComponentProps<'button'>, 'ref'> {
11
11
  variant?: ButtonVariant;
12
12
  size?: ButtonSize;
13
+
13
14
  /* The hint on the action half. */
14
15
  tooltip?: ReactNode;
15
- /* The rows of the menu the chevron opens: `Menu.Item`, `Menu.Separator` and the rest of a menu's parts. */
16
+
17
+ /* The rows of the menu the chevron opens. */
16
18
  menu: ReactNode;
19
+
17
20
  /* The accessible name of the chevron, and its hint unless `menuTooltip` says something else. */
18
21
  menuLabel: string;
22
+
19
23
  menuTooltip?: ReactNode;
20
24
  menuDisabled?: boolean;
21
- /* Where the menu lines up under the whole control. */
22
25
  menuAlign?: MenuPopupProps['align'];
23
- /* A width or a layout of the menu's own, such as `w-72`. */
24
26
  menuClassName?: string;
27
+
25
28
  /* On the box around both halves; every other prop goes to the action half. */
26
29
  className?: string;
30
+
27
31
  ref?: ComponentProps<'span'>['ref'];
28
32
  }
29
33
 
30
- /*
31
- * A primary action and a chevron that opens a menu of related ones, drawn as one button with a short
32
- * line between the halves. Each half is a button of its own, so each takes the focus, can be disabled
33
- * and carries its own hint; the menu hangs under the whole control.
34
- */
34
+ /* Each half is a button of its own, so each takes the focus, can be disabled and carries its own hint; the menu hangs under the whole control. */
35
35
  export function SplitButton({
36
36
  variant = 'secondary',
37
37
  size = 'md',
package/src/Stepper.tsx CHANGED
@@ -13,10 +13,13 @@ export interface StepperProps {
13
13
  min: number;
14
14
  max: number;
15
15
  step: number;
16
+
16
17
  /* Places after the separator, always shown: `1,0`, `1,5`. Defaults to the places `step` is written with. */
17
18
  decimals?: number;
19
+
18
20
  /* Printed right after the number, such as `px` or `%`. */
19
21
  unit?: string;
22
+
20
23
  label: string;
21
24
  className?: string;
22
25
  ref?: Ref<HTMLDivElement>;
@@ -25,16 +28,19 @@ export interface StepperProps {
25
28
  /* A number between minus and plus, in one sunken group so the three read as a single control. */
26
29
  export function Stepper({ value, onValueChange, min, max, step, decimals = placesOf(step), unit, label, className, ref }: StepperProps) {
27
30
  const { t } = useTranslation('ui');
31
+
28
32
  const reading = (amount: number) => (
29
33
  <>
30
34
  {formatFixed(amount, decimals)}
31
35
  {unit && <span className="text-text-muted">{unit}</span>}
32
36
  </>
33
37
  );
38
+
34
39
  const nudge = (direction: -1 | 1): void => {
35
40
  const next = Math.round((value + direction * step) * 100) / 100;
36
41
  onValueChange(Math.min(max, Math.max(min, next)));
37
42
  };
43
+
38
44
  return (
39
45
  <ButtonGroup
40
46
  ref={ref}
package/src/Switch.tsx CHANGED
@@ -5,8 +5,10 @@ import { Switch as BaseSwitch } from '@base-ui/react/switch';
5
5
  export interface SwitchProps {
6
6
  checked: boolean;
7
7
  onCheckedChange(checked: boolean): void;
8
+
8
9
  /* The accessible name; the row it sits in usually carries the visible one. */
9
10
  label: string;
11
+
10
12
  disabled?: boolean;
11
13
  className?: string;
12
14
  ref?: Ref<HTMLButtonElement>;
package/src/TextMenu.tsx CHANGED
@@ -15,11 +15,7 @@ export type TextMenuProps = ComponentProps<'div'> & {
15
15
  items?: ReactNode;
16
16
  };
17
17
 
18
- /*
19
- * A block of text and the menu that belongs to it: Copy for what is selected inside it, Select all
20
- * for the whole of it, on Cmd+A as well. Anywhere text can be selected a right-click has to offer
21
- * to copy it, and an app that draws every menu itself offers nothing at all without one of these.
22
- */
18
+ /* An app that draws every menu itself offers no Copy on a right-click without one of these. */
23
19
  export function TextMenu({ children, items, ref, ...rest }: TextMenuProps) {
24
20
  const { t } = useTranslation('ui');
25
21
  const host = useRef<HTMLDivElement>(null);
@@ -30,6 +26,7 @@ export function TextMenu({ children, items, ref, ...rest }: TextMenuProps) {
30
26
  if (!matchesShortcut(EDIT_SHORTCUTS.selectAll, event, isApplePlatform())) {
31
27
  return;
32
28
  }
29
+
33
30
  event.preventDefault();
34
31
  selectAllWithin(host.current);
35
32
  };
package/src/Tile.tsx CHANGED
@@ -6,19 +6,15 @@ import type { Shortcut } from './shortcut.ts';
6
6
  export interface TileProps extends Omit<ComponentProps<'button'>, 'title'> {
7
7
  icon: ReactNode;
8
8
  title: string;
9
- /* One short line under the title: where it acts, or why it waits. */
10
9
  description?: ReactNode;
11
10
  shortcut?: Shortcut;
11
+
12
12
  /* The one tile a screen leads with. */
13
13
  primary?: boolean;
14
- /* `sm` for a narrow column like the sidebar. */
14
+
15
15
  size?: 'md' | 'sm';
16
16
  }
17
17
 
18
- /*
19
- * One thing to start with, as a card that is a button: a start screen and the empty places of an app
20
- * use it, so a place to begin looks the same wherever it is offered.
21
- */
22
18
  export function Tile({ icon, title, description, shortcut, primary = false, size = 'md', className, type = 'button', ...rest }: TileProps) {
23
19
  return (
24
20
  <button