@surflane/ui 0.1.0 → 0.3.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.
- package/README.md +8 -8
- package/dist/advanced-forms.d.ts +2 -2
- package/dist/advanced-forms.js +39 -39
- package/dist/app-shell.d.ts +3 -2
- package/dist/app-shell.js +29 -13
- package/dist/button.d.ts +5 -5
- package/dist/button.js +12 -12
- package/dist/chart.d.ts +3 -3
- package/dist/chart.js +36 -34
- package/dist/commands.d.ts +5 -5
- package/dist/commands.js +17 -17
- package/dist/content.d.ts +2 -2
- package/dist/content.js +66 -28
- package/dist/context-overlays.d.ts +3 -3
- package/dist/context-overlays.js +11 -11
- package/dist/data.d.ts +58 -13
- package/dist/data.js +63 -21
- package/dist/date-picker.d.ts +8 -8
- package/dist/date-picker.js +18 -18
- package/dist/dialog-focus.js +7 -7
- package/dist/dialog.d.ts +5 -5
- package/dist/dialog.js +16 -16
- package/dist/feedback.d.ts +5 -5
- package/dist/feedback.js +20 -20
- package/dist/file-upload.d.ts +9 -9
- package/dist/file-upload.js +64 -63
- package/dist/filter-bar.d.ts +1 -1
- package/dist/filter-bar.js +9 -9
- package/dist/form-state.d.ts +1 -1
- package/dist/form-state.js +11 -11
- package/dist/forms.d.ts +5 -5
- package/dist/forms.js +32 -32
- package/dist/foundations.d.ts +10 -10
- package/dist/foundations.js +26 -26
- package/dist/icon.d.ts +8 -7
- package/dist/icon.js +23 -22
- package/dist/index.d.ts +38 -37
- package/dist/index.js +39 -38
- package/dist/input-extensions.d.ts +3 -3
- package/dist/input-extensions.js +21 -21
- package/dist/input.d.ts +2 -2
- package/dist/input.js +7 -7
- package/dist/markdown.d.ts +2 -2
- package/dist/markdown.js +38 -38
- package/dist/menu.d.ts +2 -2
- package/dist/menu.js +19 -19
- package/dist/messages.d.ts +2 -2
- package/dist/messages.js +18 -18
- package/dist/navigation-menu.d.ts +2 -2
- package/dist/navigation-menu.js +16 -16
- package/dist/navigation.d.ts +5 -5
- package/dist/navigation.js +40 -40
- package/dist/otp-field.d.ts +1 -1
- package/dist/otp-field.js +14 -14
- package/dist/pagination.d.ts +2 -2
- package/dist/pagination.js +12 -12
- package/dist/panels.d.ts +8 -8
- package/dist/panels.js +39 -39
- package/dist/parts.d.ts +2 -2
- package/dist/parts.js +8 -8
- package/dist/popover.d.ts +3 -3
- package/dist/popover.js +6 -6
- package/dist/scroll-area.d.ts +1 -1
- package/dist/scroll-area.js +20 -20
- package/dist/select.d.ts +8 -8
- package/dist/select.js +21 -21
- package/dist/sizing.d.ts +3 -3
- package/dist/sizing.js +9 -9
- package/dist/slot.d.ts +1 -1
- package/dist/slot.js +4 -4
- package/dist/status.d.ts +4 -4
- package/dist/status.js +8 -8
- package/dist/syntax-engine.js +43 -43
- package/dist/syntax-highlighter.d.ts +2 -2
- package/dist/syntax-highlighter.js +32 -32
- package/dist/table-selection.d.ts +31 -0
- package/dist/table-selection.js +50 -0
- package/dist/toast.d.ts +4 -4
- package/dist/toast.js +33 -33
- package/dist/tone-transition.d.ts +1 -1
- package/dist/tone-transition.js +17 -17
- package/dist/tooltip.d.ts +1 -1
- package/dist/tooltip.js +4 -4
- package/dist/tree.d.ts +4 -4
- package/dist/tree.js +60 -60
- package/package.json +2 -2
- package/src/styles.css +266 -142
package/dist/toast.js
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import * as React from
|
|
4
|
-
import { useComponentSize } from
|
|
5
|
-
import { Toast as BaseToast } from
|
|
6
|
-
import { part, skin } from
|
|
7
|
-
import { StatusIcon } from
|
|
8
|
-
const ToneContext = React.createContext(
|
|
9
|
-
const Root = React.forwardRef(function Root({ tone =
|
|
10
|
-
return (_jsx(ToneContext.Provider, { value: tone, children: _jsx(BaseToast.Root, { ...props, ref: ref, "data-tone": tone, className: skin(
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useComponentSize } from './sizing.js';
|
|
5
|
+
import { Toast as BaseToast } from '@base-ui/react/toast';
|
|
6
|
+
import { part, skin } from './parts.js';
|
|
7
|
+
import { StatusIcon } from './status.js';
|
|
8
|
+
const ToneContext = React.createContext('neutral');
|
|
9
|
+
const Root = React.forwardRef(function Root({ tone = 'neutral', className, ...props }, ref) {
|
|
10
|
+
return (_jsx(ToneContext.Provider, { value: tone, children: _jsx(BaseToast.Root, { ...props, ref: ref, "data-tone": tone, className: skin('pui-toast', className) }) }));
|
|
11
11
|
});
|
|
12
12
|
const Icon = React.forwardRef(function Icon({ tone, children, className, ...props }, ref) {
|
|
13
13
|
const inherited = React.useContext(ToneContext);
|
|
14
|
-
return (_jsx("span", { ...props, ref: ref, "aria-hidden":
|
|
14
|
+
return (_jsx("span", { ...props, ref: ref, "aria-hidden": 'true', className: ['pui-toast-icon', className].filter(Boolean).join(' '), children: children ?? _jsx(StatusIcon, { tone: tone ?? inherited }) }));
|
|
15
15
|
});
|
|
16
16
|
const Close = React.forwardRef(function Close({ className, children, ...props }, ref) {
|
|
17
|
-
return (_jsx(BaseToast.Close, { "aria-label":
|
|
17
|
+
return (_jsx(BaseToast.Close, { "aria-label": 'Dismiss notification', "aria-hidden": false, "data-variant": 'ghost', "data-size": 'icon', ...props, ref: ref, className: skin('pui-button pui-toast-close', className), children: children ?? (_jsx("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '1.6', "aria-hidden": 'true', children: _jsx("path", { d: 'm6 6 12 12M18 6 6 18' }) })) }));
|
|
18
18
|
});
|
|
19
19
|
const Action = React.forwardRef(function Action({ className, ...props }, ref) {
|
|
20
|
-
return (_jsx(BaseToast.Action, { "data-variant":
|
|
20
|
+
return (_jsx(BaseToast.Action, { "data-variant": 'outline', "data-size": 'sm', ...props, ref: ref, className: skin('pui-button pui-toast-action', className) }));
|
|
21
21
|
});
|
|
22
|
-
const Viewport = React.forwardRef(function Viewport({ layout =
|
|
22
|
+
const Viewport = React.forwardRef(function Viewport({ layout = 'stack', className, ...props }, ref) {
|
|
23
23
|
const viewportRef = React.useRef(null);
|
|
24
24
|
React.useLayoutEffect(() => {
|
|
25
25
|
const viewport = viewportRef.current;
|
|
26
|
-
if (!viewport || layout !==
|
|
26
|
+
if (!viewport || layout !== 'stack')
|
|
27
27
|
return;
|
|
28
28
|
const resize = new ResizeObserver(update);
|
|
29
29
|
function update() {
|
|
30
|
-
const cards = Array.from(viewport.querySelectorAll(
|
|
30
|
+
const cards = Array.from(viewport.querySelectorAll('.pui-toast:not([data-limited])'));
|
|
31
31
|
const gap = parseFloat(getComputedStyle(viewport).rowGap) || 0;
|
|
32
32
|
const height = cards.reduce((sum, card) => {
|
|
33
|
-
const natural = parseFloat(getComputedStyle(card).getPropertyValue(
|
|
33
|
+
const natural = parseFloat(getComputedStyle(card).getPropertyValue('--toast-height'));
|
|
34
34
|
return (sum + (Number.isFinite(natural) ? natural : card.offsetHeight));
|
|
35
35
|
}, 0) +
|
|
36
36
|
Math.max(0, cards.length - 1) * gap;
|
|
37
|
-
const wasOverflow = viewport.hasAttribute(
|
|
37
|
+
const wasOverflow = viewport.hasAttribute('data-stack-overflow');
|
|
38
38
|
const pinned = viewport.scrollTop >=
|
|
39
39
|
viewport.scrollHeight - viewport.clientHeight - 8;
|
|
40
40
|
const overflowing = height + 48 > window.innerHeight;
|
|
41
|
-
viewport.toggleAttribute(
|
|
41
|
+
viewport.toggleAttribute('data-stack-overflow', overflowing);
|
|
42
42
|
const value = `${height}px`;
|
|
43
|
-
if (viewport.style.getPropertyValue(
|
|
44
|
-
viewport.style.setProperty(
|
|
43
|
+
if (viewport.style.getPropertyValue('--toast-stack-height') !== value)
|
|
44
|
+
viewport.style.setProperty('--toast-stack-height', value);
|
|
45
45
|
if (overflowing && (!wasOverflow || pinned))
|
|
46
46
|
viewport.scrollTop = viewport.scrollHeight;
|
|
47
47
|
}
|
|
48
48
|
function refresh() {
|
|
49
49
|
resize.disconnect();
|
|
50
50
|
viewport
|
|
51
|
-
.querySelectorAll(
|
|
51
|
+
.querySelectorAll('.pui-toast:not([data-limited])')
|
|
52
52
|
.forEach((card) => resize.observe(card));
|
|
53
53
|
update();
|
|
54
54
|
}
|
|
55
55
|
const mutation = new MutationObserver((records) => {
|
|
56
56
|
// Natural-height variables are on roots; ignore our own viewport sizing writes.
|
|
57
57
|
if (records.some((record) => record.target !== viewport ||
|
|
58
|
-
record.attributeName ===
|
|
58
|
+
record.attributeName === 'data-expanded'))
|
|
59
59
|
refresh();
|
|
60
60
|
});
|
|
61
61
|
mutation.observe(viewport, {
|
|
62
62
|
childList: true,
|
|
63
63
|
subtree: true,
|
|
64
64
|
attributes: true,
|
|
65
|
-
attributeFilter: [
|
|
65
|
+
attributeFilter: ['data-limited', 'data-expanded', 'style'],
|
|
66
66
|
});
|
|
67
67
|
refresh();
|
|
68
|
-
window.addEventListener(
|
|
68
|
+
window.addEventListener('resize', update);
|
|
69
69
|
return () => {
|
|
70
|
-
window.removeEventListener(
|
|
71
|
-
viewport.removeAttribute(
|
|
70
|
+
window.removeEventListener('resize', update);
|
|
71
|
+
viewport.removeAttribute('data-stack-overflow');
|
|
72
72
|
resize.disconnect();
|
|
73
73
|
mutation.disconnect();
|
|
74
74
|
};
|
|
@@ -76,11 +76,11 @@ const Viewport = React.forwardRef(function Viewport({ layout = "stack", classNam
|
|
|
76
76
|
const scheme = useComponentSize();
|
|
77
77
|
return (_jsx(BaseToast.Viewport, { ...props, ref: (element) => {
|
|
78
78
|
viewportRef.current = element;
|
|
79
|
-
if (typeof ref ===
|
|
79
|
+
if (typeof ref === 'function')
|
|
80
80
|
ref(element);
|
|
81
81
|
else if (ref)
|
|
82
82
|
ref.current = element;
|
|
83
|
-
}, "data-layout": layout, "data-ui-size": scheme, className: skin(
|
|
83
|
+
}, "data-layout": layout, "data-ui-size": scheme, className: skin('pui-toast-viewport', className) }));
|
|
84
84
|
});
|
|
85
85
|
export const Toast = {
|
|
86
86
|
...BaseToast,
|
|
@@ -89,11 +89,11 @@ export const Toast = {
|
|
|
89
89
|
Close,
|
|
90
90
|
Action,
|
|
91
91
|
Viewport,
|
|
92
|
-
Content: part(BaseToast.Content,
|
|
93
|
-
Title: part(BaseToast.Title,
|
|
94
|
-
Description: part(BaseToast.Description,
|
|
95
|
-
Text: part(
|
|
96
|
-
Positioner: part(BaseToast.Positioner,
|
|
92
|
+
Content: part(BaseToast.Content, 'pui-toast-content'),
|
|
93
|
+
Title: part(BaseToast.Title, 'pui-toast-title'),
|
|
94
|
+
Description: part(BaseToast.Description, 'pui-toast-description'),
|
|
95
|
+
Text: part('div', 'pui-toast-text'),
|
|
96
|
+
Positioner: part(BaseToast.Positioner, 'pui-positioner'),
|
|
97
97
|
};
|
|
98
98
|
// Flat names for React Server Components, which cannot dot into client exports.
|
|
99
99
|
export const { Action: ToastAction, Arrow: ToastArrow, Close: ToastClose, Content: ToastContent, Description: ToastDescription, Portal: ToastPortal, Positioner: ToastPositioner, Provider: ToastProvider, Root: ToastRoot, Title: ToastTitle, Viewport: ToastViewport, Icon: ToastIcon, Text: ToastText, } = Toast;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as React from
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
/** Animate a scalar so the color-mix expression, not a browser's default
|
|
3
3
|
* color interpolation space, determines every intermediate status color. */
|
|
4
4
|
export declare function useToneTransition<Element extends HTMLElement>(tone: string, forwardedRef: React.ForwardedRef<Element>): React.RefObject<Element | null>;
|
package/dist/tone-transition.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import * as React from
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from 'react';
|
|
3
3
|
/** Animate a scalar so the color-mix expression, not a browser's default
|
|
4
4
|
* color interpolation space, determines every intermediate status color. */
|
|
5
5
|
export function useToneTransition(tone, forwardedRef) {
|
|
@@ -16,7 +16,7 @@ export function useToneTransition(tone, forwardedRef) {
|
|
|
16
16
|
animation.current?.cancel();
|
|
17
17
|
animation.current = null;
|
|
18
18
|
const element = ref.current;
|
|
19
|
-
element?.removeAttribute(
|
|
19
|
+
element?.removeAttribute('data-status-transition');
|
|
20
20
|
for (let i = 0; i < 3; i++) {
|
|
21
21
|
element?.style.removeProperty(`--pui-status-from-${i}`);
|
|
22
22
|
element?.style.removeProperty(`--pui-status-to-${i}`);
|
|
@@ -34,23 +34,23 @@ export function useToneTransition(tone, forwardedRef) {
|
|
|
34
34
|
paint.current = to;
|
|
35
35
|
if (!changed ||
|
|
36
36
|
!element.animate ||
|
|
37
|
-
matchMedia(
|
|
37
|
+
matchMedia('(prefers-reduced-motion: reduce), (forced-colors: active)')
|
|
38
38
|
.matches ||
|
|
39
|
-
document.documentElement.hasAttribute(
|
|
39
|
+
document.documentElement.hasAttribute('data-theme-switching'))
|
|
40
40
|
return;
|
|
41
41
|
const durationToken = getComputedStyle(element)
|
|
42
|
-
.getPropertyValue(
|
|
42
|
+
.getPropertyValue('--motion-feedback')
|
|
43
43
|
.trim();
|
|
44
|
-
const duration = parseFloat(durationToken) * (durationToken.endsWith(
|
|
44
|
+
const duration = parseFloat(durationToken) * (durationToken.endsWith('ms') ? 1 : 1000);
|
|
45
45
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
46
46
|
return;
|
|
47
47
|
from.forEach((value, i) => element.style.setProperty(`--pui-status-from-${i}`, value));
|
|
48
48
|
to.forEach((value, i) => element.style.setProperty(`--pui-status-to-${i}`, value));
|
|
49
|
-
element.setAttribute(
|
|
50
|
-
animation.current = element.animate([{
|
|
49
|
+
element.setAttribute('data-status-transition', '');
|
|
50
|
+
animation.current = element.animate([{ '--pui-status-progress': '0' }, { '--pui-status-progress': '1' }], {
|
|
51
51
|
duration,
|
|
52
|
-
easing: getComputedStyle(element).getPropertyValue(
|
|
53
|
-
|
|
52
|
+
easing: getComputedStyle(element).getPropertyValue('--ease-out').trim() ||
|
|
53
|
+
'ease-out',
|
|
54
54
|
});
|
|
55
55
|
animation.current.onfinish = clear;
|
|
56
56
|
}, [tone, clear]);
|
|
@@ -63,17 +63,17 @@ export function useToneTransition(tone, forwardedRef) {
|
|
|
63
63
|
const observer = new MutationObserver(refresh);
|
|
64
64
|
observer.observe(document.documentElement, {
|
|
65
65
|
attributes: true,
|
|
66
|
-
attributeFilter: [
|
|
66
|
+
attributeFilter: ['data-theme', 'data-theme-switching'],
|
|
67
67
|
});
|
|
68
68
|
const media = [
|
|
69
|
-
matchMedia(
|
|
70
|
-
matchMedia(
|
|
71
|
-
matchMedia(
|
|
69
|
+
matchMedia('(prefers-reduced-motion: reduce)'),
|
|
70
|
+
matchMedia('(forced-colors: active)'),
|
|
71
|
+
matchMedia('(prefers-color-scheme: light)'),
|
|
72
72
|
];
|
|
73
|
-
media.forEach((query) => query.addEventListener(
|
|
73
|
+
media.forEach((query) => query.addEventListener('change', refresh));
|
|
74
74
|
return () => {
|
|
75
75
|
observer.disconnect();
|
|
76
|
-
media.forEach((query) => query.removeEventListener(
|
|
76
|
+
media.forEach((query) => query.removeEventListener('change', refresh));
|
|
77
77
|
clear();
|
|
78
78
|
};
|
|
79
79
|
}, [clear]);
|
package/dist/tooltip.d.ts
CHANGED
package/dist/tooltip.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useComponentSize } from
|
|
4
|
-
import { Tooltip as BaseTooltip } from
|
|
3
|
+
import { useComponentSize } from './sizing.js';
|
|
4
|
+
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
|
5
5
|
export function TooltipProvider({ delayDuration = 150, skipDelayDuration = 300, children, }) {
|
|
6
6
|
return (_jsx(BaseTooltip.Provider, { delay: delayDuration, closeDelay: 0, timeout: skipDelayDuration, children: children }));
|
|
7
7
|
}
|
|
@@ -9,5 +9,5 @@ export function Tooltip({ label, enabled = true, children, }) {
|
|
|
9
9
|
const scheme = useComponentSize();
|
|
10
10
|
if (!enabled)
|
|
11
11
|
return children;
|
|
12
|
-
return (_jsxs(BaseTooltip.Root, { children: [_jsx(BaseTooltip.Trigger, { render: children }), _jsx(BaseTooltip.Portal, { children: _jsx(BaseTooltip.Positioner, { side:
|
|
12
|
+
return (_jsxs(BaseTooltip.Root, { children: [_jsx(BaseTooltip.Trigger, { render: children }), _jsx(BaseTooltip.Portal, { children: _jsx(BaseTooltip.Positioner, { side: 'right', sideOffset: 10, collisionPadding: 8, className: 'pui-positioner pui-tooltip-positioner', children: _jsx(BaseTooltip.Popup, { className: 'pui-tooltip', "data-ui-size": scheme, children: label }) }) })] }));
|
|
13
13
|
}
|
package/dist/tree.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import { type ComponentSize } from
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ComponentSize } from './sizing.js';
|
|
3
3
|
export type TreeNode = {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
@@ -15,13 +15,13 @@ export type TreeProps = {
|
|
|
15
15
|
selectedIds?: readonly string[];
|
|
16
16
|
defaultSelectedIds?: readonly string[];
|
|
17
17
|
onSelectedIdsChange?: (ids: string[]) => void;
|
|
18
|
-
selectionMode?:
|
|
18
|
+
selectionMode?: 'single' | 'multiple';
|
|
19
19
|
height?: number;
|
|
20
20
|
rowHeight?: number;
|
|
21
21
|
size?: ComponentSize;
|
|
22
22
|
virtualized?: boolean;
|
|
23
23
|
emptyLabel?: string;
|
|
24
|
-
dir?:
|
|
24
|
+
dir?: 'ltr' | 'rtl';
|
|
25
25
|
className?: string;
|
|
26
26
|
};
|
|
27
27
|
/** Loaded, immutable hierarchy. Remote loading, permission checks and mutations remain caller-owned. */
|
package/dist/tree.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createElement as _createElement } from "react";
|
|
4
|
-
import * as React from
|
|
5
|
-
import { useComponentSize } from
|
|
6
|
-
import { buildProxiedInstance, syncDataLoaderFeature, } from
|
|
7
|
-
import { useTree } from
|
|
8
|
-
import { defaultRangeExtractor, useVirtualizer, } from
|
|
9
|
-
import { ScrollArea } from
|
|
10
|
-
const normalize = (text) => text.normalize(
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { useComponentSize } from './sizing.js';
|
|
6
|
+
import { buildProxiedInstance, syncDataLoaderFeature, } from '@headless-tree/core';
|
|
7
|
+
import { useTree } from '@headless-tree/react';
|
|
8
|
+
import { defaultRangeExtractor, useVirtualizer, } from '@tanstack/react-virtual';
|
|
9
|
+
import { ScrollArea } from './scroll-area.js';
|
|
10
|
+
const normalize = (text) => text.normalize('NFKC').toLocaleLowerCase();
|
|
11
11
|
/** Loaded, immutable hierarchy. Remote loading, permission checks and mutations remain caller-owned. */
|
|
12
|
-
export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExpandedIdsChange, selectedIds, defaultSelectedIds = [], onSelectedIdsChange, selectionMode =
|
|
12
|
+
export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExpandedIdsChange, selectedIds, defaultSelectedIds = [], onSelectedIdsChange, selectionMode = 'single', height = 320, rowHeight: explicitRowHeight, size, virtualized = true, emptyLabel = 'No items.', dir, className, }) {
|
|
13
13
|
const scheme = useComponentSize(size);
|
|
14
14
|
const touchFloor = React.useSyncExternalStore((listener) => {
|
|
15
|
-
const query = matchMedia(
|
|
16
|
-
query.addEventListener(
|
|
17
|
-
return () => query.removeEventListener(
|
|
18
|
-
}, () => matchMedia(
|
|
15
|
+
const query = matchMedia('(max-width: 760px), (pointer: coarse)');
|
|
16
|
+
query.addEventListener('change', listener);
|
|
17
|
+
return () => query.removeEventListener('change', listener);
|
|
18
|
+
}, () => matchMedia('(max-width: 760px), (pointer: coarse)').matches, () => false);
|
|
19
19
|
const rowHeight = explicitRowHeight ??
|
|
20
|
-
Math.max(touchFloor && scheme ? 44 : 0, scheme ===
|
|
20
|
+
Math.max(touchFloor && scheme ? 44 : 0, scheme === 'small' ? 36 : scheme === 'large' ? 52 : 44);
|
|
21
21
|
const rootId = React.useId();
|
|
22
22
|
const model = React.useMemo(() => {
|
|
23
23
|
const data = new Map();
|
|
@@ -42,7 +42,7 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
42
42
|
const expanded = React.useMemo(() => [...new Set(expandedIds ?? localExpanded)].filter((id) => model.data.get(id)?.children?.length), [expandedIds, localExpanded, model]);
|
|
43
43
|
const selected = React.useMemo(() => {
|
|
44
44
|
const ids = [...new Set(selectedIds ?? localSelected)].filter((id) => model.data.has(id) && !model.data.get(id)?.disabled);
|
|
45
|
-
return selectionMode ===
|
|
45
|
+
return selectionMode === 'single' ? ids.slice(-1) : ids;
|
|
46
46
|
}, [selectedIds, localSelected, model, selectionMode]);
|
|
47
47
|
const visibleIds = React.useMemo(() => {
|
|
48
48
|
const result = [], open = new Set(expanded);
|
|
@@ -66,7 +66,7 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
66
66
|
const viewport = React.useRef(null), container = React.useRef(null);
|
|
67
67
|
const virtualizerRef = React.useRef(null);
|
|
68
68
|
const pendingFocus = React.useRef(null), focusWithin = React.useRef(false), anchor = React.useRef(null);
|
|
69
|
-
const typeahead = React.useRef({ text:
|
|
69
|
+
const typeahead = React.useRef({ text: '', time: 0 });
|
|
70
70
|
function changeExpanded(ids) {
|
|
71
71
|
const next = [...new Set(ids)];
|
|
72
72
|
if (expandedIds === undefined)
|
|
@@ -75,7 +75,7 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
75
75
|
}
|
|
76
76
|
function changeSelected(ids) {
|
|
77
77
|
const allowed = [...new Set(ids)].filter((id) => model.data.has(id) && !model.data.get(id)?.disabled);
|
|
78
|
-
const next = selectionMode ===
|
|
78
|
+
const next = selectionMode === 'single' ? allowed.slice(-1) : allowed;
|
|
79
79
|
if (selectedIds === undefined)
|
|
80
80
|
setLocalSelected(next);
|
|
81
81
|
onSelectedIdsChange?.(next);
|
|
@@ -91,14 +91,14 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
91
91
|
getChildren: (id) => model.data.get(id)?.children?.map((node) => node.id) ?? [],
|
|
92
92
|
},
|
|
93
93
|
state: { expandedItems: expanded, focusedItem: focused },
|
|
94
|
-
setExpandedItems: (update) => changeExpanded(typeof update ===
|
|
95
|
-
setFocusedItem: (update) => setLocalFocus(typeof update ===
|
|
94
|
+
setExpandedItems: (update) => changeExpanded(typeof update === 'function' ? update(expanded) : update),
|
|
95
|
+
setFocusedItem: (update) => setLocalFocus(typeof update === 'function' ? update(focused) : update),
|
|
96
96
|
scrollToItem: (item) => virtualizerRef.current?.scrollToIndex(item.getItemMeta().index, {
|
|
97
|
-
align:
|
|
97
|
+
align: 'auto',
|
|
98
98
|
}),
|
|
99
99
|
});
|
|
100
100
|
const items = tree.getItems();
|
|
101
|
-
const focusedIndex = visibleIds.indexOf(focused ??
|
|
101
|
+
const focusedIndex = visibleIds.indexOf(focused ?? '');
|
|
102
102
|
const virtualizer = useVirtualizer({
|
|
103
103
|
count: items.length,
|
|
104
104
|
getScrollElement: () => viewport.current,
|
|
@@ -121,7 +121,7 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
121
121
|
pendingFocus.current = item.getId();
|
|
122
122
|
item.setFocused();
|
|
123
123
|
if (virtualized)
|
|
124
|
-
virtualizer.scrollToIndex(item.getItemMeta().index, { align:
|
|
124
|
+
virtualizer.scrollToIndex(item.getItemMeta().index, { align: 'auto' });
|
|
125
125
|
else if (viewport.current) {
|
|
126
126
|
const top = item.getItemMeta().index * rowHeight;
|
|
127
127
|
if (top < viewport.current.scrollTop)
|
|
@@ -157,7 +157,7 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
157
157
|
const id = item.getId();
|
|
158
158
|
if (item.getItemData().disabled)
|
|
159
159
|
return;
|
|
160
|
-
if (selectionMode ===
|
|
160
|
+
if (selectionMode === 'single')
|
|
161
161
|
changeSelected([id]);
|
|
162
162
|
else if (range && anchor.current && visibleIds.includes(anchor.current)) {
|
|
163
163
|
const a = visibleIds.indexOf(anchor.current), b = visibleIds.indexOf(id);
|
|
@@ -173,9 +173,9 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
173
173
|
function keydown(event) {
|
|
174
174
|
if (event.defaultPrevented ||
|
|
175
175
|
event.nativeEvent.isComposing ||
|
|
176
|
-
event.target.closest(
|
|
176
|
+
event.target.closest('button'))
|
|
177
177
|
return;
|
|
178
|
-
const targetId = event.target.closest(
|
|
178
|
+
const targetId = event.target.closest('[data-tree-id]')?.dataset.treeId;
|
|
179
179
|
const item = targetId
|
|
180
180
|
? tree.getItemInstance(targetId)
|
|
181
181
|
: focused
|
|
@@ -184,20 +184,20 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
184
184
|
if (!item)
|
|
185
185
|
return;
|
|
186
186
|
if ([
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
'ArrowDown',
|
|
188
|
+
'ArrowUp',
|
|
189
|
+
'ArrowLeft',
|
|
190
|
+
'ArrowRight',
|
|
191
|
+
'Home',
|
|
192
|
+
'End',
|
|
193
193
|
].includes(event.key))
|
|
194
|
-
typeahead.current = { text:
|
|
194
|
+
typeahead.current = { text: '', time: 0 };
|
|
195
195
|
const index = item.getItemMeta().index;
|
|
196
|
-
const rtl = getComputedStyle(event.currentTarget).direction ===
|
|
197
|
-
const forward = rtl ?
|
|
196
|
+
const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
|
|
197
|
+
const forward = rtl ? 'ArrowLeft' : 'ArrowRight', backward = rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
198
198
|
if ((event.ctrlKey || event.metaKey) &&
|
|
199
|
-
event.key.toLowerCase() ===
|
|
200
|
-
selectionMode ===
|
|
199
|
+
event.key.toLowerCase() === 'a' &&
|
|
200
|
+
selectionMode === 'multiple') {
|
|
201
201
|
event.preventDefault();
|
|
202
202
|
changeSelected(visibleIds);
|
|
203
203
|
return;
|
|
@@ -205,26 +205,26 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
205
205
|
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
206
206
|
return;
|
|
207
207
|
if ([
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
'ArrowDown',
|
|
209
|
+
'ArrowUp',
|
|
210
|
+
'Home',
|
|
211
|
+
'End',
|
|
212
212
|
forward,
|
|
213
213
|
backward,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
'Enter',
|
|
215
|
+
' ',
|
|
216
|
+
'*',
|
|
217
217
|
].includes(event.key))
|
|
218
218
|
event.preventDefault();
|
|
219
|
-
if (event.key ===
|
|
220
|
-
const next = items[index + (event.key ===
|
|
219
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
220
|
+
const next = items[index + (event.key === 'ArrowDown' ? 1 : -1)];
|
|
221
221
|
focusItem(next);
|
|
222
|
-
if (event.shiftKey && selectionMode ===
|
|
222
|
+
if (event.shiftKey && selectionMode === 'multiple' && next)
|
|
223
223
|
select(next, true);
|
|
224
224
|
}
|
|
225
|
-
else if (event.key ===
|
|
225
|
+
else if (event.key === 'Home')
|
|
226
226
|
focusItem(items[0]);
|
|
227
|
-
else if (event.key ===
|
|
227
|
+
else if (event.key === 'End')
|
|
228
228
|
focusItem(items.at(-1));
|
|
229
229
|
else if (event.key === forward &&
|
|
230
230
|
!item.getItemData().disabled &&
|
|
@@ -240,9 +240,9 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
240
240
|
else if (item.getItemMeta().level > 0)
|
|
241
241
|
focusItem(item.getParent());
|
|
242
242
|
}
|
|
243
|
-
else if (event.key ===
|
|
243
|
+
else if (event.key === 'Enter' || event.key === ' ')
|
|
244
244
|
select(item, event.shiftKey);
|
|
245
|
-
else if (event.key ===
|
|
245
|
+
else if (event.key === '*')
|
|
246
246
|
changeExpanded([
|
|
247
247
|
...expanded,
|
|
248
248
|
...items
|
|
@@ -252,7 +252,7 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
252
252
|
.map((sibling) => sibling.getId()),
|
|
253
253
|
]);
|
|
254
254
|
else if (event.key.length === 1) {
|
|
255
|
-
const now = performance.now(), char = normalize(event.key), previous = now - typeahead.current.time < 700 ? typeahead.current.text :
|
|
255
|
+
const now = performance.now(), char = normalize(event.key), previous = now - typeahead.current.time < 700 ? typeahead.current.text : '';
|
|
256
256
|
const prefix = previous === char ? char : previous + char;
|
|
257
257
|
typeahead.current = { text: prefix, time: now };
|
|
258
258
|
for (let offset = 1; offset <= items.length; offset++) {
|
|
@@ -271,16 +271,16 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
271
271
|
.getVirtualItems()
|
|
272
272
|
.map((row) => ({ index: row.index, start: row.start }))
|
|
273
273
|
: items.map((_, index) => ({ index, start: index * rowHeight }));
|
|
274
|
-
return (_jsx(ScrollArea, { orientation:
|
|
274
|
+
return (_jsx(ScrollArea, { orientation: 'vertical', label: `${label} scroll area`, tabIndex: -1, ref: viewport, scrollChain: 'contain', rootClassName: ['pui-tree-scroll', className].filter(Boolean).join(' '), dir: dir, rootStyle: { blockSize: height, direction: dir }, className: 'pui-tree-viewport', children: items.length ? (_jsx("div", { ...containerProps, ref: (element) => {
|
|
275
275
|
container.current = element;
|
|
276
276
|
containerProps.ref(element);
|
|
277
|
-
}, dir: dir, className:
|
|
277
|
+
}, dir: dir, className: 'pui-tree', "data-ui-size": scheme, "aria-multiselectable": selectionMode === 'multiple' || undefined, onKeyDown: keydown, onFocusCapture: () => {
|
|
278
278
|
focusWithin.current = true;
|
|
279
279
|
}, onBlurCapture: (event) => {
|
|
280
280
|
if (event.relatedTarget &&
|
|
281
281
|
!event.currentTarget.contains(event.relatedTarget)) {
|
|
282
282
|
focusWithin.current = false;
|
|
283
|
-
typeahead.current.text =
|
|
283
|
+
typeahead.current.text = '';
|
|
284
284
|
}
|
|
285
285
|
}, style: {
|
|
286
286
|
blockSize: virtualized
|
|
@@ -288,24 +288,24 @@ export function Tree({ nodes, label, expandedIds, defaultExpandedIds = [], onExp
|
|
|
288
288
|
: items.length * rowHeight,
|
|
289
289
|
}, children: rows.map(({ index, start }) => {
|
|
290
290
|
const item = items[index], node = item.getItemData(), props = item.getProps();
|
|
291
|
-
return (_createElement("div", { ...props, key: item.getId(), className:
|
|
291
|
+
return (_createElement("div", { ...props, key: item.getId(), className: 'pui-tree-item', "data-tree-id": node.id, "aria-disabled": node.disabled || undefined, "aria-selected": node.disabled ? undefined : selected.includes(node.id), title: node.label, onFocus: () => item.setFocused(), onClick: (event) => {
|
|
292
292
|
focusItem(item);
|
|
293
293
|
select(item, event.shiftKey);
|
|
294
294
|
}, style: {
|
|
295
|
-
position:
|
|
295
|
+
position: 'absolute',
|
|
296
296
|
insetInline: 0,
|
|
297
297
|
top: start,
|
|
298
298
|
blockSize: rowHeight,
|
|
299
299
|
paddingInlineStart: `calc(var(--space-2) + ${Math.min(item.getItemMeta().level, 8)} * var(--space-4))`,
|
|
300
300
|
} },
|
|
301
|
-
item.isFolder() ? (_jsx("button", { type:
|
|
301
|
+
item.isFolder() ? (_jsx("button", { type: 'button', className: 'pui-tree-toggle', tabIndex: -1, disabled: node.disabled, "aria-label": `${item.isExpanded() ? 'Collapse' : 'Expand'} ${node.label}`, onClick: (event) => {
|
|
302
302
|
event.stopPropagation();
|
|
303
303
|
focusItem(item);
|
|
304
304
|
if (item.isExpanded())
|
|
305
305
|
item.collapse();
|
|
306
306
|
else
|
|
307
307
|
item.expand();
|
|
308
|
-
}, children: _jsx("svg", { "aria-hidden":
|
|
309
|
-
_jsx("span", { className:
|
|
310
|
-
}) })) : (_jsx("div", { role:
|
|
308
|
+
}, children: _jsx("svg", { "aria-hidden": 'true', width: '14', height: '14', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '1.6', "data-expanded": item.isExpanded() || undefined, children: _jsx("path", { d: 'm9 5 7 7-7 7' }) }) })) : (_jsx("span", { className: 'pui-tree-leaf', "aria-hidden": 'true' })),
|
|
309
|
+
_jsx("span", { className: 'pui-tree-label', children: node.label })));
|
|
310
|
+
}) })) : (_jsx("div", { role: 'status', className: 'pui-tree-empty', children: emptyLabel })) }));
|
|
311
311
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surflane/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Surflane React components built on Base UI, with data, chart, code and Markdown components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design-system",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"shiki": "4.5.0"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
|
-
"@surflane/design-tokens": "^0.
|
|
104
|
+
"@surflane/design-tokens": "^0.3.0",
|
|
105
105
|
"react": "^19.0.0",
|
|
106
106
|
"react-dom": "^19.0.0"
|
|
107
107
|
},
|