@adecore/ui 0.31.12 → 0.32.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 (163) hide show
  1. package/dist/AccentSwatches.js +290 -13
  2. package/dist/Banner.js +90 -9
  3. package/dist/Button.js +114 -22
  4. package/dist/ButtonGroup.js +62 -8
  5. package/dist/Checkbox.js +73 -4
  6. package/dist/ChoiceCards.js +205 -11
  7. package/dist/CloseButton.js +87 -7
  8. package/dist/ColorSwatch.js +132 -15
  9. package/dist/ColorWheelPicker.js +305 -71
  10. package/dist/ColumnResizeHandle.js +43 -8
  11. package/dist/DisabledReason.js +27 -8
  12. package/dist/DockShell.js +144 -37
  13. package/dist/EmptyState.js +95 -6
  14. package/dist/ErrorBoundary.js +254 -31
  15. package/dist/Field.js +349 -31
  16. package/dist/FileIcon.js +67 -6
  17. package/dist/Icon.js +38 -6
  18. package/dist/IconButton.js +176 -20
  19. package/dist/IconPicker.js +344 -73
  20. package/dist/Input.js +233 -14
  21. package/dist/Kbd.js +150 -10
  22. package/dist/LazyBoundary.js +14 -5
  23. package/dist/ListRow.js +75 -15
  24. package/dist/Meter.js +108 -5
  25. package/dist/MiddleTruncate.js +126 -45
  26. package/dist/PanelEmpty.js +64 -5
  27. package/dist/PanelHeader.js +95 -13
  28. package/dist/Pill.js +114 -19
  29. package/dist/ProjectSwitcher.js +225 -12
  30. package/dist/PromptDialog.js +352 -37
  31. package/dist/SectionLabel.js +62 -8
  32. package/dist/SegmentBar.js +197 -36
  33. package/dist/Segmented.js +130 -23
  34. package/dist/Select.js +310 -19
  35. package/dist/SelectSearch.js +344 -29
  36. package/dist/Separator.js +35 -4
  37. package/dist/ShortcutHints.js +146 -76
  38. package/dist/Skeleton.js +31 -4
  39. package/dist/Slider.js +172 -10
  40. package/dist/SlidingColumn.js +194 -34
  41. package/dist/Spinner.js +100 -3
  42. package/dist/Stepper.js +232 -11
  43. package/dist/Surface.js +62 -8
  44. package/dist/Switch.js +60 -6
  45. package/dist/TextMenu.js +212 -17
  46. package/dist/Tile.js +149 -5
  47. package/dist/Toasts.js +391 -33
  48. package/dist/Tooltip.js +153 -16
  49. package/dist/UIProvider.js +62 -15
  50. package/dist/Waveform.js +362 -93
  51. package/dist/Wipe.js +259 -34
  52. package/dist/ZoomControls.js +356 -14
  53. package/dist/class-name.js +1 -1
  54. package/dist/clipboard.js +15 -17
  55. package/dist/context-menu/index.parts.js +1 -1
  56. package/dist/dialog/index.parts.js +1 -1
  57. package/dist/dialog/parts.js +378 -29
  58. package/dist/dialog-layer.js +51 -18
  59. package/dist/error-boundary.js +19 -19
  60. package/dist/error-message.js +1 -1
  61. package/dist/field-context.js +7 -5
  62. package/dist/file-icon.js +23 -21
  63. package/dist/file-tree/helpers.js +133 -119
  64. package/dist/file-tree/index.parts.js +3 -3
  65. package/dist/file-tree/parts.js +418 -127
  66. package/dist/file-tree/style.js +1 -1
  67. package/dist/file-tree/use-file-tree-shift.js +43 -30
  68. package/dist/file-tree/use-file-tree.js +60 -18
  69. package/dist/file-tree/use-slots.js +220 -192
  70. package/dist/floating.js +2 -2
  71. package/dist/format/datetime.js +88 -38
  72. package/dist/format/duration.js +59 -53
  73. package/dist/format/index.js +1 -1
  74. package/dist/format/locale.js +76 -56
  75. package/dist/format/number.js +77 -61
  76. package/dist/format/regions.js +4 -2
  77. package/dist/format/time-zone.js +1 -1
  78. package/dist/icon-button-size.js +12 -2
  79. package/dist/icon-picker.js +70 -61
  80. package/dist/index.js +57 -57
  81. package/dist/key-value-list/index.parts.js +1 -1
  82. package/dist/key-value-list/parts.js +256 -22
  83. package/dist/lazy-landings.js +10 -10
  84. package/dist/lazy.js +84 -71
  85. package/dist/locales/en.js +80 -80
  86. package/dist/locales/nl.js +80 -80
  87. package/dist/locales.js +10 -7
  88. package/dist/menu/index.parts.js +1 -1
  89. package/dist/menu/parts.js +882 -44
  90. package/dist/merge-refs.js +8 -9
  91. package/dist/middle-truncate.js +24 -25
  92. package/dist/modality.js +27 -19
  93. package/dist/placement.js +11 -20
  94. package/dist/platform.js +6 -6
  95. package/dist/popover/index.parts.js +1 -1
  96. package/dist/popover/parts.js +173 -11
  97. package/dist/popup-layer.js +38 -17
  98. package/dist/prefetch.js +78 -79
  99. package/dist/preview-card/index.parts.js +1 -1
  100. package/dist/project-switcher-row.js +153 -12
  101. package/dist/select-row.js +69 -5
  102. package/dist/select-shared.js +6 -6
  103. package/dist/selection.js +16 -16
  104. package/dist/seti-icons.js +413 -413
  105. package/dist/settings/ConfirmDialog.js +55 -8
  106. package/dist/settings/DetailHeader.js +72 -4
  107. package/dist/settings/MasterDetail.js +119 -7
  108. package/dist/settings/SettingsDialog.js +820 -80
  109. package/dist/settings/SettingsRow.js +205 -18
  110. package/dist/settings/SettingsSection.js +96 -6
  111. package/dist/settings/index.js +7 -7
  112. package/dist/settings/target.js +7 -2
  113. package/dist/shortcut-hints.js +73 -55
  114. package/dist/shortcut.js +140 -104
  115. package/dist/step-places.js +1 -1
  116. package/dist/tabs/index.parts.js +1 -1
  117. package/dist/tabs/overflow.js +18 -18
  118. package/dist/tabs/parts.js +780 -166
  119. package/dist/testing/dedupe.js +82 -75
  120. package/dist/testing/fake-source.js +12 -12
  121. package/dist/testing/index.js +1 -1
  122. package/dist/toast-store.js +65 -46
  123. package/dist/tree/index.parts.js +1 -1
  124. package/dist/tree/parts.js +721 -94
  125. package/dist/tree/shift.js +28 -22
  126. package/dist/tree/style.js +2 -2
  127. package/dist/tree/use-tree-shift.js +178 -117
  128. package/dist/useAsyncAction.js +23 -19
  129. package/dist/useColumnResize.js +36 -34
  130. package/dist/useContentSize.js +62 -18
  131. package/dist/useMeasuredWidth.js +14 -3
  132. package/dist/useNow.js +73 -30
  133. package/dist/wipe-split.js +14 -14
  134. package/dist/workspace/DropPreview.js +60 -7
  135. package/dist/workspace/SplitView.js +1025 -441
  136. package/dist/workspace/TabStrip.js +810 -191
  137. package/dist/workspace/Workspace.js +276 -26
  138. package/dist/workspace/context.js +8 -3
  139. package/dist/workspace/geometry.js +173 -150
  140. package/dist/workspace/model.js +313 -250
  141. package/dist/workspace/preview.js +61 -61
  142. package/dist/zoom.js +2 -2
  143. package/package.json +1 -1
  144. package/src/ErrorBoundary.tsx +1 -1
  145. package/src/Field.tsx +4 -3
  146. package/src/IconPicker.tsx +3 -3
  147. package/src/LazyBoundary.tsx +2 -0
  148. package/src/MiddleTruncate.tsx +2 -2
  149. package/src/SegmentBar.tsx +2 -2
  150. package/src/Toasts.tsx +3 -1
  151. package/src/Tooltip.tsx +7 -10
  152. package/src/Waveform.tsx +3 -3
  153. package/src/file-tree/parts.tsx +6 -9
  154. package/src/file-tree/use-file-tree-shift.tsx +6 -9
  155. package/src/lazy.tsx +4 -0
  156. package/src/locales/en.ts +4 -4
  157. package/src/locales/nl.ts +5 -5
  158. package/src/settings/SettingsDialog.tsx +1 -1
  159. package/src/tree/parts.tsx +5 -8
  160. package/src/useContentSize.ts +3 -3
  161. package/src/workspace/SplitView.tsx +2 -2
  162. package/src/workspace/TabStrip.tsx +2 -3
  163. package/src/workspace/Workspace.tsx +2 -5
