@adecore/ui 0.23.0 → 0.24.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 (129) hide show
  1. package/dist/ButtonGroup.d.ts +1 -1
  2. package/dist/ButtonGroup.js +1 -1
  3. package/dist/Checkbox.js +1 -1
  4. package/dist/ChoiceCards.js +2 -2
  5. package/dist/CloseButton.js +1 -1
  6. package/dist/ColorSwatch.d.ts +1 -1
  7. package/dist/ColorSwatch.js +1 -1
  8. package/dist/Field.d.ts +1 -1
  9. package/dist/Field.js +4 -3
  10. package/dist/IconButton.d.ts +1 -1
  11. package/dist/IconButton.js +1 -1
  12. package/dist/ListRow.d.ts +1 -1
  13. package/dist/ListRow.js +1 -1
  14. package/dist/MiddleTruncate.d.ts +8 -0
  15. package/dist/MiddleTruncate.js +59 -0
  16. package/dist/PanelHeader.d.ts +1 -1
  17. package/dist/PanelHeader.js +1 -1
  18. package/dist/SectionLabel.d.ts +1 -1
  19. package/dist/SectionLabel.js +1 -1
  20. package/dist/Select.js +1 -1
  21. package/dist/SelectSearch.js +1 -1
  22. package/dist/Slider.js +1 -1
  23. package/dist/Surface.d.ts +1 -1
  24. package/dist/Surface.js +1 -1
  25. package/dist/Switch.js +1 -1
  26. package/dist/Toasts.d.ts +1 -1
  27. package/dist/Toasts.js +4 -5
  28. package/dist/Tooltip.d.ts +1 -1
  29. package/dist/Tooltip.js +2 -2
  30. package/dist/clipboard.d.ts +1 -1
  31. package/dist/clipboard.js +12 -2
  32. package/dist/dialog/parts.d.ts +3 -3
  33. package/dist/dialog/parts.js +2 -2
  34. package/dist/file-tree/helpers.d.ts +1 -0
  35. package/dist/file-tree/helpers.js +9 -0
  36. package/dist/file-tree/parts.d.ts +1 -1
  37. package/dist/file-tree/parts.js +1 -1
  38. package/dist/file-tree/use-slots.d.ts +8 -0
  39. package/dist/file-tree/use-slots.js +64 -22
  40. package/dist/format/datetime.d.ts +1 -0
  41. package/dist/format/datetime.js +15 -0
  42. package/dist/format/index.d.ts +1 -1
  43. package/dist/format/index.js +1 -1
  44. package/dist/index.d.ts +6 -3
  45. package/dist/index.js +5 -1
  46. package/dist/key-value-list/parts.d.ts +1 -1
  47. package/dist/key-value-list/parts.js +1 -1
  48. package/dist/locales/en.d.ts +83 -0
  49. package/dist/locales/en.js +82 -0
  50. package/dist/locales/nl.d.ts +83 -0
  51. package/dist/locales/nl.js +82 -0
  52. package/dist/locales.js +3 -2
  53. package/dist/menu/parts.d.ts +7 -7
  54. package/dist/menu/parts.js +4 -4
  55. package/dist/middle-truncate.d.ts +7 -0
  56. package/dist/middle-truncate.js +34 -0
  57. package/dist/popover/parts.d.ts +7 -7
  58. package/dist/popover/parts.js +2 -2
  59. package/dist/prefetch.js +6 -1
  60. package/dist/settings/ConfirmDialog.d.ts +4 -1
  61. package/dist/settings/ConfirmDialog.js +2 -2
  62. package/dist/settings/SettingsDialog.js +1 -1
  63. package/dist/tabs/parts.d.ts +8 -4
  64. package/dist/tabs/parts.js +84 -18
  65. package/dist/testing/dedupe.js +74 -23
  66. package/dist/theme.css +24 -8
  67. package/dist/tree/parts.d.ts +1 -1
  68. package/dist/tree/parts.js +1 -1
  69. package/dist/workspace/DropPreview.d.ts +8 -0
  70. package/dist/workspace/DropPreview.js +14 -0
  71. package/dist/workspace/SplitView.d.ts +1 -1
  72. package/dist/workspace/SplitView.js +59 -18
  73. package/dist/workspace/TabStrip.d.ts +1 -1
  74. package/dist/workspace/TabStrip.js +1 -1
  75. package/dist/workspace/Workspace.d.ts +2 -2
  76. package/dist/workspace/Workspace.js +4 -2
  77. package/dist/workspace/preview.d.ts +4 -8
  78. package/dist/workspace/preview.js +17 -2
  79. package/package.json +2 -2
  80. package/src/ButtonGroup.tsx +1 -1
  81. package/src/Checkbox.tsx +1 -1
  82. package/src/ChoiceCards.tsx +2 -2
  83. package/src/CloseButton.tsx +1 -1
  84. package/src/ColorSwatch.tsx +1 -1
  85. package/src/ErrorBoundary.tsx +4 -4
  86. package/src/Field.tsx +4 -5
  87. package/src/IconButton.tsx +1 -1
  88. package/src/ListRow.tsx +1 -1
  89. package/src/MiddleTruncate.tsx +84 -0
  90. package/src/PanelHeader.tsx +1 -1
  91. package/src/SectionLabel.tsx +1 -1
  92. package/src/Select.tsx +1 -1
  93. package/src/SelectSearch.tsx +1 -1
  94. package/src/Slider.tsx +1 -1
  95. package/src/Surface.tsx +1 -1
  96. package/src/Switch.tsx +1 -1
  97. package/src/Toasts.tsx +5 -6
  98. package/src/Tooltip.tsx +5 -5
  99. package/src/clipboard.ts +11 -2
  100. package/src/dialog/parts.tsx +2 -2
  101. package/src/file-tree/helpers.ts +10 -0
  102. package/src/file-tree/parts.tsx +1 -1
  103. package/src/file-tree/use-slots.ts +68 -23
  104. package/src/format/datetime.ts +17 -0
  105. package/src/format/index.ts +1 -0
  106. package/src/index.ts +6 -3
  107. package/src/key-value-list/parts.tsx +1 -1
  108. package/src/locales/en.ts +82 -0
  109. package/src/locales/nl.ts +82 -0
  110. package/src/locales.ts +3 -2
  111. package/src/menu/parts.tsx +4 -4
  112. package/src/middle-truncate.ts +41 -0
  113. package/src/popover/parts.tsx +2 -2
  114. package/src/prefetch.ts +6 -1
  115. package/src/settings/ConfirmDialog.tsx +7 -1
  116. package/src/settings/SettingsDialog.tsx +1 -1
  117. package/src/tabs/parts.tsx +156 -54
  118. package/src/testing/dedupe.ts +87 -24
  119. package/src/theme.css +24 -8
  120. package/src/tree/parts.tsx +1 -1
  121. package/src/workspace/DropPreview.tsx +26 -0
  122. package/src/workspace/SplitView.tsx +58 -22
  123. package/src/workspace/TabStrip.tsx +1 -1
  124. package/src/workspace/Workspace.tsx +7 -3
  125. package/src/workspace/preview.ts +22 -9
  126. package/dist/locales/en.json +0 -82
  127. package/dist/locales/nl.json +0 -82
  128. package/src/locales/en.json +0 -82
  129. package/src/locales/nl.json +0 -82
