-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseVariants.ts
More file actions
101 lines (85 loc) · 3.11 KB
/
Copy pathuseVariants.ts
File metadata and controls
101 lines (85 loc) · 3.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import {
reactive,
readonly,
provide,
inject,
onUnmounted,
type InjectionKey,
type DeepReadonly,
} from "vue";
interface VariantConfig<T extends readonly unknown[] = readonly unknown[]> {
key: string;
variants: T;
}
type VariantsSetup = Record<string, VariantConfig>;
type InferState<T extends VariantsSetup> = {
[K in keyof T]: T[K]["variants"][number];
};
function shouldIgnoreTarget(target: EventTarget | null): boolean {
const el = target as HTMLElement | null;
if (!el) return false;
const tag = el.tagName;
return el.isContentEditable || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
}
/**
* Creates a typed provider/consumer pair.
*
* @example
* // variants.ts
* export const { provideAppVariants, useAppVariants } = createVariants({
* TaskCard: { key: "1", variants: [true, false] as const },
* TaskWidth: { key: "2", variants: ["w-32", "w-35"] as const },
* });
*
* // App.vue — call once in setup()
* import { provideAppVariants } from "@/variants.ts";
* provideAppVariants();
*
* // Any descendant component
* import { useAppVariants } from "@/variants";
* const { variants } = useAppVariants();
* // variants.TaskCard → boolean
* // variants.TaskWidth → "w-32" | "w-35"
*/
export function createVariants<T extends VariantsSetup>(config: T) {
// Each createVariants call gets its own unique key —
// multiple independent variant systems can coexist in the same app.
const injectionKey: InjectionKey<DeepReadonly<InferState<T>>> = Symbol("variants");
function provideAppVariants(): { variants: DeepReadonly<InferState<T>> } {
const state = reactive<Record<string, unknown>>({});
for (const [name, options] of Object.entries(config)) {
if (options.variants.length > 0) {
state[name] = options.variants[0];
}
}
function handleKeydown(event: KeyboardEvent): void {
if (shouldIgnoreTarget(event.target) || event.repeat) return;
for (const [name, options] of Object.entries(config)) {
if (event.key === options.key) {
event.preventDefault();
const arr = options.variants as unknown[];
if (arr.length === 0) continue;
const idx = arr.indexOf(state[name]);
state[name] = arr[(idx + 1) % arr.length];
console.log(`[useVariants] ${name} → ${state[name]}`);
}
}
}
window.addEventListener("keydown", handleKeydown);
onUnmounted(() => window.removeEventListener("keydown", handleKeydown));
const readonlyState = readonly(state) as DeepReadonly<InferState<T>>;
provide(injectionKey, readonlyState);
// Return state directly for use in App.vue itself
return { variants: readonlyState };
}
function useAppVariants(): { variants: DeepReadonly<InferState<T>> } {
const variants = inject(injectionKey);
if (import.meta.env.DEV && variants === undefined) {
console.warn(
`[useAppVariants] Provider not found. Make sure provideAppVariants() is called in App.vue setup().`,
);
}
return { variants: variants as DeepReadonly<InferState<T>> };
}
return { provideAppVariants, useAppVariants };
}