@rio-m3e/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,296 @@
1
+ // src/utils/object.ts
2
+ /**
3
+ * A copy without the properties that are explicitly `undefined`, so spreading it over defaults
4
+ * keeps the defaults: `{ ...defaults, ...omitUndefined(props) }`.
5
+ */
6
+ function omitUndefined(object) {
7
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
8
+ }
9
+ // src/button-group/button-group.ts
10
+ /**
11
+ * M3E button group variants. A standard group spaces its buttons and lets a pressed button push
12
+ * its neighbors; a connected group joins them with 2px gaps and shared corners, and fills the
13
+ * width it is given.
14
+ */
15
+ const BUTTON_GROUP_VARIANTS = ["standard", "connected"];
16
+ /** Whether the group's buttons can be selected: none, one at a time, or any number. */
17
+ const BUTTON_GROUP_SELECTIONS = [
18
+ "none",
19
+ "single",
20
+ "multiple"
21
+ ];
22
+ const buttonGroupDefaults = {
23
+ variant: "standard",
24
+ size: "small",
25
+ shape: "round",
26
+ selection: "none",
27
+ required: false
28
+ };
29
+ /** How the adapter renders the button group: one root, with the buttons as its direct children. */
30
+ const buttonGroupSkeleton = {
31
+ part: "root",
32
+ tag: "div",
33
+ content: true
34
+ };
35
+ const valuesOf$1 = (selected) => selected === void 0 ? [] : Array.isArray(selected) ? selected : [selected];
36
+ function createButtonGroup(options) {
37
+ const props = () => ({
38
+ ...buttonGroupDefaults,
39
+ ...omitUndefined(options.props())
40
+ });
41
+ const isSelected = (value) => {
42
+ const p = props();
43
+ return p.selection !== "none" && valuesOf$1(p.selected).includes(value);
44
+ };
45
+ return {
46
+ parts: { root() {
47
+ const p = props();
48
+ return {
49
+ class: "rio-button-group",
50
+ role: "group",
51
+ "data-variant": p.variant,
52
+ "data-size": p.size,
53
+ "data-shape": p.shape
54
+ };
55
+ } },
56
+ size: () => props().size,
57
+ shape: () => props().shape,
58
+ selection: () => props().selection,
59
+ isSelected,
60
+ select(value, selected) {
61
+ const p = props();
62
+ if (p.selection === "none" || isSelected(value) === selected) return;
63
+ const current = valuesOf$1(p.selected);
64
+ if (!selected && p.required && current.length === 1) return;
65
+ if (p.selection === "single") options.onChange(selected ? value : void 0);
66
+ else options.onChange(selected ? [...current, value] : current.filter((other) => other !== value));
67
+ }
68
+ };
69
+ }
70
+ // src/primitives/spring/spring.ts
71
+ /**
72
+ * Where a spring is `t` seconds after it starts at `from` with `velocity`, heading to `to`.
73
+ */
74
+ function solveSpring({ dampingRatio: zeta, stiffness }, from, to, velocity, t) {
75
+ const w0 = Math.sqrt(stiffness);
76
+ const x0 = from - to;
77
+ const v0 = velocity;
78
+ if (Math.abs(zeta - 1) < 1e-6) {
79
+ const b = v0 + w0 * x0;
80
+ const decay = Math.exp(-w0 * t);
81
+ return {
82
+ value: to + (x0 + b * t) * decay,
83
+ velocity: (b - w0 * (x0 + b * t)) * decay
84
+ };
85
+ }
86
+ if (zeta < 1) {
87
+ const a = zeta * w0;
88
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
89
+ const b = (v0 + a * x0) / wd;
90
+ const decay = Math.exp(-a * t);
91
+ const cos = Math.cos(wd * t);
92
+ const sin = Math.sin(wd * t);
93
+ return {
94
+ value: to + decay * (x0 * cos + b * sin),
95
+ velocity: decay * ((wd * b - a * x0) * cos - (a * b + wd * x0) * sin)
96
+ };
97
+ }
98
+ const root = w0 * Math.sqrt(zeta * zeta - 1);
99
+ const r1 = -zeta * w0 + root;
100
+ const r2 = -zeta * w0 - root;
101
+ const c2 = (v0 - r1 * x0) / (r2 - r1);
102
+ const c1 = x0 - c2;
103
+ const e1 = Math.exp(r1 * t);
104
+ const e2 = Math.exp(r2 * t);
105
+ return {
106
+ value: to + c1 * e1 + c2 * e2,
107
+ velocity: r1 * c1 * e1 + r2 * c2 * e2
108
+ };
109
+ }
110
+ const now = () => performance.now() / 1e3;
111
+ /** A spring that animates a single number with `requestAnimationFrame`. */
112
+ function createSpring(options) {
113
+ const precision = options.precision ?? .01;
114
+ let from = options.value;
115
+ let target = options.value;
116
+ let startVelocity = 0;
117
+ let params;
118
+ let startedAt = 0;
119
+ let frame;
120
+ const state = () => params && frame !== void 0 ? solveSpring(params, from, target, startVelocity, now() - startedAt) : {
121
+ value: target,
122
+ velocity: 0
123
+ };
124
+ const settle = () => {
125
+ if (frame !== void 0) cancelAnimationFrame(frame);
126
+ frame = void 0;
127
+ from = target;
128
+ startVelocity = 0;
129
+ options.onUpdate(target);
130
+ options.onRest?.();
131
+ };
132
+ const tick = () => {
133
+ const { value, velocity } = state();
134
+ if (Math.abs(value - target) < precision && Math.abs(velocity) < precision * 10) {
135
+ settle();
136
+ return;
137
+ }
138
+ options.onUpdate(value);
139
+ frame = requestAnimationFrame(tick);
140
+ };
141
+ return {
142
+ get value() {
143
+ return state().value;
144
+ },
145
+ get velocity() {
146
+ return state().velocity;
147
+ },
148
+ get target() {
149
+ return target;
150
+ },
151
+ get moving() {
152
+ return frame !== void 0;
153
+ },
154
+ to(next, nextParams, velocity) {
155
+ const current = state();
156
+ from = current.value;
157
+ startVelocity = velocity ?? current.velocity;
158
+ target = next;
159
+ params = nextParams;
160
+ startedAt = now();
161
+ if (frame === void 0) frame = requestAnimationFrame(tick);
162
+ },
163
+ jump(value) {
164
+ target = value;
165
+ settle();
166
+ },
167
+ stop() {
168
+ const { value } = state();
169
+ if (frame !== void 0) cancelAnimationFrame(frame);
170
+ frame = void 0;
171
+ from = target = value;
172
+ startVelocity = 0;
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * Whether the user asked for reduced motion. Spatial springs should jump straight to their target
178
+ * (or be skipped) when it is on; see `docs/foundations.md`, section 3.
179
+ */
180
+ function prefersReducedMotion() {
181
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
182
+ }
183
+ /**
184
+ * Reads a motion token's spring from CSS variables on an element, for example
185
+ * `--rio-motion-fast-spatial-damping` and `-stiffness`. Reading them from the element, rather than
186
+ * from the tokens package, lets a subtree that swaps motion schemes change its springs too.
187
+ * Returns `undefined` when the theme doesn't define them.
188
+ */
189
+ function readSpringParams(element, token) {
190
+ const style = getComputedStyle(element);
191
+ const dampingRatio = parseFloat(style.getPropertyValue(`--rio-motion-${token}-damping`));
192
+ const stiffness = parseFloat(style.getPropertyValue(`--rio-motion-${token}-stiffness`));
193
+ return Number.isFinite(dampingRatio) && Number.isFinite(stiffness) ? {
194
+ dampingRatio,
195
+ stiffness
196
+ } : void 0;
197
+ }
198
+ // src/button-group/interaction.ts
199
+ const PRESSED_WIDTH_RATIO = .15;
200
+ function attachButtonGroup(root, options = {}) {
201
+ const ratio = options.pressedWidthRatio ?? PRESSED_WIDTH_RATIO;
202
+ const items = () => [...root.children].map((el) => el.classList.contains("rio-tooltip-trigger") ? el.firstElementChild : el).filter((el) => el instanceof HTMLElement);
203
+ const onKeydown = (event) => {
204
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
205
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
206
+ const focusable = items().filter((el) => !el.matches(":disabled") && el.tabIndex >= 0);
207
+ const index = focusable.findIndex((el) => el.contains(document.activeElement));
208
+ if (index === -1) return;
209
+ const next = focusable[index + (event.key === "ArrowRight" !== (getComputedStyle(root).direction === "rtl") ? 1 : -1)];
210
+ if (!next) return;
211
+ event.preventDefault();
212
+ next.focus();
213
+ };
214
+ root.addEventListener("keydown", onKeydown);
215
+ const springs = /* @__PURE__ */ new Map();
216
+ const natural = /* @__PURE__ */ new Map();
217
+ const setWidth = (el, value) => value === void 0 ? el.style.removeProperty("inline-size") : el.style.setProperty("inline-size", `${value}px`);
218
+ const springOf = (el) => {
219
+ let spring = springs.get(el);
220
+ if (!spring) {
221
+ spring = createSpring({
222
+ value: natural.get(el),
223
+ onUpdate: (value) => setWidth(el, value),
224
+ onRest: () => {
225
+ if (springs.get(el)?.target === natural.get(el)) setWidth(el, void 0);
226
+ }
227
+ });
228
+ springs.set(el, spring);
229
+ }
230
+ return spring;
231
+ };
232
+ /**
233
+ * A button's own width, without the width a spring wrote on it. The inline width comes off and
234
+ * goes back within the same task, so it never shows.
235
+ */
236
+ const measure = (el) => {
237
+ const inline = el.style.inlineSize;
238
+ el.style.removeProperty("inline-size");
239
+ const width = el.getBoundingClientRect().width;
240
+ el.style.setProperty("inline-size", inline);
241
+ return width;
242
+ };
243
+ /** Sets every button's target width. `instant` skips the springs, for changes of size. */
244
+ const layout = (instant = false) => {
245
+ const all = items();
246
+ for (const el of all) natural.set(el, measure(el));
247
+ const targets = new Map(all.map((el) => [el, natural.get(el)]));
248
+ const pressed = all.findIndex((el) => el.hasAttribute("data-pressed"));
249
+ const standard = root.getAttribute("data-variant") !== "connected";
250
+ if (pressed !== -1 && standard && !prefersReducedMotion()) {
251
+ const growth = natural.get(all[pressed]) * ratio;
252
+ const neighbors = [all[pressed - 1], all[pressed + 1]].filter((el) => el !== void 0);
253
+ let gained = 0;
254
+ for (const el of neighbors) {
255
+ const width = natural.get(el);
256
+ const given = Math.min(growth / neighbors.length, width * ratio);
257
+ targets.set(el, width - given);
258
+ gained += given;
259
+ }
260
+ targets.set(all[pressed], natural.get(all[pressed]) + gained);
261
+ }
262
+ const params = readSpringParams(root, "fast-spatial");
263
+ for (const [el, target] of targets) {
264
+ const spring = springOf(el);
265
+ if (!el.style.inlineSize && spring.value !== natural.get(el)) spring.jump(natural.get(el));
266
+ if (spring.target === target) continue;
267
+ if (params && !instant && !prefersReducedMotion()) spring.to(target, params);
268
+ else spring.jump(target);
269
+ }
270
+ };
271
+ const observer = new MutationObserver((mutations) => {
272
+ const current = items();
273
+ const own = mutations.filter((m) => current.includes(m.target));
274
+ if (own.length > 0) layout(own.some((m) => m.attributeName !== "data-pressed"));
275
+ });
276
+ observer.observe(root, {
277
+ subtree: true,
278
+ attributes: true,
279
+ attributeFilter: [
280
+ "data-pressed",
281
+ "data-size",
282
+ "data-width"
283
+ ]
284
+ });
285
+ return { detach() {
286
+ root.removeEventListener("keydown", onKeydown);
287
+ observer.disconnect();
288
+ for (const [el, spring] of springs) {
289
+ spring.stop();
290
+ setWidth(el, void 0);
291
+ }
292
+ } };
293
+ }
1
294
  // src/button/button.ts
2
295
  const BUTTON_VARIANTS = [
3
296
  "filled",
@@ -24,28 +317,63 @@ const buttonDefaults = {
24
317
  softDisabled: false,
25
318
  type: "button"
26
319
  };
320
+ /** How the adapter renders the button's parts. The icon holds the leading icon. */
321
+ const buttonSkeleton = {
322
+ part: "root",
323
+ tag: "button",
324
+ children: [
325
+ {
326
+ part: "touch",
327
+ tag: "span"
328
+ },
329
+ {
330
+ part: "icon",
331
+ tag: "span",
332
+ when: "showsIcon",
333
+ content: true
334
+ },
335
+ {
336
+ part: "label",
337
+ tag: "span",
338
+ content: true
339
+ }
340
+ ]
341
+ };
27
342
  const isToggle = (p) => !!p.toggle && p.variant !== "text";
28
343
  /** Defaults under the given props, skipping props that are explicitly `undefined`. */
29
344
  const resolve = (props) => ({
30
345
  ...buttonDefaults,
31
- ...Object.fromEntries(Object.entries(props).filter(([, v]) => v !== void 0))
346
+ ...omitUndefined(props)
32
347
  });
33
348
  function createButton(options) {
34
349
  return {
35
- rootAttrs() {
36
- const p = resolve(options.props());
37
- const toggle = isToggle(p);
38
- return {
39
- type: p.type,
40
- disabled: p.disabled ? "" : void 0,
41
- "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
42
- "aria-pressed": toggle ? String(p.selected) : void 0,
43
- "data-variant": p.variant,
44
- "data-size": p.size,
45
- "data-shape": p.shape,
46
- "data-selected": toggle ? String(p.selected) : void 0
47
- };
350
+ parts: {
351
+ root() {
352
+ const p = resolve(options.props());
353
+ const toggle = isToggle(p);
354
+ return {
355
+ class: "rio-button rio-state-layer rio-focus-ring",
356
+ type: p.type,
357
+ disabled: p.disabled ? "" : void 0,
358
+ "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
359
+ "aria-pressed": toggle ? String(p.selected) : void 0,
360
+ "data-variant": p.variant,
361
+ "data-size": p.size,
362
+ "data-shape": p.shape,
363
+ "data-selected": toggle ? String(p.selected) : void 0
364
+ };
365
+ },
366
+ touch: () => ({
367
+ class: "rio-button__touch",
368
+ "aria-hidden": "true"
369
+ }),
370
+ icon: () => ({
371
+ class: "rio-button__icon",
372
+ "aria-hidden": "true"
373
+ }),
374
+ label: () => ({ class: "rio-button__label" })
48
375
  },
376
+ showsIcon: () => !!options.content?.().icon,
49
377
  handleClick(event) {
50
378
  const p = options.props();
51
379
  if (p.disabled || p.softDisabled) {
@@ -53,7 +381,449 @@ function createButton(options) {
53
381
  return;
54
382
  }
55
383
  options.onClick(event);
56
- if (isToggle(p)) options.onSelectedChange(!p.selected);
384
+ if (isToggle(p)) options.onChange(!p.selected);
385
+ }
386
+ };
387
+ }
388
+ // src/dialog/dialog.ts
389
+ /**
390
+ * The dialog's ARIA role: `alertdialog` for a prompt that asks for a response, such as a
391
+ * confirmation, and `dialog` for one that holds a list or a form.
392
+ */
393
+ const DIALOG_ROLES = ["alertdialog", "dialog"];
394
+ const dialogDefaults = {
395
+ open: false,
396
+ role: "alertdialog"
397
+ };
398
+ /**
399
+ * The markup of an open dialog. The parts inside render from opening until `attachDialog`'s
400
+ * `onClosed`; a closed dialog is the empty `<dialog>`.
401
+ */
402
+ const dialogSkeleton = {
403
+ part: "root",
404
+ tag: "dialog",
405
+ children: [
406
+ {
407
+ part: "icon",
408
+ tag: "div",
409
+ when: "showsIcon",
410
+ content: true
411
+ },
412
+ {
413
+ part: "headline",
414
+ tag: "h2",
415
+ when: "showsHeadline",
416
+ content: true
417
+ },
418
+ {
419
+ part: "content",
420
+ tag: "div",
421
+ content: true
422
+ },
423
+ {
424
+ part: "actions",
425
+ tag: "div",
426
+ when: "showsActions",
427
+ content: true
428
+ }
429
+ ]
430
+ };
431
+ const NO_CONTENT$2 = {
432
+ icon: false,
433
+ headline: false,
434
+ actions: false
435
+ };
436
+ function createDialog(options) {
437
+ const props = () => ({
438
+ ...dialogDefaults,
439
+ ...omitUndefined(options.props())
440
+ });
441
+ const content = () => options.content?.() ?? NO_CONTENT$2;
442
+ const showsIcon = () => content().icon;
443
+ const showsHeadline = () => content().headline;
444
+ const showsActions = () => content().actions;
445
+ const { ids } = options;
446
+ return {
447
+ parts: {
448
+ root() {
449
+ const alert = props().role === "alertdialog";
450
+ return {
451
+ class: "rio-dialog",
452
+ role: alert ? "alertdialog" : void 0,
453
+ "aria-labelledby": showsHeadline() ? ids.headline : void 0,
454
+ "aria-describedby": alert ? ids.content : void 0,
455
+ "data-icon": showsIcon() ? "" : void 0
456
+ };
457
+ },
458
+ icon: () => ({
459
+ class: "rio-dialog__icon",
460
+ "aria-hidden": "true"
461
+ }),
462
+ headline: () => ({
463
+ class: "rio-dialog__headline",
464
+ id: ids.headline
465
+ }),
466
+ content: () => ({
467
+ class: "rio-dialog__content rio-focus-ring",
468
+ id: ids.content,
469
+ "data-focus-ring": "inner"
470
+ }),
471
+ actions: () => ({ class: "rio-dialog__actions" })
472
+ },
473
+ showsIcon,
474
+ showsHeadline,
475
+ showsActions
476
+ };
477
+ }
478
+ // src/primitives/overlay/overlay.ts
479
+ const registries = /* @__PURE__ */ new WeakMap();
480
+ /** What a `contain`ed overlay keeps in */
481
+ const PRESSES = [
482
+ "click",
483
+ "auxclick",
484
+ "dblclick",
485
+ "pointerdown",
486
+ "pointerup",
487
+ "mousedown",
488
+ "mouseup",
489
+ "contextmenu"
490
+ ];
491
+ const stopPropagation = (event) => event.stopPropagation();
492
+ const isOpen = ({ element, options }) => options.mode === "modal" ? element.open : element.matches(":popover-open");
493
+ /** Open on the record, but no longer in the top layer, after leaving the document */
494
+ const left = (entry) => entry.options.mode === "modal" ? !entry.element.matches(":modal") : !isOpen(entry);
495
+ const close = (entry) => {
496
+ if (!isOpen(entry)) return;
497
+ if (entry.options.mode === "modal") entry.element.close();
498
+ else entry.element.hidePopover();
499
+ };
500
+ const track = (registry) => {
501
+ if (registry.stack.length) registry.moves.observe(registry.document, {
502
+ childList: true,
503
+ subtree: true
504
+ });
505
+ else registry.moves.disconnect();
506
+ };
507
+ const opened = (entry) => {
508
+ const { stack } = entry.registry;
509
+ if (stack.includes(entry)) return;
510
+ stack.push(entry);
511
+ track(entry.registry);
512
+ for (const other of stack.filter((open) => open !== entry && open.options.transient)) {
513
+ other.reason = "replaced";
514
+ close(other);
515
+ }
516
+ };
517
+ const closed = (entry, asked) => {
518
+ const { stack } = entry.registry;
519
+ const index = stack.indexOf(entry);
520
+ if (index < 0) return;
521
+ stack.splice(index, 1);
522
+ track(entry.registry);
523
+ const reason = entry.reason ?? "dismiss";
524
+ entry.reason = void 0;
525
+ if (!asked) entry.options.onClose(reason);
526
+ const inside = (node) => !!node && entry.element.contains(node);
527
+ for (const other of stack.filter((open) => inside(open.element) || inside(open.source))) {
528
+ other.reason = "parent";
529
+ close(other);
530
+ }
531
+ };
532
+ const registryOf = (document) => {
533
+ let registry = registries.get(document);
534
+ if (!registry) {
535
+ const stack = [];
536
+ const onKeydown = (event) => {
537
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
538
+ const top = stack.at(-1);
539
+ if (!top) return;
540
+ const target = event.target;
541
+ const host = typeof target?.closest === "function" ? target.closest("dialog:modal, :popover-open:not([popover=\"manual\"])") : null;
542
+ if (host && !stack.some((open) => open.element === host)) return;
543
+ event.preventDefault();
544
+ top.options.onClose("escape");
545
+ };
546
+ registry = {
547
+ stack,
548
+ attached: 0,
549
+ onKeydown,
550
+ moves: new MutationObserver(() => {
551
+ for (const entry of stack.filter(left)) {
552
+ if (entry.options.mode === "modal") entry.element.close();
553
+ closed(entry, false);
554
+ }
555
+ }),
556
+ document
557
+ };
558
+ registries.set(document, registry);
559
+ }
560
+ return registry;
561
+ };
562
+ function attachOverlay(element, options) {
563
+ const modal = options.mode === "modal";
564
+ if (modal && element.localName !== "dialog") throw new TypeError("A modal overlay must be a <dialog> element.");
565
+ const document = element.ownerDocument;
566
+ const registry = registryOf(document);
567
+ const entry = {
568
+ element,
569
+ options,
570
+ registry
571
+ };
572
+ const onToggle = () => isOpen(entry) ? opened(entry) : closed(entry, false);
573
+ const onDialogClose = () => {
574
+ if (!isOpen(entry)) closed(entry, false);
575
+ };
576
+ const onCancel = (event) => {
577
+ event.preventDefault();
578
+ options.onClose("escape");
579
+ };
580
+ if (modal) {
581
+ element.addEventListener("close", onDialogClose);
582
+ element.addEventListener("cancel", onCancel);
583
+ } else {
584
+ element.popover = options.mode;
585
+ element.addEventListener("toggle", onToggle);
586
+ }
587
+ if (options.contain) for (const type of PRESSES) element.addEventListener(type, stopPropagation);
588
+ if (registry.attached++ === 0) document.addEventListener("keydown", registry.onKeydown);
589
+ let detached = false;
590
+ return {
591
+ show(source) {
592
+ if (detached || isOpen(entry)) return;
593
+ const index = registry.stack.indexOf(entry);
594
+ if (index >= 0) registry.stack.splice(index, 1);
595
+ track(registry);
596
+ entry.reason = void 0;
597
+ entry.source = source;
598
+ if (modal) element.showModal();
599
+ else element.showPopover({ source });
600
+ opened(entry);
601
+ },
602
+ hide() {
603
+ if (!isOpen(entry)) return;
604
+ close(entry);
605
+ closed(entry, true);
606
+ },
607
+ covered() {
608
+ const index = registry.stack.indexOf(entry);
609
+ return index >= 0 && registry.stack.slice(index + 1).some((open) => !open.options.transient);
610
+ },
611
+ detach() {
612
+ if (detached) return;
613
+ detached = true;
614
+ element.removeEventListener("close", onDialogClose);
615
+ element.removeEventListener("cancel", onCancel);
616
+ element.removeEventListener("toggle", onToggle);
617
+ for (const type of PRESSES) element.removeEventListener(type, stopPropagation);
618
+ const index = registry.stack.indexOf(entry);
619
+ if (index >= 0) registry.stack.splice(index, 1);
620
+ track(registry);
621
+ close(entry);
622
+ if (!modal) element.removeAttribute("popover");
623
+ if (--registry.attached === 0) document.removeEventListener("keydown", registry.onKeydown);
624
+ }
625
+ };
626
+ }
627
+ // src/dialog/interaction.ts
628
+ const TABBABLE = [
629
+ "a[href]",
630
+ "area[href]",
631
+ "button",
632
+ "input:not([type=\"hidden\"])",
633
+ "select",
634
+ "textarea",
635
+ "iframe",
636
+ "audio[controls]",
637
+ "video[controls]",
638
+ "summary",
639
+ "[contenteditable]:not([contenteditable=\"false\"])",
640
+ "[tabindex]"
641
+ ].join(",");
642
+ const isRadio = (element) => element.localName === "input" && element.type === "radio";
643
+ const visible = (element) => typeof element.checkVisibility === "function" ? element.checkVisibility({ visibilityProperty: true }) : element.getClientRects().length > 0;
644
+ /** The elements Tab stops at in `root`, in order. Elements inside shadow roots aren't found. */
645
+ const tabbablesIn = (root) => {
646
+ const all = [...root.querySelectorAll(TABBABLE)].filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && !element.closest("[inert]") && visible(element));
647
+ return all.filter((element) => {
648
+ if (!isRadio(element) || !element.name) return true;
649
+ const group = all.filter((other) => isRadio(other) && other.name === element.name && other.form === element.form);
650
+ return element === (group.find((radio) => radio.checked) ?? group[0]);
651
+ });
652
+ };
653
+ /** How far, in px, the content may scroll and still count as at its end */
654
+ const SCROLL_SLOP = 1;
655
+ const scrollLocks = /* @__PURE__ */ new WeakMap();
656
+ const lockScroll = (document) => {
657
+ const lock = scrollLocks.get(document);
658
+ if (lock) {
659
+ lock.count++;
660
+ return;
661
+ }
662
+ const html = document.documentElement;
663
+ const view = document.defaultView;
664
+ const { overflow, scrollbarGutter } = html.style;
665
+ const scrollbar = view.innerWidth - html.clientWidth;
666
+ html.style.overflow = "hidden";
667
+ if (scrollbar > 0 && view.getComputedStyle(html).scrollbarGutter === "auto") html.style.scrollbarGutter = "stable";
668
+ scrollLocks.set(document, {
669
+ count: 1,
670
+ restore() {
671
+ html.style.overflow = overflow;
672
+ html.style.scrollbarGutter = scrollbarGutter;
673
+ }
674
+ });
675
+ };
676
+ const unlockScroll = (document) => {
677
+ const lock = scrollLocks.get(document);
678
+ if (!lock || --lock.count > 0) return;
679
+ lock.restore();
680
+ scrollLocks.delete(document);
681
+ };
682
+ function attachDialog(root, options) {
683
+ const document = root.ownerDocument;
684
+ const cancel = (reason) => {
685
+ let prevented = false;
686
+ options.onCancel?.({
687
+ reason,
688
+ get defaultPrevented() {
689
+ return prevented;
690
+ },
691
+ preventDefault() {
692
+ prevented = true;
693
+ }
694
+ });
695
+ if (!prevented) options.onOpenChange(false);
696
+ };
697
+ const overlay = attachOverlay(root, {
698
+ mode: "modal",
699
+ contain: true,
700
+ onClose: (reason) => reason === "escape" ? cancel("escape") : options.onOpenChange(false)
701
+ });
702
+ const onKeydown = (event) => {
703
+ if (event.key !== "Tab" || event.defaultPrevented) return;
704
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
705
+ const tabbables = tabbablesIn(root);
706
+ const first = tabbables[0];
707
+ const last = tabbables.at(-1);
708
+ if (!first || !last) {
709
+ event.preventDefault();
710
+ return;
711
+ }
712
+ const focused = document.activeElement;
713
+ if (!(event.shiftKey ? focused === first || focused === root : focused === last)) return;
714
+ event.preventDefault();
715
+ (event.shiftKey ? last : first).focus();
716
+ };
717
+ const onScrim = (event) => {
718
+ if (event.target !== root) return false;
719
+ const box = root.getBoundingClientRect();
720
+ return event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
721
+ };
722
+ let pressedScrim = false;
723
+ const onPointerDown = (event) => {
724
+ pressedScrim = onScrim(event) && !overlay.covered();
725
+ };
726
+ const onClick = (event) => {
727
+ const pressed = pressedScrim;
728
+ pressedScrim = false;
729
+ if (pressed && onScrim(event)) cancel("scrim");
730
+ };
731
+ const content = () => root.querySelector(":scope > .rio-dialog__content");
732
+ const updateDividers = () => {
733
+ const element = content();
734
+ if (!element) return;
735
+ const hidden = element.scrollHeight - element.clientHeight;
736
+ element.toggleAttribute("data-overflow-top", element.scrollTop >= SCROLL_SLOP);
737
+ element.toggleAttribute("data-overflow-bottom", hidden - element.scrollTop >= SCROLL_SLOP);
738
+ };
739
+ /** The content, while it takes the focus so the keyboard can scroll it */
740
+ let reachable = null;
741
+ const setReachable = (element) => {
742
+ if (element === reachable) return;
743
+ reachable?.removeAttribute("tabindex");
744
+ reachable = element;
745
+ reachable?.setAttribute("tabindex", "0");
746
+ };
747
+ const updateOverflow = () => {
748
+ updateDividers();
749
+ const element = content();
750
+ const scrolls = !!element && element.scrollHeight - element.clientHeight >= SCROLL_SLOP;
751
+ setReachable(scrolls && tabbablesIn(element).length === 0 ? element : null);
752
+ };
753
+ const resizes = new ResizeObserver(updateOverflow);
754
+ let watched = null;
755
+ const watch = () => {
756
+ resizes.disconnect();
757
+ watched?.removeEventListener("scroll", updateDividers);
758
+ watched = content();
759
+ if (!watched) return;
760
+ watched.addEventListener("scroll", updateDividers, { passive: true });
761
+ resizes.observe(watched);
762
+ for (const child of watched.children) resizes.observe(child);
763
+ };
764
+ const unwatch = () => {
765
+ resizes.disconnect();
766
+ watched?.removeEventListener("scroll", updateDividers);
767
+ watched = null;
768
+ setReachable(null);
769
+ };
770
+ const focusFirst = () => {
771
+ if (root.querySelector("[autofocus]")) return;
772
+ const tabbables = tabbablesIn(root);
773
+ (tabbables.find((element) => element !== reachable) ?? tabbables[0])?.focus();
774
+ };
775
+ /** Whether this dialog holds a lock on the page's scroll: from opening until it closes */
776
+ let locked = false;
777
+ /** Counts closings, so only the last one reports its end, and only if the dialog stayed closed */
778
+ let closings = 0;
779
+ const closed = () => {
780
+ if (!locked) return;
781
+ locked = false;
782
+ unlockScroll(document);
783
+ const closing = ++closings;
784
+ const motion = root.getAnimations({ subtree: true }).map((animation) => animation.finished.catch(() => void 0));
785
+ Promise.all(motion).then(() => {
786
+ if (closing === closings && !root.open) options.onClosed?.();
787
+ });
788
+ };
789
+ root.addEventListener("keydown", onKeydown);
790
+ root.addEventListener("pointerdown", onPointerDown);
791
+ root.addEventListener("click", onClick);
792
+ return {
793
+ sync(open) {
794
+ if (open && !root.open) {
795
+ overlay.show();
796
+ if (!locked) {
797
+ locked = true;
798
+ lockScroll(document);
799
+ }
800
+ const element = content();
801
+ if (element) element.scrollTop = 0;
802
+ watch();
803
+ updateOverflow();
804
+ focusFirst();
805
+ } else if (!open) {
806
+ if (root.open) overlay.hide();
807
+ unwatch();
808
+ closed();
809
+ }
810
+ },
811
+ update() {
812
+ if (!root.open) return;
813
+ watch();
814
+ updateOverflow();
815
+ },
816
+ detach() {
817
+ root.removeEventListener("keydown", onKeydown);
818
+ root.removeEventListener("pointerdown", onPointerDown);
819
+ root.removeEventListener("click", onClick);
820
+ unwatch();
821
+ overlay.detach();
822
+ closings++;
823
+ if (locked) {
824
+ locked = false;
825
+ unlockScroll(document);
826
+ }
57
827
  }
58
828
  };
59
829
  }
@@ -81,25 +851,989 @@ const iconButtonDefaults = {
81
851
  softDisabled: false,
82
852
  type: "button"
83
853
  };
854
+ /**
855
+ * How the adapter renders the icon button's parts. The icon holds the selected icon when
856
+ * `showsSelectedIcon()`, otherwise the default one.
857
+ */
858
+ const iconButtonSkeleton = {
859
+ part: "root",
860
+ tag: "button",
861
+ children: [{
862
+ part: "touch",
863
+ tag: "span"
864
+ }, {
865
+ part: "icon",
866
+ tag: "span",
867
+ content: true
868
+ }]
869
+ };
84
870
  function createIconButton(options) {
85
871
  const button = createButton({
86
- ...options,
87
872
  props: () => ({
88
873
  ...options.props(),
89
874
  variant: options.props().variant
90
- })
875
+ }),
876
+ onClick: options.onClick,
877
+ onChange: options.onChange
91
878
  });
92
879
  return {
93
- rootAttrs() {
94
- const p = options.props();
95
- return {
96
- ...button.rootAttrs(),
97
- "aria-label": p.label,
98
- "data-width": p.width ?? iconButtonDefaults.width,
99
- "data-variant": p.variant ?? iconButtonDefaults.variant
100
- };
880
+ parts: {
881
+ root() {
882
+ const p = options.props();
883
+ const root = button.parts.root();
884
+ return {
885
+ ...root,
886
+ class: `${root.class} rio-icon-button`,
887
+ "aria-label": p.label,
888
+ "data-width": p.width ?? iconButtonDefaults.width,
889
+ "data-variant": p.variant ?? iconButtonDefaults.variant
890
+ };
891
+ },
892
+ touch: button.parts.touch,
893
+ icon: button.parts.icon
894
+ },
895
+ showsSelectedIcon() {
896
+ const p = options.props();
897
+ return !!(p.toggle && p.selected && options.content?.().selectedIcon);
898
+ },
899
+ handleClick: button.handleClick
900
+ };
901
+ }
902
+ // src/primitives/anchored-position/anchored-position.ts
903
+ const OPPOSITE = {
904
+ top: "bottom",
905
+ bottom: "top",
906
+ start: "end",
907
+ end: "start"
908
+ };
909
+ const VARS = [
910
+ "--rio-anchored-available-width",
911
+ "--rio-anchored-available-height",
912
+ "--rio-anchored-anchor-width"
913
+ ];
914
+ const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
915
+ /** An anchor point, rather than an element. Checked by shape, so elements from iframes count. */
916
+ const isPoint = (target) => typeof target.getBoundingClientRect !== "function";
917
+ function attachAnchoredPosition(floating, options) {
918
+ const document = floating.ownerDocument;
919
+ const view = document.defaultView;
920
+ floating.dataset.anchored = "";
921
+ const offset = options.offset ?? 0;
922
+ const alignOffset = options.alignOffset ?? 0;
923
+ const padding = options.padding ?? 8;
924
+ const preferred = options.side ?? "bottom";
925
+ const align = options.align ?? "start";
926
+ const candidates = [.../* @__PURE__ */ new Set([preferred, ...options.fallbacks ?? [OPPOSITE[preferred]]])];
927
+ /** Rounds to the device's pixel grid, so text in the element stays sharp. */
928
+ const snap = (value) => Math.round(value * view.devicePixelRatio) / view.devicePixelRatio;
929
+ let frame;
930
+ const schedule = () => {
931
+ frame ??= view.requestAnimationFrame(() => {
932
+ frame = void 0;
933
+ place();
934
+ });
935
+ };
936
+ const onScroll = (event) => {
937
+ if (event.target !== document && floating.contains(event.target)) return;
938
+ schedule();
939
+ };
940
+ const resize = new view.ResizeObserver(schedule);
941
+ resize.observe(floating);
942
+ let observed;
943
+ const observeAnchor = (element) => {
944
+ if (element === observed) return;
945
+ if (observed) resize.unobserve(observed);
946
+ observed = element;
947
+ if (element) resize.observe(element);
948
+ };
949
+ const place = () => {
950
+ const target = options.anchor();
951
+ if (!target || floating.getClientRects().length === 0) return;
952
+ if (!isPoint(target) && !target.isConnected) return;
953
+ observeAnchor(isPoint(target) ? void 0 : target);
954
+ const anchor = isPoint(target) ? {
955
+ left: target.x,
956
+ right: target.x,
957
+ top: target.y,
958
+ bottom: target.y,
959
+ width: 0
960
+ } : target.getBoundingClientRect();
961
+ const rtl = view.getComputedStyle(isPoint(target) ? floating : target).direction === "rtl";
962
+ const viewport = {
963
+ width: document.documentElement.clientWidth,
964
+ height: document.documentElement.clientHeight
965
+ };
966
+ const physical = (side) => side === "start" ? rtl ? "right" : "left" : side === "end" ? rtl ? "left" : "right" : side;
967
+ const room = {
968
+ top: anchor.top - offset - padding,
969
+ bottom: viewport.height - anchor.bottom - offset - padding,
970
+ left: anchor.left - offset - padding,
971
+ right: viewport.width - anchor.right - offset - padding
972
+ };
973
+ floating.style.left = "0px";
974
+ floating.style.top = "0px";
975
+ const borders = {
976
+ width: floating.offsetWidth - floating.clientWidth,
977
+ height: floating.offsetHeight - floating.clientHeight
978
+ };
979
+ const natural = {
980
+ width: floating.scrollWidth + borders.width,
981
+ height: floating.scrollHeight + borders.height
982
+ };
983
+ const fits = (candidate) => {
984
+ const at = physical(candidate);
985
+ return at === "top" || at === "bottom" ? room[at] >= natural.height && natural.width <= viewport.width - 2 * padding : room[at] >= natural.width && natural.height <= viewport.height - 2 * padding;
986
+ };
987
+ const roomier = (a, b) => room[physical(b)] > room[physical(a)] ? b : a;
988
+ const side = candidates.find(fits) ?? roomier(preferred, OPPOSITE[preferred]);
989
+ const at = physical(side);
990
+ const vertical = at === "top" || at === "bottom";
991
+ const available = {
992
+ width: vertical ? viewport.width - 2 * padding : room[at],
993
+ height: vertical ? room[at] : viewport.height - 2 * padding
994
+ };
995
+ floating.style.setProperty(VARS[0], `${Math.max(0, available.width)}px`);
996
+ floating.style.setProperty(VARS[1], `${Math.max(0, available.height)}px`);
997
+ floating.style.setProperty(VARS[2], `${anchor.width}px`);
998
+ const size = {
999
+ width: floating.offsetWidth,
1000
+ height: floating.offsetHeight
1001
+ };
1002
+ let x;
1003
+ let y;
1004
+ if (vertical) {
1005
+ y = at === "bottom" ? anchor.bottom + offset : anchor.top - offset - size.height;
1006
+ const startEdge = rtl ? anchor.right - size.width : anchor.left;
1007
+ const endEdge = rtl ? anchor.left : anchor.right - size.width;
1008
+ x = (align === "center" ? anchor.left + (anchor.width - size.width) / 2 : align === "start" ? startEdge : endEdge) + (rtl ? -alignOffset : alignOffset);
1009
+ } else {
1010
+ x = at === "right" ? anchor.right + offset : anchor.left - offset - size.width;
1011
+ const height = anchor.bottom - anchor.top;
1012
+ y = (align === "center" ? anchor.top + (height - size.height) / 2 : align === "start" ? anchor.top : anchor.bottom - size.height) + alignOffset;
1013
+ }
1014
+ const fit = (value, length, extent, startAtEnd) => length > extent - 2 * padding ? startAtEnd ? extent - padding - length : padding : clamp(value, padding, extent - padding - length);
1015
+ x = fit(x, size.width, viewport.width, rtl);
1016
+ y = fit(y, size.height, viewport.height, false);
1017
+ floating.style.left = `${snap(x)}px`;
1018
+ floating.style.top = `${snap(y)}px`;
1019
+ floating.dataset.side = side;
1020
+ floating.dataset.align = align;
1021
+ if (anchor.bottom < 0 || anchor.top > viewport.height || anchor.right < 0 || anchor.left > viewport.width) floating.dataset.anchorHidden = "";
1022
+ else delete floating.dataset.anchorHidden;
1023
+ };
1024
+ document.addEventListener("scroll", onScroll, {
1025
+ capture: true,
1026
+ passive: true
1027
+ });
1028
+ view.addEventListener("resize", schedule, { passive: true });
1029
+ return {
1030
+ update: place,
1031
+ detach() {
1032
+ document.removeEventListener("scroll", onScroll, { capture: true });
1033
+ view.removeEventListener("resize", schedule);
1034
+ resize.disconnect();
1035
+ if (frame !== void 0) view.cancelAnimationFrame(frame);
1036
+ for (const name of [
1037
+ "left",
1038
+ "top",
1039
+ ...VARS
1040
+ ]) floating.style.removeProperty(name);
1041
+ for (const name of [
1042
+ "side",
1043
+ "align",
1044
+ "anchorHidden",
1045
+ "anchored"
1046
+ ]) delete floating.dataset[name];
1047
+ }
1048
+ };
1049
+ }
1050
+ // src/menu/menu.ts
1051
+ /** M3E vertical menu color styles: surface-based, or tertiary-based for more emphasis. */
1052
+ const MENU_VARIANTS = ["standard", "vibrant"];
1053
+ /** Whether the menu's items can be selected: none, one at a time, or any number. */
1054
+ const MENU_SELECTIONS = [
1055
+ "none",
1056
+ "single",
1057
+ "multiple"
1058
+ ];
1059
+ const menuDefaults = {
1060
+ open: false,
1061
+ variant: "standard",
1062
+ selection: "none",
1063
+ side: "bottom",
1064
+ align: "start",
1065
+ offset: 0,
1066
+ instant: false
1067
+ };
1068
+ const menuSkeleton = {
1069
+ part: "root",
1070
+ tag: "div",
1071
+ content: true
1072
+ };
1073
+ const valuesOf = (selected) => selected === void 0 ? [] : Array.isArray(selected) ? selected : [selected];
1074
+ function createMenu(options) {
1075
+ const props = () => ({
1076
+ ...menuDefaults,
1077
+ ...omitUndefined(options.props())
1078
+ });
1079
+ const isSelected = (value) => {
1080
+ const p = props();
1081
+ return p.selection !== "none" && valuesOf(p.selected).includes(value);
1082
+ };
1083
+ return {
1084
+ parts: { root() {
1085
+ const p = props();
1086
+ return {
1087
+ class: "rio-menu",
1088
+ role: "menu",
1089
+ "aria-label": p.label,
1090
+ "data-variant": p.variant,
1091
+ "data-instant": p.instant ? "" : void 0
1092
+ };
1093
+ } },
1094
+ selection: () => props().selection,
1095
+ isSelected,
1096
+ choose(value) {
1097
+ const p = props();
1098
+ if (p.selection !== "none" && value !== void 0) {
1099
+ if (p.selection === "single") {
1100
+ if (!isSelected(value)) options.onChange(value);
1101
+ } else {
1102
+ const current = valuesOf(p.selected);
1103
+ options.onChange(isSelected(value) ? current.filter((other) => other !== value) : [...current, value]);
1104
+ return;
1105
+ }
1106
+ }
1107
+ options.onOpenChange(false);
1108
+ },
1109
+ close: () => options.onOpenChange(false),
1110
+ variant: () => props().variant,
1111
+ instant: () => props().instant
1112
+ };
1113
+ }
1114
+ const menuItemDefaults = { disabled: false };
1115
+ const menuItemSkeleton = {
1116
+ part: "root",
1117
+ tag: "button",
1118
+ children: [
1119
+ {
1120
+ part: "touch",
1121
+ tag: "span"
1122
+ },
1123
+ {
1124
+ part: "icon",
1125
+ tag: "span",
1126
+ when: "showsIcon",
1127
+ content: true
1128
+ },
1129
+ {
1130
+ part: "text",
1131
+ tag: "span",
1132
+ children: [{
1133
+ part: "label",
1134
+ tag: "span",
1135
+ content: true
1136
+ }, {
1137
+ part: "supportingText",
1138
+ tag: "span",
1139
+ when: "showsSupportingText",
1140
+ content: true
1141
+ }]
1142
+ },
1143
+ {
1144
+ part: "trailingText",
1145
+ tag: "span",
1146
+ when: "showsTrailingText",
1147
+ content: true
1148
+ },
1149
+ {
1150
+ part: "trailingIcon",
1151
+ tag: "span",
1152
+ when: "showsTrailingIcon",
1153
+ content: true
1154
+ }
1155
+ ]
1156
+ };
1157
+ const NO_CONTENT$1 = {
1158
+ icon: false,
1159
+ supportingText: false,
1160
+ trailingText: false,
1161
+ trailingIcon: false
1162
+ };
1163
+ function createMenuItem(options) {
1164
+ const props = () => ({
1165
+ ...menuItemDefaults,
1166
+ ...omitUndefined(options.props())
1167
+ });
1168
+ const content = () => options.content?.() ?? NO_CONTENT$1;
1169
+ const kind = () => {
1170
+ if (options.submenu) return void 0;
1171
+ const selection = options.menu?.()?.selection() ?? "none";
1172
+ if (selection === "none" || props().value === void 0) return void 0;
1173
+ return selection === "single" ? "radio" : "checkbox";
1174
+ };
1175
+ const selected = () => {
1176
+ const value = props().value;
1177
+ return kind() !== void 0 && !!options.menu?.()?.isSelected(value);
1178
+ };
1179
+ return {
1180
+ parts: {
1181
+ root() {
1182
+ const p = props();
1183
+ const k = kind();
1184
+ return {
1185
+ class: "rio-menu-item rio-state-layer rio-focus-ring",
1186
+ type: "button",
1187
+ role: k ? `menuitem${k}` : "menuitem",
1188
+ tabindex: "-1",
1189
+ "aria-checked": k ? String(selected()) : void 0,
1190
+ "aria-haspopup": options.submenu ? "menu" : void 0,
1191
+ "aria-expanded": options.submenu ? String(options.submenu.open()) : void 0,
1192
+ "aria-disabled": p.disabled ? "true" : void 0,
1193
+ "data-selected": k ? String(selected()) : void 0,
1194
+ "data-focus-ring": "inner"
1195
+ };
1196
+ },
1197
+ touch: () => ({
1198
+ class: "rio-menu-item__touch",
1199
+ "aria-hidden": "true"
1200
+ }),
1201
+ icon: () => ({
1202
+ class: "rio-menu-item__icon",
1203
+ "aria-hidden": "true"
1204
+ }),
1205
+ text: () => ({ class: "rio-menu-item__text" }),
1206
+ label: () => ({ class: "rio-menu-item__label" }),
1207
+ supportingText: () => ({ class: "rio-menu-item__supporting-text" }),
1208
+ trailingText: () => ({ class: "rio-menu-item__trailing-text" }),
1209
+ trailingIcon: () => ({
1210
+ class: "rio-menu-item__trailing-icon",
1211
+ "aria-hidden": "true"
1212
+ })
1213
+ },
1214
+ showsIcon: () => content().icon,
1215
+ showsSupportingText: () => content().supportingText,
1216
+ showsTrailingText: () => content().trailingText,
1217
+ showsTrailingIcon: () => content().trailingIcon,
1218
+ selected,
1219
+ handleClick(event) {
1220
+ if (props().disabled) {
1221
+ event.preventDefault();
1222
+ return;
1223
+ }
1224
+ options.onClick(event);
1225
+ if (options.submenu) options.submenu.onOpen();
1226
+ else options.menu?.()?.choose(props().value);
1227
+ }
1228
+ };
1229
+ }
1230
+ /**
1231
+ * A submenu and the item that opens it. The two render side by side in the menu, the item where it
1232
+ * sits and the submenu after it; the submenu shows in the top layer next to the item.
1233
+ */
1234
+ function createSubMenu(options) {
1235
+ const menu = createMenu({
1236
+ props: () => {
1237
+ const p = options.props();
1238
+ const parent = options.menu?.();
1239
+ return {
1240
+ open: options.open(),
1241
+ variant: parent?.variant(),
1242
+ instant: parent?.instant(),
1243
+ selection: p.selection,
1244
+ selected: p.selected,
1245
+ label: p.label
1246
+ };
1247
+ },
1248
+ onChange: options.onChange,
1249
+ onOpenChange: (open) => {
1250
+ const parent = options.menu?.();
1251
+ if (open) options.onOpenChange(true);
1252
+ else if (parent) parent.close();
1253
+ else options.onOpenChange(false);
1254
+ }
1255
+ });
1256
+ return {
1257
+ item: createMenuItem({
1258
+ props: () => ({ disabled: options.props().disabled }),
1259
+ content: options.content,
1260
+ onClick: () => {},
1261
+ submenu: {
1262
+ open: options.open,
1263
+ onOpen: () => options.onOpenChange(true)
1264
+ }
1265
+ }),
1266
+ menu
1267
+ };
1268
+ }
1269
+ const menuGroupSkeleton = {
1270
+ part: "root",
1271
+ tag: "div",
1272
+ children: [{
1273
+ part: "label",
1274
+ tag: "div",
1275
+ when: "showsLabel",
1276
+ content: true
1277
+ }, {
1278
+ part: "items",
1279
+ tag: "div",
1280
+ content: true
1281
+ }]
1282
+ };
1283
+ function createMenuGroup(options) {
1284
+ return {
1285
+ parts: {
1286
+ root: () => ({
1287
+ class: "rio-menu-group",
1288
+ role: "group",
1289
+ "aria-label": options.props().label
1290
+ }),
1291
+ label: () => ({
1292
+ class: "rio-menu-group__label",
1293
+ "aria-hidden": "true"
1294
+ }),
1295
+ items: () => ({ class: "rio-menu-group__items" })
1296
+ },
1297
+ showsLabel: () => !!options.props().label
1298
+ };
1299
+ }
1300
+ const menuDividerSkeleton = {
1301
+ part: "root",
1302
+ tag: "div"
1303
+ };
1304
+ function createMenuDivider() {
1305
+ return { parts: { root: () => ({
1306
+ class: "rio-menu-divider",
1307
+ role: "separator"
1308
+ }) } };
1309
+ }
1310
+ // src/menu/interaction.ts
1311
+ /** Where to try next when the preferred side has no room: across, then to the sides. */
1312
+ const FALLBACKS = {
1313
+ bottom: [
1314
+ "top",
1315
+ "end",
1316
+ "start"
1317
+ ],
1318
+ top: [
1319
+ "bottom",
1320
+ "end",
1321
+ "start"
1322
+ ],
1323
+ end: [
1324
+ "start",
1325
+ "bottom",
1326
+ "top"
1327
+ ],
1328
+ start: [
1329
+ "end",
1330
+ "bottom",
1331
+ "top"
1332
+ ]
1333
+ };
1334
+ /** How long typed letters add up to one search. */
1335
+ const TYPEAHEAD_MS = 500;
1336
+ /** How long the pointer rests on an item before its submenu opens */
1337
+ const HOVER_OPEN_MS = 100;
1338
+ /** How long the pointer may take to reach an open submenu across other items */
1339
+ const GRACE_MS = 300;
1340
+ /**
1341
+ * A submenu goes beside its item, 4px off the menu around it (the item is inset 4px), with its
1342
+ * first item lined up with the item (its own are inset 4px from its top).
1343
+ */
1344
+ const SUBMENU_PLACEMENT = {
1345
+ side: "end",
1346
+ align: "start",
1347
+ offset: 8,
1348
+ alignOffset: -4
1349
+ };
1350
+ const isElement = (anchor) => !!anchor && typeof anchor.focus === "function";
1351
+ /** Points are compared by value: an inline `{ x, y }` is a new object on every render. */
1352
+ const samePoint = (a, b) => a.x === b.x && a.y === b.y && a.element === b.element;
1353
+ /** Whether a point is inside a polygon, by casting a ray to the right and counting crossings */
1354
+ const isInside = ({ x, y }, polygon) => {
1355
+ let inside = false;
1356
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
1357
+ const a = polygon[i];
1358
+ const b = polygon[j];
1359
+ if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside = !inside;
1360
+ }
1361
+ return inside;
1362
+ };
1363
+ const menuStates = /* @__PURE__ */ new WeakMap();
1364
+ const stateOf = (menu) => {
1365
+ let state = menuStates.get(menu);
1366
+ if (!state) {
1367
+ state = {
1368
+ openers: /* @__PURE__ */ new Map(),
1369
+ typing: () => false
1370
+ };
1371
+ menuStates.set(menu, state);
1372
+ }
1373
+ return state;
1374
+ };
1375
+ const endGrace = (state) => {
1376
+ if (state.grace) clearTimeout(state.grace.timer);
1377
+ state.grace = void 0;
1378
+ };
1379
+ const menuAround = (element) => element.parentElement?.closest(".rio-menu");
1380
+ const topOf = (menu) => {
1381
+ let top = menu;
1382
+ for (let up = menuAround(menu); up; up = menuAround(up)) top = up;
1383
+ return top;
1384
+ };
1385
+ const activeMenus = /* @__PURE__ */ new WeakMap();
1386
+ const shapeTree = (menu, active) => {
1387
+ const top = topOf(menu);
1388
+ if (active) activeMenus.set(top, active);
1389
+ let current = activeMenus.get(top) ?? top;
1390
+ while (current && !current.matches(":popover-open")) current = menuAround(current);
1391
+ const open = [top, ...top.querySelectorAll(".rio-menu")].filter((each) => each.matches(":popover-open"));
1392
+ for (const each of open) each.toggleAttribute("data-inactive", open.length > 1 && each !== current);
1393
+ };
1394
+ function attachMenu(root, options) {
1395
+ return setup(root, options);
1396
+ }
1397
+ function setup(root, options, nested) {
1398
+ const document = root.ownerDocument;
1399
+ const view = document.defaultView;
1400
+ let reanchoring = false;
1401
+ const overlay = attachOverlay(root, {
1402
+ mode: "auto",
1403
+ onClose: (reason) => {
1404
+ if (!(reanchoring && reason === "dismiss")) options.onClose();
1405
+ }
1406
+ });
1407
+ const items = () => [...root.querySelectorAll("[role^=\"menuitem\"]")].filter((item) => item.closest("[role=\"menu\"]") === root);
1408
+ const focused = () => items().indexOf(document.activeElement);
1409
+ const focusAt = (index) => {
1410
+ const all = items();
1411
+ if (all.length) all[(index + all.length) % all.length].focus();
1412
+ };
1413
+ let anchored;
1414
+ let placement = "";
1415
+ /** What had the focus when the menu opened */
1416
+ let opener = null;
1417
+ let pinned;
1418
+ const pin = (fresh) => {
1419
+ const anchor = options.anchor();
1420
+ if (!anchor || isElement(anchor) || !anchor.element) {
1421
+ pinned = void 0;
1422
+ return;
1423
+ }
1424
+ if (!fresh && pinned && samePoint(pinned.point, anchor)) return;
1425
+ const rect = anchor.element.getBoundingClientRect();
1426
+ pinned = {
1427
+ point: { ...anchor },
1428
+ x: anchor.x - rect.left,
1429
+ y: anchor.y - rect.top
1430
+ };
1431
+ };
1432
+ /** The anchor for placing: for a point on an element, where that spot is now */
1433
+ const anchorNow = () => {
1434
+ const anchor = options.anchor();
1435
+ if (!anchor || isElement(anchor) || !anchor.element) return anchor;
1436
+ if (!anchor.element.isConnected) return void 0;
1437
+ if (!pinned || !samePoint(pinned.point, anchor)) pin(false);
1438
+ const rect = anchor.element.getBoundingClientRect();
1439
+ return {
1440
+ x: rect.left + pinned.x,
1441
+ y: rect.top + pinned.y
1442
+ };
1443
+ };
1444
+ const place = () => {
1445
+ const p = {
1446
+ ...menuDefaults,
1447
+ alignOffset: 0,
1448
+ ...omitUndefined(options.props())
1449
+ };
1450
+ const key = `${p.side} ${p.align} ${p.offset} ${p.alignOffset}`;
1451
+ if (anchored && key === placement) return anchored;
1452
+ anchored?.detach();
1453
+ placement = key;
1454
+ anchored = attachAnchoredPosition(root, {
1455
+ anchor: anchorNow,
1456
+ side: p.side,
1457
+ align: p.align,
1458
+ offset: p.offset,
1459
+ alignOffset: p.alignOffset,
1460
+ fallbacks: FALLBACKS[p.side]
1461
+ });
1462
+ return anchored;
1463
+ };
1464
+ const returnTarget = () => {
1465
+ const anchor = options.anchor();
1466
+ if (isElement(anchor) && anchor.isConnected) return anchor;
1467
+ return opener?.isConnected && !root.contains(opener) ? opener : null;
1468
+ };
1469
+ let typed = "";
1470
+ let typedAt = 0;
1471
+ const typeahead = (key) => {
1472
+ const now = performance.now();
1473
+ typed = now - typedAt > TYPEAHEAD_MS ? key : typed + key;
1474
+ typedAt = now;
1475
+ const all = items();
1476
+ const labelOf = (item) => (item.querySelector(".rio-menu-item__label")?.textContent ?? item.textContent ?? "").trim().toLowerCase();
1477
+ const search = [...typed].every((letter) => letter === typed[0]) ? typed[0] : typed;
1478
+ const start = focused() + (search.length === 1 ? 1 : 0);
1479
+ for (let step = 0; step < all.length; step++) {
1480
+ const item = all[(Math.max(start, 0) + step) % all.length];
1481
+ if (labelOf(item).startsWith(search)) {
1482
+ item.focus();
1483
+ return;
1484
+ }
1485
+ }
1486
+ };
1487
+ const state = stateOf(root);
1488
+ state.typing = () => typed !== "" && performance.now() - typedAt <= TYPEAHEAD_MS;
1489
+ const onKeydown = (event) => {
1490
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
1491
+ if (event.key === "Tab") {
1492
+ if (nested) return;
1493
+ options.onClose();
1494
+ returnTarget()?.focus();
1495
+ return;
1496
+ }
1497
+ if (event.target.closest("[role=\"menu\"]") !== root) return;
1498
+ if (activeMenus.get(topOf(root)) !== root) shapeTree(root, root);
1499
+ const handled = () => event.preventDefault();
1500
+ if (nested && event.key === (isRtl() ? "ArrowRight" : "ArrowLeft")) {
1501
+ handled();
1502
+ options.onClose();
1503
+ return;
1504
+ }
1505
+ switch (event.key) {
1506
+ case "ArrowDown":
1507
+ handled();
1508
+ focusAt(focused() + 1);
1509
+ return;
1510
+ case "ArrowUp":
1511
+ handled();
1512
+ focusAt(focused() < 0 ? -1 : focused() - 1);
1513
+ return;
1514
+ case "Home":
1515
+ handled();
1516
+ focusAt(0);
1517
+ return;
1518
+ case "End":
1519
+ handled();
1520
+ focusAt(-1);
1521
+ return;
1522
+ }
1523
+ if (event.key.length === 1 && !(event.key === " " && !typed)) {
1524
+ handled();
1525
+ typeahead(event.key.toLowerCase());
1526
+ }
1527
+ };
1528
+ root.addEventListener("keydown", onKeydown);
1529
+ const isRtl = () => view.getComputedStyle(root).direction === "rtl";
1530
+ let last;
1531
+ /** Which way the pointer last moved, across and down */
1532
+ const heading = {};
1533
+ const onPointerMove = (event) => {
1534
+ if (event.pointerType !== "mouse") return;
1535
+ const target = event.target;
1536
+ if (target.closest("[role=\"menu\"]") !== root) return;
1537
+ const point = {
1538
+ x: event.clientX,
1539
+ y: event.clientY
1540
+ };
1541
+ if (last && point.x !== last.x) heading.x = point.x > last.x ? "right" : "left";
1542
+ if (last && point.y !== last.y) heading.y = point.y > last.y ? "down" : "up";
1543
+ last = point;
1544
+ if (activeMenus.get(topOf(root)) !== root) shapeTree(root, root);
1545
+ const { grace } = state;
1546
+ if (grace) {
1547
+ if ((grace.toward === "left" || grace.toward === "right" ? heading.x : heading.y) === grace.toward && isInside(point, grace.area)) return;
1548
+ }
1549
+ endGrace(state);
1550
+ const item = target.closest("[role^=\"menuitem\"]");
1551
+ if (!item) return;
1552
+ const focusedMenu = document.activeElement?.closest("[role=\"menu\"]");
1553
+ if (focusedMenu && focusedMenu !== root && topOf(focusedMenu) === topOf(root)) item.focus({ preventScroll: true });
1554
+ for (const [opener, control] of state.openers) if (opener !== item) control.cancel();
1555
+ state.openers.get(item)?.hover();
1556
+ };
1557
+ root.addEventListener("pointermove", onPointerMove);
1558
+ const onFocusIn = (event) => {
1559
+ if (event.target.closest("[role=\"menu\"]") === root) shapeTree(root, root);
1560
+ };
1561
+ root.addEventListener("focusin", onFocusIn);
1562
+ const focusFirst = () => {
1563
+ const all = items();
1564
+ (all.find((item) => item.getAttribute("aria-checked") === "true") ?? all[0])?.focus();
1565
+ };
1566
+ /** What the menu last opened at: its anchor element or point, or null for none */
1567
+ let shownAt = null;
1568
+ const isNewAnchor = () => {
1569
+ const anchor = options.anchor();
1570
+ if (!anchor || !shownAt) return false;
1571
+ if (isElement(anchor) || isElement(shownAt)) return anchor !== shownAt;
1572
+ return !samePoint(anchor, shownAt);
1573
+ };
1574
+ const remember = () => {
1575
+ const anchor = options.anchor();
1576
+ shownAt = !anchor ? null : isElement(anchor) ? anchor : { ...anchor };
1577
+ };
1578
+ /**
1579
+ * Opens the menu. `afresh` opens it as if it had been closed: a menu still open, or still fading
1580
+ * out after a click outside, would only turn its closing back where it is, so that is cut short
1581
+ * and the opening motion plays from the start. By default, at a new anchor.
1582
+ */
1583
+ const show = (afresh = isNewAnchor()) => {
1584
+ const active = document.activeElement;
1585
+ if (!root.contains(active)) opener = active instanceof document.defaultView.HTMLElement ? active : null;
1586
+ if (afresh) {
1587
+ root.style.setProperty("transition", "none");
1588
+ if (root.matches(":popover-open")) overlay.hide();
1589
+ root.offsetWidth;
1590
+ root.style.removeProperty("transition");
1591
+ }
1592
+ const anchored = place();
1593
+ const anchor = options.anchor();
1594
+ remember();
1595
+ const on = anchor && !isElement(anchor) ? anchor.element : void 0;
1596
+ overlay.show(isElement(anchor) ? anchor : on instanceof document.defaultView.HTMLElement ? on : void 0);
1597
+ anchored.update();
1598
+ const focusing = !nested || nested.focusOnShow();
1599
+ shapeTree(root, focusing ? root : void 0);
1600
+ if (focusing) focusFirst();
1601
+ };
1602
+ let pressing = false;
1603
+ let afterPress;
1604
+ let pressTimer;
1605
+ const release = () => {
1606
+ if (!pressing) return;
1607
+ pressing = false;
1608
+ clearTimeout(pressTimer);
1609
+ const run = afterPress;
1610
+ afterPress = void 0;
1611
+ if (run) setTimeout(run);
1612
+ };
1613
+ const onContextMenu = (event) => {
1614
+ if (event.buttons === 0) return;
1615
+ pressing = true;
1616
+ clearTimeout(pressTimer);
1617
+ pressTimer = setTimeout(release, 1e3);
1618
+ };
1619
+ const onContextMenuDone = (event) => {
1620
+ setTimeout(() => {
1621
+ if (!event.defaultPrevented) release();
1622
+ });
1623
+ };
1624
+ if (!nested) {
1625
+ document.addEventListener("contextmenu", onContextMenu, { capture: true });
1626
+ view.addEventListener("contextmenu", onContextMenuDone);
1627
+ for (const type of ["pointerup", "pointercancel"]) document.addEventListener(type, release, { capture: true });
1628
+ view.addEventListener("blur", release);
1629
+ }
1630
+ let wanted = false;
1631
+ return {
1632
+ sync(open) {
1633
+ wanted = open;
1634
+ const shown = root.matches(":popover-open");
1635
+ if (open && !shown) {
1636
+ pin(true);
1637
+ if (pressing) afterPress = () => wanted && !root.matches(":popover-open") && show();
1638
+ else show();
1639
+ } else if (!open) {
1640
+ const hadFocus = root.contains(document.activeElement);
1641
+ afterPress = void 0;
1642
+ reanchoring = false;
1643
+ overlay.hide();
1644
+ typed = "";
1645
+ shapeTree(root);
1646
+ if (hadFocus) returnTarget()?.focus();
1647
+ }
1648
+ },
1649
+ update() {
1650
+ pin(false);
1651
+ if (!root.matches(":popover-open")) {
1652
+ if (wanted && !pressing && isNewAnchor()) show(true);
1653
+ return;
1654
+ }
1655
+ if (pressing) {
1656
+ reanchoring = true;
1657
+ afterPress = () => {
1658
+ reanchoring = false;
1659
+ if (wanted) show(true);
1660
+ };
1661
+ return;
1662
+ }
1663
+ if (isNewAnchor()) show(true);
1664
+ else {
1665
+ remember();
1666
+ place().update();
1667
+ }
1668
+ },
1669
+ focusFirst,
1670
+ detach() {
1671
+ root.removeEventListener("keydown", onKeydown);
1672
+ root.removeEventListener("pointermove", onPointerMove);
1673
+ root.removeEventListener("focusin", onFocusIn);
1674
+ endGrace(state);
1675
+ document.removeEventListener("contextmenu", onContextMenu, { capture: true });
1676
+ view.removeEventListener("contextmenu", onContextMenuDone);
1677
+ for (const type of ["pointerup", "pointercancel"]) document.removeEventListener(type, release, { capture: true });
1678
+ view.removeEventListener("blur", release);
1679
+ clearTimeout(pressTimer);
1680
+ pressing = false;
1681
+ afterPress = void 0;
1682
+ overlay.detach();
1683
+ anchored?.detach();
1684
+ }
1685
+ };
1686
+ }
1687
+ let submenuIds = 0;
1688
+ /**
1689
+ * A submenu's behavior in the DOM: a menu (see `attachMenu`) that `item` opens, shown beside it.
1690
+ * `item` sits in a menu that `attachMenu` or this function runs, and `root` follows it.
1691
+ */
1692
+ function attachSubMenu(item, root, options) {
1693
+ const view = item.ownerDocument.defaultView;
1694
+ const parent = item.closest("[role=\"menu\"]");
1695
+ let open = false;
1696
+ /** Whether the opening asked for moves the focus in: from the keyboard, not the pointer */
1697
+ let focusIn = false;
1698
+ const request = (next, focus = false) => {
1699
+ focusIn = focus;
1700
+ options.onOpenChange(next);
1701
+ };
1702
+ const dom = setup(root, {
1703
+ anchor: () => item,
1704
+ props: () => SUBMENU_PLACEMENT,
1705
+ onClose: () => request(false)
1706
+ }, { focusOnShow: () => focusIn });
1707
+ const disabled = () => item.getAttribute("aria-disabled") === "true";
1708
+ root.id ||= `rio-submenu-${++submenuIds}`;
1709
+ item.setAttribute("popovertarget", root.id);
1710
+ item.setAttribute("popovertargetaction", "show");
1711
+ const onClickFirst = (event) => {
1712
+ focusIn = event.detail === 0;
1713
+ };
1714
+ const onClick = (event) => event.preventDefault();
1715
+ item.addEventListener("click", onClickFirst, { capture: true });
1716
+ item.addEventListener("click", onClick);
1717
+ let timer;
1718
+ const stopTimer = () => {
1719
+ clearTimeout(timer);
1720
+ timer = void 0;
1721
+ };
1722
+ const opener = {
1723
+ hover() {
1724
+ if (open || disabled() || timer !== void 0) return;
1725
+ timer = setTimeout(() => {
1726
+ timer = void 0;
1727
+ if (!parent || parent.matches(":popover-open")) request(true);
1728
+ }, HOVER_OPEN_MS);
1729
+ },
1730
+ cancel() {
1731
+ stopTimer();
1732
+ if (open) request(false);
1733
+ }
1734
+ };
1735
+ if (parent) stateOf(parent).openers.set(item, opener);
1736
+ const onPointerLeave = (event) => {
1737
+ if (event.pointerType !== "mouse") return;
1738
+ stopTimer();
1739
+ if (!open || !parent) return;
1740
+ const state = stateOf(parent);
1741
+ const box = root.getBoundingClientRect();
1742
+ const from = item.getBoundingClientRect();
1743
+ const toward = box.top >= from.bottom - 1 ? "down" : box.bottom <= from.top + 1 ? "up" : box.left >= from.left ? "right" : "left";
1744
+ const across = toward === "left" || toward === "right";
1745
+ const bleed = toward === "right" || toward === "down" ? -5 : 5;
1746
+ const exit = across ? {
1747
+ x: event.clientX + bleed,
1748
+ y: event.clientY
1749
+ } : {
1750
+ x: event.clientX,
1751
+ y: event.clientY + bleed
1752
+ };
1753
+ const near = {
1754
+ right: box.left,
1755
+ left: box.right,
1756
+ down: box.top,
1757
+ up: box.bottom
1758
+ }[toward];
1759
+ const far = {
1760
+ right: box.right,
1761
+ left: box.left,
1762
+ down: box.bottom,
1763
+ up: box.top
1764
+ }[toward];
1765
+ endGrace(state);
1766
+ state.grace = {
1767
+ area: across ? [
1768
+ exit,
1769
+ {
1770
+ x: near,
1771
+ y: box.top
1772
+ },
1773
+ {
1774
+ x: far,
1775
+ y: box.top
1776
+ },
1777
+ {
1778
+ x: far,
1779
+ y: box.bottom
1780
+ },
1781
+ {
1782
+ x: near,
1783
+ y: box.bottom
1784
+ }
1785
+ ] : [
1786
+ exit,
1787
+ {
1788
+ x: box.left,
1789
+ y: near
1790
+ },
1791
+ {
1792
+ x: box.left,
1793
+ y: far
1794
+ },
1795
+ {
1796
+ x: box.right,
1797
+ y: far
1798
+ },
1799
+ {
1800
+ x: box.right,
1801
+ y: near
1802
+ }
1803
+ ],
1804
+ toward,
1805
+ timer: setTimeout(() => endGrace(state), GRACE_MS)
1806
+ };
1807
+ };
1808
+ item.addEventListener("pointerleave", onPointerLeave);
1809
+ const onKeydown = (event) => {
1810
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
1811
+ const forward = view.getComputedStyle(item).direction === "rtl" ? "ArrowLeft" : "ArrowRight";
1812
+ const space = event.key === " " && !(parent && stateOf(parent).typing());
1813
+ if (event.key !== forward && event.key !== "Enter" && !space) return;
1814
+ event.preventDefault();
1815
+ if (disabled()) return;
1816
+ if (open) dom.focusFirst();
1817
+ else request(true, true);
1818
+ };
1819
+ item.addEventListener("keydown", onKeydown);
1820
+ return {
1821
+ sync(next) {
1822
+ open = next;
1823
+ if (!next) stopTimer();
1824
+ dom.sync(next);
101
1825
  },
102
- handleClick: button.handleClick
1826
+ detach() {
1827
+ stopTimer();
1828
+ item.removeEventListener("click", onClickFirst, { capture: true });
1829
+ item.removeEventListener("click", onClick);
1830
+ item.removeEventListener("pointerleave", onPointerLeave);
1831
+ item.removeEventListener("keydown", onKeydown);
1832
+ item.removeAttribute("popovertarget");
1833
+ item.removeAttribute("popovertargetaction");
1834
+ if (parent && stateOf(parent).openers.get(item) === opener) stateOf(parent).openers.delete(item);
1835
+ dom.detach();
1836
+ }
103
1837
  };
104
1838
  }
105
1839
  // src/primitives/drag/drag.ts
@@ -219,7 +1953,7 @@ const PADDING = 10;
219
1953
  const SOFT_EDGE_RATIO = .35;
220
1954
  const SOFT_EDGE_MINIMUM = 75;
221
1955
  const isDisabled = (el) => el.matches(":disabled, [aria-disabled=\"true\"]");
222
- const reducedMotion$1 = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1956
+ const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
223
1957
  const setPressed = (els, pressed) => {
224
1958
  for (const el of els) if (pressed) el.dataset.pressed = "";
225
1959
  else delete el.dataset.pressed;
@@ -264,7 +1998,7 @@ function attachRipple(element, options = {}) {
264
1998
  animation?.cancel();
265
1999
  animation = surface.animate([frame(from, radius.from), frame(to, radius.to)], {
266
2000
  pseudoElement: "::after",
267
- duration: reducedMotion$1() ? 0 : GROW_MS,
2001
+ duration: reducedMotion() ? 0 : GROW_MS,
268
2002
  easing: GROW_EASING,
269
2003
  fill: "forwards"
270
2004
  });
@@ -337,138 +2071,13 @@ function attachRipple(element, options = {}) {
337
2071
  element.removeEventListener("click", onClick);
338
2072
  } };
339
2073
  }
340
- // src/primitives/spring/spring.ts
341
- /**
342
- * Where a spring is `t` seconds after it starts at `from` with `velocity`, heading to `to`.
343
- */
344
- function solveSpring({ dampingRatio: zeta, stiffness }, from, to, velocity, t) {
345
- const w0 = Math.sqrt(stiffness);
346
- const x0 = from - to;
347
- const v0 = velocity;
348
- if (Math.abs(zeta - 1) < 1e-6) {
349
- const b = v0 + w0 * x0;
350
- const decay = Math.exp(-w0 * t);
351
- return {
352
- value: to + (x0 + b * t) * decay,
353
- velocity: (b - w0 * (x0 + b * t)) * decay
354
- };
355
- }
356
- if (zeta < 1) {
357
- const a = zeta * w0;
358
- const wd = w0 * Math.sqrt(1 - zeta * zeta);
359
- const b = (v0 + a * x0) / wd;
360
- const decay = Math.exp(-a * t);
361
- const cos = Math.cos(wd * t);
362
- const sin = Math.sin(wd * t);
363
- return {
364
- value: to + decay * (x0 * cos + b * sin),
365
- velocity: decay * ((wd * b - a * x0) * cos - (a * b + wd * x0) * sin)
366
- };
367
- }
368
- const root = w0 * Math.sqrt(zeta * zeta - 1);
369
- const r1 = -zeta * w0 + root;
370
- const r2 = -zeta * w0 - root;
371
- const c2 = (v0 - r1 * x0) / (r2 - r1);
372
- const c1 = x0 - c2;
373
- const e1 = Math.exp(r1 * t);
374
- const e2 = Math.exp(r2 * t);
375
- return {
376
- value: to + c1 * e1 + c2 * e2,
377
- velocity: r1 * c1 * e1 + r2 * c2 * e2
378
- };
379
- }
380
- const now = () => performance.now() / 1e3;
381
- /** A spring that animates a single number with `requestAnimationFrame`. */
382
- function createSpring(options) {
383
- const precision = options.precision ?? .01;
384
- let from = options.value;
385
- let target = options.value;
386
- let startVelocity = 0;
387
- let params;
388
- let startedAt = 0;
389
- let frame;
390
- const state = () => params && frame !== void 0 ? solveSpring(params, from, target, startVelocity, now() - startedAt) : {
391
- value: target,
392
- velocity: 0
393
- };
394
- const settle = () => {
395
- if (frame !== void 0) cancelAnimationFrame(frame);
396
- frame = void 0;
397
- from = target;
398
- startVelocity = 0;
399
- options.onUpdate(target);
400
- options.onRest?.();
401
- };
402
- const tick = () => {
403
- const { value, velocity } = state();
404
- if (Math.abs(value - target) < precision && Math.abs(velocity) < precision * 10) {
405
- settle();
406
- return;
407
- }
408
- options.onUpdate(value);
409
- frame = requestAnimationFrame(tick);
410
- };
411
- return {
412
- get value() {
413
- return state().value;
414
- },
415
- get velocity() {
416
- return state().velocity;
417
- },
418
- get target() {
419
- return target;
420
- },
421
- get moving() {
422
- return frame !== void 0;
423
- },
424
- to(next, nextParams, velocity) {
425
- const current = state();
426
- from = current.value;
427
- startVelocity = velocity ?? current.velocity;
428
- target = next;
429
- params = nextParams;
430
- startedAt = now();
431
- if (frame === void 0) frame = requestAnimationFrame(tick);
432
- },
433
- jump(value) {
434
- target = value;
435
- settle();
436
- },
437
- stop() {
438
- const { value } = state();
439
- if (frame !== void 0) cancelAnimationFrame(frame);
440
- frame = void 0;
441
- from = target = value;
442
- startVelocity = 0;
443
- }
444
- };
445
- }
446
- /**
447
- * Reads a motion token's spring from CSS variables on an element, for example
448
- * `--rio-motion-fast-spatial-damping` and `-stiffness`. Reading them from the element, rather than
449
- * from the tokens package, lets a subtree that swaps motion schemes change its springs too.
450
- * Returns `undefined` when the theme doesn't define them.
451
- */
452
- function readSpringParams(element, token) {
453
- const style = getComputedStyle(element);
454
- const dampingRatio = parseFloat(style.getPropertyValue(`--rio-motion-${token}-damping`));
455
- const stiffness = parseFloat(style.getPropertyValue(`--rio-motion-${token}-stiffness`));
456
- return Number.isFinite(dampingRatio) && Number.isFinite(stiffness) ? {
457
- dampingRatio,
458
- stiffness
459
- } : void 0;
460
- }
461
2074
  // src/primitives/spring-property/spring-property.ts
462
- /** The data attributes Rio's components use for state. */
463
- const STATE_ATTRIBUTES = [
464
- "data-pressed",
465
- "data-dragging",
466
- "data-selected",
467
- "data-shape",
468
- "data-size",
469
- "data-icon"
2075
+ /** The transition events that report a change of the target. */
2076
+ const EVENTS = [
2077
+ "transitionrun",
2078
+ "transitionend",
2079
+ "transitioncancel"
470
2080
  ];
471
- const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
472
2081
  function attachSpringProperty(element, options) {
473
2082
  const token = options.token ?? "fast-spatial";
474
2083
  const read = () => parseFloat(getComputedStyle(element).getPropertyValue(options.target));
@@ -485,16 +2094,20 @@ function attachSpringProperty(element, options) {
485
2094
  const target = read();
486
2095
  if (!Number.isFinite(target)) return;
487
2096
  const params = readSpringParams(element, token);
488
- if (!params || reducedMotion()) spring.jump(target);
2097
+ if (!params || prefersReducedMotion()) spring.jump(target);
489
2098
  else spring.to(target, params, velocity);
490
2099
  };
491
- const observer = new MutationObserver(() => {
2100
+ const onTransition = (event) => {
2101
+ if (event.target !== element || event.propertyName !== options.target) return;
492
2102
  if (!held && read() !== spring.target) go();
2103
+ };
2104
+ for (const type of EVENTS) element.addEventListener(type, onTransition);
2105
+ const resync = new ResizeObserver(() => {
2106
+ if (held || spring.moving) return;
2107
+ const now = read();
2108
+ if (Number.isFinite(now) && now !== spring.target) spring.jump(now);
493
2109
  });
494
- observer.observe(element, {
495
- attributes: true,
496
- attributeFilter: options.watch ?? STATE_ATTRIBUTES
497
- });
2110
+ resync.observe(element);
498
2111
  return {
499
2112
  get value() {
500
2113
  return spring.value;
@@ -508,25 +2121,182 @@ function attachSpringProperty(element, options) {
508
2121
  go(velocity);
509
2122
  },
510
2123
  detach() {
511
- observer.disconnect();
2124
+ for (const type of EVENTS) element.removeEventListener(type, onTransition);
2125
+ resync.disconnect();
512
2126
  spring.stop();
513
2127
  options.apply(void 0);
514
2128
  }
515
2129
  };
516
2130
  }
517
2131
  // src/primitives/shape-morph/shape-morph.ts
2132
+ const CORNERS = {
2133
+ start: ["border-start-start-radius", "border-end-start-radius"],
2134
+ end: ["border-start-end-radius", "border-end-end-radius"]
2135
+ };
518
2136
  function attachShapeMorph(element, options = {}) {
519
- const radius = attachSpringProperty(element, {
520
- ...options,
521
- target: "--rio-shape-morph-target",
522
- apply: (value) => value === void 0 ? element.style.removeProperty("border-radius") : element.style.setProperty("border-radius", `${value}px`)
523
- });
524
2137
  element.dataset.shapeMorph = "";
2138
+ const springs = ["start", "end"].map((side) => attachSpringProperty(element, {
2139
+ ...options,
2140
+ target: `--rio-shape-morph-${side}-target`,
2141
+ apply: (value) => {
2142
+ for (const corner of CORNERS[side]) if (value === void 0) element.style.removeProperty(corner);
2143
+ else element.style.setProperty(corner, `${value}px`);
2144
+ }
2145
+ }));
525
2146
  return { detach() {
526
- radius.detach();
2147
+ for (const spring of springs) spring.detach();
527
2148
  delete element.dataset.shapeMorph;
528
2149
  } };
529
2150
  }
2151
+ // src/split-button/menu-icon.ts
2152
+ const inlineVar$1 = (el, name, unit) => (value) => value === void 0 ? el.style.removeProperty(name) : el.style.setProperty(name, `${value}${unit}`);
2153
+ /** Attach to the trailing button. */
2154
+ function attachSplitButtonMenuIcon(trailing) {
2155
+ const token = "standard-fast-spatial";
2156
+ const turn = attachSpringProperty(trailing, {
2157
+ target: "--rio-split-button-menu-turn-target",
2158
+ token,
2159
+ apply: inlineVar$1(trailing, "--_menu-turn", "deg")
2160
+ });
2161
+ const shift = attachSpringProperty(trailing, {
2162
+ target: "--rio-split-button-menu-shift-target",
2163
+ token,
2164
+ apply: inlineVar$1(trailing, "--_menu-shift", "px")
2165
+ });
2166
+ return { detach() {
2167
+ turn.detach();
2168
+ shift.detach();
2169
+ } };
2170
+ }
2171
+ // src/split-button/split-button.ts
2172
+ /** Split button colors: the button's, without text. */
2173
+ const SPLIT_BUTTON_VARIANTS = [
2174
+ "filled",
2175
+ "tonal",
2176
+ "outlined",
2177
+ "elevated"
2178
+ ];
2179
+ const splitButtonDefaults = {
2180
+ variant: "filled",
2181
+ size: "small",
2182
+ open: false,
2183
+ popup: "menu",
2184
+ disabled: false,
2185
+ softDisabled: false,
2186
+ type: "button"
2187
+ };
2188
+ /**
2189
+ * How the adapter renders the split button's parts. The icon and the label hold the leading
2190
+ * button's content; the menu icon holds the expand icon the app provides.
2191
+ */
2192
+ const splitButtonSkeleton = {
2193
+ part: "root",
2194
+ tag: "div",
2195
+ children: [{
2196
+ part: "leading",
2197
+ tag: "button",
2198
+ children: [
2199
+ {
2200
+ part: "touch",
2201
+ tag: "span"
2202
+ },
2203
+ {
2204
+ part: "icon",
2205
+ tag: "span",
2206
+ when: "showsIcon",
2207
+ content: true
2208
+ },
2209
+ {
2210
+ part: "label",
2211
+ tag: "span",
2212
+ when: "showsLabel",
2213
+ content: true
2214
+ }
2215
+ ]
2216
+ }, {
2217
+ part: "trailing",
2218
+ tag: "button",
2219
+ children: [{
2220
+ part: "touch",
2221
+ tag: "span"
2222
+ }, {
2223
+ part: "menuIcon",
2224
+ tag: "span",
2225
+ content: true
2226
+ }]
2227
+ }]
2228
+ };
2229
+ function createSplitButton(options) {
2230
+ const props = () => ({
2231
+ ...splitButtonDefaults,
2232
+ ...omitUndefined(options.props())
2233
+ });
2234
+ const content = () => options.content?.() ?? {
2235
+ icon: false,
2236
+ label: true
2237
+ };
2238
+ const shared = () => {
2239
+ const p = props();
2240
+ return {
2241
+ variant: p.variant,
2242
+ size: p.size,
2243
+ disabled: p.disabled,
2244
+ softDisabled: p.softDisabled
2245
+ };
2246
+ };
2247
+ const leading = createButton({
2248
+ props: () => ({
2249
+ ...shared(),
2250
+ type: props().type
2251
+ }),
2252
+ onClick: options.onClick,
2253
+ onChange: () => {}
2254
+ });
2255
+ const trailing = createButton({
2256
+ props: () => ({
2257
+ ...shared(),
2258
+ type: "button"
2259
+ }),
2260
+ onClick: () => options.onChange(!props().open),
2261
+ onChange: () => {}
2262
+ });
2263
+ return {
2264
+ parts: {
2265
+ root: () => ({ class: "rio-split-button" }),
2266
+ leading() {
2267
+ const root = leading.parts.root();
2268
+ return {
2269
+ ...root,
2270
+ class: `${root.class} rio-split-button__leading`,
2271
+ "aria-label": content().label ? void 0 : props().label
2272
+ };
2273
+ },
2274
+ trailing() {
2275
+ const p = props();
2276
+ const root = trailing.parts.root();
2277
+ return {
2278
+ ...root,
2279
+ class: `${root.class} rio-split-button__trailing`,
2280
+ "aria-label": p.menuLabel,
2281
+ "aria-haspopup": p.popup,
2282
+ "aria-expanded": String(p.open),
2283
+ "data-open": p.open ? "" : void 0
2284
+ };
2285
+ },
2286
+ touch: leading.parts.touch,
2287
+ icon: leading.parts.icon,
2288
+ label: leading.parts.label,
2289
+ menuIcon: () => ({
2290
+ class: "rio-split-button__menu-icon",
2291
+ "aria-hidden": "true"
2292
+ })
2293
+ },
2294
+ showsIcon: () => content().icon,
2295
+ showsLabel: () => content().label,
2296
+ handleLeadingClick: leading.handleClick,
2297
+ handleTrailingClick: trailing.handleClick
2298
+ };
2299
+ }
530
2300
  // src/switch/handle.ts
531
2301
  /** A release faster than this (px/s) picks the side it's heading to, wherever the handle is. */
532
2302
  const FLING_VELOCITY = 300;
@@ -559,7 +2329,7 @@ function attachSwitchHandle(root, options) {
559
2329
  onEnd({ velocityX }) {
560
2330
  const velocity = direction * velocityX;
561
2331
  const next = Math.abs(velocity) > FLING_VELOCITY ? velocity > 0 : x.value > (off + on) / 2;
562
- if (next !== options.selected()) options.onSelectedChange(next);
2332
+ if (next !== options.selected()) options.onChange(next);
563
2333
  x.release(velocity);
564
2334
  },
565
2335
  onCancel() {
@@ -578,28 +2348,451 @@ const switchDefaults = {
578
2348
  disabled: false,
579
2349
  softDisabled: false
580
2350
  };
2351
+ /**
2352
+ * How the adapter renders the switch's parts. The state part is the circle where the state layer
2353
+ * and the ripple draw. The icon holds the selected icon when `showsSelectedIcon()`, otherwise the
2354
+ * unselected one.
2355
+ */
2356
+ const switchSkeleton = {
2357
+ part: "root",
2358
+ tag: "button",
2359
+ children: [
2360
+ {
2361
+ part: "touch",
2362
+ tag: "span"
2363
+ },
2364
+ {
2365
+ part: "state",
2366
+ tag: "span"
2367
+ },
2368
+ {
2369
+ part: "handle",
2370
+ tag: "span",
2371
+ children: [{
2372
+ part: "icon",
2373
+ tag: "span",
2374
+ when: "showsIcon",
2375
+ content: true
2376
+ }]
2377
+ }
2378
+ ]
2379
+ };
581
2380
  function createSwitch(options) {
2381
+ const showsIcon = () => {
2382
+ const content = options.content?.();
2383
+ return !!(options.props().selected ? content?.selectedIcon : content?.unselectedIcon);
2384
+ };
582
2385
  return {
583
- rootAttrs() {
584
- const p = options.props();
585
- const selected = String(!!p.selected);
586
- return {
587
- type: "button",
588
- role: "switch",
589
- "aria-checked": selected,
590
- disabled: p.disabled ? "" : void 0,
591
- "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
592
- "data-selected": selected
593
- };
2386
+ parts: {
2387
+ root() {
2388
+ const p = options.props();
2389
+ const selected = String(!!p.selected);
2390
+ return {
2391
+ class: "rio-switch rio-focus-ring",
2392
+ type: "button",
2393
+ role: "switch",
2394
+ "aria-checked": selected,
2395
+ disabled: p.disabled ? "" : void 0,
2396
+ "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
2397
+ "data-selected": selected,
2398
+ "data-icon": showsIcon() ? "" : void 0
2399
+ };
2400
+ },
2401
+ touch: () => ({
2402
+ class: "rio-switch__touch",
2403
+ "aria-hidden": "true"
2404
+ }),
2405
+ state: () => ({
2406
+ class: "rio-switch__state",
2407
+ "aria-hidden": "true"
2408
+ }),
2409
+ handle: () => ({
2410
+ class: "rio-switch__handle",
2411
+ "aria-hidden": "true"
2412
+ }),
2413
+ icon: () => ({ class: "rio-switch__icon" })
594
2414
  },
2415
+ showsIcon,
2416
+ showsSelectedIcon: () => !!(options.props().selected && options.content?.().selectedIcon),
595
2417
  handleClick(event) {
596
2418
  const p = options.props();
597
2419
  if (p.disabled || p.softDisabled) {
598
2420
  event.preventDefault();
599
2421
  return;
600
2422
  }
601
- options.onSelectedChange(!p.selected);
2423
+ options.onChange(!p.selected);
2424
+ }
2425
+ };
2426
+ }
2427
+ // src/tooltip/interaction.ts
2428
+ /** How long the pointer rests on the element before its tooltip shows */
2429
+ const SHOW_DELAY_MS = 500;
2430
+ /** How long a tooltip stays after the pointer and the focus leave */
2431
+ const HIDE_DELAY_MS = 1500;
2432
+ /** How soon after a tooltip closes the next one still shows at once */
2433
+ const WARM_MS = 300;
2434
+ /** How long a touch is held to show the tooltip */
2435
+ const LONG_PRESS_MS = 500;
2436
+ /** How long after a touch that showed the tooltip ends its click is swallowed */
2437
+ const SWALLOW_CLICK_MS = 600;
2438
+ /** How far a held touch may move, in px, and still count as held */
2439
+ const MOVE_SLOP = 10;
2440
+ const shown = /* @__PURE__ */ new Set();
2441
+ let lastClosedAt = -Infinity;
2442
+ const warm = () => shown.size > 0 || performance.now() - lastClosedAt < WARM_MS;
2443
+ const textOf = (element) => (element?.textContent ?? "").replace(/\s+/g, " ").trim();
2444
+ /** The element's accessible name, near enough to tell whether a tooltip repeats it */
2445
+ const nameOf = (element) => {
2446
+ const labelledBy = element.getAttribute("aria-labelledby");
2447
+ if (labelledBy) {
2448
+ const document = element.ownerDocument;
2449
+ return labelledBy.split(/\s+/).map((id) => textOf(document.getElementById(id))).join(" ").trim();
2450
+ }
2451
+ return element.getAttribute("aria-label")?.trim() || textOf(element);
2452
+ };
2453
+ /** Adds or removes one id in a space-separated attribute, keeping the others. */
2454
+ const setToken = (element, name, id, on) => {
2455
+ const ids = (element.getAttribute(name) ?? "").split(/\s+/).filter((each) => each && each !== id);
2456
+ if (on) ids.push(id);
2457
+ if (ids.length) element.setAttribute(name, ids.join(" "));
2458
+ else element.removeAttribute(name);
2459
+ };
2460
+ function attachTooltip(trigger, root, options) {
2461
+ const document = trigger.ownerDocument;
2462
+ const view = document.defaultView;
2463
+ /** The element the tooltip is for: the trigger's first child, ahead of the tooltip */
2464
+ const element = () => {
2465
+ const first = trigger.firstElementChild;
2466
+ return first instanceof view.HTMLElement && first !== root ? first : null;
2467
+ };
2468
+ const isOpen = () => root.matches(":popover-open");
2469
+ const request = (open) => options.onOpenChange(open);
2470
+ let overlay;
2471
+ let overlayPersistent;
2472
+ /** When the pointer last went down on the element, whether the tooltip was open */
2473
+ let openAtPress = false;
2474
+ const overlayNow = () => {
2475
+ const persistent = options.persistent();
2476
+ if (overlay && persistent === overlayPersistent) return overlay;
2477
+ overlay?.detach();
2478
+ overlayPersistent = persistent;
2479
+ const onClose = () => request(false);
2480
+ overlay = attachOverlay(root, persistent ? {
2481
+ mode: "auto",
2482
+ contain: true,
2483
+ onClose
2484
+ } : {
2485
+ mode: "manual",
2486
+ transient: true,
2487
+ contain: true,
2488
+ onClose
2489
+ });
2490
+ return overlay;
2491
+ };
2492
+ let anchored;
2493
+ let placement = "";
2494
+ const place = () => {
2495
+ const p = options.placement();
2496
+ const key = `${p.side} ${p.align} ${p.offset}`;
2497
+ if (anchored && key === placement) return anchored;
2498
+ anchored?.detach();
2499
+ placement = key;
2500
+ anchored = attachAnchoredPosition(root, {
2501
+ anchor: element,
2502
+ side: p.side,
2503
+ align: p.align,
2504
+ offset: p.offset
2505
+ });
2506
+ return anchored;
2507
+ };
2508
+ let described;
2509
+ let expanded;
2510
+ const updateAria = () => {
2511
+ const target = element();
2512
+ const text = options.interactive() ? root.querySelector(".rio-tooltip__supporting-text") : root;
2513
+ const describe = !!target && !!text?.id && nameOf(target) !== textOf(text);
2514
+ if (described && (!describe || described.element !== target || described.id !== text.id)) {
2515
+ setToken(described.element, "aria-describedby", described.id, false);
2516
+ described = void 0;
2517
+ }
2518
+ if (describe && !described) {
2519
+ setToken(target, "aria-describedby", text.id, true);
2520
+ described = {
2521
+ element: target,
2522
+ id: text.id
2523
+ };
2524
+ }
2525
+ const persistent = options.persistent();
2526
+ if (expanded && (!persistent || expanded !== target)) {
2527
+ expanded.removeAttribute("aria-expanded");
2528
+ expanded = void 0;
2529
+ }
2530
+ if (persistent && target) {
2531
+ target.setAttribute("aria-expanded", String(isOpen()));
2532
+ expanded = target;
2533
+ }
2534
+ };
2535
+ let showTimer;
2536
+ let hideTimer;
2537
+ const stopShow = () => {
2538
+ clearTimeout(showTimer);
2539
+ showTimer = void 0;
2540
+ };
2541
+ const stopHide = () => {
2542
+ clearTimeout(hideTimer);
2543
+ hideTimer = void 0;
2544
+ };
2545
+ const hideLater = () => {
2546
+ stopHide();
2547
+ hideTimer = setTimeout(() => {
2548
+ hideTimer = void 0;
2549
+ const focused = document.activeElement;
2550
+ const keyboard = !!focused && trigger.contains(focused) && focused.matches(":focus-visible");
2551
+ if (!trigger.matches(":hover") && !keyboard) request(false);
2552
+ }, HIDE_DELAY_MS);
2553
+ };
2554
+ const onPointerEnter = (event) => {
2555
+ if (event.pointerType !== "mouse" || options.persistent()) return;
2556
+ stopHide();
2557
+ if (isOpen() || showTimer !== void 0) return;
2558
+ if (warm()) {
2559
+ request(true);
2560
+ return;
2561
+ }
2562
+ showTimer = setTimeout(() => {
2563
+ showTimer = void 0;
2564
+ request(true);
2565
+ }, SHOW_DELAY_MS);
2566
+ };
2567
+ const onPointerLeave = (event) => {
2568
+ if (event.pointerType !== "mouse" || options.persistent()) return;
2569
+ stopShow();
2570
+ if (isOpen()) hideLater();
2571
+ };
2572
+ const onFocusIn = (event) => {
2573
+ if (options.persistent()) return;
2574
+ const focused = event.target;
2575
+ if (!root.contains(focused) && !focused.matches(":focus-visible")) return;
2576
+ stopShow();
2577
+ stopHide();
2578
+ if (!isOpen()) request(true);
2579
+ };
2580
+ const onFocusOut = (event) => {
2581
+ const next = event.relatedTarget;
2582
+ if (next && trigger.contains(next)) return;
2583
+ if (options.persistent()) {
2584
+ if (next && isOpen()) request(false);
2585
+ return;
2586
+ }
2587
+ if (isOpen()) hideLater();
2588
+ };
2589
+ let pressTimer;
2590
+ let pressedAt;
2591
+ let swallowUntil = -Infinity;
2592
+ const stopPress = () => {
2593
+ clearTimeout(pressTimer);
2594
+ pressTimer = void 0;
2595
+ pressedAt = void 0;
2596
+ };
2597
+ const onPointerDown = (event) => {
2598
+ if (!element()?.contains(event.target)) return;
2599
+ openAtPress = isOpen();
2600
+ if (options.persistent()) return;
2601
+ stopShow();
2602
+ if (event.pointerType === "touch") {
2603
+ stopPress();
2604
+ pressedAt = {
2605
+ x: event.clientX,
2606
+ y: event.clientY
2607
+ };
2608
+ pressTimer = setTimeout(() => {
2609
+ stopPress();
2610
+ swallowUntil = Infinity;
2611
+ stopHide();
2612
+ request(true);
2613
+ }, LONG_PRESS_MS);
2614
+ return;
2615
+ }
2616
+ stopHide();
2617
+ if (isOpen()) request(false);
2618
+ };
2619
+ const onPointerMove = (event) => {
2620
+ if (!pressedAt) return;
2621
+ if (Math.hypot(event.clientX - pressedAt.x, event.clientY - pressedAt.y) > MOVE_SLOP) stopPress();
2622
+ };
2623
+ const onPointerUp = (event) => {
2624
+ if (event.pointerType !== "touch" || options.persistent()) return;
2625
+ stopPress();
2626
+ if (swallowUntil === Infinity) {
2627
+ swallowUntil = performance.now() + SWALLOW_CLICK_MS;
2628
+ if (isOpen()) hideLater();
2629
+ }
2630
+ };
2631
+ const onPointerCancel = () => {
2632
+ stopPress();
2633
+ swallowUntil = -Infinity;
2634
+ };
2635
+ const onContextMenu = (event) => {
2636
+ if (pressTimer !== void 0 || performance.now() < swallowUntil) event.preventDefault();
2637
+ };
2638
+ const onClickFirst = (event) => {
2639
+ if (performance.now() >= swallowUntil || !element()?.contains(event.target)) return;
2640
+ swallowUntil = -Infinity;
2641
+ event.preventDefault();
2642
+ event.stopImmediatePropagation();
2643
+ };
2644
+ const onClick = (event) => {
2645
+ if (!options.persistent()) return;
2646
+ if (!element()?.contains(event.target)) return;
2647
+ request(!(event.detail === 0 ? isOpen() : openAtPress));
2648
+ };
2649
+ trigger.addEventListener("pointerenter", onPointerEnter);
2650
+ trigger.addEventListener("pointerleave", onPointerLeave);
2651
+ trigger.addEventListener("focusin", onFocusIn);
2652
+ trigger.addEventListener("focusout", onFocusOut);
2653
+ trigger.addEventListener("pointerdown", onPointerDown);
2654
+ trigger.addEventListener("pointermove", onPointerMove);
2655
+ trigger.addEventListener("pointerup", onPointerUp);
2656
+ trigger.addEventListener("pointercancel", onPointerCancel);
2657
+ trigger.addEventListener("contextmenu", onContextMenu);
2658
+ trigger.addEventListener("click", onClickFirst, { capture: true });
2659
+ trigger.addEventListener("click", onClick);
2660
+ overlayNow();
2661
+ updateAria();
2662
+ return {
2663
+ sync(open) {
2664
+ const showing = isOpen();
2665
+ if (open && !showing) {
2666
+ const target = element();
2667
+ if (!target) {
2668
+ request(false);
2669
+ return;
2670
+ }
2671
+ overlayNow().show(target);
2672
+ place().update();
2673
+ shown.add(root);
2674
+ } else if (!open) {
2675
+ stopShow();
2676
+ stopHide();
2677
+ if (showing) overlay?.hide();
2678
+ if (shown.delete(root)) lastClosedAt = performance.now();
2679
+ }
2680
+ updateAria();
2681
+ },
2682
+ update: updateAria,
2683
+ detach() {
2684
+ trigger.removeEventListener("pointerenter", onPointerEnter);
2685
+ trigger.removeEventListener("pointerleave", onPointerLeave);
2686
+ trigger.removeEventListener("focusin", onFocusIn);
2687
+ trigger.removeEventListener("focusout", onFocusOut);
2688
+ trigger.removeEventListener("pointerdown", onPointerDown);
2689
+ trigger.removeEventListener("pointermove", onPointerMove);
2690
+ trigger.removeEventListener("pointerup", onPointerUp);
2691
+ trigger.removeEventListener("pointercancel", onPointerCancel);
2692
+ trigger.removeEventListener("contextmenu", onContextMenu);
2693
+ trigger.removeEventListener("click", onClickFirst, { capture: true });
2694
+ trigger.removeEventListener("click", onClick);
2695
+ stopShow();
2696
+ stopHide();
2697
+ stopPress();
2698
+ if (described) setToken(described.element, "aria-describedby", described.id, false);
2699
+ expanded?.removeAttribute("aria-expanded");
2700
+ if (shown.delete(root)) lastClosedAt = performance.now();
2701
+ overlay?.detach();
2702
+ anchored?.detach();
2703
+ }
2704
+ };
2705
+ }
2706
+ // src/tooltip/tooltip.ts
2707
+ /** M3 tooltip variants: a brief label, or a card with a subhead, text, and actions. */
2708
+ const TOOLTIP_VARIANTS = ["plain", "rich"];
2709
+ const tooltipDefaults = {
2710
+ variant: "plain",
2711
+ persistent: false,
2712
+ open: false,
2713
+ offset: 4
2714
+ };
2715
+ /** The trigger holds the app's element first, then the tooltip (`tooltipSkeleton`). */
2716
+ const tooltipTriggerSkeleton = {
2717
+ part: "trigger",
2718
+ tag: "span",
2719
+ content: true
2720
+ };
2721
+ /** Spans throughout, so a tooltip may sit inside a paragraph. */
2722
+ const tooltipSkeleton = {
2723
+ part: "root",
2724
+ tag: "span",
2725
+ children: [
2726
+ {
2727
+ part: "subhead",
2728
+ tag: "span",
2729
+ when: "showsSubhead",
2730
+ content: true
2731
+ },
2732
+ {
2733
+ part: "supportingText",
2734
+ tag: "span",
2735
+ content: true
2736
+ },
2737
+ {
2738
+ part: "actions",
2739
+ tag: "span",
2740
+ when: "showsActions",
2741
+ content: true
2742
+ }
2743
+ ]
2744
+ };
2745
+ const NO_CONTENT = {
2746
+ subhead: false,
2747
+ actions: false
2748
+ };
2749
+ function createTooltip(options) {
2750
+ const props = () => ({
2751
+ ...tooltipDefaults,
2752
+ ...omitUndefined(options.props())
2753
+ });
2754
+ const rich = () => props().variant === "rich";
2755
+ const content = () => options.content?.() ?? NO_CONTENT;
2756
+ const showsSubhead = () => rich() && content().subhead;
2757
+ const showsActions = () => rich() && content().actions;
2758
+ const interactive = showsActions;
2759
+ const { ids } = options;
2760
+ return {
2761
+ parts: {
2762
+ trigger: () => ({ class: "rio-tooltip-trigger" }),
2763
+ root() {
2764
+ const p = props();
2765
+ const dialog = interactive();
2766
+ return {
2767
+ class: "rio-tooltip",
2768
+ id: ids.root,
2769
+ role: dialog ? "dialog" : "tooltip",
2770
+ "aria-labelledby": dialog ? showsSubhead() ? ids.subhead : ids.supportingText : void 0,
2771
+ "data-variant": p.variant
2772
+ };
2773
+ },
2774
+ subhead: () => ({
2775
+ class: "rio-tooltip__subhead",
2776
+ id: ids.subhead
2777
+ }),
2778
+ supportingText: () => ({
2779
+ class: "rio-tooltip__supporting-text",
2780
+ id: ids.supportingText
2781
+ }),
2782
+ actions: () => ({ class: "rio-tooltip__actions" })
2783
+ },
2784
+ showsSubhead,
2785
+ showsActions,
2786
+ interactive,
2787
+ persistent: () => rich() && props().persistent,
2788
+ placement() {
2789
+ const p = props();
2790
+ return {
2791
+ side: p.side ?? (rich() ? "bottom" : "top"),
2792
+ align: p.align ?? (rich() ? "start" : "center"),
2793
+ offset: p.offset
2794
+ };
602
2795
  }
603
2796
  };
604
2797
  }
605
- export { BUTTON_SIZES, BUTTON_VARIANTS, ICON_BUTTON_VARIANTS, ICON_BUTTON_WIDTHS, STATE_ATTRIBUTES, attachDrag, attachRipple, attachShapeMorph, attachSpringProperty, attachSwitchHandle, buttonDefaults, createButton, createIconButton, createSpring, createSwitch, iconButtonDefaults, readSpringParams, solveSpring, switchDefaults };
2798
+ export { BUTTON_GROUP_SELECTIONS, BUTTON_GROUP_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, DIALOG_ROLES, ICON_BUTTON_VARIANTS, ICON_BUTTON_WIDTHS, MENU_SELECTIONS, MENU_VARIANTS, SPLIT_BUTTON_VARIANTS, TOOLTIP_VARIANTS, attachAnchoredPosition, attachButtonGroup, attachDialog, attachDrag, attachMenu, attachOverlay, attachRipple, attachShapeMorph, attachSplitButtonMenuIcon, attachSpringProperty, attachSubMenu, attachSwitchHandle, attachTooltip, buttonDefaults, buttonGroupDefaults, buttonGroupSkeleton, buttonSkeleton, createButton, createButtonGroup, createDialog, createIconButton, createMenu, createMenuDivider, createMenuGroup, createMenuItem, createSplitButton, createSpring, createSubMenu, createSwitch, createTooltip, dialogDefaults, dialogSkeleton, iconButtonDefaults, iconButtonSkeleton, menuDefaults, menuDividerSkeleton, menuGroupSkeleton, menuItemDefaults, menuItemSkeleton, menuSkeleton, prefersReducedMotion, readSpringParams, solveSpring, splitButtonDefaults, splitButtonSkeleton, switchDefaults, switchSkeleton, tooltipDefaults, tooltipSkeleton, tooltipTriggerSkeleton };