@rtzll/reflow 0.2.0 → 0.4.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 +1 -1
- package/dist/basic.js +1 -1
- package/dist/breadcrumb.css +10 -0
- package/dist/breadcrumb.js +67 -38
- package/dist/color-picker.css +16 -0
- package/dist/color-picker.js +83 -9
- package/dist/compositions.css +27 -7
- package/dist/compositions.js +67 -2
- package/dist/dates.css +108 -3
- package/dist/dates.js +16 -5
- package/dist/flows.css +4 -1
- package/dist/foundations.css +21 -14
- package/dist/groups.css +34 -0
- package/dist/groups.js +9 -1
- package/dist/hover-card.d.ts +32 -0
- package/dist/hover-card.js +119 -0
- package/dist/icon-data.d.ts +13 -0
- package/dist/icon-data.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/interactive.d.ts +3 -1
- package/dist/interactive.js +3 -1
- 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/selection-runs.d.ts +4 -0
- package/dist/selection-runs.js +221 -0
- package/dist/sidebar.css +8 -0
- package/dist/styles.css +2 -0
- package/dist/systems.js +5 -1
- package/package.json +1 -1
package/dist/dates.css
CHANGED
|
@@ -31,6 +31,42 @@
|
|
|
31
31
|
'opsz' 18;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* With week numbers, the title belongs to the days: it starts past the
|
|
35
|
+
week column. */
|
|
36
|
+
.rf-calendar[data-week-numbers] .rf-calendar-header {
|
|
37
|
+
padding-inline-start: calc(var(--rf-date-cell) * 0.75 + 8px);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Months side by side, as in the reference: each title centered over its
|
|
41
|
+
days, previous at the far left, next at the far right. */
|
|
42
|
+
.rf-calendar-header[data-centered] {
|
|
43
|
+
position: relative;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
padding-inline: 0;
|
|
46
|
+
}
|
|
47
|
+
.rf-calendar[data-week-numbers] .rf-calendar-header[data-centered] {
|
|
48
|
+
padding-inline-start: calc(var(--rf-date-cell) * 0.75);
|
|
49
|
+
}
|
|
50
|
+
.rf-calendar-header[data-centered] .rf-calendar-months-label {
|
|
51
|
+
margin-inline-end: 0;
|
|
52
|
+
text-align: center;
|
|
53
|
+
}
|
|
54
|
+
.rf-calendar-header[data-centered] .rf-calendar-nav {
|
|
55
|
+
position: absolute;
|
|
56
|
+
inset-block-start: 0;
|
|
57
|
+
}
|
|
58
|
+
.rf-calendar-header[data-centered] .rf-calendar-nav[data-direction='previous'] {
|
|
59
|
+
inset-inline-start: 0;
|
|
60
|
+
}
|
|
61
|
+
.rf-calendar[data-week-numbers]
|
|
62
|
+
.rf-calendar-header[data-centered]
|
|
63
|
+
.rf-calendar-nav[data-direction='previous'] {
|
|
64
|
+
inset-inline-start: calc(var(--rf-date-cell) * 0.75);
|
|
65
|
+
}
|
|
66
|
+
.rf-calendar-header[data-centered] .rf-calendar-nav[data-direction='next'] {
|
|
67
|
+
inset-inline-end: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
34
70
|
.rf-calendar-nav,
|
|
35
71
|
.rf-calendar-day {
|
|
36
72
|
appearance: none;
|
|
@@ -102,6 +138,46 @@
|
|
|
102
138
|
gap: 4px;
|
|
103
139
|
}
|
|
104
140
|
|
|
141
|
+
/* Stacked months, as in the reference: one weekday row above them, kept in
|
|
142
|
+
view while a short popup scrolls, 16px between the months, and arrows that
|
|
143
|
+
page up and down. */
|
|
144
|
+
.rf-calendar[data-stacked] .rf-calendar-months {
|
|
145
|
+
gap: 16px;
|
|
146
|
+
}
|
|
147
|
+
.rf-calendar-weekdays {
|
|
148
|
+
position: sticky;
|
|
149
|
+
inset-block-start: 0;
|
|
150
|
+
z-index: 3;
|
|
151
|
+
display: flex;
|
|
152
|
+
margin-block-end: 16px;
|
|
153
|
+
background: inherit;
|
|
154
|
+
color: var(--rf-muted);
|
|
155
|
+
font-size: var(--rf-text-caption, 12px);
|
|
156
|
+
line-height: var(--rf-leading-caption, 16px);
|
|
157
|
+
}
|
|
158
|
+
.rf-calendar-weekdays > span {
|
|
159
|
+
display: grid;
|
|
160
|
+
flex: none;
|
|
161
|
+
place-items: center;
|
|
162
|
+
inline-size: var(--rf-date-cell);
|
|
163
|
+
block-size: calc(var(--rf-date-cell) * 0.8);
|
|
164
|
+
block-size: round(down, calc(var(--rf-date-cell) * 0.8), 1px);
|
|
165
|
+
}
|
|
166
|
+
.rf-calendar-weekdays > .rf-calendar-weekdays-week {
|
|
167
|
+
inline-size: calc(var(--rf-date-cell) * 0.75);
|
|
168
|
+
}
|
|
169
|
+
.rf-calendar[data-stacked] .rf-calendar-grid thead {
|
|
170
|
+
position: absolute;
|
|
171
|
+
inline-size: 1px;
|
|
172
|
+
block-size: 1px;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
clip-path: inset(50%);
|
|
175
|
+
white-space: nowrap;
|
|
176
|
+
}
|
|
177
|
+
.rf-calendar[data-stacked] .rf-calendar-nav svg {
|
|
178
|
+
rotate: 90deg;
|
|
179
|
+
}
|
|
180
|
+
|
|
105
181
|
.rf-calendar-grid {
|
|
106
182
|
border-collapse: separate;
|
|
107
183
|
border-spacing: 0;
|
|
@@ -186,13 +262,42 @@
|
|
|
186
262
|
color: var(--rf-bg);
|
|
187
263
|
}
|
|
188
264
|
|
|
265
|
+
/* Picking a range's end, as in the reference: the proposed end is filled
|
|
266
|
+
like a selected day and glides to the pointer, with a faint ring 1px inside
|
|
267
|
+
it that goes solid while pressed. Its number draws above the fill. */
|
|
189
268
|
.rf-calendar-preview-end {
|
|
190
269
|
z-index: 1;
|
|
191
|
-
|
|
270
|
+
background: var(--rf-fg);
|
|
271
|
+
}
|
|
272
|
+
.rf-calendar-preview-end::before {
|
|
273
|
+
content: '';
|
|
274
|
+
position: absolute;
|
|
275
|
+
inset: 1px;
|
|
276
|
+
border: 1px solid var(--rf-bg);
|
|
277
|
+
border-radius: max(0px, calc(var(--rf-radius, 8px) - 1px));
|
|
278
|
+
opacity: 0.4;
|
|
279
|
+
transition: opacity 80ms ease;
|
|
280
|
+
}
|
|
281
|
+
.rf-calendar-months:has(.rf-calendar-day[data-rf-selected]:active)
|
|
282
|
+
.rf-calendar-preview-end::before {
|
|
283
|
+
opacity: 1;
|
|
284
|
+
}
|
|
285
|
+
.rf-calendar-day {
|
|
286
|
+
z-index: 2;
|
|
287
|
+
}
|
|
288
|
+
.rf-calendar-day[data-rf-selected] {
|
|
289
|
+
color: var(--rf-bg);
|
|
192
290
|
}
|
|
193
|
-
|
|
194
291
|
.rf-calendar-months:not([data-rf-has-selection]) .rf-calendar-day[data-rf-selected] {
|
|
195
|
-
|
|
292
|
+
background: var(--rf-fg);
|
|
293
|
+
}
|
|
294
|
+
/* The same faint ring marks a filled day under the pointer. */
|
|
295
|
+
.rf-calendar-day[data-selected][data-rf-hovered] {
|
|
296
|
+
outline: 1px solid color-mix(in oklab, var(--rf-bg) 40%, transparent);
|
|
297
|
+
outline-offset: -2px;
|
|
298
|
+
}
|
|
299
|
+
.rf-calendar-day[data-selected][data-rf-hovered]:active {
|
|
300
|
+
outline-color: var(--rf-bg);
|
|
196
301
|
}
|
|
197
302
|
|
|
198
303
|
@media (forced-colors: active) {
|
package/dist/dates.js
CHANGED
|
@@ -255,7 +255,7 @@ export function Calendar(handle) {
|
|
|
255
255
|
}
|
|
256
256
|
function monthButton(direction, disabled) {
|
|
257
257
|
const label = direction === 'left' ? 'Previous month' : 'Next month';
|
|
258
|
-
return (_jsx(Tooltip, { content: label, side: "bottom", renderTrigger: (trigger) => (_jsx("button", { ...trigger, type: "button", class: "rf-calendar-nav", "data-rf-hover-item": true, "aria-label": label, disabled: disabled, mix: [trigger.mix, on('click', () => moveMonth(direction === 'left' ? -1 : 1))], children: _jsx(DateArrow, { direction: direction }) })) }));
|
|
258
|
+
return (_jsx(Tooltip, { content: label, side: "bottom", renderTrigger: (trigger) => (_jsx("button", { ...trigger, type: "button", class: "rf-calendar-nav", "data-direction": direction === 'left' ? 'previous' : 'next', "data-rf-hover-item": true, "aria-label": label, disabled: disabled, mix: [trigger.mix, on('click', () => moveMonth(direction === 'left' ? -1 : 1))], children: _jsx(DateArrow, { direction: direction }) })) }));
|
|
259
259
|
}
|
|
260
260
|
function onDayKey(event, day) {
|
|
261
261
|
let next;
|
|
@@ -300,9 +300,19 @@ export function Calendar(handle) {
|
|
|
300
300
|
event.preventDefault();
|
|
301
301
|
focusDay(next);
|
|
302
302
|
}
|
|
303
|
-
|
|
303
|
+
// As in the reference: side by side, each title is centered over its
|
|
304
|
+
// month, previous sits at the far left and next at the far right. One
|
|
305
|
+
// month, or months stacked, keep the title left and both arrows together
|
|
306
|
+
// on the first month's right.
|
|
307
|
+
const sideBySide = months.length > 1 && orientation !== 'vertical';
|
|
308
|
+
// Stacked months share one weekday row above them, as in the reference;
|
|
309
|
+
// each month's own header stays in its table for screen readers.
|
|
310
|
+
const stacked = months.length > 1 && orientation === 'vertical';
|
|
311
|
+
const previousDisabled = firstMonth <= monthStart(min);
|
|
312
|
+
const nextDisabled = firstMonth >= monthStart(max);
|
|
313
|
+
return (_jsxs("section", { class: "rf-calendar", "aria-label": label, "data-orientation": orientation, "data-week-numbers": showWeekNumbers ? '' : undefined, "data-stacked": stacked ? '' : undefined, style: cellSize && Number.isFinite(cellSize) && cellSize > 0
|
|
304
314
|
? { '--rf-date-cell-size': `${cellSize}px` }
|
|
305
|
-
: undefined, children: [_jsxs("div", { class: "rf-calendar-months", mix: [
|
|
315
|
+
: undefined, children: [stacked && (_jsxs("div", { class: "rf-calendar-weekdays", "aria-hidden": "true", children: [showWeekNumbers && _jsx("span", { class: "rf-calendar-weekdays-week" }), weekdays.map((weekday) => (_jsx("span", { children: weekday.slice(0, 2) }, weekday)))] })), _jsxs("div", { class: "rf-calendar-months", mix: [
|
|
306
316
|
gridRef,
|
|
307
317
|
hover,
|
|
308
318
|
on('pointerleave', () => {
|
|
@@ -318,8 +328,9 @@ export function Calendar(handle) {
|
|
|
318
328
|
const weekCount = Math.ceil((firstDayOffset + daysInMonth) / 7);
|
|
319
329
|
const weeks = Array.from({ length: weekCount }, (_, row) => Array.from({ length: 7 }, (_, column) => addDays(start, row * 7 + column)));
|
|
320
330
|
const monthIso = iso(month).slice(0, 7);
|
|
321
|
-
return (_jsxs("div", { class: "rf-calendar-month", children: [_jsxs("div", { class: "rf-calendar-header", children: [index === 0 && monthButton('left',
|
|
322
|
-
|
|
331
|
+
return (_jsxs("div", { class: "rf-calendar-month", children: [_jsxs("div", { class: "rf-calendar-header", "data-centered": sideBySide ? '' : undefined, children: [sideBySide && index === 0 && monthButton('left', previousDisabled), _jsx("div", { class: "rf-calendar-months-label", "aria-live": index === 0 ? 'polite' : undefined, "aria-atomic": index === 0 ? 'true' : undefined, children: monthLabel.format(month) }), sideBySide
|
|
332
|
+
? index === months.length - 1 && monthButton('right', nextDisabled)
|
|
333
|
+
: index === 0 && (_jsxs(_Fragment, { children: [monthButton('left', previousDisabled), monthButton('right', nextDisabled)] }))] }), _jsxs("table", { class: "rf-calendar-grid", role: "grid", "aria-label": monthLabel.format(month), children: [_jsx("thead", { children: _jsxs("tr", { children: [showWeekNumbers && (_jsx("th", { scope: "col", class: "rf-calendar-week", abbr: "Week number", children: _jsx("span", { class: "rf-basic-sr-only", children: "Week number" }) })), weekdays.map((weekday) => (_jsx("th", { scope: "col", abbr: weekday, children: weekday.slice(0, 2) }, weekday)))] }) }), _jsx("tbody", { children: weeks.map((week) => (_jsxs("tr", { children: [showWeekNumbers && (_jsx("th", { scope: "row", class: "rf-calendar-week", children: getWeekNumber(week[0]) })), week.map((day) => {
|
|
323
334
|
const dayIso = iso(day);
|
|
324
335
|
const outside = !dayIso.startsWith(monthIso);
|
|
325
336
|
if (!dayIso || (months.length > 1 && outside))
|
package/dist/flows.css
CHANGED
|
@@ -141,7 +141,10 @@
|
|
|
141
141
|
rows continue below and goes at the end, which is why the footer draws no
|
|
142
142
|
line of its own. Without a footer the list is the panel's last child and
|
|
143
143
|
that line would double the panel edge, so it is dropped. The lines ride the
|
|
144
|
-
scroll root because the viewport's fade mask would erase them.
|
|
144
|
+
scroll root because the viewport's fade mask would erase them.
|
|
145
|
+
This follows the reference's intent, not its pixels: there the stronger
|
|
146
|
+
line sits on the root's border, which the root's overflow clip hides, so
|
|
147
|
+
its header line never strengthens. Keep the strengthening. */
|
|
145
148
|
.rf-command-scroll::before,
|
|
146
149
|
.rf-command-scroll::after {
|
|
147
150
|
content: '';
|
package/dist/foundations.css
CHANGED
|
@@ -441,38 +441,45 @@
|
|
|
441
441
|
position: absolute;
|
|
442
442
|
top: 50%;
|
|
443
443
|
left: 50%;
|
|
444
|
-
width: var(--rf-
|
|
445
|
-
height: var(--rf-
|
|
444
|
+
width: var(--rf-icon-size);
|
|
445
|
+
height: var(--rf-icon-size);
|
|
446
446
|
transform: translate(-50%, -50%);
|
|
447
447
|
pointer-events: none;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
.rf-button-spinner
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
450
|
+
.rf-button-spinner g {
|
|
451
|
+
transform-box: fill-box;
|
|
452
|
+
transform-origin: center;
|
|
453
|
+
animation: rf-button-spinner-turn 1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.rf-button-spinner circle {
|
|
457
|
+
/* A nonzero dash keeps Safari's round caps visible as dots. */
|
|
458
|
+
stroke-dasharray: 0.01 49.99;
|
|
459
|
+
stroke-dashoffset: -25;
|
|
460
|
+
animation: rf-button-spinner-breathe 1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
|
|
455
461
|
}
|
|
456
462
|
|
|
457
|
-
@keyframes rf-button-spinner {
|
|
463
|
+
@keyframes rf-button-spinner-turn {
|
|
458
464
|
to {
|
|
459
|
-
|
|
465
|
+
transform: rotate(360deg);
|
|
460
466
|
}
|
|
461
467
|
}
|
|
462
468
|
|
|
463
|
-
@keyframes rf-button-spinner-
|
|
469
|
+
@keyframes rf-button-spinner-breathe {
|
|
464
470
|
0%,
|
|
465
471
|
100% {
|
|
466
|
-
stroke-dasharray:
|
|
472
|
+
stroke-dasharray: 0.01 49.99;
|
|
473
|
+
stroke-dashoffset: -25;
|
|
467
474
|
}
|
|
468
475
|
50% {
|
|
469
|
-
stroke-dasharray:
|
|
476
|
+
stroke-dasharray: 26 24;
|
|
477
|
+
stroke-dashoffset: -12;
|
|
470
478
|
}
|
|
471
479
|
}
|
|
472
480
|
|
|
473
481
|
@media (prefers-reduced-motion: reduce) {
|
|
474
|
-
.rf-button-spinner
|
|
475
|
-
stroke-dasharray: none;
|
|
482
|
+
.rf-button-spinner :is(g, circle) {
|
|
476
483
|
animation: none;
|
|
477
484
|
}
|
|
478
485
|
}
|
package/dist/groups.css
CHANGED
|
@@ -243,6 +243,40 @@
|
|
|
243
243
|
.rf-choice-group-list[data-orientation='horizontal'] .rf-choice-group-row {
|
|
244
244
|
border-radius: var(--rf-radius);
|
|
245
245
|
}
|
|
246
|
+
/* Once hydrated, a vertical group draws its checked fill as moving blocks, one
|
|
247
|
+
per run of checked rows, under the hover fill; the rows draw none. */
|
|
248
|
+
.rf-choice-runs {
|
|
249
|
+
position: absolute;
|
|
250
|
+
z-index: -1;
|
|
251
|
+
inset: 0;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
}
|
|
254
|
+
.rf-choice-run {
|
|
255
|
+
position: absolute;
|
|
256
|
+
background: var(--rf-active);
|
|
257
|
+
transition:
|
|
258
|
+
top var(--rf-spring-moderate),
|
|
259
|
+
left var(--rf-spring-moderate),
|
|
260
|
+
width var(--rf-spring-moderate),
|
|
261
|
+
height var(--rf-spring-moderate),
|
|
262
|
+
border-radius var(--rf-spring-moderate) var(--rf-corner-delay, 0ms),
|
|
263
|
+
opacity 80ms ease;
|
|
264
|
+
}
|
|
265
|
+
.rf-choice-run[data-instant] {
|
|
266
|
+
transition: none;
|
|
267
|
+
}
|
|
268
|
+
.rf-choice-run[data-leaving] {
|
|
269
|
+
opacity: 0;
|
|
270
|
+
transition: opacity var(--rf-exit-moderate);
|
|
271
|
+
}
|
|
272
|
+
.rf-choice-group-list[data-rf-selection-blocks] > .rf-choice-group-row:has(input:checked) {
|
|
273
|
+
background: transparent;
|
|
274
|
+
}
|
|
275
|
+
@media (prefers-reduced-motion: reduce) {
|
|
276
|
+
.rf-choice-run:not([data-leaving]) {
|
|
277
|
+
transition: opacity 80ms ease;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
246
280
|
.rf-choice-group-row[aria-disabled='true'] {
|
|
247
281
|
opacity: 0.45;
|
|
248
282
|
}
|
package/dist/groups.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as accordion from '@remix-run/ui/accordion';
|
|
|
4
4
|
import { on } from 'remix/component';
|
|
5
5
|
import { Checkbox } from './basic.js';
|
|
6
6
|
import { fluidHover } from './interactive.js';
|
|
7
|
+
import { selectionRuns } from './selection-runs.js';
|
|
7
8
|
export function Accordion(handle) {
|
|
8
9
|
return () => {
|
|
9
10
|
const { items, variant = 'grouped', highlight = 'item', headingLevel = 3, children: _children, ...props } = handle.props;
|
|
@@ -12,9 +13,16 @@ export function Accordion(handle) {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
export function CheckboxGroup(handle) {
|
|
16
|
+
// The checked fill moves with the selection, as in the reference. A
|
|
17
|
+
// controlled value changes without a change event, so each render resyncs.
|
|
18
|
+
const runs = selectionRuns();
|
|
15
19
|
return () => {
|
|
16
20
|
const { options, label, hideLabel, name, disabled, value, defaultValue, onValueChange, orientation = 'vertical', } = handle.props;
|
|
21
|
+
const vertical = orientation !== 'horizontal';
|
|
22
|
+
if (vertical)
|
|
23
|
+
handle.queueTask(runs.sync);
|
|
17
24
|
return (_jsxs("fieldset", { class: "rf-choice-group", disabled: disabled, children: [_jsx("legend", { class: hideLabel ? 'rf-basic-sr-only' : undefined, children: label }), _jsxs("div", { class: "rf-choice-group-list", "data-orientation": orientation, mix: [
|
|
25
|
+
vertical && runs.mix,
|
|
18
26
|
fluidHover({
|
|
19
27
|
axis: orientation === 'horizontal' ? 'x' : 'y',
|
|
20
28
|
focusSelector: ':has(input:focus-visible)',
|
|
@@ -43,7 +51,7 @@ export function CheckboxGroup(handle) {
|
|
|
43
51
|
const selected = event.currentTarget.querySelectorAll('input[type="checkbox"]:checked');
|
|
44
52
|
onValueChange(Array.from(selected, (input) => input.value));
|
|
45
53
|
}),
|
|
46
|
-
], children: [_jsx("div", { class: "rf-highlight", "aria-hidden": "true", "data-rf-hover-layer": true }), _jsx("div", { class: "rf-highlight rf-focus-ring", "aria-hidden": "true", "data-rf-focus-layer": true }), options.map((option) => (_jsx("div", { class: "rf-choice-group-row", "data-rf-hover-item": true, "aria-disabled": disabled || option.disabled || undefined, children: _jsx(Checkbox, { name: name, value: option.value, disabled: option.disabled, checked: value?.includes(option.value), defaultChecked: value === undefined ? defaultValue?.includes(option.value) : undefined, label: _jsxs(_Fragment, { children: [option.label, option.description && _jsx("small", { children: option.description })] }) }) }, option.value)))] })] }));
|
|
54
|
+
], children: [vertical && _jsx("div", { class: "rf-choice-runs", "aria-hidden": "true", "data-rf-selection-runs": true }), _jsx("div", { class: "rf-highlight", "aria-hidden": "true", "data-rf-hover-layer": true }), _jsx("div", { class: "rf-highlight rf-focus-ring", "aria-hidden": "true", "data-rf-focus-layer": true }), options.map((option) => (_jsx("div", { class: "rf-choice-group-row", "data-rf-hover-item": true, "aria-disabled": disabled || option.disabled || undefined, children: _jsx(Checkbox, { name: name, value: option.value, disabled: option.disabled, checked: value?.includes(option.value), defaultChecked: value === undefined ? defaultValue?.includes(option.value) : undefined, label: _jsxs(_Fragment, { children: [option.label, option.description && _jsx("small", { children: option.description })] }) }) }, option.value)))] })] }));
|
|
47
55
|
};
|
|
48
56
|
}
|
|
49
57
|
export function RadioGroup(handle) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Handle, type MixInput, type RemixNode } from 'remix/component';
|
|
2
|
+
import type { PopoverPlacement } from './popover.tsx';
|
|
3
|
+
export interface HoverCardTriggerProps {
|
|
4
|
+
mix: MixInput<HTMLElement>;
|
|
5
|
+
}
|
|
6
|
+
export interface HoverCardProps {
|
|
7
|
+
/** The card's content. */
|
|
8
|
+
children?: RemixNode;
|
|
9
|
+
/** Renders the trigger, usually a link. Spread the provided props onto it. */
|
|
10
|
+
renderTrigger: (props: HoverCardTriggerProps) => RemixNode;
|
|
11
|
+
open?: boolean;
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
onOpenChange?: (open: boolean) => void;
|
|
14
|
+
/** Which side of the trigger the card opens on. Default bottom. */
|
|
15
|
+
placement?: PopoverPlacement;
|
|
16
|
+
/** Distance from the trigger in pixels. Default 8. */
|
|
17
|
+
offset?: number;
|
|
18
|
+
/** Milliseconds the pointer rests on the trigger, or focus stays on it, before the card opens. Default 500. */
|
|
19
|
+
openDelay?: number;
|
|
20
|
+
/** Milliseconds after the pointer leaves the trigger and the card before it closes. Default 200. */
|
|
21
|
+
closeDelay?: number;
|
|
22
|
+
size?: 'default' | 'compact';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A preview card for a link or a name, such as a profile or a page, shown
|
|
26
|
+
* while the pointer rests on its trigger or keyboard focus is on it. It is
|
|
27
|
+
* supplementary: the trigger still works on its own, a tap on a touch screen
|
|
28
|
+
* follows it, and the card never takes focus or locks scrolling. The pointer
|
|
29
|
+
* can cross into the card and use its links; it closes once the pointer has
|
|
30
|
+
* left both, or on Escape. It shares Popover's surface and motion.
|
|
31
|
+
*/
|
|
32
|
+
export declare function HoverCard(handle: Handle<HoverCardProps>): () => import("remix/component").RemixElement;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "remix/component/jsx-runtime";
|
|
2
|
+
import { ref } from 'remix/component';
|
|
3
|
+
import { anchor } from '@remix-run/ui/anchor';
|
|
4
|
+
/**
|
|
5
|
+
* A preview card for a link or a name, such as a profile or a page, shown
|
|
6
|
+
* while the pointer rests on its trigger or keyboard focus is on it. It is
|
|
7
|
+
* supplementary: the trigger still works on its own, a tap on a touch screen
|
|
8
|
+
* follows it, and the card never takes focus or locks scrolling. The pointer
|
|
9
|
+
* can cross into the card and use its links; it closes once the pointer has
|
|
10
|
+
* left both, or on Escape. It shares Popover's surface and motion.
|
|
11
|
+
*/
|
|
12
|
+
export function HoverCard(handle) {
|
|
13
|
+
let trigger = null;
|
|
14
|
+
let card = null;
|
|
15
|
+
let localOpen = handle.props.defaultOpen ?? false;
|
|
16
|
+
let stopAnchor;
|
|
17
|
+
let openTimer;
|
|
18
|
+
let closeTimer;
|
|
19
|
+
const isOpen = () => handle.props.open ?? localOpen;
|
|
20
|
+
function setOpen(next) {
|
|
21
|
+
clearTimeout(openTimer);
|
|
22
|
+
clearTimeout(closeTimer);
|
|
23
|
+
if (isOpen() === next)
|
|
24
|
+
return;
|
|
25
|
+
localOpen = next;
|
|
26
|
+
handle.props.onOpenChange?.(next);
|
|
27
|
+
void handle.update();
|
|
28
|
+
}
|
|
29
|
+
function scheduleOpen() {
|
|
30
|
+
clearTimeout(closeTimer);
|
|
31
|
+
if (isOpen())
|
|
32
|
+
return;
|
|
33
|
+
clearTimeout(openTimer);
|
|
34
|
+
openTimer = setTimeout(() => setOpen(true), Math.max(0, handle.props.openDelay ?? 500));
|
|
35
|
+
}
|
|
36
|
+
function scheduleClose() {
|
|
37
|
+
clearTimeout(openTimer);
|
|
38
|
+
clearTimeout(closeTimer);
|
|
39
|
+
if (!isOpen())
|
|
40
|
+
return;
|
|
41
|
+
closeTimer = setTimeout(() => setOpen(false), Math.max(0, handle.props.closeDelay ?? 200));
|
|
42
|
+
}
|
|
43
|
+
// Shows or hides the card to match the open state, after each render.
|
|
44
|
+
function sync() {
|
|
45
|
+
if (!trigger?.isConnected || !card?.isConnected)
|
|
46
|
+
return;
|
|
47
|
+
const showing = card.matches(':popover-open');
|
|
48
|
+
if (isOpen() && !showing) {
|
|
49
|
+
card.showPopover?.();
|
|
50
|
+
stopAnchor = anchor(card, trigger, {
|
|
51
|
+
placement: handle.props.placement ?? 'bottom',
|
|
52
|
+
offset: handle.props.offset ?? 8,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (!isOpen() && showing) {
|
|
56
|
+
stopAnchor?.();
|
|
57
|
+
stopAnchor = undefined;
|
|
58
|
+
if (card.contains(document.activeElement))
|
|
59
|
+
trigger.focus({ preventScroll: true });
|
|
60
|
+
card.hidePopover?.();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const triggerMix = ref((element, signal) => {
|
|
64
|
+
if (!(element instanceof HTMLElement))
|
|
65
|
+
return;
|
|
66
|
+
trigger = element;
|
|
67
|
+
const options = { signal };
|
|
68
|
+
element.addEventListener('pointerenter', (event) => {
|
|
69
|
+
if (event.pointerType !== 'touch')
|
|
70
|
+
scheduleOpen();
|
|
71
|
+
}, options);
|
|
72
|
+
element.addEventListener('pointerleave', (event) => {
|
|
73
|
+
if (event.pointerType !== 'touch')
|
|
74
|
+
scheduleClose();
|
|
75
|
+
}, options);
|
|
76
|
+
element.addEventListener('focus', scheduleOpen, options);
|
|
77
|
+
element.addEventListener('blur', (event) => {
|
|
78
|
+
// Tabbing on into the card keeps it open.
|
|
79
|
+
if (!(event.relatedTarget instanceof Node && card?.contains(event.relatedTarget)))
|
|
80
|
+
scheduleClose();
|
|
81
|
+
}, options);
|
|
82
|
+
element.ownerDocument.addEventListener('keydown', (event) => {
|
|
83
|
+
if (event.key !== 'Escape' || !isOpen())
|
|
84
|
+
return;
|
|
85
|
+
setOpen(false);
|
|
86
|
+
}, options);
|
|
87
|
+
signal.addEventListener('abort', () => {
|
|
88
|
+
clearTimeout(openTimer);
|
|
89
|
+
clearTimeout(closeTimer);
|
|
90
|
+
stopAnchor?.();
|
|
91
|
+
if (trigger === element)
|
|
92
|
+
trigger = null;
|
|
93
|
+
}, { once: true });
|
|
94
|
+
});
|
|
95
|
+
const cardMix = ref((element, signal) => {
|
|
96
|
+
if (!(element instanceof HTMLElement))
|
|
97
|
+
return;
|
|
98
|
+
card = element;
|
|
99
|
+
const options = { signal };
|
|
100
|
+
element.addEventListener('pointerenter', () => clearTimeout(closeTimer), options);
|
|
101
|
+
element.addEventListener('pointerleave', (event) => {
|
|
102
|
+
if (event.pointerType !== 'touch')
|
|
103
|
+
scheduleClose();
|
|
104
|
+
}, options);
|
|
105
|
+
element.addEventListener('focusout', (event) => {
|
|
106
|
+
const next = event.relatedTarget;
|
|
107
|
+
if (!(next instanceof Node && (element.contains(next) || next === trigger)))
|
|
108
|
+
scheduleClose();
|
|
109
|
+
}, options);
|
|
110
|
+
signal.addEventListener('abort', () => {
|
|
111
|
+
if (card === element)
|
|
112
|
+
card = null;
|
|
113
|
+
}, { once: true });
|
|
114
|
+
});
|
|
115
|
+
return () => {
|
|
116
|
+
handle.queueTask(sync);
|
|
117
|
+
return (_jsxs(_Fragment, { children: [handle.props.renderTrigger({ mix: triggerMix }), _jsx("div", { class: "rf-menu-surface rf-popover-surface rf-hover-card", popover: "manual", "data-density": handle.props.size, mix: cardMix, children: handle.props.children })] }));
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/icon-data.d.ts
CHANGED
|
@@ -281,6 +281,19 @@ export declare const defaultIcons: {
|
|
|
281
281
|
}], readonly ['path', {
|
|
282
282
|
readonly d: 'M12 5v14';
|
|
283
283
|
}]];
|
|
284
|
+
readonly pause: readonly [readonly ['rect', {
|
|
285
|
+
readonly x: '14';
|
|
286
|
+
readonly y: '3';
|
|
287
|
+
readonly width: '5';
|
|
288
|
+
readonly height: '18';
|
|
289
|
+
readonly rx: '1';
|
|
290
|
+
}], readonly ['rect', {
|
|
291
|
+
readonly x: '5';
|
|
292
|
+
readonly y: '3';
|
|
293
|
+
readonly width: '5';
|
|
294
|
+
readonly height: '18';
|
|
295
|
+
readonly rx: '1';
|
|
296
|
+
}]];
|
|
284
297
|
readonly 'rotate-ccw': readonly [readonly ['path', {
|
|
285
298
|
readonly d: 'M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8';
|
|
286
299
|
}], readonly ['path', {
|
package/dist/icon-data.js
CHANGED
|
@@ -246,6 +246,10 @@ export const defaultIcons = {
|
|
|
246
246
|
['path', { d: 'M5 12h14' }],
|
|
247
247
|
['path', { d: 'M12 5v14' }],
|
|
248
248
|
],
|
|
249
|
+
pause: [
|
|
250
|
+
['rect', { x: '14', y: '3', width: '5', height: '18', rx: '1' }],
|
|
251
|
+
['rect', { x: '5', y: '3', width: '5', height: '18', rx: '1' }],
|
|
252
|
+
],
|
|
249
253
|
'rotate-ccw': [
|
|
250
254
|
['path', { d: 'M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8' }],
|
|
251
255
|
['path', { d: 'M3 3v5h5' }],
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export * from './flows.tsx';
|
|
|
10
10
|
export * from './range-color.tsx';
|
|
11
11
|
export * from './color-picker.tsx';
|
|
12
12
|
export * from './breadcrumb.tsx';
|
|
13
|
+
export * from './segmented-control.tsx';
|
|
14
|
+
export * from './popover.tsx';
|
|
15
|
+
export * from './hover-card.tsx';
|
|
13
16
|
export * from './systems.ts';
|
|
14
17
|
export * from './queued-stack.tsx';
|
|
15
18
|
export * from './scroll-area.tsx';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,9 @@ export * from './flows.js';
|
|
|
10
10
|
export * from './range-color.js';
|
|
11
11
|
export * from './color-picker.js';
|
|
12
12
|
export * from './breadcrumb.js';
|
|
13
|
+
export * from './segmented-control.js';
|
|
14
|
+
export * from './popover.js';
|
|
15
|
+
export * from './hover-card.js';
|
|
13
16
|
export * from './systems.js';
|
|
14
17
|
export * from './queued-stack.js';
|
|
15
18
|
export * from './scroll-area.js';
|
package/dist/interactive.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Handle, RemixNode } from 'remix/component';
|
|
1
|
+
import type { Handle, MixInput, RemixNode } from 'remix/component';
|
|
2
2
|
export interface FluidHoverOptions {
|
|
3
3
|
/** Track the pointer on one axis or across a two-dimensional grid. */
|
|
4
4
|
axis?: 'x' | 'y' | 'both';
|
|
@@ -62,6 +62,8 @@ export interface DropdownProps {
|
|
|
62
62
|
/** `bare` renders an unstyled button trigger that takes its look from `triggerClass`, for sidebar rows and actions. */
|
|
63
63
|
variant?: 'outline' | 'ghost' | 'bare';
|
|
64
64
|
triggerClass?: string;
|
|
65
|
+
/** Extra behavior on the trigger, such as a Tooltip's trigger props. */
|
|
66
|
+
triggerMix?: MixInput;
|
|
65
67
|
/** Which side of the trigger the menu opens on. Default bottom. */
|
|
66
68
|
side?: 'top' | 'bottom';
|
|
67
69
|
searchable?: boolean;
|
package/dist/interactive.js
CHANGED
|
@@ -376,12 +376,14 @@ export function Dropdown(handle) {
|
|
|
376
376
|
offset: 6,
|
|
377
377
|
}),
|
|
378
378
|
hoverMix,
|
|
379
|
+
handle.props.triggerMix,
|
|
379
380
|
], children: handle.props.triggerContent ?? label })), !handle.props.inline && variant !== 'bare' && (_jsx(Button, { class: "rf-dropdown-trigger", variant: variant === 'ghost' ? 'ghost' : 'tertiary', size: size, trailingIcon: handle.props.chevron === false ? undefined : (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round" }) })), mix: [
|
|
380
381
|
menu.trigger({
|
|
381
382
|
placement: `${handle.props.side ?? 'bottom'}-${align}`,
|
|
382
383
|
offset: 6,
|
|
383
384
|
}),
|
|
384
385
|
hoverMix,
|
|
386
|
+
handle.props.triggerMix,
|
|
385
387
|
], children: handle.props.triggerContent ?? label })), _jsx(DropdownSurface, { inline: handle.props.inline, hover: hovering ? { control: hover, closeDelay } : undefined, items: displayedItems, emptyMessage: handle.props.emptyMessage, search: searchable || handle.props.creatable
|
|
386
388
|
? { query, setQuery, placeholder: searchPlaceholder }
|
|
387
389
|
: undefined })] }) }));
|
|
@@ -517,7 +519,7 @@ function DropdownSurface(handle) {
|
|
|
517
519
|
? search.query.slice(0, -1)
|
|
518
520
|
: search.query + event.key);
|
|
519
521
|
}, true),
|
|
520
|
-
], children: [_jsx("span", { class: "rf-highlight rf-menu-hover", "aria-hidden": "true", "data-rf-hover-layer": true }),
|
|
522
|
+
], children: [_jsx("span", { class: "rf-highlight rf-menu-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 }), items.length === 0 && search && (_jsx("div", { class: "rf-menu-empty", role: "status", children: handle.props.emptyMessage ?? 'No results' })), items.map((item, index) => (_jsxs("div", { role: "none", class: "rf-menu-entry", children: [item.separatorBefore && (index > 0 || !normalized) && (_jsx("div", { class: "rf-menu-separator", role: "separator" })), item.group && item.group !== items[index - 1]?.group && (_jsx("div", { class: "rf-menu-group-label", children: item.group })), item.children && item.children.length > 0 ? (_jsxs(menu.Context, { label: item.label, children: [_jsx("div", { class: "rf-menu-item", "data-rf-hover-item": true, mix: menu.submenuTrigger({
|
|
521
523
|
value: item.value,
|
|
522
524
|
label: item.label,
|
|
523
525
|
disabled: item.disabled,
|
package/dist/overlays.js
CHANGED
|
@@ -134,10 +134,14 @@ export function Tooltip(handle) {
|
|
|
134
134
|
internalOpen = next;
|
|
135
135
|
handle.props.onOpenChange?.(next);
|
|
136
136
|
}
|
|
137
|
+
function menuIsOpen() {
|
|
138
|
+
return (trigger?.getAttribute('aria-haspopup') === 'menu' &&
|
|
139
|
+
trigger.getAttribute('aria-expanded') === 'true');
|
|
140
|
+
}
|
|
137
141
|
// The tooltip is shown when `open` forces it, otherwise when hover or focus asks for it.
|
|
138
142
|
function sync() {
|
|
139
143
|
const visible = handle.props.open ?? internalOpen;
|
|
140
|
-
if (visible)
|
|
144
|
+
if (visible && !menuIsOpen())
|
|
141
145
|
open();
|
|
142
146
|
else
|
|
143
147
|
close();
|
|
@@ -150,7 +154,7 @@ export function Tooltip(handle) {
|
|
|
150
154
|
}
|
|
151
155
|
function show() {
|
|
152
156
|
clearTimeout(hideTimer);
|
|
153
|
-
if (!trigger?.isConnected || !popup?.isConnected || dismissed)
|
|
157
|
+
if (!trigger?.isConnected || !popup?.isConnected || dismissed || menuIsOpen())
|
|
154
158
|
return;
|
|
155
159
|
setInternal(true);
|
|
156
160
|
sync();
|
|
@@ -199,6 +203,14 @@ export function Tooltip(handle) {
|
|
|
199
203
|
if (!(element instanceof HTMLElement))
|
|
200
204
|
return;
|
|
201
205
|
trigger = element;
|
|
206
|
+
if (element.getAttribute('aria-haspopup') === 'menu') {
|
|
207
|
+
const observer = new MutationObserver(() => {
|
|
208
|
+
if (menuIsOpen())
|
|
209
|
+
hide();
|
|
210
|
+
});
|
|
211
|
+
observer.observe(element, { attributeFilter: ['aria-expanded'] });
|
|
212
|
+
signal.addEventListener('abort', () => observer.disconnect(), { once: true });
|
|
213
|
+
}
|
|
202
214
|
element.addEventListener('pointerenter', (event) => {
|
|
203
215
|
if (event.pointerType === 'touch')
|
|
204
216
|
return;
|