@adecore/ui 0.38.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 (210) 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/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. package/dist/key-value-list/parts.js +1 -5
  59. package/dist/lazy.js +4 -19
  60. package/dist/locales.js +2 -6
  61. package/dist/menu/parts.js +61 -133
  62. package/dist/middle-truncate.js +2 -3
  63. package/dist/placement.js +0 -1
  64. package/dist/popover/parts.js +1 -5
  65. package/dist/popup-layer.js +2 -4
  66. package/dist/prefetch.js +2 -5
  67. package/dist/selection.js +1 -5
  68. package/dist/settings/MasterDetail.js +1 -7
  69. package/dist/settings/SettingsDialog.js +1 -2
  70. package/dist/settings/SettingsRow.js +2 -5
  71. package/dist/settings/SettingsSection.js +1 -1
  72. package/dist/shortcut-hints.js +1 -5
  73. package/dist/shortcut.js +1 -2
  74. package/dist/tabs/overflow.js +1 -6
  75. package/dist/tabs/parts.js +69 -95
  76. package/dist/testing/dedupe.js +5 -7
  77. package/dist/tint.d.ts +2 -0
  78. package/dist/tint.js +11 -0
  79. package/dist/toast-store.js +2 -10
  80. package/dist/tree/use-tree-shift.js +2 -6
  81. package/dist/useColumnResize.js +9 -10
  82. package/dist/useNow.js +2 -9
  83. package/dist/workspace/DropPreview.js +1 -5
  84. package/dist/workspace/preview.js +4 -9
  85. package/package.json +9 -9
  86. package/src/AccentSwatches.tsx +9 -3
  87. package/src/Banner.tsx +5 -11
  88. package/src/Button.tsx +4 -0
  89. package/src/Checkbox.tsx +3 -0
  90. package/src/ChoiceCards.tsx +12 -11
  91. package/src/CloseButton.tsx +1 -0
  92. package/src/ColorSwatch.tsx +7 -8
  93. package/src/ColorWheelPicker.tsx +28 -12
  94. package/src/ColumnResizeHandle.tsx +1 -0
  95. package/src/CopyButton.tsx +6 -0
  96. package/src/DisabledReason.tsx +1 -0
  97. package/src/DockShell.tsx +11 -10
  98. package/src/EmptyState.tsx +10 -6
  99. package/src/ErrorBoundary.tsx +13 -10
  100. package/src/Field.tsx +11 -13
  101. package/src/FileIcon.tsx +4 -4
  102. package/src/Icon.tsx +2 -4
  103. package/src/IconButton.tsx +14 -5
  104. package/src/IconPicker.tsx +30 -15
  105. package/src/Input.tsx +11 -9
  106. package/src/Kbd.tsx +8 -7
  107. package/src/LazyBoundary.tsx +3 -3
  108. package/src/Meter.tsx +7 -2
  109. package/src/MiddleTruncate.tsx +15 -6
  110. package/src/PanelEmpty.tsx +9 -7
  111. package/src/PanelHeader.tsx +2 -5
  112. package/src/Pill.tsx +7 -8
  113. package/src/ProjectSwitcher.tsx +16 -6
  114. package/src/PromptDialog.tsx +31 -26
  115. package/src/RelativeTime.tsx +6 -1
  116. package/src/SegmentBar.tsx +22 -11
  117. package/src/Segmented.tsx +16 -11
  118. package/src/Select.tsx +7 -4
  119. package/src/SelectSearch.tsx +3 -0
  120. package/src/ShortcutHints.tsx +24 -3
  121. package/src/Skeleton.tsx +1 -0
  122. package/src/Slider.tsx +4 -1
  123. package/src/SlidingColumn.tsx +18 -14
  124. package/src/Spinner.tsx +1 -0
  125. package/src/SplitButton.tsx +8 -8
  126. package/src/Stepper.tsx +6 -0
  127. package/src/Switch.tsx +2 -0
  128. package/src/TextMenu.tsx +2 -5
  129. package/src/Tile.tsx +2 -6
  130. package/src/Toasts.tsx +8 -12
  131. package/src/Tooltip.tsx +10 -5
  132. package/src/UIProvider.tsx +7 -7
  133. package/src/Waveform.tsx +41 -12
  134. package/src/Wipe.tsx +5 -6
  135. package/src/ZoomControls.tsx +5 -2
  136. package/src/clipboard.ts +4 -5
  137. package/src/combobox/parts.tsx +18 -14
  138. package/src/command-palette/context.ts +2 -0
  139. package/src/command-palette/index.parts.ts +1 -0
  140. package/src/command-palette/parts.tsx +20 -14
  141. package/src/command-palette/virtual.tsx +21 -0
  142. package/src/dialog/parts.tsx +10 -13
  143. package/src/dialog-layer.ts +5 -3
  144. package/src/error-boundary.ts +7 -20
  145. package/src/file-icon.ts +8 -9
  146. package/src/file-tree/empty-lines.ts +24 -8
  147. package/src/file-tree/helpers.ts +34 -0
  148. package/src/file-tree/highlight.ts +19 -0
  149. package/src/file-tree/index.parts.ts +1 -0
  150. package/src/file-tree/parts.tsx +41 -5
  151. package/src/file-tree/use-empty-lines.ts +22 -5
  152. package/src/file-tree/use-file-tree-shift.tsx +10 -0
  153. package/src/file-tree/use-file-tree.ts +2 -0
  154. package/src/file-tree/use-slots.ts +77 -12
  155. package/src/format/datetime.ts +42 -17
  156. package/src/format/duration.ts +28 -10
  157. package/src/format/index.ts +3 -0
  158. package/src/format/locale.ts +33 -42
  159. package/src/format/number.ts +21 -27
  160. package/src/format/regions.ts +4 -7
  161. package/src/icon-picker.ts +23 -16
  162. package/src/index.ts +9 -0
  163. package/src/key-value-list/parts.tsx +1 -5
  164. package/src/lazy-landings.ts +2 -0
  165. package/src/lazy.tsx +23 -19
  166. package/src/locales.ts +2 -6
  167. package/src/match.ts +14 -0
  168. package/src/menu/parts.tsx +30 -55
  169. package/src/middle-truncate.ts +12 -3
  170. package/src/modality.ts +4 -0
  171. package/src/placement.ts +2 -1
  172. package/src/platform.ts +2 -0
  173. package/src/popover/parts.tsx +3 -5
  174. package/src/popup-layer.ts +6 -4
  175. package/src/prefetch.ts +18 -5
  176. package/src/project-switcher-row.tsx +2 -0
  177. package/src/segmented-keys.ts +6 -0
  178. package/src/select-shared.ts +2 -0
  179. package/src/selection.ts +5 -5
  180. package/src/settings/ConfirmDialog.tsx +3 -0
  181. package/src/settings/MasterDetail.tsx +3 -9
  182. package/src/settings/SettingsDialog.tsx +27 -9
  183. package/src/settings/SettingsRow.tsx +16 -10
  184. package/src/settings/SettingsSection.tsx +10 -5
  185. package/src/settings/index.ts +3 -0
  186. package/src/settings/target.ts +1 -0
  187. package/src/shortcut-hints.ts +10 -5
  188. package/src/shortcut.ts +26 -6
  189. package/src/tabs/overflow.ts +8 -6
  190. package/src/tabs/parts.tsx +50 -26
  191. package/src/testing/dedupe.ts +27 -7
  192. package/src/testing/fake-source.ts +1 -0
  193. package/src/tint.ts +13 -0
  194. package/src/toast-store.ts +20 -14
  195. package/src/tree/parts.tsx +8 -0
  196. package/src/tree/shift.ts +6 -0
  197. package/src/tree/use-tree-shift.tsx +23 -7
  198. package/src/useAsyncAction.ts +8 -0
  199. package/src/useColumnResize.ts +24 -15
  200. package/src/useContentSize.ts +5 -0
  201. package/src/useMeasuredWidth.ts +1 -0
  202. package/src/useNow.ts +6 -11
  203. package/src/workspace/DropPreview.tsx +3 -5
  204. package/src/workspace/SplitView.tsx +109 -0
  205. package/src/workspace/TabStrip.tsx +42 -0
  206. package/src/workspace/Workspace.tsx +5 -0
  207. package/src/workspace/context.ts +1 -0
  208. package/src/workspace/geometry.ts +49 -0
  209. package/src/workspace/model.ts +72 -0
  210. package/src/workspace/preview.ts +15 -15
