@rtzll/reflow 0.4.0 → 0.6.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/avatar.css +116 -0
- package/dist/avatar.d.ts +42 -0
- package/dist/avatar.js +74 -0
- package/dist/basic.d.ts +1 -20
- package/dist/basic.js +3 -27
- package/dist/chat-layout.css +121 -0
- package/dist/chat-layout.d.ts +24 -0
- package/dist/chat-layout.js +108 -0
- package/dist/citation.css +119 -0
- package/dist/citation.d.ts +35 -0
- package/dist/citation.js +45 -0
- package/dist/code-block.css +292 -0
- package/dist/code-block.d.ts +59 -0
- package/dist/code-block.js +117 -0
- package/dist/colors.d.ts +21 -0
- package/dist/colors.js +21 -0
- package/dist/compositions.css +54 -3
- package/dist/compositions.d.ts +8 -1
- package/dist/compositions.js +2 -0
- package/dist/drawer.css +236 -0
- package/dist/drawer.d.ts +45 -0
- package/dist/drawer.js +228 -0
- package/dist/foundations.css +6 -0
- package/dist/highlight.d.ts +36 -0
- package/dist/highlight.js +134 -0
- package/dist/hover-card.d.ts +4 -0
- package/dist/hover-card.js +5 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -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 +6 -0
- package/dist/toaster.css +268 -0
- package/dist/toaster.d.ts +79 -0
- package/dist/toaster.js +419 -0
- package/package.json +19 -3
package/dist/colors.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The colours Badge and Avatar share. Internal: not exported from the package. */
|
|
2
|
+
export const badgeColors = {
|
|
3
|
+
accent: 'var(--rf-accent)',
|
|
4
|
+
gray: '#a3a3a3',
|
|
5
|
+
red: '#ef4444',
|
|
6
|
+
orange: '#f97316',
|
|
7
|
+
amber: '#f59e0b',
|
|
8
|
+
yellow: '#eab308',
|
|
9
|
+
lime: '#84cc16',
|
|
10
|
+
green: '#22c55e',
|
|
11
|
+
emerald: '#10b981',
|
|
12
|
+
teal: '#14b8a6',
|
|
13
|
+
cyan: '#06b6d4',
|
|
14
|
+
blue: '#3b82f6',
|
|
15
|
+
indigo: '#6366f1',
|
|
16
|
+
violet: '#8b5cf6',
|
|
17
|
+
purple: '#a855f7',
|
|
18
|
+
fuchsia: '#d946ef',
|
|
19
|
+
pink: '#ec4899',
|
|
20
|
+
rose: '#f43f5e',
|
|
21
|
+
};
|
package/dist/compositions.css
CHANGED
|
@@ -141,6 +141,57 @@
|
|
|
141
141
|
pointer-events: auto;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
/* System: an event in the conversation, such as a model switch, as a quiet
|
|
145
|
+
centred caption between hairlines. It spans the transcript, fades in
|
|
146
|
+
where it lands, and keeps its time and actions in view. */
|
|
147
|
+
.rf-chat-message[data-role='system'] {
|
|
148
|
+
align-items: stretch;
|
|
149
|
+
align-self: stretch;
|
|
150
|
+
justify-self: stretch;
|
|
151
|
+
max-width: none;
|
|
152
|
+
transform-origin: center;
|
|
153
|
+
}
|
|
154
|
+
.rf-chat-system {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
margin: 0;
|
|
159
|
+
padding-block: 8px;
|
|
160
|
+
color: var(--rf-muted);
|
|
161
|
+
font: var(--rf-text-caption) / var(--rf-leading-caption) var(--rf-font);
|
|
162
|
+
text-align: center;
|
|
163
|
+
}
|
|
164
|
+
.rf-chat-system::before,
|
|
165
|
+
.rf-chat-system::after {
|
|
166
|
+
content: '';
|
|
167
|
+
flex: 1;
|
|
168
|
+
min-inline-size: 16px;
|
|
169
|
+
border-block-start: 1px solid color-mix(in oklab, var(--rf-border) 70%, transparent);
|
|
170
|
+
}
|
|
171
|
+
.rf-chat-system-icon {
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
flex: none;
|
|
174
|
+
}
|
|
175
|
+
.rf-chat-system-icon > svg {
|
|
176
|
+
inline-size: 14px;
|
|
177
|
+
block-size: 14px;
|
|
178
|
+
}
|
|
179
|
+
.rf-chat-system-text {
|
|
180
|
+
min-inline-size: 0;
|
|
181
|
+
text-wrap: balance;
|
|
182
|
+
}
|
|
183
|
+
.rf-chat-system .rf-chat-time::before {
|
|
184
|
+
content: '·';
|
|
185
|
+
margin-inline-end: 8px;
|
|
186
|
+
}
|
|
187
|
+
.rf-chat-message[data-role='system'][data-animate] {
|
|
188
|
+
animation-name: rf-message-fade;
|
|
189
|
+
}
|
|
190
|
+
@keyframes rf-message-fade {
|
|
191
|
+
from {
|
|
192
|
+
opacity: 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
144
195
|
@keyframes rf-message-enter {
|
|
145
196
|
from {
|
|
146
197
|
opacity: 0;
|
|
@@ -470,7 +521,7 @@
|
|
|
470
521
|
}
|
|
471
522
|
.rf-queue {
|
|
472
523
|
--rf-queue-row: 44px;
|
|
473
|
-
--rf-queue-gap:
|
|
524
|
+
--rf-queue-gap: var(--rf-stack-gap);
|
|
474
525
|
position: relative;
|
|
475
526
|
list-style: none;
|
|
476
527
|
padding: 0;
|
|
@@ -502,7 +553,7 @@
|
|
|
502
553
|
padding: 0 8px;
|
|
503
554
|
cursor: grab;
|
|
504
555
|
z-index: calc(3 - var(--rf-queue-depth));
|
|
505
|
-
scale: calc(1 - var(--rf-queue-depth) *
|
|
556
|
+
scale: calc(1 - var(--rf-queue-depth) * var(--rf-stack-scale-step)) 1;
|
|
506
557
|
transition:
|
|
507
558
|
margin-block-start var(--rf-spring-moderate),
|
|
508
559
|
scale var(--rf-spring-moderate),
|
|
@@ -511,7 +562,7 @@
|
|
|
511
562
|
.rf-queue > li + li {
|
|
512
563
|
margin-block-start: var(
|
|
513
564
|
--rf-queue-spacing,
|
|
514
|
-
calc(var(--rf-queue-peek,
|
|
565
|
+
calc(var(--rf-queue-peek, var(--rf-stack-peek)) - var(--rf-queue-row))
|
|
515
566
|
);
|
|
516
567
|
}
|
|
517
568
|
.rf-queue > li:nth-child(n + 4) {
|
package/dist/compositions.d.ts
CHANGED
|
@@ -22,13 +22,20 @@ export interface ChatAttachment {
|
|
|
22
22
|
}
|
|
23
23
|
export interface ChatMessageProps {
|
|
24
24
|
transitionId?: string;
|
|
25
|
-
|
|
25
|
+
/** `system` shows an event in the conversation, such as a model switch, as a quiet centred line. */
|
|
26
|
+
role: 'user' | 'assistant' | 'system';
|
|
26
27
|
children?: RemixNode;
|
|
28
|
+
/** Files shown with a user or assistant message. */
|
|
27
29
|
attachments?: readonly ChatAttachment[];
|
|
30
|
+
/** Names the message. Defaults to You, Assistant or System. */
|
|
28
31
|
label?: string;
|
|
29
32
|
size?: ControlSize;
|
|
33
|
+
/** Shown under a user message on hover, and inline after a system event. */
|
|
30
34
|
time?: string;
|
|
35
|
+
/** Shown on hover under a message, and always after a system event. */
|
|
31
36
|
actions?: RemixNode;
|
|
37
|
+
/** A small glyph before a system event. */
|
|
38
|
+
icon?: RemixNode;
|
|
32
39
|
animate?: boolean;
|
|
33
40
|
}
|
|
34
41
|
export declare function ChatMessage(handle: Handle<ChatMessageProps>): () => import("remix/component").RemixElement;
|
package/dist/compositions.js
CHANGED
|
@@ -90,6 +90,8 @@ export function ChatMessage(handle) {
|
|
|
90
90
|
return () => {
|
|
91
91
|
const { role, label, attachments, children, size, time, actions, animate = true } = handle.props;
|
|
92
92
|
followMove();
|
|
93
|
+
if (role === 'system')
|
|
94
|
+
return (_jsx("article", { class: "rf-chat-message", "data-role": "system", "data-size": size, "data-animate": animate ? '' : undefined, "aria-label": label ?? 'System', mix: track, children: _jsxs("p", { class: "rf-chat-system", children: [handle.props.icon != null && (_jsx("span", { class: "rf-chat-system-icon", "aria-hidden": "true", children: handle.props.icon })), _jsx("span", { class: "rf-chat-system-text", children: children }), time && _jsx("span", { class: "rf-chat-time", children: time }), actions != null && _jsx("span", { class: "rf-chat-actions", children: actions })] }) }));
|
|
93
95
|
return (_jsxs("article", { class: "rf-chat-message", "data-role": role, "data-size": size, "data-animate": animate ? '' : undefined, "aria-label": label ?? (role === 'user' ? 'You' : 'Assistant'), mix: track, children: [attachments?.length ? (_jsx("ul", { class: "rf-chat-attachments", "aria-label": "Attachments", children: attachments.map((attachment, index) => (_jsxs("li", { class: "rf-chat-thumb", title: attachment.detail ? `${attachment.name} (${attachment.detail})` : undefined, children: [attachment.previewUrl ? (_jsx("img", { src: attachment.previewUrl, alt: attachment.name })) : (_jsx("span", { class: "rf-chat-thumb-glyph", role: "img", "aria-label": attachment.name, children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [_jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), _jsx("path", { d: "M14 3v5h5" })] }) })), attachment.url && _jsx("a", { href: attachment.url, "aria-label": attachment.name })] }, index))) })) : null, children != null && children !== '' && (_jsx("div", { class: "rf-chat-body", style: handle.props.transitionId
|
|
94
96
|
? { viewTransitionName: messageTransitionName(handle.props.transitionId) }
|
|
95
97
|
: undefined, children: children })), ((role === 'user' && time) || actions) && (_jsxs("div", { class: "rf-chat-meta", children: [role === 'user' && time && _jsx("span", { class: "rf-chat-time", children: time }), actions != null && _jsx("span", { class: "rf-chat-actions", children: actions })] }))] }));
|
package/dist/drawer.css
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/* Drawer: a modal panel attached to one edge of the viewport. The panel
|
|
2
|
+
and the backdrop share one motion, in and out and back from a drag: a
|
|
3
|
+
long ease that starts fast and settles gently, with no overshoot, so the
|
|
4
|
+
panel never lifts off its edge and the backdrop darkens as it arrives.
|
|
5
|
+
While dragged the panel follows --rf-drawer-drag, the distance toward its
|
|
6
|
+
edge, and the backdrop lightens with --rf-drawer-progress. A strip of
|
|
7
|
+
panel color past the edge covers the gap when a drag pulls it the other
|
|
8
|
+
way. */
|
|
9
|
+
.rf-drawer {
|
|
10
|
+
--rf-drawer-motion: 500ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
11
|
+
--rf-drawer-bg: var(--rf-surface-5);
|
|
12
|
+
--rf-drawer-drag: 0px;
|
|
13
|
+
--rf-drawer-closed: 0 100%;
|
|
14
|
+
--rf-drawer-tail: 0 32px 0 0 var(--rf-drawer-bg);
|
|
15
|
+
position: fixed;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
inset: auto 0 0;
|
|
18
|
+
inline-size: 100%;
|
|
19
|
+
max-inline-size: none;
|
|
20
|
+
block-size: auto;
|
|
21
|
+
max-block-size: calc(100dvh - 48px);
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 24px;
|
|
24
|
+
border: 0;
|
|
25
|
+
border-radius: var(--rf-panel-radius) var(--rf-panel-radius) 0 0;
|
|
26
|
+
outline: none;
|
|
27
|
+
overflow: auto;
|
|
28
|
+
overscroll-behavior: contain;
|
|
29
|
+
background: var(--rf-drawer-bg);
|
|
30
|
+
color: var(--rf-fg);
|
|
31
|
+
box-shadow:
|
|
32
|
+
var(--rf-drawer-tail),
|
|
33
|
+
0 0 0 1px light-dark(rgb(0 0 0 / 6%), rgb(255 255 255 / 6%)),
|
|
34
|
+
0 0 24px light-dark(rgb(0 0 0 / 10%), rgb(0 0 0 / 30%));
|
|
35
|
+
font: var(--rf-text-body) / var(--rf-leading-body) var(--rf-font);
|
|
36
|
+
translate: var(--rf-drawer-closed);
|
|
37
|
+
transition:
|
|
38
|
+
translate var(--rf-drawer-motion),
|
|
39
|
+
overlay var(--rf-drawer-motion) allow-discrete,
|
|
40
|
+
display var(--rf-drawer-motion) allow-discrete;
|
|
41
|
+
}
|
|
42
|
+
.rf-drawer[open] {
|
|
43
|
+
translate: 0 var(--rf-drawer-drag);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Top and bottom drawers keep their content in a readable column. */
|
|
47
|
+
.rf-drawer[data-side='bottom'],
|
|
48
|
+
.rf-drawer[data-side='top'] {
|
|
49
|
+
padding-inline: max(24px, calc((100% - 560px) / 2));
|
|
50
|
+
}
|
|
51
|
+
.rf-drawer[data-side='top'] {
|
|
52
|
+
--rf-drawer-closed: 0 -100%;
|
|
53
|
+
--rf-drawer-tail: 0 -32px 0 0 var(--rf-drawer-bg);
|
|
54
|
+
inset: 0 0 auto;
|
|
55
|
+
border-radius: 0 0 var(--rf-panel-radius) var(--rf-panel-radius);
|
|
56
|
+
}
|
|
57
|
+
.rf-drawer[data-side='top'][open] {
|
|
58
|
+
translate: 0 calc(-1 * var(--rf-drawer-drag));
|
|
59
|
+
}
|
|
60
|
+
.rf-drawer[data-side='right'],
|
|
61
|
+
.rf-drawer[data-side='left'] {
|
|
62
|
+
inline-size: min(calc(100% - 48px), 400px);
|
|
63
|
+
block-size: 100dvh;
|
|
64
|
+
max-block-size: none;
|
|
65
|
+
}
|
|
66
|
+
.rf-drawer[data-side='right'] {
|
|
67
|
+
--rf-drawer-closed: 100% 0;
|
|
68
|
+
--rf-drawer-tail: 32px 0 0 0 var(--rf-drawer-bg);
|
|
69
|
+
inset: 0 0 0 auto;
|
|
70
|
+
border-radius: var(--rf-panel-radius) 0 0 var(--rf-panel-radius);
|
|
71
|
+
}
|
|
72
|
+
.rf-drawer[data-side='right'][open] {
|
|
73
|
+
translate: var(--rf-drawer-drag) 0;
|
|
74
|
+
}
|
|
75
|
+
.rf-drawer[data-side='left'] {
|
|
76
|
+
--rf-drawer-closed: -100% 0;
|
|
77
|
+
--rf-drawer-tail: -32px 0 0 0 var(--rf-drawer-bg);
|
|
78
|
+
inset: 0 auto 0 0;
|
|
79
|
+
border-radius: 0 var(--rf-panel-radius) var(--rf-panel-radius) 0;
|
|
80
|
+
}
|
|
81
|
+
.rf-drawer[data-side='left'][open] {
|
|
82
|
+
translate: calc(-1 * var(--rf-drawer-drag)) 0;
|
|
83
|
+
}
|
|
84
|
+
.rf-drawer[data-dragging] {
|
|
85
|
+
cursor: grabbing;
|
|
86
|
+
user-select: none;
|
|
87
|
+
transition: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.rf-drawer::backdrop {
|
|
91
|
+
background: rgb(0 0 0 / 40%);
|
|
92
|
+
opacity: 0;
|
|
93
|
+
transition:
|
|
94
|
+
opacity 500ms cubic-bezier(0.32, 0.72, 0, 1),
|
|
95
|
+
overlay 500ms cubic-bezier(0.32, 0.72, 0, 1) allow-discrete,
|
|
96
|
+
display 500ms cubic-bezier(0.32, 0.72, 0, 1) allow-discrete;
|
|
97
|
+
}
|
|
98
|
+
[data-theme='dark'] .rf-drawer::backdrop {
|
|
99
|
+
background: rgb(0 0 0 / 70%);
|
|
100
|
+
}
|
|
101
|
+
.rf-drawer[open]::backdrop {
|
|
102
|
+
opacity: 1;
|
|
103
|
+
}
|
|
104
|
+
.rf-drawer[data-dragging]::backdrop {
|
|
105
|
+
opacity: calc(1 - var(--rf-drawer-progress, 0));
|
|
106
|
+
transition: none;
|
|
107
|
+
}
|
|
108
|
+
/* Where the panel and backdrop start from as the drawer opens. Starting
|
|
109
|
+
styles cascade with the rest, so these follow the open rules above and
|
|
110
|
+
match the per-side ones in specificity, or those would win and the
|
|
111
|
+
drawer would appear without its slide. */
|
|
112
|
+
@starting-style {
|
|
113
|
+
.rf-drawer[data-side][open] {
|
|
114
|
+
translate: var(--rf-drawer-closed);
|
|
115
|
+
}
|
|
116
|
+
.rf-drawer[data-side][open]::backdrop {
|
|
117
|
+
opacity: 0;
|
|
118
|
+
}
|
|
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-drawer-motion),
|
|
197
|
+
overlay var(--rf-drawer-motion) allow-discrete,
|
|
198
|
+
display var(--rf-drawer-motion) allow-discrete;
|
|
199
|
+
}
|
|
200
|
+
.rf-drawer[open] {
|
|
201
|
+
opacity: 1;
|
|
202
|
+
}
|
|
203
|
+
@starting-style {
|
|
204
|
+
.rf-drawer[data-side][open] {
|
|
205
|
+
translate: none;
|
|
206
|
+
opacity: 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
@media (forced-colors: active) {
|
|
211
|
+
.rf-drawer {
|
|
212
|
+
border: 1px solid CanvasText;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Flush: the content fills the panel and the title stays for assistive technology. */
|
|
217
|
+
.rf-drawer[data-flush] {
|
|
218
|
+
padding: 0;
|
|
219
|
+
}
|
|
220
|
+
.rf-drawer[data-flush] .rf-drawer-body {
|
|
221
|
+
flex: 1;
|
|
222
|
+
min-block-size: 0;
|
|
223
|
+
margin: 0;
|
|
224
|
+
}
|
|
225
|
+
.rf-drawer[data-flush] .rf-drawer-header {
|
|
226
|
+
position: absolute;
|
|
227
|
+
width: 1px;
|
|
228
|
+
height: 1px;
|
|
229
|
+
overflow: hidden;
|
|
230
|
+
clip-path: inset(50%);
|
|
231
|
+
}
|
|
232
|
+
.rf-drawer[data-flush] .rf-drawer-handle {
|
|
233
|
+
position: absolute;
|
|
234
|
+
inset-inline: 0;
|
|
235
|
+
margin-block: 8px;
|
|
236
|
+
}
|
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;
|
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
|
+
}
|
package/dist/foundations.css
CHANGED
|
@@ -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) {
|