@refraction-ui/shared 0.1.0
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 +21 -0
- package/dist/index.cjs +316 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +242 -0
- package/dist/index.d.ts +242 -0
- package/dist/index.js +300 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 elloloop
|
|
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,21 @@
|
|
|
1
|
+
# @refraction-ui/shared
|
|
2
|
+
|
|
3
|
+
Shared utilities used across all Refraction UI packages. Part of [Refraction UI](https://elloloop.github.io/refraction-ui/).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @refraction-ui/shared
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { cn } from '@refraction-ui/shared'
|
|
15
|
+
|
|
16
|
+
const className = cn('base-class', condition && 'conditional-class')
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## License
|
|
20
|
+
|
|
21
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/aria.ts
|
|
4
|
+
function mergeAriaProps(...propSets) {
|
|
5
|
+
const result = {};
|
|
6
|
+
for (const props of propSets) {
|
|
7
|
+
for (const [key, value] of Object.entries(props)) {
|
|
8
|
+
if (value !== void 0) {
|
|
9
|
+
result[key] = value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
var idCounter = 0;
|
|
16
|
+
function generateId(prefix = "rfr") {
|
|
17
|
+
idCounter++;
|
|
18
|
+
return `${prefix}-${idCounter}`;
|
|
19
|
+
}
|
|
20
|
+
function resetIdCounter() {
|
|
21
|
+
idCounter = 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/keyboard.ts
|
|
25
|
+
var Keys = {
|
|
26
|
+
Enter: "Enter",
|
|
27
|
+
Space: " ",
|
|
28
|
+
Escape: "Escape",
|
|
29
|
+
Tab: "Tab",
|
|
30
|
+
ArrowUp: "ArrowUp",
|
|
31
|
+
ArrowDown: "ArrowDown",
|
|
32
|
+
ArrowLeft: "ArrowLeft",
|
|
33
|
+
ArrowRight: "ArrowRight",
|
|
34
|
+
Home: "Home",
|
|
35
|
+
End: "End",
|
|
36
|
+
PageUp: "PageUp",
|
|
37
|
+
PageDown: "PageDown",
|
|
38
|
+
Backspace: "Backspace",
|
|
39
|
+
Delete: "Delete"
|
|
40
|
+
};
|
|
41
|
+
function createKeyboardHandler(handlers) {
|
|
42
|
+
return (event) => {
|
|
43
|
+
const handler = handlers[event.key];
|
|
44
|
+
if (handler) {
|
|
45
|
+
handler(event);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/state-machine.ts
|
|
51
|
+
function createMachine(config) {
|
|
52
|
+
let current = config.initial;
|
|
53
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
54
|
+
return {
|
|
55
|
+
get state() {
|
|
56
|
+
return current;
|
|
57
|
+
},
|
|
58
|
+
send(event) {
|
|
59
|
+
const stateConfig = config.states[current];
|
|
60
|
+
const next = stateConfig?.on?.[event];
|
|
61
|
+
if (next && next !== current) {
|
|
62
|
+
current = next;
|
|
63
|
+
for (const fn of listeners) {
|
|
64
|
+
fn(current);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
subscribe(fn) {
|
|
69
|
+
listeners.add(fn);
|
|
70
|
+
return () => {
|
|
71
|
+
listeners.delete(fn);
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
matches(state) {
|
|
75
|
+
return current === state;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/cn.ts
|
|
81
|
+
function cn(...inputs) {
|
|
82
|
+
const classes = [];
|
|
83
|
+
for (const input of inputs) {
|
|
84
|
+
if (!input) continue;
|
|
85
|
+
if (typeof input === "string") {
|
|
86
|
+
classes.push(input);
|
|
87
|
+
} else if (typeof input === "number") {
|
|
88
|
+
classes.push(String(input));
|
|
89
|
+
} else if (Array.isArray(input)) {
|
|
90
|
+
const nested = cn(...input);
|
|
91
|
+
if (nested) classes.push(nested);
|
|
92
|
+
} else if (typeof input === "object") {
|
|
93
|
+
for (const [key, value] of Object.entries(input)) {
|
|
94
|
+
if (value) classes.push(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return classes.join(" ");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/cva.ts
|
|
102
|
+
function cva(config) {
|
|
103
|
+
return (props) => {
|
|
104
|
+
const classes = [];
|
|
105
|
+
if (config.base) {
|
|
106
|
+
classes.push(config.base);
|
|
107
|
+
}
|
|
108
|
+
if (config.variants) {
|
|
109
|
+
for (const [variantKey, variantOptions] of Object.entries(config.variants)) {
|
|
110
|
+
const selectedValue = props?.[variantKey] ?? config.defaultVariants?.[variantKey];
|
|
111
|
+
if (selectedValue != null) {
|
|
112
|
+
const variantClass = variantOptions[selectedValue];
|
|
113
|
+
if (variantClass) {
|
|
114
|
+
classes.push(variantClass);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (config.compoundVariants) {
|
|
120
|
+
for (const compound of config.compoundVariants) {
|
|
121
|
+
const { class: compoundClass, ...conditions } = compound;
|
|
122
|
+
let matches = true;
|
|
123
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
124
|
+
const propValue = props?.[key] ?? config.defaultVariants?.[key];
|
|
125
|
+
if (propValue !== value) {
|
|
126
|
+
matches = false;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (matches) {
|
|
131
|
+
classes.push(compoundClass);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (props?.className) {
|
|
136
|
+
classes.push(props.className);
|
|
137
|
+
}
|
|
138
|
+
return classes.filter(Boolean).join(" ");
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/focus-trap.ts
|
|
143
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
144
|
+
function getFocusableElements(container) {
|
|
145
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
146
|
+
}
|
|
147
|
+
function createFocusTrap(config) {
|
|
148
|
+
let active = false;
|
|
149
|
+
let previouslyFocused = null;
|
|
150
|
+
function handleKeyDown(event) {
|
|
151
|
+
if (event.key === "Escape" && config.onEscape) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
config.onEscape();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (event.key !== "Tab") return;
|
|
157
|
+
const focusable = getFocusableElements(config.container);
|
|
158
|
+
if (focusable.length === 0) return;
|
|
159
|
+
const first = focusable[0];
|
|
160
|
+
const last = focusable[focusable.length - 1];
|
|
161
|
+
if (event.shiftKey) {
|
|
162
|
+
if (document.activeElement === first) {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
last.focus();
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
if (document.activeElement === last) {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
first.focus();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
activate() {
|
|
175
|
+
if (active) return;
|
|
176
|
+
active = true;
|
|
177
|
+
if (typeof document !== "undefined") {
|
|
178
|
+
previouslyFocused = document.activeElement;
|
|
179
|
+
}
|
|
180
|
+
if (config.initialFocus) {
|
|
181
|
+
const target = config.initialFocus();
|
|
182
|
+
if (target && "focus" in target) {
|
|
183
|
+
target.focus();
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const focusable = getFocusableElements(config.container);
|
|
187
|
+
if (focusable.length > 0) {
|
|
188
|
+
focusable[0].focus();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
config.container.addEventListener(
|
|
192
|
+
"keydown",
|
|
193
|
+
handleKeyDown
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
deactivate() {
|
|
197
|
+
if (!active) return;
|
|
198
|
+
active = false;
|
|
199
|
+
config.container.removeEventListener(
|
|
200
|
+
"keydown",
|
|
201
|
+
handleKeyDown
|
|
202
|
+
);
|
|
203
|
+
if (config.returnFocusOnDeactivate !== false && previouslyFocused && "focus" in previouslyFocused) {
|
|
204
|
+
previouslyFocused.focus();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
isActive() {
|
|
208
|
+
return active;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/live-region.ts
|
|
214
|
+
function createLiveRegion(config = {}) {
|
|
215
|
+
const { politeness = "polite", clearAfterMs = 5e3 } = config;
|
|
216
|
+
let element = null;
|
|
217
|
+
let clearTimeout = null;
|
|
218
|
+
function getElement() {
|
|
219
|
+
if (!element && typeof document !== "undefined") {
|
|
220
|
+
element = document.createElement("div");
|
|
221
|
+
element.setAttribute("role", "status");
|
|
222
|
+
element.setAttribute("aria-live", politeness);
|
|
223
|
+
element.setAttribute("aria-atomic", "true");
|
|
224
|
+
Object.assign(element.style, {
|
|
225
|
+
position: "absolute",
|
|
226
|
+
width: "1px",
|
|
227
|
+
height: "1px",
|
|
228
|
+
padding: "0",
|
|
229
|
+
margin: "-1px",
|
|
230
|
+
overflow: "hidden",
|
|
231
|
+
clip: "rect(0, 0, 0, 0)",
|
|
232
|
+
whiteSpace: "nowrap",
|
|
233
|
+
border: "0"
|
|
234
|
+
});
|
|
235
|
+
document.body.appendChild(element);
|
|
236
|
+
}
|
|
237
|
+
return element;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
announce(message) {
|
|
241
|
+
const el = getElement();
|
|
242
|
+
if (!el) return;
|
|
243
|
+
if (clearTimeout !== null) {
|
|
244
|
+
globalThis.clearTimeout(clearTimeout);
|
|
245
|
+
}
|
|
246
|
+
el.textContent = message;
|
|
247
|
+
if (clearAfterMs > 0) {
|
|
248
|
+
clearTimeout = globalThis.setTimeout(() => {
|
|
249
|
+
el.textContent = "";
|
|
250
|
+
clearTimeout = null;
|
|
251
|
+
}, clearAfterMs);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
clear() {
|
|
255
|
+
if (clearTimeout !== null) {
|
|
256
|
+
globalThis.clearTimeout(clearTimeout);
|
|
257
|
+
clearTimeout = null;
|
|
258
|
+
}
|
|
259
|
+
if (element) {
|
|
260
|
+
element.textContent = "";
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
destroy() {
|
|
264
|
+
if (clearTimeout !== null) {
|
|
265
|
+
globalThis.clearTimeout(clearTimeout);
|
|
266
|
+
clearTimeout = null;
|
|
267
|
+
}
|
|
268
|
+
if (element && element.parentNode) {
|
|
269
|
+
element.parentNode.removeChild(element);
|
|
270
|
+
element = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/motion.ts
|
|
277
|
+
function prefersReducedMotion() {
|
|
278
|
+
if (typeof window === "undefined") return false;
|
|
279
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
280
|
+
}
|
|
281
|
+
function getAnimationDuration(normalDuration) {
|
|
282
|
+
return prefersReducedMotion() ? "0ms" : normalDuration;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/skip-link.ts
|
|
286
|
+
function createSkipLink(props = {}) {
|
|
287
|
+
const targetId = props.targetId ?? "main-content";
|
|
288
|
+
const label = props.label ?? "Skip to main content";
|
|
289
|
+
return {
|
|
290
|
+
ariaProps: {
|
|
291
|
+
role: "link",
|
|
292
|
+
"aria-label": label
|
|
293
|
+
},
|
|
294
|
+
href: `#${targetId}`,
|
|
295
|
+
label,
|
|
296
|
+
className: "sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:underline"
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
exports.FOCUSABLE_SELECTOR = FOCUSABLE_SELECTOR;
|
|
301
|
+
exports.Keys = Keys;
|
|
302
|
+
exports.cn = cn;
|
|
303
|
+
exports.createFocusTrap = createFocusTrap;
|
|
304
|
+
exports.createKeyboardHandler = createKeyboardHandler;
|
|
305
|
+
exports.createLiveRegion = createLiveRegion;
|
|
306
|
+
exports.createMachine = createMachine;
|
|
307
|
+
exports.createSkipLink = createSkipLink;
|
|
308
|
+
exports.cva = cva;
|
|
309
|
+
exports.generateId = generateId;
|
|
310
|
+
exports.getAnimationDuration = getAnimationDuration;
|
|
311
|
+
exports.getFocusableElements = getFocusableElements;
|
|
312
|
+
exports.mergeAriaProps = mergeAriaProps;
|
|
313
|
+
exports.prefersReducedMotion = prefersReducedMotion;
|
|
314
|
+
exports.resetIdCounter = resetIdCounter;
|
|
315
|
+
//# sourceMappingURL=index.cjs.map
|
|
316
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/aria.ts","../src/keyboard.ts","../src/state-machine.ts","../src/cn.ts","../src/cva.ts","../src/focus-trap.ts","../src/live-region.ts","../src/motion.ts","../src/skip-link.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** Merge multiple ARIA prop objects, later values override earlier ones */\nexport function mergeAriaProps(\n ...propSets: Array<Record<string, unknown>>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const props of propSets) {\n for (const [key, value] of Object.entries(props)) {\n if (value !== undefined) {\n result[key] = value\n }\n }\n }\n return result\n}\n\nlet idCounter = 0\n\n/**\n * Generate a unique ID, safe for SSR (deterministic within a render pass).\n * In browsers, uses crypto.randomUUID when available.\n */\nexport function generateId(prefix = 'rfr'): string {\n idCounter++\n return `${prefix}-${idCounter}`\n}\n\n/** Reset the ID counter (useful for tests) */\nexport function resetIdCounter(): void {\n idCounter = 0\n}\n","/** Standard keyboard key constants */\nexport const Keys = {\n Enter: 'Enter',\n Space: ' ',\n Escape: 'Escape',\n Tab: 'Tab',\n ArrowUp: 'ArrowUp',\n ArrowDown: 'ArrowDown',\n ArrowLeft: 'ArrowLeft',\n ArrowRight: 'ArrowRight',\n Home: 'Home',\n End: 'End',\n PageUp: 'PageUp',\n PageDown: 'PageDown',\n Backspace: 'Backspace',\n Delete: 'Delete',\n} as const\n\nexport type KeyboardKey = (typeof Keys)[keyof typeof Keys]\n\n/** Map of key → handler function */\nexport type KeyboardHandlerMap = Partial<\n Record<string, (event: KeyboardEvent) => void>\n>\n\n/** Create a keyboard event handler from a handler map */\nexport function createKeyboardHandler(\n handlers: KeyboardHandlerMap,\n): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n const handler = handlers[event.key]\n if (handler) {\n handler(event)\n }\n }\n}\n","/**\n * Minimal state machine — zero dependencies, < 1KB.\n * Inspired by XState concepts but dramatically simpler.\n */\n\nexport interface MachineConfig<TState extends string, TEvent extends string> {\n initial: TState\n states: Record<TState, {\n on?: Partial<Record<TEvent, TState>>\n }>\n}\n\nexport interface Machine<TState extends string, TEvent extends string> {\n /** Current state */\n state: TState\n /** Send an event to transition */\n send(event: TEvent): void\n /** Subscribe to state changes. Returns unsubscribe function. */\n subscribe(fn: (state: TState) => void): () => void\n /** Check if machine is in a given state */\n matches(state: TState): boolean\n}\n\nexport function createMachine<TState extends string, TEvent extends string>(\n config: MachineConfig<TState, TEvent>,\n): Machine<TState, TEvent> {\n let current = config.initial\n const listeners = new Set<(state: TState) => void>()\n\n return {\n get state() {\n return current\n },\n\n send(event: TEvent) {\n const stateConfig = config.states[current]\n const next = stateConfig?.on?.[event]\n if (next && next !== current) {\n current = next\n for (const fn of listeners) {\n fn(current)\n }\n }\n },\n\n subscribe(fn: (state: TState) => void) {\n listeners.add(fn)\n return () => {\n listeners.delete(fn)\n }\n },\n\n matches(state: TState) {\n return current === state\n },\n }\n}\n","/**\n * Lightweight class name utility — our own implementation.\n * Handles conditional classes, arrays, and falsy values.\n * No external dependencies (no clsx, no tailwind-merge).\n *\n * For Tailwind class conflict resolution (e.g., 'p-2 p-4' → 'p-4'),\n * consumers can use @refraction-ui/tailwind-config which provides\n * a tw-merge-aware variant of this function.\n */\n\ntype ClassValue = string | number | boolean | undefined | null | ClassValue[]\ntype ClassRecord = Record<string, boolean | undefined | null>\n\nexport function cn(...inputs: Array<ClassValue | ClassRecord>): string {\n const classes: string[] = []\n\n for (const input of inputs) {\n if (!input) continue\n\n if (typeof input === 'string') {\n classes.push(input)\n } else if (typeof input === 'number') {\n classes.push(String(input))\n } else if (Array.isArray(input)) {\n const nested = cn(...input)\n if (nested) classes.push(nested)\n } else if (typeof input === 'object') {\n for (const [key, value] of Object.entries(input)) {\n if (value) classes.push(key)\n }\n }\n }\n\n return classes.join(' ')\n}\n","/**\n * Lightweight class-variance-authority alternative — zero dependencies.\n * Creates variant-driven class name functions for components.\n */\n\ntype ClassValue = string | undefined | null | false\n\ninterface VariantConfig {\n [variant: string]: Record<string, string>\n}\n\ninterface CVAConfig<V extends VariantConfig> {\n base?: string\n variants?: V\n defaultVariants?: {\n [K in keyof V]?: keyof V[K]\n }\n compoundVariants?: Array<\n {\n [K in keyof V]?: keyof V[K]\n } & { class: string }\n >\n}\n\ntype VariantProps<V extends VariantConfig> = {\n [K in keyof V]?: keyof V[K]\n}\n\nexport function cva<V extends VariantConfig>(config: CVAConfig<V>) {\n return (props?: VariantProps<V> & { className?: string }): string => {\n const classes: string[] = []\n\n if (config.base) {\n classes.push(config.base)\n }\n\n if (config.variants) {\n for (const [variantKey, variantOptions] of Object.entries(config.variants)) {\n const selectedValue =\n (props as Record<string, unknown> | undefined)?.[variantKey] ??\n config.defaultVariants?.[variantKey]\n\n if (selectedValue != null) {\n const variantClass = (variantOptions as Record<string, string>)[\n selectedValue as string\n ]\n if (variantClass) {\n classes.push(variantClass)\n }\n }\n }\n }\n\n if (config.compoundVariants) {\n for (const compound of config.compoundVariants) {\n const { class: compoundClass, ...conditions } = compound\n let matches = true\n\n for (const [key, value] of Object.entries(conditions)) {\n const propValue =\n (props as Record<string, unknown>)?.[key] ??\n config.defaultVariants?.[key]\n if (propValue !== value) {\n matches = false\n break\n }\n }\n\n if (matches) {\n classes.push(compoundClass as string)\n }\n }\n }\n\n if (props?.className) {\n classes.push(props.className)\n }\n\n return classes.filter(Boolean).join(' ')\n }\n}\n","/**\n * Focus trap utilities — keeps keyboard focus within a container.\n * Used by Dialog, DropdownMenu, and other overlay components.\n * Pure TypeScript, no DOM dependency (accepts element via parameter).\n */\n\n/** Selector for all natively focusable elements */\nexport const FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\n/** Get all focusable elements within a container */\nexport function getFocusableElements(container: Element): Element[] {\n return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR))\n}\n\n/** Configuration for creating a focus trap */\nexport interface FocusTrapConfig {\n /** The container element */\n container: Element\n /** Called to get the initial focus target */\n initialFocus?: () => Element | null\n /** Called when Escape is pressed */\n onEscape?: () => void\n /** Whether to return focus to trigger on deactivate */\n returnFocusOnDeactivate?: boolean\n}\n\n/** A focus trap instance */\nexport interface FocusTrap {\n activate(): void\n deactivate(): void\n isActive(): boolean\n}\n\n/** Create a focus trap that keeps Tab/Shift+Tab within a container */\nexport function createFocusTrap(config: FocusTrapConfig): FocusTrap {\n let active = false\n let previouslyFocused: Element | null = null\n\n function handleKeyDown(event: KeyboardEvent): void {\n if (event.key === 'Escape' && config.onEscape) {\n event.preventDefault()\n config.onEscape()\n return\n }\n\n if (event.key !== 'Tab') return\n\n const focusable = getFocusableElements(config.container)\n if (focusable.length === 0) return\n\n const first = focusable[0] as HTMLElement\n const last = focusable[focusable.length - 1] as HTMLElement\n\n if (event.shiftKey) {\n // Shift+Tab: if on first element, wrap to last\n if (document.activeElement === first) {\n event.preventDefault()\n last.focus()\n }\n } else {\n // Tab: if on last element, wrap to first\n if (document.activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n }\n }\n\n return {\n activate() {\n if (active) return\n active = true\n\n // Remember the currently focused element so we can restore it later\n if (typeof document !== 'undefined') {\n previouslyFocused = document.activeElement\n }\n\n // Set initial focus\n if (config.initialFocus) {\n const target = config.initialFocus()\n if (target && 'focus' in target) {\n ;(target as HTMLElement).focus()\n }\n } else {\n const focusable = getFocusableElements(config.container)\n if (focusable.length > 0) {\n ;(focusable[0] as HTMLElement).focus()\n }\n }\n\n // Attach keydown listener to the container\n config.container.addEventListener(\n 'keydown',\n handleKeyDown as EventListener,\n )\n },\n\n deactivate() {\n if (!active) return\n active = false\n\n config.container.removeEventListener(\n 'keydown',\n handleKeyDown as EventListener,\n )\n\n // Restore focus to the previously focused element\n if (\n config.returnFocusOnDeactivate !== false &&\n previouslyFocused &&\n 'focus' in previouslyFocused\n ) {\n ;(previouslyFocused as HTMLElement).focus()\n }\n },\n\n isActive() {\n return active\n },\n }\n}\n","/**\n * Screen reader announcement utility.\n * Creates and manages an aria-live region for dynamic content updates.\n */\n\nexport interface LiveRegionConfig {\n /** 'polite' (default) or 'assertive' */\n politeness?: 'polite' | 'assertive'\n /** Clear the announcement after this many ms. Default: 5000 */\n clearAfterMs?: number\n}\n\nexport interface LiveRegion {\n announce(message: string): void\n clear(): void\n destroy(): void\n}\n\n/** Create a live region that announces messages to screen readers */\nexport function createLiveRegion(config: LiveRegionConfig = {}): LiveRegion {\n const { politeness = 'polite', clearAfterMs = 5000 } = config\n let element: HTMLElement | null = null\n let clearTimeout: ReturnType<typeof setTimeout> | null = null\n\n function getElement(): HTMLElement {\n if (!element && typeof document !== 'undefined') {\n element = document.createElement('div')\n element.setAttribute('role', 'status')\n element.setAttribute('aria-live', politeness)\n element.setAttribute('aria-atomic', 'true')\n // Visually hidden but accessible to screen readers\n Object.assign(element.style, {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: '0',\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: '0',\n })\n document.body.appendChild(element)\n }\n return element!\n }\n\n return {\n announce(message: string): void {\n const el = getElement()\n if (!el) return\n\n // Clear any pending timeout\n if (clearTimeout !== null) {\n globalThis.clearTimeout(clearTimeout)\n }\n\n // Set text content — screen readers will detect the change via aria-live\n el.textContent = message\n\n if (clearAfterMs > 0) {\n clearTimeout = globalThis.setTimeout(() => {\n el.textContent = ''\n clearTimeout = null\n }, clearAfterMs)\n }\n },\n\n clear(): void {\n if (clearTimeout !== null) {\n globalThis.clearTimeout(clearTimeout)\n clearTimeout = null\n }\n if (element) {\n element.textContent = ''\n }\n },\n\n destroy(): void {\n if (clearTimeout !== null) {\n globalThis.clearTimeout(clearTimeout)\n clearTimeout = null\n }\n if (element && element.parentNode) {\n element.parentNode.removeChild(element)\n element = null\n }\n },\n }\n}\n","/**\n * Reduced motion utilities.\n * Respects user preferences for reduced motion.\n */\n\n/** Check if user prefers reduced motion */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches\n}\n\n/** Get animation duration — returns '0ms' if reduced motion preferred */\nexport function getAnimationDuration(normalDuration: string): string {\n return prefersReducedMotion() ? '0ms' : normalDuration\n}\n","/**\n * Skip-to-content link utility.\n * Provides a keyboard-accessible link that is visually hidden until focused.\n */\n\n/** Props for a skip-to-content link */\nexport interface SkipLinkProps {\n /** ID of the target element. Default: 'main-content' */\n targetId?: string\n /** Label text for the link. Default: 'Skip to main content' */\n label?: string\n}\n\n/** Create props for a skip-to-content link */\nexport function createSkipLink(props: SkipLinkProps = {}): {\n ariaProps: Record<string, string>\n href: string\n label: string\n /** CSS classes — visually hidden until focused */\n className: string\n} {\n const targetId = props.targetId ?? 'main-content'\n const label = props.label ?? 'Skip to main content'\n\n return {\n ariaProps: {\n role: 'link',\n 'aria-label': label,\n },\n href: `#${targetId}`,\n label,\n className:\n 'sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:underline',\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic base prop types for all refraction-ui components.
|
|
3
|
+
* These are headless core types — no React, Angular, or Astro imports.
|
|
4
|
+
* Framework wrappers extend these with framework-specific types.
|
|
5
|
+
*/
|
|
6
|
+
/** Base props shared by all components */
|
|
7
|
+
interface BaseProps {
|
|
8
|
+
id?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: Record<string, string | number>;
|
|
11
|
+
[dataAttr: `data-${string}`]: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Accessibility props */
|
|
14
|
+
interface AccessibilityProps {
|
|
15
|
+
role?: string;
|
|
16
|
+
tabIndex?: number;
|
|
17
|
+
'aria-label'?: string;
|
|
18
|
+
'aria-labelledby'?: string;
|
|
19
|
+
'aria-describedby'?: string;
|
|
20
|
+
'aria-controls'?: string;
|
|
21
|
+
'aria-expanded'?: boolean;
|
|
22
|
+
'aria-selected'?: boolean;
|
|
23
|
+
'aria-hidden'?: boolean;
|
|
24
|
+
'aria-disabled'?: boolean;
|
|
25
|
+
'aria-pressed'?: boolean | 'mixed';
|
|
26
|
+
'aria-checked'?: boolean | 'mixed';
|
|
27
|
+
'aria-current'?: boolean | 'page' | 'step' | 'location' | 'date' | 'time';
|
|
28
|
+
'aria-live'?: 'off' | 'assertive' | 'polite';
|
|
29
|
+
'aria-atomic'?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Theme customization props (framework-agnostic) */
|
|
32
|
+
interface ThemeProps {
|
|
33
|
+
variant?: string;
|
|
34
|
+
size?: string;
|
|
35
|
+
colorScheme?: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Composition props (framework-agnostic) */
|
|
39
|
+
interface CompositionProps {
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Standard size scale */
|
|
44
|
+
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
45
|
+
/** Standard component variants */
|
|
46
|
+
type Variant = 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';
|
|
47
|
+
/** Layout orientation */
|
|
48
|
+
type Orientation = 'horizontal' | 'vertical';
|
|
49
|
+
/** Positioning side */
|
|
50
|
+
type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
51
|
+
/** Alignment */
|
|
52
|
+
type Align = 'start' | 'center' | 'end';
|
|
53
|
+
/** Data state attribute values */
|
|
54
|
+
type DataState = 'open' | 'closed' | 'active' | 'inactive';
|
|
55
|
+
|
|
56
|
+
/** Defines the CSS custom properties a component reads */
|
|
57
|
+
interface TokenContract {
|
|
58
|
+
/** Component name */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Map of token name → CSS custom property info */
|
|
61
|
+
tokens: Record<string, TokenDefinition>;
|
|
62
|
+
}
|
|
63
|
+
interface TokenDefinition {
|
|
64
|
+
/** CSS custom property name (e.g., '--rfr-button-bg') */
|
|
65
|
+
variable: string;
|
|
66
|
+
/** Fallback value when the variable is not set */
|
|
67
|
+
fallback: string;
|
|
68
|
+
/** Description of what this token controls */
|
|
69
|
+
description?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Standard keyboard key constants */
|
|
73
|
+
declare const Keys: {
|
|
74
|
+
readonly Enter: "Enter";
|
|
75
|
+
readonly Space: " ";
|
|
76
|
+
readonly Escape: "Escape";
|
|
77
|
+
readonly Tab: "Tab";
|
|
78
|
+
readonly ArrowUp: "ArrowUp";
|
|
79
|
+
readonly ArrowDown: "ArrowDown";
|
|
80
|
+
readonly ArrowLeft: "ArrowLeft";
|
|
81
|
+
readonly ArrowRight: "ArrowRight";
|
|
82
|
+
readonly Home: "Home";
|
|
83
|
+
readonly End: "End";
|
|
84
|
+
readonly PageUp: "PageUp";
|
|
85
|
+
readonly PageDown: "PageDown";
|
|
86
|
+
readonly Backspace: "Backspace";
|
|
87
|
+
readonly Delete: "Delete";
|
|
88
|
+
};
|
|
89
|
+
type KeyboardKey = (typeof Keys)[keyof typeof Keys];
|
|
90
|
+
/** Map of key → handler function */
|
|
91
|
+
type KeyboardHandlerMap = Partial<Record<string, (event: KeyboardEvent) => void>>;
|
|
92
|
+
/** Create a keyboard event handler from a handler map */
|
|
93
|
+
declare function createKeyboardHandler(handlers: KeyboardHandlerMap): (event: KeyboardEvent) => void;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Minimal state machine — zero dependencies, < 1KB.
|
|
97
|
+
* Inspired by XState concepts but dramatically simpler.
|
|
98
|
+
*/
|
|
99
|
+
interface MachineConfig<TState extends string, TEvent extends string> {
|
|
100
|
+
initial: TState;
|
|
101
|
+
states: Record<TState, {
|
|
102
|
+
on?: Partial<Record<TEvent, TState>>;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
interface Machine<TState extends string, TEvent extends string> {
|
|
106
|
+
/** Current state */
|
|
107
|
+
state: TState;
|
|
108
|
+
/** Send an event to transition */
|
|
109
|
+
send(event: TEvent): void;
|
|
110
|
+
/** Subscribe to state changes. Returns unsubscribe function. */
|
|
111
|
+
subscribe(fn: (state: TState) => void): () => void;
|
|
112
|
+
/** Check if machine is in a given state */
|
|
113
|
+
matches(state: TState): boolean;
|
|
114
|
+
}
|
|
115
|
+
declare function createMachine<TState extends string, TEvent extends string>(config: MachineConfig<TState, TEvent>): Machine<TState, TEvent>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Focus trap utilities — keeps keyboard focus within a container.
|
|
119
|
+
* Used by Dialog, DropdownMenu, and other overlay components.
|
|
120
|
+
* Pure TypeScript, no DOM dependency (accepts element via parameter).
|
|
121
|
+
*/
|
|
122
|
+
/** Selector for all natively focusable elements */
|
|
123
|
+
declare const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
124
|
+
/** Get all focusable elements within a container */
|
|
125
|
+
declare function getFocusableElements(container: Element): Element[];
|
|
126
|
+
/** Configuration for creating a focus trap */
|
|
127
|
+
interface FocusTrapConfig {
|
|
128
|
+
/** The container element */
|
|
129
|
+
container: Element;
|
|
130
|
+
/** Called to get the initial focus target */
|
|
131
|
+
initialFocus?: () => Element | null;
|
|
132
|
+
/** Called when Escape is pressed */
|
|
133
|
+
onEscape?: () => void;
|
|
134
|
+
/** Whether to return focus to trigger on deactivate */
|
|
135
|
+
returnFocusOnDeactivate?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/** A focus trap instance */
|
|
138
|
+
interface FocusTrap {
|
|
139
|
+
activate(): void;
|
|
140
|
+
deactivate(): void;
|
|
141
|
+
isActive(): boolean;
|
|
142
|
+
}
|
|
143
|
+
/** Create a focus trap that keeps Tab/Shift+Tab within a container */
|
|
144
|
+
declare function createFocusTrap(config: FocusTrapConfig): FocusTrap;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Screen reader announcement utility.
|
|
148
|
+
* Creates and manages an aria-live region for dynamic content updates.
|
|
149
|
+
*/
|
|
150
|
+
interface LiveRegionConfig {
|
|
151
|
+
/** 'polite' (default) or 'assertive' */
|
|
152
|
+
politeness?: 'polite' | 'assertive';
|
|
153
|
+
/** Clear the announcement after this many ms. Default: 5000 */
|
|
154
|
+
clearAfterMs?: number;
|
|
155
|
+
}
|
|
156
|
+
interface LiveRegion {
|
|
157
|
+
announce(message: string): void;
|
|
158
|
+
clear(): void;
|
|
159
|
+
destroy(): void;
|
|
160
|
+
}
|
|
161
|
+
/** Create a live region that announces messages to screen readers */
|
|
162
|
+
declare function createLiveRegion(config?: LiveRegionConfig): LiveRegion;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Skip-to-content link utility.
|
|
166
|
+
* Provides a keyboard-accessible link that is visually hidden until focused.
|
|
167
|
+
*/
|
|
168
|
+
/** Props for a skip-to-content link */
|
|
169
|
+
interface SkipLinkProps {
|
|
170
|
+
/** ID of the target element. Default: 'main-content' */
|
|
171
|
+
targetId?: string;
|
|
172
|
+
/** Label text for the link. Default: 'Skip to main content' */
|
|
173
|
+
label?: string;
|
|
174
|
+
}
|
|
175
|
+
/** Create props for a skip-to-content link */
|
|
176
|
+
declare function createSkipLink(props?: SkipLinkProps): {
|
|
177
|
+
ariaProps: Record<string, string>;
|
|
178
|
+
href: string;
|
|
179
|
+
label: string;
|
|
180
|
+
/** CSS classes — visually hidden until focused */
|
|
181
|
+
className: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Merge multiple ARIA prop objects, later values override earlier ones */
|
|
185
|
+
declare function mergeAriaProps(...propSets: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
186
|
+
/**
|
|
187
|
+
* Generate a unique ID, safe for SSR (deterministic within a render pass).
|
|
188
|
+
* In browsers, uses crypto.randomUUID when available.
|
|
189
|
+
*/
|
|
190
|
+
declare function generateId(prefix?: string): string;
|
|
191
|
+
/** Reset the ID counter (useful for tests) */
|
|
192
|
+
declare function resetIdCounter(): void;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Lightweight class name utility — our own implementation.
|
|
196
|
+
* Handles conditional classes, arrays, and falsy values.
|
|
197
|
+
* No external dependencies (no clsx, no tailwind-merge).
|
|
198
|
+
*
|
|
199
|
+
* For Tailwind class conflict resolution (e.g., 'p-2 p-4' → 'p-4'),
|
|
200
|
+
* consumers can use @refraction-ui/tailwind-config which provides
|
|
201
|
+
* a tw-merge-aware variant of this function.
|
|
202
|
+
*/
|
|
203
|
+
type ClassValue = string | number | boolean | undefined | null | ClassValue[];
|
|
204
|
+
type ClassRecord = Record<string, boolean | undefined | null>;
|
|
205
|
+
declare function cn(...inputs: Array<ClassValue | ClassRecord>): string;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Lightweight class-variance-authority alternative — zero dependencies.
|
|
209
|
+
* Creates variant-driven class name functions for components.
|
|
210
|
+
*/
|
|
211
|
+
interface VariantConfig {
|
|
212
|
+
[variant: string]: Record<string, string>;
|
|
213
|
+
}
|
|
214
|
+
interface CVAConfig<V extends VariantConfig> {
|
|
215
|
+
base?: string;
|
|
216
|
+
variants?: V;
|
|
217
|
+
defaultVariants?: {
|
|
218
|
+
[K in keyof V]?: keyof V[K];
|
|
219
|
+
};
|
|
220
|
+
compoundVariants?: Array<{
|
|
221
|
+
[K in keyof V]?: keyof V[K];
|
|
222
|
+
} & {
|
|
223
|
+
class: string;
|
|
224
|
+
}>;
|
|
225
|
+
}
|
|
226
|
+
type VariantProps<V extends VariantConfig> = {
|
|
227
|
+
[K in keyof V]?: keyof V[K];
|
|
228
|
+
};
|
|
229
|
+
declare function cva<V extends VariantConfig>(config: CVAConfig<V>): (props?: VariantProps<V> & {
|
|
230
|
+
className?: string;
|
|
231
|
+
}) => string;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Reduced motion utilities.
|
|
235
|
+
* Respects user preferences for reduced motion.
|
|
236
|
+
*/
|
|
237
|
+
/** Check if user prefers reduced motion */
|
|
238
|
+
declare function prefersReducedMotion(): boolean;
|
|
239
|
+
/** Get animation duration — returns '0ms' if reduced motion preferred */
|
|
240
|
+
declare function getAnimationDuration(normalDuration: string): string;
|
|
241
|
+
|
|
242
|
+
export { type AccessibilityProps, type Align, type BaseProps, type CompositionProps, type DataState, FOCUSABLE_SELECTOR, type FocusTrap, type FocusTrapConfig, type KeyboardHandlerMap, type KeyboardKey, Keys, type LiveRegion, type LiveRegionConfig, type Machine, type MachineConfig, type Orientation, type Side, type Size, type SkipLinkProps, type ThemeProps, type TokenContract, type TokenDefinition, type Variant, cn, createFocusTrap, createKeyboardHandler, createLiveRegion, createMachine, createSkipLink, cva, generateId, getAnimationDuration, getFocusableElements, mergeAriaProps, prefersReducedMotion, resetIdCounter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic base prop types for all refraction-ui components.
|
|
3
|
+
* These are headless core types — no React, Angular, or Astro imports.
|
|
4
|
+
* Framework wrappers extend these with framework-specific types.
|
|
5
|
+
*/
|
|
6
|
+
/** Base props shared by all components */
|
|
7
|
+
interface BaseProps {
|
|
8
|
+
id?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: Record<string, string | number>;
|
|
11
|
+
[dataAttr: `data-${string}`]: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Accessibility props */
|
|
14
|
+
interface AccessibilityProps {
|
|
15
|
+
role?: string;
|
|
16
|
+
tabIndex?: number;
|
|
17
|
+
'aria-label'?: string;
|
|
18
|
+
'aria-labelledby'?: string;
|
|
19
|
+
'aria-describedby'?: string;
|
|
20
|
+
'aria-controls'?: string;
|
|
21
|
+
'aria-expanded'?: boolean;
|
|
22
|
+
'aria-selected'?: boolean;
|
|
23
|
+
'aria-hidden'?: boolean;
|
|
24
|
+
'aria-disabled'?: boolean;
|
|
25
|
+
'aria-pressed'?: boolean | 'mixed';
|
|
26
|
+
'aria-checked'?: boolean | 'mixed';
|
|
27
|
+
'aria-current'?: boolean | 'page' | 'step' | 'location' | 'date' | 'time';
|
|
28
|
+
'aria-live'?: 'off' | 'assertive' | 'polite';
|
|
29
|
+
'aria-atomic'?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Theme customization props (framework-agnostic) */
|
|
32
|
+
interface ThemeProps {
|
|
33
|
+
variant?: string;
|
|
34
|
+
size?: string;
|
|
35
|
+
colorScheme?: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Composition props (framework-agnostic) */
|
|
39
|
+
interface CompositionProps {
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Standard size scale */
|
|
44
|
+
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
45
|
+
/** Standard component variants */
|
|
46
|
+
type Variant = 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';
|
|
47
|
+
/** Layout orientation */
|
|
48
|
+
type Orientation = 'horizontal' | 'vertical';
|
|
49
|
+
/** Positioning side */
|
|
50
|
+
type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
51
|
+
/** Alignment */
|
|
52
|
+
type Align = 'start' | 'center' | 'end';
|
|
53
|
+
/** Data state attribute values */
|
|
54
|
+
type DataState = 'open' | 'closed' | 'active' | 'inactive';
|
|
55
|
+
|
|
56
|
+
/** Defines the CSS custom properties a component reads */
|
|
57
|
+
interface TokenContract {
|
|
58
|
+
/** Component name */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Map of token name → CSS custom property info */
|
|
61
|
+
tokens: Record<string, TokenDefinition>;
|
|
62
|
+
}
|
|
63
|
+
interface TokenDefinition {
|
|
64
|
+
/** CSS custom property name (e.g., '--rfr-button-bg') */
|
|
65
|
+
variable: string;
|
|
66
|
+
/** Fallback value when the variable is not set */
|
|
67
|
+
fallback: string;
|
|
68
|
+
/** Description of what this token controls */
|
|
69
|
+
description?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Standard keyboard key constants */
|
|
73
|
+
declare const Keys: {
|
|
74
|
+
readonly Enter: "Enter";
|
|
75
|
+
readonly Space: " ";
|
|
76
|
+
readonly Escape: "Escape";
|
|
77
|
+
readonly Tab: "Tab";
|
|
78
|
+
readonly ArrowUp: "ArrowUp";
|
|
79
|
+
readonly ArrowDown: "ArrowDown";
|
|
80
|
+
readonly ArrowLeft: "ArrowLeft";
|
|
81
|
+
readonly ArrowRight: "ArrowRight";
|
|
82
|
+
readonly Home: "Home";
|
|
83
|
+
readonly End: "End";
|
|
84
|
+
readonly PageUp: "PageUp";
|
|
85
|
+
readonly PageDown: "PageDown";
|
|
86
|
+
readonly Backspace: "Backspace";
|
|
87
|
+
readonly Delete: "Delete";
|
|
88
|
+
};
|
|
89
|
+
type KeyboardKey = (typeof Keys)[keyof typeof Keys];
|
|
90
|
+
/** Map of key → handler function */
|
|
91
|
+
type KeyboardHandlerMap = Partial<Record<string, (event: KeyboardEvent) => void>>;
|
|
92
|
+
/** Create a keyboard event handler from a handler map */
|
|
93
|
+
declare function createKeyboardHandler(handlers: KeyboardHandlerMap): (event: KeyboardEvent) => void;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Minimal state machine — zero dependencies, < 1KB.
|
|
97
|
+
* Inspired by XState concepts but dramatically simpler.
|
|
98
|
+
*/
|
|
99
|
+
interface MachineConfig<TState extends string, TEvent extends string> {
|
|
100
|
+
initial: TState;
|
|
101
|
+
states: Record<TState, {
|
|
102
|
+
on?: Partial<Record<TEvent, TState>>;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
interface Machine<TState extends string, TEvent extends string> {
|
|
106
|
+
/** Current state */
|
|
107
|
+
state: TState;
|
|
108
|
+
/** Send an event to transition */
|
|
109
|
+
send(event: TEvent): void;
|
|
110
|
+
/** Subscribe to state changes. Returns unsubscribe function. */
|
|
111
|
+
subscribe(fn: (state: TState) => void): () => void;
|
|
112
|
+
/** Check if machine is in a given state */
|
|
113
|
+
matches(state: TState): boolean;
|
|
114
|
+
}
|
|
115
|
+
declare function createMachine<TState extends string, TEvent extends string>(config: MachineConfig<TState, TEvent>): Machine<TState, TEvent>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Focus trap utilities — keeps keyboard focus within a container.
|
|
119
|
+
* Used by Dialog, DropdownMenu, and other overlay components.
|
|
120
|
+
* Pure TypeScript, no DOM dependency (accepts element via parameter).
|
|
121
|
+
*/
|
|
122
|
+
/** Selector for all natively focusable elements */
|
|
123
|
+
declare const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
124
|
+
/** Get all focusable elements within a container */
|
|
125
|
+
declare function getFocusableElements(container: Element): Element[];
|
|
126
|
+
/** Configuration for creating a focus trap */
|
|
127
|
+
interface FocusTrapConfig {
|
|
128
|
+
/** The container element */
|
|
129
|
+
container: Element;
|
|
130
|
+
/** Called to get the initial focus target */
|
|
131
|
+
initialFocus?: () => Element | null;
|
|
132
|
+
/** Called when Escape is pressed */
|
|
133
|
+
onEscape?: () => void;
|
|
134
|
+
/** Whether to return focus to trigger on deactivate */
|
|
135
|
+
returnFocusOnDeactivate?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/** A focus trap instance */
|
|
138
|
+
interface FocusTrap {
|
|
139
|
+
activate(): void;
|
|
140
|
+
deactivate(): void;
|
|
141
|
+
isActive(): boolean;
|
|
142
|
+
}
|
|
143
|
+
/** Create a focus trap that keeps Tab/Shift+Tab within a container */
|
|
144
|
+
declare function createFocusTrap(config: FocusTrapConfig): FocusTrap;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Screen reader announcement utility.
|
|
148
|
+
* Creates and manages an aria-live region for dynamic content updates.
|
|
149
|
+
*/
|
|
150
|
+
interface LiveRegionConfig {
|
|
151
|
+
/** 'polite' (default) or 'assertive' */
|
|
152
|
+
politeness?: 'polite' | 'assertive';
|
|
153
|
+
/** Clear the announcement after this many ms. Default: 5000 */
|
|
154
|
+
clearAfterMs?: number;
|
|
155
|
+
}
|
|
156
|
+
interface LiveRegion {
|
|
157
|
+
announce(message: string): void;
|
|
158
|
+
clear(): void;
|
|
159
|
+
destroy(): void;
|
|
160
|
+
}
|
|
161
|
+
/** Create a live region that announces messages to screen readers */
|
|
162
|
+
declare function createLiveRegion(config?: LiveRegionConfig): LiveRegion;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Skip-to-content link utility.
|
|
166
|
+
* Provides a keyboard-accessible link that is visually hidden until focused.
|
|
167
|
+
*/
|
|
168
|
+
/** Props for a skip-to-content link */
|
|
169
|
+
interface SkipLinkProps {
|
|
170
|
+
/** ID of the target element. Default: 'main-content' */
|
|
171
|
+
targetId?: string;
|
|
172
|
+
/** Label text for the link. Default: 'Skip to main content' */
|
|
173
|
+
label?: string;
|
|
174
|
+
}
|
|
175
|
+
/** Create props for a skip-to-content link */
|
|
176
|
+
declare function createSkipLink(props?: SkipLinkProps): {
|
|
177
|
+
ariaProps: Record<string, string>;
|
|
178
|
+
href: string;
|
|
179
|
+
label: string;
|
|
180
|
+
/** CSS classes — visually hidden until focused */
|
|
181
|
+
className: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Merge multiple ARIA prop objects, later values override earlier ones */
|
|
185
|
+
declare function mergeAriaProps(...propSets: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
186
|
+
/**
|
|
187
|
+
* Generate a unique ID, safe for SSR (deterministic within a render pass).
|
|
188
|
+
* In browsers, uses crypto.randomUUID when available.
|
|
189
|
+
*/
|
|
190
|
+
declare function generateId(prefix?: string): string;
|
|
191
|
+
/** Reset the ID counter (useful for tests) */
|
|
192
|
+
declare function resetIdCounter(): void;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Lightweight class name utility — our own implementation.
|
|
196
|
+
* Handles conditional classes, arrays, and falsy values.
|
|
197
|
+
* No external dependencies (no clsx, no tailwind-merge).
|
|
198
|
+
*
|
|
199
|
+
* For Tailwind class conflict resolution (e.g., 'p-2 p-4' → 'p-4'),
|
|
200
|
+
* consumers can use @refraction-ui/tailwind-config which provides
|
|
201
|
+
* a tw-merge-aware variant of this function.
|
|
202
|
+
*/
|
|
203
|
+
type ClassValue = string | number | boolean | undefined | null | ClassValue[];
|
|
204
|
+
type ClassRecord = Record<string, boolean | undefined | null>;
|
|
205
|
+
declare function cn(...inputs: Array<ClassValue | ClassRecord>): string;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Lightweight class-variance-authority alternative — zero dependencies.
|
|
209
|
+
* Creates variant-driven class name functions for components.
|
|
210
|
+
*/
|
|
211
|
+
interface VariantConfig {
|
|
212
|
+
[variant: string]: Record<string, string>;
|
|
213
|
+
}
|
|
214
|
+
interface CVAConfig<V extends VariantConfig> {
|
|
215
|
+
base?: string;
|
|
216
|
+
variants?: V;
|
|
217
|
+
defaultVariants?: {
|
|
218
|
+
[K in keyof V]?: keyof V[K];
|
|
219
|
+
};
|
|
220
|
+
compoundVariants?: Array<{
|
|
221
|
+
[K in keyof V]?: keyof V[K];
|
|
222
|
+
} & {
|
|
223
|
+
class: string;
|
|
224
|
+
}>;
|
|
225
|
+
}
|
|
226
|
+
type VariantProps<V extends VariantConfig> = {
|
|
227
|
+
[K in keyof V]?: keyof V[K];
|
|
228
|
+
};
|
|
229
|
+
declare function cva<V extends VariantConfig>(config: CVAConfig<V>): (props?: VariantProps<V> & {
|
|
230
|
+
className?: string;
|
|
231
|
+
}) => string;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Reduced motion utilities.
|
|
235
|
+
* Respects user preferences for reduced motion.
|
|
236
|
+
*/
|
|
237
|
+
/** Check if user prefers reduced motion */
|
|
238
|
+
declare function prefersReducedMotion(): boolean;
|
|
239
|
+
/** Get animation duration — returns '0ms' if reduced motion preferred */
|
|
240
|
+
declare function getAnimationDuration(normalDuration: string): string;
|
|
241
|
+
|
|
242
|
+
export { type AccessibilityProps, type Align, type BaseProps, type CompositionProps, type DataState, FOCUSABLE_SELECTOR, type FocusTrap, type FocusTrapConfig, type KeyboardHandlerMap, type KeyboardKey, Keys, type LiveRegion, type LiveRegionConfig, type Machine, type MachineConfig, type Orientation, type Side, type Size, type SkipLinkProps, type ThemeProps, type TokenContract, type TokenDefinition, type Variant, cn, createFocusTrap, createKeyboardHandler, createLiveRegion, createMachine, createSkipLink, cva, generateId, getAnimationDuration, getFocusableElements, mergeAriaProps, prefersReducedMotion, resetIdCounter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// src/aria.ts
|
|
2
|
+
function mergeAriaProps(...propSets) {
|
|
3
|
+
const result = {};
|
|
4
|
+
for (const props of propSets) {
|
|
5
|
+
for (const [key, value] of Object.entries(props)) {
|
|
6
|
+
if (value !== void 0) {
|
|
7
|
+
result[key] = value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
var idCounter = 0;
|
|
14
|
+
function generateId(prefix = "rfr") {
|
|
15
|
+
idCounter++;
|
|
16
|
+
return `${prefix}-${idCounter}`;
|
|
17
|
+
}
|
|
18
|
+
function resetIdCounter() {
|
|
19
|
+
idCounter = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/keyboard.ts
|
|
23
|
+
var Keys = {
|
|
24
|
+
Enter: "Enter",
|
|
25
|
+
Space: " ",
|
|
26
|
+
Escape: "Escape",
|
|
27
|
+
Tab: "Tab",
|
|
28
|
+
ArrowUp: "ArrowUp",
|
|
29
|
+
ArrowDown: "ArrowDown",
|
|
30
|
+
ArrowLeft: "ArrowLeft",
|
|
31
|
+
ArrowRight: "ArrowRight",
|
|
32
|
+
Home: "Home",
|
|
33
|
+
End: "End",
|
|
34
|
+
PageUp: "PageUp",
|
|
35
|
+
PageDown: "PageDown",
|
|
36
|
+
Backspace: "Backspace",
|
|
37
|
+
Delete: "Delete"
|
|
38
|
+
};
|
|
39
|
+
function createKeyboardHandler(handlers) {
|
|
40
|
+
return (event) => {
|
|
41
|
+
const handler = handlers[event.key];
|
|
42
|
+
if (handler) {
|
|
43
|
+
handler(event);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/state-machine.ts
|
|
49
|
+
function createMachine(config) {
|
|
50
|
+
let current = config.initial;
|
|
51
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
52
|
+
return {
|
|
53
|
+
get state() {
|
|
54
|
+
return current;
|
|
55
|
+
},
|
|
56
|
+
send(event) {
|
|
57
|
+
const stateConfig = config.states[current];
|
|
58
|
+
const next = stateConfig?.on?.[event];
|
|
59
|
+
if (next && next !== current) {
|
|
60
|
+
current = next;
|
|
61
|
+
for (const fn of listeners) {
|
|
62
|
+
fn(current);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
subscribe(fn) {
|
|
67
|
+
listeners.add(fn);
|
|
68
|
+
return () => {
|
|
69
|
+
listeners.delete(fn);
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
matches(state) {
|
|
73
|
+
return current === state;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/cn.ts
|
|
79
|
+
function cn(...inputs) {
|
|
80
|
+
const classes = [];
|
|
81
|
+
for (const input of inputs) {
|
|
82
|
+
if (!input) continue;
|
|
83
|
+
if (typeof input === "string") {
|
|
84
|
+
classes.push(input);
|
|
85
|
+
} else if (typeof input === "number") {
|
|
86
|
+
classes.push(String(input));
|
|
87
|
+
} else if (Array.isArray(input)) {
|
|
88
|
+
const nested = cn(...input);
|
|
89
|
+
if (nested) classes.push(nested);
|
|
90
|
+
} else if (typeof input === "object") {
|
|
91
|
+
for (const [key, value] of Object.entries(input)) {
|
|
92
|
+
if (value) classes.push(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return classes.join(" ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/cva.ts
|
|
100
|
+
function cva(config) {
|
|
101
|
+
return (props) => {
|
|
102
|
+
const classes = [];
|
|
103
|
+
if (config.base) {
|
|
104
|
+
classes.push(config.base);
|
|
105
|
+
}
|
|
106
|
+
if (config.variants) {
|
|
107
|
+
for (const [variantKey, variantOptions] of Object.entries(config.variants)) {
|
|
108
|
+
const selectedValue = props?.[variantKey] ?? config.defaultVariants?.[variantKey];
|
|
109
|
+
if (selectedValue != null) {
|
|
110
|
+
const variantClass = variantOptions[selectedValue];
|
|
111
|
+
if (variantClass) {
|
|
112
|
+
classes.push(variantClass);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (config.compoundVariants) {
|
|
118
|
+
for (const compound of config.compoundVariants) {
|
|
119
|
+
const { class: compoundClass, ...conditions } = compound;
|
|
120
|
+
let matches = true;
|
|
121
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
122
|
+
const propValue = props?.[key] ?? config.defaultVariants?.[key];
|
|
123
|
+
if (propValue !== value) {
|
|
124
|
+
matches = false;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (matches) {
|
|
129
|
+
classes.push(compoundClass);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (props?.className) {
|
|
134
|
+
classes.push(props.className);
|
|
135
|
+
}
|
|
136
|
+
return classes.filter(Boolean).join(" ");
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/focus-trap.ts
|
|
141
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
142
|
+
function getFocusableElements(container) {
|
|
143
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
144
|
+
}
|
|
145
|
+
function createFocusTrap(config) {
|
|
146
|
+
let active = false;
|
|
147
|
+
let previouslyFocused = null;
|
|
148
|
+
function handleKeyDown(event) {
|
|
149
|
+
if (event.key === "Escape" && config.onEscape) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
config.onEscape();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (event.key !== "Tab") return;
|
|
155
|
+
const focusable = getFocusableElements(config.container);
|
|
156
|
+
if (focusable.length === 0) return;
|
|
157
|
+
const first = focusable[0];
|
|
158
|
+
const last = focusable[focusable.length - 1];
|
|
159
|
+
if (event.shiftKey) {
|
|
160
|
+
if (document.activeElement === first) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
last.focus();
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
if (document.activeElement === last) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
first.focus();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
activate() {
|
|
173
|
+
if (active) return;
|
|
174
|
+
active = true;
|
|
175
|
+
if (typeof document !== "undefined") {
|
|
176
|
+
previouslyFocused = document.activeElement;
|
|
177
|
+
}
|
|
178
|
+
if (config.initialFocus) {
|
|
179
|
+
const target = config.initialFocus();
|
|
180
|
+
if (target && "focus" in target) {
|
|
181
|
+
target.focus();
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
const focusable = getFocusableElements(config.container);
|
|
185
|
+
if (focusable.length > 0) {
|
|
186
|
+
focusable[0].focus();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
config.container.addEventListener(
|
|
190
|
+
"keydown",
|
|
191
|
+
handleKeyDown
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
deactivate() {
|
|
195
|
+
if (!active) return;
|
|
196
|
+
active = false;
|
|
197
|
+
config.container.removeEventListener(
|
|
198
|
+
"keydown",
|
|
199
|
+
handleKeyDown
|
|
200
|
+
);
|
|
201
|
+
if (config.returnFocusOnDeactivate !== false && previouslyFocused && "focus" in previouslyFocused) {
|
|
202
|
+
previouslyFocused.focus();
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
isActive() {
|
|
206
|
+
return active;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/live-region.ts
|
|
212
|
+
function createLiveRegion(config = {}) {
|
|
213
|
+
const { politeness = "polite", clearAfterMs = 5e3 } = config;
|
|
214
|
+
let element = null;
|
|
215
|
+
let clearTimeout = null;
|
|
216
|
+
function getElement() {
|
|
217
|
+
if (!element && typeof document !== "undefined") {
|
|
218
|
+
element = document.createElement("div");
|
|
219
|
+
element.setAttribute("role", "status");
|
|
220
|
+
element.setAttribute("aria-live", politeness);
|
|
221
|
+
element.setAttribute("aria-atomic", "true");
|
|
222
|
+
Object.assign(element.style, {
|
|
223
|
+
position: "absolute",
|
|
224
|
+
width: "1px",
|
|
225
|
+
height: "1px",
|
|
226
|
+
padding: "0",
|
|
227
|
+
margin: "-1px",
|
|
228
|
+
overflow: "hidden",
|
|
229
|
+
clip: "rect(0, 0, 0, 0)",
|
|
230
|
+
whiteSpace: "nowrap",
|
|
231
|
+
border: "0"
|
|
232
|
+
});
|
|
233
|
+
document.body.appendChild(element);
|
|
234
|
+
}
|
|
235
|
+
return element;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
announce(message) {
|
|
239
|
+
const el = getElement();
|
|
240
|
+
if (!el) return;
|
|
241
|
+
if (clearTimeout !== null) {
|
|
242
|
+
globalThis.clearTimeout(clearTimeout);
|
|
243
|
+
}
|
|
244
|
+
el.textContent = message;
|
|
245
|
+
if (clearAfterMs > 0) {
|
|
246
|
+
clearTimeout = globalThis.setTimeout(() => {
|
|
247
|
+
el.textContent = "";
|
|
248
|
+
clearTimeout = null;
|
|
249
|
+
}, clearAfterMs);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
clear() {
|
|
253
|
+
if (clearTimeout !== null) {
|
|
254
|
+
globalThis.clearTimeout(clearTimeout);
|
|
255
|
+
clearTimeout = null;
|
|
256
|
+
}
|
|
257
|
+
if (element) {
|
|
258
|
+
element.textContent = "";
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
destroy() {
|
|
262
|
+
if (clearTimeout !== null) {
|
|
263
|
+
globalThis.clearTimeout(clearTimeout);
|
|
264
|
+
clearTimeout = null;
|
|
265
|
+
}
|
|
266
|
+
if (element && element.parentNode) {
|
|
267
|
+
element.parentNode.removeChild(element);
|
|
268
|
+
element = null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/motion.ts
|
|
275
|
+
function prefersReducedMotion() {
|
|
276
|
+
if (typeof window === "undefined") return false;
|
|
277
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
278
|
+
}
|
|
279
|
+
function getAnimationDuration(normalDuration) {
|
|
280
|
+
return prefersReducedMotion() ? "0ms" : normalDuration;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/skip-link.ts
|
|
284
|
+
function createSkipLink(props = {}) {
|
|
285
|
+
const targetId = props.targetId ?? "main-content";
|
|
286
|
+
const label = props.label ?? "Skip to main content";
|
|
287
|
+
return {
|
|
288
|
+
ariaProps: {
|
|
289
|
+
role: "link",
|
|
290
|
+
"aria-label": label
|
|
291
|
+
},
|
|
292
|
+
href: `#${targetId}`,
|
|
293
|
+
label,
|
|
294
|
+
className: "sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:underline"
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export { FOCUSABLE_SELECTOR, Keys, cn, createFocusTrap, createKeyboardHandler, createLiveRegion, createMachine, createSkipLink, cva, generateId, getAnimationDuration, getFocusableElements, mergeAriaProps, prefersReducedMotion, resetIdCounter };
|
|
299
|
+
//# sourceMappingURL=index.js.map
|
|
300
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/aria.ts","../src/keyboard.ts","../src/state-machine.ts","../src/cn.ts","../src/cva.ts","../src/focus-trap.ts","../src/live-region.ts","../src/motion.ts","../src/skip-link.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Merge multiple ARIA prop objects, later values override earlier ones */\nexport function mergeAriaProps(\n ...propSets: Array<Record<string, unknown>>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const props of propSets) {\n for (const [key, value] of Object.entries(props)) {\n if (value !== undefined) {\n result[key] = value\n }\n }\n }\n return result\n}\n\nlet idCounter = 0\n\n/**\n * Generate a unique ID, safe for SSR (deterministic within a render pass).\n * In browsers, uses crypto.randomUUID when available.\n */\nexport function generateId(prefix = 'rfr'): string {\n idCounter++\n return `${prefix}-${idCounter}`\n}\n\n/** Reset the ID counter (useful for tests) */\nexport function resetIdCounter(): void {\n idCounter = 0\n}\n","/** Standard keyboard key constants */\nexport const Keys = {\n Enter: 'Enter',\n Space: ' ',\n Escape: 'Escape',\n Tab: 'Tab',\n ArrowUp: 'ArrowUp',\n ArrowDown: 'ArrowDown',\n ArrowLeft: 'ArrowLeft',\n ArrowRight: 'ArrowRight',\n Home: 'Home',\n End: 'End',\n PageUp: 'PageUp',\n PageDown: 'PageDown',\n Backspace: 'Backspace',\n Delete: 'Delete',\n} as const\n\nexport type KeyboardKey = (typeof Keys)[keyof typeof Keys]\n\n/** Map of key → handler function */\nexport type KeyboardHandlerMap = Partial<\n Record<string, (event: KeyboardEvent) => void>\n>\n\n/** Create a keyboard event handler from a handler map */\nexport function createKeyboardHandler(\n handlers: KeyboardHandlerMap,\n): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n const handler = handlers[event.key]\n if (handler) {\n handler(event)\n }\n }\n}\n","/**\n * Minimal state machine — zero dependencies, < 1KB.\n * Inspired by XState concepts but dramatically simpler.\n */\n\nexport interface MachineConfig<TState extends string, TEvent extends string> {\n initial: TState\n states: Record<TState, {\n on?: Partial<Record<TEvent, TState>>\n }>\n}\n\nexport interface Machine<TState extends string, TEvent extends string> {\n /** Current state */\n state: TState\n /** Send an event to transition */\n send(event: TEvent): void\n /** Subscribe to state changes. Returns unsubscribe function. */\n subscribe(fn: (state: TState) => void): () => void\n /** Check if machine is in a given state */\n matches(state: TState): boolean\n}\n\nexport function createMachine<TState extends string, TEvent extends string>(\n config: MachineConfig<TState, TEvent>,\n): Machine<TState, TEvent> {\n let current = config.initial\n const listeners = new Set<(state: TState) => void>()\n\n return {\n get state() {\n return current\n },\n\n send(event: TEvent) {\n const stateConfig = config.states[current]\n const next = stateConfig?.on?.[event]\n if (next && next !== current) {\n current = next\n for (const fn of listeners) {\n fn(current)\n }\n }\n },\n\n subscribe(fn: (state: TState) => void) {\n listeners.add(fn)\n return () => {\n listeners.delete(fn)\n }\n },\n\n matches(state: TState) {\n return current === state\n },\n }\n}\n","/**\n * Lightweight class name utility — our own implementation.\n * Handles conditional classes, arrays, and falsy values.\n * No external dependencies (no clsx, no tailwind-merge).\n *\n * For Tailwind class conflict resolution (e.g., 'p-2 p-4' → 'p-4'),\n * consumers can use @refraction-ui/tailwind-config which provides\n * a tw-merge-aware variant of this function.\n */\n\ntype ClassValue = string | number | boolean | undefined | null | ClassValue[]\ntype ClassRecord = Record<string, boolean | undefined | null>\n\nexport function cn(...inputs: Array<ClassValue | ClassRecord>): string {\n const classes: string[] = []\n\n for (const input of inputs) {\n if (!input) continue\n\n if (typeof input === 'string') {\n classes.push(input)\n } else if (typeof input === 'number') {\n classes.push(String(input))\n } else if (Array.isArray(input)) {\n const nested = cn(...input)\n if (nested) classes.push(nested)\n } else if (typeof input === 'object') {\n for (const [key, value] of Object.entries(input)) {\n if (value) classes.push(key)\n }\n }\n }\n\n return classes.join(' ')\n}\n","/**\n * Lightweight class-variance-authority alternative — zero dependencies.\n * Creates variant-driven class name functions for components.\n */\n\ntype ClassValue = string | undefined | null | false\n\ninterface VariantConfig {\n [variant: string]: Record<string, string>\n}\n\ninterface CVAConfig<V extends VariantConfig> {\n base?: string\n variants?: V\n defaultVariants?: {\n [K in keyof V]?: keyof V[K]\n }\n compoundVariants?: Array<\n {\n [K in keyof V]?: keyof V[K]\n } & { class: string }\n >\n}\n\ntype VariantProps<V extends VariantConfig> = {\n [K in keyof V]?: keyof V[K]\n}\n\nexport function cva<V extends VariantConfig>(config: CVAConfig<V>) {\n return (props?: VariantProps<V> & { className?: string }): string => {\n const classes: string[] = []\n\n if (config.base) {\n classes.push(config.base)\n }\n\n if (config.variants) {\n for (const [variantKey, variantOptions] of Object.entries(config.variants)) {\n const selectedValue =\n (props as Record<string, unknown> | undefined)?.[variantKey] ??\n config.defaultVariants?.[variantKey]\n\n if (selectedValue != null) {\n const variantClass = (variantOptions as Record<string, string>)[\n selectedValue as string\n ]\n if (variantClass) {\n classes.push(variantClass)\n }\n }\n }\n }\n\n if (config.compoundVariants) {\n for (const compound of config.compoundVariants) {\n const { class: compoundClass, ...conditions } = compound\n let matches = true\n\n for (const [key, value] of Object.entries(conditions)) {\n const propValue =\n (props as Record<string, unknown>)?.[key] ??\n config.defaultVariants?.[key]\n if (propValue !== value) {\n matches = false\n break\n }\n }\n\n if (matches) {\n classes.push(compoundClass as string)\n }\n }\n }\n\n if (props?.className) {\n classes.push(props.className)\n }\n\n return classes.filter(Boolean).join(' ')\n }\n}\n","/**\n * Focus trap utilities — keeps keyboard focus within a container.\n * Used by Dialog, DropdownMenu, and other overlay components.\n * Pure TypeScript, no DOM dependency (accepts element via parameter).\n */\n\n/** Selector for all natively focusable elements */\nexport const FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\n/** Get all focusable elements within a container */\nexport function getFocusableElements(container: Element): Element[] {\n return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR))\n}\n\n/** Configuration for creating a focus trap */\nexport interface FocusTrapConfig {\n /** The container element */\n container: Element\n /** Called to get the initial focus target */\n initialFocus?: () => Element | null\n /** Called when Escape is pressed */\n onEscape?: () => void\n /** Whether to return focus to trigger on deactivate */\n returnFocusOnDeactivate?: boolean\n}\n\n/** A focus trap instance */\nexport interface FocusTrap {\n activate(): void\n deactivate(): void\n isActive(): boolean\n}\n\n/** Create a focus trap that keeps Tab/Shift+Tab within a container */\nexport function createFocusTrap(config: FocusTrapConfig): FocusTrap {\n let active = false\n let previouslyFocused: Element | null = null\n\n function handleKeyDown(event: KeyboardEvent): void {\n if (event.key === 'Escape' && config.onEscape) {\n event.preventDefault()\n config.onEscape()\n return\n }\n\n if (event.key !== 'Tab') return\n\n const focusable = getFocusableElements(config.container)\n if (focusable.length === 0) return\n\n const first = focusable[0] as HTMLElement\n const last = focusable[focusable.length - 1] as HTMLElement\n\n if (event.shiftKey) {\n // Shift+Tab: if on first element, wrap to last\n if (document.activeElement === first) {\n event.preventDefault()\n last.focus()\n }\n } else {\n // Tab: if on last element, wrap to first\n if (document.activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n }\n }\n\n return {\n activate() {\n if (active) return\n active = true\n\n // Remember the currently focused element so we can restore it later\n if (typeof document !== 'undefined') {\n previouslyFocused = document.activeElement\n }\n\n // Set initial focus\n if (config.initialFocus) {\n const target = config.initialFocus()\n if (target && 'focus' in target) {\n ;(target as HTMLElement).focus()\n }\n } else {\n const focusable = getFocusableElements(config.container)\n if (focusable.length > 0) {\n ;(focusable[0] as HTMLElement).focus()\n }\n }\n\n // Attach keydown listener to the container\n config.container.addEventListener(\n 'keydown',\n handleKeyDown as EventListener,\n )\n },\n\n deactivate() {\n if (!active) return\n active = false\n\n config.container.removeEventListener(\n 'keydown',\n handleKeyDown as EventListener,\n )\n\n // Restore focus to the previously focused element\n if (\n config.returnFocusOnDeactivate !== false &&\n previouslyFocused &&\n 'focus' in previouslyFocused\n ) {\n ;(previouslyFocused as HTMLElement).focus()\n }\n },\n\n isActive() {\n return active\n },\n }\n}\n","/**\n * Screen reader announcement utility.\n * Creates and manages an aria-live region for dynamic content updates.\n */\n\nexport interface LiveRegionConfig {\n /** 'polite' (default) or 'assertive' */\n politeness?: 'polite' | 'assertive'\n /** Clear the announcement after this many ms. Default: 5000 */\n clearAfterMs?: number\n}\n\nexport interface LiveRegion {\n announce(message: string): void\n clear(): void\n destroy(): void\n}\n\n/** Create a live region that announces messages to screen readers */\nexport function createLiveRegion(config: LiveRegionConfig = {}): LiveRegion {\n const { politeness = 'polite', clearAfterMs = 5000 } = config\n let element: HTMLElement | null = null\n let clearTimeout: ReturnType<typeof setTimeout> | null = null\n\n function getElement(): HTMLElement {\n if (!element && typeof document !== 'undefined') {\n element = document.createElement('div')\n element.setAttribute('role', 'status')\n element.setAttribute('aria-live', politeness)\n element.setAttribute('aria-atomic', 'true')\n // Visually hidden but accessible to screen readers\n Object.assign(element.style, {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: '0',\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: '0',\n })\n document.body.appendChild(element)\n }\n return element!\n }\n\n return {\n announce(message: string): void {\n const el = getElement()\n if (!el) return\n\n // Clear any pending timeout\n if (clearTimeout !== null) {\n globalThis.clearTimeout(clearTimeout)\n }\n\n // Set text content — screen readers will detect the change via aria-live\n el.textContent = message\n\n if (clearAfterMs > 0) {\n clearTimeout = globalThis.setTimeout(() => {\n el.textContent = ''\n clearTimeout = null\n }, clearAfterMs)\n }\n },\n\n clear(): void {\n if (clearTimeout !== null) {\n globalThis.clearTimeout(clearTimeout)\n clearTimeout = null\n }\n if (element) {\n element.textContent = ''\n }\n },\n\n destroy(): void {\n if (clearTimeout !== null) {\n globalThis.clearTimeout(clearTimeout)\n clearTimeout = null\n }\n if (element && element.parentNode) {\n element.parentNode.removeChild(element)\n element = null\n }\n },\n }\n}\n","/**\n * Reduced motion utilities.\n * Respects user preferences for reduced motion.\n */\n\n/** Check if user prefers reduced motion */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches\n}\n\n/** Get animation duration — returns '0ms' if reduced motion preferred */\nexport function getAnimationDuration(normalDuration: string): string {\n return prefersReducedMotion() ? '0ms' : normalDuration\n}\n","/**\n * Skip-to-content link utility.\n * Provides a keyboard-accessible link that is visually hidden until focused.\n */\n\n/** Props for a skip-to-content link */\nexport interface SkipLinkProps {\n /** ID of the target element. Default: 'main-content' */\n targetId?: string\n /** Label text for the link. Default: 'Skip to main content' */\n label?: string\n}\n\n/** Create props for a skip-to-content link */\nexport function createSkipLink(props: SkipLinkProps = {}): {\n ariaProps: Record<string, string>\n href: string\n label: string\n /** CSS classes — visually hidden until focused */\n className: string\n} {\n const targetId = props.targetId ?? 'main-content'\n const label = props.label ?? 'Skip to main content'\n\n return {\n ariaProps: {\n role: 'link',\n 'aria-label': label,\n },\n href: `#${targetId}`,\n label,\n className:\n 'sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:underline',\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@refraction-ui/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {},
|
|
19
|
+
"devDependencies": {},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/elloloop/refraction-ui.git",
|
|
25
|
+
"directory": "packages/shared"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://elloloop.github.io/refraction-ui/",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"test:coverage": "vitest run --coverage",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"lint": "eslint src --ext .ts",
|
|
36
|
+
"clean": "rm -rf dist"
|
|
37
|
+
}
|
|
38
|
+
}
|