@adecore/ui 0.53.3 → 0.54.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.
- package/dist/Banner.d.ts +1 -1
- package/dist/ButtonGroup.js +1 -1
- package/dist/Checkbox.js +1 -1
- package/dist/ChoiceCards.d.ts +1 -1
- package/dist/ChoiceCards.js +2 -2
- package/dist/CloseButton.js +1 -1
- package/dist/ColorSwatch.d.ts +1 -1
- package/dist/ColorSwatch.js +1 -1
- package/dist/ColorWheelPicker.js +1 -1
- package/dist/CopyButton.js +1 -1
- package/dist/DockShell.js +1 -1
- package/dist/EmptyState.d.ts +1 -1
- package/dist/EmptyState.js +1 -1
- package/dist/ErrorBoundary.js +122 -112
- package/dist/Field.d.ts +1 -1
- package/dist/Field.js +2 -2
- package/dist/FileIcon.d.ts +1 -1
- package/dist/FileIcon.js +44 -36
- package/dist/Icon.d.ts +1 -1
- package/dist/IconButton.d.ts +3 -3
- package/dist/IconButton.js +2 -2
- package/dist/IconPicker.js +35 -32
- package/dist/Input.d.ts +1 -1
- package/dist/ListRow.js +1 -1
- package/dist/MiddleTruncate.js +2 -2
- package/dist/PanelEmpty.d.ts +1 -1
- package/dist/PanelHeader.d.ts +1 -1
- package/dist/PanelHeader.js +1 -1
- package/dist/ProjectSwitcher.js +1 -1
- package/dist/PromptDialog.d.ts +1 -1
- package/dist/ScrollArea.d.ts +12 -0
- package/dist/ScrollArea.js +144 -0
- package/dist/SectionLabel.js +1 -1
- package/dist/SegmentBar.js +1 -1
- package/dist/Segmented.d.ts +1 -1
- package/dist/Select.d.ts +1 -1
- package/dist/Select.js +22 -16
- package/dist/SelectSearch.js +49 -33
- package/dist/ShortcutHints.js +1 -1
- package/dist/Slider.js +2 -2
- package/dist/SlidingColumn.js +1 -1
- package/dist/SplitButton.js +1 -1
- package/dist/Surface.js +1 -1
- package/dist/Switch.js +1 -1
- package/dist/TextMenu.js +2 -2
- package/dist/Toasts.js +2 -2
- package/dist/Tooltip.d.ts +1 -1
- package/dist/Tooltip.js +1 -1
- package/dist/UIProvider.d.ts +1 -1
- package/dist/Waveform.js +1 -1
- package/dist/Wipe.js +2 -2
- package/dist/combobox/parts.d.ts +3 -3
- package/dist/combobox/parts.js +53 -32
- package/dist/command-palette/parts.d.ts +1 -1
- package/dist/command-palette/parts.js +22 -7
- package/dist/command-palette/virtual.js +4 -4
- package/dist/dialog/parts.d.ts +1 -1
- package/dist/dialog/parts.js +2 -2
- package/dist/file-icon.d.ts +2 -0
- package/dist/file-icon.js +12 -0
- package/dist/file-tree/parts.d.ts +2 -2
- package/dist/file-tree/parts.js +3 -3
- package/dist/file-tree/use-file-tree-shift.d.ts +1 -1
- package/dist/file-tree/use-file-tree-shift.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/key-value-list/parts.js +1 -1
- package/dist/menu/parts.d.ts +3 -3
- package/dist/menu/parts.js +46 -30
- package/dist/popover/parts.d.ts +3 -3
- package/dist/popover/parts.js +110 -71
- package/dist/project-switcher-row.js +1 -1
- package/dist/scrollbar-geometry.d.ts +15 -0
- package/dist/scrollbar-geometry.js +48 -0
- package/dist/settings/MasterDetail.js +40 -30
- package/dist/settings/SettingsDialog.d.ts +1 -1
- package/dist/settings/SettingsDialog.js +27 -11
- package/dist/settings/SettingsRow.d.ts +1 -1
- package/dist/settings/SettingsRow.js +1 -1
- package/dist/settings/SettingsSection.d.ts +1 -1
- package/dist/tabs/parts.d.ts +1 -1
- package/dist/tabs/parts.js +3 -3
- package/dist/testing/dedupe.js +1 -1
- package/dist/theme.css +91 -1
- package/dist/tree/parts.js +2 -2
- package/dist/useOverlayScrollbars.d.ts +7 -0
- package/dist/useOverlayScrollbars.js +281 -0
- package/dist/workspace/PaneBar.js +2 -2
- package/dist/workspace/SplitView.d.ts +1 -1
- package/dist/workspace/SplitView.js +4 -4
- package/dist/workspace/TabStrip.d.ts +1 -1
- package/dist/workspace/TabStrip.js +2 -2
- package/dist/workspace/Workspace.d.ts +1 -1
- package/dist/workspace/Workspace.js +2 -2
- package/dist/workspace/WorkspaceRail.js +1 -1
- package/dist/workspace/WorkspaceTopRow.js +1 -1
- package/package.json +1 -1
- package/src/AccentSwatches.tsx +1 -1
- package/src/Banner.tsx +1 -1
- package/src/Button.tsx +1 -1
- package/src/ButtonGroup.tsx +1 -1
- package/src/Checkbox.tsx +2 -2
- package/src/ChoiceCards.tsx +3 -3
- package/src/CloseButton.tsx +2 -2
- package/src/ColorSwatch.tsx +2 -2
- package/src/ColorWheelPicker.tsx +1 -1
- package/src/ColumnResizeHandle.tsx +1 -1
- package/src/CopyButton.tsx +1 -1
- package/src/DockShell.tsx +1 -1
- package/src/EmptyState.tsx +2 -2
- package/src/ErrorBoundary.tsx +4 -3
- package/src/Field.tsx +2 -2
- package/src/FileIcon.tsx +15 -6
- package/src/Icon.tsx +2 -2
- package/src/IconButton.tsx +3 -3
- package/src/IconPicker.tsx +7 -6
- package/src/Input.tsx +2 -2
- package/src/Kbd.tsx +1 -1
- package/src/ListRow.tsx +1 -1
- package/src/Meter.tsx +1 -1
- package/src/MiddleTruncate.tsx +2 -2
- package/src/PanelEmpty.tsx +2 -2
- package/src/PanelHeader.tsx +2 -2
- package/src/Pill.tsx +1 -1
- package/src/ProjectSwitcher.tsx +2 -2
- package/src/PromptDialog.tsx +1 -1
- package/src/RelativeTime.tsx +1 -1
- package/src/ScrollArea.tsx +47 -0
- package/src/SectionLabel.tsx +1 -1
- package/src/SegmentBar.tsx +1 -1
- package/src/Segmented.tsx +1 -1
- package/src/Select.tsx +6 -2
- package/src/SelectSearch.tsx +19 -12
- package/src/Separator.tsx +1 -1
- package/src/ShortcutHints.tsx +1 -1
- package/src/Skeleton.tsx +1 -1
- package/src/Slider.tsx +2 -2
- package/src/SlidingColumn.tsx +1 -1
- package/src/Spinner.tsx +1 -1
- package/src/SplitButton.tsx +1 -1
- package/src/Stepper.tsx +1 -1
- package/src/Surface.tsx +1 -1
- package/src/Switch.tsx +2 -2
- package/src/TextMenu.tsx +2 -2
- package/src/Tile.tsx +1 -1
- package/src/Toasts.tsx +2 -2
- package/src/Tooltip.tsx +2 -2
- package/src/UIProvider.tsx +1 -1
- package/src/Waveform.tsx +1 -1
- package/src/Wipe.tsx +2 -2
- package/src/ZoomControls.tsx +1 -1
- package/src/combobox/parts.tsx +9 -4
- package/src/command-palette/parts.tsx +25 -19
- package/src/command-palette/virtual.tsx +4 -4
- package/src/dialog/parts.tsx +2 -2
- package/src/file-icon.ts +15 -0
- package/src/file-tree/parts.tsx +6 -6
- package/src/file-tree/use-empty-lines.ts +1 -1
- package/src/file-tree/use-file-tree-shift.tsx +2 -2
- package/src/file-tree/use-slots.ts +2 -2
- package/src/index.ts +2 -0
- package/src/key-value-list/parts.tsx +1 -1
- package/src/menu/parts.tsx +11 -6
- package/src/popover/parts.tsx +13 -5
- package/src/project-switcher-row.tsx +2 -2
- package/src/scrollbar-geometry.ts +67 -0
- package/src/select-shared.ts +1 -1
- package/src/settings/DetailHeader.tsx +1 -1
- package/src/settings/MasterDetail.tsx +9 -6
- package/src/settings/SettingsDialog.tsx +16 -9
- package/src/settings/SettingsRow.tsx +1 -1
- package/src/settings/SettingsSection.tsx +1 -1
- package/src/tabs/parts.tsx +4 -4
- package/src/testing/dedupe.ts +1 -1
- package/src/theme.css +91 -1
- package/src/tree/parts.tsx +2 -2
- package/src/useOverlayScrollbars.tsx +263 -0
- package/src/workspace/DropPreview.tsx +1 -1
- package/src/workspace/PaneBar.tsx +2 -2
- package/src/workspace/SplitView.tsx +5 -5
- package/src/workspace/TabStrip.tsx +3 -3
- package/src/workspace/Workspace.tsx +2 -2
- package/src/workspace/WorkspaceRail.tsx +1 -1
- package/src/workspace/WorkspaceTopRow.tsx +1 -1
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode, type Ref } from 'react';
|
|
2
|
-
import clsx from 'clsx';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
1
|
import { Tabs } from '@base-ui/react/tabs';
|
|
2
|
+
import clsx from 'clsx';
|
|
5
3
|
import { Search, X, type LucideIcon } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState, type ComponentProps, type ComponentType, type ReactNode, type Ref } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { CloseButton } from '../CloseButton.tsx';
|
|
7
7
|
import { DialogDescription, DialogPopup, DialogRoot, DialogTitle } from '../dialog/parts.tsx';
|
|
8
8
|
import { ErrorBoundary } from '../ErrorBoundary.tsx';
|
|
9
9
|
import { Icon } from '../Icon.tsx';
|
|
10
10
|
import { IconButton } from '../IconButton.tsx';
|
|
11
|
-
import { LazyBoundary } from '../LazyBoundary.tsx';
|
|
12
11
|
import { Input } from '../Input.tsx';
|
|
12
|
+
import { LazyBoundary } from '../LazyBoundary.tsx';
|
|
13
|
+
import { ScrollArea } from '../ScrollArea.tsx';
|
|
13
14
|
import { Select } from '../Select.tsx';
|
|
14
15
|
import { SettingsTargetContext } from './target.ts';
|
|
15
16
|
|
|
17
|
+
/* A pane that is not split scrolls as one column: the panel's own props land on the viewport, which stays the tab panel. */
|
|
18
|
+
function renderScrollingPanel({ ref, className, ...props }: ComponentProps<'div'>) {
|
|
19
|
+
return <ScrollArea ref={ref} className="min-h-0 min-w-0 grow" viewportClassName={className} {...props} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
export interface SettingsSectionEntry {
|
|
17
23
|
id: string;
|
|
18
24
|
icon: LucideIcon;
|
|
@@ -125,7 +131,7 @@ function SettingsNav({ groups, footer, account }: Pick<SettingsDialogProps, 'gro
|
|
|
125
131
|
|
|
126
132
|
return (
|
|
127
133
|
<Tabs.List aria-label={t('settings.sections')} activateOnFocus className="flex min-h-0 grow flex-col">
|
|
128
|
-
<
|
|
134
|
+
<ScrollArea viewportRef={list} viewportClassName="scroll-fade-y flex flex-col gap-3.5">
|
|
129
135
|
{groups.map((group) => (
|
|
130
136
|
<div key={group.sections[0]!.id} className="flex flex-col gap-0.5">
|
|
131
137
|
{group.label !== null && <span className="px-2.5 pt-2.5 pb-1 text-xs font-medium text-text-faint">{group.label}</span>}
|
|
@@ -134,7 +140,7 @@ function SettingsNav({ groups, footer, account }: Pick<SettingsDialogProps, 'gro
|
|
|
134
140
|
))}
|
|
135
141
|
</div>
|
|
136
142
|
))}
|
|
137
|
-
</
|
|
143
|
+
</ScrollArea>
|
|
138
144
|
{(footer?.length || account) && (
|
|
139
145
|
<div className="mt-auto flex flex-col gap-3 pt-3.5">
|
|
140
146
|
{footer?.map((section) => (
|
|
@@ -232,7 +238,7 @@ function SearchResults({ search, query, labelOf, onPick }: SearchResultsProps) {
|
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
return (
|
|
235
|
-
<ul
|
|
241
|
+
<ScrollArea render={<ul />} viewportClassName="flex flex-col gap-0.5" aria-label={t('settings.search.results')}>
|
|
236
242
|
{results.map((result) => (
|
|
237
243
|
<li key={`${result.section}:${result.id ?? ''}`}>
|
|
238
244
|
<button
|
|
@@ -245,7 +251,7 @@ function SearchResults({ search, query, labelOf, onPick }: SearchResultsProps) {
|
|
|
245
251
|
</button>
|
|
246
252
|
</li>
|
|
247
253
|
))}
|
|
248
|
-
</
|
|
254
|
+
</ScrollArea>
|
|
249
255
|
);
|
|
250
256
|
}
|
|
251
257
|
|
|
@@ -354,10 +360,11 @@ export function SettingsDialog({
|
|
|
354
360
|
className={clsx(
|
|
355
361
|
'flex min-h-0 min-w-0 grow flex-col outline-none',
|
|
356
362
|
// A split pane scrolls each of its sides itself.
|
|
357
|
-
!entry.split && 'gap-7
|
|
363
|
+
!entry.split && 'gap-7 px-8 pb-10 max-[960px]:px-4',
|
|
358
364
|
// Under a hero the bar's fill marks the edge; a fade would wipe the hero out behind the clear bar.
|
|
359
365
|
!entry.split && !Hero && 'scroll-fade-top pt-1'
|
|
360
366
|
)}
|
|
367
|
+
render={entry.split ? undefined : renderScrollingPanel}
|
|
361
368
|
onScroll={(event) => {
|
|
362
369
|
const top = event.currentTarget.scrollTop;
|
|
363
370
|
event.currentTarget.toggleAttribute('data-fade-start', top > 0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect, useRef, type ReactNode, type Ref } from 'react';
|
|
2
1
|
import clsx from 'clsx';
|
|
3
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, type ReactNode, type Ref } from 'react';
|
|
4
4
|
import { Icon } from '../Icon.tsx';
|
|
5
5
|
import { mergeRefs } from '../merge-refs.ts';
|
|
6
6
|
import { useSettingsTarget } from './target.ts';
|
package/src/tabs/parts.tsx
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Tabs as BaseTabs } from '@base-ui/react/tabs';
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { ChevronDown, X } from 'lucide-react';
|
|
1
5
|
import {
|
|
2
6
|
Children,
|
|
3
7
|
createContext,
|
|
@@ -11,11 +15,7 @@ import {
|
|
|
11
15
|
type ReactElement,
|
|
12
16
|
type ReactNode
|
|
13
17
|
} from 'react';
|
|
14
|
-
import clsx from 'clsx';
|
|
15
|
-
import { ChevronDown, X } from 'lucide-react';
|
|
16
18
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { Tabs as BaseTabs } from '@base-ui/react/tabs';
|
|
18
|
-
import { useRender } from '@base-ui/react/use-render';
|
|
19
19
|
import { withClass } from '../class-name.ts';
|
|
20
20
|
import { formatNumber } from '../format/number.ts';
|
|
21
21
|
import { Icon } from '../Icon.tsx';
|
package/src/testing/dedupe.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Glob, plugin } from 'bun';
|
|
1
2
|
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
3
|
import { dirname, join, sep } from 'node:path';
|
|
3
|
-
import { Glob, plugin } from 'bun';
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
* What `resolve.dedupe` does for Vite, for `bun test` in an app that links a checkout: a second React,
|
package/src/theme.css
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
--surface-raised: #ffffff;
|
|
12
12
|
--surface-sunken: #ececef;
|
|
13
13
|
--track: rgb(0 0 0 / 0.09);
|
|
14
|
+
/* The overlay scrollbars (`ScrollArea`): alphas, so a bar takes the color of whatever it floats over. */
|
|
15
|
+
--scrollbar-thumb: rgb(0 0 0 / 0.22);
|
|
16
|
+
--scrollbar-thumb-hover: rgb(0 0 0 / 0.38);
|
|
17
|
+
--scrollbar-thumb-active: rgb(0 0 0 / 0.5);
|
|
18
|
+
--scrollbar-track: rgb(0 0 0 / 0.04);
|
|
14
19
|
/* A bar that is not the one in focus: between the sunken page behind it and a focused bar, so it
|
|
15
20
|
still reads as a bar. The toolbar of a cell in the grid that does not have the keyboard. */
|
|
16
21
|
--surface-idle: color-mix(in srgb, var(--surface) 50%, var(--surface-sunken));
|
|
@@ -122,6 +127,10 @@
|
|
|
122
127
|
--surface-raised: #18181c;
|
|
123
128
|
--surface-sunken: #0b0b0e;
|
|
124
129
|
--track: rgb(255 255 255 / 0.11);
|
|
130
|
+
--scrollbar-thumb: rgb(255 255 255 / 0.22);
|
|
131
|
+
--scrollbar-thumb-hover: rgb(255 255 255 / 0.38);
|
|
132
|
+
--scrollbar-thumb-active: rgb(255 255 255 / 0.5);
|
|
133
|
+
--scrollbar-track: rgb(255 255 255 / 0.04);
|
|
125
134
|
--surface-hover: #202024;
|
|
126
135
|
--surface-active: #28282e;
|
|
127
136
|
/* Derived, so an app that tints its surfaces tints its fields along. */
|
|
@@ -811,7 +820,7 @@
|
|
|
811
820
|
position: absolute;
|
|
812
821
|
inset-block: 0;
|
|
813
822
|
border-radius: 999px;
|
|
814
|
-
background: var(--
|
|
823
|
+
background: var(--scrollbar-thumb);
|
|
815
824
|
}
|
|
816
825
|
@media (prefers-reduced-motion: reduce) {
|
|
817
826
|
.adecore-tree-shift-track {
|
|
@@ -1083,3 +1092,84 @@
|
|
|
1083
1092
|
/* The cut part stays in the text at no width, so a selection copies it and a find matches across the cut. */
|
|
1084
1093
|
.middle-truncate-middle { display: inline-block; width: 0; overflow: hidden; vertical-align: top; }
|
|
1085
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
@layer components {
|
|
1097
|
+
/* `ScrollArea` and `useOverlayScrollbars`. The scroller stays native and hides its own bar; thin bars
|
|
1098
|
+
float over the content, hidden at rest and shown while it scrolls. */
|
|
1099
|
+
[data-overlay-scrollbars] { scrollbar-width: none; }
|
|
1100
|
+
[data-overlay-scrollbars]::-webkit-scrollbar { display: none; }
|
|
1101
|
+
/* A drag that strays over the content must not select it, nor may a long press on a bar. */
|
|
1102
|
+
[data-overlay-scrollbars][data-scrollbar-dragging], .ade-scrollbar { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
|
|
1103
|
+
.ade-scroll-area { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
1104
|
+
/* Its own stacking layer, so a sticky header with a z-index inside never covers the bars. */
|
|
1105
|
+
/* `contain` lets the box bounce at its own end; the page's `overscroll-behavior: none` would take the bounce of a scroll chained up to it. */
|
|
1106
|
+
.ade-scroll-viewport { flex: 1 1 auto; min-height: 0; overflow: auto; isolation: isolate; overscroll-behavior: contain; }
|
|
1107
|
+
.ade-scroll-viewport[data-axis="y"] { overflow-x: hidden; }
|
|
1108
|
+
.ade-scroll-viewport[data-axis="x"] { overflow-y: hidden; }
|
|
1109
|
+
.ade-scrollbar {
|
|
1110
|
+
--scrollbar-size: 12px;
|
|
1111
|
+
position: absolute;
|
|
1112
|
+
z-index: 1;
|
|
1113
|
+
display: none;
|
|
1114
|
+
opacity: 0;
|
|
1115
|
+
pointer-events: none;
|
|
1116
|
+
transition: opacity 240ms ease;
|
|
1117
|
+
}
|
|
1118
|
+
.ade-scrollbar[data-overflow] { display: block; }
|
|
1119
|
+
/* A popup's bars lie beside it in its positioner; the ends keep clear of its rounded corners. */
|
|
1120
|
+
:is(.menu-popup, .picker-popup) ~ .ade-scrollbar { --scrollbar-inset: 4px; }
|
|
1121
|
+
.ade-scrollbar[data-visible] { opacity: 1; pointer-events: auto; transition-duration: 120ms; }
|
|
1122
|
+
/* `coveredBottom` lifts the bars of its own area only, never those of a scroller inside it. */
|
|
1123
|
+
.ade-scroll-viewport { --scrollbar-covered-bottom: 0px; }
|
|
1124
|
+
/* The two bars meet without overlapping: each stops a bar's width short of the corner. */
|
|
1125
|
+
.ade-scrollbar[data-orientation="vertical"] { top: var(--scrollbar-inset, 2px); bottom: calc(var(--scrollbar-covered-bottom, 0px) + var(--scrollbar-inset, 2px)); right: 0; width: var(--scrollbar-size); }
|
|
1126
|
+
.ade-scrollbar[data-orientation="vertical"][data-corner] { bottom: calc(var(--scrollbar-covered-bottom, 0px) + var(--scrollbar-size)); }
|
|
1127
|
+
.ade-scrollbar[data-orientation="horizontal"] { left: var(--scrollbar-inset, 2px); right: var(--scrollbar-inset, 2px); bottom: var(--scrollbar-covered-bottom, 0px); height: var(--scrollbar-size); }
|
|
1128
|
+
.ade-scrollbar[data-orientation="horizontal"][data-corner] { right: var(--scrollbar-size); }
|
|
1129
|
+
.ade-scrollbar::before {
|
|
1130
|
+
content: "";
|
|
1131
|
+
position: absolute;
|
|
1132
|
+
border-radius: 999px;
|
|
1133
|
+
background: var(--scrollbar-track);
|
|
1134
|
+
opacity: 0;
|
|
1135
|
+
transition: opacity 120ms ease;
|
|
1136
|
+
}
|
|
1137
|
+
/* The track is as thick as the widened pill, also on touch where the hit area is wider. */
|
|
1138
|
+
.ade-scrollbar[data-orientation="vertical"]::before { top: 0; bottom: 0; right: 2px; width: 8px; }
|
|
1139
|
+
.ade-scrollbar[data-orientation="horizontal"]::before { left: 0; right: 0; bottom: 2px; height: 8px; }
|
|
1140
|
+
/* The thumb spans the whole hit area across, so a press beside the pill still grabs it; the pill is its `::before`. */
|
|
1141
|
+
.ade-scrollbar-thumb { position: absolute; top: 0; left: 0; touch-action: none; }
|
|
1142
|
+
.ade-scrollbar[data-orientation="vertical"] .ade-scrollbar-thumb { right: 0; }
|
|
1143
|
+
.ade-scrollbar[data-orientation="horizontal"] .ade-scrollbar-thumb { bottom: 0; }
|
|
1144
|
+
.ade-scrollbar-thumb::before {
|
|
1145
|
+
content: "";
|
|
1146
|
+
position: absolute;
|
|
1147
|
+
border-radius: 999px;
|
|
1148
|
+
background: var(--scrollbar-thumb);
|
|
1149
|
+
transition: background-color 120ms ease, width 120ms ease, height 120ms ease, right 120ms ease, bottom 120ms ease;
|
|
1150
|
+
}
|
|
1151
|
+
.ade-scrollbar[data-orientation="vertical"] .ade-scrollbar-thumb::before { top: 0; bottom: 0; right: 3px; width: 6px; }
|
|
1152
|
+
.ade-scrollbar[data-orientation="horizontal"] .ade-scrollbar-thumb::before { left: 0; right: 0; bottom: 3px; height: 6px; }
|
|
1153
|
+
.ade-scrollbar[data-dragging]::before { opacity: 1; }
|
|
1154
|
+
.ade-scrollbar[data-dragging] .ade-scrollbar-thumb::before { background: var(--scrollbar-thumb-active); }
|
|
1155
|
+
.ade-scrollbar[data-orientation="vertical"][data-dragging] .ade-scrollbar-thumb::before { right: 2px; width: 8px; }
|
|
1156
|
+
.ade-scrollbar[data-orientation="horizontal"][data-dragging] .ade-scrollbar-thumb::before { bottom: 2px; height: 8px; }
|
|
1157
|
+
@media (hover: hover) {
|
|
1158
|
+
.ade-scrollbar:hover::before { opacity: 1; }
|
|
1159
|
+
.ade-scrollbar:hover:not([data-dragging]) .ade-scrollbar-thumb::before { background: var(--scrollbar-thumb-hover); }
|
|
1160
|
+
.ade-scrollbar[data-orientation="vertical"]:hover .ade-scrollbar-thumb::before { right: 2px; width: 8px; }
|
|
1161
|
+
.ade-scrollbar[data-orientation="horizontal"]:hover .ade-scrollbar-thumb::before { bottom: 2px; height: 8px; }
|
|
1162
|
+
}
|
|
1163
|
+
/* A finger needs a wider target and a thicker pill. Only the thumb takes a touch, so a tap near the
|
|
1164
|
+
edge still reaches the row under the bar. */
|
|
1165
|
+
@media (pointer: coarse) {
|
|
1166
|
+
.ade-scrollbar { --scrollbar-size: 20px; }
|
|
1167
|
+
.ade-scrollbar[data-visible] { pointer-events: none; }
|
|
1168
|
+
.ade-scrollbar[data-visible] .ade-scrollbar-thumb { pointer-events: auto; }
|
|
1169
|
+
.ade-scrollbar[data-orientation="vertical"] .ade-scrollbar-thumb::before { right: 2px; width: 8px; }
|
|
1170
|
+
.ade-scrollbar[data-orientation="horizontal"] .ade-scrollbar-thumb::before { bottom: 2px; height: 8px; }
|
|
1171
|
+
}
|
|
1172
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1173
|
+
.ade-scrollbar, .ade-scrollbar::before, .ade-scrollbar-thumb::before { transition: none; }
|
|
1174
|
+
}
|
|
1175
|
+
}
|
package/src/tree/parts.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type MouseEvent } from 'react';
|
|
2
|
-
import clsx from 'clsx';
|
|
3
1
|
import { useRender } from '@base-ui/react/use-render';
|
|
2
|
+
import clsx from 'clsx';
|
|
4
3
|
import { ChevronRight } from 'lucide-react';
|
|
4
|
+
import { type MouseEvent } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Checkbox as UICheckbox, type CheckboxProps } from '../Checkbox.tsx';
|
|
7
7
|
import { Icon } from '../Icon.tsx';
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { useLayoutEffect, useState, type ReactElement } from 'react';
|
|
2
|
+
import { overflows, pageToward, scrollAfterDrag, thumbOf, type ScrollMetrics } from './scrollbar-geometry.ts';
|
|
3
|
+
|
|
4
|
+
export type ScrollAreaAxis = 'y' | 'x' | 'both';
|
|
5
|
+
|
|
6
|
+
export interface OverlayScrollbarsOptions {
|
|
7
|
+
/* The directions that get a bar. */
|
|
8
|
+
axis?: ScrollAreaAxis;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* How long the bars stay after the last scroll, or after the pointer left a bar. */
|
|
12
|
+
export const SCROLLBAR_LINGER_MS = 600;
|
|
13
|
+
|
|
14
|
+
/* The theme hides the native bar of a scroller that carries this attribute. */
|
|
15
|
+
const OVERLAY_ATTRIBUTE = 'data-overlay-scrollbars';
|
|
16
|
+
|
|
17
|
+
interface Bar {
|
|
18
|
+
readonly element: HTMLElement;
|
|
19
|
+
readonly thumb: HTMLElement;
|
|
20
|
+
readonly vertical: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Draws thin bars over `viewport`, a scroller that stays native, so wheel, keys, `scrollIntoView` and
|
|
25
|
+
* a virtualizer keep working on it. Put the returned element in a positioned ancestor whose box is
|
|
26
|
+
* the viewport's: the bars lie along its right and bottom edges.
|
|
27
|
+
*/
|
|
28
|
+
export function useOverlayScrollbars(viewport: HTMLElement | null, { axis = 'y' }: OverlayScrollbarsOptions = {}): ReactElement {
|
|
29
|
+
const [vertical, setVertical] = useState<HTMLDivElement | null>(null);
|
|
30
|
+
const [horizontal, setHorizontal] = useState<HTMLDivElement | null>(null);
|
|
31
|
+
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
if (viewport === null) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bars: Bar[] = [vertical, horizontal]
|
|
38
|
+
.filter((element) => element !== null)
|
|
39
|
+
.map((element) => ({ element, thumb: element.firstElementChild as HTMLElement, vertical: element === vertical }));
|
|
40
|
+
|
|
41
|
+
let overBar = false;
|
|
42
|
+
let dragging = false;
|
|
43
|
+
let lingering = false;
|
|
44
|
+
let lingerTimer = 0;
|
|
45
|
+
|
|
46
|
+
const metricsOf = (bar: Bar): ScrollMetrics =>
|
|
47
|
+
bar.vertical
|
|
48
|
+
? { track: bar.element.clientHeight, view: viewport.clientHeight, content: viewport.scrollHeight }
|
|
49
|
+
: { track: bar.element.clientWidth, view: viewport.clientWidth, content: viewport.scrollWidth };
|
|
50
|
+
|
|
51
|
+
const positionOf = (bar: Bar): number => (bar.vertical ? viewport.scrollTop : viewport.scrollLeft);
|
|
52
|
+
|
|
53
|
+
const show = (): void => {
|
|
54
|
+
const visible = overBar || dragging || lingering;
|
|
55
|
+
|
|
56
|
+
for (const bar of bars) {
|
|
57
|
+
bar.element.toggleAttribute('data-visible', visible);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const layout = (): void => {
|
|
62
|
+
const overflowing = bars.map((bar) =>
|
|
63
|
+
bar.vertical ? overflows(viewport.clientHeight, viewport.scrollHeight) : overflows(viewport.clientWidth, viewport.scrollWidth)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const corner = overflowing.length === 2 && overflowing.every(Boolean);
|
|
67
|
+
|
|
68
|
+
bars.forEach((bar, index) => {
|
|
69
|
+
bar.element.toggleAttribute('data-overflow', overflowing[index]);
|
|
70
|
+
bar.element.toggleAttribute('data-corner', corner);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
bars.forEach((bar, index) => {
|
|
74
|
+
if (!overflowing[index]) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { size, offset } = thumbOf(metricsOf(bar), positionOf(bar));
|
|
79
|
+
|
|
80
|
+
if (bar.vertical) {
|
|
81
|
+
bar.thumb.style.height = `${size}px`;
|
|
82
|
+
bar.thumb.style.translate = `0 ${offset}px`;
|
|
83
|
+
} else {
|
|
84
|
+
bar.thumb.style.width = `${size}px`;
|
|
85
|
+
bar.thumb.style.translate = `${offset}px 0`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const linger = (): void => {
|
|
91
|
+
lingering = true;
|
|
92
|
+
window.clearTimeout(lingerTimer);
|
|
93
|
+
|
|
94
|
+
lingerTimer = window.setTimeout(() => {
|
|
95
|
+
lingering = false;
|
|
96
|
+
show();
|
|
97
|
+
}, SCROLLBAR_LINGER_MS);
|
|
98
|
+
|
|
99
|
+
show();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onScroll = (): void => {
|
|
103
|
+
layout();
|
|
104
|
+
linger();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Only a scroll brings the bars; a bar the pointer reached while they showed keeps them, since a hidden one takes no pointer.
|
|
108
|
+
const onBarEnter = (event: PointerEvent): void => {
|
|
109
|
+
if (event.pointerType !== 'touch') {
|
|
110
|
+
overBar = true;
|
|
111
|
+
show();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const onBarLeave = (): void => {
|
|
116
|
+
if (overBar) {
|
|
117
|
+
overBar = false;
|
|
118
|
+
linger();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const reducedMotion = (): boolean => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
123
|
+
|
|
124
|
+
const pressHandlers = bars.map((bar) => {
|
|
125
|
+
const onThumbDown = (event: PointerEvent): void => {
|
|
126
|
+
if (event.button !== 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The press belongs to the thumb: no text selection, no page from the track under it.
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
event.stopPropagation();
|
|
133
|
+
bar.thumb.setPointerCapture(event.pointerId);
|
|
134
|
+
|
|
135
|
+
const metrics = metricsOf(bar);
|
|
136
|
+
const from = bar.vertical ? event.clientY : event.clientX;
|
|
137
|
+
const start = positionOf(bar);
|
|
138
|
+
dragging = true;
|
|
139
|
+
bar.element.setAttribute('data-dragging', '');
|
|
140
|
+
viewport.setAttribute('data-scrollbar-dragging', '');
|
|
141
|
+
show();
|
|
142
|
+
|
|
143
|
+
const onMove = (move: PointerEvent): void => {
|
|
144
|
+
const next = scrollAfterDrag(metrics, start, (bar.vertical ? move.clientY : move.clientX) - from);
|
|
145
|
+
|
|
146
|
+
if (bar.vertical) {
|
|
147
|
+
viewport.scrollTop = next;
|
|
148
|
+
} else {
|
|
149
|
+
viewport.scrollLeft = next;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const onEnd = (): void => {
|
|
154
|
+
bar.thumb.removeEventListener('pointermove', onMove);
|
|
155
|
+
bar.thumb.removeEventListener('pointerup', onEnd);
|
|
156
|
+
bar.thumb.removeEventListener('pointercancel', onEnd);
|
|
157
|
+
bar.thumb.removeEventListener('lostpointercapture', onEnd);
|
|
158
|
+
dragging = false;
|
|
159
|
+
bar.element.removeAttribute('data-dragging');
|
|
160
|
+
viewport.removeAttribute('data-scrollbar-dragging');
|
|
161
|
+
linger();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
bar.thumb.addEventListener('pointermove', onMove);
|
|
165
|
+
bar.thumb.addEventListener('pointerup', onEnd);
|
|
166
|
+
bar.thumb.addEventListener('pointercancel', onEnd);
|
|
167
|
+
bar.thumb.addEventListener('lostpointercapture', onEnd);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// WebKit on iOS starts a text selection on a long press whatever the pointer events do; only the touch itself can be refused.
|
|
171
|
+
const onThumbTouch = (event: TouchEvent): void => {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const onTrackDown = (event: PointerEvent): void => {
|
|
176
|
+
if (event.button !== 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
const thumb = bar.thumb.getBoundingClientRect();
|
|
182
|
+
|
|
183
|
+
const delta = bar.vertical
|
|
184
|
+
? pageToward(event.clientY, thumb.top, thumb.bottom, viewport.clientHeight)
|
|
185
|
+
: pageToward(event.clientX, thumb.left, thumb.right, viewport.clientWidth);
|
|
186
|
+
|
|
187
|
+
if (delta === 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const behavior = reducedMotion() ? 'instant' : 'smooth';
|
|
192
|
+
viewport.scrollBy(bar.vertical ? { top: delta, behavior } : { left: delta, behavior });
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
bar.thumb.addEventListener('pointerdown', onThumbDown);
|
|
196
|
+
bar.thumb.addEventListener('touchstart', onThumbTouch, { passive: false });
|
|
197
|
+
bar.element.addEventListener('pointerdown', onTrackDown);
|
|
198
|
+
bar.element.addEventListener('pointerenter', onBarEnter);
|
|
199
|
+
bar.element.addEventListener('pointerleave', onBarLeave);
|
|
200
|
+
|
|
201
|
+
return () => {
|
|
202
|
+
bar.thumb.removeEventListener('pointerdown', onThumbDown);
|
|
203
|
+
bar.thumb.removeEventListener('touchstart', onThumbTouch);
|
|
204
|
+
bar.element.removeEventListener('pointerdown', onTrackDown);
|
|
205
|
+
bar.element.removeEventListener('pointerenter', onBarEnter);
|
|
206
|
+
bar.element.removeEventListener('pointerleave', onBarLeave);
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// A document without layout, such as a test's, has no observers; the bars then follow the scroll alone.
|
|
211
|
+
const sizes = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(layout);
|
|
212
|
+
sizes?.observe(viewport);
|
|
213
|
+
|
|
214
|
+
const observeContent = (): void => {
|
|
215
|
+
for (const child of viewport.children) {
|
|
216
|
+
sizes?.observe(child);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// A row added to the content changes what scrolls without resizing the rows already observed.
|
|
221
|
+
const content =
|
|
222
|
+
typeof MutationObserver === 'undefined'
|
|
223
|
+
? null
|
|
224
|
+
: new MutationObserver(() => {
|
|
225
|
+
observeContent();
|
|
226
|
+
layout();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
content?.observe(viewport, { childList: true });
|
|
230
|
+
observeContent();
|
|
231
|
+
viewport.setAttribute(OVERLAY_ATTRIBUTE, '');
|
|
232
|
+
viewport.addEventListener('scroll', onScroll, { passive: true });
|
|
233
|
+
layout();
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
for (const release of pressHandlers) {
|
|
237
|
+
release();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
sizes?.disconnect();
|
|
241
|
+
content?.disconnect();
|
|
242
|
+
window.clearTimeout(lingerTimer);
|
|
243
|
+
viewport.removeAttribute(OVERLAY_ATTRIBUTE);
|
|
244
|
+
viewport.removeAttribute('data-scrollbar-dragging');
|
|
245
|
+
viewport.removeEventListener('scroll', onScroll);
|
|
246
|
+
};
|
|
247
|
+
}, [viewport, vertical, horizontal]);
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<>
|
|
251
|
+
{axis !== 'x' && (
|
|
252
|
+
<div ref={setVertical} aria-hidden className="ade-scrollbar" data-orientation="vertical">
|
|
253
|
+
<div className="ade-scrollbar-thumb" />
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
{axis !== 'y' && (
|
|
257
|
+
<div ref={setHorizontal} aria-hidden className="ade-scrollbar" data-orientation="horizontal">
|
|
258
|
+
<div className="ade-scrollbar-thumb" />
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ComponentProps, type DragEvent, type ReactNode, type RefObject } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
1
|
import clsx from 'clsx';
|
|
4
2
|
import { MoreHorizontal } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, useState, type ComponentProps, type DragEvent, type ReactNode, type RefObject } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { CloseButton } from '../CloseButton.tsx';
|
|
7
7
|
import * as ContextMenu from '../context-menu/index.parts.ts';
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { GripVertical, Maximize2, Minimize2 } from 'lucide-react';
|
|
1
4
|
import {
|
|
2
5
|
useCallback,
|
|
3
6
|
useContext,
|
|
@@ -12,12 +15,10 @@ import {
|
|
|
12
15
|
type KeyboardEvent,
|
|
13
16
|
type ReactNode
|
|
14
17
|
} from 'react';
|
|
15
|
-
import { useRender } from '@base-ui/react/use-render';
|
|
16
|
-
import { GripVertical, Maximize2, Minimize2 } from 'lucide-react';
|
|
17
|
-
import clsx from 'clsx';
|
|
18
18
|
import { useTranslation } from 'react-i18next';
|
|
19
19
|
import { IconButton } from '../IconButton.tsx';
|
|
20
20
|
import { WorkspaceContext, type WorkspaceLayout } from './context.ts';
|
|
21
|
+
import { DropPreview } from './DropPreview.tsx';
|
|
21
22
|
import {
|
|
22
23
|
paneRadius,
|
|
23
24
|
resizeSplit,
|
|
@@ -30,9 +31,8 @@ import {
|
|
|
30
31
|
type SplitRect
|
|
31
32
|
} from './geometry.ts';
|
|
32
33
|
import { splitPanes, updateSplitLayout, type SplitCommand, type SplitLayout, type SplitPane } from './model.ts';
|
|
33
|
-
import { TabStrip, type TabStripItem, type TabStripProps } from './TabStrip.tsx';
|
|
34
|
-
import { DropPreview } from './DropPreview.tsx';
|
|
35
34
|
import { rectDropPreview, sameDropPreview, tabDropPreview, type DropPreviewShape } from './preview.ts';
|
|
35
|
+
import { TabStrip, type TabStripItem, type TabStripProps } from './TabStrip.tsx';
|
|
36
36
|
|
|
37
37
|
export interface SplitViewInfo {
|
|
38
38
|
paneId: string;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Pin, X } from 'lucide-react';
|
|
1
4
|
import {
|
|
2
5
|
createContext,
|
|
3
6
|
useContext,
|
|
@@ -12,9 +15,6 @@ import {
|
|
|
12
15
|
type UIEvent,
|
|
13
16
|
type WheelEvent
|
|
14
17
|
} from 'react';
|
|
15
|
-
import { useRender } from '@base-ui/react/use-render';
|
|
16
|
-
import { Pin, X } from 'lucide-react';
|
|
17
|
-
import clsx from 'clsx';
|
|
18
18
|
import { useTranslation } from 'react-i18next';
|
|
19
19
|
import { Icon } from '../Icon.tsx';
|
|
20
20
|
import { IconButton, type IconButtonProps } from '../IconButton.tsx';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react';
|
|
2
|
-
import clsx from 'clsx';
|
|
3
1
|
import { useRender } from '@base-ui/react/use-render';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
4
|
import { TopRowSlotContext, WorkspaceContext, type WorkspaceLayout } from './context.ts';
|
|
5
5
|
|
|
6
6
|
export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useContext, type ComponentProps, type ReactNode } from 'react';
|
|
2
1
|
import clsx from 'clsx';
|
|
2
|
+
import { useContext, type ComponentProps, type ReactNode } from 'react';
|
|
3
3
|
import { TopRowSlotContext } from './context.ts';
|
|
4
4
|
|
|
5
5
|
export interface WorkspaceTopRowProps extends Omit<ComponentProps<'header'>, 'children'> {
|