@rtzll/reflow 0.4.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 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;
@@ -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 ?? bannerIcon(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
+ 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;
@@ -470,7 +470,7 @@
470
470
  }
471
471
  .rf-queue {
472
472
  --rf-queue-row: 44px;
473
- --rf-queue-gap: 5px;
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) * 0.035) 1;
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, 6px) - var(--rf-queue-row))
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) {
@@ -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
+ }
@@ -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;
package/dist/drawer.js ADDED
@@ -0,0 +1,228 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "remix/component/jsx-runtime";
2
+ import { on, ref } from 'remix/component';
3
+ import { Button } from './basic.js';
4
+ import { Icon } from './icon.js';
5
+ // A release past this share of the panel's size, or faster than this many
6
+ // pixels per millisecond, closes it; anything less springs back.
7
+ const closeShare = 0.25;
8
+ const flickVelocity = 0.4;
9
+ const interactive = 'button, a, input, select, textarea, label, [contenteditable], [role="slider"]';
10
+ /**
11
+ * A panel that slides in from an edge of the screen, for navigation on small
12
+ * screens, filters, details or a short task. It is a modal dialog built on
13
+ * the native element, like Dialog: it traps focus, closes on Escape or a
14
+ * click on the backdrop, and returns focus to its trigger. It follows a drag
15
+ * toward its edge and closes past a quarter of its size or on a quick flick;
16
+ * a drag the other way resists.
17
+ */
18
+ export function Drawer(handle) {
19
+ let dialog = null;
20
+ let invoker = null;
21
+ let internalOpen = handle.props.defaultOpen ?? false;
22
+ let pressedBackdrop = false;
23
+ let drag;
24
+ const id = handle.id;
25
+ const titleId = `${id}-title`;
26
+ const descriptionId = `${id}-description`;
27
+ const open = () => handle.props.open ?? internalOpen;
28
+ const side = () => handle.props.side ?? 'bottom';
29
+ const dismissible = () => handle.props.dismissible ?? true;
30
+ function requestOpen(next) {
31
+ if (open() === next)
32
+ return;
33
+ internalOpen = next;
34
+ handle.props.onOpenChange?.(next);
35
+ void handle.update();
36
+ }
37
+ function close() {
38
+ requestOpen(false);
39
+ }
40
+ function sync() {
41
+ if (!dialog?.isConnected || dialog.open === open())
42
+ return;
43
+ if (open()) {
44
+ setDrag(dialog, 0);
45
+ dialog.showModal();
46
+ }
47
+ else
48
+ dialog.close();
49
+ }
50
+ function setDrag(element, offset, size = 1) {
51
+ element.style.setProperty('--rf-drawer-drag', `${Math.round(offset)}px`);
52
+ element.style.setProperty('--rf-drawer-progress', String(Math.min(1, Math.max(0, offset / size))));
53
+ }
54
+ function outsidePanel(event, element) {
55
+ if (event.target !== element)
56
+ return false;
57
+ const rect = element.getBoundingClientRect();
58
+ return (event.clientX < rect.left ||
59
+ event.clientX > rect.right ||
60
+ event.clientY < rect.top ||
61
+ event.clientY > rect.bottom);
62
+ }
63
+ // Whether the content under the pointer would scroll instead: a drag
64
+ // toward the edge belongs to a scroller that is not at its start.
65
+ function scrollsFirst(target, element) {
66
+ const vertical = side() === 'bottom' || side() === 'top';
67
+ for (let node = target; node; node = node.parentElement) {
68
+ if (node instanceof HTMLElement) {
69
+ const style = getComputedStyle(node);
70
+ const overflow = vertical ? style.overflowY : style.overflowX;
71
+ if (/auto|scroll/.test(overflow)) {
72
+ if (side() === 'bottom' && node.scrollTop > 0)
73
+ return true;
74
+ if (side() === 'top' && node.scrollTop + node.clientHeight < node.scrollHeight - 1)
75
+ return true;
76
+ if (side() === 'right' && node.scrollLeft > 0)
77
+ return true;
78
+ if (side() === 'left' && node.scrollLeft + node.clientWidth < node.scrollWidth - 1)
79
+ return true;
80
+ }
81
+ }
82
+ if (node === element)
83
+ break;
84
+ }
85
+ return false;
86
+ }
87
+ // How far the pointer has moved toward the drawer's edge, and across it.
88
+ function travel(event, start) {
89
+ const dx = event.clientX - start.x;
90
+ const dy = event.clientY - start.y;
91
+ switch (side()) {
92
+ case 'bottom':
93
+ return [dy, dx];
94
+ case 'top':
95
+ return [-dy, dx];
96
+ case 'right':
97
+ return [dx, dy];
98
+ case 'left':
99
+ return [-dx, dy];
100
+ }
101
+ }
102
+ function endDrag(event, element) {
103
+ if (!drag || drag.pointer !== event.pointerId)
104
+ return;
105
+ const current = drag;
106
+ drag = undefined;
107
+ if (!current.started)
108
+ return;
109
+ element.removeAttribute('data-dragging');
110
+ const now = performance.now();
111
+ // Held still before letting go, the throw has no speed left.
112
+ const velocity = now - current.time > 100
113
+ ? 0
114
+ : (current.offset - current.previousOffset) /
115
+ Math.max(1, current.time - current.previousTime);
116
+ if (current.offset > 0 &&
117
+ (current.offset > current.size * closeShare || velocity > flickVelocity)) {
118
+ close();
119
+ return;
120
+ }
121
+ setDrag(element, 0);
122
+ }
123
+ const dialogMix = ref((element, signal) => {
124
+ if (!(element instanceof HTMLDialogElement))
125
+ return;
126
+ dialog = element;
127
+ const options = { signal };
128
+ element.addEventListener('cancel', (event) => {
129
+ event.preventDefault();
130
+ if (dismissible())
131
+ close();
132
+ }, options);
133
+ element.addEventListener('close', () => {
134
+ if (element.open)
135
+ return;
136
+ close();
137
+ const target = invoker;
138
+ invoker = null;
139
+ if (target?.isConnected &&
140
+ element.ownerDocument.activeElement === element.ownerDocument.body)
141
+ target.focus({ preventScroll: true });
142
+ }, options);
143
+ element.addEventListener('pointerdown', (event) => {
144
+ pressedBackdrop = outsidePanel(event, element);
145
+ if (pressedBackdrop || !dismissible() || event.button !== 0)
146
+ return;
147
+ const target = event.target;
148
+ if (!(target instanceof Element) || target.closest(interactive))
149
+ return;
150
+ if (scrollsFirst(target, element))
151
+ return;
152
+ const rect = element.getBoundingClientRect();
153
+ const now = performance.now();
154
+ drag = {
155
+ pointer: event.pointerId,
156
+ x: event.clientX,
157
+ y: event.clientY,
158
+ started: false,
159
+ offset: 0,
160
+ size: side() === 'bottom' || side() === 'top' ? rect.height : rect.width,
161
+ time: now,
162
+ previousTime: now,
163
+ previousOffset: 0,
164
+ };
165
+ }, options);
166
+ element.addEventListener('pointermove', (event) => {
167
+ if (!drag || drag.pointer !== event.pointerId)
168
+ return;
169
+ const [along, across] = travel(event, drag);
170
+ if (!drag.started) {
171
+ if (Math.abs(along) < 4 && Math.abs(across) < 4)
172
+ return;
173
+ // Sideways first, it is a selection or a scroll, not a drag.
174
+ if (Math.abs(across) > Math.abs(along)) {
175
+ drag = undefined;
176
+ return;
177
+ }
178
+ drag.started = true;
179
+ element.setAttribute('data-dragging', '');
180
+ try {
181
+ element.setPointerCapture(event.pointerId);
182
+ }
183
+ catch {
184
+ // Synthetic pointers have nothing to capture.
185
+ }
186
+ }
187
+ // Toward the edge it follows the pointer; away from it, it resists.
188
+ const offset = along >= 0 ? along : along / (1 + Math.abs(along) / 24);
189
+ drag.previousOffset = drag.offset;
190
+ drag.previousTime = drag.time;
191
+ drag.offset = offset;
192
+ drag.time = performance.now();
193
+ setDrag(element, offset, drag.size);
194
+ }, options);
195
+ element.addEventListener('pointerup', (event) => endDrag(event, element), options);
196
+ element.addEventListener('pointercancel', (event) => endDrag(event, element), options);
197
+ element.addEventListener('click', (event) => {
198
+ if (pressedBackdrop && outsidePanel(event, element) && dismissible())
199
+ close();
200
+ pressedBackdrop = false;
201
+ }, options);
202
+ signal.addEventListener('abort', () => {
203
+ if (dialog === element)
204
+ dialog = null;
205
+ }, { once: true });
206
+ });
207
+ return () => {
208
+ const props = handle.props;
209
+ handle.queueTask(sync);
210
+ const footer = typeof props.footer === 'function' ? props.footer(close) : props.footer;
211
+ const vertical = side() === 'bottom' || side() === 'top';
212
+ const showHandle = props.handle ?? (vertical && dismissible());
213
+ const showClose = props.showCloseButton ?? (!vertical && !props.flush);
214
+ const triggerProps = {
215
+ 'aria-haspopup': 'dialog',
216
+ 'aria-expanded': open(),
217
+ 'aria-controls': id,
218
+ mix: on('click', (event) => {
219
+ if (event.currentTarget instanceof HTMLElement)
220
+ invoker = event.currentTarget;
221
+ requestOpen(true);
222
+ }),
223
+ };
224
+ return (_jsxs(_Fragment, { children: [props.renderTrigger
225
+ ? props.renderTrigger(triggerProps)
226
+ : props.trigger != null && (_jsx(Button, { variant: props.triggerVariant ?? 'tertiary', size: props.triggerSize, ...triggerProps, children: props.trigger })), _jsxs("dialog", { id: id, class: props.class ? `rf-drawer ${props.class}` : 'rf-drawer', "data-side": side(), "data-flush": props.flush || undefined, "aria-modal": "true", "aria-labelledby": titleId, "aria-describedby": props.description ? descriptionId : undefined, mix: dialogMix, children: [showHandle && _jsx("div", { class: "rf-drawer-handle", "aria-hidden": "true" }), _jsxs("header", { class: "rf-drawer-header", children: [_jsx("h2", { id: titleId, children: props.title }), props.description && _jsx("p", { id: descriptionId, children: props.description })] }), props.children != null && _jsx("div", { class: "rf-drawer-body", children: props.children }), footer != null && _jsx("footer", { class: "rf-drawer-footer", children: footer }), showClose && (_jsx(Button, { variant: "ghost", size: "icon-compact", class: "rf-drawer-close", "aria-label": "Close drawer", mix: on('click', close), children: _jsx(Icon, { name: "x", size: 14 }) }))] })] }));
227
+ };
228
+ }
@@ -198,6 +198,12 @@
198
198
  --rf-exit-fast: 60ms ease-out;