@@ -1,7 +1,8 @@
1
1
  import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
2
- import { Tabs as BaseTabs } from '@base-ui-components/react/tabs';
3
- import { useRender } from '@base-ui-components/react/use-render';
2
+ import { Tabs as BaseTabs } from '@base-ui/react/tabs';
3
+ import { useRender } from '@base-ui/react/use-render';
4
4
  export type TabsValue = BaseTabs.Tab.Value;
5
+ export type TabsVariant = 'line' | 'pill';
5
6
  export type TabsRootProps = Omit<ComponentProps<typeof BaseTabs.Root>, 'onValueChange'> & {
6
7
  onValueChange?(value: TabsValue): void;
7
8
  };
@@ -10,14 +11,17 @@ export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, 'classNam
10
11
  className?: string;
11
12
  start?: ReactNode;
12
13
  end?: ReactNode;
14
+ after?: ReactNode;
15
+ variant?: TabsVariant;
16
+ overflow?: 'menu' | 'scroll';
13
17
  };
14
- export declare function TabsList({ className, children, start, end, ...props }: TabsListProps): import("react").JSX.Element;
18
+ export declare function TabsList({ className, children, start, end, after, variant, overflow, ...props }: TabsListProps): import("react").JSX.Element;
15
19
  type TabProps = ComponentProps<typeof BaseTabs.Tab>;
16
20
  export type TabsTabProps = TabProps & {
17
21
  onClose?(): void;
18
22
  };
