@opalui/react 0.0.0-stage → 0.0.2

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.
Files changed (72) hide show
  1. package/dist/components/button/button.css +186 -0
  2. package/dist/components/button/button.css.js +1 -0
  3. package/dist/components/button/button.d.ts +26 -0
  4. package/dist/components/button/button.js +39 -0
  5. package/dist/components/button/button.js.map +1 -0
  6. package/dist/components/glass/glass.css +43 -0
  7. package/dist/components/glass/glass.css.js +1 -0
  8. package/dist/components/glass/glass.d.ts +5 -0
  9. package/dist/components/glass/glass.js +19 -0
  10. package/dist/components/glass/glass.js.map +1 -0
  11. package/dist/components/input/input.css +70 -0
  12. package/dist/components/input/input.css.js +1 -0
  13. package/dist/components/input/input.d.ts +17 -0
  14. package/dist/components/input/input.js +36 -0
  15. package/dist/components/input/input.js.map +1 -0
  16. package/dist/components/menu/menu.css +112 -0
  17. package/dist/components/menu/menu.css.js +1 -0
  18. package/dist/components/menu/menu.d.ts +18 -0
  19. package/dist/components/menu/menu.js +125 -0
  20. package/dist/components/menu/menu.js.map +1 -0
  21. package/dist/components/segmented-control/segmented-control.css +136 -0
  22. package/dist/components/segmented-control/segmented-control.css.js +1 -0
  23. package/dist/components/segmented-control/segmented-control.d.ts +21 -0
  24. package/dist/components/segmented-control/segmented-control.js +147 -0
  25. package/dist/components/segmented-control/segmented-control.js.map +1 -0
  26. package/dist/components/select/select.css +164 -0
  27. package/dist/components/select/select.css.js +1 -0
  28. package/dist/components/select/select.d.ts +27 -0
  29. package/dist/components/select/select.js +140 -0
  30. package/dist/components/select/select.js.map +1 -0
  31. package/dist/components/slider/slider.css +114 -0
  32. package/dist/components/slider/slider.css.js +1 -0
  33. package/dist/components/slider/slider.d.ts +17 -0
  34. package/dist/components/slider/slider.js +40 -0
  35. package/dist/components/slider/slider.js.map +1 -0
  36. package/dist/components/switch/switch.css +130 -0
  37. package/dist/components/switch/switch.css.js +1 -0
  38. package/dist/components/switch/switch.d.ts +14 -0
  39. package/dist/components/switch/switch.js +74 -0
  40. package/dist/components/switch/switch.js.map +1 -0
  41. package/dist/components/textarea/textarea.css +56 -0
  42. package/dist/components/textarea/textarea.css.js +1 -0
  43. package/dist/components/textarea/textarea.d.ts +8 -0
  44. package/dist/components/textarea/textarea.js +17 -0
  45. package/dist/components/textarea/textarea.js.map +1 -0
  46. package/dist/index.d.ts +9 -0
  47. package/dist/index.js +10 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/internal/glass/lens.d.ts +44 -0
  50. package/dist/internal/glass/lens.js +116 -0
  51. package/dist/internal/glass/lens.js.map +1 -0
  52. package/dist/internal/glass/map.d.ts +1 -0
  53. package/dist/internal/glass/map.js +48 -0
  54. package/dist/internal/glass/map.js.map +1 -0
  55. package/dist/internal/glass/material.d.ts +55 -0
  56. package/dist/internal/glass/material.js +22 -0
  57. package/dist/internal/glass/material.js.map +1 -0
  58. package/dist/internal/glass/styles.d.ts +2 -0
  59. package/dist/internal/glass/styles.js +4 -0
  60. package/dist/internal/glass/styles.js.map +1 -0
  61. package/dist/internal/glass/support.d.ts +5 -0
  62. package/dist/internal/glass/support.js +8 -0
  63. package/dist/internal/glass/support.js.map +1 -0
  64. package/dist/internal/glass.css +34 -0
  65. package/dist/internal/glass.css.js +1 -0
  66. package/dist/internal/styles.d.ts +2 -0
  67. package/dist/internal/styles.js +9 -0
  68. package/dist/internal/styles.js.map +1 -0
  69. package/dist/styles.css +11 -0
  70. package/dist/styles.css.js +1 -0
  71. package/package.json +90 -4
  72. package/README.md +0 -3