package/src/Checkbox.tsx CHANGED
@@ -7,10 +7,13 @@ import { Icon } from './Icon.tsx';
7
7
  export interface CheckboxProps {
8
8
  checked: boolean;
9
9
  onCheckedChange(checked: boolean): void;
10
+
10
11
  /* The accessible name; a `<label>` around the box and its text usually carries the visible one. */
11
12
  label: string;
13
+
12
14
  /* Some of the items it stands for are picked and some are not, as on a box that picks every row of a list. */
13
15
  indeterminate?: boolean;
16
+
14
17
  disabled?: boolean;
15
18
  className?: string;
16
19
  ref?: Ref<HTMLButtonElement>;
@@ -8,7 +8,6 @@ import { Icon } from './Icon.tsx';
8
8
  export interface Choice<Value extends string | number> {
9
9
  value: Value;
10
10
  title: string;
11
- /* One line under the title that says what picking it means. */
12
11
  description: string;
13
12
  icon?: LucideIcon;
14
13
  disabled?: boolean;
@@ -18,16 +17,20 @@ export interface ChoiceCardsProps<Value extends string | number> {
18
17
  value: Value;
19
18
  onValueChange(value: Value): void;
20
19
  choices: readonly Choice<Value>[];
21
- /* What the group picks, read by a screen reader since the row has no visible heading of its own. */
20
+
21
+ /* Read by a screen reader, since the row has no visible heading of its own. */
22
22
  label: string;
23
- /* Side by side at the same width, or one under the other for choices whose descriptions run long. */
23
+
24
24
  orientation?: 'horizontal' | 'vertical';
25
- /* A set number of columns, filled row by row, for more choices than one row has room for; takes the place of `orientation`. */
25
+
26
+ /* Filled row by row; takes the place of `orientation`. */
26
27
  columns?: number;
27
- /* Where the radio stands in a card: after the text, or before the icon and the title. */
28
+
28
29
  radio?: 'start' | 'end';
29
- /* What the checked choice still asks for, such as a folder or an address, right under its card; only a vertical group without columns has room for it. */
30
+
31
+ /* What the checked choice still asks for, under its card; only a vertical group without columns has room for it. */
30
32
  detail?: ReactNode;
33
+
31
34
  disabled?: boolean;
32
35
  className?: string;
33
36
  ref?: Ref<HTMLDivElement>;
@@ -35,12 +38,14 @@ export interface ChoiceCardsProps<Value extends string | number> {
35
38
 
36
39
  function ChoiceCard<Value extends string | number>({ choice, radio }: { choice: Choice<Value>; radio: 'start' | 'end' }) {
37
40
  const id = useId();
41
+
38
42
  // As tall as the title's line, so the dot centers on it.
39
43
  const dot = (
40
44
  <span className="flex h-(--text-sm--line-height) shrink-0 items-center" aria-hidden>
41
45
  <span className="size-4 rounded-full border border-border-strong group-data-checked:border-5 group-data-checked:border-accent group-data-checked:bg-accent-text" />
42
46
  </span>
43
47
  );
48
+
44
49
  return (
45
50
  <Radio.Root
46
51
  value={choice.value}
@@ -68,11 +73,6 @@ function ChoiceCard<Value extends string | number>({ choice, radio }: { choice:
68
73
  );
69
74
  }
70
75
 
71
- /*
72
- * One of a few options, each a card that says what it means, for a choice that deserves more than a
73
- * select: the cards stand side by side at the same width, one under the other or in a set number of
74
- * columns, and the arrow keys move between them.
75
- */
76
76
  export function ChoiceCards<Value extends string | number>({
77
77
  value,
78
78
  onValueChange,
@@ -87,6 +87,7 @@ export function ChoiceCards<Value extends string | number>({
87
87
  ref
88
88
  }: ChoiceCardsProps<Value>) {
89
89
  const vertical = columns === undefined && orientation === 'vertical';
90
+
90
91
  return (
91
92
  <RadioGroup
92
93
  ref={ref}
@@ -17,5 +17,6 @@ export function CloseButton({ label, kbd, size, className, ref, ...closes }: Clo
17
17
  if ('dialog' in closes) {
18
18
  return <IconButton ref={ref} icon={X} label={label} kbd={kbd} size={size} className={className} render={<Dialog.Close />} />;
19
19
  }
20
+
20
21
  return <IconButton ref={ref} icon={X} label={label} kbd={kbd} size={size} className={className} onClick={closes.onClick} />;
21
22
  }
@@ -14,21 +14,20 @@ const PICKED_RING = {
14
14
  } as const;
15
15
 
16
16
  export interface ColorSwatchProps extends ComponentProps<'button'> {
17
- /* Any CSS color. Without one the swatch is an outlined circle: "no color", or the button that opens the rest of a palette. */
17
+ /* Without one the swatch is an outlined circle: "no color", or the button that opens the rest of a palette. */
18
18
  color?: string;
19
+
19
20
  picked?: boolean;
20
- /* On the surface behind a popup, or inside the popup itself. */
21
21
  on?: keyof typeof PICKED_RING;
22
- /* Whether a picked swatch draws its tick. Off where the ring alone marks it, such as on a swatch of two tones, where a tick reads as a glitch. */
22
+
23
+ /* Off where the ring alone marks a pick, such as on a swatch of two tones, where a tick reads as a glitch. */
23
24
  tick?: boolean;
24
- /* Another element to be the swatch, such as a menu trigger, or `<Menu.Item unstyled />` so the row's padding and highlight stay off it. */
25
+
26
+ /* Such as `<Menu.Item unstyled />`, so the row's padding and highlight stay off it. */
25
27
  render?: useRender.RenderProp;
26
28
  }
27
29
 
28
- /*
29
- * One color, drawn as the color itself. Its name belongs in a tooltip, so the circle carries a tick
30
- * alone when it is picked. Children replace the tick, such as the ellipsis of a "more colors" swatch.
31
- */
30
+ /* Its name belongs in a tooltip. Children replace the tick, such as the ellipsis of a "more colors" swatch. */
32
31
  export function ColorSwatch({ color, picked = false, on = 'surface', tick = true, render, className, style, ref, children, ...props }: ColorSwatchProps) {
33
32
  return useRender({
34
33
  render: render ?? <button type="button" />,
@@ -6,25 +6,33 @@ import { PopoverPopup, PopoverRoot, PopoverTrigger } from './popover/parts.tsx';
6
6
  import { triggerClassName } from './select-shared.ts';
7
7
 
8
8
  const SWATCH_SIZE = 24;
9
+
9
10
  /* The outer ring stands this far from the middle, further when its swatches would not keep a gap between them otherwise. */
10
11
  const MIN_RADIUS = 97;
12
+
11
13
  const MIN_GAP = 8;
14
+
12
15
  /* Room past the outer ring for the ring a picked swatch wears. */
13
16
  const EDGE = 7;
14
17
 
15
18
  export interface ColorWheelPickerProps<Id extends string> {
16
19
  value: Id;
17
20
  onValueChange(id: Id): void;
18
- /* The one under the pointer or the focus while the wheel is open, so the caller can show it before it is picked; null once none is looked at. */
21
+
22
+ /* The one under the pointer or the focus while the wheel is open; null once none is looked at. */
19
23
  onPreviewChange?(id: Id | null): void;
20
- /* The accessible name of the trigger and of the wheel; the row the picker stands in usually carries the visible one. */
24
+
25
+ /* The accessible name; the row the picker stands in usually carries the visible one. */
21
26
  label: string;
22
- /* The swatches round the wheel, a ring at a time from the outside in, each in its own order from the top. */
27
+
28
+ /* From the outside in, each ring in its own order from the top. */
23
29
  rings: readonly (readonly Id[])[];
24
- /* The one in the middle, if any. */
30
+
25
31
  center?: Id;
32
+
26
33
  /* Any CSS background, a gradient too. */
27
34
  colorOf(id: Id): string;
35
+
28
36
  labelOf(id: Id): string;
29
37
  disabled?: boolean;
30
38
  align?: 'start' | 'end';
@@ -38,17 +46,21 @@ interface Spot<Id> {
38
46
  top: number;
39
47
  }
40
48
 
41
- /* Every swatch's top left corner in whole pixels, the rings evenly apart from the outer one to the middle, and the size of the wheel. */
49
+ /* Top left corners in whole pixels, the rings evenly apart from the outer one to the middle. */
42
50
  function layout<Id>(rings: readonly (readonly Id[])[], center: Id | undefined): { size: number; spots: Spot<Id>[] } {
43
51
  const outer = rings[0]?.length ?? 0;
44
52
  const radius = Math.max(MIN_RADIUS, outer > 1 ? Math.ceil((SWATCH_SIZE + MIN_GAP) / (2 * Math.sin(Math.PI / outer))) : 0);
45
53
  const size = 2 * (radius + SWATCH_SIZE / 2 + EDGE);
46
54
  const middle = (size - SWATCH_SIZE) / 2;
55
+
47
56
  const spot = (id: Id, turn: number, distance: number): Spot<Id> => {
48
57
  const angle = turn * 2 * Math.PI - Math.PI / 2;
58
+
49
59
  return { id, left: Math.round(middle + distance * Math.cos(angle)), top: Math.round(middle + distance * Math.sin(angle)) };
50
60
  };
61
+
51
62
  const spots = rings.flatMap((ring, depth) => ring.map((id, i) => spot(id, i / ring.length, (radius * (rings.length - depth)) / rings.length)));
63
+
52
64
  return { size, spots: center === undefined ? spots : [...spots, spot(center, 0, 0)] };
53
65
  }
54
66
 
@@ -62,11 +74,8 @@ const STEPS: Record<string, (index: number, count: number) => number> = {
62
74
  };
63
75
 
64
76
  /*
65
- * A color picked from a wheel behind a trigger that holds the one in use and lines up with a select.
66
- * While the wheel is open the swatch under the pointer or the focus is reported as a preview, so the
67
- * caller can show it in place; its name stands under the wheel, which is why a swatch carries no
68
- * tooltip. The wheel is one tab stop: the arrows walk it in its order and only Enter, Space or a click
69
- * picks. Every preview ends with null, after a pick as well as on closing without one or on going away while open.
77
+ * The looked-at swatch's name stands under the wheel, so a swatch carries no tooltip. Only Enter, Space
78
+ * or a click picks. Every preview ends with null: after a pick, on closing, or on going away while open.
70
79
  */
71
80
  export function ColorWheelPicker<Id extends string>({
72
81
  value,
@@ -92,16 +101,18 @@ export function ColorWheelPicker<Id extends string>({
92
101
  const tabStop = spots.find((spot) => spot.id === (focused ?? value))?.id ?? spots[0]?.id;
93
102
  // Read by the cleanup below, which runs after the state and the props of its render are gone.
94
103
  const preview = useRef({ looking: false, onPreviewChange });
104
+
95
105
  useEffect(() => {
96
106
  preview.current.onPreviewChange = onPreviewChange;
97
107
  });
108
+
98
109
  const look = (id: Id | null): void => {
99
110
  setLooking(id);
100
111
  preview.current.looking = id !== null;
101
112
  onPreviewChange?.(id);
102
113
  };
103
- // A picker taken away while a color is previewed (its dialog closed under it) gets no close of its
104
- // popover, so it ends the preview itself rather than leave the caller showing a color nobody picked.
114
+
115
+ // A picker taken away while a color is previewed gets no close of its popover, so it ends the preview itself.
105
116
  useEffect(
106
117
  () => () => {
107
118
  if (preview.current.looking) {
@@ -110,21 +121,26 @@ export function ColorWheelPicker<Id extends string>({
110
121
  },
111
122
  []
112
123
  );
124
+
113
125
  const close = (): void => {
114
126
  setOpen(false);
115
127
  setFocused(null);
116
128
  look(null);
117
129
  };
130
+
118
131
  const step = (event: KeyboardEvent<HTMLDivElement>): void => {
119
132
  const next = STEPS[event.key];
120
133
  const swatches = [...(wheel.current?.querySelectorAll<HTMLElement>('[role="radio"]') ?? [])];
121
134
  const index = swatches.indexOf(event.target as HTMLElement);
135
+
122
136
  if (next === undefined || index < 0) {
123
137
  return;
124
138
  }
139
+
125
140
  event.preventDefault();
126
141
  swatches[next(index, swatches.length)]?.focus();
127
142
  };
143
+
128
144
  return (
129
145
  <PopoverRoot
130
146
  open={open}
@@ -5,6 +5,7 @@ import type { ColumnEdge } from './useColumnResize.ts';
5
5
  export interface ColumnResizeHandleProps {
6
6
  /* The edge the column hangs from, as handed to `useColumnResize`; the handle sits on the other one. */
7
7
  from: ColumnEdge;
8
+
8
9
  onPointerDown(event: ReactPointerEvent<HTMLElement>): void;
9
10
  className?: string;
10
11
  style?: CSSProperties;
@@ -11,8 +11,10 @@ const COPIED_MS = 1500;
11
11
 
12
12
  interface CopyButtonBase {
13
13
  text: string;
14
+
14
15
  /* The accessible name and the tooltip of the icon, or the word on the button with `showLabel`. */
15
16
  label?: string;
17
+
16
18
  disabled?: boolean;
17
19
  className?: string;
18
20
  ref?: Ref<HTMLButtonElement>;
@@ -42,12 +44,15 @@ export function CopyButton(props: CopyButtonProps) {
42
44
  if (copiedAt === null) {
43
45
  return;
44
46
  }
47
+
45
48
  const timer = window.setTimeout(() => setCopiedAt(null), COPIED_MS);
49
+
46
50
  return () => window.clearTimeout(timer);
47
51
  }, [copiedAt]);
48
52
 
49
53
  const copied = copiedAt !== null;
50
54
  const name = label ?? t('action.copy');
55
+
51
56
  const copy = async (): Promise<void> => {
52
57
  // A refusal goes unconfirmed and unreported: a person can do nothing about it (`clipboard.ts`).
53
58
  if (await copyText(text)) {
@@ -73,6 +78,7 @@ export function CopyButton(props: CopyButtonProps) {
73
78
  onClick={() => void copy()}
74
79
  />
75
80
  );
81
+
76
82
  return (
77
83
  <>
78
84
  {button}
@@ -14,6 +14,7 @@ export function DisabledReason({ reason, children }: DisabledReasonProps) {
14
14
  if (reason === null) {
15
15
  return children;
16
16
  }
17
+
17
18
  return (
18
19
  <Tooltip label={reason} side="right">
19
20
  {children}
package/src/DockShell.tsx CHANGED
@@ -7,21 +7,16 @@ const REVEAL_ZONE = 120;
7
7
 
8
8
  export interface DockShellProps extends HTMLAttributes<HTMLDivElement> {
9
9
  children: ReactNode;
10
- /* The dock waits below the window's edge until the pointer comes down to it. */
11
10
  autoHide?: boolean;
12
- /* Classes for the bar itself, such as letting a dock with many buttons wrap. */
13
11
  barClassName?: string;
14
- /* Told whenever the bar slides away or comes back, for what stands on top of it. */
15
12
  onHiddenChange?: (hidden: boolean) => void;
16
13
  ref?: Ref<HTMLDivElement>;
17
14
  }
18
15
 
19
16
  /*
20
- * The floating bar at the bottom of a canvas-like view, and the one place that knows how to get out
21
- * of the way. With `autoHide` it waits below the edge until the pointer comes down to it, an open
22
- * menu keeps it up (it would take its own popup with it), and a keyboard reaches it by tabbing. The
23
- * buttons stay in the tab order while it is out of sight, so focus brings it back. Something that
24
- * moves with the dock marks itself `data-holds-dock`, so reaching for it does not hide the dock.
17
+ * An open menu keeps a hiding dock up, or it would take its own popup with it. The buttons stay in the
18
+ * tab order while out of sight, so focus brings it back. Something that moves with the dock marks itself
19
+ * `data-holds-dock`, so reaching for it does not hide the dock.
25
20
  */
26
21
  export function DockShell({ children, autoHide = false, className, barClassName, onHiddenChange, ref, ...rest }: DockShellProps) {
27
22
  const barRef = useRef<HTMLDivElement>(null);
@@ -31,23 +26,27 @@ export function DockShell({ children, autoHide = false, className, barClassName,
31
26
  if (!autoHide) {
32
27
  return;
33
28
  }
29
+
34
30
  let pointerNear = false;
31
+
35
32
  const evaluate = (): void => {
36
33
  const bar = barRef.current;
37
34
  const holds = bar !== null && (bar.querySelector('[data-popup-open]') !== null || bar.querySelector(':focus-visible') !== null);
38
35
  setRevealed(pointerNear || holds);
39
36
  };
37
+
40
38
  const onMove = (event: PointerEvent): void => {
41
- /* Something that moves with the dock would slide away under the pointer reaching for it,
42
- so over one the dock stays as it is. */
43
39
  if (event.target instanceof Element && event.target.closest('[data-holds-dock]') !== null) {
44
40
  return;
45
41
  }
42
+
46
43
  pointerNear = window.innerHeight - event.clientY <= REVEAL_ZONE;
47
44
  evaluate();
48
45
  };
46
+
49
47
  window.addEventListener('pointermove', onMove);
50
48
  window.addEventListener('focusin', evaluate);
49
+
51
50
  return () => {
52
51
  window.removeEventListener('pointermove', onMove);
53
52
  window.removeEventListener('focusin', evaluate);
@@ -56,9 +55,11 @@ export function DockShell({ children, autoHide = false, className, barClassName,
56
55
  }, [autoHide]);
57
56
 
58
57
  const hidden = autoHide && !revealed;
58
+
59
59
  useEffect(() => {
60
60
  onHiddenChange?.(hidden);
61
61
  }, [hidden, onHiddenChange]);
62
+
62
63
  return (
63
64
  <div ref={ref} {...rest} className={clsx('pointer-events-none absolute inset-x-0 bottom-4 flex justify-center', className)}>
64
65
  <Surface
@@ -8,21 +8,25 @@ import { Spinner } from './Spinner.tsx';
8
8
  const ICON_SIZE = 20;
9
9
 
10
10
  export interface EmptyStateProps {
11
- /* A Lucide icon, or a mark of another kind (a provider's logo) that takes a `size`. */
11
+ /* A Lucide icon, or a mark of another kind that takes a `size`. */
12
12
  icon?: LucideIcon | ReactElement<{ size?: number }>;
13
- /* A `Spinner` in place of the icon, for a state that is still loading. */
13
+
14
+ /* A `Spinner` in place of the icon. */
14
15
  busy?: boolean;
15
- /* A heading above the sentence, for a state that is an outcome rather than a list with nothing in it. */
16
+
17
+ /* For a state that is an outcome rather than a list with nothing in it. */
16
18
  title?: ReactNode;
17
- /* One sentence. What is missing, and what puts something there. */
19
+
20
+ /* One sentence: what is missing, and what puts something there. */
18
21
  children: ReactNode;
19
- /* The button or the shortcut that fills the space; kept to one. */
22
+
23
+ /* Kept to one. */
20
24
  action?: ReactNode;
25
+
21
26
  className?: string;
22
27
  ref?: Ref<HTMLDivElement>;
23
28
  }
24
29
 
25
- /* What a list, a canvas or a thread shows before it holds anything. */
26
30
  export function EmptyState({ icon, busy = false, title, children, action, className, ref }: EmptyStateProps) {
27
31
  return (
28
32
  <div ref={ref} className={clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className)}>
@@ -9,16 +9,20 @@ import { errorMessageOf, errorReport, shouldReset, type ResetKeys } from './erro
9
9
  import { Icon } from './Icon.tsx';
10
10
 
11
11
  export interface ErrorBoundaryProps {
12
- /* What failed, as the first line of the message: "Could not show this view". */
12
+ /* The first line of the message: "Could not show this view". */
13
13
  label: string;
14
- /* What the children draw from; the boundary tries again when one of them changes. */
14
+
15
15
  resetKeys?: ResetKeys;
16
- /* A small surface, so its message drops the icon and the spacing. */
16
+
17
+ /* Drops the icon and the spacing, for a small surface. */
17
18
  compact?: boolean;
18
- /* The last resort has nothing around it to fall back to, so it offers a reload as well. */
19
+
20
+ /* For the last resort, which has nothing around it to fall back to. */
19
21
  reload?: boolean;
20
- /* The box of the message; by default it covers the positioned parent. */
22
+
23
+ /* By default the message covers the positioned parent. */
21
24
  className?: string;
25
+
22
26
  children: ReactNode;
23
27
  }
24
28
 
@@ -43,6 +47,7 @@ interface FallbackProps {
43
47
  /* A function component, so the words come from the i18next instance the app renders with. */
44
48
  function ErrorFallback({ label, error, componentStack, compact, reload, className, onRetry }: FallbackProps) {
45
49
  const { t } = useTranslation('ui');
50
+
46
51
  return (
47
52
  <div role="alert" className={clsx('grid place-items-center overflow-auto bg-surface', compact ? 'p-3' : 'p-6', className)}>
48
53
  <div className={clsx('flex max-w-[360px] flex-col items-center text-center', compact ? 'gap-1' : 'gap-2')}>
@@ -68,11 +73,7 @@ function ErrorFallback({ label, error, componentStack, compact, reload, classNam
68
73
  );
69
74
  }
70
75
 
71
- /*
72
- * Keeps a render failure to the subtree it happened in. Without one React unmounts the whole tree,
73
- * and whatever lives in it (an embedded page, a running terminal) goes down with it. Key it so it
74
- * resets only while it holds an error: `resetKeys` for what the children draw from.
75
- */
76
+ /* Without one React unmounts the whole tree, and whatever lives in it (an embedded page, a running terminal) goes down with it. */
76
77
  export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
77
78
  override state: ErrorBoundaryState = CLEAR;
78
79
 
@@ -99,7 +100,9 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
99
100
  if (!this.state.failed) {
100
101
  return this.props.children;
101
102
  }
103
+
102
104
  const { label, compact = false, reload = false, className = 'absolute inset-0' } = this.props;
105
+
103
106
  return (
104
107
  <ErrorFallback
105
108
  label={label}
package/src/Field.tsx CHANGED
@@ -6,6 +6,7 @@ import { SectionLabel } from './SectionLabel.tsx';
6
6
 
7
7
  const HINT = 'text-xs text-text-muted';
8
8
  const ERROR = 'text-xs text-status-error';
9
+
9
10
  // Beside the control the label reads at the size of what is typed, on the middle of the control's first line however tall it grows.
10
11
  const LABEL_BESIDE = 'flex h-8 items-center self-start text-sm text-text-muted';
11
12
 
@@ -13,7 +14,6 @@ const isShown = (line: ReactNode): boolean => line !== undefined && line !== nul
13
14
 
14
15
  export type FieldHintProps = useRender.ComponentProps<'p'>;
15
16
 
16
- /* The line under a field that says what goes in it. */
17
17
  export function FieldHint({ render, className, ref, ...props }: FieldHintProps) {
18
18
  return useRender({
19
19
  render,
@@ -25,7 +25,7 @@ export function FieldHint({ render, className, ref, ...props }: FieldHintProps)
25
25
 
26
26
  export type FormErrorProps = useRender.ComponentProps<'p'>;
27
27
 
28
- /* What went wrong in a dialog or a form. An alert, so a screen reader hears it arrive. */
28
+ /* An alert, so a screen reader hears it arrive. */
29
29
  export function FormError({ render, className, ref, ...props }: FormErrorProps) {
30
30
  return useRender({
31
31
  render,
@@ -48,6 +48,7 @@ function FieldLabel({ id, group, horizontal, children }: { id: string; group: bo
48
48
  </label>
49
49
  );
50
50
  }
51
+
51
52
  return group ? (
52
53
  <SectionLabel id={`${id}-label`}>{children}</SectionLabel>
53
54
  ) : (
@@ -58,26 +59,22 @@ function FieldLabel({ id, group, horizontal, children }: { id: string; group: bo
58
59
  export interface FieldProps {
59
60
  label?: ReactNode;
60
61
  hint?: ReactNode;
62
+
61
63
  /* Shown under the control and marks it invalid; null or empty shows nothing. */
62
64
  error?: ReactNode;
63
- /*
64
- * For what a `<label>` cannot point at: a `Segmented`, a `Select`, cards to choose from, a path
65
- * in a box with a button beside it. The label then names a group around it, which the hint and
66
- * the error describe.
67
- */
65
+
66
+ /* For what a `<label>` cannot point at, such as a `Segmented`: the label then names a group around it. */
68
67
  group?: boolean;
69
- /* `horizontal` puts the label in a column of its own beside the control, for a form in a dialog where a stack of fields would scroll. */
68
+
70
69
  orientation?: 'vertical' | 'horizontal';
71
70
  className?: string;
72
71
  ref?: Ref<HTMLDivElement>;
73
- /* The control. An `Input` or a `TextArea` connects to the label and the lines under it by itself, unless the field is a `group`. */
72
+
73
+ /* An `Input` or a `TextArea` connects to the label and the lines under it by itself, unless the field is a `group`. */
74
74
  children: ReactNode;
75
75
  }
76
76
 
77
- /*
78
- * A labelled control, or a labelled group, with an optional hint and error under it, stacked 6px apart.
79
- * Beside each other the label column is 112px, so the controls of every field in a form line up.
80
- */
77
+ /* Horizontal, the label column is 112px, so the controls of every field in a form line up. */
81
78
  export function Field({ label, hint, error, group = false, orientation = 'vertical', className, ref, children }: FieldProps) {
82
79
  const horizontal = orientation === 'horizontal';
83
80
  const id = useId();
@@ -87,6 +84,7 @@ export function Field({ label, hint, error, group = false, orientation = 'vertic
87
84
  const lines = [hasHint ? `${id}-hint` : null, hasError ? `${id}-error` : null].filter((part) => part !== null);
88
85
  const describedBy = lines.length > 0 ? lines.join(' ') : undefined;
89
86
  const control = group ? null : { id: `${id}-control`, describedBy, invalid: hasError };
87
+
90
88
  return (
91
89
  <div
92
90
  ref={ref}
package/src/FileIcon.tsx CHANGED
@@ -5,20 +5,20 @@ import { fileIconFor, mountFileIconSprite } from './file-icon.ts';
5
5
  export interface FileIconProps {
6
6
  /* Absolute or relative; only the last segment decides which icon this is. */
7
7
  path: string;
8
+
8
9
  size?: number;
9
10
  className?: string;
10
11
  ref?: Ref<SVGSVGElement>;
11
12
  }
12
13
 
13
14
  /*
14
- * The icon a file gets in a file tree, drawn anywhere else the same file's name shows up. It is the
15
- * one place the library steps outside Lucide. The glyphs are Seti's, and so are their colors, because
16
- * a TypeScript blue or a Vue green is the mark of the file type, not a theme choice. Decorative like
17
- * every other icon here, so the name beside it does the reading.
15
+ * The one place the library steps outside Lucide. The glyphs are Seti's, and so are their colors: a
16
+ * TypeScript blue is the mark of the file type, not a theme choice. Decorative like every other icon here.
18
17
  */
19
18
  export function FileIcon({ path, size = 16, className, ref }: FileIconProps) {
20
19
  // Before the browser paints, so a `<use>` never points at a symbol that is not in the document yet.
21
20
  useInsertionEffect(mountFileIconSprite, []);
21
+
22
22
  return (
23
23
  <svg ref={ref} className={clsx('file-icon', className)} width={size} height={size} viewBox="0 0 16 16" aria-hidden>
24
24
  <use href={`#${fileIconFor(path)}`} />
package/src/Icon.tsx CHANGED
@@ -13,10 +13,8 @@ export interface IconProps {
13
13
  ref?: Ref<SVGSVGElement>;
14
14
  }
15
15
 
16
- /* One Lucide icon, sized in pixels. Icons here are decorative. The name a screen reader reads
17
- sits on the button or the label next to it, so the glyph stays hidden. */
16
+ /* Decorative: the name a screen reader reads sits on the button or the label next to it. */
18
17
  export function Icon({ icon: Glyph, size = 16, className, ref }: IconProps) {
19
- // An inline svg sits on the text baseline, which drops a square glyph below the words next to
20
- // it; the middle of the box against the middle of the text is what reads as aligned.
18
+ // An inline svg sits on the text baseline, which drops a square glyph below the words next to it.
21
19
  return <Glyph ref={ref} size={size} strokeWidth={STROKE_WIDTH} className={clsx('align-middle', className)} aria-hidden />;
22
20
  }
@@ -13,26 +13,33 @@ export type { IconButtonSize };
13
13
  export interface IconButtonProps extends Omit<ComponentProps<'button'>, 'children'> {
14
14
  /* Left out for a button that draws something other than one Lucide glyph, handed in as children. */
15
15
  icon?: LucideIcon;
16
+
16
17
  /* The accessible name, and the tooltip unless `tooltip` says otherwise. */
17
18
  label: string;
19
+
18
20
  /* A tooltip that says more than the name (a reason it is disabled), or `false` for none at all. */
19
21
  tooltip?: ReactNode | false;
22
+
20
23
  kbd?: Shortcut | string;
21
24
  tooltipSide?: TooltipSide;
22
25
  size?: IconButtonSize;
23
- /* A toggle that is on, drawn as a pressed key. Use `aria-pressed` instead where the button is a real toggle. */
26
+
27
+ /* Drawn as a pressed key. Use `aria-pressed` instead where the button is a real toggle. */
24
28
  active?: boolean;
25
- /* A `Spinner` in place of the icon, for a step that is running. */
29
+
30
+ /* A `Spinner` in place of the icon. */
26
31
  busy?: boolean;
27
- /* Classes for the icon rather than the button: a tone of its own, a turn, a fill. The size stays the button's. */
32
+
33
+ /* For the icon rather than the button. The size stays the button's. */
28
34
  iconClassName?: string;
35
+
29
36
  /* Drawn after the icon: a count, or a word beside it together with `w-auto`. */
30
37
  children?: ReactNode;
31
- /* Another element to be the button, such as a menu trigger: `render={<Menu.Trigger />}`. */
38
+
32
39
  render?: useRender.RenderProp;
33
40
  }
34
41
 
35
- /* Every button that is an icon and no word. A square, and the one place the icon inside it is sized. */
42
+ /* The one place the icon inside an icon button is sized. */
36
43
  export function IconButton({
37
44
  icon,
38
45
  label,
@@ -69,9 +76,11 @@ export function IconButton({
69
76
  )
70
77
  }
71
78
  });
79
+
72
80
  if (tooltip === false) {
73
81
  return button;
74
82
  }
83
+
75
84
  return (
76
85
  <Tooltip label={tooltip ?? label} kbd={kbd} side={tooltipSide}>
77
86
  {button as ReactElement<Record<string, unknown>>}