19
23
  export declare function TabsTab({ className, onClose, children, ...props }: TabsTabProps): import("react").JSX.Element;
20
- export declare const TabsPanel: import("react").ForwardRefExoticComponent<import("@base-ui-components/react/tabs").TabsPanelProps & import("react").RefAttributes<HTMLDivElement>>;
24
+ export declare const TabsPanel: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react").TabsPanelProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
21
25
  export type TabsCountProps = Omit<useRender.ComponentProps<'span'>, 'children'> & {
22
26
  value: number;
23
27
  };
@@ -1,10 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createContext, isValidElement, useContext, useLayoutEffect, useRef, useState } from 'react';
2
+ import { Children, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown, X } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { Tabs as BaseTabs } from '@base-ui-components/react/tabs';
7
- import { useRender } from '@base-ui-components/react/use-render';
6
+ import { Tabs as BaseTabs } from '@base-ui/react/tabs';
7
+ import { useRender } from '@base-ui/react/use-render';
8
8
  import { withClass } from '../class-name.js';
9
9
  import { formatNumber } from '../format/number.js';
10
10
  import { Icon } from '../Icon.js';
@@ -19,17 +19,34 @@ const InMenuContext = createContext(false);
19
19
  const TAB = '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';
20
20
  /* The button that opens the tabs that do not fit reads as one more tab, at the very end of the strip. */
21
21
  const MORE = clsx(TAB, 'ml-auto data-[popup-open]:text-text');
22
+ /* 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. */
23
+ const PILL = 'relative flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md px-2 text-xs font-medium text-text-muted hover:bg-surface-hover hover:text-text data-[active]:bg-surface-active data-[active]:text-text data-[disabled]:opacity-50 has-[button]:pr-1';
24
+ const PILL_MORE = clsx(PILL, 'ml-auto data-[popup-open]:bg-surface-active data-[popup-open]:text-text');
25
+ const VariantContext = createContext('line');
26
+ const STYLES = {
27
+ line: { strip: 'gap-4 border-b border-border', list: 'items-end gap-4', row: 'gap-4', tab: TAB, more: MORE },
28
+ pill: { strip: 'gap-1', list: 'items-center gap-1', row: 'gap-1', tab: PILL, more: PILL_MORE }
29
+ };
22
30
  /* Holds the picked tab itself, uncontrolled or not, so a tab picked from the menu of the strip changes it too. */
23
31
  export function TabsRoot({ value, defaultValue = 0, onValueChange, ...props }) {
24
32
  const [own, setOwn] = useState(defaultValue);
25
- const current = value === undefined ? own : value;
26
- const select = (next) => {
27
- if (value === undefined) {
28
- setOwn(next);
29
- }
30
- onValueChange?.(next);
31
- };
32
- return (_jsx(SelectionContext, { value: { value: current, select }, children: _jsx(BaseTabs.Root, { value: current, onValueChange: (next) => select(next), ...props }) }));
33
+ const controlled = value !== undefined;
34
+ const current = controlled ? value : own;
35
+ // The latest handler without a new context for every render of a parent that writes it inline.
36
+ const latest = useRef(onValueChange);
37
+ useLayoutEffect(() => {
38
+ latest.current = onValueChange;
39
+ });
40
+ const selection = useMemo(() => {
41
+ const select = (next) => {
42
+ if (!controlled) {
43
+ setOwn(next);
44
+ }
45
+ latest.current?.(next);
46
+ };
47
+ return { value: current, select };
48
+ }, [controlled, current]);
49
+ return (_jsx(SelectionContext, { value: selection, children: _jsx(BaseTabs.Root, { value: current, onValueChange: (next) => selection.select(next), ...props }) }));
33
50
  }
34
51
  const sameIndices = (left, right) => left !== null && left.length === right.length && left.every((index, at) => index === right[at]);
35
52
  /*
@@ -37,13 +54,19 @@ const sameIndices = (left, right) => left !== null && left.length === right.leng
37
54
  * in a row nobody sees, so the strip knows what fits before it draws. The tabs are the direct children,
38
55
  * since the menu repeats what a tab holds.
39
56
  */