@@ -0,0 +1,112 @@
1
+ /* Shared material. Public variables can be set once on a theme container. */
2
+ :where(.opal-glass, .opal-button, .opal-input, .opal-menu-panel, .opal-switch, .opal-slider, .opal-segmented, .opal-textarea, .opal-select) {
3
+ --_opal-glass-rim: var(--opal-glass-rim, inset 0 0 0 .5px rgb(255 255 255 / 7%), inset .65px 1px .65px rgb(255 255 255 / var(--opal-material-rim, .28)), inset -.4px -.65px .8px rgb(255 255 255 / calc(var(--opal-material-rim, .28) * .43)));
4
+ --_opal-glass-edge: 0 0 0 .5px rgb(40 35 30 / calc(var(--opal-material-edge, .5) * .16)), -.65px 0 0 rgb(40 35 30 / calc(var(--opal-material-edge, .5) * .48)), .65px 0 0 rgb(40 35 30 / calc(var(--opal-material-edge, .5) * .48));
5
+ --_opal-glass-shadow: var(--opal-glass-shadow, 0 3px 8px rgb(15 34 47 / 12%));
6
+ }
7
+
8
+ /* One light material for resting controls: clear white face, fine contour,
9
+ a bright inner lip and a broad, low-contrast shadow. */
10
+ :where([data-theme="light"]) {
11
+ --opal-control-background: linear-gradient(180deg, rgb(255 255 255 / 40%) 0%, rgb(255 255 255 / 8%) 32%, rgb(255 255 255 / 4%) 65%, rgb(255 255 255 / 28%) 100%), rgb(255 255 255 / 32%);
12
+ --opal-control-active-background: rgb(255 255 255 / 60%);
13
+ --opal-control-rim-opacity: 1;
14
+ --opal-button-background: var(--opal-control-background);
15
+ --opal-button-hover-background: var(--opal-control-active-background);
16
+ --opal-button-color: #171717;
17
+ --opal-input-background: var(--opal-control-background);
18
+ --opal-input-focus-background: var(--opal-control-active-background);
19
+ --opal-input-color: #171717;
20
+ --opal-segmented-background: var(--opal-control-background);
21
+ --opal-glass-shadow: 0 5px 12px -4px rgb(0 0 0 / 4%);
22
+ --opal-glass-highlight:
23
+ inset 0 0 0 1px rgb(255 255 255 / 78%),
24
+ inset 0 1px 0 rgb(255 255 255 / 100%),
25
+ inset 0 -1px 0 rgb(255 255 255 / 90%);
26
+ }
27
+
28
+ /* Resolve the contour on each surface, where its material settings live.
29
+ edge=.5 reproduces the established 8%/24% light contour exactly. */
30
+ :where([data-theme="light"]) :where(.opal-glass, .opal-button, .opal-input, .opal-menu-panel, .opal-switch, .opal-slider, .opal-segmented, .opal-textarea, .opal-select) {
31
+ --_opal-glass-rim: var(--opal-glass-rim, var(--_opal-glass-edge), var(--opal-glass-highlight));
32
+ }
33
+
34
+ :where(.opal-input,.opal-textarea,.opal-select) { --opal-material-frost:12px; --opal-material-rim:0.22; --opal-material-edge:0.5; }
35
+ :where(.opal-menu-panel) { --opal-material-frost:24px; --opal-material-rim:0.3; --opal-material-edge:0.5; }
36
+
37
+
38
+ .opal-menu { display: inline-flex; vertical-align: middle; }
39
+ .opal-menu-panel {
40
+ display: none;
41
+ position: fixed;
42
+ inset: auto;
43
+ margin: 0;
44
+ padding: 6px;
45
+ box-sizing: border-box;
46
+ width: 228px;
47
+ max-width: calc(100vw - 16px);
48
+ max-height: calc(100dvh - 16px);
49
+ overflow: auto;
50
+ z-index: 1000;
51
+ border: 0;
52
+ border-radius: 22px;
53
+ color: var(--opal-menu-color, #202124);
54
+ background: var(--opal-menu-background, rgb(250 250 252 / 86%));
55
+ backdrop-filter: blur(var(--opal-material-frost,24px)) saturate(150%);
56
+ -webkit-backdrop-filter: blur(var(--opal-material-frost,24px)) saturate(150%);
57
+ box-shadow: 0 12px 36px rgb(0 0 0 / 18%), inset 0 0 0 .5px var(--opal-menu-edge, rgb(0 0 0 / 12%)), var(--_opal-glass-rim);
58
+ font: inherit;
59
+ outline: none;
60
+ opacity: 0;
61
+ transform: scale(.6);
62
+ transition: opacity 160ms ease, transform 300ms cubic-bezier(.2,.8,.2,1), display 300ms allow-discrete, overlay 300ms allow-discrete;
63
+ }
64
+ .opal-menu-panel[data-open] { display: block; opacity: 1; transform: scale(1); }
65
+ @starting-style { .opal-menu-panel[data-open] { opacity: 0; transform: scale(.6); } }
66
+ .opal-menu-highlight {
67
+ position: absolute;
68
+ top: 0;
69
+ inset-inline: 6px;
70
+ border-radius: 16px;
71
+ pointer-events: none;
72
+ background: var(--opal-menu-highlight, rgb(0 0 0 / 7%));
73
+ transition: transform 220ms cubic-bezier(.22,.61,.36,1), height 220ms cubic-bezier(.22,.61,.36,1);
74
+ }
75
+ .opal-menu-item {
76
+ position: relative;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 12px;
80
+ width: 100%;
81
+ min-height: 38px;
82
+ padding: 8px 12px;
83
+ border: 0;
84
+ border-radius: 16px;
85
+ background: transparent;
86
+ color: inherit;
87
+ font: inherit;
88
+ font-size: 14px;
89
+ line-height: 1.35;
90
+ text-align: start;
91
+ cursor: pointer;
92
+ outline: none;
93
+ -webkit-tap-highlight-color: transparent;
94
+ transition: transform 160ms ease;
95
+ }
96
+ .opal-menu-item:active:not(:disabled) { transform: scale(.98); }
97
+ .opal-menu-item:focus-visible { box-shadow: inset 0 0 0 2px var(--opal-focus-color, #2885ff); }
98
+ .opal-menu-item[data-destructive] { color: var(--opal-menu-destructive, #d6293e); }
99
+ .opal-menu-item:disabled { opacity: .4; cursor: default; }
100
+ .opal-menu-icon { display: inline-flex; width: 18px; height: 18px; flex: 0 0 18px; }
101
+ .opal-menu-icon > svg { width: 100%; height: 100%; }
102
+ .opal-menu-separator { height: 1px; margin: 6px 10px; background: var(--opal-menu-edge, rgb(0 0 0 / 10%)); }
103
+ @media (prefers-reduced-motion: reduce) { .opal-menu-panel, .opal-menu-item { transition: none; transform: none; } }
104
+ @media (prefers-reduced-motion: reduce) { .opal-menu-highlight { transition: none; } }
105
+ @media (prefers-reduced-transparency: reduce) { .opal-menu-panel { background: var(--opal-menu-solid, #fafafa); backdrop-filter: none; -webkit-backdrop-filter: none; } }
106
+ @media (forced-colors: active) {
107
+ .opal-menu-panel { background: Canvas; color: CanvasText; border: 1px solid ButtonText; }
108
+ .opal-menu-item:focus { outline: 2px solid Highlight; }
109
+ .opal-menu-highlight { background: transparent; }
110
+ .opal-menu-item[data-destructive] { color: ButtonText; }
111
+ .opal-menu-separator { background: ButtonText; }
112
+ }
@@ -0,0 +1 @@
1
+ export default ".opal-menu { display: inline-flex; vertical-align: middle; }\n.opal-menu-panel {\n display: none;\n position: fixed;\n inset: auto;\n margin: 0;\n padding: 6px;\n box-sizing: border-box;\n width: 228px;\n max-width: calc(100vw - 16px);\n max-height: calc(100dvh - 16px);\n overflow: auto;\n z-index: 1000;\n border: 0;\n border-radius: 22px;\n color: var(--opal-menu-color, #202124);\n background: var(--opal-menu-background, rgb(250 250 252 / 86%));\n backdrop-filter: blur(var(--opal-material-frost,24px)) saturate(150%);\n -webkit-backdrop-filter: blur(var(--opal-material-frost,24px)) saturate(150%);\n box-shadow: 0 12px 36px rgb(0 0 0 / 18%), inset 0 0 0 .5px var(--opal-menu-edge, rgb(0 0 0 / 12%)), var(--_opal-glass-rim);\n font: inherit;\n outline: none;\n opacity: 0;\n transform: scale(.6);\n transition: opacity 160ms ease, transform 300ms cubic-bezier(.2,.8,.2,1), display 300ms allow-discrete, overlay 300ms allow-discrete;\n}\n.opal-menu-panel[data-open] { display: block; opacity: 1; transform: scale(1); }\n@starting-style { .opal-menu-panel[data-open] { opacity: 0; transform: scale(.6); } }\n.opal-menu-highlight {\n position: absolute;\n top: 0;\n inset-inline: 6px;\n border-radius: 16px;\n pointer-events: none;\n background: var(--opal-menu-highlight, rgb(0 0 0 / 7%));\n transition: transform 220ms cubic-bezier(.22,.61,.36,1), height 220ms cubic-bezier(.22,.61,.36,1);\n}\n.opal-menu-item {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 38px;\n padding: 8px 12px;\n border: 0;\n border-radius: 16px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 14px;\n line-height: 1.35;\n text-align: start;\n cursor: pointer;\n outline: none;\n -webkit-tap-highlight-color: transparent;\n transition: transform 160ms ease;\n}\n.opal-menu-item:active:not(:disabled) { transform: scale(.98); }\n.opal-menu-item:focus-visible { box-shadow: inset 0 0 0 2px var(--opal-focus-color, #2885ff); }\n.opal-menu-item[data-destructive] { color: var(--opal-menu-destructive, #d6293e); }\n.opal-menu-item:disabled { opacity: .4; cursor: default; }\n.opal-menu-icon { display: inline-flex; width: 18px; height: 18px; flex: 0 0 18px; }\n.opal-menu-icon > svg { width: 100%; height: 100%; }\n.opal-menu-separator { height: 1px; margin: 6px 10px; background: var(--opal-menu-edge, rgb(0 0 0 / 10%)); }\n@media (prefers-reduced-motion: reduce) { .opal-menu-panel, .opal-menu-item { transition: none; transform: none; } }\n@media (prefers-reduced-motion: reduce) { .opal-menu-highlight { transition: none; } }\n@media (prefers-reduced-transparency: reduce) { .opal-menu-panel { background: var(--opal-menu-solid, #fafafa); backdrop-filter: none; -webkit-backdrop-filter: none; } }\n@media (forced-colors: active) {\n .opal-menu-panel { background: Canvas; color: CanvasText; border: 1px solid ButtonText; }\n .opal-menu-item:focus { outline: 2px solid Highlight; }\n .opal-menu-highlight { background: transparent; }\n .opal-menu-item[data-destructive] { color: ButtonText; }\n .opal-menu-separator { background: ButtonText; }\n}\n";
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from 'react';
2
+ export type MenuItem = {
3
+ label: string;
4
+ icon?: ReactNode;
5
+ onSelect?: () => void;
6
+ disabled?: boolean;
7
+ destructive?: boolean;
8
+ } | 'separator';
9
+ export type MenuProps = {
10
+ 'aria-label': string;
11
+ items: readonly MenuItem[];
12
+ icon?: ReactNode;
13
+ children?: ReactNode;
14
+ disabled?: boolean;
15
+ className?: string;
16
+ };
17
+ /** A compact action menu. Styles are included automatically. */
18
+ export declare function Menu({ items, icon, children, disabled, className, 'aria-label': label }: MenuProps): import("react").JSX.Element;
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from 'react';
4
+ import { Button } from '../button/button.js';
5
+ import { stylesheetHref } from '../../internal/styles.js';
6
+ import componentCss from './menu.css.js';
7
+ import glassCss from '../../internal/glass/styles.js';
8
+ const css = `${glassCss}\n${componentCss}`;
9
+ const styleHref = stylesheetHref('menu', css);
10
+ /** A compact action menu. Styles are included automatically. */
11
+ export function Menu({ items, icon, children, disabled, className, 'aria-label': label }) {
12
+ const id = useId();
13
+ const trigger = useRef(null);
14
+ const panel = useRef(null);
15
+ const [open, setOpen] = useState(false);
16
+ const [highlight, setHighlight] = useState(null);
17
+ function highlightItem(item) {
18
+ setHighlight({ top: item.offsetTop, height: item.offsetHeight });
19
+ }
20
+ const search = useRef({ text: '', time: 0 });
21
+ const options = () => Array.from(panel.current?.querySelectorAll('[role="menuitem"]:not(:disabled)') ?? []);
22
+ function close(restoreFocus = true) {
23
+ panel.current?.hidePopover?.();
24
+ setOpen(false);
25
+ setHighlight(null);
26
+ if (restoreFocus)
27
+ trigger.current?.focus({ preventScroll: true });
28
+ }
29
+ function position() {
30
+ if (!trigger.current || !panel.current)
31
+ return;
32
+ const anchor = trigger.current.getBoundingClientRect();
33
+ const menu = panel.current;
34
+ const width = menu.offsetWidth;
35
+ const height = menu.offsetHeight;
36
+ menu.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - width - 8))}px`;
37
+ menu.style.top = `${Math.max(8, Math.min(anchor.top, window.innerHeight - height - 8))}px`;
38
+ menu.style.transformOrigin = `${Math.max(0, anchor.left - parseFloat(menu.style.left) + anchor.width / 2)}px ${Math.max(0, anchor.top - parseFloat(menu.style.top) + anchor.height / 2)}px`;
39
+ }
40
+ function show(last = false) {
41
+ if (disabled)
42
+ return;
43
+ const menu = panel.current;
44
+ if (!menu)
45
+ return;
46
+ menu.dataset.open = 'true';
47
+ menu.showPopover?.();
48
+ position();
49
+ setOpen(true);
50
+ search.current = { text: '', time: 0 };
51
+ const enabled = options();
52
+ (last ? enabled.at(-1) : enabled[0])?.focus({ preventScroll: true });
53
+ if (!enabled.length)
54
+ menu.focus({ preventScroll: true });
55
+ }
56
+ useEffect(() => {
57
+ if (!open)
58
+ return;
59
+ const outside = (event) => {
60
+ if (!panel.current?.contains(event.target) && !trigger.current?.contains(event.target))
61
+ close(false);
62
+ };
63
+ const blur = () => close(false);
64
+ document.addEventListener('pointerdown', outside);
65
+ window.addEventListener('resize', position);
66
+ window.addEventListener('scroll', position, true);
67
+ window.addEventListener('blur', blur);
68
+ return () => {
69
+ document.removeEventListener('pointerdown', outside);
70
+ window.removeEventListener('resize', position);
71
+ window.removeEventListener('scroll', position, true);
72
+ window.removeEventListener('blur', blur);
73
+ };
74
+ }, [open]);
75
+ useEffect(() => { if (disabled)
76
+ close(false); }, [disabled]);
77
+ useEffect(() => {
78
+ if (!open || !panel.current)
79
+ return;
80
+ const observer = new ResizeObserver(() => {
81
+ const active = document.activeElement;
82
+ if (active instanceof HTMLButtonElement && panel.current?.contains(active))
83
+ highlightItem(active);
84
+ });
85
+ observer.observe(panel.current);
86
+ options().forEach(item => observer.observe(item));
87
+ return () => observer.disconnect();
88
+ }, [open, items]);
89
+ return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), _jsxs("span", { className: ['opal-menu', className].filter(Boolean).join(' '), children: [_jsx(Button, { ref: trigger, "aria-label": label, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": id, disabled: disabled, icon: icon ?? (children ? undefined : _jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.6" }), _jsx("circle", { cx: "12", cy: "12", r: "1.6" }), _jsx("circle", { cx: "19", cy: "12", r: "1.6" })] })), onClick: () => open ? close() : show(), onKeyDown: event => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
90
+ event.preventDefault();
91
+ show(event.key === 'ArrowUp');
92
+ } }, children: children }), _jsxs("div", { ref: panel, id: id, role: "menu", "aria-label": label, tabIndex: -1, popover: "manual", className: "opal-menu-panel", "data-open": open || undefined, onKeyDown: event => {
93
+ const enabled = options();
94
+ const current = enabled.indexOf(document.activeElement);
95
+ if (event.key === 'Escape') {
96
+ event.preventDefault();
97
+ event.stopPropagation();
98
+ close();
99
+ return;
100
+ }
101
+ if (event.key === 'Tab') {
102
+ close();
103
+ return;
104
+ }
105
+ if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
106
+ event.preventDefault();
107
+ const index = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1
108
+ : (current + (event.key === 'ArrowDown' ? 1 : -1) + enabled.length) % enabled.length;
109
+ enabled[index]?.focus();
110
+ return;
111
+ }
112
+ if (event.key.length === 1 && event.key !== ' ' && !event.metaKey && !event.ctrlKey && !event.altKey) {
113
+ event.preventDefault();
114
+ const now = Date.now();
115
+ search.current.text = (now - search.current.time < 600 ? search.current.text : '') + event.key.toLowerCase();
116
+ search.current.time = now;
117
+ const query = new Set(search.current.text).size === 1 ? event.key.toLowerCase() : search.current.text;
118
+ const ordered = [...enabled.slice(current + 1), ...enabled.slice(0, current + 1)];
119
+ ordered.find(item => item.textContent?.trim().toLowerCase().startsWith(query))?.focus();
120
+ }
121
+ }, children: [highlight && _jsx("span", { className: "opal-menu-highlight", "aria-hidden": "true", style: { transform: `translateY(${highlight.top}px)`, height: highlight.height } }), items.map((item, index) => item === 'separator' ? _jsx("div", { role: "separator", className: "opal-menu-separator" }, index)
122
+ : _jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, disabled: item.disabled, className: "opal-menu-item", "data-destructive": item.destructive || undefined, onFocus: event => highlightItem(event.currentTarget), onPointerMove: event => { if (event.pointerType === 'mouse' && !item.disabled && document.activeElement !== event.currentTarget)
123
+ event.currentTarget.focus({ preventScroll: true }); }, onClick: () => { close(); item.onSelect?.(); }, children: [item.icon && _jsx("span", { className: "opal-menu-icon", "aria-hidden": "true", children: item.icon }), _jsx("span", { children: item.label })] }, index))] })] })] });
124
+ }
125
+ //# sourceMappingURL=menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.js","sourceRoot":"","sources":["../../../src/components/menu/menu.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { useEffect, useId, useRef, useState, type ReactNode } from 'react'\nimport { Button } from '../button/button.js'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport componentCss from './menu.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\n\nconst styleHref = stylesheetHref('menu', css)\nexport type MenuItem = { label: string; icon?: ReactNode; onSelect?: () => void; disabled?: boolean; destructive?: boolean } | 'separator'\nexport type MenuProps = {\n 'aria-label': string\n items: readonly MenuItem[]\n icon?: ReactNode\n children?: ReactNode\n disabled?: boolean\n className?: string\n}\n\n/** A compact action menu. Styles are included automatically. */\nexport function Menu({ items, icon, children, disabled, className, 'aria-label': label }: MenuProps) {\n const id = useId()\n const trigger = useRef<HTMLButtonElement>(null)\n const panel = useRef<HTMLDivElement>(null)\n const [open, setOpen] = useState(false)\n const [highlight, setHighlight] = useState<{ top: number; height: number } | null>(null)\n function highlightItem(item: HTMLButtonElement) {\n setHighlight({ top: item.offsetTop, height: item.offsetHeight })\n }\n const search = useRef({ text: '', time: 0 })\n const options = () => Array.from(panel.current?.querySelectorAll<HTMLButtonElement>('[role=\"menuitem\"]:not(:disabled)') ?? [])\n function close(restoreFocus = true) {\n panel.current?.hidePopover?.()\n setOpen(false)\n setHighlight(null)\n if (restoreFocus) trigger.current?.focus({ preventScroll: true })\n }\n function position() {\n if (!trigger.current || !panel.current) return\n const anchor = trigger.current.getBoundingClientRect()\n const menu = panel.current\n const width = menu.offsetWidth\n const height = menu.offsetHeight\n menu.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - width - 8))}px`\n menu.style.top = `${Math.max(8, Math.min(anchor.top, window.innerHeight - height - 8))}px`\n menu.style.transformOrigin = `${Math.max(0, anchor.left - parseFloat(menu.style.left) + anchor.width / 2)}px ${Math.max(0, anchor.top - parseFloat(menu.style.top) + anchor.height / 2)}px`\n }\n function show(last = false) {\n if (disabled) return\n const menu = panel.current\n if (!menu) return\n menu.dataset.open = 'true'\n menu.showPopover?.()\n position()\n setOpen(true)\n search.current = { text: '', time: 0 }\n const enabled = options()\n ;(last ? enabled.at(-1) : enabled[0])?.focus({ preventScroll: true })\n if (!enabled.length) menu.focus({ preventScroll: true })\n }\n useEffect(() => {\n if (!open) return\n const outside = (event: PointerEvent) => {\n if (!panel.current?.contains(event.target as Node) && !trigger.current?.contains(event.target as Node)) close(false)\n }\n const blur = () => close(false)\n document.addEventListener('pointerdown', outside)\n window.addEventListener('resize', position)\n window.addEventListener('scroll', position, true)\n window.addEventListener('blur', blur)\n return () => {\n document.removeEventListener('pointerdown', outside)\n window.removeEventListener('resize', position)\n window.removeEventListener('scroll', position, true)\n window.removeEventListener('blur', blur)\n }\n }, [open])\n useEffect(() => { if (disabled) close(false) }, [disabled])\n useEffect(() => {\n if (!open || !panel.current) return\n const observer = new ResizeObserver(() => {\n const active = document.activeElement\n if (active instanceof HTMLButtonElement && panel.current?.contains(active)) highlightItem(active)\n })\n observer.observe(panel.current)\n options().forEach(item => observer.observe(item))\n return () => observer.disconnect()\n }, [open, items])\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n <span className={['opal-menu', className].filter(Boolean).join(' ')}>\n <Button ref={trigger} aria-label={label} aria-haspopup=\"menu\" aria-expanded={open} aria-controls={id}\n disabled={disabled} icon={icon ?? (children ? undefined : <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" /><circle cx=\"12\" cy=\"12\" r=\"1.6\" /><circle cx=\"19\" cy=\"12\" r=\"1.6\" /></svg>)}\n onClick={() => open ? close() : show()}\n onKeyDown={event => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); show(event.key === 'ArrowUp') } }}>\n {children}\n </Button>\n <div ref={panel} id={id} role=\"menu\" aria-label={label} tabIndex={-1} popover=\"manual\"\n className=\"opal-menu-panel\" data-open={open || undefined}\n onKeyDown={event => {\n const enabled = options()\n const current = enabled.indexOf(document.activeElement as HTMLButtonElement)\n if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(); return }\n if (event.key === 'Tab') { close(); return }\n if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {\n event.preventDefault()\n const index = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1\n : (current + (event.key === 'ArrowDown' ? 1 : -1) + enabled.length) % enabled.length\n enabled[index]?.focus(); return\n }\n if (event.key.length === 1 && event.key !== ' ' && !event.metaKey && !event.ctrlKey && !event.altKey) {\n event.preventDefault()\n const now = Date.now()\n search.current.text = (now - search.current.time < 600 ? search.current.text : '') + event.key.toLowerCase()\n search.current.time = now\n const query = new Set(search.current.text).size === 1 ? event.key.toLowerCase() : search.current.text\n const ordered = [...enabled.slice(current + 1), ...enabled.slice(0, current + 1)]\n ordered.find(item => item.textContent?.trim().toLowerCase().startsWith(query))?.focus()\n }\n }}>\n {highlight && <span className=\"opal-menu-highlight\" aria-hidden=\"true\"\n style={{ transform: `translateY(${highlight.top}px)`, height: highlight.height }} />}\n {items.map((item, index) => item === 'separator' ? <div key={index} role=\"separator\" className=\"opal-menu-separator\" />\n : <button key={index} type=\"button\" role=\"menuitem\" tabIndex={-1} disabled={item.disabled}\n className=\"opal-menu-item\" data-destructive={item.destructive || undefined}\n onFocus={event => highlightItem(event.currentTarget)}\n onPointerMove={event => { if (event.pointerType === 'mouse' && !item.disabled && document.activeElement !== event.currentTarget) event.currentTarget.focus({ preventScroll: true }) }}\n onClick={() => { close(); item.onSelect?.() }}>\n {item.icon && <span className=\"opal-menu-icon\" aria-hidden=\"true\">{item.icon}</span>}\n <span>{item.label}</span>\n </button>)}\n </div>\n </span>\n </>\n}\n"]}
@@ -0,0 +1,136 @@
1
+ /* Shared material. Public variables can be set once on a theme container. */
2
+ :where(.opal-glass, .opal-button, .opal-input, .opal-menu-panel, .opal-switch, .opal-slider, .opal-segmented, .opal-textarea, .opal-select) {
3
+ --_opal-glass-rim: var(--opal-glass-rim, inset 0 0 0 .5px rgb(255 255 255 / 7%), inset .65px 1px .65px rgb(255 255 255 / var(--opal-material-rim, .28)), inset -.4px -.65px .8px rgb(255 255 255 / calc(var(--opal-material-rim, .28) * .43)));
4
+ --_opal-glass-edge: 0 0 0 .5px rgb(40 35 30 / calc(var(--opal-material-edge, .5) * .16)), -.65px 0 0 rgb(40 35 30 / calc(var(--opal-material-edge, .5) * .48)), .65px 0 0 rgb(40 35 30 / calc(var(--opal-material-edge, .5) * .48));
5
+ --_opal-glass-shadow: var(--opal-glass-shadow, 0 3px 8px rgb(15 34 47 / 12%));
6
+ }
7
+
8
+ /* One light material for resting controls: clear white face, fine contour,
9
+ a bright inner lip and a broad, low-contrast shadow. */
10
+ :where([data-theme="light"]) {
11
+ --opal-control-background: linear-gradient(180deg, rgb(255 255 255 / 40%) 0%, rgb(255 255 255 / 8%) 32%, rgb(255 255 255 / 4%) 65%, rgb(255 255 255 / 28%) 100%), rgb(255 255 255 / 32%);
12
+ --opal-control-active-background: rgb(255 255 255 / 60%);
13
+ --opal-control-rim-opacity: 1;
14
+ --opal-button-background: var(--opal-control-background);
15
+ --opal-button-hover-background: var(--opal-control-active-background);
16
+ --opal-button-color: #171717;
17
+ --opal-input-background: var(--opal-control-background);
18
+ --opal-input-focus-background: var(--opal-control-active-background);
19
+ --opal-input-color: #171717;
20
+ --opal-segmented-background: var(--opal-control-background);
21
+ --opal-glass-shadow: 0 5px 12px -4px rgb(0 0 0 / 4%);
22
+ --opal-glass-highlight:
23
+ inset 0 0 0 1px rgb(255 255 255 / 78%),
24
+ inset 0 1px 0 rgb(255 255 255 / 100%),
25
+ inset 0 -1px 0 rgb(255 255 255 / 90%);
26
+ }
27
+
28
+ /* Resolve the contour on each surface, where its material settings live.
29
+ edge=.5 reproduces the established 8%/24% light contour exactly. */
30
+ :where([data-theme="light"]) :where(.opal-glass, .opal-button, .opal-input, .opal-menu-panel, .opal-switch, .opal-slider, .opal-segmented, .opal-textarea, .opal-select) {
31
+ --_opal-glass-rim: var(--opal-glass-rim, var(--_opal-glass-edge), var(--opal-glass-highlight));
32
+ }
33
+
34
+ :where(.opal-input,.opal-textarea,.opal-select) { --opal-material-frost:12px; --opal-material-rim:0.22; --opal-material-edge:0.5; }
35
+ :where(.opal-menu-panel) { --opal-material-frost:24px; --opal-material-rim:0.3; --opal-material-edge:0.5; }
36
+
37
+
38
+ .opal-segmented-filter { position: absolute; pointer-events: none; }
39
+ .opal-segmented {
40
+ position: relative;
41
+ display: inline-grid;
42
+ grid-template-columns: repeat(var(--opal-segment-count), minmax(0, 1fr));
43
+ box-sizing: border-box;
44
+ padding: 3px;
45
+ border-radius: 999px;
46
+ background: var(--opal-segmented-background, rgb(0 0 0 / 5%));
47
+ color: var(--opal-segmented-color, #202020);
48
+ box-shadow: var(--opal-segmented-shadow, var(--_opal-glass-shadow));
49
+ vertical-align: middle;
50
+ max-width: 100%;
51
+ isolation: isolate;
52
+ }
53
+ .opal-segmented::after {
54
+ content: ''; position: absolute; inset: 0; border-radius: inherit;
55
+ pointer-events: none;
56
+ box-shadow: var(--opal-segmented-rim, var(--_opal-glass-rim));
57
+ opacity: var(--opal-control-rim-opacity, .7);
58
+ }
59
+ .opal-segmented-indicator {
60
+ position: absolute;
61
+ inset-block: 3px;
62
+ inset-inline-start: calc(3px + (100% - 6px) / var(--opal-segment-count) * var(--opal-segment-index));
63
+ width: calc((100% - 6px) / var(--opal-segment-count));
64
+ border-radius: 999px;
65
+ pointer-events: none;
66
+ background: var(--opal-segmented-thumb, #fff);
67
+ box-shadow: 0 1px 3px rgb(0 0 0 / 10%), inset 0 0 0 0.5px rgb(255 255 255 / 18%);
68
+ transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 450ms cubic-bezier(0.22, 0.61, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;
69
+ }
70
+ .opal-segmented-item {
71
+ position: relative;
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ min-width: 0;
76
+ min-height: 36px;
77
+ padding: 7px 24px;
78
+ border: 0;
79
+ border-radius: 999px;
80
+ background: transparent;
81
+ color: inherit;
82
+ font: inherit;
83
+ line-height: 1.25;
84
+ cursor: pointer;
85
+ touch-action: none;
86
+ -webkit-tap-highlight-color: transparent;
87
+ user-select: none;
88
+ -webkit-user-select: none;
89
+ }
90
+ .opal-segmented-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: transform 170ms ease; }
91
+ .opal-segmented-item[aria-checked="true"] { color: var(--opal-segmented-selected-color, inherit); }
92
+ .opal-segmented-item:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 3px; }
93
+ .opal-segmented:is([data-dragging], :has(.opal-segmented-item:active:not(:disabled))) .opal-segmented-indicator {
94
+ transform: translateX(var(--opal-segment-pull, 0px)) scale(calc(1.14 + var(--opal-segment-stretch, 0)), calc(1.45 - var(--opal-segment-stretch, 0) * 0.75));
95
+ background: var(--opal-segmented-lifted, rgb(255 255 255 / 30%));
96
+ z-index: 0;
97
+ -webkit-backdrop-filter: blur(.25px) saturate(110%);
98
+ backdrop-filter: blur(.25px) saturate(110%);
99
+ box-shadow: 0 5px 12px rgb(0 0 0 / 13%), var(--_opal-glass-rim);
100
+ transition-duration: 170ms, 170ms, 140ms, 170ms;
101
+ }
102
+ /* Pointer movement is direct; restoring the normal transition on release
103
+ lets the pill settle onto the nearest enabled segment. */
104
+ .opal-segmented[data-tracking] .opal-segmented-indicator {
105
+ transition-property: transform, background-color, box-shadow;
106
+ transition-duration: 170ms, 140ms, 170ms;
107
+ }
108
+ /* A small overshoot on release gives the resisted pull a soft rebound. */
109
+ .opal-segmented[data-rebounding]:not([data-dragging]) .opal-segmented-indicator {
110
+ transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 600ms cubic-bezier(0.22, 1.5, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;
111
+ }
112
+ .opal-segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }
113
+ .opal-segmented[data-disabled] { opacity: 0.4; }
114
+ .opal-segmented[data-disabled] .opal-segmented-item { opacity: 1; }
115
+ @media (prefers-reduced-motion: reduce) {
116
+ .opal-segmented .opal-segmented-indicator, .opal-segmented .opal-segmented-label { transition: none !important; transform: none !important; }
117
+ }
118
+ @media (prefers-reduced-transparency: reduce) {
119
+ .opal-segmented { background: var(--opal-segmented-solid-background, #ededed); }
120
+ .opal-segmented .opal-segmented-indicator { z-index: 0; background: var(--opal-segmented-thumb, #fff) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
121
+ }
122
+ @media (forced-colors: active) {
123
+ .opal-segmented::after { display: none; }
124
+ .opal-segmented { background: Canvas; color: ButtonText; outline: 1px solid ButtonText; }
125
+ .opal-segmented .opal-segmented-indicator { z-index: 0; background: Highlight !important; box-shadow: none !important; transform: none !important; }
126
+ .opal-segmented-item[aria-checked="true"] { color: HighlightText; }
127
+ .opal-segmented-item:disabled { color: GrayText; }
128
+ }
129
+
130
+ /* An inert visual layer owns the ink; native buttons retain all semantics. */
131
+ .opal-segmented-optical-labels { position:absolute; inset:-24px; padding:27px; display:grid; grid-template-columns:repeat(var(--opal-segment-count),minmax(0,1fr)); pointer-events:none; visibility:hidden; z-index:1; }
132
+ .opal-segmented-optical-label { display:flex; align-items:center; justify-content:center; min-width:0; white-space:nowrap; }
133
+ .opal-segmented-optical-label[data-selected] { color:var(--opal-segmented-selected-color,inherit); }
134
+ .opal-segmented-optical-label[data-disabled] { opacity:.4; }
135
+ .opal-segmented[data-lensing] .opal-segmented-optical-labels { visibility:visible; }
136
+ .opal-segmented[data-lensing] .opal-segmented-label { opacity:0; }
@@ -0,0 +1 @@
1
+ export default ".opal-segmented-filter { position: absolute; pointer-events: none; }\n.opal-segmented {\n position: relative;\n display: inline-grid;\n grid-template-columns: repeat(var(--opal-segment-count), minmax(0, 1fr));\n box-sizing: border-box;\n padding: 3px;\n border-radius: 999px;\n background: var(--opal-segmented-background, rgb(0 0 0 / 5%));\n color: var(--opal-segmented-color, #202020);\n box-shadow: var(--opal-segmented-shadow, var(--_opal-glass-shadow));\n vertical-align: middle;\n max-width: 100%;\n isolation: isolate;\n}\n.opal-segmented::after {\n content: ''; position: absolute; inset: 0; border-radius: inherit;\n pointer-events: none;\n box-shadow: var(--opal-segmented-rim, var(--_opal-glass-rim));\n opacity: var(--opal-control-rim-opacity, .7);\n}\n.opal-segmented-indicator {\n position: absolute;\n inset-block: 3px;\n inset-inline-start: calc(3px + (100% - 6px) / var(--opal-segment-count) * var(--opal-segment-index));\n width: calc((100% - 6px) / var(--opal-segment-count));\n border-radius: 999px;\n pointer-events: none;\n background: var(--opal-segmented-thumb, #fff);\n box-shadow: 0 1px 3px rgb(0 0 0 / 10%), inset 0 0 0 0.5px rgb(255 255 255 / 18%);\n transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 450ms cubic-bezier(0.22, 0.61, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;\n}\n.opal-segmented-item {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 36px;\n padding: 7px 24px;\n border: 0;\n border-radius: 999px;\n background: transparent;\n color: inherit;\n font: inherit;\n line-height: 1.25;\n cursor: pointer;\n touch-action: none;\n -webkit-tap-highlight-color: transparent;\n user-select: none;\n -webkit-user-select: none;\n}\n.opal-segmented-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: transform 170ms ease; }\n.opal-segmented-item[aria-checked=\"true\"] { color: var(--opal-segmented-selected-color, inherit); }\n.opal-segmented-item:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 3px; }\n.opal-segmented:is([data-dragging], :has(.opal-segmented-item:active:not(:disabled))) .opal-segmented-indicator {\n transform: translateX(var(--opal-segment-pull, 0px)) scale(calc(1.14 + var(--opal-segment-stretch, 0)), calc(1.45 - var(--opal-segment-stretch, 0) * 0.75));\n background: var(--opal-segmented-lifted, rgb(255 255 255 / 30%));\n z-index: 0;\n -webkit-backdrop-filter: blur(.25px) saturate(110%);\n backdrop-filter: blur(.25px) saturate(110%);\n box-shadow: 0 5px 12px rgb(0 0 0 / 13%), var(--_opal-glass-rim);\n transition-duration: 170ms, 170ms, 140ms, 170ms;\n}\n/* Pointer movement is direct; restoring the normal transition on release\n lets the pill settle onto the nearest enabled segment. */\n.opal-segmented[data-tracking] .opal-segmented-indicator {\n transition-property: transform, background-color, box-shadow;\n transition-duration: 170ms, 140ms, 170ms;\n}\n/* A small overshoot on release gives the resisted pull a soft rebound. */\n.opal-segmented[data-rebounding]:not([data-dragging]) .opal-segmented-indicator {\n transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 600ms cubic-bezier(0.22, 1.5, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;\n}\n.opal-segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }\n.opal-segmented[data-disabled] { opacity: 0.4; }\n.opal-segmented[data-disabled] .opal-segmented-item { opacity: 1; }\n@media (prefers-reduced-motion: reduce) {\n .opal-segmented .opal-segmented-indicator, .opal-segmented .opal-segmented-label { transition: none !important; transform: none !important; }\n}\n@media (prefers-reduced-transparency: reduce) {\n .opal-segmented { background: var(--opal-segmented-solid-background, #ededed); }\n .opal-segmented .opal-segmented-indicator { z-index: 0; background: var(--opal-segmented-thumb, #fff) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }\n}\n@media (forced-colors: active) {\n .opal-segmented::after { display: none; }\n .opal-segmented { background: Canvas; color: ButtonText; outline: 1px solid ButtonText; }\n .opal-segmented .opal-segmented-indicator { z-index: 0; background: Highlight !important; box-shadow: none !important; transform: none !important; }\n .opal-segmented-item[aria-checked=\"true\"] { color: HighlightText; }\n .opal-segmented-item:disabled { color: GrayText; }\n}\n\n/* An inert visual layer owns the ink; native buttons retain all semantics. */\n.opal-segmented-optical-labels { position:absolute; inset:-24px; padding:27px; display:grid; grid-template-columns:repeat(var(--opal-segment-count),minmax(0,1fr)); pointer-events:none; visibility:hidden; z-index:1; }\n.opal-segmented-optical-label { display:flex; align-items:center; justify-content:center; min-width:0; white-space:nowrap; }\n.opal-segmented-optical-label[data-selected] { color:var(--opal-segmented-selected-color,inherit); }\n.opal-segmented-optical-label[data-disabled] { opacity:.4; }\n.opal-segmented[data-lensing] .opal-segmented-optical-labels { visibility:visible; }\n.opal-segmented[data-lensing] .opal-segmented-label { opacity:0; }\n";
@@ -0,0 +1,21 @@
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
+ export type SegmentedControlItem = {
3
+ value: string;
4
+ label: ReactNode;
5
+ disabled?: boolean;
6
+ };
7
+ export type SegmentedControlProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'role'> & {
8
+ items: readonly SegmentedControlItem[];
9
+ value?: string;
10
+ defaultValue?: string;
11
+ onValueChange?: (value: string) => void;
12
+ disabled?: boolean;
13
+ };
14
+ /** A single-choice control. Supply aria-label or aria-labelledby for its group. */
15
+ export declare const SegmentedControl: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "defaultValue" | "role" | "children" | "onChange"> & {
16
+ items: readonly SegmentedControlItem[];
17
+ value?: string;
18
+ defaultValue?: string;
19
+ onValueChange?: (value: string) => void;
20
+ disabled?: boolean;
21
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,147 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { forwardRef, useEffect, useRef, useState } from 'react';
4
+ import { stylesheetHref } from '../../internal/styles.js';
5
+ import { useGlassLens } from '../../internal/glass/lens.js';
6
+ import { materials } from '../../internal/glass/material.js';
7
+ import componentCss from './segmented-control.css.js';
8
+ import glassCss from '../../internal/glass/styles.js';
9
+ const css = `${glassCss}\n${componentCss}`;
10
+ const styleHref = stylesheetHref('segmented-control', css);
11
+ /** A single-choice control. Supply aria-label or aria-labelledby for its group. */
12
+ export const SegmentedControl = forwardRef(function SegmentedControl({ items, value, defaultValue, onValueChange, disabled = false, className, style, ...props }, ref) {
13
+ const [internalValue, setInternalValue] = useState(() => defaultValue ?? items.find(item => !item.disabled)?.value);
14
+ const [pressed, setPressed] = useState();
15
+ const [dragPosition, setDragPosition] = useState();
16
+ const [overdrag, setOverdrag] = useState({ stretch: 0, offset: 0 });
17
+ const [rebounding, setRebounding] = useState(false);
18
+ const indicator = useRef(null);
19
+ const labelSource = useRef(null);
20
+ const glass = useGlassLens(indicator, materials.selection, { source: labelSource, active: pressed !== undefined && !disabled, ink: true });
21
+ const pointerClick = useRef(null);
22
+ const drag = useRef(null);
23
+ function nearestEnabled(position) {
24
+ return items.reduce((best, item, i) => item.disabled ? best
25
+ : best < 0 || Math.abs(i - position) < Math.abs(best - position) ? i : best, -1);
26
+ }
27
+ function movePill(clientX) {
28
+ const active = drag.current;
29
+ if (!active || disabled)
30
+ return;
31
+ const group = active.target.parentElement;
32
+ if (!group)
33
+ return;
34
+ const width = active.target.getBoundingClientRect().width;
35
+ if (!width)
36
+ return;
37
+ const direction = getComputedStyle(group).direction === 'rtl' ? -1 : 1;
38
+ const rawPosition = active.startIndex + direction * (clientX - active.startX) / width;
39
+ const position = Math.max(0, Math.min(items.length - 1, rawPosition));
40
+ const excess = rawPosition - position;
41
+ // Increasing resistance caps the stretch at 14% of a segment's width.
42
+ // Shift slightly beyond half the extension so the whole pill drifts with the pull.
43
+ const stretch = 0.14 * (1 - Math.exp(-Math.abs(excess) / 0.9));
44
+ setOverdrag({ stretch, offset: direction * Math.sign(excess) * stretch * width * 0.85 });
45
+ active.position = position;
46
+ active.stretch = stretch;
47
+ setDragPosition(position);
48
+ const nearest = nearestEnabled(position);
49
+ setPressed(items[nearest]?.value);
50
+ return stretch;
51
+ }
52
+ function endDrag() {
53
+ const active = drag.current;
54
+ drag.current = null;
55
+ setPressed(undefined);
56
+ setDragPosition(undefined);
57
+ setOverdrag({ stretch: 0, offset: 0 });
58
+ setRebounding(false);
59
+ if (active?.target.hasPointerCapture(active.pointerId))
60
+ active.target.releasePointerCapture(active.pointerId);
61
+ }
62
+ useEffect(() => {
63
+ window.addEventListener('blur', endDrag);
64
+ return () => window.removeEventListener('blur', endDrag);
65
+ }, []);
66
+ useEffect(() => { if (disabled)
67
+ endDrag(); }, [disabled]);
68
+ const selected = value === undefined ? internalValue : value;
69
+ const selectedIndex = items.findIndex(item => item.value === selected);
70
+ const pressedIndex = items.findIndex(item => item.value === pressed && !item.disabled);
71
+ const index = dragPosition ?? (pressedIndex >= 0 ? pressedIndex : selectedIndex);
72
+ const firstEnabled = items.findIndex(item => !item.disabled);
73
+ const focusIndex = selectedIndex >= 0 && !items[selectedIndex]?.disabled ? selectedIndex : firstEnabled;
74
+ function select(next) {
75
+ if (disabled || items.find(item => item.value === next)?.disabled || next === selected)
76
+ return;
77
+ if (value === undefined)
78
+ setInternalValue(next);
79
+ onValueChange?.(next);
80
+ }
81
+ function finishDrag(pointerId, clientX) {
82
+ const active = drag.current;
83
+ if (!active || active.pointerId !== pointerId)
84
+ return;
85
+ // Cancellation/capture loss may carry empty coordinates; keep the last
86
+ // tracked position in those cases instead of reverting to the press origin.
87
+ if (clientX !== undefined)
88
+ movePill(clientX);
89
+ const nextIndex = nearestEnabled(active.position);
90
+ const next = items[nextIndex];
91
+ const button = active.target.parentElement?.querySelectorAll('.opal-segmented-item')[nextIndex];
92
+ pointerClick.current = { target: active.target, until: Date.now() + 500 };
93
+ endDrag();
94
+ if (next)
95
+ select(next.value);
96
+ setRebounding(active.stretch > 0.005);
97
+ button?.focus({ preventScroll: true });
98
+ }
99
+ return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), glass.filter, _jsxs("div", { ...props, ref: ref, role: "radiogroup", "aria-disabled": disabled || undefined, className: ['opal-segmented', className].filter(Boolean).join(' '), "data-lensing": glass.lensing || undefined, "data-disabled": disabled || undefined, "data-dragging": pressed !== undefined && !disabled || undefined, "data-tracking": dragPosition !== undefined && !disabled || undefined, "data-rebounding": rebounding || undefined, style: { '--opal-segment-count': Math.max(1, items.length), '--opal-segment-index': Math.max(0, index),
100
+ '--opal-segment-stretch': overdrag.stretch, '--opal-segment-pull': `${overdrag.offset}px`, ...glass.surfaceStyle, ...style }, children: [index >= 0 && _jsx("span", { ref: indicator, className: "opal-segmented-indicator", "aria-hidden": "true", onTransitionEnd: event => { if (event.propertyName === 'transform')
101
+ setRebounding(false); } }), _jsx("span", { ref: labelSource, className: "opal-segmented-optical-labels", "aria-hidden": "true", style: glass.sourceStyle, children: items.map(item => _jsx("span", { className: "opal-segmented-optical-label", "data-selected": item.value === selected || undefined, "data-disabled": item.disabled || undefined, children: item.label }, item.value)) }), items.map((item, itemIndex) => _jsx("button", { type: "button", role: "radio", "aria-checked": item.value === selected, disabled: disabled || item.disabled, tabIndex: !disabled && itemIndex === focusIndex ? 0 : -1, className: "opal-segmented-item", "data-pressed": pressed === item.value || undefined, onPointerDown: event => {
102
+ if (event.button !== 0 || !event.isPrimary || drag.current || disabled)
103
+ return;
104
+ setRebounding(false);
105
+ event.currentTarget.setPointerCapture(event.pointerId);
106
+ drag.current = { pointerId: event.pointerId, startX: event.clientX, startIndex: itemIndex, position: itemIndex, stretch: 0, target: event.currentTarget };
107
+ setPressed(item.value);
108
+ select(item.value);
109
+ }, onPointerMove: event => {
110
+ const active = drag.current;
111
+ if (!active || active.pointerId !== event.pointerId || disabled)
112
+ return;
113
+ // Fractional segment positions follow the pointer without selecting
114
+ // intermediate values. The initial grab offset is preserved.
115
+ movePill(event.clientX);
116
+ }, onPointerUp: event => {
117
+ // A fast release can arrive without a final pointermove.
118
+ finishDrag(event.pointerId, event.clientX);
119
+ }, onPointerCancel: event => finishDrag(event.pointerId), onLostPointerCapture: event => finishDrag(event.pointerId), onDragStart: event => event.preventDefault(), onClick: event => {
120
+ setPressed(undefined);
121
+ const pending = pointerClick.current;
122
+ pointerClick.current = null;
123
+ if (pending?.target === event.currentTarget && Date.now() < pending.until)
124
+ return;
125
+ // Pointer selection already happened on press. Keep keyboard and
126
+ // assistive-technology clicks working without a second callback.
127
+ if (event.detail === 0)
128
+ select(item.value);
129
+ }, onKeyDown: event => {
130
+ pointerClick.current = null;
131
+ if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key))
132
+ return;
133
+ event.preventDefault();
134
+ const group = event.currentTarget.parentElement;
135
+ const rtl = getComputedStyle(group).direction === 'rtl';
136
+ const enabled = items.map((entry, i) => entry.disabled ? -1 : i).filter(i => i >= 0);
137
+ const position = enabled.indexOf(itemIndex);
138
+ const backward = event.key === 'ArrowUp' || event.key === (rtl ? 'ArrowRight' : 'ArrowLeft');
139
+ const next = event.key === 'Home' ? enabled[0] : event.key === 'End' ? enabled.at(-1)
140
+ : enabled[(position + (backward ? -1 : 1) + enabled.length) % enabled.length];
141
+ if (next === undefined)
142
+ return;
143
+ select(items[next].value);
144
+ group.querySelectorAll('.opal-segmented-item')[next]?.focus();
145
+ }, children: _jsx("span", { className: "opal-segmented-label", children: item.label }) }, item.value))] })] });
146
+ });
147
+ //# sourceMappingURL=segmented-control.js.map