@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,186 @@
|
|
|
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
|
+
/* The complete CSS material is server-rendered; lensing is optional. */
|
|
39
|
+
.opal-button-filter { position: absolute; pointer-events: none; }
|
|
40
|
+
.opal-button {
|
|
41
|
+
position: relative;
|
|
42
|
+
z-index: 0;
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
min-height: 2.875rem;
|
|
49
|
+
padding: 0.75rem 1.125rem;
|
|
50
|
+
border: 0;
|
|
51
|
+
border-radius: var(--opal-button-radius, 999px);
|
|
52
|
+
background: transparent;
|
|
53
|
+
color: var(--opal-button-text-color, var(--opal-button-tint, var(--opal-button-color, #171e22)));
|
|
54
|
+
box-shadow: var(--opal-button-shadow, 0 2px 6px rgb(15 34 47 / 10%));
|
|
55
|
+
font: inherit;
|
|
56
|
+
font-weight: 400;
|
|
57
|
+
line-height: 1.25;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
touch-action: manipulation;
|
|
60
|
+
-webkit-tap-highlight-color: transparent;
|
|
61
|
+
transform: scale(1);
|
|
62
|
+
transition:
|
|
63
|
+
transform 600ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
64
|
+
box-shadow 450ms ease;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.opal-button[data-icon="leading"] { padding-inline-start: 0.875rem; }
|
|
68
|
+
.opal-button[data-icon="only"] {
|
|
69
|
+
width: 2.875rem;
|
|
70
|
+
height: 2.875rem;
|
|
71
|
+
padding: 0;
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
}
|
|
74
|
+
.opal-button-icon {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
flex: 0 0 auto;
|
|
79
|
+
width: 1.25em;
|
|
80
|
+
height: 1.25em;
|
|
81
|
+
line-height: 1;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
}
|
|
84
|
+
.opal-button-icon > svg,
|
|
85
|
+
.opal-button-icon > img { display: block; width: 100%; height: 100%; }
|
|
86
|
+
|
|
87
|
+
.opal-button::before,
|
|
88
|
+
.opal-button::after {
|
|
89
|
+
content: "";
|
|
90
|
+
position: absolute;
|
|
91
|
+
inset: 0;
|
|
92
|
+
border-radius: inherit;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.opal-button::before {
|
|
97
|
+
z-index: -1;
|
|
98
|
+
background: var(--opal-button-background, rgb(236 247 255 / 46%));
|
|
99
|
+
-webkit-backdrop-filter: blur(12px) saturate(115%);
|
|
100
|
+
backdrop-filter: var(--opal-material-filter, blur(var(--opal-material-frost, 12px))) saturate(115%);
|
|
101
|
+
transition: background-color 240ms ease;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.opal-button::after {
|
|
105
|
+
/* Diffuse light only along the edge; leave the center uniformly tinted. */
|
|
106
|
+
box-shadow: var(--opal-button-rim,
|
|
107
|
+
inset 0 0 0.6px 0.5px rgb(255 255 255 / 12%),
|
|
108
|
+
inset 0 0.75px 1.3px rgb(255 255 255 / 24%),
|
|
109
|
+
inset 0 -0.75px 1.3px rgb(255 255 255 / 14%));
|
|
110
|
+
opacity: 0.7;
|
|
111
|
+
transition: opacity 450ms ease;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.opal-button:active:not(:disabled)::after { opacity: 1; }
|
|
115
|
+
|
|
116
|
+
/* A crisp upper-left reflection gives glass a curved edge without adding
|
|
117
|
+
another gradient across the face. Theme-specific rims can override it. */
|
|
118
|
+
.opal-button[data-variant="glass"]::after {
|
|
119
|
+
opacity: var(--opal-control-rim-opacity, .7);
|
|
120
|
+
box-shadow: var(--opal-button-rim, var(--_opal-glass-rim));
|
|
121
|
+
}
|
|
122
|
+
.opal-button[data-variant="glass"]:not(:active) {
|
|
123
|
+
box-shadow: var(--opal-button-shadow, var(--_opal-glass-shadow));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@media (hover: hover) {
|
|
127
|
+
.opal-button:not([data-variant="filled"]):hover:not(:disabled)::before {
|
|
128
|
+
background-color: var(--opal-button-hover-background, rgb(236 247 255 / 56%));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.opal-button:active:not(:disabled) {
|
|
133
|
+
transform: scale(var(--opal-button-press-scale, 1.045));
|
|
134
|
+
transition-duration: 360ms, 450ms;
|
|
135
|
+
box-shadow: var(--opal-button-pressed-shadow, 0 4px 10px rgb(15 34 47 / 14%));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Filled is opaque in every state and does not sample the backdrop. */
|
|
139
|
+
.opal-button[data-variant="filled"] {
|
|
140
|
+
/* Glass theme colors must not override contrast selected for a solid fill. */
|
|
141
|
+
color: var(--opal-button-text-color, var(--opal-button-filled-foreground, #fff));
|
|
142
|
+
}
|
|
143
|
+
.opal-button[data-variant="filled"]::before {
|
|
144
|
+
background: var(--opal-button-tint, var(--opal-button-filled-background, #007aff));
|
|
145
|
+
box-shadow:
|
|
146
|
+
inset 0 1px 2px rgb(255 255 255 / 24%),
|
|
147
|
+
inset 0 -1px 2px rgb(255 255 255 / 10%);
|
|
148
|
+
-webkit-backdrop-filter: none;
|
|
149
|
+
backdrop-filter: none;
|
|
150
|
+
transition: filter 240ms ease;
|
|
151
|
+
}
|
|
152
|
+
@media (hover: hover) {
|
|
153
|
+
.opal-button[data-variant="filled"]:hover:not(:disabled)::before { filter: brightness(1.08); }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.opal-button:focus-visible {
|
|
157
|
+
outline: 2px solid var(--opal-focus-color, currentColor);
|
|
158
|
+
outline-offset: 4px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.opal-button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
162
|
+
|
|
163
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
164
|
+
.opal-button:not([data-variant="filled"])::before, .opal-button:not([data-variant="filled"]):hover:not(:disabled)::before {
|
|
165
|
+
background-color: var(--opal-button-fallback-background, #ededed);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
.opal-button, .opal-button::before, .opal-button::after { transition: none; }
|
|
171
|
+
.opal-button:active:not(:disabled) { transform: none; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
175
|
+
.opal-button:not([data-variant="filled"])::before, .opal-button:not([data-variant="filled"]):hover:not(:disabled)::before {
|
|
176
|
+
background: var(--opal-button-fallback-background, #ededed);
|
|
177
|
+
-webkit-backdrop-filter: none;
|
|
178
|
+
backdrop-filter: none;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@media (forced-colors: active) {
|
|
183
|
+
.opal-button[data-variant] { border: 1px solid ButtonText; background: ButtonFace; color: ButtonText; box-shadow: none; }
|
|
184
|
+
.opal-button::before, .opal-button::after { display: none; }
|
|
185
|
+
.opal-button:disabled { color: GrayText; border-color: GrayText; opacity: 1; }
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "/* The complete CSS material is server-rendered; lensing is optional. */\n.opal-button-filter { position: absolute; pointer-events: none; }\n.opal-button {\n position: relative;\n z-index: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n box-sizing: border-box;\n min-height: 2.875rem;\n padding: 0.75rem 1.125rem;\n border: 0;\n border-radius: var(--opal-button-radius, 999px);\n background: transparent;\n color: var(--opal-button-text-color, var(--opal-button-tint, var(--opal-button-color, #171e22)));\n box-shadow: var(--opal-button-shadow, 0 2px 6px rgb(15 34 47 / 10%));\n font: inherit;\n font-weight: 400;\n line-height: 1.25;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n transform: scale(1);\n transition:\n transform 600ms cubic-bezier(0.22, 0.61, 0.36, 1),\n box-shadow 450ms ease;\n}\n\n.opal-button[data-icon=\"leading\"] { padding-inline-start: 0.875rem; }\n.opal-button[data-icon=\"only\"] {\n width: 2.875rem;\n height: 2.875rem;\n padding: 0;\n flex-shrink: 0;\n}\n.opal-button-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 1.25em;\n height: 1.25em;\n line-height: 1;\n pointer-events: none;\n}\n.opal-button-icon > svg,\n.opal-button-icon > img { display: block; width: 100%; height: 100%; }\n\n.opal-button::before,\n.opal-button::after {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n}\n\n.opal-button::before {\n z-index: -1;\n background: var(--opal-button-background, rgb(236 247 255 / 46%));\n -webkit-backdrop-filter: blur(12px) saturate(115%);\n backdrop-filter: var(--opal-material-filter, blur(var(--opal-material-frost, 12px))) saturate(115%);\n transition: background-color 240ms ease;\n}\n\n.opal-button::after {\n /* Diffuse light only along the edge; leave the center uniformly tinted. */\n box-shadow: var(--opal-button-rim,\n inset 0 0 0.6px 0.5px rgb(255 255 255 / 12%),\n inset 0 0.75px 1.3px rgb(255 255 255 / 24%),\n inset 0 -0.75px 1.3px rgb(255 255 255 / 14%));\n opacity: 0.7;\n transition: opacity 450ms ease;\n}\n\n.opal-button:active:not(:disabled)::after { opacity: 1; }\n\n/* A crisp upper-left reflection gives glass a curved edge without adding\n another gradient across the face. Theme-specific rims can override it. */\n.opal-button[data-variant=\"glass\"]::after {\n opacity: var(--opal-control-rim-opacity, .7);\n box-shadow: var(--opal-button-rim, var(--_opal-glass-rim));\n}\n.opal-button[data-variant=\"glass\"]:not(:active) {\n box-shadow: var(--opal-button-shadow, var(--_opal-glass-shadow));\n}\n\n@media (hover: hover) {\n .opal-button:not([data-variant=\"filled\"]):hover:not(:disabled)::before {\n background-color: var(--opal-button-hover-background, rgb(236 247 255 / 56%));\n }\n}\n\n.opal-button:active:not(:disabled) {\n transform: scale(var(--opal-button-press-scale, 1.045));\n transition-duration: 360ms, 450ms;\n box-shadow: var(--opal-button-pressed-shadow, 0 4px 10px rgb(15 34 47 / 14%));\n}\n\n/* Filled is opaque in every state and does not sample the backdrop. */\n.opal-button[data-variant=\"filled\"] {\n /* Glass theme colors must not override contrast selected for a solid fill. */\n color: var(--opal-button-text-color, var(--opal-button-filled-foreground, #fff));\n}\n.opal-button[data-variant=\"filled\"]::before {\n background: var(--opal-button-tint, var(--opal-button-filled-background, #007aff));\n box-shadow:\n inset 0 1px 2px rgb(255 255 255 / 24%),\n inset 0 -1px 2px rgb(255 255 255 / 10%);\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n transition: filter 240ms ease;\n}\n@media (hover: hover) {\n .opal-button[data-variant=\"filled\"]:hover:not(:disabled)::before { filter: brightness(1.08); }\n}\n\n.opal-button:focus-visible {\n outline: 2px solid var(--opal-focus-color, currentColor);\n outline-offset: 4px;\n}\n\n.opal-button:disabled { opacity: 0.4; cursor: not-allowed; }\n\n@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {\n .opal-button:not([data-variant=\"filled\"])::before, .opal-button:not([data-variant=\"filled\"]):hover:not(:disabled)::before {\n background-color: var(--opal-button-fallback-background, #ededed);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .opal-button, .opal-button::before, .opal-button::after { transition: none; }\n .opal-button:active:not(:disabled) { transform: none; }\n}\n\n@media (prefers-reduced-transparency: reduce) {\n .opal-button:not([data-variant=\"filled\"])::before, .opal-button:not([data-variant=\"filled\"]):hover:not(:disabled)::before {\n background: var(--opal-button-fallback-background, #ededed);\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n }\n}\n\n@media (forced-colors: active) {\n .opal-button[data-variant] { border: 1px solid ButtonText; background: ButtonFace; color: ButtonText; box-shadow: none; }\n .opal-button::before, .opal-button::after { display: none; }\n .opal-button:disabled { color: GrayText; border-color: GrayText; opacity: 1; }\n}\n";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode, type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
export type ButtonProps = ComponentPropsWithoutRef<'button'> & {
|
|
3
|
+
/** Glass is the default; filled uses an opaque background. */
|
|
4
|
+
variant?: 'glass' | 'filled';
|
|
5
|
+
/** Leading icon. Without a label, renders a circular button; supply aria-label. */
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
/** An opaque #RGB or #RRGGBB color. Glass: text. Filled: background. */
|
|
8
|
+
tint?: `#${string}`;
|
|
9
|
+
/** Explicit #RGB or #RRGGBB text color; overrides tint and automatic text-color selection. */
|
|
10
|
+
textColor?: `#${string}`;
|
|
11
|
+
/** Glass color separation: 0 disables it, 0.6 is subtle, 4 exaggerates it. */
|
|
12
|
+
chromaticAberration?: number;
|
|
13
|
+
};
|
|
14
|
+
/** A native button with OpalUI styling. Styles are included automatically. */
|
|
15
|
+
export declare const Button: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & {
|
|
16
|
+
/** Glass is the default; filled uses an opaque background. */
|
|
17
|
+
variant?: "glass" | "filled";
|
|
18
|
+
/** Leading icon. Without a label, renders a circular button; supply aria-label. */
|
|
19
|
+
icon?: ReactNode;
|
|
20
|
+
/** An opaque #RGB or #RRGGBB color. Glass: text. Filled: background. */
|
|
21
|
+
tint?: `#${string}`;
|
|
22
|
+
/** Explicit #RGB or #RRGGBB text color; overrides tint and automatic text-color selection. */
|
|
23
|
+
textColor?: `#${string}`;
|
|
24
|
+
/** Glass color separation: 0 disables it, 0.6 is subtle, 4 exaggerates it. */
|
|
25
|
+
chromaticAberration?: number;
|
|
26
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Children, forwardRef, useImperativeHandle, useRef } 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 './button.css.js';
|
|
8
|
+
import glassCss from '../../internal/glass/styles.js';
|
|
9
|
+
const css = `${glassCss}\n${componentCss}`;
|
|
10
|
+
const styleHref = stylesheetHref('button', css);
|
|
11
|
+
function isHexColor(value) {
|
|
12
|
+
return !!value && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
|
|
13
|
+
}
|
|
14
|
+
function tintStyles(tint) {
|
|
15
|
+
if (!isHexColor(tint))
|
|
16
|
+
return {};
|
|
17
|
+
const hex = tint.length === 4 ? tint.slice(1).split('').map(char => char + char).join('') : tint.slice(1);
|
|
18
|
+
const channels = [0, 2, 4].map(offset => {
|
|
19
|
+
const value = parseInt(hex.slice(offset, offset + 2), 16) / 255;
|
|
20
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
21
|
+
});
|
|
22
|
+
const luminance = channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
23
|
+
return {
|
|
24
|
+
'--opal-button-tint': tint,
|
|
25
|
+
// Appearance-first: keep white on saturated midtone fills (including
|
|
26
|
+
// orange/red), switching to black for light fills. Not a WCAG guarantee.
|
|
27
|
+
'--opal-button-filled-foreground': luminance > 0.55 ? '#000' : '#fff',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** A native button with OpalUI styling. Styles are included automatically. */
|
|
31
|
+
export const Button = forwardRef(function Button({ className, style, icon, children, variant = 'glass', tint, textColor, chromaticAberration = 0.6, type = 'button', ...props }, ref) {
|
|
32
|
+
const hasIcon = Children.toArray(icon).length > 0;
|
|
33
|
+
const hasLabel = Children.toArray(children).some(child => typeof child !== 'string' || child.trim().length > 0);
|
|
34
|
+
const element = useRef(null);
|
|
35
|
+
useImperativeHandle(ref, () => element.current, []);
|
|
36
|
+
const glass = useGlassLens(element, { ...materials.surface, dispersion: Math.max(0, Math.min(4, Number.isFinite(chromaticAberration) ? chromaticAberration : .6)) / 5 }, { enabled: variant === 'glass' });
|
|
37
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), glass.filter, _jsxs("button", { ...props, ref: element, "data-variant": variant, "data-icon": hasIcon ? (hasLabel ? "leading" : "only") : undefined, style: { ...tintStyles(tint), ...(isHexColor(textColor) ? { '--opal-button-text-color': textColor } : {}), ...glass.surfaceStyle, ...style }, type: type, className: ['opal-button', className].filter(Boolean).join(' '), children: [hasIcon && _jsx("span", { className: "opal-button-icon", "aria-hidden": "true", children: icon }), children] })] }));
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../../../src/components/button/button.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { Children, forwardRef, useImperativeHandle, useRef, type CSSProperties, type ReactNode, type ComponentPropsWithoutRef } 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 './button.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\n\nconst styleHref = stylesheetHref('button', css)\n\nexport type ButtonProps = ComponentPropsWithoutRef<'button'> & {\n /** Glass is the default; filled uses an opaque background. */\n variant?: 'glass' | 'filled'\n /** Leading icon. Without a label, renders a circular button; supply aria-label. */\n icon?: ReactNode\n /** An opaque #RGB or #RRGGBB color. Glass: text. Filled: background. */\n tint?: `#${string}`\n /** Explicit #RGB or #RRGGBB text color; overrides tint and automatic text-color selection. */\n textColor?: `#${string}`\n /** Glass color separation: 0 disables it, 0.6 is subtle, 4 exaggerates it. */\n chromaticAberration?: number\n}\n\nfunction isHexColor(value: string | undefined): value is string {\n return !!value && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)\n}\n\nfunction tintStyles(tint: ButtonProps['tint']): CSSProperties {\n if (!isHexColor(tint)) return {}\n const hex = tint.length === 4 ? tint.slice(1).split('').map(char => char + char).join('') : tint.slice(1)\n const channels = [0, 2, 4].map(offset => {\n const value = parseInt(hex.slice(offset, offset + 2), 16) / 255\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4\n })\n const luminance = channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722\n return {\n '--opal-button-tint': tint,\n // Appearance-first: keep white on saturated midtone fills (including\n // orange/red), switching to black for light fills. Not a WCAG guarantee.\n '--opal-button-filled-foreground': luminance > 0.55 ? '#000' : '#fff',\n } as CSSProperties\n}\n\n/** A native button with OpalUI styling. Styles are included automatically. */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n function Button({ className, style, icon, children, variant = 'glass', tint, textColor, chromaticAberration = 0.6, type = 'button', ...props }, ref) {\n const hasIcon = Children.toArray(icon).length > 0\n const hasLabel = Children.toArray(children).some(child => typeof child !== 'string' || child.trim().length > 0)\n const element = useRef<HTMLButtonElement>(null)\n useImperativeHandle(ref, () => element.current!, [])\n const glass = useGlassLens(element, { ...materials.surface, dispersion: Math.max(0, Math.min(4, Number.isFinite(chromaticAberration) ? chromaticAberration : .6)) / 5 }, { enabled: variant === 'glass' })\n return (\n <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n {glass.filter}\n <button\n {...props}\n ref={element}\n data-variant={variant}\n data-icon={hasIcon ? (hasLabel ? \"leading\" : \"only\") : undefined}\n style={{ ...tintStyles(tint), ...(isHexColor(textColor) ? { '--opal-button-text-color': textColor } : {}), ...glass.surfaceStyle, ...style } as CSSProperties}\n type={type}\n className={['opal-button', className].filter(Boolean).join(' ')}\n >\n {hasIcon && <span className=\"opal-button-icon\" aria-hidden=\"true\">{icon}</span>}\n {children}\n </button>\n </>\n )\n },\n)\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
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-glass { position:relative; isolation:isolate; border-radius:28px; padding:28px; color:var(--opal-glass-color,inherit); }
|
|
39
|
+
.opal-glass::before { content:''; position:absolute; inset:0; z-index:-1; border-radius:inherit; pointer-events:none; background:var(--opal-glass-background,rgb(255 255 255 / 22%)); -webkit-backdrop-filter:blur(var(--opal-material-frost,2px)); backdrop-filter:var(--opal-material-filter,blur(var(--opal-material-frost,2px))); box-shadow:var(--_opal-glass-edge),var(--_opal-glass-shadow); }
|
|
40
|
+
@media(prefers-reduced-transparency:reduce) { .opal-glass::before { background:var(--opal-glass-solid,#f5f5f5); backdrop-filter:none; -webkit-backdrop-filter:none; } .opal-glass { color:var(--opal-glass-solid-color,#202020); } }
|
|
41
|
+
@media(forced-colors:active) { .opal-glass { border:1px solid CanvasText; color:CanvasText; } .opal-glass::before { background:Canvas; backdrop-filter:none; box-shadow:none; } }
|
|
42
|
+
|
|
43
|
+
.opal-glass::after { content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none; box-shadow:var(--opal-glass-rim,var(--opal-glass-highlight,inset 0 0 0 .5px rgb(255 255 255 / 30%),inset .65px 1px .65px rgb(255 255 255 / 90%),inset -.4px -.65px .8px rgb(255 255 255 / 45%))); opacity:var(--opal-material-rim,.3); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default ".opal-glass { position:relative; isolation:isolate; border-radius:28px; padding:28px; color:var(--opal-glass-color,inherit); }\n.opal-glass::before { content:''; position:absolute; inset:0; z-index:-1; border-radius:inherit; pointer-events:none; background:var(--opal-glass-background,rgb(255 255 255 / 22%)); -webkit-backdrop-filter:blur(var(--opal-material-frost,2px)); backdrop-filter:var(--opal-material-filter,blur(var(--opal-material-frost,2px))); box-shadow:var(--_opal-glass-edge),var(--_opal-glass-shadow); }\n@media(prefers-reduced-transparency:reduce) { .opal-glass::before { background:var(--opal-glass-solid,#f5f5f5); backdrop-filter:none; -webkit-backdrop-filter:none; } .opal-glass { color:var(--opal-glass-solid-color,#202020); } }\n@media(forced-colors:active) { .opal-glass { border:1px solid CanvasText; color:CanvasText; } .opal-glass::before { background:Canvas; backdrop-filter:none; box-shadow:none; } }\n\n.opal-glass::after { content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none; box-shadow:var(--opal-glass-rim,var(--opal-glass-highlight,inset 0 0 0 .5px rgb(255 255 255 / 30%),inset .65px 1px .65px rgb(255 255 255 / 90%),inset -.4px -.65px .8px rgb(255 255 255 / 45%))); opacity:var(--opal-material-rim,.3); }\n";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { type GlassSettings } from '../../internal/glass/material.js';
|
|
3
|
+
export type GlassProps = ComponentPropsWithoutRef<'div'> & Partial<GlassSettings>;
|
|
4
|
+
/** A glass surface. Children remain sharp; only the backdrop is refracted. */
|
|
5
|
+
export declare const Glass: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & Partial<GlassSettings> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
4
|
+
import { useGlassLens } from '../../internal/glass/lens.js';
|
|
5
|
+
import { materials, resolveMaterial } from '../../internal/glass/material.js';
|
|
6
|
+
import { stylesheetHref } from '../../internal/styles.js';
|
|
7
|
+
import sharedCss from '../../internal/glass/styles.js';
|
|
8
|
+
import componentCss from './glass.css.js';
|
|
9
|
+
const css = `${sharedCss}\n${componentCss}`;
|
|
10
|
+
const href = stylesheetHref('glass', css);
|
|
11
|
+
/** A glass surface. Children remain sharp; only the backdrop is refracted. */
|
|
12
|
+
export const Glass = forwardRef(function Glass({ bend, dispersion, frost, rim, edge, className, style, children, ...props }, ref) {
|
|
13
|
+
const element = useRef(null);
|
|
14
|
+
useImperativeHandle(ref, () => element.current, []);
|
|
15
|
+
const material = resolveMaterial(materials.surface, { bend, dispersion, frost, rim, edge });
|
|
16
|
+
const glass = useGlassLens(element, material);
|
|
17
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { href: href, precedence: "opalui", children: css }), glass.filter, _jsx("div", { ...props, ref: element, className: ['opal-glass', className].filter(Boolean).join(' '), style: { ...glass.surfaceStyle, ...style }, children: children })] });
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=glass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass.js","sourceRoot":"","sources":["../../../src/components/glass/glass.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAiC,MAAM,OAAO,CAAA;AAC9F,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAsB,MAAM,kCAAkC,CAAA;AACjG,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,SAAS,MAAM,gCAAgC,CAAA;AACtD,OAAO,YAAY,MAAM,oBAAoB,CAAA;AAC7C,MAAM,GAAG,GAAG,GAAG,SAAS,KAAK,YAAY,EAAE,CAAA;AAC3C,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAEzC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAA6B,SAAS,KAAK,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG;IAC1J,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAC5C,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAQ,EAAE,EAAE,CAAC,CAAA;IACpD,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3F,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAC7C,OAAO,8BACL,gBAAO,IAAI,EAAE,IAAI,EAAE,UAAU,EAAC,QAAQ,YAAE,GAAG,GAAS,EACnD,KAAK,CAAC,MAAM,EACb,iBAAS,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,YAAY,EAAE,GAAG,KAAK,EAAE,YAAG,QAAQ,GAAO,IACzJ,CAAA;AACL,CAAC,CAAC,CAAA","sourcesContent":["'use client'\n\nimport { forwardRef, useImperativeHandle, useRef, type ComponentPropsWithoutRef } from 'react'\nimport { useGlassLens } from '../../internal/glass/lens.js'\nimport { materials, resolveMaterial, type GlassSettings } from '../../internal/glass/material.js'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport sharedCss from '../../internal/glass/styles.js'\nimport componentCss from './glass.css?inline'\nconst css = `${sharedCss}\\n${componentCss}`\nconst href = stylesheetHref('glass', css)\nexport type GlassProps = ComponentPropsWithoutRef<'div'> & Partial<GlassSettings>\n/** A glass surface. Children remain sharp; only the backdrop is refracted. */\nexport const Glass = forwardRef<HTMLDivElement, GlassProps>(function Glass({ bend, dispersion, frost, rim, edge, className, style, children, ...props }, ref) {\n const element = useRef<HTMLDivElement>(null)\n useImperativeHandle(ref, () => element.current!, [])\n const material = resolveMaterial(materials.surface, { bend, dispersion, frost, rim, edge })\n const glass = useGlassLens(element, material)\n return <>\n <style href={href} precedence=\"opalui\">{css}</style>\n {glass.filter}\n <div {...props} ref={element} className={['opal-glass', className].filter(Boolean).join(' ')} style={{ ...glass.surfaceStyle, ...style }}>{children}</div>\n </>\n})\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
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-input {
|
|
39
|
+
position: relative;
|
|
40
|
+
display: inline-flex; align-items: center; gap: 10px; box-sizing: border-box;
|
|
41
|
+
width: 360px; max-width: 100%; min-height: 44px; padding: 0 15px; border-radius: 999px;
|
|
42
|
+
background: var(--opal-input-background, rgb(255 255 255 / 65%)); color: var(--opal-input-color, #171e22);
|
|
43
|
+
box-shadow: var(--opal-input-shadow, var(--_opal-glass-shadow));
|
|
44
|
+
backdrop-filter: blur(var(--opal-material-frost,12px)); -webkit-backdrop-filter: blur(var(--opal-material-frost,12px));
|
|
45
|
+
transition: background-color 180ms ease, box-shadow 180ms ease;
|
|
46
|
+
}
|
|
47
|
+
.opal-input::after {
|
|
48
|
+
content: ''; position: absolute; inset: 0; border-radius: inherit;
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
box-shadow: var(--opal-input-rim, var(--_opal-glass-rim));
|
|
51
|
+
opacity: var(--opal-control-rim-opacity, .7);
|
|
52
|
+
}
|
|
53
|
+
.opal-input:focus-within {
|
|
54
|
+
background: var(--opal-input-focus-background, rgb(255 255 255 / 90%));
|
|
55
|
+
}
|
|
56
|
+
.opal-input:has([aria-invalid="true"]) { outline: 2px solid #e5484d; outline-offset: 2px; }
|
|
57
|
+
.opal-input-field { width: 100%; min-width: 0; padding: 12px 0; border: 0; outline: 0; background: transparent; color: inherit; font: inherit; line-height: 1.25; }
|
|
58
|
+
.opal-input-field::placeholder { color: inherit; opacity: .5; }
|
|
59
|
+
.opal-input-field::-webkit-search-cancel-button, .opal-input-field::-webkit-search-decoration { -webkit-appearance: none; }
|
|
60
|
+
.opal-input-icon { display: inline-flex; width: 20px; height: 20px; flex: 0 0 20px; opacity: .55; pointer-events: none; }
|
|
61
|
+
.opal-input-icon > svg { width: 100%; height: 100%; }
|
|
62
|
+
.opal-input-clear { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 28px; width: 28px; height: 28px; margin-inline-end: -6px; padding: 4px; border: 0; border-radius: 50%; background: transparent; color: inherit; opacity: .55; cursor: pointer; }
|
|
63
|
+
.opal-input-clear svg { width: 20px; height: 20px; }
|
|
64
|
+
.opal-input-clear:hover { opacity: 1; }
|
|
65
|
+
.opal-input-clear:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 1px; }
|
|
66
|
+
.opal-input[data-disabled] { opacity: .4; cursor: not-allowed; }
|
|
67
|
+
.opal-input-field:disabled { cursor: not-allowed; }
|
|
68
|
+
@media (prefers-reduced-motion: reduce) { .opal-input { transition: none; } }
|
|
69
|
+
@media (prefers-reduced-transparency: reduce) { .opal-input { background: var(--opal-input-solid, #fff); backdrop-filter: none; -webkit-backdrop-filter: none; } }
|
|
70
|
+
@media (forced-colors: active) { .opal-input { border: 1px solid ButtonText; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default ".opal-input {\n position: relative;\n display: inline-flex; align-items: center; gap: 10px; box-sizing: border-box;\n width: 360px; max-width: 100%; min-height: 44px; padding: 0 15px; border-radius: 999px;\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, box-shadow 180ms ease;\n}\n.opal-input::after {\n content: ''; position: absolute; inset: 0; border-radius: inherit;\n pointer-events: none;\n box-shadow: var(--opal-input-rim, var(--_opal-glass-rim));\n opacity: var(--opal-control-rim-opacity, .7);\n}\n.opal-input:focus-within {\n background: var(--opal-input-focus-background, rgb(255 255 255 / 90%));\n}\n.opal-input:has([aria-invalid=\"true\"]) { outline: 2px solid #e5484d; outline-offset: 2px; }\n.opal-input-field { width: 100%; min-width: 0; padding: 12px 0; border: 0; outline: 0; background: transparent; color: inherit; font: inherit; line-height: 1.25; }\n.opal-input-field::placeholder { color: inherit; opacity: .5; }\n.opal-input-field::-webkit-search-cancel-button, .opal-input-field::-webkit-search-decoration { -webkit-appearance: none; }\n.opal-input-icon { display: inline-flex; width: 20px; height: 20px; flex: 0 0 20px; opacity: .55; pointer-events: none; }\n.opal-input-icon > svg { width: 100%; height: 100%; }\n.opal-input-clear { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 28px; width: 28px; height: 28px; margin-inline-end: -6px; padding: 4px; border: 0; border-radius: 50%; background: transparent; color: inherit; opacity: .55; cursor: pointer; }\n.opal-input-clear svg { width: 20px; height: 20px; }\n.opal-input-clear:hover { opacity: 1; }\n.opal-input-clear:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 1px; }\n.opal-input[data-disabled] { opacity: .4; cursor: not-allowed; }\n.opal-input-field:disabled { cursor: not-allowed; }\n@media (prefers-reduced-motion: reduce) { .opal-input { transition: none; } }\n@media (prefers-reduced-transparency: reduce) { .opal-input { background: var(--opal-input-solid, #fff); backdrop-filter: none; -webkit-backdrop-filter: none; } }\n@media (forced-colors: active) { .opal-input { border: 1px solid ButtonText; } }\n";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
export type InputProps = Omit<ComponentPropsWithoutRef<'input'>, 'children' | 'type'> & {
|
|
3
|
+
type?: 'text' | 'search' | 'email' | 'url' | 'tel' | 'password' | 'number';
|
|
4
|
+
icon?: ReactNode;
|
|
5
|
+
/** Defaults to true for search inputs. */
|
|
6
|
+
clearable?: boolean;
|
|
7
|
+
clearLabel?: string;
|
|
8
|
+
onValueChange?: (value: string) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const Input: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "type" | "children"> & {
|
|
11
|
+
type?: "text" | "search" | "email" | "url" | "tel" | "password" | "number";
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
/** Defaults to true for search inputs. */
|
|
14
|
+
clearable?: boolean;
|
|
15
|
+
clearLabel?: string;
|
|
16
|
+
onValueChange?: (value: string) => void;
|
|
17
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
4
|
+
import { stylesheetHref } from '../../internal/styles.js';
|
|
5
|
+
import componentCss from './input.css.js';
|
|
6
|
+
import glassCss from '../../internal/glass/styles.js';
|
|
7
|
+
const css = `${glassCss}\n${componentCss}`;
|
|
8
|
+
const styleHref = stylesheetHref('input', css);
|
|
9
|
+
export const Input = forwardRef(function Input({ icon, className, style, type = 'text', clearable = type === 'search', clearLabel = 'Clear input', onChange, onValueChange, value, defaultValue, disabled, readOnly, ...props }, ref) {
|
|
10
|
+
const input = useRef(null);
|
|
11
|
+
useImperativeHandle(ref, () => input.current, []);
|
|
12
|
+
const [text, setText] = useState(String(value ?? defaultValue ?? ''));
|
|
13
|
+
const current = value === undefined ? text : String(value);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const form = input.current?.form;
|
|
16
|
+
const reset = () => queueMicrotask(() => { if (input.current)
|
|
17
|
+
setText(input.current.value); });
|
|
18
|
+
form?.addEventListener('reset', reset);
|
|
19
|
+
return () => form?.removeEventListener('reset', reset);
|
|
20
|
+
}, []);
|
|
21
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), _jsxs("span", { className: ['opal-input', className].filter(Boolean).join(' '), style: style, "data-disabled": disabled || undefined, children: [icon && _jsx("span", { className: "opal-input-icon", "aria-hidden": "true", children: icon }), _jsx("input", { ...props, ref: input, type: type, value: value, defaultValue: defaultValue, disabled: disabled, readOnly: readOnly, className: "opal-input-field", onChange: event => {
|
|
22
|
+
setText(event.currentTarget.value);
|
|
23
|
+
onChange?.(event);
|
|
24
|
+
if (!event.defaultPrevented)
|
|
25
|
+
onValueChange?.(event.currentTarget.value);
|
|
26
|
+
} }), clearable && current.length > 0 && !disabled && !readOnly && _jsx("button", { className: "opal-input-clear", type: "button", "aria-label": clearLabel, onClick: () => {
|
|
27
|
+
const field = input.current;
|
|
28
|
+
if (!field)
|
|
29
|
+
return;
|
|
30
|
+
// Use the native setter so React receives the same change as typing.
|
|
31
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(field, '');
|
|
32
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
33
|
+
field.focus({ preventScroll: true });
|
|
34
|
+
}, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "m7 7 10 10M17 7 7 17" }) }) })] })] });
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../../src/components/input/input.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport componentCss from './input.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\nconst styleHref = stylesheetHref('input', css)\nexport type InputProps = Omit<ComponentPropsWithoutRef<'input'>, 'children' | 'type'> & {\n type?: 'text' | 'search' | 'email' | 'url' | 'tel' | 'password' | 'number'\n icon?: ReactNode\n /** Defaults to true for search inputs. */\n clearable?: boolean\n clearLabel?: string\n onValueChange?: (value: string) => void\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n { icon, className, style, type = 'text', clearable = type === 'search', clearLabel = 'Clear input', onChange, onValueChange, value, defaultValue, disabled, readOnly, ...props }, ref,\n) {\n const input = useRef<HTMLInputElement>(null)\n useImperativeHandle(ref, () => input.current!, [])\n const [text, setText] = useState(String(value ?? defaultValue ?? ''))\n const current = value === undefined ? text : String(value)\n useEffect(() => {\n const form = input.current?.form\n const reset = () => queueMicrotask(() => { if (input.current) setText(input.current.value) })\n form?.addEventListener('reset', reset)\n return () => form?.removeEventListener('reset', reset)\n }, [])\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n <span className={['opal-input', className].filter(Boolean).join(' ')} style={style} data-disabled={disabled || undefined}>\n {icon && <span className=\"opal-input-icon\" aria-hidden=\"true\">{icon}</span>}\n <input {...props} ref={input} type={type} value={value} defaultValue={defaultValue} disabled={disabled} readOnly={readOnly}\n className=\"opal-input-field\" onChange={event => {\n setText(event.currentTarget.value)\n onChange?.(event)\n if (!event.defaultPrevented) onValueChange?.(event.currentTarget.value)\n }} />\n {clearable && current.length > 0 && !disabled && !readOnly && <button className=\"opal-input-clear\" type=\"button\" aria-label={clearLabel}\n onClick={() => {\n const field = input.current\n if (!field) return\n // Use the native setter so React receives the same change as typing.\n Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(field, '')\n field.dispatchEvent(new Event('input', { bubbles: true }))\n field.focus({ preventScroll: true })\n }}><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\"><path d=\"m7 7 10 10M17 7 7 17\" /></svg></button>}\n </span>\n </>\n})\n"]}
|