40
- export function TabsList({ className, children, start, end, ...props }) {
57
+ export function TabsList({ className, children, start, end, after, variant = 'line', overflow = 'menu', ...props }) {
58
+ const style = STYLES[variant];
41
59
  const { t } = useTranslation('ui');
42
60
  const { value, select } = useContext(SelectionContext);
43
61
  const strip = useRef(null);
44
62
  const measure = useRef(null);
45
63
  const head = useRef(null);
46
64
  const tail = useRef(null);
65
+ const beside = useRef(null);
66
+ const list = useRef(null);
67
+ const scrolls = overflow === 'scroll';
68
+ // Which edges of a scrolling row still hide a tab, where it fades out.
69
+ const [edges, setEdges] = useState({ start: false, end: false });
47
70
  // Null until the first measure, which runs before the first paint.
48
71
  const [shown, setShown] = useState(null);
49
72
  const tabs = Children.toArray(children).filter((child) => isValidElement(child) && child.type === TabsTab);
@@ -51,13 +74,13 @@ export function TabsList({ className, children, start, end, ...props }) {
51
74
  useLayoutEffect(() => {
52
75
  const host = strip.current;
53
76
  const row = measure.current;
54
- if (!host || !row) {
77
+ if (scrolls || !host || !row) {
55
78
  return;
56
79
  }
57
80
  const fit = () => {
58
81
  const style = getComputedStyle(host);
59
82
  const gap = parseFloat(style.columnGap) || 0;
60
- const taken = [head.current, tail.current].reduce((sum, part) => (part === null ? sum : sum + part.getBoundingClientRect().width + gap), 0);
83
+ const taken = [head.current, beside.current, tail.current].reduce((sum, part) => (part === null ? sum : sum + part.getBoundingClientRect().width + gap), 0);
61
84
  const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
62
85
  const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
63
86
  const more = cells.pop() ?? 0;
@@ -68,23 +91,66 @@ export function TabsList({ className, children, start, end, ...props }) {
68
91
  const observer = new ResizeObserver(fit);
69
92
  observer.observe(host);
70
93
  observer.observe(row);
71
- for (const part of [head.current, tail.current]) {
94
+ for (const part of [head.current, beside.current, tail.current]) {
72
95
  if (part !== null) {
73
96
  observer.observe(part);
74
97
  }
75
98
  }
76
99
  return () => observer.disconnect();
77
100
  });
101
+ // A picked tab that scrolled out of sight comes back into it, wherever it was picked from.
102
+ useEffect(() => {
103
+ if (scrolls) {
104
+ list.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
105
+ }
106
+ }, [scrolls, value]);
107
+ useEffect(() => {
108
+ const row = list.current;
109
+ if (!scrolls || !row) {
110
+ return;
111
+ }
112
+ const measure = () => {
113
+ const start = row.scrollLeft > 0;
114
+ const end = row.scrollLeft + row.clientWidth < row.scrollWidth - 1;
115
+ setEdges((current) => (current.start === start && current.end === end ? current : { start, end }));
116
+ };
117
+ measure();
118
+ row.addEventListener('scroll', measure, { passive: true });
119
+ const observer = new ResizeObserver(measure);
120
+ observer.observe(row);
121
+ for (const tab of row.children) {
122
+ observer.observe(tab);
123
+ }
124
+ return () => {
125
+ row.removeEventListener('scroll', measure);
126
+ observer.disconnect();
127
+ };
128
+ });
129
+ // A mouse wheel only turns vertically, and on a row that scrolls sideways that is the way it should turn.
130
+ useEffect(() => {
131
+ const row = list.current;
132
+ if (!scrolls || !row) {
133
+ return;
134
+ }
135
+ const onWheel = (event) => {
136
+ if (!event.deltaX && event.deltaY) {
137
+ row.scrollLeft += event.deltaY;
138
+ }
139
+ };
140
+ row.addEventListener('wheel', onWheel, { passive: true });
141
+ return () => row.removeEventListener('wheel', onWheel);
142
+ }, [scrolls]);
78
143
  const visible = shown === null ? tabs : shown.map((index) => tabs[index]);
79
144
  const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
80
145
  const more = (_jsxs(_Fragment, { children: [t('tabs.more'), _jsx(Icon, { icon: ChevronDown, size: 14 })] }));
81
- return (_jsxs("div", { ref: strip, className: clsx('relative flex items-center gap-4 border-b border-border', className), children: [start !== undefined && start !== null && (_jsx("div", { ref: head, className: "flex shrink-0 items-center gap-1", children: start })), _jsx(BaseTabs.List, { className: "flex min-w-0 items-end gap-4", ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsx(MenuTrigger, { className: MORE, children: more }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), end !== undefined && end !== null && (_jsx("div", { ref: tail, className: clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto'), children: end })), _jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: "pointer-events-none invisible absolute top-0 left-0 flex gap-4", children: [tabs.map((tab) => (_jsxs("span", { className: TAB, children: [tab.props.children, tab.props.onClose !== undefined && _jsx("span", { className: "size-5 shrink-0" })] }, tab.key))), _jsx("span", { className: MORE, children: more })] })] }));
146
+ return (_jsx(VariantContext, { value: variant, children: _jsxs("div", { ref: strip, className: clsx('relative flex items-center', style.strip, className), children: [start !== undefined && start !== null && (_jsx("div", { ref: head, className: "flex shrink-0 items-center gap-1", children: start })), _jsx(BaseTabs.List, { ref: list, className: clsx('flex min-w-0', style.list, scrolls && 'tabs-scroll scroll-fade-x'), "data-fade-start": scrolls && edges.start ? '' : undefined, "data-fade-end": scrolls && edges.end ? '' : undefined, ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsx(MenuTrigger, { className: style.more, children: more }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), after !== undefined && after !== null && (_jsx("div", { ref: beside, className: "flex shrink-0 items-center gap-1", children: after })), end !== undefined && end !== null && (_jsx("div", { ref: tail, className: clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto'), children: end })), !scrolls && (_jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: clsx('pointer-events-none invisible absolute top-0 left-0 flex', style.row), children: [tabs.map((tab) => (_jsxs("span", { className: style.tab, children: [tab.props.children, tab.props.onClose !== undefined && _jsx("span", { className: "size-5 shrink-0" })] }, tab.key))), _jsx("span", { className: style.more, children: more })] }))] }) }));
82
147
  }
83
148
  /* The close button stays out of the tab order, so a closable tab is still one stop; the keys close it from the tab itself. */
84
149
  export function TabsTab({ className, onClose, children, ...props }) {
85
150
  const { t } = useTranslation('ui');
151
+ const tab = STYLES[useContext(VariantContext)].tab;
86
152
  if (onClose === undefined) {
87
- return (_jsx(BaseTabs.Tab, { className: withClass(TAB, className), ...props, children: children }));
153
+ return (_jsx(BaseTabs.Tab, { className: withClass(tab, className), ...props, children: children }));
88
154
  }
89
155
  const { onKeyDown, onAuxClick, onMouseDown, ...rest } = props;
90
156
  const handleKeyDown = (e) => {
@@ -108,7 +174,7 @@ export function TabsTab({ className, onClose, children, ...props }) {
108
174
  onClose();
109
175
  }
110
176
  };
111
- return (_jsxs(BaseTabs.Tab, { nativeButton: false, render: _jsx("div", {}), className: withClass(TAB, className), onKeyDown: handleKeyDown, onMouseDown: handleMouseDown, onAuxClick: handleAuxClick, ...rest, children: [children, _jsx(IconButton, { icon: X, size: "2xs", label: t('tabs.close'), tabIndex: -1, onClick: (e) => {
177
+ return (_jsxs(BaseTabs.Tab, { nativeButton: false, render: _jsx("div", {}), className: withClass(tab, className), onKeyDown: handleKeyDown, onMouseDown: handleMouseDown, onAuxClick: handleAuxClick, ...rest, children: [children, _jsx(IconButton, { icon: X, size: "2xs", label: t('tabs.close'), tabIndex: -1, onClick: (e) => {
112
178
  e.stopPropagation();
113
179
  onClose();
114
180
  } })] }));
@@ -1,15 +1,17 @@
1
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { dirname, join, sep } from 'node:path';
3
3
  import { Glob, plugin } from 'bun';
4
4
  /*
5
5
  * A preload for `bun test` in an app that links a checkout of this library: what `resolve.dedupe`
6
6
  * does for Vite. The checkout has node_modules of its own, and a second React, i18next or Base UI
7
7
  * breaks every hook, every word and every compound part. Bun has no dedupe and its resolve hook never
8
8
  * sees a package import, so every file of these packages in the checkout loads as a stand-in for the
9
- * same file in the app's copy. Does nothing for a package the checkout has no copy of, which is every
10
- * package once the library is installed from the registry.
9
+ * same file in the app's copy. Does nothing once the library is installed from the registry.
10
+ *
11
+ * CodeMirror and Lezer reject an extension, a facet or a node prop made by another copy, and the chat
12
+ * views take both from the app; shiki is in the list for the highlighter an app hands them.
11
13
  */
12
- const SHARED = ['react', 'react-dom', 'i18next', 'react-i18next', '@base-ui-components/react'];
14
+ const SHARED = ['react', 'react-dom', 'i18next', 'react-i18next', '@base-ui/react', '@codemirror/*', '@lezer/*', 'shiki'];
13
15
  const NAME = '@adecore/ui';
14
16
  /* The package root, from `src/testing` or `dist/testing` alike. */
15
17
  const library = realpathSync(join(import.meta.dir, '..', '..'));
@@ -18,19 +20,44 @@ const manifestOf = (folder) => {
18
20
  return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
19
21
  };
20
22
  const dependsOnLibrary = (manifest) => manifest !== null && [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((deps) => deps !== undefined && NAME in deps);
23
+ const membersOf = (root, manifest) => {
24
+ const workspaces = manifest?.workspaces;
25
+ const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
26
+ return patterns
27
+ .flatMap((pattern) => [...new Glob(`${pattern}/package.json`).scanSync({ cwd: root, onlyFiles: true })])
28
+ .map((path) => join(root, dirname(path)));
29
+ };
21
30
  /*
22
31
  * Where the app's copies are found: the folder the tests run in, then each workspace of it that
23
32
  * depends on the library. With an isolated install the root of a monorepo has no React of its own.
24
33
  */
25
34
  const appFolders = () => {
26
35
  const root = process.cwd();
27
- const workspaces = manifestOf(root)?.workspaces;
28
- const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
29
- const members = patterns
30
- .flatMap((pattern) => [...new Glob(`${pattern}/package.json`).scanSync({ cwd: root, onlyFiles: true })])
31
- .map((path) => join(root, dirname(path)))
32
- .filter((folder) => dependsOnLibrary(manifestOf(folder)));
33
- return [root, ...members];
36
+ return [root, ...membersOf(root, manifestOf(root)).filter((folder) => dependsOnLibrary(manifestOf(folder)))];
37
+ };
38
+ /* Where the checkout keeps its copies: the library's other packages in the same monorepo bring their own. */
39
+ const checkoutFolders = () => {
40
+ for (let folder = dirname(library); dirname(folder) !== folder; folder = dirname(folder)) {
41
+ const manifest = manifestOf(folder);
42
+ if (manifest?.workspaces !== undefined) {
43
+ const members = membersOf(folder, manifest);
44
+ return members.includes(library) ? [folder, ...members] : [library];
45
+ }
46
+ }
47
+ return [library];
48
+ };
49
+ /* The packages a name of the list stands for in a folder: itself, or every package of the scope for `@scope/*`. */
50
+ const namesIn = (folder, pattern) => {
51
+ if (!pattern.endsWith('/*')) {
52
+ return [pattern];
53
+ }
54
+ const scope = pattern.slice(0, -2);
55
+ const path = join(folder, 'node_modules', scope);
56
+ return existsSync(path)
57
+ ? readdirSync(path)
58
+ .filter((entry) => !entry.startsWith('.'))
59
+ .map((entry) => `${scope}/${entry}`)
60
+ : [];
34
61
  };
35
62
  /* The copy of a package that `from` resolves to, walking up the node_modules folders as Node does. */
36
63
  const copyFrom = (from, name) => {
@@ -44,21 +71,45 @@ const copyFrom = (from, name) => {
44
71
  }
45
72
  }
46
73
  };
47
- const folders = appFolders();
48
- const copies = SHARED.flatMap((name) => {
49
- const theirs = join(library, 'node_modules', name);
50
- if (!existsSync(theirs)) {
74
+ const findCopies = () => {
75
+ const checkout = checkoutFolders();
76
+ const inside = (path) => path === checkout[0] || path.startsWith(`${checkout[0]}/`);
77
+ // A run inside the checkout is the library's own; a node_modules above it belongs to no app.
78
+ if (inside(realpathSync(process.cwd()))) {
51
79
  return [];
52
80
  }
53
- const real = realpathSync(theirs);
54
- const ours = folders.map((folder) => copyFrom(folder, name)).find((copy) => copy !== null && !copy.startsWith(`${library}/`));
55
- return ours === undefined || ours === real ? [] : [{ theirs: `${real}/`, ours: `${ours}/` }];
56
- });
81
+ const folders = appFolders();
82
+ const added = folders.flatMap((folder) => {
83
+ const names = manifestOf(folder)?.adecore?.dedupe;
84
+ return Array.isArray(names) ? names.filter((name) => typeof name === 'string') : [];
85
+ });
86
+ const names = new Set([...SHARED, ...added].flatMap((pattern) => checkout.flatMap((folder) => namesIn(folder, pattern))));
87
+ return [...names].flatMap((name) => {
88
+ const ours = folders.map((folder) => copyFrom(folder, name)).find((copy) => copy !== null && !inside(copy));
89
+ if (ours === undefined) {
90
+ return [];
91
+ }
92
+ const theirs = new Set(checkout
93
+ .map((folder) => join(folder, 'node_modules', name))
94
+ .filter((path) => existsSync(path))
95
+ .map((path) => realpathSync(path)));
96
+ return [...theirs].filter((real) => real !== ours).map((real) => ({ theirs: `${real}/`, ours: `${ours}/` }));
97
+ });
98
+ };
99
+ // From the registry the package manager already decided what to share; a copy nested in the library is on purpose.
100
+ const copies = library.split(sep).includes('node_modules') ? [] : findCopies();
57
101
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
58
- // Bun refuses `export default` beside `export *`, so the stand-in names every export of the real file.
102
+ let transpiler = null;
103
+ /*
104
+ * Re-exports the app's file without running it: `require` refuses an ES module of the app that is
105
+ * still loading. `export *` skips the default, which CommonJS always has and an ES module only when it says so.
106
+ */
59
107
  const standIn = (path) => {
60
- const names = Object.keys(require(path)).filter((name) => name !== 'default' && /^[A-Za-z_$][\w$]*$/.test(name));
61
- return [`import * as real from ${JSON.stringify(path)};`, 'export default real.default;', `export const { ${names.join(', ')} } = real;`].join('\n');
108
+ transpiler ??= new Bun.Transpiler({ loader: 'js' });
109
+ const { exports, imports } = transpiler.scan(readFileSync(path, 'utf8'));
110
+ const esm = exports.length > 0 || imports.some((entry) => entry.kind === 'import-statement');
111
+ const target = JSON.stringify(path);
112
+ return [`export * from ${target};`, ...(!esm || exports.includes('default') ? [`export { default } from ${target};`] : [])].join('\n');
62
113
  };
63
114
  if (copies.length > 0) {
64
115
  plugin({
package/dist/theme.css CHANGED
@@ -473,18 +473,16 @@
473
473
  }
474
474
  /* A picture holds the eye better against a heavier dim than a dialog of controls needs. */
475
475
  .lightbox-backdrop { background: color-mix(in srgb, var(--bg) 80%, transparent); }
476
- /* Tooltips. The positioner animates its position, so a tooltip slides to the next button
477
- instead of blinking; the popup fades and scales from its anchor side. Both drop the motion
478
- while Base UI says the tooltip is instant. */
476
+ /* Tooltips. The popup fades and scales from its anchor side, and drops the motion while Base UI
477
+ says the tooltip is instant. The positioner does not animate its place: Base UI parks it at the
478
+ window's origin until it is measured, so it would slide in from the corner. */
479
479
  /* As wide as its label up to the viewport's max width, so a short label near the edge of the
480
480
  window does not wrap to the room left of it before the positioner moves it back in. */
481
481
  .tooltip-positioner {
482
482
  width: max-content;
483
483
  z-index: var(--z-tooltip);
484
484
  pointer-events: none;
485
- transition: top 150ms ease, left 150ms ease, right 150ms ease, bottom 150ms ease;
486
485
  }
487
- .tooltip-positioner[data-instant] { transition: none; }
488
486
  .tooltip-popup {
489
487
  display: flex;
490
488
  border-radius: 7px;
@@ -824,14 +822,14 @@
824
822
  .ade-workspace[data-layout="roomy"][data-toolbar][data-sidebar] > .ade-workspace-center > .ade-workspace-content { border-top-left-radius: 8px; }
825
823
  .ade-workspace[data-layout="roomy"][data-toolbar][data-side-panel] > .ade-workspace-center > .ade-workspace-content { border-top-right-radius: 8px; }
826
824
  .ade-split-divider:is(:hover, :focus-visible, [data-resizing])::after { background: var(--accent); }
827
- .ade-split-preview {
825
+ .ade-drop-preview {
828
826
  position: absolute;
829
827
  inset: 0;
830
828
  z-index: 4;
831
829
  pointer-events: none;
832
830
  overflow: visible;
833
831
  }
834
- .ade-split-preview path {
832
+ .ade-drop-preview path {
835
833
  fill: color-mix(in srgb, var(--accent) 8%, transparent);
836
834
  stroke: var(--accent);
837
835
  stroke-width: 2px;
@@ -848,6 +846,15 @@
848
846
  scrollbar-width: none;
849
847
  }
850
848
  .ade-tab-strip::-webkit-scrollbar { display: none; }
849
+ /* Tabs that scroll sideways instead of handing the rest to a menu. The pixel at the bottom holds a
850
+ line tab's underline, which the scroll box would clip otherwise. */
851
+ .tabs-scroll {
852
+ overflow-x: auto;
853
+ overflow-y: hidden;
854
+ padding-bottom: 1px;
855
+ scrollbar-width: none;
856
+ }
857
+ .tabs-scroll::-webkit-scrollbar { display: none; }
851
858
  .ade-document-tab {
852
859
  position: relative;
853
860
  display: inline-flex;
@@ -885,5 +892,14 @@
885
892
  .ade-document-tab[data-detail] .ade-document-tab-open { max-width: 200px; }
886
893
  .ade-tab-unsaved { width: 6px; height: 6px; flex: none; border-radius: 999px; background: var(--text-muted); }
887
894
  .ade-tab-insertion { position: absolute; top: 4px; bottom: 4px; width: 2px; pointer-events: none; background: var(--accent); }
888
- @media (prefers-reduced-motion: reduce) { .ade-split-preview path { transition: none; } }
895
+ @media (prefers-reduced-motion: reduce) { .ade-drop-preview path { transition: none; } }
896
+ }
897
+
898
+ @layer components {
899
+ /* `MiddleTruncate`. The text again, unseen and without height, so the element asks for the width of all of it while it draws a part. */
900
+ .middle-truncate { display: block; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
901
+ .middle-truncate::before { content: attr(data-text) / ""; display: block; height: 0; visibility: hidden; }
902
+ .middle-truncate-head::after { content: "\2026" / ""; }
903
+ /* The cut part stays in the text at no width, so a selection copies it and a find matches across the cut. */
904
+ .middle-truncate-middle { display: inline-block; width: 0; overflow: hidden; vertical-align: top; }
889
905
  }
@@ -1,4 +1,4 @@
1
- import { useRender } from '@base-ui-components/react/use-render';
1
+ import { useRender } from '@base-ui/react/use-render';
2
2
  import { type CheckboxProps } from '../Checkbox.tsx';
3
3
  export type TreeRootProps = useRender.ComponentProps<'div'> & {
4
4
  overflow?: 'truncate' | 'scroll';
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback } from 'react';
3
3
  import clsx from 'clsx';
4
- import { useRender } from '@base-ui-components/react/use-render';
4
+ import { useRender } from '@base-ui/react/use-render';
5
5
  import { ChevronRight } from 'lucide-react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Checkbox as UICheckbox } from '../Checkbox.js';
@@ -0,0 +1,8 @@
1
+ import type { Ref } from 'react';
2
+ import { type DropPreviewShape } from './preview.ts';
3
+ export interface DropPreviewProps {
4
+ shape: DropPreviewShape | null;
5
+ className?: string;
6
+ ref?: Ref<SVGSVGElement>;
7
+ }
8
+ export declare function DropPreview({ shape, className, ref }: DropPreviewProps): import("react").JSX.Element | null;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { dropPreviewPath } from './preview.js';
4
+ /*
5
+ * The outline `SplitView` draws where a drag would land, for an app with a drag layer of its own. It
6
+ * fills its nearest positioned ancestor. One shape moves into the next as a transition of its path,
7
+ * and the first shape after a null appears in place.
8
+ */
9
+ export function DropPreview({ shape, className, ref }) {
10
+ if (shape === null) {
11
+ return null;
12
+ }
13
+ return (_jsx("svg", { ref: ref, className: clsx('ade-drop-preview', className), width: "100%", height: "100%", "aria-hidden": "true", children: _jsx("path", { style: { d: `path("${dropPreviewPath(shape)}")` } }) }));
14
+ }
@@ -1,5 +1,5 @@
1
1
  import { type DragEvent, type ReactNode } from 'react';
2
- import { useRender } from '@base-ui-components/react/use-render';
2
+ import { useRender } from '@base-ui/react/use-render';
3
3
  import { type WorkspaceLayout } from './context.ts';
4
4
  import { type SplitEdges, type SplitMinimum, type SplitRect } from './geometry.ts';
5
5
  import { type SplitCommand, type SplitLayout, type SplitPane } from './model.ts';