@protohiro/state-layers 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.cjs +474 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +444 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 protohiro.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @protohiro/state-layers
|
|
2
|
+
|
|
3
|
+
React hooks for visual state layers on existing elements.
|
|
4
|
+
|
|
5
|
+
This package provides CSS-first hooks like `useFocusRingLayer`, `useInvalidStateLayer`, and `useLoadingSheenLayer` without wrapper components or layout measurement.
|
|
6
|
+
|
|
7
|
+
For workspace docs and demo usage, see the root [`README.md`](../../README.md).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
mergeRefs: () => mergeRefs,
|
|
24
|
+
useFocusRingLayer: () => useFocusRingLayer,
|
|
25
|
+
useInvalidStateLayer: () => useInvalidStateLayer,
|
|
26
|
+
useLoadingSheenLayer: () => useLoadingSheenLayer
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/runtime.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
|
|
33
|
+
// src/styles.ts
|
|
34
|
+
var STYLE_ELEMENT_ID = "protohiro-state-layers-styles";
|
|
35
|
+
var STATE_LAYER_CSS = `
|
|
36
|
+
.psl-anchor {
|
|
37
|
+
position: relative;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.psl-focus-ring,
|
|
41
|
+
.psl-invalid,
|
|
42
|
+
.psl-loading {
|
|
43
|
+
isolation: isolate;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.psl-focus-ring {
|
|
47
|
+
--psl-focus-ring-color: rgba(59, 130, 246, 0.9);
|
|
48
|
+
--psl-focus-ring-inset: 0px;
|
|
49
|
+
--psl-focus-ring-offset: 0px;
|
|
50
|
+
--psl-focus-ring-width: 2px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.psl-focus-ring::before {
|
|
54
|
+
border-radius: inherit;
|
|
55
|
+
box-shadow: 0 0 0 var(--psl-focus-ring-width) var(--psl-focus-ring-color);
|
|
56
|
+
content: "";
|
|
57
|
+
inset: calc(var(--psl-focus-ring-inset) - var(--psl-focus-ring-offset));
|
|
58
|
+
opacity: 1;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
position: absolute;
|
|
61
|
+
transition: opacity 140ms ease, transform 140ms ease;
|
|
62
|
+
transform: scale(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.psl-invalid,
|
|
66
|
+
.psl-loading {
|
|
67
|
+
--psl-invalid-color: rgba(239, 68, 68, 0.92);
|
|
68
|
+
--psl-invalid-inset: 0px;
|
|
69
|
+
--psl-invalid-shadow-width: 0px;
|
|
70
|
+
--psl-invalid-width: 0px;
|
|
71
|
+
--psl-invalid-opacity: 0;
|
|
72
|
+
--psl-invalid-visible-color: var(--psl-invalid-color);
|
|
73
|
+
--psl-loading-angle: 110deg;
|
|
74
|
+
--psl-loading-duration: 1400ms;
|
|
75
|
+
--psl-loading-intensity: 0;
|
|
76
|
+
--psl-loading-sheen-color: rgba(255, 255, 255, 0.9);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.psl-invalid::after,
|
|
80
|
+
.psl-loading::after {
|
|
81
|
+
border-radius: inherit;
|
|
82
|
+
box-shadow: 0 0 0 var(--psl-invalid-shadow-width) var(--psl-invalid-visible-color);
|
|
83
|
+
content: "";
|
|
84
|
+
inset: var(--psl-invalid-inset);
|
|
85
|
+
opacity: var(--psl-invalid-opacity);
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
position: absolute;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.psl-loading::after,
|
|
91
|
+
.psl-invalid.psl-loading::after {
|
|
92
|
+
background-image: linear-gradient(
|
|
93
|
+
var(--psl-loading-angle),
|
|
94
|
+
transparent 0%,
|
|
95
|
+
transparent 45%,
|
|
96
|
+
var(--psl-loading-sheen-color) 50%,
|
|
97
|
+
transparent 100%
|
|
98
|
+
);
|
|
99
|
+
background-position: 200% 50%;
|
|
100
|
+
background-repeat: no-repeat;
|
|
101
|
+
background-size: 220% 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.psl-loading:not(.psl-invalid)::after {
|
|
105
|
+
opacity: var(--psl-loading-intensity);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.psl-invalid.psl-loading::after {
|
|
109
|
+
opacity: var(--psl-invalid-opacity);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.psl-invalid:is(input, textarea, select) {
|
|
113
|
+
--psl-invalid-shadow-width: 0px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.psl-invalid:is(input, textarea, select) {
|
|
117
|
+
outline: var(--psl-invalid-width-input, 0px) solid var(--psl-invalid-visible-color);
|
|
118
|
+
outline-offset: var(--psl-invalid-inset);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.psl-invalid {
|
|
122
|
+
--psl-invalid-shadow-width: var(--psl-invalid-width);
|
|
123
|
+
--psl-invalid-width-input: var(--psl-invalid-width);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.psl-loading::after {
|
|
127
|
+
animation: psl-loading-sheen var(--psl-loading-duration) linear infinite;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@keyframes psl-loading-sheen {
|
|
131
|
+
from {
|
|
132
|
+
background-position: 200% 50%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
to {
|
|
136
|
+
background-position: -120% 50%;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.psl-loading::after {
|
|
142
|
+
animation-duration: calc(var(--psl-loading-duration) * 2);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@supports (color: color-mix(in srgb, black 50%, white 50%)) {
|
|
147
|
+
.psl-focus-ring {
|
|
148
|
+
--psl-focus-ring-color: color-mix(in srgb, #3b82f6 78%, white 22%);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.psl-invalid,
|
|
152
|
+
.psl-loading {
|
|
153
|
+
--psl-invalid-color: color-mix(in srgb, #ef4444 82%, white 18%);
|
|
154
|
+
--psl-invalid-visible-color: color-mix(
|
|
155
|
+
in srgb,
|
|
156
|
+
var(--psl-invalid-color) calc(var(--psl-invalid-opacity) * 100%),
|
|
157
|
+
transparent
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.psl-invalid:not(.psl-loading)::after {
|
|
162
|
+
opacity: 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.psl-loading::after,
|
|
166
|
+
.psl-invalid.psl-loading::after {
|
|
167
|
+
background-image: linear-gradient(
|
|
168
|
+
var(--psl-loading-angle),
|
|
169
|
+
transparent 0%,
|
|
170
|
+
color-mix(in srgb, var(--psl-loading-sheen-color) calc(var(--psl-loading-intensity) * 100%), transparent) 45%,
|
|
171
|
+
color-mix(in srgb, var(--psl-loading-sheen-color) calc(var(--psl-loading-intensity) * 100%), transparent) 55%,
|
|
172
|
+
transparent 100%
|
|
173
|
+
);
|
|
174
|
+
opacity: 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
// src/runtime.ts
|
|
180
|
+
var hasInjectedStyles = false;
|
|
181
|
+
var anchorConsumers = /* @__PURE__ */ new WeakMap();
|
|
182
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react.useEffect : import_react.useLayoutEffect;
|
|
183
|
+
var MISSING_ATTRIBUTE = "__psl_missing_attribute__";
|
|
184
|
+
function ensureGlobalStyles() {
|
|
185
|
+
if (hasInjectedStyles || typeof document === "undefined") {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const existing = document.getElementById(STYLE_ELEMENT_ID);
|
|
189
|
+
if (existing) {
|
|
190
|
+
hasInjectedStyles = true;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const style = document.createElement("style");
|
|
194
|
+
style.id = STYLE_ELEMENT_ID;
|
|
195
|
+
style.textContent = STATE_LAYER_CSS;
|
|
196
|
+
document.head.appendChild(style);
|
|
197
|
+
hasInjectedStyles = true;
|
|
198
|
+
}
|
|
199
|
+
function mergeRefs(...refs) {
|
|
200
|
+
return (value) => {
|
|
201
|
+
for (const ref of refs) {
|
|
202
|
+
if (!ref) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (typeof ref === "function") {
|
|
206
|
+
ref(value);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
ref.current = value;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function useMergedNodeRef(...refs) {
|
|
214
|
+
const [node, setNode] = (0, import_react.useState)(null);
|
|
215
|
+
const mergedRef = (0, import_react.useMemo)(() => mergeRefs(setNode, ...refs), refs);
|
|
216
|
+
return [node, mergedRef];
|
|
217
|
+
}
|
|
218
|
+
function addAnchorConsumer(element) {
|
|
219
|
+
const current = anchorConsumers.get(element);
|
|
220
|
+
if (!current) {
|
|
221
|
+
anchorConsumers.set(element, {
|
|
222
|
+
count: 1,
|
|
223
|
+
hadClass: element.classList.contains("psl-anchor")
|
|
224
|
+
});
|
|
225
|
+
element.classList.add("psl-anchor");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
anchorConsumers.set(element, {
|
|
229
|
+
...current,
|
|
230
|
+
count: current.count + 1
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function removeAnchorConsumer(element) {
|
|
234
|
+
const currentCount = anchorConsumers.get(element);
|
|
235
|
+
if (!currentCount) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (currentCount.count === 1) {
|
|
239
|
+
anchorConsumers.delete(element);
|
|
240
|
+
if (!currentCount.hadClass) {
|
|
241
|
+
element.classList.remove("psl-anchor");
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
anchorConsumers.set(element, {
|
|
246
|
+
...currentCount,
|
|
247
|
+
count: currentCount.count - 1
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function applyStateLayer(element, className, requireAnchor = true) {
|
|
251
|
+
const touchedVariables = /* @__PURE__ */ new Set();
|
|
252
|
+
const baselineAttributes = /* @__PURE__ */ new Map();
|
|
253
|
+
let currentAttributes = {};
|
|
254
|
+
let anchorApplied = false;
|
|
255
|
+
let isActive = false;
|
|
256
|
+
const sync = (nextVariables, nextActive, nextAttributes) => {
|
|
257
|
+
if (nextActive) {
|
|
258
|
+
element.classList.add(className);
|
|
259
|
+
if (requireAnchor && !anchorApplied && !element.style.position) {
|
|
260
|
+
addAnchorConsumer(element);
|
|
261
|
+
anchorApplied = true;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
element.classList.remove(className);
|
|
265
|
+
if (anchorApplied) {
|
|
266
|
+
removeAnchorConsumer(element);
|
|
267
|
+
anchorApplied = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const nextVariableEntries = nextActive ? nextVariables : {};
|
|
271
|
+
for (const variable of Array.from(touchedVariables)) {
|
|
272
|
+
if (!(variable in nextVariableEntries) || nextVariableEntries[variable] == null) {
|
|
273
|
+
element.style.removeProperty(variable);
|
|
274
|
+
touchedVariables.delete(variable);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const [variable, value] of Object.entries(nextVariableEntries)) {
|
|
278
|
+
if (value == null) {
|
|
279
|
+
element.style.removeProperty(variable);
|
|
280
|
+
touchedVariables.delete(variable);
|
|
281
|
+
} else {
|
|
282
|
+
element.style.setProperty(variable, value);
|
|
283
|
+
touchedVariables.add(variable);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const nextAttributeEntries = nextActive ? nextAttributes ?? {} : {};
|
|
287
|
+
for (const name of Object.keys(currentAttributes)) {
|
|
288
|
+
if (!(name in nextAttributeEntries) || nextAttributeEntries[name] == null) {
|
|
289
|
+
const baselineValue = baselineAttributes.get(name);
|
|
290
|
+
if (baselineValue == null || baselineValue === MISSING_ATTRIBUTE) {
|
|
291
|
+
element.removeAttribute(name);
|
|
292
|
+
} else {
|
|
293
|
+
element.setAttribute(name, baselineValue);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const [name, value] of Object.entries(nextAttributeEntries)) {
|
|
298
|
+
if (!baselineAttributes.has(name)) {
|
|
299
|
+
baselineAttributes.set(name, element.getAttribute(name) ?? MISSING_ATTRIBUTE);
|
|
300
|
+
}
|
|
301
|
+
if (value == null) {
|
|
302
|
+
const baselineValue = baselineAttributes.get(name);
|
|
303
|
+
if (baselineValue == null || baselineValue === MISSING_ATTRIBUTE) {
|
|
304
|
+
element.removeAttribute(name);
|
|
305
|
+
} else {
|
|
306
|
+
element.setAttribute(name, baselineValue);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
element.setAttribute(name, value);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
currentAttributes = { ...nextAttributeEntries };
|
|
313
|
+
isActive = nextActive;
|
|
314
|
+
};
|
|
315
|
+
return {
|
|
316
|
+
update(nextVariables, nextActive, nextAttributes) {
|
|
317
|
+
sync(nextVariables, nextActive, nextAttributes);
|
|
318
|
+
},
|
|
319
|
+
cleanup() {
|
|
320
|
+
if (isActive) {
|
|
321
|
+
element.classList.remove(className);
|
|
322
|
+
}
|
|
323
|
+
if (anchorApplied) {
|
|
324
|
+
removeAnchorConsumer(element);
|
|
325
|
+
}
|
|
326
|
+
for (const variable of touchedVariables) {
|
|
327
|
+
element.style.removeProperty(variable);
|
|
328
|
+
}
|
|
329
|
+
for (const name of Object.keys(currentAttributes)) {
|
|
330
|
+
const baselineValue = baselineAttributes.get(name);
|
|
331
|
+
if (baselineValue == null || baselineValue === MISSING_ATTRIBUTE) {
|
|
332
|
+
element.removeAttribute(name);
|
|
333
|
+
} else {
|
|
334
|
+
element.setAttribute(name, baselineValue);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
currentAttributes = {};
|
|
338
|
+
isActive = false;
|
|
339
|
+
anchorApplied = false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function useStateLayer({
|
|
344
|
+
active = true,
|
|
345
|
+
className,
|
|
346
|
+
variables = {},
|
|
347
|
+
attributes,
|
|
348
|
+
requireAnchor = true
|
|
349
|
+
}) {
|
|
350
|
+
const [node, ref] = useMergedNodeRef();
|
|
351
|
+
const layerRef = (0, import_react.useRef)(null);
|
|
352
|
+
useIsomorphicLayoutEffect(() => {
|
|
353
|
+
ensureGlobalStyles();
|
|
354
|
+
}, []);
|
|
355
|
+
useIsomorphicLayoutEffect(() => {
|
|
356
|
+
if (!node) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const layer = applyStateLayer(node, className, requireAnchor);
|
|
360
|
+
layerRef.current = layer;
|
|
361
|
+
layer.update(variables, active, attributes);
|
|
362
|
+
return () => {
|
|
363
|
+
layer.cleanup();
|
|
364
|
+
layerRef.current = null;
|
|
365
|
+
};
|
|
366
|
+
}, [className, node, requireAnchor]);
|
|
367
|
+
useIsomorphicLayoutEffect(() => {
|
|
368
|
+
layerRef.current?.update(variables, active, attributes);
|
|
369
|
+
}, [active, attributes, variables]);
|
|
370
|
+
return (0, import_react.useCallback)((nextNode) => {
|
|
371
|
+
ref(nextNode);
|
|
372
|
+
}, [ref]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/hooks/useFocusRingLayer.ts
|
|
376
|
+
var import_react2 = require("react");
|
|
377
|
+
var useIsomorphicLayoutEffect2 = typeof window === "undefined" ? import_react2.useEffect : import_react2.useLayoutEffect;
|
|
378
|
+
function readFocusVisible(node) {
|
|
379
|
+
if (typeof node.matches === "function") {
|
|
380
|
+
try {
|
|
381
|
+
return node.matches(":focus-visible");
|
|
382
|
+
} catch {
|
|
383
|
+
return document.activeElement === node;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return document.activeElement === node;
|
|
387
|
+
}
|
|
388
|
+
function useFocusRingLayer(options = {}) {
|
|
389
|
+
const { color, inset, offset, ref, visible, width } = options;
|
|
390
|
+
const [focusVisible, setFocusVisible] = (0, import_react2.useState)(Boolean(visible));
|
|
391
|
+
const [node, mergedRef] = useMergedNodeRef(ref);
|
|
392
|
+
useIsomorphicLayoutEffect2(() => {
|
|
393
|
+
if (visible != null) {
|
|
394
|
+
setFocusVisible(visible);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!node) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
setFocusVisible(readFocusVisible(node));
|
|
401
|
+
}, [node, visible]);
|
|
402
|
+
(0, import_react2.useEffect)(() => {
|
|
403
|
+
if (!node || visible != null) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const onFocus = () => {
|
|
407
|
+
setFocusVisible(readFocusVisible(node));
|
|
408
|
+
};
|
|
409
|
+
const onBlur = () => setFocusVisible(false);
|
|
410
|
+
node.addEventListener("focus", onFocus);
|
|
411
|
+
node.addEventListener("blur", onBlur);
|
|
412
|
+
return () => {
|
|
413
|
+
node.removeEventListener("focus", onFocus);
|
|
414
|
+
node.removeEventListener("blur", onBlur);
|
|
415
|
+
};
|
|
416
|
+
}, [node, visible]);
|
|
417
|
+
const layerRef = useStateLayer({
|
|
418
|
+
active: focusVisible,
|
|
419
|
+
className: "psl-focus-ring",
|
|
420
|
+
variables: {
|
|
421
|
+
"--psl-focus-ring-color": color,
|
|
422
|
+
"--psl-focus-ring-inset": inset,
|
|
423
|
+
"--psl-focus-ring-offset": offset,
|
|
424
|
+
"--psl-focus-ring-width": width
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return (0, import_react2.useMemo)(() => {
|
|
428
|
+
return (nextNode) => {
|
|
429
|
+
mergedRef(nextNode);
|
|
430
|
+
layerRef(nextNode);
|
|
431
|
+
};
|
|
432
|
+
}, [layerRef, mergedRef]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/hooks/useInvalidStateLayer.ts
|
|
436
|
+
var import_react3 = require("react");
|
|
437
|
+
function useInvalidStateLayer(options = {}) {
|
|
438
|
+
const { active = false, color, inset, opacity, ref, width } = options;
|
|
439
|
+
const layerRef = useStateLayer({
|
|
440
|
+
active,
|
|
441
|
+
className: "psl-invalid",
|
|
442
|
+
variables: {
|
|
443
|
+
"--psl-invalid-color": color,
|
|
444
|
+
"--psl-invalid-inset": inset,
|
|
445
|
+
"--psl-invalid-opacity": opacity == null ? void 0 : String(opacity),
|
|
446
|
+
"--psl-invalid-width": width
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
return (0, import_react3.useMemo)(() => mergeRefs(layerRef, ref), [layerRef, ref]);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/hooks/useLoadingSheenLayer.ts
|
|
453
|
+
var import_react4 = require("react");
|
|
454
|
+
function useLoadingSheenLayer(options = {}) {
|
|
455
|
+
const { active = false, angle, duration, intensity, ref, sheenColor } = options;
|
|
456
|
+
const layerRef = useStateLayer({
|
|
457
|
+
active,
|
|
458
|
+
className: "psl-loading",
|
|
459
|
+
variables: {
|
|
460
|
+
"--psl-loading-angle": angle,
|
|
461
|
+
"--psl-loading-duration": duration,
|
|
462
|
+
"--psl-loading-intensity": intensity == null ? void 0 : String(intensity),
|
|
463
|
+
"--psl-loading-sheen-color": sheenColor
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return (0, import_react4.useMemo)(() => mergeRefs(layerRef, ref), [layerRef, ref]);
|
|
467
|
+
}
|
|
468
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
469
|
+
0 && (module.exports = {
|
|
470
|
+
mergeRefs,
|
|
471
|
+
useFocusRingLayer,
|
|
472
|
+
useInvalidStateLayer,
|
|
473
|
+
useLoadingSheenLayer
|
|
474
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare function mergeRefs<T>(...refs: Array<React.Ref<T> | undefined>): React.RefCallback<T>;
|
|
2
|
+
|
|
3
|
+
type FocusRingLayerOptions<T extends HTMLElement> = {
|
|
4
|
+
color?: string;
|
|
5
|
+
inset?: string;
|
|
6
|
+
offset?: string;
|
|
7
|
+
visible?: boolean;
|
|
8
|
+
width?: string;
|
|
9
|
+
ref?: React.Ref<T>;
|
|
10
|
+
};
|
|
11
|
+
declare function useFocusRingLayer<T extends HTMLElement = HTMLElement>(options?: FocusRingLayerOptions<T>): React.RefCallback<T>;
|
|
12
|
+
|
|
13
|
+
type InvalidStateLayerOptions<T extends HTMLElement> = {
|
|
14
|
+
active?: boolean;
|
|
15
|
+
color?: string;
|
|
16
|
+
inset?: string;
|
|
17
|
+
opacity?: number;
|
|
18
|
+
ref?: React.Ref<T>;
|
|
19
|
+
width?: string;
|
|
20
|
+
};
|
|
21
|
+
declare function useInvalidStateLayer<T extends HTMLElement = HTMLElement>(options?: InvalidStateLayerOptions<T>): React.RefCallback<T>;
|
|
22
|
+
|
|
23
|
+
type LoadingSheenLayerOptions<T extends HTMLElement> = {
|
|
24
|
+
active?: boolean;
|
|
25
|
+
angle?: string;
|
|
26
|
+
duration?: string;
|
|
27
|
+
intensity?: number;
|
|
28
|
+
ref?: React.Ref<T>;
|
|
29
|
+
sheenColor?: string;
|
|
30
|
+
};
|
|
31
|
+
declare function useLoadingSheenLayer<T extends HTMLElement = HTMLElement>(options?: LoadingSheenLayerOptions<T>): React.RefCallback<T>;
|
|
32
|
+
|
|
33
|
+
export { type FocusRingLayerOptions, type InvalidStateLayerOptions, type LoadingSheenLayerOptions, mergeRefs, useFocusRingLayer, useInvalidStateLayer, useLoadingSheenLayer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare function mergeRefs<T>(...refs: Array<React.Ref<T> | undefined>): React.RefCallback<T>;
|
|
2
|
+
|
|
3
|
+
type FocusRingLayerOptions<T extends HTMLElement> = {
|
|
4
|
+
color?: string;
|
|
5
|
+
inset?: string;
|
|
6
|
+
offset?: string;
|
|
7
|
+
visible?: boolean;
|
|
8
|
+
width?: string;
|
|
9
|
+
ref?: React.Ref<T>;
|
|
10
|
+
};
|
|
11
|
+
declare function useFocusRingLayer<T extends HTMLElement = HTMLElement>(options?: FocusRingLayerOptions<T>): React.RefCallback<T>;
|
|
12
|
+
|
|
13
|
+
type InvalidStateLayerOptions<T extends HTMLElement> = {
|
|
14
|
+
active?: boolean;
|
|
15
|
+
color?: string;
|
|
16
|
+
inset?: string;
|
|
17
|
+
opacity?: number;
|
|
18
|
+
ref?: React.Ref<T>;
|
|
19
|
+
width?: string;
|
|
20
|
+
};
|
|
21
|
+
declare function useInvalidStateLayer<T extends HTMLElement = HTMLElement>(options?: InvalidStateLayerOptions<T>): React.RefCallback<T>;
|
|
22
|
+
|
|
23
|
+
type LoadingSheenLayerOptions<T extends HTMLElement> = {
|
|
24
|
+
active?: boolean;
|
|
25
|
+
angle?: string;
|
|
26
|
+
duration?: string;
|
|
27
|
+
intensity?: number;
|
|
28
|
+
ref?: React.Ref<T>;
|
|
29
|
+
sheenColor?: string;
|
|
30
|
+
};
|
|
31
|
+
declare function useLoadingSheenLayer<T extends HTMLElement = HTMLElement>(options?: LoadingSheenLayerOptions<T>): React.RefCallback<T>;
|
|
32
|
+
|
|
33
|
+
export { type FocusRingLayerOptions, type InvalidStateLayerOptions, type LoadingSheenLayerOptions, mergeRefs, useFocusRingLayer, useInvalidStateLayer, useLoadingSheenLayer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/styles.ts
|
|
5
|
+
var STYLE_ELEMENT_ID = "protohiro-state-layers-styles";
|
|
6
|
+
var STATE_LAYER_CSS = `
|
|
7
|
+
.psl-anchor {
|
|
8
|
+
position: relative;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.psl-focus-ring,
|
|
12
|
+
.psl-invalid,
|
|
13
|
+
.psl-loading {
|
|
14
|
+
isolation: isolate;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.psl-focus-ring {
|
|
18
|
+
--psl-focus-ring-color: rgba(59, 130, 246, 0.9);
|
|
19
|
+
--psl-focus-ring-inset: 0px;
|
|
20
|
+
--psl-focus-ring-offset: 0px;
|
|
21
|
+
--psl-focus-ring-width: 2px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.psl-focus-ring::before {
|
|
25
|
+
border-radius: inherit;
|
|
26
|
+
box-shadow: 0 0 0 var(--psl-focus-ring-width) var(--psl-focus-ring-color);
|
|
27
|
+
content: "";
|
|
28
|
+
inset: calc(var(--psl-focus-ring-inset) - var(--psl-focus-ring-offset));
|
|
29
|
+
opacity: 1;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
position: absolute;
|
|
32
|
+
transition: opacity 140ms ease, transform 140ms ease;
|
|
33
|
+
transform: scale(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.psl-invalid,
|
|
37
|
+
.psl-loading {
|
|
38
|
+
--psl-invalid-color: rgba(239, 68, 68, 0.92);
|
|
39
|
+
--psl-invalid-inset: 0px;
|
|
40
|
+
--psl-invalid-shadow-width: 0px;
|
|
41
|
+
--psl-invalid-width: 0px;
|
|
42
|
+
--psl-invalid-opacity: 0;
|
|
43
|
+
--psl-invalid-visible-color: var(--psl-invalid-color);
|
|
44
|
+
--psl-loading-angle: 110deg;
|
|
45
|
+
--psl-loading-duration: 1400ms;
|
|
46
|
+
--psl-loading-intensity: 0;
|
|
47
|
+
--psl-loading-sheen-color: rgba(255, 255, 255, 0.9);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.psl-invalid::after,
|
|
51
|
+
.psl-loading::after {
|
|
52
|
+
border-radius: inherit;
|
|
53
|
+
box-shadow: 0 0 0 var(--psl-invalid-shadow-width) var(--psl-invalid-visible-color);
|
|
54
|
+
content: "";
|
|
55
|
+
inset: var(--psl-invalid-inset);
|
|
56
|
+
opacity: var(--psl-invalid-opacity);
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
position: absolute;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.psl-loading::after,
|
|
62
|
+
.psl-invalid.psl-loading::after {
|
|
63
|
+
background-image: linear-gradient(
|
|
64
|
+
var(--psl-loading-angle),
|
|
65
|
+
transparent 0%,
|
|
66
|
+
transparent 45%,
|
|
67
|
+
var(--psl-loading-sheen-color) 50%,
|
|
68
|
+
transparent 100%
|
|
69
|
+
);
|
|
70
|
+
background-position: 200% 50%;
|
|
71
|
+
background-repeat: no-repeat;
|
|
72
|
+
background-size: 220% 100%;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.psl-loading:not(.psl-invalid)::after {
|
|
76
|
+
opacity: var(--psl-loading-intensity);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.psl-invalid.psl-loading::after {
|
|
80
|
+
opacity: var(--psl-invalid-opacity);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.psl-invalid:is(input, textarea, select) {
|
|
84
|
+
--psl-invalid-shadow-width: 0px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.psl-invalid:is(input, textarea, select) {
|
|
88
|
+
outline: var(--psl-invalid-width-input, 0px) solid var(--psl-invalid-visible-color);
|
|
89
|
+
outline-offset: var(--psl-invalid-inset);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.psl-invalid {
|
|
93
|
+
--psl-invalid-shadow-width: var(--psl-invalid-width);
|
|
94
|
+
--psl-invalid-width-input: var(--psl-invalid-width);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.psl-loading::after {
|
|
98
|
+
animation: psl-loading-sheen var(--psl-loading-duration) linear infinite;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@keyframes psl-loading-sheen {
|
|
102
|
+
from {
|
|
103
|
+
background-position: 200% 50%;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
to {
|
|
107
|
+
background-position: -120% 50%;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media (prefers-reduced-motion: reduce) {
|
|
112
|
+
.psl-loading::after {
|
|
113
|
+
animation-duration: calc(var(--psl-loading-duration) * 2);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@supports (color: color-mix(in srgb, black 50%, white 50%)) {
|
|
118
|
+
.psl-focus-ring {
|
|
119
|
+
--psl-focus-ring-color: color-mix(in srgb, #3b82f6 78%, white 22%);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.psl-invalid,
|
|
123
|
+
.psl-loading {
|
|
124
|
+
--psl-invalid-color: color-mix(in srgb, #ef4444 82%, white 18%);
|
|
125
|
+
--psl-invalid-visible-color: color-mix(
|
|
126
|
+
in srgb,
|
|
127
|
+
var(--psl-invalid-color) calc(var(--psl-invalid-opacity) * 100%),
|
|
128
|
+
transparent
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.psl-invalid:not(.psl-loading)::after {
|
|
133
|
+
opacity: 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.psl-loading::after,
|
|
137
|
+
.psl-invalid.psl-loading::after {
|
|
138
|
+
background-image: linear-gradient(
|
|
139
|
+
var(--psl-loading-angle),
|
|
140
|
+
transparent 0%,
|
|
141
|
+
color-mix(in srgb, var(--psl-loading-sheen-color) calc(var(--psl-loading-intensity) * 100%), transparent) 45%,
|
|
142
|
+
color-mix(in srgb, var(--psl-loading-sheen-color) calc(var(--psl-loading-intensity) * 100%), transparent) 55%,
|
|
143
|
+
transparent 100%
|
|
144
|
+
);
|
|
145
|
+
opacity: 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
// src/runtime.ts
|
|
151
|
+
var hasInjectedStyles = false;
|
|
152
|
+
var anchorConsumers = /* @__PURE__ */ new WeakMap();
|
|
153
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
154
|
+
var MISSING_ATTRIBUTE = "__psl_missing_attribute__";
|
|
155
|
+
function ensureGlobalStyles() {
|
|
156
|
+
if (hasInjectedStyles || typeof document === "undefined") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const existing = document.getElementById(STYLE_ELEMENT_ID);
|
|
160
|
+
if (existing) {
|
|
161
|
+
hasInjectedStyles = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const style = document.createElement("style");
|
|
165
|
+
style.id = STYLE_ELEMENT_ID;
|
|
166
|
+
style.textContent = STATE_LAYER_CSS;
|
|
167
|
+
document.head.appendChild(style);
|
|
168
|
+
hasInjectedStyles = true;
|
|
169
|
+
}
|
|
170
|
+
function mergeRefs(...refs) {
|
|
171
|
+
return (value) => {
|
|
172
|
+
for (const ref of refs) {
|
|
173
|
+
if (!ref) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (typeof ref === "function") {
|
|
177
|
+
ref(value);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
ref.current = value;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function useMergedNodeRef(...refs) {
|
|
185
|
+
const [node, setNode] = useState(null);
|
|
186
|
+
const mergedRef = useMemo(() => mergeRefs(setNode, ...refs), refs);
|
|
187
|
+
return [node, mergedRef];
|
|
188
|
+
}
|
|
189
|
+
function addAnchorConsumer(element) {
|
|
190
|
+
const current = anchorConsumers.get(element);
|
|
191
|
+
if (!current) {
|
|
192
|
+
anchorConsumers.set(element, {
|
|
193
|
+
count: 1,
|
|
194
|
+
hadClass: element.classList.contains("psl-anchor")
|
|
195
|
+
});
|
|
196
|
+
element.classList.add("psl-anchor");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
anchorConsumers.set(element, {
|
|
200
|
+
...current,
|
|
201
|
+
count: current.count + 1
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function removeAnchorConsumer(element) {
|
|
205
|
+
const currentCount = anchorConsumers.get(element);
|
|
206
|
+
if (!currentCount) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (currentCount.count === 1) {
|
|
210
|
+
anchorConsumers.delete(element);
|
|
211
|
+
if (!currentCount.hadClass) {
|
|
212
|
+
element.classList.remove("psl-anchor");
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
anchorConsumers.set(element, {
|
|
217
|
+
...currentCount,
|
|
218
|
+
count: currentCount.count - 1
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function applyStateLayer(element, className, requireAnchor = true) {
|
|
222
|
+
const touchedVariables = /* @__PURE__ */ new Set();
|
|
223
|
+
const baselineAttributes = /* @__PURE__ */ new Map();
|
|
224
|
+
let currentAttributes = {};
|
|
225
|
+
let anchorApplied = false;
|
|
226
|
+
let isActive = false;
|
|
227
|
+
const sync = (nextVariables, nextActive, nextAttributes) => {
|
|
228
|
+
if (nextActive) {
|
|
229
|
+
element.classList.add(className);
|
|
230
|
+
if (requireAnchor && !anchorApplied && !element.style.position) {
|
|
231
|
+
addAnchorConsumer(element);
|
|
232
|
+
anchorApplied = true;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
element.classList.remove(className);
|
|
236
|
+
if (anchorApplied) {
|
|
237
|
+
removeAnchorConsumer(element);
|
|
238
|
+
anchorApplied = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const nextVariableEntries = nextActive ? nextVariables : {};
|
|
242
|
+
for (const variable of Array.from(touchedVariables)) {
|
|
243
|
+
if (!(variable in nextVariableEntries) || nextVariableEntries[variable] == null) {
|
|
244
|
+
element.style.removeProperty(variable);
|
|
245
|
+
touchedVariables.delete(variable);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const [variable, value] of Object.entries(nextVariableEntries)) {
|
|
249
|
+
if (value == null) {
|
|
250
|
+
element.style.removeProperty(variable);
|
|
251
|
+
touchedVariables.delete(variable);
|
|
252
|
+
} else {
|
|
253
|
+
element.style.setProperty(variable, value);
|
|
254
|
+
touchedVariables.add(variable);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const nextAttributeEntries = nextActive ? nextAttributes ?? {} : {};
|
|
258
|
+
for (const name of Object.keys(currentAttributes)) {
|
|
259
|
+
if (!(name in nextAttributeEntries) || nextAttributeEntries[name] == null) {
|
|
260
|
+
const baselineValue = baselineAttributes.get(name);
|
|
261
|
+
if (baselineValue == null || baselineValue === MISSING_ATTRIBUTE) {
|
|
262
|
+
element.removeAttribute(name);
|
|
263
|
+
} else {
|
|
264
|
+
element.setAttribute(name, baselineValue);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const [name, value] of Object.entries(nextAttributeEntries)) {
|
|
269
|
+
if (!baselineAttributes.has(name)) {
|
|
270
|
+
baselineAttributes.set(name, element.getAttribute(name) ?? MISSING_ATTRIBUTE);
|
|
271
|
+
}
|
|
272
|
+
if (value == null) {
|
|
273
|
+
const baselineValue = baselineAttributes.get(name);
|
|
274
|
+
if (baselineValue == null || baselineValue === MISSING_ATTRIBUTE) {
|
|
275
|
+
element.removeAttribute(name);
|
|
276
|
+
} else {
|
|
277
|
+
element.setAttribute(name, baselineValue);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
element.setAttribute(name, value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
currentAttributes = { ...nextAttributeEntries };
|
|
284
|
+
isActive = nextActive;
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
update(nextVariables, nextActive, nextAttributes) {
|
|
288
|
+
sync(nextVariables, nextActive, nextAttributes);
|
|
289
|
+
},
|
|
290
|
+
cleanup() {
|
|
291
|
+
if (isActive) {
|
|
292
|
+
element.classList.remove(className);
|
|
293
|
+
}
|
|
294
|
+
if (anchorApplied) {
|
|
295
|
+
removeAnchorConsumer(element);
|
|
296
|
+
}
|
|
297
|
+
for (const variable of touchedVariables) {
|
|
298
|
+
element.style.removeProperty(variable);
|
|
299
|
+
}
|
|
300
|
+
for (const name of Object.keys(currentAttributes)) {
|
|
301
|
+
const baselineValue = baselineAttributes.get(name);
|
|
302
|
+
if (baselineValue == null || baselineValue === MISSING_ATTRIBUTE) {
|
|
303
|
+
element.removeAttribute(name);
|
|
304
|
+
} else {
|
|
305
|
+
element.setAttribute(name, baselineValue);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
currentAttributes = {};
|
|
309
|
+
isActive = false;
|
|
310
|
+
anchorApplied = false;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function useStateLayer({
|
|
315
|
+
active = true,
|
|
316
|
+
className,
|
|
317
|
+
variables = {},
|
|
318
|
+
attributes,
|
|
319
|
+
requireAnchor = true
|
|
320
|
+
}) {
|
|
321
|
+
const [node, ref] = useMergedNodeRef();
|
|
322
|
+
const layerRef = useRef(null);
|
|
323
|
+
useIsomorphicLayoutEffect(() => {
|
|
324
|
+
ensureGlobalStyles();
|
|
325
|
+
}, []);
|
|
326
|
+
useIsomorphicLayoutEffect(() => {
|
|
327
|
+
if (!node) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const layer = applyStateLayer(node, className, requireAnchor);
|
|
331
|
+
layerRef.current = layer;
|
|
332
|
+
layer.update(variables, active, attributes);
|
|
333
|
+
return () => {
|
|
334
|
+
layer.cleanup();
|
|
335
|
+
layerRef.current = null;
|
|
336
|
+
};
|
|
337
|
+
}, [className, node, requireAnchor]);
|
|
338
|
+
useIsomorphicLayoutEffect(() => {
|
|
339
|
+
layerRef.current?.update(variables, active, attributes);
|
|
340
|
+
}, [active, attributes, variables]);
|
|
341
|
+
return useCallback((nextNode) => {
|
|
342
|
+
ref(nextNode);
|
|
343
|
+
}, [ref]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/hooks/useFocusRingLayer.ts
|
|
347
|
+
import { useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
348
|
+
var useIsomorphicLayoutEffect2 = typeof window === "undefined" ? useEffect2 : useLayoutEffect2;
|
|
349
|
+
function readFocusVisible(node) {
|
|
350
|
+
if (typeof node.matches === "function") {
|
|
351
|
+
try {
|
|
352
|
+
return node.matches(":focus-visible");
|
|
353
|
+
} catch {
|
|
354
|
+
return document.activeElement === node;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return document.activeElement === node;
|
|
358
|
+
}
|
|
359
|
+
function useFocusRingLayer(options = {}) {
|
|
360
|
+
const { color, inset, offset, ref, visible, width } = options;
|
|
361
|
+
const [focusVisible, setFocusVisible] = useState2(Boolean(visible));
|
|
362
|
+
const [node, mergedRef] = useMergedNodeRef(ref);
|
|
363
|
+
useIsomorphicLayoutEffect2(() => {
|
|
364
|
+
if (visible != null) {
|
|
365
|
+
setFocusVisible(visible);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!node) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
setFocusVisible(readFocusVisible(node));
|
|
372
|
+
}, [node, visible]);
|
|
373
|
+
useEffect2(() => {
|
|
374
|
+
if (!node || visible != null) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const onFocus = () => {
|
|
378
|
+
setFocusVisible(readFocusVisible(node));
|
|
379
|
+
};
|
|
380
|
+
const onBlur = () => setFocusVisible(false);
|
|
381
|
+
node.addEventListener("focus", onFocus);
|
|
382
|
+
node.addEventListener("blur", onBlur);
|
|
383
|
+
return () => {
|
|
384
|
+
node.removeEventListener("focus", onFocus);
|
|
385
|
+
node.removeEventListener("blur", onBlur);
|
|
386
|
+
};
|
|
387
|
+
}, [node, visible]);
|
|
388
|
+
const layerRef = useStateLayer({
|
|
389
|
+
active: focusVisible,
|
|
390
|
+
className: "psl-focus-ring",
|
|
391
|
+
variables: {
|
|
392
|
+
"--psl-focus-ring-color": color,
|
|
393
|
+
"--psl-focus-ring-inset": inset,
|
|
394
|
+
"--psl-focus-ring-offset": offset,
|
|
395
|
+
"--psl-focus-ring-width": width
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return useMemo2(() => {
|
|
399
|
+
return (nextNode) => {
|
|
400
|
+
mergedRef(nextNode);
|
|
401
|
+
layerRef(nextNode);
|
|
402
|
+
};
|
|
403
|
+
}, [layerRef, mergedRef]);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/hooks/useInvalidStateLayer.ts
|
|
407
|
+
import { useMemo as useMemo3 } from "react";
|
|
408
|
+
function useInvalidStateLayer(options = {}) {
|
|
409
|
+
const { active = false, color, inset, opacity, ref, width } = options;
|
|
410
|
+
const layerRef = useStateLayer({
|
|
411
|
+
active,
|
|
412
|
+
className: "psl-invalid",
|
|
413
|
+
variables: {
|
|
414
|
+
"--psl-invalid-color": color,
|
|
415
|
+
"--psl-invalid-inset": inset,
|
|
416
|
+
"--psl-invalid-opacity": opacity == null ? void 0 : String(opacity),
|
|
417
|
+
"--psl-invalid-width": width
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
return useMemo3(() => mergeRefs(layerRef, ref), [layerRef, ref]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/hooks/useLoadingSheenLayer.ts
|
|
424
|
+
import { useMemo as useMemo4 } from "react";
|
|
425
|
+
function useLoadingSheenLayer(options = {}) {
|
|
426
|
+
const { active = false, angle, duration, intensity, ref, sheenColor } = options;
|
|
427
|
+
const layerRef = useStateLayer({
|
|
428
|
+
active,
|
|
429
|
+
className: "psl-loading",
|
|
430
|
+
variables: {
|
|
431
|
+
"--psl-loading-angle": angle,
|
|
432
|
+
"--psl-loading-duration": duration,
|
|
433
|
+
"--psl-loading-intensity": intensity == null ? void 0 : String(intensity),
|
|
434
|
+
"--psl-loading-sheen-color": sheenColor
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
return useMemo4(() => mergeRefs(layerRef, ref), [layerRef, ref]);
|
|
438
|
+
}
|
|
439
|
+
export {
|
|
440
|
+
mergeRefs,
|
|
441
|
+
useFocusRingLayer,
|
|
442
|
+
useInvalidStateLayer,
|
|
443
|
+
useLoadingSheenLayer
|
|
444
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@protohiro/state-layers",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "React hooks for visual state layers on existing elements.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/protohiro-com/state-layers.git",
|
|
13
|
+
"directory": "packages/react"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/protohiro-com/state-layers#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/protohiro-com/state-layers/issues"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/index.d.cts",
|
|
31
|
+
"default": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
41
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@testing-library/react": "^16.3.0",
|
|
45
|
+
"@types/react": "^19.1.12",
|
|
46
|
+
"@types/react-dom": "^19.1.9",
|
|
47
|
+
"jsdom": "^26.1.0",
|
|
48
|
+
"react": "^19.1.1",
|
|
49
|
+
"react-dom": "^19.1.1",
|
|
50
|
+
"tsup": "^8.5.0",
|
|
51
|
+
"typescript": "^5.9.2",
|
|
52
|
+
"vitest": "^3.2.4"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
56
|
+
"pack:check": "npm pack --dry-run",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|