@opalui/react 0.0.0-stage → 0.0.1

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 +157 -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 +16 -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 +43 -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 +85 -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 +109 -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 +137 -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 +87 -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 +103 -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 +29 -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 +48 -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 +7 -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,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,109 @@
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-shadow: var(--opal-glass-shadow, 0 3px 8px rgb(15 34 47 / 12%));
5
+ }
6
+
7
+ :where(.opal-input,.opal-textarea,.opal-select) { --opal-material-frost:12px; --opal-material-rim:0.22; }
8
+ :where(.opal-menu-panel) { --opal-material-frost:24px; --opal-material-rim:0.3; }
9
+
10
+
11
+ .opal-segmented-filter { position: absolute; pointer-events: none; }
12
+ .opal-segmented {
13
+ position: relative;
14
+ display: inline-grid;
15
+ grid-template-columns: repeat(var(--opal-segment-count), minmax(0, 1fr));
16
+ box-sizing: border-box;
17
+ padding: 3px;
18
+ border-radius: 999px;
19
+ background: var(--opal-segmented-background, rgb(0 0 0 / 5%));
20
+ color: var(--opal-segmented-color, #202020);
21
+ box-shadow: var(--opal-segmented-shadow, var(--_opal-glass-shadow));
22
+ vertical-align: middle;
23
+ max-width: 100%;
24
+ isolation: isolate;
25
+ }
26
+ .opal-segmented::after {
27
+ content: ''; position: absolute; inset: 0; border-radius: inherit;
28
+ pointer-events: none;
29
+ box-shadow: var(--opal-segmented-rim, var(--_opal-glass-rim));
30
+ opacity: .7;
31
+ }
32
+ .opal-segmented-indicator {
33
+ position: absolute;
34
+ inset-block: 3px;
35
+ inset-inline-start: calc(3px + (100% - 6px) / var(--opal-segment-count) * var(--opal-segment-index));
36
+ width: calc((100% - 6px) / var(--opal-segment-count));
37
+ border-radius: 999px;
38
+ pointer-events: none;
39
+ background: var(--opal-segmented-thumb, #fff);
40
+ box-shadow: 0 1px 3px rgb(0 0 0 / 10%), inset 0 0 0 0.5px rgb(255 255 255 / 18%);
41
+ 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;
42
+ }
43
+ .opal-segmented-item {
44
+ position: relative;
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ min-width: 0;
49
+ min-height: 36px;
50
+ padding: 7px 24px;
51
+ border: 0;
52
+ border-radius: 999px;
53
+ background: transparent;
54
+ color: inherit;
55
+ font: inherit;
56
+ line-height: 1.25;
57
+ cursor: pointer;
58
+ touch-action: none;
59
+ -webkit-tap-highlight-color: transparent;
60
+ user-select: none;
61
+ -webkit-user-select: none;
62
+ }
63
+ .opal-segmented-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: transform 170ms ease; }
64
+ .opal-segmented-item[aria-checked="true"] { color: var(--opal-segmented-selected-color, inherit); }
65
+ .opal-segmented-item:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 3px; }
66
+ .opal-segmented:is([data-dragging], :has(.opal-segmented-item:active:not(:disabled))) .opal-segmented-indicator {
67
+ 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));
68
+ background: var(--opal-segmented-lifted, rgb(255 255 255 / 30%));
69
+ z-index: 0;
70
+ -webkit-backdrop-filter: blur(.25px) saturate(110%);
71
+ backdrop-filter: blur(.25px) saturate(110%);
72
+ box-shadow: 0 5px 12px rgb(0 0 0 / 13%), var(--_opal-glass-rim);
73
+ transition-duration: 170ms, 170ms, 140ms, 170ms;
74
+ }
75
+ /* Pointer movement is direct; restoring the normal transition on release
76
+ lets the pill settle onto the nearest enabled segment. */
77
+ .opal-segmented[data-tracking] .opal-segmented-indicator {
78
+ transition-property: transform, background-color, box-shadow;
79
+ transition-duration: 170ms, 140ms, 170ms;
80
+ }
81
+ /* A small overshoot on release gives the resisted pull a soft rebound. */
82
+ .opal-segmented[data-rebounding]:not([data-dragging]) .opal-segmented-indicator {
83
+ 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;
84
+ }
85
+ .opal-segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }
86
+ .opal-segmented[data-disabled] { opacity: 0.4; }
87
+ .opal-segmented[data-disabled] .opal-segmented-item { opacity: 1; }
88
+ @media (prefers-reduced-motion: reduce) {
89
+ .opal-segmented .opal-segmented-indicator, .opal-segmented .opal-segmented-label { transition: none !important; transform: none !important; }
90
+ }
91
+ @media (prefers-reduced-transparency: reduce) {
92
+ .opal-segmented { background: var(--opal-segmented-solid-background, #ededed); }
93
+ .opal-segmented .opal-segmented-indicator { z-index: 0; background: var(--opal-segmented-thumb, #fff) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
94
+ }
95
+ @media (forced-colors: active) {
96
+ .opal-segmented::after { display: none; }
97
+ .opal-segmented { background: Canvas; color: ButtonText; outline: 1px solid ButtonText; }
98
+ .opal-segmented .opal-segmented-indicator { z-index: 0; background: Highlight !important; box-shadow: none !important; transform: none !important; }
99
+ .opal-segmented-item[aria-checked="true"] { color: HighlightText; }
100
+ .opal-segmented-item:disabled { color: GrayText; }
101
+ }
102
+
103
+ /* An inert visual layer owns the ink; native buttons retain all semantics. */
104
+ .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; }
105
+ .opal-segmented-optical-label { display:flex; align-items:center; justify-content:center; min-width:0; white-space:nowrap; }
106
+ .opal-segmented-optical-label[data-selected] { color:var(--opal-segmented-selected-color,inherit); }
107
+ .opal-segmented-optical-label[data-disabled] { opacity:.4; }
108
+ .opal-segmented[data-lensing] .opal-segmented-optical-labels { visibility:visible; }
109
+ .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: .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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmented-control.js","sourceRoot":"","sources":["../../../src/components/segmented-control/segmented-control.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { forwardRef, useEffect, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from 'react'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport { useGlassLens } from '../../internal/glass/lens.js'\nimport { materials } from '../../internal/glass/material.js'\nimport componentCss from './segmented-control.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\n\nconst styleHref = stylesheetHref('segmented-control', css)\n\nexport type SegmentedControlItem = { value: string; label: ReactNode; disabled?: boolean }\nexport type SegmentedControlProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'role'> & {\n items: readonly SegmentedControlItem[]\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n disabled?: boolean\n}\n\n/** A single-choice control. Supply aria-label or aria-labelledby for its group. */\nexport const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(function SegmentedControl(\n { items, value, defaultValue, onValueChange, disabled = false, className, style, ...props }, ref,\n) {\n const [internalValue, setInternalValue] = useState(() => defaultValue ?? items.find(item => !item.disabled)?.value)\n const [pressed, setPressed] = useState<string>()\n const [dragPosition, setDragPosition] = useState<number>()\n const [overdrag, setOverdrag] = useState({ stretch: 0, offset: 0 })\n const [rebounding, setRebounding] = useState(false)\n const indicator = useRef<HTMLSpanElement>(null)\n const labelSource = useRef<HTMLSpanElement>(null)\n const glass = useGlassLens(indicator, materials.selection, { source: labelSource, active: pressed !== undefined && !disabled, ink: true })\n const pointerClick = useRef<{ target: HTMLButtonElement; until: number } | null>(null)\n const drag = useRef<{ pointerId: number; startX: number; startIndex: number; position: number; stretch: number; target: HTMLButtonElement } | null>(null)\n\n function nearestEnabled(position: number) {\n return items.reduce((best, item, i) => item.disabled ? best\n : best < 0 || Math.abs(i - position) < Math.abs(best - position) ? i : best, -1)\n }\n\n function movePill(clientX: number) {\n const active = drag.current\n if (!active || disabled) return\n const group = active.target.parentElement\n if (!group) return\n const width = active.target.getBoundingClientRect().width\n if (!width) return\n const direction = getComputedStyle(group).direction === 'rtl' ? -1 : 1\n const rawPosition = active.startIndex + direction * (clientX - active.startX) / width\n const position = Math.max(0, Math.min(items.length - 1, rawPosition))\n const excess = rawPosition - position\n // Increasing resistance caps the stretch at 14% of a segment's width.\n // Shift slightly beyond half the extension so the whole pill drifts with the pull.\n const stretch = 0.14 * (1 - Math.exp(-Math.abs(excess) / 0.9))\n setOverdrag({ stretch, offset: direction * Math.sign(excess) * stretch * width * 0.85 })\n active.position = position\n active.stretch = stretch\n setDragPosition(position)\n const nearest = nearestEnabled(position)\n setPressed(items[nearest]?.value)\n return stretch\n }\n function endDrag() {\n const active = drag.current\n drag.current = null\n setPressed(undefined)\n setDragPosition(undefined)\n setOverdrag({ stretch: 0, offset: 0 })\n setRebounding(false)\n if (active?.target.hasPointerCapture(active.pointerId)) active.target.releasePointerCapture(active.pointerId)\n }\n useEffect(() => {\n window.addEventListener('blur', endDrag)\n return () => window.removeEventListener('blur', endDrag)\n }, [])\n useEffect(() => { if (disabled) endDrag() }, [disabled])\n const selected = value === undefined ? internalValue : value\n const selectedIndex = items.findIndex(item => item.value === selected)\n const pressedIndex = items.findIndex(item => item.value === pressed && !item.disabled)\n const index = dragPosition ?? (pressedIndex >= 0 ? pressedIndex : selectedIndex)\n const firstEnabled = items.findIndex(item => !item.disabled)\n const focusIndex = selectedIndex >= 0 && !items[selectedIndex]?.disabled ? selectedIndex : firstEnabled\n\n function select(next: string) {\n if (disabled || items.find(item => item.value === next)?.disabled || next === selected) return\n if (value === undefined) setInternalValue(next)\n onValueChange?.(next)\n }\n\n function finishDrag(pointerId: number, clientX?: number) {\n const active = drag.current\n if (!active || active.pointerId !== pointerId) return\n // Cancellation/capture loss may carry empty coordinates; keep the last\n // tracked position in those cases instead of reverting to the press origin.\n if (clientX !== undefined) movePill(clientX)\n const nextIndex = nearestEnabled(active.position)\n const next = items[nextIndex]\n const button = active.target.parentElement?.querySelectorAll<HTMLButtonElement>('.opal-segmented-item')[nextIndex]\n pointerClick.current = { target: active.target, until: Date.now() + 500 }\n endDrag()\n if (next) select(next.value)\n setRebounding(active.stretch > 0.005)\n button?.focus({ preventScroll: true })\n }\n\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n {glass.filter}\n <div {...props} ref={ref} role=\"radiogroup\" aria-disabled={disabled || undefined}\n className={['opal-segmented', className].filter(Boolean).join(' ')}\n data-lensing={glass.lensing || undefined}\n data-disabled={disabled || undefined}\n data-dragging={pressed !== undefined && !disabled || undefined}\n data-tracking={dragPosition !== undefined && !disabled || undefined}\n data-rebounding={rebounding || undefined}\n style={{ '--opal-segment-count': Math.max(1, items.length), '--opal-segment-index': Math.max(0, index),\n '--opal-segment-stretch': overdrag.stretch, '--opal-segment-pull': `${overdrag.offset}px`, ...glass.surfaceStyle, ...style } as CSSProperties}>\n {index >= 0 && <span ref={indicator} className=\"opal-segmented-indicator\" aria-hidden=\"true\"\n onTransitionEnd={event => { if (event.propertyName === 'transform') setRebounding(false) }} />}\n <span ref={labelSource} className=\"opal-segmented-optical-labels\" aria-hidden=\"true\" style={glass.sourceStyle}>\n {items.map(item => <span key={item.value} className=\"opal-segmented-optical-label\" data-selected={item.value === selected || undefined} data-disabled={item.disabled || undefined}>{item.label}</span>)}\n </span>\n {items.map((item, itemIndex) => <button key={item.value} type=\"button\" role=\"radio\"\n aria-checked={item.value === selected} disabled={disabled || item.disabled}\n tabIndex={!disabled && itemIndex === focusIndex ? 0 : -1}\n className=\"opal-segmented-item\"\n data-pressed={pressed === item.value || undefined}\n onPointerDown={event => {\n if (event.button !== 0 || !event.isPrimary || drag.current || disabled) return\n setRebounding(false)\n event.currentTarget.setPointerCapture(event.pointerId)\n drag.current = { pointerId: event.pointerId, startX: event.clientX, startIndex: itemIndex, position: itemIndex, stretch: 0, target: event.currentTarget }\n setPressed(item.value)\n select(item.value)\n }}\n onPointerMove={event => {\n const active = drag.current\n if (!active || active.pointerId !== event.pointerId || disabled) return\n // Fractional segment positions follow the pointer without selecting\n // intermediate values. The initial grab offset is preserved.\n movePill(event.clientX)\n }}\n onPointerUp={event => {\n // A fast release can arrive without a final pointermove.\n finishDrag(event.pointerId, event.clientX)\n }}\n onPointerCancel={event => finishDrag(event.pointerId)}\n onLostPointerCapture={event => finishDrag(event.pointerId)}\n onDragStart={event => event.preventDefault()}\n onClick={event => {\n setPressed(undefined)\n const pending = pointerClick.current\n pointerClick.current = null\n if (pending?.target === event.currentTarget && Date.now() < pending.until) return\n // Pointer selection already happened on press. Keep keyboard and\n // assistive-technology clicks working without a second callback.\n if (event.detail === 0) select(item.value)\n }}\n onKeyDown={event => {\n pointerClick.current = null\n if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return\n event.preventDefault()\n const group = event.currentTarget.parentElement!\n const rtl = getComputedStyle(group).direction === 'rtl'\n const enabled = items.map((entry, i) => entry.disabled ? -1 : i).filter(i => i >= 0)\n const position = enabled.indexOf(itemIndex)\n const backward = event.key === 'ArrowUp' || event.key === (rtl ? 'ArrowRight' : 'ArrowLeft')\n const next = event.key === 'Home' ? enabled[0] : event.key === 'End' ? enabled.at(-1)\n : enabled[(position + (backward ? -1 : 1) + enabled.length) % enabled.length]\n if (next === undefined) return\n select(items[next]!.value)\n group.querySelectorAll<HTMLButtonElement>('.opal-segmented-item')[next]?.focus()\n }}>\n <span className=\"opal-segmented-label\">{item.label}</span>\n </button>)}\n </div>\n </>\n})\n"]}