@rtzll/reflow 0.3.0 → 0.5.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/dist/basic.js +3 -8
- package/dist/breadcrumb.css +10 -0
- package/dist/breadcrumb.js +38 -40
- package/dist/compositions.css +3 -3
- package/dist/drawer.css +240 -0
- package/dist/drawer.d.ts +45 -0
- package/dist/drawer.js +228 -0
- package/dist/foundations.css +27 -14
- package/dist/hover-card.d.ts +32 -0
- package/dist/hover-card.js +119 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/overlays.js +14 -2
- package/dist/popover.css +69 -0
- package/dist/popover.d.ts +38 -0
- package/dist/popover.js +57 -0
- package/dist/segmented-control.css +172 -0
- package/dist/segmented-control.d.ts +34 -0
- package/dist/segmented-control.js +38 -0
- package/dist/sidebar.css +14 -57
- package/dist/sidebar.js +9 -13
- package/dist/status-icon.d.ts +3 -0
- package/dist/status-icon.js +7 -0
- package/dist/styles.css +4 -0
- package/dist/systems.js +5 -1
- package/dist/toaster.css +268 -0
- package/dist/toaster.d.ts +79 -0
- package/dist/toaster.js +418 -0
- package/package.json +1 -1
package/dist/basic.js
CHANGED
|
@@ -4,6 +4,7 @@ import { on, ref } from 'remix/component';
|
|
|
4
4
|
import { jsx } from 'remix/component/jsx-runtime';
|
|
5
5
|
import { fluidHover } from './interactive.js';
|
|
6
6
|
import { Tooltip } from './overlays.js';
|
|
7
|
+
import { statusIcon } from './status-icon.js';
|
|
7
8
|
export function Button(handle) {
|
|
8
9
|
return () => {
|
|
9
10
|
const props = handle.props;
|
|
@@ -13,7 +14,7 @@ export function Button(handle) {
|
|
|
13
14
|
const iconOnly = props.size === 'icon' || props.size === 'icon-compact';
|
|
14
15
|
const leading = iconOnly ? undefined : props.leadingIcon;
|
|
15
16
|
const trailing = iconOnly ? undefined : props.trailingIcon;
|
|
16
|
-
const content = (_jsxs(_Fragment, { children: [_jsxs("span", { class: "rf-button-content", children: [leading && (_jsx("span", { class: "rf-button-icon", "aria-hidden": "true", children: leading })), _jsx("span", { class: "rf-button-label", children: props.children }), trailing && (_jsx("span", { class: "rf-button-icon", "aria-hidden": "true", children: trailing }))] }), props.loading && (_jsx("svg", { class: "rf-button-spinner", viewBox: "0 0
|
|
17
|
+
const content = (_jsxs(_Fragment, { children: [_jsxs("span", { class: "rf-button-content", children: [leading && (_jsx("span", { class: "rf-button-icon", "aria-hidden": "true", children: leading })), _jsx("span", { class: "rf-button-label", children: props.children }), trailing && (_jsx("span", { class: "rf-button-icon", "aria-hidden": "true", children: trailing }))] }), props.loading && (_jsx("svg", { class: "rf-button-spinner", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("g", { children: _jsx("circle", { cx: "8", cy: "8", r: "6.25", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", pathLength: "100", transform: "rotate(-90 8 8)" }) }) }))] }));
|
|
17
18
|
const appearance = {
|
|
18
19
|
class: classNames,
|
|
19
20
|
'data-slot': 'button',
|
|
@@ -308,7 +309,7 @@ export function Banner(handle) {
|
|
|
308
309
|
if (visible)
|
|
309
310
|
handOff = false;
|
|
310
311
|
wasVisible = visible;
|
|
311
|
-
return (_jsx("div", { class: "rf-banner-collapse", "data-open": visible ? 'true' : 'false', "data-variant": variant, mix: rootRef, children: _jsx("div", { class: "rf-banner-overflow", inert: !visible, "aria-hidden": !visible || undefined, children: _jsxs("div", { ...props, class: classList('rf-banner', classProp, className), "data-slot": "banner", "data-size": size, "data-status": status, "data-contrast": contrast, role: role ?? (status === 'error' || status === 'warning' ? 'alert' : 'status'), children: [_jsx("span", { class: "rf-banner-icon", "aria-hidden": "true", children: icon ??
|
|
312
|
+
return (_jsx("div", { class: "rf-banner-collapse", "data-open": visible ? 'true' : 'false', "data-variant": variant, mix: rootRef, children: _jsx("div", { class: "rf-banner-overflow", inert: !visible, "aria-hidden": !visible || undefined, children: _jsxs("div", { ...props, class: classList('rf-banner', classProp, className), "data-slot": "banner", "data-size": size, "data-status": status, "data-contrast": contrast, role: role ?? (status === 'error' || status === 'warning' ? 'alert' : 'status'), children: [_jsx("span", { class: "rf-banner-icon", "aria-hidden": "true", children: icon ?? statusIcon(status) }), _jsx("div", { class: "rf-banner-content", children: children }), dismissible && (_jsx("button", { type: "button", class: "rf-banner-dismiss", "aria-label": dismissLabel, mix: on('click', (event) => {
|
|
312
313
|
handOff = event.currentTarget === document.activeElement;
|
|
313
314
|
onDismiss?.();
|
|
314
315
|
if (open === undefined) {
|
|
@@ -318,12 +319,6 @@ export function Banner(handle) {
|
|
|
318
319
|
}), children: _jsx(Icon, { name: "x" }) }))] }) }) }));
|
|
319
320
|
};
|
|
320
321
|
}
|
|
321
|
-
function bannerIcon(status) {
|
|
322
|
-
if (status === 'default') {
|
|
323
|
-
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 16v-4m0-4h.01" })] }));
|
|
324
|
-
}
|
|
325
|
-
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", children: [status === 'warning' ? (_jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z", fill: "currentColor", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" })) : (_jsx("circle", { cx: "12", cy: "12", r: "10.5", fill: "currentColor" })), _jsx("g", { stroke: "var(--rf-bg)", "stroke-width": "2.25", "stroke-linecap": "round", "stroke-linejoin": "round", children: status === 'success' ? (_jsx("path", { d: "m8.5 12.25 2.5 2.5 4.5-5" })) : status === 'warning' ? (_jsx("path", { d: "M12 9.5v3.5M12 17h.01" })) : status === 'error' ? (_jsx("path", { d: "M12 7.5v5M12 16.5h.01" })) : (_jsx("path", { d: "M12 16.5v-5M12 7.5h.01" })) })] }));
|
|
326
|
-
}
|
|
327
322
|
export function BannerTitle(handle) {
|
|
328
323
|
return () => {
|
|
329
324
|
const { class: classProp, className, ...props } = handle.props;
|
package/dist/breadcrumb.css
CHANGED
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
font-size: var(--rf-text-body);
|
|
34
34
|
line-height: var(--rf-leading-body);
|
|
35
35
|
}
|
|
36
|
+
.rf-breadcrumb-measure {
|
|
37
|
+
position: absolute;
|
|
38
|
+
inline-size: 0;
|
|
39
|
+
block-size: 0;
|
|
40
|
+
overflow: clip;
|
|
41
|
+
visibility: hidden;
|
|
42
|
+
}
|
|
43
|
+
.rf-breadcrumb-measure > .rf-breadcrumb-list {
|
|
44
|
+
inline-size: max-content;
|
|
45
|
+
}
|
|
36
46
|
|
|
37
47
|
/* Steps keep their width; only the current page gives way and truncates. */
|
|
38
48
|
.rf-breadcrumb-item {
|
package/dist/breadcrumb.js
CHANGED
|
@@ -11,14 +11,12 @@ const fallbackEllipsisWidth = 44;
|
|
|
11
11
|
*/
|
|
12
12
|
export function Breadcrumb(handle) {
|
|
13
13
|
let list = null;
|
|
14
|
-
let hidden
|
|
14
|
+
let hidden;
|
|
15
15
|
// Natural widths per step, remembered so a hidden step returns as soon as it fits.
|
|
16
16
|
let widths = [];
|
|
17
17
|
let ellipsisWidth = 0;
|
|
18
|
-
// Steps whose label is cut, by index, read as the pointer or keyboard focus
|
|
19
|
-
// arrives; and the step whose menu is open, whose full name stays hidden.
|
|
18
|
+
// Steps whose label is cut, by index, read as the pointer or keyboard focus arrives.
|
|
20
19
|
let cut = new Set();
|
|
21
|
-
let menuOpen;
|
|
22
20
|
const listRef = ref((node, signal) => {
|
|
23
21
|
if (!(node instanceof HTMLOListElement))
|
|
24
22
|
return;
|
|
@@ -26,18 +24,8 @@ export function Breadcrumb(handle) {
|
|
|
26
24
|
const observer = new ResizeObserver(() => measure());
|
|
27
25
|
if (node.parentElement)
|
|
28
26
|
observer.observe(node.parentElement);
|
|
29
|
-
const menus = new MutationObserver(() => {
|
|
30
|
-
const open = node.querySelector('[data-rf-step][aria-expanded="true"]');
|
|
31
|
-
const next = open ? Number(open.dataset.rfStep) : undefined;
|
|
32
|
-
if (next === menuOpen)
|
|
33
|
-
return;
|
|
34
|
-
menuOpen = next;
|
|
35
|
-
void handle.update();
|
|
36
|
-
});
|
|
37
|
-
menus.observe(node, { subtree: true, attributeFilter: ['aria-expanded'] });
|
|
38
27
|
signal.addEventListener('abort', () => {
|
|
39
28
|
observer.disconnect();
|
|
40
|
-
menus.disconnect();
|
|
41
29
|
if (list === node)
|
|
42
30
|
list = null;
|
|
43
31
|
});
|
|
@@ -54,34 +42,39 @@ export function Breadcrumb(handle) {
|
|
|
54
42
|
}
|
|
55
43
|
function measure() {
|
|
56
44
|
const nav = list?.parentElement;
|
|
57
|
-
if (!list || !nav)
|
|
45
|
+
if (!list || !nav || !handle.props.collapse)
|
|
58
46
|
return;
|
|
59
47
|
const count = handle.props.items.length;
|
|
48
|
+
const maxHidden = Math.max(0, count - 3);
|
|
60
49
|
const items = Array.from(list.children);
|
|
61
50
|
const steps = items.filter((item) => !item.hasAttribute('data-ellipsis'));
|
|
62
51
|
const ellipsis = items.find((item) => item.hasAttribute('data-ellipsis'));
|
|
63
52
|
if (ellipsis)
|
|
64
53
|
ellipsisWidth = naturalWidth(ellipsis);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
const measuring = nav.querySelector('.rf-breadcrumb-measure > ol');
|
|
55
|
+
if (measuring) {
|
|
56
|
+
widths = Array.from(measuring.children, (item) => item.getBoundingClientRect().width);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// The rendered steps are 0, then hidden + 1 onward.
|
|
60
|
+
const hiddenCount = Math.min(hidden ?? 0, maxHidden);
|
|
61
|
+
steps.forEach((item, i) => {
|
|
62
|
+
const index = i === 0 ? i : i + hiddenCount;
|
|
63
|
+
if (index < count)
|
|
64
|
+
widths[index] = naturalWidth(item);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
71
67
|
let next = 0;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
shown -= width(1 + next);
|
|
83
|
-
next++;
|
|
84
|
-
}
|
|
68
|
+
const available = nav.clientWidth;
|
|
69
|
+
const width = (i) => widths[i] ?? 0;
|
|
70
|
+
let total = 0;
|
|
71
|
+
for (let i = 0; i < count; i++)
|
|
72
|
+
total += width(i);
|
|
73
|
+
if (total > available + 0.5) {
|
|
74
|
+
let shown = total + (ellipsisWidth || fallbackEllipsisWidth);
|
|
75
|
+
while (next < maxHidden && shown > available + 0.5) {
|
|
76
|
+
shown -= width(1 + next);
|
|
77
|
+
next++;
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
if (next !== hidden) {
|
|
@@ -110,17 +103,19 @@ export function Breadcrumb(handle) {
|
|
|
110
103
|
return () => {
|
|
111
104
|
const { items, label = 'Breadcrumb', size, separator, collapse, openOnHover } = handle.props;
|
|
112
105
|
widths.length = items.length;
|
|
113
|
-
const
|
|
106
|
+
const maxHidden = Math.max(0, items.length - 3);
|
|
107
|
+
if (!collapse)
|
|
108
|
+
hidden = undefined;
|
|
109
|
+
// Start with the shortest usable trail, then restore the steps that fit.
|
|
110
|
+
const hide = collapse ? Math.min(hidden ?? maxHidden, maxHidden) : 0;
|
|
114
111
|
// Every render: a label change can change a step's width.
|
|
115
112
|
if (collapse)
|
|
116
113
|
handle.queueTask(() => measure());
|
|
117
114
|
const trailing = separator === null ? null : (_jsx("span", { class: "rf-breadcrumb-separator", "aria-hidden": "true", children: separator ?? _jsx(Icon, { name: "chevron-right" }) }));
|
|
118
115
|
const content = (item) => (_jsxs(_Fragment, { children: [item.icon, _jsx("span", { class: "rf-breadcrumb-label", children: item.label })] }));
|
|
119
116
|
// The full name of a cut step, in the library tooltip under it, as in
|
|
120
|
-
// the reference.
|
|
121
|
-
|
|
122
|
-
// described by the tooltip only while its name is cut.
|
|
123
|
-
const withFullName = (item, index, render) => (_jsx(Tooltip, { content: item.label, side: "bottom", open: cut.has(index) && menuOpen !== index ? undefined : false, renderTrigger: (props) => render(cut.has(index) ? props : { 'aria-describedby': '', mix: props.mix }) }));
|
|
117
|
+
// the reference. The step is described by the tooltip only while its name is cut.
|
|
118
|
+
const withFullName = (item, index, render) => (_jsx(Tooltip, { content: item.label, side: "bottom", open: cut.has(index) ? undefined : false, renderTrigger: (props) => render(cut.has(index) ? props : { 'aria-describedby': '', mix: props.mix }) }));
|
|
124
119
|
const described = (trigger) => trigger['aria-describedby'] || undefined;
|
|
125
120
|
const step = (item, index) => {
|
|
126
121
|
const last = index === items.length - 1;
|
|
@@ -159,6 +154,9 @@ export function Breadcrumb(handle) {
|
|
|
159
154
|
fluidHover({ axis: 'x', focusSelector: ':has(:focus-visible)' }),
|
|
160
155
|
on('pointerover', checkCut),
|
|
161
156
|
on('focusin', checkCut),
|
|
162
|
-
], children: [_jsx("span", { class: "rf-highlight rf-breadcrumb-hover", "aria-hidden": "true", "data-rf-hover-layer": true }), _jsx("span", { class: "rf-highlight rf-focus-ring", "aria-hidden": "true", "data-rf-focus-layer": true }), _jsx("ol", { class: "rf-breadcrumb-list", mix: listRef, children: rows })
|
|
157
|
+
], children: [_jsx("span", { class: "rf-highlight rf-breadcrumb-hover", "aria-hidden": "true", "data-rf-hover-layer": true }), _jsx("span", { class: "rf-highlight rf-focus-ring", "aria-hidden": "true", "data-rf-focus-layer": true }), _jsx("ol", { class: "rf-breadcrumb-list", mix: listRef, children: rows }), collapse && hidden === undefined && maxHidden > 0 && (_jsx("div", { class: "rf-breadcrumb-measure", "aria-hidden": "true", inert: true, children: _jsx("ol", { class: "rf-breadcrumb-list", children: items.map((item, index) => {
|
|
158
|
+
const last = index === items.length - 1;
|
|
159
|
+
return (_jsx("li", { class: "rf-breadcrumb-item", children: _jsxs("span", { class: last ? 'rf-breadcrumb-crumb rf-breadcrumb-page' : 'rf-breadcrumb-crumb', children: [content(item), !last && trailing] }) }, index));
|
|
160
|
+
}) }) }))] }));
|
|
163
161
|
};
|
|
164
162
|
}
|
package/dist/compositions.css
CHANGED
|
@@ -470,7 +470,7 @@
|
|
|
470
470
|
}
|
|
471
471
|
.rf-queue {
|
|
472
472
|
--rf-queue-row: 44px;
|
|
473
|
-
--rf-queue-gap:
|
|
473
|
+
--rf-queue-gap: var(--rf-stack-gap);
|
|
474
474
|
position: relative;
|
|
475
475
|
list-style: none;
|
|
476
476
|
padding: 0;
|
|
@@ -502,7 +502,7 @@
|
|
|
502
502
|
padding: 0 8px;
|
|
503
503
|
cursor: grab;
|
|
504
504
|
z-index: calc(3 - var(--rf-queue-depth));
|
|
505
|
-
scale: calc(1 - var(--rf-queue-depth) *
|
|
505
|
+
scale: calc(1 - var(--rf-queue-depth) * var(--rf-stack-scale-step)) 1;
|
|
506
506
|
transition:
|
|
507
507
|
margin-block-start var(--rf-spring-moderate),
|
|
508
508
|
scale var(--rf-spring-moderate),
|
|
@@ -511,7 +511,7 @@
|
|
|
511
511
|
.rf-queue > li + li {
|
|
512
512
|
margin-block-start: var(
|
|
513
513
|
--rf-queue-spacing,
|
|
514
|
-
calc(var(--rf-queue-peek,
|
|
514
|
+
calc(var(--rf-queue-peek, var(--rf-stack-peek)) - var(--rf-queue-row))
|
|
515
515
|
);
|
|
516
516
|
}
|
|
517
517
|
.rf-queue > li:nth-child(n + 4) {
|
package/dist/drawer.css
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/* Drawer: a modal panel attached to one edge of the viewport. It slides in
|
|
2
|
+
on the moderate spring, which has no bounce, so the panel never lifts off
|
|
3
|
+
its edge, and leaves on Dialog's exit tween. While dragged it follows
|
|
4
|
+
--rf-drawer-drag, the distance toward its edge, and the backdrop fades
|
|
5
|
+
with --rf-drawer-progress. A strip of panel color past the edge covers
|
|
6
|
+
the gap when a drag pulls it the other way. */
|
|
7
|
+
.rf-drawer {
|
|
8
|
+
--rf-drawer-bg: var(--rf-surface-5);
|
|
9
|
+
--rf-drawer-drag: 0px;
|
|
10
|
+
--rf-drawer-closed: 0 100%;
|
|
11
|
+
--rf-drawer-tail: 0 32px 0 0 var(--rf-drawer-bg);
|
|
12
|
+
position: fixed;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
inset: auto 0 0;
|
|
15
|
+
inline-size: 100%;
|
|
16
|
+
max-inline-size: none;
|
|
17
|
+
block-size: auto;
|
|
18
|
+
max-block-size: calc(100dvh - 48px);
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 24px;
|
|
21
|
+
border: 0;
|
|
22
|
+
border-radius: var(--rf-panel-radius) var(--rf-panel-radius) 0 0;
|
|
23
|
+
outline: none;
|
|
24
|
+
overflow: auto;
|
|
25
|
+
overscroll-behavior: contain;
|
|
26
|
+
background: var(--rf-drawer-bg);
|
|
27
|
+
color: var(--rf-fg);
|
|
28
|
+
box-shadow:
|
|
29
|
+
var(--rf-drawer-tail),
|
|
30
|
+
0 0 0 1px light-dark(rgb(0 0 0 / 6%), rgb(255 255 255 / 6%)),
|
|
31
|
+
0 0 24px light-dark(rgb(0 0 0 / 10%), rgb(0 0 0 / 30%));
|
|
32
|
+
font: var(--rf-text-body) / var(--rf-leading-body) var(--rf-font);
|
|
33
|
+
translate: var(--rf-drawer-closed);
|
|
34
|
+
transition:
|
|
35
|
+
translate var(--rf-exit-slow),
|
|
36
|
+
overlay 160ms allow-discrete,
|
|
37
|
+
display 160ms allow-discrete;
|
|
38
|
+
}
|
|
39
|
+
.rf-drawer[open] {
|
|
40
|
+
translate: 0 var(--rf-drawer-drag);
|
|
41
|
+
transition:
|
|
42
|
+
translate var(--rf-spring-moderate),
|
|
43
|
+
overlay 160ms allow-discrete,
|
|
44
|
+
display 160ms allow-discrete;
|
|
45
|
+
}
|
|
46
|
+
@starting-style {
|
|
47
|
+
.rf-drawer[open] {
|
|
48
|
+
translate: var(--rf-drawer-closed);
|
|
49
|
+
}
|
|
50
|
+
.rf-drawer[open]::backdrop {
|
|
51
|
+
opacity: 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Top and bottom drawers keep their content in a readable column. */
|
|
56
|
+
.rf-drawer[data-side='bottom'],
|
|
57
|
+
.rf-drawer[data-side='top'] {
|
|
58
|
+
padding-inline: max(24px, calc((100% - 560px) / 2));
|
|
59
|
+
}
|
|
60
|
+
.rf-drawer[data-side='top'] {
|
|
61
|
+
--rf-drawer-closed: 0 -100%;
|
|
62
|
+
--rf-drawer-tail: 0 -32px 0 0 var(--rf-drawer-bg);
|
|
63
|
+
inset: 0 0 auto;
|
|
64
|
+
border-radius: 0 0 var(--rf-panel-radius) var(--rf-panel-radius);
|
|
65
|
+
}
|
|
66
|
+
.rf-drawer[data-side='top'][open] {
|
|
67
|
+
translate: 0 calc(-1 * var(--rf-drawer-drag));
|
|
68
|
+
}
|
|
69
|
+
.rf-drawer[data-side='right'],
|
|
70
|
+
.rf-drawer[data-side='left'] {
|
|
71
|
+
inline-size: min(calc(100% - 48px), 400px);
|
|
72
|
+
block-size: 100dvh;
|
|
73
|
+
max-block-size: none;
|
|
74
|
+
}
|
|
75
|
+
.rf-drawer[data-side='right'] {
|
|
76
|
+
--rf-drawer-closed: 100% 0;
|
|
77
|
+
--rf-drawer-tail: 32px 0 0 0 var(--rf-drawer-bg);
|
|
78
|
+
inset: 0 0 0 auto;
|
|
79
|
+
border-radius: var(--rf-panel-radius) 0 0 var(--rf-panel-radius);
|
|
80
|
+
}
|
|
81
|
+
.rf-drawer[data-side='right'][open] {
|
|
82
|
+
translate: var(--rf-drawer-drag) 0;
|
|
83
|
+
}
|
|
84
|
+
.rf-drawer[data-side='left'] {
|
|
85
|
+
--rf-drawer-closed: -100% 0;
|
|
86
|
+
--rf-drawer-tail: -32px 0 0 0 var(--rf-drawer-bg);
|
|
87
|
+
inset: 0 auto 0 0;
|
|
88
|
+
border-radius: 0 var(--rf-panel-radius) var(--rf-panel-radius) 0;
|
|
89
|
+
}
|
|
90
|
+
.rf-drawer[data-side='left'][open] {
|
|
91
|
+
translate: calc(-1 * var(--rf-drawer-drag)) 0;
|
|
92
|
+
}
|
|
93
|
+
.rf-drawer[data-dragging] {
|
|
94
|
+
cursor: grabbing;
|
|
95
|
+
user-select: none;
|
|
96
|
+
transition: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.rf-drawer::backdrop {
|
|
100
|
+
background: rgb(0 0 0 / 40%);
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transition:
|
|
103
|
+
opacity var(--rf-exit-slow),
|
|
104
|
+
overlay 160ms allow-discrete,
|
|
105
|
+
display 160ms allow-discrete;
|
|
106
|
+
}
|
|
107
|
+
[data-theme='dark'] .rf-drawer::backdrop {
|
|
108
|
+
background: rgb(0 0 0 / 70%);
|
|
109
|
+
}
|
|
110
|
+
.rf-drawer[open]::backdrop {
|
|
111
|
+
opacity: calc(1 - var(--rf-drawer-progress, 0));
|
|
112
|
+
transition:
|
|
113
|
+
opacity var(--rf-spring-moderate),
|
|
114
|
+
overlay 160ms allow-discrete,
|
|
115
|
+
display 160ms allow-discrete;
|
|
116
|
+
}
|
|
117
|
+
.rf-drawer[data-dragging]::backdrop {
|
|
118
|
+
transition: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.rf-drawer-handle {
|
|
122
|
+
inline-size: 40px;
|
|
123
|
+
block-size: 4px;
|
|
124
|
+
margin: -12px auto 16px;
|
|
125
|
+
border-radius: 9999px;
|
|
126
|
+
background: var(--rf-border);
|
|
127
|
+
cursor: grab;
|
|
128
|
+
}
|
|
129
|
+
.rf-drawer[open] {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
}
|
|
133
|
+
/* At the top, the handle sits on the edge that pulls away. */
|
|
134
|
+
.rf-drawer[data-side='top'] .rf-drawer-handle {
|
|
135
|
+
order: 1;
|
|
136
|
+
margin: 16px auto -12px;
|
|
137
|
+
}
|
|
138
|
+
/* On the sides, the footer stays at the bottom of the panel. */
|
|
139
|
+
.rf-drawer:is([data-side='left'], [data-side='right']) .rf-drawer-body {
|
|
140
|
+
flex: 1;
|
|
141
|
+
}
|
|
142
|
+
.rf-drawer-handle,
|
|
143
|
+
.rf-drawer-header {
|
|
144
|
+
touch-action: none;
|
|
145
|
+
}
|
|
146
|
+
.rf-drawer-header {
|
|
147
|
+
display: grid;
|
|
148
|
+
gap: 6px;
|
|
149
|
+
padding-inline-end: 32px;
|
|
150
|
+
}
|
|
151
|
+
.rf-drawer-header h2 {
|
|
152
|
+
margin: 0;
|
|
153
|
+
font: 400 var(--rf-text-title) / var(--rf-leading-title) var(--rf-heading-font);
|
|
154
|
+
font-variation-settings: var(--rf-weight-strong);
|
|
155
|
+
}
|
|
156
|
+
.rf-drawer-header p {
|
|
157
|
+
margin: 0;
|
|
158
|
+
color: var(--rf-muted);
|
|
159
|
+
}
|
|
160
|
+
.rf-drawer:not(:has(.rf-drawer-close)) .rf-drawer-header {
|
|
161
|
+
padding-inline-end: 0;
|
|
162
|
+
}
|
|
163
|
+
.rf-drawer-body {
|
|
164
|
+
margin-block-start: 16px;
|
|
165
|
+
}
|
|
166
|
+
.rf-drawer-footer {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-wrap: wrap;
|
|
169
|
+
justify-content: flex-end;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
margin-block-start: 24px;
|
|
172
|
+
}
|
|
173
|
+
.rf-drawer-close {
|
|
174
|
+
gap: normal;
|
|
175
|
+
position: absolute;
|
|
176
|
+
inset-block-start: 12px;
|
|
177
|
+
inset-inline-end: 12px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[data-density='compact'] .rf-drawer {
|
|
181
|
+
padding: 16px;
|
|
182
|
+
}
|
|
183
|
+
[data-density='compact'] .rf-drawer:is([data-side='bottom'], [data-side='top']) {
|
|
184
|
+
padding-inline: max(16px, calc((100% - 520px) / 2));
|
|
185
|
+
}
|
|
186
|
+
[data-density='compact'] .rf-drawer-handle {
|
|
187
|
+
margin-block: -6px 12px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Reduced motion: the drawer fades in place; a drag still moves it. */
|
|
191
|
+
@media (prefers-reduced-motion: reduce) {
|
|
192
|
+
.rf-drawer {
|
|
193
|
+
translate: none;
|
|
194
|
+
opacity: 0;
|
|
195
|
+
transition:
|
|
196
|
+
opacity var(--rf-exit-slow),
|
|
197
|
+
overlay 160ms allow-discrete,
|
|
198
|
+
display 160ms allow-discrete;
|
|
199
|
+
}
|
|
200
|
+
.rf-drawer[open] {
|
|
201
|
+
opacity: 1;
|
|
202
|
+
transition:
|
|
203
|
+
opacity var(--rf-spring-moderate),
|
|
204
|
+
overlay 160ms allow-discrete,
|
|
205
|
+
display 160ms allow-discrete;
|
|
206
|
+
}
|
|
207
|
+
@starting-style {
|
|
208
|
+
.rf-drawer[open] {
|
|
209
|
+
translate: none;
|
|
210
|
+
opacity: 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
@media (forced-colors: active) {
|
|
215
|
+
.rf-drawer {
|
|
216
|
+
border: 1px solid CanvasText;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Flush: the content fills the panel and the title stays for assistive technology. */
|
|
221
|
+
.rf-drawer[data-flush] {
|
|
222
|
+
padding: 0;
|
|
223
|
+
}
|
|
224
|
+
.rf-drawer[data-flush] .rf-drawer-body {
|
|
225
|
+
flex: 1;
|
|
226
|
+
min-block-size: 0;
|
|
227
|
+
margin: 0;
|
|
228
|
+
}
|
|
229
|
+
.rf-drawer[data-flush] .rf-drawer-header {
|
|
230
|
+
position: absolute;
|
|
231
|
+
width: 1px;
|
|
232
|
+
height: 1px;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
clip-path: inset(50%);
|
|
235
|
+
}
|
|
236
|
+
.rf-drawer[data-flush] .rf-drawer-handle {
|
|
237
|
+
position: absolute;
|
|
238
|
+
inset-inline: 0;
|
|
239
|
+
margin-block: 8px;
|
|
240
|
+
}
|
package/dist/drawer.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Handle, type MixInput, type RemixNode } from 'remix/component';
|
|
2
|
+
import { type ButtonProps } from './basic.tsx';
|
|
3
|
+
export type DrawerSide = 'bottom' | 'top' | 'left' | 'right';
|
|
4
|
+
export interface DrawerTriggerProps {
|
|
5
|
+
'aria-haspopup': 'dialog';
|
|
6
|
+
'aria-expanded': boolean;
|
|
7
|
+
'aria-controls': string;
|
|
8
|
+
mix: MixInput<HTMLElement>;
|
|
9
|
+
}
|
|
10
|
+
export interface DrawerProps {
|
|
11
|
+
/** Contents of the button that opens the drawer. */
|
|
12
|
+
trigger?: RemixNode;
|
|
13
|
+
triggerVariant?: ButtonProps['variant'];
|
|
14
|
+
triggerSize?: ButtonProps['size'];
|
|
15
|
+
/** Renders your own trigger instead of the built-in button. Spread the provided props onto it. */
|
|
16
|
+
renderTrigger?: (props: DrawerTriggerProps) => RemixNode;
|
|
17
|
+
title: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
children?: RemixNode;
|
|
20
|
+
/** Footer actions; a callback receives a function that closes the drawer. */
|
|
21
|
+
footer?: RemixNode | ((close: () => void) => RemixNode);
|
|
22
|
+
/** The edge the drawer slides in from. Default bottom. */
|
|
23
|
+
side?: DrawerSide;
|
|
24
|
+
/** Shows a grab handle. Default true for dismissible top and bottom drawers. */
|
|
25
|
+
handle?: boolean;
|
|
26
|
+
/** Shows a close button. Default true for left and right drawers. */
|
|
27
|
+
showCloseButton?: boolean;
|
|
28
|
+
/** Whether Escape, a click on the backdrop or a drag can close it. Default true. */
|
|
29
|
+
dismissible?: boolean;
|
|
30
|
+
/** Let the content fill the panel edge to edge, and visually hide the title. */
|
|
31
|
+
flush?: boolean;
|
|
32
|
+
class?: string;
|
|
33
|
+
open?: boolean;
|
|
34
|
+
defaultOpen?: boolean;
|
|
35
|
+
onOpenChange?: (open: boolean) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A panel that slides in from an edge of the screen, for navigation on small
|
|
39
|
+
* screens, filters, details or a short task. It is a modal dialog built on
|
|
40
|
+
* the native element, like Dialog: it traps focus, closes on Escape or a
|
|
41
|
+
* click on the backdrop, and returns focus to its trigger. It follows a drag
|
|
42
|
+
* toward its edge and closes past a quarter of its size or on a quick flick;
|
|
43
|
+
* a drag the other way resists.
|
|
44
|
+
*/
|
|
45
|
+
export declare function Drawer(handle: Handle<DrawerProps>): () => import("remix/component").RemixElement;
|