199
199
  --rf-exit-moderate: 120ms ease-out;
200
200
  --rf-exit-slow: 160ms ease-out;
201
+ /* Piles: QueuedStack and the Toaster share these. Each card behind the
202
+ front one peeks out by the peek and narrows by the scale step; spread
203
+ out, cards sit a gap apart. */
204
+ --rf-stack-peek: 6px;
205
+ --rf-stack-gap: 5px;
206
+ --rf-stack-scale-step: 0.035;
201
207
  }
202
208
 
203
209
  @media (prefers-reduced-motion: reduce) {
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export * from './breadcrumb.tsx';
13
13
  export * from './segmented-control.tsx';
14
14
  export * from './popover.tsx';
15
15
  export * from './hover-card.tsx';
16
+ export * from './toaster.tsx';
17
+ export * from './drawer.tsx';
16
18
  export * from './systems.ts';
17
19
  export * from './queued-stack.tsx';
18
20
  export * from './scroll-area.tsx';
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ export * from './breadcrumb.js';
13
13
  export * from './segmented-control.js';
14
14
  export * from './popover.js';
15
15
  export * from './hover-card.js';
16
+ export * from './toaster.js';
17
+ export * from './drawer.js';
16
18
  export * from './systems.js';
17
19
  export * from './queued-stack.js';
18
20
  export * from './scroll-area.js';