@adecore/ui 0.39.0 → 0.40.1

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 +10 -10
  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
@@ -5,7 +5,6 @@ import {
5
5
  useContext,
6
6
  useEffect,
7
7
  useLayoutEffect,
8
- useMemo,
9
8
  useRef,
10
9
  useState,
11
10
  type ComponentProps,
@@ -41,7 +40,6 @@ const InMenuContext = createContext(false);
41
40
  const TAB =
42
41
  'relative flex h-9 shrink-0 items-center gap-1.5 whitespace-nowrap text-xs font-medium text-text-muted hover:text-text data-[active]:text-text data-[disabled]:opacity-50 after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 data-[active]:after:bg-text';
43
42
 
44
- /* The button that opens the tabs that do not fit reads as one more tab, at the very end of the strip. */
45
43
  const MORE = clsx(TAB, 'ml-auto data-[popup-open]:text-text');
46
44
 
47
45
  /* A tab as a key in a bar, for tabs that share a toolbar with buttons and must not read as the tabs of a pane under it. */
@@ -70,18 +68,21 @@ export function TabsRoot({ value, defaultValue = 0, onValueChange, ...props }: T
70
68
  const current = controlled ? value : own;
71
69
  // The latest handler without a new context for every render of a parent that writes it inline.
72
70
  const latest = useRef(onValueChange);
71
+
73
72
  useLayoutEffect(() => {
74
73
  latest.current = onValueChange;
75
74
  });
76
- const selection = useMemo(() => {
77
- const select = (next: TabsValue): void => {
78
- if (!controlled) {
79
- setOwn(next);
80
- }
81
- latest.current?.(next);
82
- };
83
- return { value: current, select };
84
- }, [controlled, current]);
75
+
76
+ const select = (next: TabsValue): void => {
77
+ if (!controlled) {
78
+ setOwn(next);
79
+ }
80
+
81
+ latest.current?.(next);
82
+ };
83
+
84
+ const selection = { value: current, select };
85
+
85
86
  return (
86
87
  <SelectionContext value={selection}>
87
88
  <BaseTabs.Root value={current} onValueChange={(next) => selection.select(next)} {...props} />
@@ -92,14 +93,19 @@ export function TabsRoot({ value, defaultValue = 0, onValueChange, ...props }: T
92
93
  export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, 'className'> & {
93
94
  /* On the strip, which runs the width of the pane; padding here insets the tabs from its edges. */
94
95
  className?: string;
96
+
95
97
  /* Drawn before the tabs, such as what they are the views of. It keeps its own width, like `end`. */
96
98
  start?: ReactNode;
99
+
97
100
  /* Drawn after the tabs, such as a button that adds one. It keeps its own width: the tabs fit in what is left. */
98
101
  end?: ReactNode;
102
+
99
103
  /* Like `end`, but right beside the last tab instead of at the end of the strip, so it moves with the tabs. */
100
104
  after?: ReactNode;
105
+
101
106
  /* `line` underlines the picked tab over the strip's border; `pill` fills it, for tabs in a toolbar. */
102
107
  variant?: TabsVariant;
108
+
103
109
  /* What happens to tabs that do not fit: a "More" menu at the end of the strip, or a row that scrolls sideways. */
104
110
  overflow?: 'menu' | 'scroll';
105
111
  };
@@ -108,9 +114,8 @@ const sameIndices = (left: readonly number[] | null, right: readonly number[]):
108
114
  left !== null && left.length === right.length && left.every((index, at) => index === right[at]);
109
115
 
110
116
  /*
111
- * The tabs that fit, and a menu at the end of the strip with the ones that do not. Every tab is measured
112
- * in a row nobody sees, so the strip knows what fits before it draws. The tabs are the direct children,
113
- * since the menu repeats what a tab holds.
117
+ * Every tab is measured in a row nobody sees, so the strip knows what fits before it draws. The tabs
118
+ * are the direct children, since the menu repeats what a tab holds.
114
119
  */
115
120
  export function TabsList({ className, children, start, end, after, variant = 'line', overflow = 'menu', ...props }: TabsListProps) {
116
121
  const style = STYLES[variant];
@@ -123,43 +128,51 @@ export function TabsList({ className, children, start, end, after, variant = 'li
123
128
  const beside = useRef<HTMLDivElement>(null);
124
129
  const list = useRef<HTMLDivElement>(null);
125
130
  const scrolls = overflow === 'scroll';
126
- // Which edges of a scrolling row still hide a tab, where it fades out.
127
131
  const [edges, setEdges] = useState({ start: false, end: false });
128
132
  // Null until the first measure, which runs before the first paint.
129
133
  const [shown, setShown] = useState<number[] | null>(null);
134
+
130
135
  const tabs = Children.toArray(children).filter(
131
136
  (child): child is ReactElement<ComponentProps<typeof TabsTab>> => isValidElement(child) && child.type === TabsTab
132
137
  );
138
+
133
139
  const kept = tabs.flatMap((tab, index) => (tab.props.value === value || tab.props.pinned === true ? [index] : []));
134
140
 
135
141
  useLayoutEffect(() => {
136
142
  const host = strip.current;
137
143
  const row = measure.current;
144
+
138
145
  if (scrolls || !host || !row) {
139
146
  return;
140
147
  }
148
+
141
149
  const fit = (): void => {
142
150
  const style = getComputedStyle(host);
143
151
  const gap = parseFloat(style.columnGap) || 0;
152
+
144
153
  const taken = [head.current, beside.current, tail.current].reduce(
145
154
  (sum, part) => (part === null ? sum : sum + part.getBoundingClientRect().width + gap),
146
155
  0
147
156
  );
157
+
148
158
  const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
149
159
  const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
150
160
  const more = cells.pop() ?? 0;
151
161
  const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, kept);
152
162
  setShown((current) => (sameIndices(current, next) ? current : next));
153
163
  };
164
+
154
165
  fit();
155
166
  const observer = new ResizeObserver(fit);
156
167
  observer.observe(host);
157
168
  observer.observe(row);
169
+
158
170
  for (const part of [head.current, beside.current, tail.current]) {
159
171
  if (part !== null) {
160
172
  observer.observe(part);
161
173
  }
162
174
  }
175
+
163
176
  return () => observer.disconnect();
164
177
  });
165
178
 
@@ -168,11 +181,14 @@ export function TabsList({ className, children, start, end, after, variant = 'li
168
181
  useEffect(() => {
169
182
  const row = list.current;
170
183
  const tab = row?.querySelector<HTMLElement>('[aria-selected="true"]');
184
+
171
185
  if (!scrolls || !row || !tab) {
172
186
  return;
173
187
  }
188
+
174
189
  const strip = row.getBoundingClientRect();
175
190
  const picked = tab.getBoundingClientRect();
191
+
176
192
  if (picked.left < strip.left) {
177
193
  row.scrollLeft -= strip.left - picked.left;
178
194
  } else if (picked.right > strip.right) {
@@ -182,21 +198,26 @@ export function TabsList({ className, children, start, end, after, variant = 'li
182
198
 
183
199
  useEffect(() => {
184
200
  const row = list.current;
201
+
185
202
  if (!scrolls || !row) {
186
203
  return;
187
204
  }
205
+
188
206
  const measure = (): void => {
189
207
  const start = row.scrollLeft > 0;
190
208
  const end = row.scrollLeft + row.clientWidth < row.scrollWidth - 1;
191
209
  setEdges((current) => (current.start === start && current.end === end ? current : { start, end }));
192
210
  };
211
+
193
212
  measure();
194
213
  row.addEventListener('scroll', measure, { passive: true });
195
214
  const observer = new ResizeObserver(measure);
196
215
  observer.observe(row);
216
+
197
217
  for (const tab of row.children) {
198
218
  observer.observe(tab);
199
219
  }
220
+
200
221
  return () => {
201
222
  row.removeEventListener('scroll', measure);
202
223
  observer.disconnect();
@@ -206,20 +227,25 @@ export function TabsList({ className, children, start, end, after, variant = 'li
206
227
  // A mouse wheel only turns vertically, and on a row that scrolls sideways that is the way it should turn.
207
228
  useEffect(() => {
208
229
  const row = list.current;
230
+
209
231
  if (!scrolls || !row) {
210
232
  return;
211
233
  }
234
+
212
235
  const onWheel = (event: WheelEvent): void => {
213
236
  if (!event.deltaX && event.deltaY) {
214
237
  row.scrollLeft += event.deltaY;
215
238
  }
216
239
  };
240
+
217
241
  row.addEventListener('wheel', onWheel, { passive: true });
242
+
218
243
  return () => row.removeEventListener('wheel', onWheel);
219
244
  }, [scrolls]);
220
245
 
221
246
  const visible = shown === null ? tabs : shown.map((index) => tabs[index]!);
222
247
  const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
248
+
223
249
  const more = (
224
250
  <>
225
251
  {t('tabs.more')}
@@ -289,6 +315,7 @@ type TabProps = ComponentProps<typeof BaseTabs.Tab>;
289
315
  export type TabsTabProps = TabProps & {
290
316
  /* Makes the tab closable: a close button in it, a middle click, and Delete or Backspace while the tab has focus. */
291
317
  onClose?(): void;
318
+
292
319
  /* Keeps the tab in the strip, never in the menu, and leaves it unclosable even with `onClose`. */
293
320
  pinned?: boolean;
294
321
  };
@@ -298,17 +325,9 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
298
325
  const { t } = useTranslation('ui');
299
326
  const tab = STYLES[useContext(VariantContext)].tab;
300
327
 
301
- if (pinned) {
302
- return (
303
- <BaseTabs.Tab className={withClass(tab, className)} data-pinned="" {...props}>
304
- {children}
305
- </BaseTabs.Tab>
306
- );
307
- }
308
-
309
- if (onClose === undefined) {
328
+ if (pinned || onClose === undefined) {
310
329
  return (
311
- <BaseTabs.Tab className={withClass(tab, className)} {...props}>
330
+ <BaseTabs.Tab className={withClass(tab, className)} data-pinned={pinned ? '' : undefined} {...props}>
312
331
  {children}
313
332
  </BaseTabs.Tab>
314
333
  );
@@ -318,6 +337,7 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
318
337
 
319
338
  const handleKeyDown = (e: Parameters<NonNullable<TabProps['onKeyDown']>>[0]): void => {
320
339
  onKeyDown?.(e);
340
+
321
341
  if (!e.defaultPrevented && e.target === e.currentTarget && (e.key === 'Delete' || e.key === 'Backspace')) {
322
342
  e.preventDefault();
323
343
  onClose();
@@ -327,6 +347,7 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
327
347
  // A middle press starts autoscroll on some platforms; the release is what closes.
328
348
  const handleMouseDown = (e: Parameters<NonNullable<TabProps['onAuxClick']>>[0]): void => {
329
349
  onMouseDown?.(e);
350
+
330
351
  if (e.button === 1) {
331
352
  e.preventDefault();
332
353
  }
@@ -334,6 +355,7 @@ export function TabsTab({ className, onClose, pinned = false, children, ...props
334
355
 
335
356
  const handleAuxClick = (e: Parameters<NonNullable<TabProps['onAuxClick']>>[0]): void => {
336
357
  onAuxClick?.(e);
358
+
337
359
  if (!e.defaultPrevented && e.button === 1) {
338
360
  e.preventDefault();
339
361
  onClose();
@@ -371,9 +393,10 @@ export type TabsCountProps = Omit<useRender.ComponentProps<'span'>, 'children'>
371
393
  value: number;
372
394
  };
373
395
 
374
- /* How much a tab holds, after its label. Nothing at zero: an empty view needs no number to say so. */
396
+ /* Nothing at zero: an empty view needs no number to say so. */
375
397
  export function TabsCount({ value, render, className, ref, ...props }: TabsCountProps) {
376
398
  const inMenu = useContext(InMenuContext);
399
+
377
400
  const element = useRender({
378
401
  render,
379
402
  ref,
@@ -384,5 +407,6 @@ export function TabsCount({ value, render, className, ref, ...props }: TabsCount
384
407
  className: clsx('inline-flex shrink-0', inMenu && 'ml-auto', className)
385
408
  }
386
409
  });
410
+
387
411
  return value === 0 ? null : element;
388
412
  }
@@ -3,11 +3,10 @@ import { dirname, join, sep } from 'node:path';
3
3
  import { Glob, plugin } from 'bun';
4
4
 
5
5
  /*
6
- * A preload for `bun test` in an app that links a checkout of this library: what `resolve.dedupe`
7
- * does for Vite. The checkout has node_modules of its own, and a second React, i18next or Base UI
8
- * breaks every hook, every word and every compound part. Bun has no dedupe and its resolve hook never
9
- * sees a package import, so every file of these packages in the checkout loads as a stand-in for the
10
- * same file in the app's copy. Does nothing once the library is installed from the registry.
6
+ * What `resolve.dedupe` does for Vite, for `bun test` in an app that links a checkout: a second React,
7
+ * i18next or Base UI breaks every hook, word and compound part. Bun has no dedupe and its resolve hook
8
+ * never sees a package import, so each file of these packages in the checkout loads as a stand-in for
9
+ * the app's copy.
11
10
  *
12
11
  * CodeMirror and Lezer reject an extension, a facet or a node prop made by another copy, and the chat
13
12
  * views take both from the app; shiki is in the list for the highlighter an app hands them.
@@ -34,6 +33,7 @@ interface Copy {
34
33
 
35
34
  const manifestOf = (folder: string): Manifest | null => {
36
35
  const path = join(folder, 'package.json');
36
+
37
37
  return existsSync(path) ? (JSON.parse(readFileSync(path, 'utf8')) as Manifest) : null;
38
38
  };
39
39
 
@@ -43,6 +43,7 @@ const dependsOnLibrary = (manifest: Manifest | null): boolean =>
43
43
  const membersOf = (root: string, manifest: Manifest | null): string[] => {
44
44
  const workspaces = manifest?.workspaces;
45
45
  const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
46
+
46
47
  return patterns
47
48
  .flatMap((pattern) => [...new Glob(`${pattern}/package.json`).scanSync({ cwd: root, onlyFiles: true })])
48
49
  .map((path) => join(root, dirname(path)));
@@ -54,6 +55,7 @@ const membersOf = (root: string, manifest: Manifest | null): string[] => {
54
55
  */
55
56
  const appFolders = (): string[] => {
56
57
  const root = process.cwd();
58
+
57
59
  return [root, ...membersOf(root, manifestOf(root)).filter((folder) => dependsOnLibrary(manifestOf(folder)))];
58
60
  };
59
61
 
@@ -61,21 +63,25 @@ const appFolders = (): string[] => {
61
63
  const checkoutFolders = (): string[] => {
62
64
  for (let folder = dirname(library); dirname(folder) !== folder; folder = dirname(folder)) {
63
65
  const manifest = manifestOf(folder);
66
+
64
67
  if (manifest?.workspaces !== undefined) {
65
68
  const members = membersOf(folder, manifest);
69
+
66
70
  return members.includes(library) ? [folder, ...members] : [library];
67
71
  }
68
72
  }
73
+
69
74
  return [library];
70
75
  };
71
76
 
72
- /* The packages a name of the list stands for in a folder: itself, or every package of the scope for `@scope/*`. */
73
77
  const namesIn = (folder: string, pattern: string): string[] => {
74
78
  if (!pattern.endsWith('/*')) {
75
79
  return [pattern];
76
80
  }
81
+
77
82
  const scope = pattern.slice(0, -2);
78
83
  const path = join(folder, 'node_modules', scope);
84
+
79
85
  return existsSync(path)
80
86
  ? readdirSync(path)
81
87
  .filter((entry) => !entry.startsWith('.'))
@@ -83,13 +89,15 @@ const namesIn = (folder: string, pattern: string): string[] => {
83
89
  : [];
84
90
  };
85
91
 
86
- /* The copy of a package that `from` resolves to, walking up the node_modules folders as Node does. */
92
+ /* Walks up the node_modules folders as Node does. */
87
93
  const copyFrom = (from: string, name: string): string | null => {
88
94
  for (let folder = from; ; folder = dirname(folder)) {
89
95
  const candidate = join(folder, 'node_modules', name);
96
+
90
97
  if (existsSync(candidate)) {
91
98
  return realpathSync(candidate);
92
99
  }
100
+
93
101
  if (dirname(folder) === folder) {
94
102
  return null;
95
103
  }
@@ -98,28 +106,38 @@ const copyFrom = (from: string, name: string): string | null => {
98
106
 
99
107
  const findCopies = (): Copy[] => {
100
108
  const checkout = checkoutFolders();
109
+
101
110
  const inside = (path: string): boolean => path === checkout[0] || path.startsWith(`${checkout[0]}${sep}`);
111
+
102
112
  // A run inside the checkout is the library's own; a node_modules above it belongs to no app.
103
113
  if (inside(realpathSync(process.cwd()))) {
104
114
  return [];
105
115
  }
116
+
106
117
  const folders = appFolders();
118
+
107
119
  const added = folders.flatMap((folder) => {
108
120
  const names = manifestOf(folder)?.adecore?.dedupe;
121
+
109
122
  return Array.isArray(names) ? names.filter((name): name is string => typeof name === 'string') : [];
110
123
  });
124
+
111
125
  const names = new Set([...SHARED, ...added].flatMap((pattern) => checkout.flatMap((folder) => namesIn(folder, pattern))));
126
+
112
127
  return [...names].flatMap((name) => {
113
128
  const ours = folders.map((folder) => copyFrom(folder, name)).find((copy): copy is string => copy !== null && !inside(copy));
129
+
114
130
  if (ours === undefined) {
115
131
  return [];
116
132
  }
133
+
117
134
  const theirs = new Set(
118
135
  checkout
119
136
  .map((folder) => join(folder, 'node_modules', name))
120
137
  .filter((path) => existsSync(path))
121
138
  .map((path) => realpathSync(path))
122
139
  );
140
+
123
141
  return [...theirs].filter((real) => real !== ours).map((real) => ({ theirs: `${real}${sep}`, ours: `${ours}${sep}` }));
124
142
  });
125
143
  };
@@ -140,6 +158,7 @@ const standIn = (path: string): string => {
140
158
  const { exports, imports } = transpiler.scan(readFileSync(path, 'utf8'));
141
159
  const esm = exports.length > 0 || imports.some((entry) => entry.kind === 'import-statement');
142
160
  const target = JSON.stringify(path);
161
+
143
162
  return [`export * from ${target};`, ...(!esm || exports.includes('default') ? [`export { default } from ${target};`] : [])].join('\n');
144
163
  };
145
164
 
@@ -149,6 +168,7 @@ if (copies.length > 0) {
149
168
  setup(build) {
150
169
  build.onLoad({ filter: new RegExp(`^(${copies.map((copy) => escape(copy.theirs)).join('|')})`) }, (args) => {
151
170
  const copy = copies.find((candidate) => args.path.startsWith(candidate.theirs))!;
171
+
152
172
  return { contents: standIn(copy.ours + args.path.slice(copy.theirs.length)), loader: 'js' };
153
173
  });
154
174
  }
@@ -9,6 +9,7 @@ export interface FakeFormatSource extends FormatSource {
9
9
  export const fakeFormatSource = (): FakeFormatSource => {
10
10
  let language = 'en';
11
11
  let region: string = FORMAT_LANGUAGE;
12
+
12
13
  return {
13
14
  language: () => language,
14
15
  region: () => region,
package/src/tint.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import type { CSSProperties } from 'react';
2
2
 
3
3
  /*
4
- * The custom properties that let what stands on a colored ground follow its color: the hover of a
5
- * button and a separator. The theme's utilities read `--surface-hover` and `--border` themselves
6
- * (`@theme inline`), so a host sets this on the colored element and its buttons and separators
7
- * take it. `--border` is mixed from `--border-strong`, since a property that reads itself is no
8
- * value at all.
4
+ * The theme's utilities read `--surface-hover` and `--border` themselves (`@theme inline`), so
5
+ * buttons and separators under the element take its color. `--border` mixes from `--border-strong`,
6
+ * since a property that reads itself is no value at all.
9
7
  */
10
8
  export function tintStyle(color: string, ground = 'var(--surface-raised)'): CSSProperties {
11
9
  return {
@@ -4,7 +4,6 @@ import type { Shortcut } from './shortcut.ts';
4
4
  // How long a toast that went well stays up; a failure waits for the person instead.
5
5
  export const SUCCESS_MS = 4000;
6
6
 
7
- // How long a deletion can still be taken back.
8
7
  export const UNDO_MS = 8000;
9
8
 
10
9
  // `setTimeout` fires at once past this, so a longer lifetime, `Infinity` too, is a toast that stays.
@@ -16,35 +15,39 @@ export type ToastKind = 'progress' | 'success' | 'error' | 'deleted';
16
15
  export interface ToastAction {
17
16
  label: string;
18
17
  run(): void;
18
+
19
19
  /* The key that does the same, printed beside the label. The key itself is bound elsewhere. */
20
20
  shortcut?: Shortcut;
21
21
  }
22
22
 
23
- /* When a toast that goes by itself started counting and when it goes, as epoch milliseconds. */
23
+ /* Epoch milliseconds. */
24
24
  export interface ToastDeadline {
25
25
  start: number;
26
26
  end: number;
27
27
  }
28
28
 
29
- /* How far into its lifetime a toast is at `now`, held inside that lifetime. */
30
29
  export const elapsedOf = (deadline: ToastDeadline, now: number): number => Math.min(Math.max(now - deadline.start, 0), deadline.end - deadline.start);
31
30
 
32
31
  /* An app that carries more on a toast extends this and hands its own type to `createToastStore`. */
33
32
  export interface Toast {
34
33
  id: string;
35
34
  title: string;
36
- /* The line under the title, often the one that says why something stopped. */
37
35
  description?: string;
38
36
  kind: ToastKind;
39
37
  action?: ToastAction;
40
- /* More than one thing to do about it, such as looking at what went wrong or trying again; drawn after `action`, in order. */
38
+
39
+ /* Drawn after `action`, in order. */
41
40
  actions?: ToastAction[];
41
+
42
42
  /* A success that waits to be dismissed, for news that lands while nobody is looking yet. */
43
43
  persist?: boolean;
44
+
44
45
  /* Milliseconds until the toast takes itself away, whatever its kind; over the kind's own lifetime and `persist`. Zero or less is ignored. */
45
46
  lifetime?: number;
46
- /* Runs once the toast is gone, whether it ran out or was dismissed. */
47
+
48
+ /* Whether it ran out or was dismissed. */
47
49
  onClose?: () => void;
50
+
48
51
  /* Set by the store from the timer that takes the toast away, so what counts down on screen is that timer. */
49
52
  deadline?: ToastDeadline;
50
53
  }
@@ -55,18 +58,17 @@ export type ToastPatch<T extends Toast = Toast> = Partial<Omit<T, 'id' | 'deadli
55
58
 
56
59
  export interface ToastStore<T extends Toast = Toast> {
57
60
  toasts: T[];
61
+
58
62
  /* Puts one up, or moves the one with this id to what it says now; answers its id. */
59
63
  show(toast: ToastInput<T>): string;
64
+
60
65
  update(id: string, patch: ToastPatch<T>): void;
61
66
  dismiss(id: string): void;
62
67
  }
63
68
 
64
69
  export type ToastStoreHook<T extends Toast = Toast> = UseBoundStore<StoreApi<ToastStore<T>>>;
65
70
 
66
- /*
67
- * The one place an app says how something went that no surface on screen shows. A long action keeps
68
- * one id from progress to its outcome, so a person never watches two cards for one thing.
69
- */
71
+ /* A long action keeps one id from progress to its outcome, so a person never watches two cards for one thing. */
70
72
  export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> => {
71
73
  const timers = new Map<string, ReturnType<typeof setTimeout>>();
72
74
  let counter = 0;
@@ -77,18 +79,17 @@ export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> =
77
79
  timers.delete(id);
78
80
  };
79
81
 
80
- /*
81
- * A toast that went well takes itself away, and so does the offer to undo; a failure and a running
82
- * action stay, unless the toast names a lifetime of its own.
83
- */
82
+ // A failure and a running action stay, unless the toast names a lifetime of its own.
84
83
  const schedule = (id: string, toast: Pick<Toast, 'kind' | 'persist' | 'lifetime'>): ToastDeadline | undefined => {
85
84
  stop(id);
86
85
  const { kind, persist } = toast;
87
86
  const own = toast.lifetime !== undefined && toast.lifetime > 0 ? toast.lifetime : undefined;
88
87
  const lifetime = own ?? (kind === 'deleted' ? UNDO_MS : kind === 'success' && persist !== true ? SUCCESS_MS : null);
88
+
89
89
  if (lifetime === null || lifetime > LONGEST_TIMER_MS) {
90
90
  return undefined;
91
91
  }
92
+
92
93
  timers.set(
93
94
  id,
94
95
  setTimeout(() => {
@@ -96,7 +97,9 @@ export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> =
96
97
  get().dismiss(id);
97
98
  }, lifetime)
98
99
  );
100
+
99
101
  const start = Date.now();
102
+
100
103
  return { start, end: start + lifetime };
101
104
  };
102
105
 
@@ -108,13 +111,16 @@ export const createToastStore = <T extends Toast = Toast>(): ToastStoreHook<T> =
108
111
  const next = { ...toast, id, deadline: schedule(id, toast) } as T;
109
112
  const toasts = get().toasts;
110
113
  set({ toasts: toasts.some((entry) => entry.id === id) ? toasts.map((entry) => (entry.id === id ? next : entry)) : [...toasts, next] });
114
+
111
115
  return id;
112
116
  },
113
117
  update(id, patch) {
114
118
  const current = get().toasts.find((entry) => entry.id === id);
119
+
115
120
  if (current === undefined) {
116
121
  return;
117
122
  }
123
+
118
124
  const merged: T = { ...current, ...patch };
119
125
  const next: T = { ...merged, deadline: schedule(id, merged) };
120
126
  set({ toasts: get().toasts.map((entry) => (entry.id === id ? next : entry)) });
@@ -25,10 +25,12 @@ 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
+
28
29
  const rootRef = (node: HTMLDivElement | null) => {
29
30
  attach(scrolls ? node : null);
30
31
  mergeRefs<HTMLDivElement>(ref)(node);
31
32
  };
33
+
32
34
  return useRender({
33
35
  render,
34
36
  ref: rootRef,
@@ -115,6 +117,7 @@ export type TreeChevronProps = useRender.ComponentProps<'button'> & {
115
117
 
116
118
  export function TreeChevron({ expanded, onExpandedChange, render, className, ref, onClick, onDoubleClick, ...props }: TreeChevronProps) {
117
119
  const { t } = useTranslation('ui');
120
+
118
121
  return useRender({
119
122
  render,
120
123
  ref,
@@ -128,6 +131,7 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
128
131
  onClick: (event: MouseEvent<HTMLButtonElement>) => {
129
132
  event.stopPropagation();
130
133
  onClick?.(event);
134
+
131
135
  if (!event.defaultPrevented) {
132
136
  onExpandedChange?.(!expanded);
133
137
  }
@@ -139,16 +143,19 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
139
143
  }
140
144
 
141
145
  export type TreeLabelProps = useRender.ComponentProps<'span'>;
146
+
142
147
  export function TreeLabel({ render, className, ref, ...props }: TreeLabelProps) {
143
148
  return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx('adecore-tree-label', className) } });
144
149
  }
145
150
 
146
151
  export type TreeDecorationProps = useRender.ComponentProps<'span'>;
152
+
147
153
  export function TreeDecoration({ render, className, ref, ...props }: TreeDecorationProps) {
148
154
  return useRender({ render, ref, defaultTagName: 'span', props: { ...props, className: clsx('adecore-tree-decoration', className) } });
149
155
  }
150
156
 
151
157
  export type TreeControlProps = useRender.ComponentProps<'span'>;
158
+
152
159
  export function TreeControl({ render, className, ref, onClick, onDoubleClick, onKeyDown, onPointerDown, ...props }: TreeControlProps) {
153
160
  return useRender({
154
161
  render,
@@ -167,6 +174,7 @@ export function TreeControl({ render, className, ref, onClick, onDoubleClick, on
167
174
  }
168
175
 
169
176
  export type TreeCheckboxProps = CheckboxProps;
177
+
170
178
  export function TreeCheckbox(props: TreeCheckboxProps) {
171
179
  return (
172
180
  <TreeControl>
package/src/tree/shift.ts CHANGED
@@ -27,15 +27,19 @@ export function sidewaysDelta(step: WheelStep, pageWidth: number): number {
27
27
  const swapped = step.shiftKey && step.deltaX === 0;
28
28
  const sideways = swapped ? step.deltaY : step.deltaX;
29
29
  const across = swapped ? 0 : step.deltaY;
30
+
30
31
  if (sideways === 0 || Math.abs(sideways) <= Math.abs(across)) {
31
32
  return 0;
32
33
  }
34
+
33
35
  if (step.deltaMode === 1) {
34
36
  return sideways * LINE_WIDTH;
35
37
  }
38
+
36
39
  if (step.deltaMode === 2) {
37
40
  return sideways * pageWidth;
38
41
  }
42
+
39
43
  return sideways;
40
44
  }
41
45
 
@@ -43,6 +47,8 @@ export function shiftThumb(shift: number, max: number, track: number, view: numb
43
47
  if (max <= 0 || track <= 0) {
44
48
  return { left: 0, width: Math.max(track, 0) };
45
49
  }
50
+
46
51
  const width = Math.min(track, Math.max(MIN_THUMB, Math.round((track * view) / (view + max))));
52
+
47
53
  return { left: Math.round(((track - width) * clampShift(shift, max)) / max), width };
48
54
  }