@@ -6,43 +6,50 @@ export const BODY_RADIUS = 4;
6
6
  /* The stroke is centered on the path, so the path runs half of it inside the box to keep the outer edge on it. */
7
7
  export const DROP_STROKE = 2;
8
8
  function coordinate(value) {
9
- return String(Math.round(value * 100) / 100 + 0);
9
+ return String(Math.round(value * 100) / 100 + 0);
10
10
  }
11
11
  /*
12
12
  * The preview for a split or a trade: the rectangle as a tab with no height, parked just inside its
13
13
  * top left corner so the corner keeps its radius. It is where a tab target grows out of the rectangle.
14
14
  */
15
- export function rectDropPreview({ x, y, width, height }) {
16
- return { x, y, width, height, barHeight: 0, tabLeft: x + BODY_RADIUS, tabWidth: 0, tabTop: y, radius: TAB_RADIUS };
15
+ export function rectDropPreview({
16
+ x,
17
+ y,
18
+ width,
19
+ height
20
+ }) {
21
+ return {
22
+ x,
23
+ y,
24
+ width,
25
+ height,
26
+ barHeight: 0,
27
+ tabLeft: x + BODY_RADIUS,
28
+ tabWidth: 0,
29
+ tabTop: y,
30
+ radius: TAB_RADIUS
31
+ };
17
32
  }
18
33
  /* The preview for a tab: the tab and the body of the cell it lands in, `origin` being the cell's top left in the grid. */
19
34
  export function tabDropPreview(drop, origin) {
20
- return {
21
- x: origin.x,
22
- y: origin.y,
23
- width: drop.width,
24
- height: drop.height,
25
- barHeight: drop.barHeight,
26
- tabLeft: origin.x + drop.left,
27
- tabWidth: drop.tabWidth,
28
- tabTop: origin.y + TAB_TOP_INSET,
29
- radius: TAB_RADIUS
30
- };
35
+ return {
36
+ x: origin.x,
37
+ y: origin.y,
38
+ width: drop.width,
39
+ height: drop.height,
40
+ barHeight: drop.barHeight,
41
+ tabLeft: origin.x + drop.left,
42
+ tabWidth: drop.tabWidth,
43
+ tabTop: origin.y + TAB_TOP_INSET,
44
+ radius: TAB_RADIUS
45
+ };
31
46
  }
32
47
  /* Two shapes that draw the same outline, so a drag that stays in one place can skip a render. */
33
48
  export function sameDropPreview(left, right) {
34
- if (left === null || right === null) {
35
- return left === right;
36
- }
37
- return (left.x === right.x &&
38
- left.y === right.y &&
39
- left.width === right.width &&
40
- left.height === right.height &&
41
- left.barHeight === right.barHeight &&
42
- left.tabLeft === right.tabLeft &&
43
- left.tabWidth === right.tabWidth &&
44
- left.tabTop === right.tabTop &&
45
- left.radius === right.radius);
49
+ if (left === null || right === null) {
50
+ return left === right;
51
+ }
52
+ return left.x === right.x && left.y === right.y && left.width === right.width && left.height === right.height && left.barHeight === right.barHeight && left.tabLeft === right.tabLeft && left.tabWidth === right.tabWidth && left.tabTop === right.tabTop && left.radius === right.radius;
46
53
  }
47
54
  /*
48
55
  * The outline of a tab grown into the body under it, as one closed path so no line runs between the
@@ -50,38 +57,31 @@ export function sameDropPreview(left, right) {
50
57
  * the browser can move one outline into another with a transition on `d`. `inset` is how far the
51
58
  * path runs inside the box, which is half the stroke, so the outer edge of the line is on the box.
52
59
  */
53
- export function dropPreviewPath({ x, y, width, height, barHeight, tabLeft, tabWidth, tabTop, radius }, inset = DROP_STROKE / 2) {
54
- const left = inset;
55
- const right = width - inset;
56
- const bottom = height - inset;
57
- const top = barHeight + inset;
58
- const tabStart = Math.max(left, Math.min(tabLeft - x + inset, right));
59
- const tabEnd = Math.max(tabStart, Math.min(tabLeft - x + tabWidth - inset, right));
60
- const tabY = Math.min(tabTop - y + inset, top);
61
- const tabCorner = Math.max(0, Math.min(radius - inset, (tabEnd - tabStart) / 2, top - tabY));
62
- const bodyCorner = Math.max(0, BODY_RADIUS - inset);
63
- // A tab flush with a side of the box continues that side, so the body has no corner there.
64
- const cornerLeft = Math.min(bodyCorner, tabStart - left);
65
- const cornerRight = Math.min(bodyCorner, right - tabEnd);
66
- const cornerBottom = Math.max(0, Math.min(bodyCorner, (right - left) / 2, (bottom - top) / 2));
67
- const at = (px, py) => `${coordinate(x + px)},${coordinate(y + py)}`;
68
- const arc = (corner, px, py) => `A${coordinate(corner)},${coordinate(corner)} 0 0 1 ${at(px, py)}`;
69
- return [
70
- `M${at(left + cornerLeft, top)}`,
71
- `H${coordinate(x + tabStart)}`,
72
- `V${coordinate(y + tabY + tabCorner)}`,
73
- arc(tabCorner, tabStart + tabCorner, tabY),
74
- `H${coordinate(x + tabEnd - tabCorner)}`,
75
- arc(tabCorner, tabEnd, tabY + tabCorner),
76
- `V${coordinate(y + top)}`,
77
- `H${coordinate(x + right - cornerRight)}`,
78
- arc(cornerRight, right, top + cornerRight),
79
- `V${coordinate(y + bottom - cornerBottom)}`,
80
- arc(cornerBottom, right - cornerBottom, bottom),
81
- `H${coordinate(x + left + cornerBottom)}`,
82
- arc(cornerBottom, left, bottom - cornerBottom),
83
- `V${coordinate(y + top + cornerLeft)}`,
84
- arc(cornerLeft, left + cornerLeft, top),
85
- 'Z'
86
- ].join(' ');
87
- }
60
+ export function dropPreviewPath({
61
+ x,
62
+ y,
63
+ width,
64
+ height,
65
+ barHeight,
66
+ tabLeft,
67
+ tabWidth,
68
+ tabTop,
69
+ radius
70
+ }, inset = DROP_STROKE / 2) {
71
+ const left = inset;
72
+ const right = width - inset;
73
+ const bottom = height - inset;
74
+ const top = barHeight + inset;
75
+ const tabStart = Math.max(left, Math.min(tabLeft - x + inset, right));
76
+ const tabEnd = Math.max(tabStart, Math.min(tabLeft - x + tabWidth - inset, right));
77
+ const tabY = Math.min(tabTop - y + inset, top);
78
+ const tabCorner = Math.max(0, Math.min(radius - inset, (tabEnd - tabStart) / 2, top - tabY));
79
+ const bodyCorner = Math.max(0, BODY_RADIUS - inset);
80
+ // A tab flush with a side of the box continues that side, so the body has no corner there.
81
+ const cornerLeft = Math.min(bodyCorner, tabStart - left);
82
+ const cornerRight = Math.min(bodyCorner, right - tabEnd);
83
+ const cornerBottom = Math.max(0, Math.min(bodyCorner, (right - left) / 2, (bottom - top) / 2));
84
+ const at = (px, py) => `${coordinate(x + px)},${coordinate(y + py)}`;
85
+ const arc = (corner, px, py) => `A${coordinate(corner)},${coordinate(corner)} 0 0 1 ${at(px, py)}`;
86
+ return [`M${at(left + cornerLeft, top)}`, `H${coordinate(x + tabStart)}`, `V${coordinate(y + tabY + tabCorner)}`, arc(tabCorner, tabStart + tabCorner, tabY), `H${coordinate(x + tabEnd - tabCorner)}`, arc(tabCorner, tabEnd, tabY + tabCorner), `V${coordinate(y + top)}`, `H${coordinate(x + right - cornerRight)}`, arc(cornerRight, right, top + cornerRight), `V${coordinate(y + bottom - cornerBottom)}`, arc(cornerBottom, right - cornerBottom, bottom), `H${coordinate(x + left + cornerBottom)}`, arc(cornerBottom, left, bottom - cornerBottom), `V${coordinate(y + top + cornerLeft)}`, arc(cornerLeft, left + cornerLeft, top), 'Z'].join(' ');
87
+ }
package/dist/zoom.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* The percentages a zoom menu offers when the app names none. */
2
2
  export const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
3
3
  /* Compared on the rounded percent the readout shows, so 0.999 still ticks 100%. */
4
- export const activeZoomPreset = (zoom, presets) => presets.find((preset) => preset === Math.round(zoom * 100)) ?? null;
4
+ export const activeZoomPreset = (zoom, presets) => presets.find(preset => preset === Math.round(zoom * 100)) ?? null;
5
5
  /* Ten percent a click, on the readout's own rounded percent, so + and - come back where they were. */
6
- export const steppedZoom = (zoom, by) => Math.round(zoom * 100 + by) / 100;
6
+ export const steppedZoom = (zoom, by) => Math.round(zoom * 100 + by) / 100;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/ui",
3
- "version": "0.31.12",
3
+ "version": "0.32.0",
4
4
  "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -10,7 +10,7 @@ import { Icon } from './Icon.tsx';
10
10
  import { IconButton } from './IconButton.tsx';
11
11
 
12
12
  export interface ErrorBoundaryProps {
13
- /* What failed, as the first line of the message: "This view failed to render". */
13
+ /* What failed, as the first line of the message: "Could not show this view". */
14
14
  label: string;
15
15
  /* What the children draw from; the boundary tries again when one of them changes. */
16
16
  resetKeys?: ResetKeys;
package/src/Field.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useId, useMemo, type ReactNode, type Ref } from 'react';
1
+ import { useId, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useRender } from '@base-ui/react/use-render';
4
4
  import { FieldContext } from './field-context.ts';
@@ -84,8 +84,9 @@ export function Field({ label, hint, error, group = false, orientation = 'vertic
84
84
  const hasLabel = label !== undefined;
85
85
  const hasHint = isShown(hint);
86
86
  const hasError = isShown(error);
87
- const describedBy = [hasHint ? `${id}-hint` : null, hasError ? `${id}-error` : null].filter((part) => part !== null).join(' ') || undefined;
88
- const control = useMemo(() => (group ? null : { id: `${id}-control`, describedBy, invalid: hasError }), [group, id, describedBy, hasError]);
87
+ const lines = [hasHint ? `${id}-hint` : null, hasError ? `${id}-error` : null].filter((part) => part !== null);
88
+ const describedBy = lines.length > 0 ? lines.join(' ') : undefined;
89
+ const control = group ? null : { id: `${id}-control`, describedBy, invalid: hasError };
89
90
  return (
90
91
  <div
91
92
  ref={ref}
@@ -1,4 +1,4 @@
1
- import { useId, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent, type Ref } from 'react';
1
+ import { useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { Ban, Search, X } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
@@ -74,8 +74,8 @@ export function IconPicker({
74
74
  const [focused, setFocused] = useState<string | null>(null);
75
75
  const headed = isGrouped(icons);
76
76
  const framed = searchable || rows !== undefined;
77
- const sections = useMemo(() => sectionsOf(icons), [icons]);
78
- const shown = useMemo(() => filterSections(sections, query, keywords), [sections, query, keywords]);
77
+ const sections = sectionsOf(icons);
78
+ const shown = filterSections(sections, query, keywords);
79
79
  const tabStop = tabStopOf(
80
80
  shown.flatMap((section) => section.icons.map(([name]) => name)),
81
81
  focused,
@@ -12,6 +12,8 @@ export interface LazyBoundaryProps {
12
12
  * lands, so a chunk that takes 20 ms shows after 20 ms.
13
13
  */
14
14
  export function LazyBoundary({ fallback = null, children }: LazyBoundaryProps) {
15
+ // Compiled, the same boundary would come back and the landing would wait for React's own retry after all.
16
+ 'use no memo';
15
17
  useSyncExternalStore(subscribeLandings, readLandings, readLandings);
16
18
  return <Suspense fallback={fallback}>{children}</Suspense>;
17
19
  }
@@ -1,4 +1,4 @@
1
- import { useLayoutEffect, useMemo, useState, type Ref } from 'react';
1
+ import { useLayoutEffect, useState, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { fitMiddle, type MiddleCut } from './middle-truncate.ts';
4
4
  import { mergeRefs } from './merge-refs.ts';
@@ -42,7 +42,7 @@ function sameCut(first: MiddleCut | null, second: MiddleCut | null): boolean {
42
42
  export function MiddleTruncate({ text, tooltip = true, className, ref }: MiddleTruncateProps) {
43
43
  const [element, setElement] = useState<HTMLSpanElement | null>(null);
44
44
  const [cut, setCut] = useState<MiddleCut | null>(null);
45
- const rootRef = useMemo(() => mergeRefs(setElement, ref), [ref]);
45
+ const rootRef = mergeRefs(setElement, ref);
46
46
 
47
47
  useLayoutEffect(() => {
48
48
  if (element === null) {
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState, type Ref } from 'react';
1
+ import { useEffect, useRef, useState, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { mergeRefs } from './merge-refs.ts';
4
4
  import { Tooltip } from './Tooltip.tsx';
@@ -37,7 +37,7 @@ export interface SegmentBarProps {
37
37
  place; the gap between two parts that touch comes out of the one before. */
38
38
  export function SegmentBar({ parts, range, size = 'md', onSelect, label, className, ref }: SegmentBarProps) {
39
39
  const [measure, width] = useMeasuredWidth();
40
- const rootRef = useMemo(() => mergeRefs(ref, measure), [ref, measure]);
40
+ const rootRef = mergeRefs(ref, measure);
41
41
  const labels = useRef<(HTMLSpanElement | null)[]>([]);
42
42
  const [cut, setCut] = useState<readonly boolean[]>([]);
43
43
 
package/src/Toasts.tsx CHANGED
@@ -2,6 +2,7 @@ import { useState, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { CircleAlert, CircleCheck, Trash, X } from 'lucide-react';
5
+ import { useStore } from 'zustand';
5
6
  import { Button } from './Button.tsx';
6
7
  import { Icon } from './Icon.tsx';
7
8
  import { IconButton } from './IconButton.tsx';
@@ -133,7 +134,8 @@ export interface ToastsProps<T extends Toast> {
133
134
  * a deletion after eight, and a failure waits to be read.
134
135
  */
135
136
  export function Toasts<T extends Toast>({ store, footer, className, ref }: ToastsProps<T>) {
136
- const toasts = store((state) => state.toasts);
137
+ // Through `useStore`: the compiler takes a call of the prop itself for a plain function and would skip it on a later draw.
138
+ const toasts = useStore(store, (state) => state.toasts);
137
139
  // Stays mounted with nothing in it: a screen reader skips what a live region says the moment it is inserted.
138
140
  return (
139
141
  <div
package/src/Tooltip.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, type ComponentProps, type ReactElement, type ReactNode } from 'react';
1
+ import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
2
2
  import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
3
3
  import { Kbd } from './Kbd.tsx';
4
4
  import type { Shortcut } from './shortcut.ts';
@@ -37,15 +37,12 @@ export interface TooltipProps {
37
37
  /* A hint on hover and on keyboard focus. Every hint in an app is one of these, never a `title`. */
38
38
  export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }: TooltipProps) {
39
39
  // Held Cmd (Ctrl off macOS) prints this shortcut under the trigger (`ShortcutHints`).
40
- const hintRef = useCallback(
41
- (element: HTMLElement | null) => {
42
- if (element === null || kbd === undefined || typeof kbd === 'string') {
43
- return undefined;
44
- }
45
- return registerShortcutHint(element, kbd);
46
- },
47
- [kbd]
48
- );
40
+ const hintRef = (element: HTMLElement | null) => {
41
+ if (element === null || kbd === undefined || typeof kbd === 'string') {
42
+ return undefined;
43
+ }
44
+ return registerShortcutHint(element, kbd);
45
+ };
49
46
 
50
47
  return (
51
48
  <BaseTooltip.Root disabled={disabled} open={open} onOpenChange={onOpenChange} disableHoverablePopup={children === undefined}>
package/src/Waveform.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef, type KeyboardEvent, type PointerEvent, type Ref } from 'react';
1
+ import { useRef, type KeyboardEvent, type PointerEvent, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { formatClockDuration } from './format/duration.ts';
@@ -80,12 +80,12 @@ export function Waveform({
80
80
  }: WaveformProps) {
81
81
  const { t } = useTranslation('ui');
82
82
  const [measure, width] = useMeasuredWidth();
83
- const rootRef = useMemo(() => mergeRefs(ref, measure), [ref, measure]);
83
+ const rootRef = mergeRefs(ref, measure);
84
84
  const dragged = useRef(0);
85
85
  const [from, to] = range ?? [0, duration];
86
86
  const span = to - from;
87
87
  const count = Math.floor(width / BAR_PITCH);
88
- const bars = useMemo(() => barsOf(levels, duration, from, to, count), [levels, duration, from, to, count]);
88
+ const bars = barsOf(levels, duration, from, to, count);
89
89
 
90
90
  const positionOf = (time: number): number => (span > 0 ? (time - from) / span : 0);
91
91
  const inView = (time: number): boolean => positionOf(time) >= 0 && positionOf(time) <= 1;
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
1
+ import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { useRender } from '@base-ui/react/use-render';
4
4
  import { FileTree as PierreFileTree, type FileTreeProps as PierreProps } from '@pierre/trees/react';
@@ -55,14 +55,11 @@ export function FileTreeRoot({
55
55
  const { t } = useTranslation('ui');
56
56
  const [frame, setFrame] = useState<HTMLDivElement | null>(null);
57
57
  const { attach, bar } = useFileTreeShift(model, resetKey);
58
- const frameRef = useCallback(
59
- (node: Element | null) => {
60
- setFrame(node as HTMLDivElement | null);
61
- attach(node as HTMLElement | null);
62
- mergeRefs<Element>(ref)(node);
63
- },
64
- [ref, attach]
65
- );
58
+ const frameRef = (node: Element | null) => {
59
+ setFrame(node as HTMLDivElement | null);
60
+ attach(node as HTMLElement | null);
61
+ mergeRefs<Element>(ref)(node);
62
+ };
66
63
  const slots = useFileTreeSlots(model, frame, label ?? t('tree.label'));
67
64
  const latestLoads = useRef({ onLoadChildren, onExpandedPathsChange });
68
65
  useLayoutEffect(() => {
@@ -1,4 +1,4 @@
1
- import { useMemo, type ReactElement } from 'react';
1
+ import { type ReactElement } from 'react';
2
2
  import type { FileTree } from '@pierre/trees';
3
3
  import { useTreeShift, type TreeShiftSource } from '../tree/use-tree-shift.tsx';
4
4
  import { shiftNeed } from '../tree/shift.ts';
@@ -40,13 +40,10 @@ function needOf(row: HTMLElement, shift: number, gap: number, range: Range): num
40
40
 
41
41
  /* The file tree draws its rows in a shadow root, and only the name slides: its decorations sit over the row. */
42
42
  export function useFileTreeShift(model: FileTree, resetKey?: string): { attach(node: HTMLElement | null): void; bar: ReactElement } {
43
- const source = useMemo(
44
- (): TreeShiftSource => ({
45
- root: () => model.getFileTreeContainer()?.shadowRoot ?? null,
46
- rows: (root) => [...root.querySelectorAll<HTMLElement>(ROWS)],
47
- need: needOf
48
- }),
49
- [model]
50
- );
43
+ const source: TreeShiftSource = {
44
+ root: () => model.getFileTreeContainer()?.shadowRoot ?? null,
45
+ rows: (root) => [...root.querySelectorAll<HTMLElement>(ROWS)],
46
+ need: needOf
47
+ };
51
48
  return useTreeShift(source, resetKey);
52
49
  }
package/src/lazy.tsx CHANGED
@@ -79,6 +79,8 @@ export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name
79
79
  const Lazy = lazy(() => open(loaded));
80
80
  // Once `loaded` has it, `Lazy` never committed anything, so switching over remounts nothing.
81
81
  const LazyNamed = (props: ComponentProps<Module[Name]>) => {
82
+ // `loaded` fills in place; compiled, the element drawn before the module landed would stay.
83
+ 'use no memo';
82
84
  const Component: ComponentType<any> = loaded.current ?? Lazy;
83
85
  return <Component {...props} />;
84
86
  };
@@ -114,6 +116,8 @@ export function lazyDialog<Module extends Record<Name, ComponentType>, Name exte
114
116
  const subscribe = (listener: () => void) => loaded.subscribe(listener);
115
117
  const isLoaded = () => loaded.current !== null;
116
118
  const LazyDialog = () => {
119
+ // As in `lazyNamed`: what `loaded` holds is read again on every render.
120
+ 'use no memo';
117
121
  const opened = useOpenedOnce(useStore(isOpen));
118
122
  useSyncExternalStore(subscribe, isLoaded);
119
123
  const Loaded: ComponentType | null = loaded.current;
package/src/locales/en.ts CHANGED
@@ -17,11 +17,11 @@ export default {
17
17
  section: 'Settings section',
18
18
  sections: 'Settings sections',
19
19
  close: 'Close settings',
20
- failed: 'This settings page failed to render',
20
+ failed: 'Could not show this settings page',
21
21
  search: {
22
22
  placeholder: 'Search settings',
23
23
  clear: 'Clear search',
24
- none: 'Nothing matches "{{query}}".',
24
+ none: 'No results for “{{query}}”',
25
25
  results: 'Search results',
26
26
  pane: 'Section'
27
27
  }
@@ -36,12 +36,12 @@ export default {
36
36
  },
37
37
  select: {
38
38
  search: 'Search',
39
- noMatch: 'No option matches “{{query}}”'
39
+ noMatch: 'No match for “{{query}}”'
40
40
  },
41
41
  iconPicker: {
42
42
  label: 'Icon',
43
43
  none: 'No icon',
44
- search: 'Find an icon',
44
+ search: 'Search icons',
45
45
  clear: 'Clear search',
46
46
  noMatch: 'No icon matches “{{query}}”'
47
47
  },
package/src/locales/nl.ts CHANGED
@@ -17,11 +17,11 @@ export default {
17
17
  section: 'Onderdeel van de instellingen',
18
18
  sections: 'Onderdelen van de instellingen',
19
19
  close: 'Instellingen sluiten',
20
- failed: 'Deze instellingenpagina kon niet worden getekend',
20
+ failed: 'Kon deze instellingenpagina niet tonen',
21
21
  search: {
22
22
  placeholder: 'Instellingen zoeken',
23
23
  clear: 'Zoekopdracht wissen',
24
- none: 'Niets komt overeen met "{{query}}".',
24
+ none: 'Geen resultaten voor “{{query}}”',
25
25
  results: 'Zoekresultaten',
26
26
  pane: 'Onderdeel'
27
27
  }
@@ -36,12 +36,12 @@ export default {
36
36
  },
37
37
  select: {
38
38
  search: 'Zoeken',
39
- noMatch: 'Geen optie past bij “{{query}}”'
39
+ noMatch: 'Geen resultaat voor “{{query}}”'
40
40
  },
41
41
  iconPicker: {
42
42
  label: 'Icoon',
43
43
  none: 'Geen icoon',
44
- search: 'Zoek een icoon',
44
+ search: 'Iconen zoeken',
45
45
  clear: 'Zoekopdracht wissen',
46
46
  noMatch: 'Geen icoon past bij “{{query}}”'
47
47
  },
@@ -70,7 +70,7 @@ export default {
70
70
  collapse: 'Invouwen'
71
71
  },
72
72
  workspace: {
73
- tabs: 'Open views',
73
+ tabs: 'Open weergaven',
74
74
  unsaved: 'Niet-opgeslagen wijzigingen',
75
75
  closeTab: 'Sluit {{name}}',
76
76
  resize: 'Panelen vergroten of verkleinen',
@@ -239,7 +239,7 @@ export function SettingsDialog({
239
239
  useEffect(() => {
240
240
  latestShown.current = onTargetShown;
241
241
  });
242
- const targetValue = useMemo(() => ({ target, shown: () => latestShown.current?.() }), [target]);
242
+ const targetValue = { target, shown: () => latestShown.current?.() };
243
243
 
244
244
  const labelOf = (id: string): string => sections.find((entry) => entry.id === id)?.label ?? id;
245
245
  const pick = (result: SettingsSearchResult): void => onNavigate({ section: result.section, target: result.id });
@@ -1,4 +1,4 @@
1
- import { useCallback, type MouseEvent } from 'react';
1
+ import { type MouseEvent } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useRender } from '@base-ui/react/use-render';
4
4
  import { ChevronRight } from 'lucide-react';
@@ -25,13 +25,10 @@ export type TreeRootProps = useRender.ComponentProps<'div'> & {
25
25
  export function TreeRoot({ overflow = 'truncate', render, className, ref, children, ...props }: TreeRootProps) {
26
26
  const { attach, bar } = useTreeShift(TREE_ROWS);
27
27
  const scrolls = overflow === 'scroll';
28
- const rootRef = useCallback(
29
- (node: HTMLDivElement | null) => {
30
- attach(scrolls ? node : null);
31
- mergeRefs<HTMLDivElement>(ref)(node);
32
- },
33
- [scrolls, ref, attach]
34
- );
28
+ const rootRef = (node: HTMLDivElement | null) => {
29
+ attach(scrolls ? node : null);
30
+ mergeRefs<HTMLDivElement>(ref)(node);
31
+ };
35
32
  return useRender({
36
33
  render,
37
34
  ref: rootRef,
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
 
3
3
  export interface ContentSize {
4
4
  width: number;
@@ -10,7 +10,7 @@ export function useContentSize(): [(node: HTMLElement | null) => void, ContentSi
10
10
  const [size, setSize] = useState<ContentSize>({ width: 0, height: 0 });
11
11
  const observer = useRef<ResizeObserver | null>(null);
12
12
  useEffect(() => () => observer.current?.disconnect(), []);
13
- const measure = useCallback((node: HTMLElement | null): void => {
13
+ const measure = (node: HTMLElement | null): void => {
14
14
  observer.current?.disconnect();
15
15
  if (node === null) {
16
16
  return;
@@ -22,6 +22,6 @@ export function useContentSize(): [(node: HTMLElement | null) => void, ContentSi
22
22
  update(node.clientWidth, node.clientHeight);
23
23
  observer.current = new ResizeObserver(([entry]) => update(entry!.contentRect.width, entry!.contentRect.height));
24
24
  observer.current.observe(node);
25
- }, []);
25
+ };
26
26
  return [measure, size];
27
27
  }
@@ -287,13 +287,13 @@ export function SplitView({
287
287
  useLayoutEffect(() => {
288
288
  latest.current = { value, onValueChange };
289
289
  });
290
- const dispatch = useCallback((command: SplitCommand): void => {
290
+ const dispatch = (command: SplitCommand): void => {
291
291
  const state = latest.current;
292
292
  const next = updateSplitLayout(state.value, command);
293
293
  if (next !== state.value) {
294
294
  state.onValueChange(next, command);
295
295
  }
296
- }, []);
296
+ };
297
297
  const measure = useCallback((): void => {
298
298
  const element = root.current;
299
299
  if (!element) {
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  createContext,
3
3
  useContext,
4
- useCallback,
5
4
  useEffect,
6
5
  useLayoutEffect,
7
6
  useRef,
@@ -109,7 +108,7 @@ export function TabStrip({
109
108
  };
110
109
  const [edges, setEdges] = useState({ start: false, end: false });
111
110
  const [insertion, setInsertion] = useState(0);
112
- const measure = useCallback((): void => {
111
+ const measure = (): void => {
113
112
  const element = strip.current;
114
113
  if (!element) {
115
114
  return;
@@ -123,7 +122,7 @@ export function TabStrip({
123
122
  const last = tabs.at(-1);
124
123
  setInsertion(after?.offsetLeft ?? (last ? last.offsetLeft + last.offsetWidth : 0));
125
124
  }
126
- }, [insertAt]);
125
+ };
127
126
  // Stays set after the active tab changes, as its width can settle frames later; a person's own scroll ends it.
128
127
  const revealing = useRef(false);
129
128
  const reveal = (): void => {
@@ -1,4 +1,4 @@
1
- import { useMemo, type ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useRender } from '@base-ui/react/use-render';
4
4
  import { WorkspaceContext, type WorkspaceLayout } from './context.ts';
@@ -70,9 +70,6 @@ export function Workspace({
70
70
  )
71
71
  }
72
72
  });
73
- const context = useMemo(
74
- () => ({ layout, edges: { top: !hasToolbar, right: !rightOpen, bottom: true, left: !leftOpen } }),
75
- [layout, hasToolbar, rightOpen, leftOpen]
76
- );
73
+ const context = { layout, edges: { top: !hasToolbar, right: !rightOpen, bottom: true, left: !leftOpen } };
77
74
  return <WorkspaceContext value={context}>{element}</WorkspaceContext>;
78
75
  }