@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.
- package/dist/components/button/button.css +186 -0
- package/dist/components/button/button.css.js +1 -0
- package/dist/components/button/button.d.ts +26 -0
- package/dist/components/button/button.js +39 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/glass/glass.css +43 -0
- package/dist/components/glass/glass.css.js +1 -0
- package/dist/components/glass/glass.d.ts +5 -0
- package/dist/components/glass/glass.js +19 -0
- package/dist/components/glass/glass.js.map +1 -0
- package/dist/components/input/input.css +70 -0
- package/dist/components/input/input.css.js +1 -0
- package/dist/components/input/input.d.ts +17 -0
- package/dist/components/input/input.js +36 -0
- package/dist/components/input/input.js.map +1 -0
- package/dist/components/menu/menu.css +112 -0
- package/dist/components/menu/menu.css.js +1 -0
- package/dist/components/menu/menu.d.ts +18 -0
- package/dist/components/menu/menu.js +125 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/segmented-control/segmented-control.css +136 -0
- package/dist/components/segmented-control/segmented-control.css.js +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +21 -0
- package/dist/components/segmented-control/segmented-control.js +147 -0
- package/dist/components/segmented-control/segmented-control.js.map +1 -0
- package/dist/components/select/select.css +164 -0
- package/dist/components/select/select.css.js +1 -0
- package/dist/components/select/select.d.ts +27 -0
- package/dist/components/select/select.js +140 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/slider/slider.css +114 -0
- package/dist/components/slider/slider.css.js +1 -0
- package/dist/components/slider/slider.d.ts +17 -0
- package/dist/components/slider/slider.js +40 -0
- package/dist/components/slider/slider.js.map +1 -0
- package/dist/components/switch/switch.css +130 -0
- package/dist/components/switch/switch.css.js +1 -0
- package/dist/components/switch/switch.d.ts +14 -0
- package/dist/components/switch/switch.js +74 -0
- package/dist/components/switch/switch.js.map +1 -0
- package/dist/components/textarea/textarea.css +56 -0
- package/dist/components/textarea/textarea.css.js +1 -0
- package/dist/components/textarea/textarea.d.ts +8 -0
- package/dist/components/textarea/textarea.js +17 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/glass/lens.d.ts +44 -0
- package/dist/internal/glass/lens.js +116 -0
- package/dist/internal/glass/lens.js.map +1 -0
- package/dist/internal/glass/map.d.ts +1 -0
- package/dist/internal/glass/map.js +48 -0
- package/dist/internal/glass/map.js.map +1 -0
- package/dist/internal/glass/material.d.ts +55 -0
- package/dist/internal/glass/material.js +22 -0
- package/dist/internal/glass/material.js.map +1 -0
- package/dist/internal/glass/styles.d.ts +2 -0
- package/dist/internal/glass/styles.js +4 -0
- package/dist/internal/glass/styles.js.map +1 -0
- package/dist/internal/glass/support.d.ts +5 -0
- package/dist/internal/glass/support.js +8 -0
- package/dist/internal/glass/support.js.map +1 -0
- package/dist/internal/glass.css +34 -0
- package/dist/internal/glass.css.js +1 -0
- package/dist/internal/styles.d.ts +2 -0
- package/dist/internal/styles.js +9 -0
- package/dist/internal/styles.js.map +1 -0
- package/dist/styles.css +11 -0
- package/dist/styles.css.js +1 -0
- package/package.json +90 -4
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slider.js","sourceRoot":"","sources":["../../../src/components/slider/slider.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties } from 'react'\nimport { useGlassHeld, useGlassLens } from '../../internal/glass/lens.js'\nimport { materials } from '../../internal/glass/material.js'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport componentCss from './slider.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\nconst styleHref = stylesheetHref('slider', css)\nexport type SliderProps = Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'children' | 'value' | 'defaultValue' | 'min' | 'max'> & {\n value?: number\n defaultValue?: number\n min?: number\n max?: number\n tint?: `#${string}`\n onValueChange?: (value: number) => void\n}\n\nexport const Slider = forwardRef<HTMLInputElement, SliderProps>(function Slider(\n { value, defaultValue, min = 0, max = 100, step = 1, tint, className, style, onChange, onValueChange, ...props }, ref,\n) {\n const root = useRef<HTMLSpanElement>(null)\n const thumb = useRef<HTMLSpanElement>(null)\n const track = useRef<HTMLSpanElement>(null)\n const held = useGlassHeld(root)\n const glass = useGlassLens(thumb, materials.thumb, { source: track, active: held && !props.disabled })\n const input = useRef<HTMLInputElement>(null)\n\n useImperativeHandle(ref, () => input.current!, [])\n const [internal, setInternal] = useState(defaultValue ?? (min + max) / 2)\n const current = value ?? internal\n const progress = max > min ? Math.max(0, Math.min(100, (current - min) / (max - min) * 100)) : 0\n useEffect(() => {\n const field = input.current\n if (field) setInternal(field.valueAsNumber)\n const form = field?.form\n const reset = () => queueMicrotask(() => { if (field) setInternal(field.valueAsNumber) })\n form?.addEventListener('reset', reset)\n return () => form?.removeEventListener('reset', reset)\n }, [min, max, step])\n const tintStyle: CSSProperties = tint && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(tint) ? { '--opal-slider-tint': tint } as CSSProperties : {}\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n {glass.filter}\n <span ref={root} className={['opal-slider', className].filter(Boolean).join(' ')}\n style={{ '--opal-slider-progress': `${progress}%`, '--opal-slider-fraction': progress / 100, ...tintStyle, ...glass.surfaceStyle, ...style } as CSSProperties}>\n <input {...props} ref={input} type=\"range\" min={min} max={max} step={step} value={value} defaultValue={defaultValue}\n className=\"opal-slider-input\"\n onChange={event => {\n setInternal(event.currentTarget.valueAsNumber)\n onChange?.(event)\n if (!event.defaultPrevented) onValueChange?.(event.currentTarget.valueAsNumber)\n }} />\n <span ref={track} className=\"opal-slider-optical-track\" style={glass.sourceStyle} aria-hidden=\"true\"><span className=\"opal-slider-track\" /></span>\n <span ref={thumb} className=\"opal-slider-thumb\" aria-hidden=\"true\" />\n </span>\n </>\n})\n"]}
|
|
@@ -0,0 +1,130 @@
|
|
|
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-switch {
|
|
39
|
+
--opal-switch-track-color: var(--opal-switch-off-background, #73767c);
|
|
40
|
+
--opal-switch-offset: 0px;
|
|
41
|
+
position: relative;
|
|
42
|
+
display: inline-block;
|
|
43
|
+
flex: 0 0 auto;
|
|
44
|
+
width: 64px;
|
|
45
|
+
height: 28px;
|
|
46
|
+
vertical-align: middle;
|
|
47
|
+
overflow: visible;
|
|
48
|
+
-webkit-tap-highlight-color: transparent;
|
|
49
|
+
}
|
|
50
|
+
.opal-switch:has(.opal-switch-input:checked) {
|
|
51
|
+
--opal-switch-track-color: var(--opal-switch-on-background, #28cd50);
|
|
52
|
+
--opal-switch-offset: 22px;
|
|
53
|
+
}
|
|
54
|
+
.opal-switch-input {
|
|
55
|
+
position: absolute;
|
|
56
|
+
z-index: 3;
|
|
57
|
+
inset: -8px 0;
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 44px;
|
|
60
|
+
margin: 0;
|
|
61
|
+
opacity: 0;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
touch-action: pan-y;
|
|
64
|
+
}
|
|
65
|
+
.opal-switch-track, .opal-switch-thumb { position:absolute; pointer-events:none; box-sizing:border-box; }
|
|
66
|
+
.opal-switch-track {
|
|
67
|
+
inset: 0;
|
|
68
|
+
border-radius: 999px;
|
|
69
|
+
background: var(--opal-switch-track-color);
|
|
70
|
+
transition: background-color 320ms ease;
|
|
71
|
+
}
|
|
72
|
+
.opal-switch-thumb {
|
|
73
|
+
left: 3px;
|
|
74
|
+
top: 3px;
|
|
75
|
+
width: 36px;
|
|
76
|
+
height: 22px;
|
|
77
|
+
border-radius: 999px;
|
|
78
|
+
translate: var(--opal-switch-offset) 0;
|
|
79
|
+
background: #fff;
|
|
80
|
+
box-shadow: 0 2px 5px rgb(0 0 0 / 13%), inset 0 0 0 0.5px rgb(255 255 255 / 70%);
|
|
81
|
+
transition:
|
|
82
|
+
translate 480ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
83
|
+
transform 500ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
84
|
+
background-color 300ms ease,
|
|
85
|
+
box-shadow 400ms ease;
|
|
86
|
+
}
|
|
87
|
+
.opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb {
|
|
88
|
+
transform: scale(calc(1.45 + var(--opal-switch-stretch, 0) * .12), calc(1.45 - var(--opal-switch-stretch, 0) * .08));
|
|
89
|
+
background: rgb(255 255 255 / 8%);
|
|
90
|
+
-webkit-backdrop-filter: blur(var(--opal-material-frost, .25px));
|
|
91
|
+
backdrop-filter: blur(var(--opal-material-frost, .25px));
|
|
92
|
+
box-shadow: 0 7px 16px rgb(0 0 0 / 18%), var(--_opal-glass-rim);
|
|
93
|
+
}
|
|
94
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
95
|
+
.opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb {
|
|
96
|
+
transition:
|
|
97
|
+
translate 480ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
98
|
+
transform 170ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
99
|
+
background-color 140ms ease-out,
|
|
100
|
+
box-shadow 170ms ease-out;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/* Follow the pointer directly; restore the soft settling transition on release. */
|
|
104
|
+
.opal-switch[data-dragging] .opal-switch-thumb { transition: background-color 140ms ease-out, box-shadow 170ms ease-out; }
|
|
105
|
+
.opal-switch-input:focus-visible ~ .opal-switch-optical-track .opal-switch-track {
|
|
106
|
+
outline: 2px solid var(--opal-focus-color, #2885ff);
|
|
107
|
+
outline-offset: 5px;
|
|
108
|
+
}
|
|
109
|
+
.opal-switch:has(.opal-switch-input:disabled) { opacity: 0.4; }
|
|
110
|
+
.opal-switch-input:disabled { cursor: not-allowed; }
|
|
111
|
+
@media (prefers-reduced-motion: reduce) {
|
|
112
|
+
.opal-switch *, .opal-switch *::before, .opal-switch *::after { transition: none; }
|
|
113
|
+
.opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb { transform: none; }
|
|
114
|
+
.opal-switch:has(.opal-switch-input:active) .opal-switch-track { mask-image: none; -webkit-mask-image: none; }
|
|
115
|
+
.opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb { background: #fff; }
|
|
116
|
+
}
|
|
117
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
118
|
+
.opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb { background: #fff; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
119
|
+
.opal-switch:has(.opal-switch-input:active) .opal-switch-track { mask-image: none; -webkit-mask-image: none; }
|
|
120
|
+
}
|
|
121
|
+
@media (forced-colors: active) {
|
|
122
|
+
.opal-switch { opacity: 1 !important; }
|
|
123
|
+
.opal-switch-track { background: Canvas; border: 1px solid ButtonText; mask-image: none !important; -webkit-mask-image: none !important; }
|
|
124
|
+
.opal-switch:has(.opal-switch-input:checked) .opal-switch-track { background: Highlight; }
|
|
125
|
+
.opal-switch .opal-switch-thumb { background: ButtonText !important; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; transform: none !important; }
|
|
126
|
+
.opal-switch-input:disabled ~ .opal-switch-thumb { background: GrayText !important; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.opal-switch-optical-track { position:absolute; inset:-24px; pointer-events:none; }
|
|
130
|
+
.opal-switch-optical-track .opal-switch-track { left:24px; right:24px; width:calc(100% - 48px); top:24px; bottom:24px; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default ".opal-switch {\n --opal-switch-track-color: var(--opal-switch-off-background, #73767c);\n --opal-switch-offset: 0px;\n position: relative;\n display: inline-block;\n flex: 0 0 auto;\n width: 64px;\n height: 28px;\n vertical-align: middle;\n overflow: visible;\n -webkit-tap-highlight-color: transparent;\n}\n.opal-switch:has(.opal-switch-input:checked) {\n --opal-switch-track-color: var(--opal-switch-on-background, #28cd50);\n --opal-switch-offset: 22px;\n}\n.opal-switch-input {\n position: absolute;\n z-index: 3;\n inset: -8px 0;\n width: 100%;\n height: 44px;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n touch-action: pan-y;\n}\n.opal-switch-track, .opal-switch-thumb { position:absolute; pointer-events:none; box-sizing:border-box; }\n.opal-switch-track {\n inset: 0;\n border-radius: 999px;\n background: var(--opal-switch-track-color);\n transition: background-color 320ms ease;\n}\n.opal-switch-thumb {\n left: 3px;\n top: 3px;\n width: 36px;\n height: 22px;\n border-radius: 999px;\n translate: var(--opal-switch-offset) 0;\n background: #fff;\n box-shadow: 0 2px 5px rgb(0 0 0 / 13%), inset 0 0 0 0.5px rgb(255 255 255 / 70%);\n transition:\n translate 480ms cubic-bezier(0.22, 0.61, 0.36, 1),\n transform 500ms cubic-bezier(0.22, 0.61, 0.36, 1),\n background-color 300ms ease,\n box-shadow 400ms ease;\n}\n.opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb {\n transform: scale(calc(1.45 + var(--opal-switch-stretch, 0) * .12), calc(1.45 - var(--opal-switch-stretch, 0) * .08));\n background: rgb(255 255 255 / 8%);\n -webkit-backdrop-filter: blur(var(--opal-material-frost, .25px));\n backdrop-filter: blur(var(--opal-material-frost, .25px));\n box-shadow: 0 7px 16px rgb(0 0 0 / 18%), var(--_opal-glass-rim);\n}\n@media (prefers-reduced-motion: no-preference) {\n .opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb {\n transition:\n translate 480ms cubic-bezier(0.22, 0.61, 0.36, 1),\n transform 170ms cubic-bezier(0.16, 1, 0.3, 1),\n background-color 140ms ease-out,\n box-shadow 170ms ease-out;\n }\n}\n/* Follow the pointer directly; restore the soft settling transition on release. */\n.opal-switch[data-dragging] .opal-switch-thumb { transition: background-color 140ms ease-out, box-shadow 170ms ease-out; }\n.opal-switch-input:focus-visible ~ .opal-switch-optical-track .opal-switch-track {\n outline: 2px solid var(--opal-focus-color, #2885ff);\n outline-offset: 5px;\n}\n.opal-switch:has(.opal-switch-input:disabled) { opacity: 0.4; }\n.opal-switch-input:disabled { cursor: not-allowed; }\n@media (prefers-reduced-motion: reduce) {\n .opal-switch *, .opal-switch *::before, .opal-switch *::after { transition: none; }\n .opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb { transform: none; }\n .opal-switch:has(.opal-switch-input:active) .opal-switch-track { mask-image: none; -webkit-mask-image: none; }\n .opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb { background: #fff; }\n}\n@media (prefers-reduced-transparency: reduce) {\n .opal-switch-input:active:not(:disabled) ~ .opal-switch-thumb { background: #fff; backdrop-filter: none; -webkit-backdrop-filter: none; }\n .opal-switch:has(.opal-switch-input:active) .opal-switch-track { mask-image: none; -webkit-mask-image: none; }\n}\n@media (forced-colors: active) {\n .opal-switch { opacity: 1 !important; }\n .opal-switch-track { background: Canvas; border: 1px solid ButtonText; mask-image: none !important; -webkit-mask-image: none !important; }\n .opal-switch:has(.opal-switch-input:checked) .opal-switch-track { background: Highlight; }\n .opal-switch .opal-switch-thumb { background: ButtonText !important; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; transform: none !important; }\n .opal-switch-input:disabled ~ .opal-switch-thumb { background: GrayText !important; }\n}\n\n.opal-switch-optical-track { position:absolute; inset:-24px; pointer-events:none; }\n.opal-switch-optical-track .opal-switch-track { left:24px; right:24px; width:calc(100% - 48px); top:24px; bottom:24px; }\n";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
export type SwitchProps = Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'role' | 'size' | 'children'> & {
|
|
3
|
+
/** Active track color as an opaque #RGB or #RRGGBB hex. Defaults to green. */
|
|
4
|
+
tint?: `#${string}`;
|
|
5
|
+
/** Called after the native change handler, unless it prevents default. */
|
|
6
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
7
|
+
};
|
|
8
|
+
/** A native checkbox with switch semantics, automatic styles, and a glass thumb. */
|
|
9
|
+
export declare const Switch: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "size" | "type" | "role" | "children"> & {
|
|
10
|
+
/** Active track color as an opaque #RGB or #RRGGBB hex. Defaults to green. */
|
|
11
|
+
tint?: `#${string}`;
|
|
12
|
+
/** Called after the native change handler, unless it prevents default. */
|
|
13
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
14
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useRef, useState } from 'react';
|
|
4
|
+
import { useGlassHeld, useGlassLens } from '../../internal/glass/lens.js';
|
|
5
|
+
import { materials } from '../../internal/glass/material.js';
|
|
6
|
+
import { stylesheetHref } from '../../internal/styles.js';
|
|
7
|
+
import componentCss from './switch.css.js';
|
|
8
|
+
import glassCss from '../../internal/glass/styles.js';
|
|
9
|
+
const css = `${glassCss}\n${componentCss}`;
|
|
10
|
+
const styleHref = stylesheetHref('switch', css);
|
|
11
|
+
/** A native checkbox with switch semantics, automatic styles, and a glass thumb. */
|
|
12
|
+
export const Switch = forwardRef(function Switch({ className, style, tint, onChange, onCheckedChange, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onClick, ...props }, ref) {
|
|
13
|
+
const root = useRef(null);
|
|
14
|
+
const thumb = useRef(null);
|
|
15
|
+
const track = useRef(null);
|
|
16
|
+
const held = useGlassHeld(root);
|
|
17
|
+
const glass = useGlassLens(thumb, materials.switch, { contract: true, source: track, active: held && !props.disabled });
|
|
18
|
+
const gesture = useRef(null);
|
|
19
|
+
const releaseValue = useRef(null);
|
|
20
|
+
const [drag, setDrag] = useState(null);
|
|
21
|
+
function resetDrag() { gesture.current = null; setDrag(null); }
|
|
22
|
+
const tintStyle = tint && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(tint)
|
|
23
|
+
? { '--opal-switch-on-background': tint }
|
|
24
|
+
: {};
|
|
25
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), glass.filter, _jsxs("span", { ref: root, className: ['opal-switch', className].filter(Boolean).join(' '), "data-dragging": drag ? '' : undefined, style: { ...tintStyle, ...glass.surfaceStyle, ...style, ...(drag ? { '--opal-switch-offset': `${drag.offset}px`, '--opal-switch-stretch': drag.stretch } : {}) }, children: [_jsx("input", { ...props, ref: ref, type: "checkbox", role: "switch", className: "opal-switch-input", onPointerDown: event => {
|
|
26
|
+
onPointerDown?.(event);
|
|
27
|
+
releaseValue.current = null;
|
|
28
|
+
if (event.defaultPrevented || props.disabled || event.button !== 0 || !event.isPrimary)
|
|
29
|
+
return;
|
|
30
|
+
gesture.current = { id: event.pointerId, x: event.clientX, offset: event.currentTarget.checked ? 22 : 0, moved: false };
|
|
31
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
32
|
+
}, onPointerMove: event => {
|
|
33
|
+
onPointerMove?.(event);
|
|
34
|
+
const current = gesture.current;
|
|
35
|
+
if (!current || current.id !== event.pointerId || event.defaultPrevented)
|
|
36
|
+
return;
|
|
37
|
+
const delta = event.clientX - current.x;
|
|
38
|
+
if (!current.moved && Math.abs(delta) < 3)
|
|
39
|
+
return;
|
|
40
|
+
current.moved = true;
|
|
41
|
+
const raw = current.offset + delta;
|
|
42
|
+
const clamped = Math.max(0, Math.min(22, raw));
|
|
43
|
+
const overshoot = 5 * Math.tanh((raw - clamped) / 30);
|
|
44
|
+
setDrag({ offset: clamped + overshoot, stretch: Math.abs(overshoot) / 5 });
|
|
45
|
+
}, onPointerUp: event => {
|
|
46
|
+
onPointerUp?.(event);
|
|
47
|
+
const current = gesture.current;
|
|
48
|
+
if (!current || current.id !== event.pointerId)
|
|
49
|
+
return;
|
|
50
|
+
resetDrag();
|
|
51
|
+
if (current.moved && !event.defaultPrevented) {
|
|
52
|
+
releaseValue.current = current.offset + event.clientX - current.x >= 11;
|
|
53
|
+
}
|
|
54
|
+
// Keep capture through release: the browser delivers one native click
|
|
55
|
+
// to this input, even when the pointer ends outside the switch.
|
|
56
|
+
}, onPointerCancel: event => { onPointerCancel?.(event); releaseValue.current = null; resetDrag(); }, onLostPointerCapture: event => { onLostPointerCapture?.(event); resetDrag(); }, onClick: event => {
|
|
57
|
+
const next = releaseValue.current;
|
|
58
|
+
releaseValue.current = null;
|
|
59
|
+
// Native checkbox activation toggles checked before click handlers run.
|
|
60
|
+
// Cancel only when that toggle disagrees with the drag destination.
|
|
61
|
+
if (next !== null && event.currentTarget.checked !== next) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
onClick?.(event);
|
|
66
|
+
}, onChange: (event) => {
|
|
67
|
+
if (event.defaultPrevented)
|
|
68
|
+
return;
|
|
69
|
+
onChange?.(event);
|
|
70
|
+
if (!event.defaultPrevented)
|
|
71
|
+
onCheckedChange?.(event.currentTarget.checked);
|
|
72
|
+
} }), _jsx("span", { ref: track, className: "opal-switch-optical-track", style: glass.sourceStyle, "aria-hidden": "true", children: _jsx("span", { className: "opal-switch-track" }) }), _jsx("span", { ref: thumb, className: "opal-switch-thumb", "aria-hidden": "true" })] })] });
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=switch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"switch.js","sourceRoot":"","sources":["../../../src/components/switch/switch.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { forwardRef, useRef, useState, type CSSProperties, type ComponentPropsWithoutRef } from 'react'\nimport { useGlassHeld, useGlassLens } from '../../internal/glass/lens.js'\nimport { materials } from '../../internal/glass/material.js'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport componentCss from './switch.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\n\nconst styleHref = stylesheetHref('switch', css)\n\nexport type SwitchProps = Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'role' | 'size' | 'children'> & {\n /** Active track color as an opaque #RGB or #RRGGBB hex. Defaults to green. */\n tint?: `#${string}`\n /** Called after the native change handler, unless it prevents default. */\n onCheckedChange?: (checked: boolean) => void\n}\n\n/** A native checkbox with switch semantics, automatic styles, and a glass thumb. */\nexport const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(\n { className, style, tint, onChange, onCheckedChange, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onClick, ...props }, ref,\n) {\n const root = useRef<HTMLSpanElement>(null)\n const thumb = useRef<HTMLSpanElement>(null)\n const track = useRef<HTMLSpanElement>(null)\n const held = useGlassHeld(root)\n const glass = useGlassLens(thumb, materials.switch, { contract: true, source: track, active: held && !props.disabled })\n const gesture = useRef<{ id: number; x: number; offset: number; moved: boolean } | null>(null)\n const releaseValue = useRef<boolean | null>(null)\n const [drag, setDrag] = useState<{ offset: number; stretch: number } | null>(null)\n function resetDrag() { gesture.current = null; setDrag(null) }\n const tintStyle = tint && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(tint)\n ? { '--opal-switch-on-background': tint }\n : {}\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n {glass.filter}\n <span ref={root} className={['opal-switch', className].filter(Boolean).join(' ')} data-dragging={drag ? '' : undefined}\n style={{ ...tintStyle, ...glass.surfaceStyle, ...style, ...(drag ? { '--opal-switch-offset': `${drag.offset}px`, '--opal-switch-stretch': drag.stretch } : {}) } as CSSProperties}>\n <input {...props} ref={ref} type=\"checkbox\" role=\"switch\" className=\"opal-switch-input\"\n onPointerDown={event => {\n onPointerDown?.(event)\n releaseValue.current = null\n if (event.defaultPrevented || props.disabled || event.button !== 0 || !event.isPrimary) return\n gesture.current = { id: event.pointerId, x: event.clientX, offset: event.currentTarget.checked ? 22 : 0, moved: false }\n event.currentTarget.setPointerCapture(event.pointerId)\n }}\n onPointerMove={event => {\n onPointerMove?.(event)\n const current = gesture.current\n if (!current || current.id !== event.pointerId || event.defaultPrevented) return\n const delta = event.clientX - current.x\n if (!current.moved && Math.abs(delta) < 3) return\n current.moved = true\n const raw = current.offset + delta\n const clamped = Math.max(0, Math.min(22, raw))\n const overshoot = 5 * Math.tanh((raw - clamped) / 30)\n setDrag({ offset: clamped + overshoot, stretch: Math.abs(overshoot) / 5 })\n }}\n onPointerUp={event => {\n onPointerUp?.(event)\n const current = gesture.current\n if (!current || current.id !== event.pointerId) return\n resetDrag()\n if (current.moved && !event.defaultPrevented) {\n releaseValue.current = current.offset + event.clientX - current.x >= 11\n }\n // Keep capture through release: the browser delivers one native click\n // to this input, even when the pointer ends outside the switch.\n }}\n onPointerCancel={event => { onPointerCancel?.(event); releaseValue.current = null; resetDrag() }}\n onLostPointerCapture={event => { onLostPointerCapture?.(event); resetDrag() }}\n onClick={event => {\n const next = releaseValue.current\n releaseValue.current = null\n // Native checkbox activation toggles checked before click handlers run.\n // Cancel only when that toggle disagrees with the drag destination.\n if (next !== null && event.currentTarget.checked !== next) { event.preventDefault(); return }\n onClick?.(event)\n }}\n onChange={(event) => {\n if (event.defaultPrevented) return\n onChange?.(event)\n if (!event.defaultPrevented) onCheckedChange?.(event.currentTarget.checked)\n }} />\n <span ref={track} className=\"opal-switch-optical-track\" style={glass.sourceStyle} aria-hidden=\"true\"><span className=\"opal-switch-track\" /></span>\n <span ref={thumb} className=\"opal-switch-thumb\" aria-hidden=\"true\" />\n </span>\n </>\n})\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
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-textarea {
|
|
39
|
+
position: relative; display: inline-flex; box-sizing: border-box;
|
|
40
|
+
width: 360px; max-width: 100%; padding: 6px; border-radius: 20px;
|
|
41
|
+
background: var(--opal-input-background, rgb(255 255 255 / 65%)); color: var(--opal-input-color, #171e22);
|
|
42
|
+
box-shadow: var(--opal-input-shadow, var(--_opal-glass-shadow));
|
|
43
|
+
backdrop-filter: blur(var(--opal-material-frost,12px)); -webkit-backdrop-filter: blur(var(--opal-material-frost,12px));
|
|
44
|
+
transition: background-color 180ms ease;
|
|
45
|
+
}
|
|
46
|
+
.opal-textarea::after { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: var(--opal-input-rim, var(--_opal-glass-rim)); opacity: var(--opal-control-rim-opacity, .7); }
|
|
47
|
+
.opal-textarea:focus-within { background: var(--opal-input-focus-background, rgb(255 255 255 / 90%)); }
|
|
48
|
+
/* Inset the native resize grip from the outer rounded glass corner. */
|
|
49
|
+
.opal-textarea-field { display: block; box-sizing: border-box; width: 100%; min-width: 0; min-height: 88px; resize: vertical; padding: 8px 10px; border: 0; border-radius: 14px; outline: none; background: transparent; color: inherit; font: inherit; line-height: 1.5; }
|
|
50
|
+
.opal-textarea-field::placeholder { color: inherit; opacity: .5; }
|
|
51
|
+
.opal-textarea:has([aria-invalid="true"]) { outline: 2px solid #e5484d; outline-offset: 2px; }
|
|
52
|
+
.opal-textarea[data-disabled] { opacity: .4; }
|
|
53
|
+
.opal-textarea-field:disabled { cursor: not-allowed; resize: none; }
|
|
54
|
+
@media (prefers-reduced-motion: reduce) { .opal-textarea { transition: none; } }
|
|
55
|
+
@media (prefers-reduced-transparency: reduce) { .opal-textarea { background: var(--opal-input-solid, #fff); backdrop-filter: none; -webkit-backdrop-filter: none; } }
|
|
56
|
+
@media (forced-colors: active) { .opal-textarea { border: 1px solid ButtonText; } .opal-textarea:focus-within { outline: 2px solid Highlight; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default ".opal-textarea {\n position: relative; display: inline-flex; box-sizing: border-box;\n width: 360px; max-width: 100%; padding: 6px; border-radius: 20px;\n background: var(--opal-input-background, rgb(255 255 255 / 65%)); color: var(--opal-input-color, #171e22);\n box-shadow: var(--opal-input-shadow, var(--_opal-glass-shadow));\n backdrop-filter: blur(var(--opal-material-frost,12px)); -webkit-backdrop-filter: blur(var(--opal-material-frost,12px));\n transition: background-color 180ms ease;\n}\n.opal-textarea::after { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: var(--opal-input-rim, var(--_opal-glass-rim)); opacity: var(--opal-control-rim-opacity, .7); }\n.opal-textarea:focus-within { background: var(--opal-input-focus-background, rgb(255 255 255 / 90%)); }\n/* Inset the native resize grip from the outer rounded glass corner. */\n.opal-textarea-field { display: block; box-sizing: border-box; width: 100%; min-width: 0; min-height: 88px; resize: vertical; padding: 8px 10px; border: 0; border-radius: 14px; outline: none; background: transparent; color: inherit; font: inherit; line-height: 1.5; }\n.opal-textarea-field::placeholder { color: inherit; opacity: .5; }\n.opal-textarea:has([aria-invalid=\"true\"]) { outline: 2px solid #e5484d; outline-offset: 2px; }\n.opal-textarea[data-disabled] { opacity: .4; }\n.opal-textarea-field:disabled { cursor: not-allowed; resize: none; }\n@media (prefers-reduced-motion: reduce) { .opal-textarea { transition: none; } }\n@media (prefers-reduced-transparency: reduce) { .opal-textarea { background: var(--opal-input-solid, #fff); backdrop-filter: none; -webkit-backdrop-filter: none; } }\n@media (forced-colors: active) { .opal-textarea { border: 1px solid ButtonText; } .opal-textarea:focus-within { outline: 2px solid Highlight; } }\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
export type TextareaProps = ComponentPropsWithoutRef<'textarea'> & {
|
|
3
|
+
onValueChange?: (value: string) => void;
|
|
4
|
+
};
|
|
5
|
+
/** Native multiline input, using the same material as Input. */
|
|
6
|
+
export declare const Textarea: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, "ref"> & {
|
|
7
|
+
onValueChange?: (value: string) => void;
|
|
8
|
+
} & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { stylesheetHref } from '../../internal/styles.js';
|
|
5
|
+
import componentCss from './textarea.css.js';
|
|
6
|
+
import glassCss from '../../internal/glass/styles.js';
|
|
7
|
+
const css = `${glassCss}\n${componentCss}`;
|
|
8
|
+
const styleHref = stylesheetHref('textarea', css);
|
|
9
|
+
/** Native multiline input, using the same material as Input. */
|
|
10
|
+
export const Textarea = forwardRef(function Textarea({ className, style, onChange, onValueChange, rows = 5, disabled, ...props }, ref) {
|
|
11
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), _jsx("span", { className: ['opal-textarea', className].filter(Boolean).join(' '), style: style, "data-disabled": disabled || undefined, children: _jsx("textarea", { ...props, ref: ref, rows: rows, disabled: disabled, className: "opal-textarea-field", onChange: event => {
|
|
12
|
+
onChange?.(event);
|
|
13
|
+
if (!event.defaultPrevented)
|
|
14
|
+
onValueChange?.(event.currentTarget.value);
|
|
15
|
+
} }) })] });
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=textarea.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textarea.js","sourceRoot":"","sources":["../../../src/components/textarea/textarea.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,UAAU,EAAiC,MAAM,OAAO,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,YAAY,MAAM,uBAAuB,CAAA;AAChD,OAAO,QAAQ,MAAM,gCAAgC,CAAA;AACrD,MAAM,GAAG,GAAG,GAAG,QAAQ,KAAK,YAAY,EAAE,CAAA;AAC1C,MAAM,SAAS,GAAG,cAAc,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;AAGjD,gEAAgE;AAChE,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAqC,SAAS,QAAQ,CACtF,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG;IAEhF,OAAO,8BACL,gBAAO,IAAI,EAAE,SAAS,EAAE,UAAU,EAAC,QAAQ,YAAE,GAAG,GAAS,EACzD,eAAM,SAAS,EAAE,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,mBAAiB,QAAQ,IAAI,SAAS,YACzH,sBAAc,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAC,qBAAqB,EAAC,QAAQ,EAAE,KAAK,CAAC,EAAE;wBAC/G,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAA;wBACjB,IAAI,CAAC,KAAK,CAAC,gBAAgB;4BAAE,aAAa,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;oBACzE,CAAC,GAAI,GACA,IACN,CAAA;AACL,CAAC,CAAC,CAAA","sourcesContent":["'use client'\n\nimport { forwardRef, type ComponentPropsWithoutRef } from 'react'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport componentCss from './textarea.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\nconst styleHref = stylesheetHref('textarea', css)\nexport type TextareaProps = ComponentPropsWithoutRef<'textarea'> & { onValueChange?: (value: string) => void }\n\n/** Native multiline input, using the same material as Input. */\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n { className, style, onChange, onValueChange, rows = 5, disabled, ...props }, ref,\n) {\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n <span className={['opal-textarea', className].filter(Boolean).join(' ')} style={style} data-disabled={disabled || undefined}>\n <textarea {...props} ref={ref} rows={rows} disabled={disabled} className=\"opal-textarea-field\" onChange={event => {\n onChange?.(event)\n if (!event.defaultPrevented) onValueChange?.(event.currentTarget.value)\n }} />\n </span>\n </>\n})\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Button, type ButtonProps } from './components/button/button.js';
|
|
2
|
+
export { Switch, type SwitchProps } from './components/switch/switch.js';
|
|
3
|
+
export { SegmentedControl, type SegmentedControlProps, type SegmentedControlItem } from './components/segmented-control/segmented-control.js';
|
|
4
|
+
export { Menu, type MenuProps, type MenuItem } from './components/menu/menu.js';
|
|
5
|
+
export { Input, type InputProps } from './components/input/input.js';
|
|
6
|
+
export { Slider, type SliderProps } from './components/slider/slider.js';
|
|
7
|
+
export { Textarea, type TextareaProps } from './components/textarea/textarea.js';
|
|
8
|
+
export { Select, type SelectProps, type SelectItem } from './components/select/select.js';
|
|
9
|
+
export { Glass, type GlassProps } from './components/glass/glass.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Button } from './components/button/button.js';
|
|
2
|
+
export { Switch } from './components/switch/switch.js';
|
|
3
|
+
export { SegmentedControl } from './components/segmented-control/segmented-control.js';
|
|
4
|
+
export { Menu } from './components/menu/menu.js';
|
|
5
|
+
export { Input } from './components/input/input.js';
|
|
6
|
+
export { Slider } from './components/slider/slider.js';
|
|
7
|
+
export { Textarea } from './components/textarea/textarea.js';
|
|
8
|
+
export { Select } from './components/select/select.js';
|
|
9
|
+
export { Glass } from './components/glass/glass.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,MAAM,EAAoB,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAyD,MAAM,qDAAqD,CAAA;AAC7I,OAAO,EAAE,IAAI,EAAiC,MAAM,2BAA2B,CAAA;AAC/E,OAAO,EAAE,KAAK,EAAmB,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,MAAM,EAAoB,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,QAAQ,EAAsB,MAAM,mCAAmC,CAAA;AAChF,OAAO,EAAE,MAAM,EAAqC,MAAM,+BAA+B,CAAA;AACzF,OAAO,EAAE,KAAK,EAAmB,MAAM,6BAA6B,CAAA","sourcesContent":["export { Button, type ButtonProps } from './components/button/button.js'\nexport { Switch, type SwitchProps } from './components/switch/switch.js'\nexport { SegmentedControl, type SegmentedControlProps, type SegmentedControlItem } from './components/segmented-control/segmented-control.js'\nexport { Menu, type MenuProps, type MenuItem } from './components/menu/menu.js'\nexport { Input, type InputProps } from './components/input/input.js'\nexport { Slider, type SliderProps } from './components/slider/slider.js'\nexport { Textarea, type TextareaProps } from './components/textarea/textarea.js'\nexport { Select, type SelectProps, type SelectItem } from './components/select/select.js'\nexport { Glass, type GlassProps } from './components/glass/glass.js'\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CSSProperties, type RefObject } from 'react';
|
|
2
|
+
import { type GlassSettings } from './material.js';
|
|
3
|
+
type Box = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
radius: number;
|
|
9
|
+
map: string;
|
|
10
|
+
mask: string;
|
|
11
|
+
};
|
|
12
|
+
type Frame = Box & {
|
|
13
|
+
amount: number;
|
|
14
|
+
sourceWidth: number;
|
|
15
|
+
sourceHeight: number;
|
|
16
|
+
light: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** A single filter graph serves live backgrounds and content owned by a control.
|
|
19
|
+
* Content is composited back outside the lens, so neighbors stay untouched. */
|
|
20
|
+
export declare function LensFilter({ id, frame: f, material: m, ink, contract }: {
|
|
21
|
+
id: string;
|
|
22
|
+
frame: Frame;
|
|
23
|
+
material: GlassSettings;
|
|
24
|
+
ink?: boolean;
|
|
25
|
+
contract?: boolean;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
/** Lifecycle, measurements, preferences and progressive enhancement live here.
|
|
28
|
+
* Owned content uses ordinary SVG filters; arbitrary backdrops use Chromium's
|
|
29
|
+
* reference-filter support. No DOM or canvas work happens during SSR. */
|
|
30
|
+
export declare function useGlassLens(surface: RefObject<HTMLElement | null>, material: GlassSettings, options?: {
|
|
31
|
+
source?: RefObject<HTMLElement | null>;
|
|
32
|
+
active?: boolean;
|
|
33
|
+
ink?: boolean;
|
|
34
|
+
contract?: boolean;
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
}): {
|
|
37
|
+
filter: import("react").JSX.Element | null;
|
|
38
|
+
sourceStyle: CSSProperties | undefined;
|
|
39
|
+
surfaceStyle: CSSProperties;
|
|
40
|
+
lensing: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** Observe native presses without replacing the control's input handlers. */
|
|
43
|
+
export declare function useGlassHeld(root: RefObject<HTMLElement | null>): boolean;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { glassLensMap } from './map.js';
|
|
5
|
+
import { resolveMaterial } from './material.js';
|
|
6
|
+
import { supportsOwnedGlassLens } from './support.js';
|
|
7
|
+
/** A single filter graph serves live backgrounds and content owned by a control.
|
|
8
|
+
* Content is composited back outside the lens, so neighbors stay untouched. */
|
|
9
|
+
export function LensFilter({ id, frame: f, material: m, ink = false, contract = false }) {
|
|
10
|
+
// Reversing the sampling direction compresses owned content inside the lens.
|
|
11
|
+
const strength = Math.min(f.width, f.height) * m.bend * f.amount * (contract ? -1 : 1);
|
|
12
|
+
const invert = ink && f.light;
|
|
13
|
+
return _jsx("svg", { width: "0", height: "0", style: { position: 'absolute', pointerEvents: 'none' }, "aria-hidden": "true", focusable: "false", children: _jsx("defs", { children: _jsxs("filter", { id: id, filterUnits: "userSpaceOnUse", primitiveUnits: "userSpaceOnUse", x: -f.sourceWidth * .5, y: -f.sourceHeight * .5, width: f.sourceWidth * 2, height: f.sourceHeight * 2, colorInterpolationFilters: "sRGB", children: [_jsx("feColorMatrix", { in: "SourceGraphic", type: "matrix", values: invert ? '-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0' : '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0', result: "ink" }), _jsx("feGaussianBlur", { in: "ink", stdDeviation: m.frost * f.amount, result: "soft" }), _jsx("feImage", { href: f.map, x: f.x, y: f.y, width: f.width, height: f.height, preserveAspectRatio: "none", result: "map" }), _jsx("feImage", { href: f.mask, x: f.x, y: f.y, width: f.width, height: f.height, preserveAspectRatio: "none", result: "shape" }), _jsx("feDisplacementMap", { in: "soft", in2: "map", scale: strength * (1 - m.dispersion), xChannelSelector: "R", yChannelSelector: "G", result: "rays-r" }), _jsx("feDisplacementMap", { in: "soft", in2: "map", scale: strength, xChannelSelector: "R", yChannelSelector: "G", result: "rays-g" }), _jsx("feDisplacementMap", { in: "soft", in2: "map", scale: strength * (1 + m.dispersion), xChannelSelector: "R", yChannelSelector: "G", result: "rays-b" }), _jsx("feColorMatrix", { in: "rays-r", values: "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0", result: "r" }), _jsx("feColorMatrix", { in: "rays-g", values: "0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0", result: "g" }), _jsx("feColorMatrix", { in: "rays-b", values: "0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0", result: "b" }), _jsx("feBlend", { in: "r", in2: "g", mode: "screen", result: "rg" }), _jsx("feBlend", { in: "rg", in2: "b", mode: "screen", result: "rgb" }), _jsx("feColorMatrix", { in: "rgb", values: invert ? '-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0' : '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0', result: "colored" }), _jsx("feComposite", { in: "colored", in2: "shape", operator: "in", result: "inside" }), _jsx("feComposite", { in: "SourceGraphic", in2: "shape", operator: "out", result: "outside" }), _jsx("feComposite", { in: "inside", in2: "outside", operator: "arithmetic", k2: "1", k3: "1" })] }) }) });
|
|
14
|
+
}
|
|
15
|
+
/** Lifecycle, measurements, preferences and progressive enhancement live here.
|
|
16
|
+
* Owned content uses ordinary SVG filters; arbitrary backdrops use Chromium's
|
|
17
|
+
* reference-filter support. No DOM or canvas work happens during SSR. */
|
|
18
|
+
export function useGlassLens(surface, material, options = {}) {
|
|
19
|
+
const { source, active = true, ink = false, contract = false, enabled = true } = options;
|
|
20
|
+
const id = `opal-glass-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
|
|
21
|
+
const [frame, setFrame] = useState();
|
|
22
|
+
const amount = useRef(0);
|
|
23
|
+
const m = resolveMaterial(material);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const glass = surface.current;
|
|
26
|
+
if (!glass || !enabled) {
|
|
27
|
+
setFrame(undefined);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (source && !supportsOwnedGlassLens(navigator.userAgent)) {
|
|
31
|
+
amount.current = 0;
|
|
32
|
+
setFrame(undefined);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!source && (!/(?:Chrome|Chromium|Edg)\//.test(navigator.userAgent) || !CSS.supports('backdrop-filter', 'url("#lens")')))
|
|
36
|
+
return;
|
|
37
|
+
const transparency = matchMedia('(prefers-reduced-transparency: reduce)');
|
|
38
|
+
const motion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
39
|
+
const contrast = matchMedia('(forced-colors: active)');
|
|
40
|
+
let raf = 0, previous = 0, stopped = false;
|
|
41
|
+
const measure = (now) => {
|
|
42
|
+
if (stopped)
|
|
43
|
+
return;
|
|
44
|
+
const dt = previous ? Math.min(64, now - previous) : 16;
|
|
45
|
+
previous = now;
|
|
46
|
+
const target = active && !transparency.matches && !contrast.matches ? 1 : 0;
|
|
47
|
+
amount.current = motion.matches || !source ? target : amount.current + (target - amount.current) * (1 - Math.exp(-dt / 65));
|
|
48
|
+
if (Math.abs(amount.current - target) < .002)
|
|
49
|
+
amount.current = target;
|
|
50
|
+
if (!amount.current)
|
|
51
|
+
setFrame(undefined);
|
|
52
|
+
else {
|
|
53
|
+
const rect = glass.getBoundingClientRect();
|
|
54
|
+
const content = source?.current;
|
|
55
|
+
const parent = content?.getBoundingClientRect();
|
|
56
|
+
const width = content ? rect.width : glass.offsetWidth;
|
|
57
|
+
const height = content ? rect.height : glass.offsetHeight;
|
|
58
|
+
const scale = glass.offsetWidth ? width / glass.offsetWidth : 1;
|
|
59
|
+
const radius = Math.min(width / 2, height / 2, (parseFloat(getComputedStyle(glass).borderTopLeftRadius) || 0) * scale);
|
|
60
|
+
try {
|
|
61
|
+
const map = width && height ? glassLensMap(Math.round(width), Math.round(height), Math.round(radius)) : undefined;
|
|
62
|
+
if (map) {
|
|
63
|
+
const mask = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="100%" height="100%" rx="${radius}" fill="white"/></svg>`)}`;
|
|
64
|
+
const next = { x: parent ? rect.left - parent.left : 0, y: parent ? rect.top - parent.top : 0, width, height, radius, map, mask, amount: amount.current, sourceWidth: content?.offsetWidth ?? width, sourceHeight: content?.offsetHeight ?? height, light: glass.closest('[data-theme]')?.getAttribute('data-theme') !== 'dark' && getComputedStyle(glass).colorScheme !== 'dark' };
|
|
65
|
+
setFrame(old => old && old.x === next.x && old.y === next.y && old.width === next.width && old.height === next.height && old.amount === next.amount && old.light === next.light && old.sourceWidth === next.sourceWidth && old.sourceHeight === next.sourceHeight ? old : next);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
setFrame(undefined);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (source && ((active && !transparency.matches && !contrast.matches) || amount.current > 0))
|
|
73
|
+
raf = requestAnimationFrame(measure);
|
|
74
|
+
};
|
|
75
|
+
const refresh = () => { cancelAnimationFrame(raf); previous = 0; raf = requestAnimationFrame(measure); };
|
|
76
|
+
const resize = new ResizeObserver(refresh);
|
|
77
|
+
resize.observe(glass);
|
|
78
|
+
if (source?.current)
|
|
79
|
+
resize.observe(source.current);
|
|
80
|
+
const theme = new MutationObserver(refresh);
|
|
81
|
+
theme.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ['data-theme'] });
|
|
82
|
+
transparency.addEventListener('change', refresh);
|
|
83
|
+
motion.addEventListener('change', refresh);
|
|
84
|
+
contrast.addEventListener('change', refresh);
|
|
85
|
+
refresh();
|
|
86
|
+
return () => { stopped = true; cancelAnimationFrame(raf); resize.disconnect(); theme.disconnect(); transparency.removeEventListener('change', refresh); motion.removeEventListener('change', refresh); contrast.removeEventListener('change', refresh); };
|
|
87
|
+
}, [surface, source, active, enabled, m.bend, m.dispersion, m.frost, m.rim]);
|
|
88
|
+
const filterId = source && frame ? `${id}-${Math.round(frame.x * 10)}-${Math.round(frame.y * 10)}-${Math.round(frame.width * 10)}-${Math.round(frame.amount * 100)}` : id;
|
|
89
|
+
return {
|
|
90
|
+
filter: frame ? _jsx(LensFilter, { id: filterId, frame: frame, material: m, ink: ink, contract: contract }) : null,
|
|
91
|
+
sourceStyle: frame && source ? { filter: `url("#${filterId}")` } : undefined,
|
|
92
|
+
surfaceStyle: { '--opal-material-frost': `${m.frost}px`, '--opal-material-rim': m.rim, '--opal-material-edge': m.edge, ...(frame && !source ? { '--opal-material-filter': `url("#${id}")` } : {}) },
|
|
93
|
+
lensing: !!frame,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Observe native presses without replacing the control's input handlers. */
|
|
97
|
+
export function useGlassHeld(root) {
|
|
98
|
+
const [held, setHeld] = useState(false);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const element = root.current;
|
|
101
|
+
if (!element)
|
|
102
|
+
return;
|
|
103
|
+
const down = (event) => {
|
|
104
|
+
if (event.isPrimary && event.button === 0 && !event.target.disabled)
|
|
105
|
+
setHeld(true);
|
|
106
|
+
};
|
|
107
|
+
const up = () => setHeld(false);
|
|
108
|
+
element.addEventListener('pointerdown', down);
|
|
109
|
+
window.addEventListener('pointerup', up);
|
|
110
|
+
window.addEventListener('pointercancel', up);
|
|
111
|
+
window.addEventListener('blur', up);
|
|
112
|
+
return () => { element.removeEventListener('pointerdown', down); window.removeEventListener('pointerup', up); window.removeEventListener('pointercancel', up); window.removeEventListener('blur', up); };
|
|
113
|
+
}, [root]);
|
|
114
|
+
return held;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=lens.js.map
|