@variiant-ui/react-vite 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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @variiant-ui/react-vite
2
+
3
+ React + Vite component variant tooling with a compact browser overlay and production-safe variant selection.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @variiant-ui/react-vite
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { defineConfig } from "vite";
15
+ import react from "@vitejs/plugin-react";
16
+ import { variantPlugin } from "@variiant-ui/react-vite";
17
+
18
+ export default defineConfig({
19
+ plugins: [variantPlugin(), react()],
20
+ });
21
+ ```
22
+
23
+ Keep normal component imports in the app and place exploratory variants under a mirrored top-level `.variants/` tree. If production should ship something other than `source`, add a `variant.json` manifest to declare the selected implementation.
24
+
25
+ Default shortcuts:
26
+
27
+ - `Cmd/Ctrl + Shift + .` toggles the overlay
28
+ - `Cmd/Ctrl + Alt + ArrowUp/ArrowDown` changes the active mounted component
29
+ - `Cmd/Ctrl + Shift + ArrowLeft/ArrowRight` changes the active variant
30
+
31
+ See the repo root `README.md` and `docs/` for the architecture, proving app, and adoption notes.
@@ -0,0 +1,31 @@
1
+ import { Plugin } from 'vite';
2
+ export { R as RuntimeState, V as VariantDefinition, a as VariantRuntimeController, b as VariantRuntimeSnapshot, c as VariantRuntimeStorage, d as VariantShortcutConfig, e as createVariantRuntimeController, f as defaultShortcuts, s as setVariantShortcuts } from './runtime-api-CpNtY-8z.js';
3
+
4
+ type VariantManifest = {
5
+ source: string;
6
+ exportName?: string;
7
+ selected?: string;
8
+ displayName?: string;
9
+ variants: string[] | Record<string, string>;
10
+ };
11
+ type VariantTargetEntry = {
12
+ exportName: string;
13
+ selected: string;
14
+ displayName: string;
15
+ variantImportPaths: Record<string, string>;
16
+ };
17
+ type VariantRegistryEntry = {
18
+ key: string;
19
+ sourceAbsolutePath: string;
20
+ sourceRelativePath: string;
21
+ sourceImportPath: string;
22
+ hasDefaultExport: boolean;
23
+ targets: Record<string, VariantTargetEntry>;
24
+ };
25
+ type VariantPluginOptions = {
26
+ projectRoot?: string;
27
+ variantsDir?: string;
28
+ };
29
+ declare function variantPlugin(options?: VariantPluginOptions): Plugin;
30
+
31
+ export { type VariantManifest, type VariantPluginOptions, type VariantRegistryEntry, variantPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,626 @@
1
+ // src/plugin.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ var sourceExtensions = [".tsx", ".ts", ".jsx", ".js", ".mts", ".mjs", ".cts", ".cjs"];
5
+ function variantPlugin(options = {}) {
6
+ let projectRoot = "";
7
+ let registry = /* @__PURE__ */ new Map();
8
+ const refreshRegistry = () => {
9
+ projectRoot = options.projectRoot ? path.resolve(options.projectRoot) : process.cwd();
10
+ registry = loadRegistry(projectRoot, options.variantsDir ?? ".variants");
11
+ };
12
+ return {
13
+ name: "variiant-react-vite",
14
+ enforce: "pre",
15
+ configResolved(config) {
16
+ projectRoot = options.projectRoot ? path.resolve(options.projectRoot) : config.root;
17
+ registry = loadRegistry(projectRoot, options.variantsDir ?? ".variants");
18
+ },
19
+ configureServer(server) {
20
+ server.watcher.add(path.join(projectRoot, options.variantsDir ?? ".variants"));
21
+ const reload = () => {
22
+ refreshRegistry();
23
+ server.ws.send({ type: "full-reload" });
24
+ };
25
+ server.watcher.on("add", reload);
26
+ server.watcher.on("change", reload);
27
+ server.watcher.on("unlink", reload);
28
+ },
29
+ async resolveId(source, importer, resolveOptions) {
30
+ if (!importer || source.startsWith("\0")) {
31
+ return null;
32
+ }
33
+ const normalizedImporter = normalizePath(importer);
34
+ if (normalizedImporter.startsWith("\0variant-proxy:") || normalizedImporter.includes("/.variants/")) {
35
+ return null;
36
+ }
37
+ const resolved = await this.resolve(source, importer, {
38
+ ...resolveOptions,
39
+ skipSelf: true
40
+ });
41
+ if (!resolved) {
42
+ return null;
43
+ }
44
+ const entry = registry.get(normalizePath(resolved.id));
45
+ if (!entry) {
46
+ return null;
47
+ }
48
+ return `\0variant-proxy:${encodeURIComponent(entry.sourceAbsolutePath)}`;
49
+ },
50
+ load(id) {
51
+ if (!id.startsWith("\0variant-proxy:")) {
52
+ return null;
53
+ }
54
+ const sourceAbsolutePath = decodeURIComponent(id.slice("\0variant-proxy:".length));
55
+ const entry = registry.get(normalizePath(sourceAbsolutePath));
56
+ if (!entry) {
57
+ throw new Error(`Missing variant registry entry for ${sourceAbsolutePath}`);
58
+ }
59
+ if (this.meta.watchMode) {
60
+ return buildDevelopmentProxyModule(entry);
61
+ }
62
+ return buildProductionProxyModule(entry);
63
+ }
64
+ };
65
+ }
66
+ function loadRegistry(projectRoot, variantsDirName) {
67
+ const variantsRoot = path.join(projectRoot, variantsDirName);
68
+ const results = /* @__PURE__ */ new Map();
69
+ if (!fs.existsSync(variantsRoot)) {
70
+ return results;
71
+ }
72
+ loadConventionalVariants(projectRoot, variantsRoot, results);
73
+ loadManifestVariants(projectRoot, variantsRoot, results);
74
+ return results;
75
+ }
76
+ function loadConventionalVariants(projectRoot, variantsRoot, results) {
77
+ const variantFiles = walkForVariantFiles(variantsRoot);
78
+ for (const variantFilePath of variantFiles) {
79
+ const relativePath = normalizePath(path.relative(variantsRoot, variantFilePath));
80
+ const match = parseConventionalVariantPath(relativePath);
81
+ if (!match) {
82
+ continue;
83
+ }
84
+ const sourceAbsolutePath = normalizePath(path.resolve(projectRoot, match.sourceRelativePath));
85
+ const entry = ensureRegistryEntry(results, projectRoot, sourceAbsolutePath, match.sourceRelativePath);
86
+ const target = ensureTargetEntry(entry, {
87
+ exportName: match.exportName,
88
+ selected: "source",
89
+ displayName: deriveDisplayName(match.sourceRelativePath, match.exportName)
90
+ });
91
+ if (match.variantName in target.variantImportPaths) {
92
+ throw new Error(
93
+ `Duplicate conventional variant "${match.variantName}" for ${match.sourceRelativePath}#${match.exportName}.`
94
+ );
95
+ }
96
+ target.variantImportPaths[match.variantName] = toImportPath(variantFilePath);
97
+ }
98
+ }
99
+ function loadManifestVariants(projectRoot, variantsRoot, results) {
100
+ const configPaths = walkForVariantConfigs(variantsRoot);
101
+ for (const configPath of configPaths) {
102
+ const configDir = path.dirname(configPath);
103
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
104
+ if (!parsed.source || typeof parsed.source !== "string") {
105
+ throw new Error(`Variant config ${configPath} must declare a string "source".`);
106
+ }
107
+ if (!parsed.variants || !Array.isArray(parsed.variants) && typeof parsed.variants !== "object") {
108
+ throw new Error(`Variant config ${configPath} must declare "variants".`);
109
+ }
110
+ const sourceRelativePath = normalizePath(parsed.source);
111
+ const sourceAbsolutePath = normalizePath(path.resolve(projectRoot, sourceRelativePath));
112
+ const exportName = parsed.exportName ?? "default";
113
+ const variantImportPaths = resolveVariantImportPaths(configDir, parsed.variants);
114
+ const selected = parsed.selected ?? "source";
115
+ if (selected !== "source" && !(selected in variantImportPaths)) {
116
+ throw new Error(
117
+ `Variant config ${configPath} selects "${selected}" but no matching variant file was found.`
118
+ );
119
+ }
120
+ const entry = ensureRegistryEntry(results, projectRoot, sourceAbsolutePath, sourceRelativePath);
121
+ const target = ensureTargetEntry(entry, {
122
+ exportName,
123
+ selected,
124
+ displayName: parsed.displayName ?? deriveDisplayName(sourceRelativePath, exportName)
125
+ });
126
+ if (Object.keys(target.variantImportPaths).length > 0) {
127
+ throw new Error(
128
+ `Duplicate variant target for ${sourceRelativePath}#${exportName}. Remove either the manifest or the conventional variant folder.`
129
+ );
130
+ }
131
+ target.selected = selected;
132
+ target.displayName = parsed.displayName ?? target.displayName;
133
+ target.variantImportPaths = variantImportPaths;
134
+ }
135
+ }
136
+ function walkForVariantConfigs(rootDir) {
137
+ const results = [];
138
+ const walk = (currentDir) => {
139
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
140
+ const entryPath = path.join(currentDir, entry.name);
141
+ if (entry.isDirectory()) {
142
+ walk(entryPath);
143
+ continue;
144
+ }
145
+ if (entry.isFile() && entry.name === "variant.json") {
146
+ results.push(entryPath);
147
+ }
148
+ }
149
+ };
150
+ walk(rootDir);
151
+ return results.sort();
152
+ }
153
+ function walkForVariantFiles(rootDir) {
154
+ const results = [];
155
+ const walk = (currentDir) => {
156
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
157
+ const entryPath = path.join(currentDir, entry.name);
158
+ if (entry.isDirectory()) {
159
+ walk(entryPath);
160
+ continue;
161
+ }
162
+ if (entry.isFile() && entry.name !== "variant.json" && sourceExtensions.some((extension) => entry.name.endsWith(extension))) {
163
+ results.push(normalizePath(entryPath));
164
+ }
165
+ }
166
+ };
167
+ walk(rootDir);
168
+ return results.sort();
169
+ }
170
+ function parseConventionalVariantPath(relativePath) {
171
+ const segments = relativePath.split("/").filter(Boolean);
172
+ if (segments.length < 3) {
173
+ return null;
174
+ }
175
+ const sourceSegmentIndex = findSourceSegmentIndex(segments);
176
+ if (sourceSegmentIndex === -1 || sourceSegmentIndex !== segments.length - 3) {
177
+ return null;
178
+ }
179
+ const exportName = segments[sourceSegmentIndex + 1];
180
+ const variantFileName = segments[sourceSegmentIndex + 2];
181
+ const extension = sourceExtensions.find((candidate) => variantFileName.endsWith(candidate));
182
+ if (!extension) {
183
+ return null;
184
+ }
185
+ const variantName = variantFileName.slice(0, -extension.length);
186
+ if (!exportName || !variantName) {
187
+ return null;
188
+ }
189
+ return {
190
+ sourceRelativePath: segments.slice(0, sourceSegmentIndex + 1).join("/"),
191
+ exportName,
192
+ variantName
193
+ };
194
+ }
195
+ function findSourceSegmentIndex(segments) {
196
+ for (let index = segments.length - 3; index >= 0; index -= 1) {
197
+ if (sourceExtensions.some((extension) => segments[index]?.endsWith(extension))) {
198
+ return index;
199
+ }
200
+ }
201
+ return -1;
202
+ }
203
+ function resolveVariantImportPaths(configDir, variants) {
204
+ if (Array.isArray(variants)) {
205
+ return Object.fromEntries(
206
+ variants.map((variantName) => [
207
+ variantName,
208
+ toImportPath(resolveFile(configDir, variantName))
209
+ ])
210
+ );
211
+ }
212
+ return Object.fromEntries(
213
+ Object.entries(variants).map(([variantName, relativePath]) => [
214
+ variantName,
215
+ toImportPath(resolveCustomPath(configDir, relativePath))
216
+ ])
217
+ );
218
+ }
219
+ function resolveFile(configDir, variantName) {
220
+ for (const extension of sourceExtensions) {
221
+ const candidate = path.join(configDir, `${variantName}${extension}`);
222
+ if (fs.existsSync(candidate)) {
223
+ return normalizePath(candidate);
224
+ }
225
+ }
226
+ throw new Error(
227
+ `Could not find variant file for "${variantName}" in ${configDir}. Expected one of ${sourceExtensions.join(", ")}.`
228
+ );
229
+ }
230
+ function resolveCustomPath(configDir, relativePath) {
231
+ const absolute = normalizePath(path.resolve(configDir, relativePath));
232
+ if (fs.existsSync(absolute)) {
233
+ return absolute;
234
+ }
235
+ throw new Error(`Variant file ${relativePath} does not exist in ${configDir}.`);
236
+ }
237
+ function ensureRegistryEntry(results, projectRoot, sourceAbsolutePath, sourceRelativePath) {
238
+ const existing = results.get(sourceAbsolutePath);
239
+ if (existing) {
240
+ return existing;
241
+ }
242
+ const created = {
243
+ key: sourceRelativePath,
244
+ sourceAbsolutePath,
245
+ sourceRelativePath,
246
+ sourceImportPath: toImportPath(sourceAbsolutePath),
247
+ hasDefaultExport: detectHasDefaultExport(sourceAbsolutePath),
248
+ targets: {}
249
+ };
250
+ created.key = normalizePath(path.relative(projectRoot, sourceAbsolutePath));
251
+ results.set(sourceAbsolutePath, created);
252
+ return created;
253
+ }
254
+ function ensureTargetEntry(entry, target) {
255
+ const existing = entry.targets[target.exportName];
256
+ if (existing) {
257
+ return existing;
258
+ }
259
+ const created = {
260
+ exportName: target.exportName,
261
+ selected: target.selected,
262
+ displayName: target.displayName,
263
+ variantImportPaths: {}
264
+ };
265
+ entry.targets[target.exportName] = created;
266
+ return created;
267
+ }
268
+ function buildDevelopmentProxyModule(entry) {
269
+ const importLines = [
270
+ `export * from ${JSON.stringify(entry.sourceImportPath)};`,
271
+ `import * as SourceModule from ${JSON.stringify(entry.sourceImportPath)};`,
272
+ `import { createVariantProxy, installVariantOverlay } from "@variiant-ui/react-vite/runtime";`
273
+ ];
274
+ const outputLines = ["installVariantOverlay();"];
275
+ for (const target of sortTargets(entry.targets)) {
276
+ if (target.exportName === "default" && !entry.hasDefaultExport) {
277
+ throw new Error(
278
+ `Variant target ${entry.sourceRelativePath}#default requires the source module to have a default export.`
279
+ );
280
+ }
281
+ const targetIdentifier = toIdentifier(target.exportName === "default" ? "default" : target.exportName);
282
+ const variantsIdentifier = `${targetIdentifier}Variants`;
283
+ const proxyIdentifier = `${targetIdentifier}VariantProxy`;
284
+ const variantLines = [` source: ${getSourceAccessExpression(target.exportName)},`];
285
+ for (const [variantName, importPath] of Object.entries(target.variantImportPaths)) {
286
+ const identifier = toIdentifier(`${target.exportName}-${variantName}`);
287
+ importLines.push(`import ${identifier} from ${JSON.stringify(importPath)};`);
288
+ variantLines.push(` ${JSON.stringify(variantName)}: ${identifier},`);
289
+ }
290
+ outputLines.push(`const ${variantsIdentifier} = {`);
291
+ outputLines.push(variantLines.join("\n"));
292
+ outputLines.push("};");
293
+ outputLines.push("");
294
+ outputLines.push(`const ${proxyIdentifier} = createVariantProxy({`);
295
+ outputLines.push(` sourceId: ${JSON.stringify(buildSourceId(entry.sourceRelativePath, target.exportName))},`);
296
+ outputLines.push(` displayName: ${JSON.stringify(target.displayName)},`);
297
+ outputLines.push(` selected: ${JSON.stringify(target.selected)},`);
298
+ outputLines.push(` variants: ${variantsIdentifier},`);
299
+ outputLines.push("});");
300
+ outputLines.push("");
301
+ outputLines.push(buildVariantExport(target.exportName, proxyIdentifier));
302
+ outputLines.push("");
303
+ }
304
+ if (entry.hasDefaultExport && !("default" in entry.targets)) {
305
+ outputLines.push("export default SourceModule.default;");
306
+ }
307
+ return `${importLines.join("\n")}
308
+
309
+ ${outputLines.join("\n")}
310
+ `;
311
+ }
312
+ function buildProductionProxyModule(entry) {
313
+ const lines = [`export * from ${JSON.stringify(entry.sourceImportPath)};`];
314
+ const defaultTarget = entry.targets.default;
315
+ if (defaultTarget?.selected && defaultTarget.selected !== "source") {
316
+ const selectedImport = defaultTarget.variantImportPaths[defaultTarget.selected];
317
+ lines.push(`export { default } from ${JSON.stringify(selectedImport)};`);
318
+ } else if (entry.hasDefaultExport) {
319
+ lines.push(`export { default } from ${JSON.stringify(entry.sourceImportPath)};`);
320
+ }
321
+ for (const target of sortTargets(entry.targets)) {
322
+ if (target.exportName === "default" || target.selected === "source") {
323
+ continue;
324
+ }
325
+ const selectedImport = target.variantImportPaths[target.selected];
326
+ lines.push(`export { default as ${target.exportName} } from ${JSON.stringify(selectedImport)};`);
327
+ }
328
+ return `${lines.join("\n")}
329
+ `;
330
+ }
331
+ function sortTargets(targets) {
332
+ return Object.values(targets).sort((left, right) => {
333
+ if (left.exportName === "default") {
334
+ return -1;
335
+ }
336
+ if (right.exportName === "default") {
337
+ return 1;
338
+ }
339
+ return left.exportName.localeCompare(right.exportName);
340
+ });
341
+ }
342
+ function getSourceAccessExpression(exportName) {
343
+ return exportName === "default" ? "SourceModule.default" : `SourceModule[${JSON.stringify(exportName)}]`;
344
+ }
345
+ function deriveDisplayName(sourceRelativePath, exportName) {
346
+ if (exportName !== "default") {
347
+ return exportName;
348
+ }
349
+ const extension = sourceExtensions.find((candidate) => sourceRelativePath.endsWith(candidate)) ?? "";
350
+ const fileName = path.posix.basename(sourceRelativePath, extension);
351
+ const displayName = fileName === "index" ? path.posix.basename(path.posix.dirname(sourceRelativePath)) : fileName;
352
+ return humanize(displayName);
353
+ }
354
+ function humanize(value) {
355
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
356
+ }
357
+ function detectHasDefaultExport(sourceAbsolutePath) {
358
+ if (!fs.existsSync(sourceAbsolutePath)) {
359
+ return false;
360
+ }
361
+ const source = fs.readFileSync(sourceAbsolutePath, "utf8");
362
+ return /\bexport\s+default\b/.test(source) || /\bas\s+default\b/.test(source);
363
+ }
364
+ function toIdentifier(name) {
365
+ const safeName = name.replace(/[^a-zA-Z0-9]+(.)/g, (_match, nextCharacter) => nextCharacter.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
366
+ return `${safeName.charAt(0).toUpperCase() + safeName.slice(1)}Variant`;
367
+ }
368
+ function normalizePath(value) {
369
+ return value.split(path.sep).join(path.posix.sep);
370
+ }
371
+ function toImportPath(value) {
372
+ return normalizePath(value);
373
+ }
374
+ function buildSourceId(sourceRelativePath, exportName) {
375
+ return exportName === "default" ? sourceRelativePath : `${sourceRelativePath}#${exportName}`;
376
+ }
377
+ function buildVariantExport(exportName, proxyIdentifier) {
378
+ return exportName === "default" ? `export default ${proxyIdentifier};` : `export { ${proxyIdentifier} as ${exportName} };`;
379
+ }
380
+
381
+ // src/runtime-core.ts
382
+ var defaultShortcuts = {
383
+ toggleOverlay: ["meta+shift+.", "ctrl+shift+."],
384
+ nextComponent: ["meta+alt+arrowdown", "ctrl+alt+arrowdown"],
385
+ previousComponent: ["meta+alt+arrowup", "ctrl+alt+arrowup"],
386
+ nextVariant: ["meta+shift+arrowright", "ctrl+shift+arrowright"],
387
+ previousVariant: ["meta+shift+arrowleft", "ctrl+shift+arrowleft"],
388
+ closeOverlay: "escape"
389
+ };
390
+ function rotate(items, currentIndex, direction) {
391
+ if (items.length === 0) {
392
+ return null;
393
+ }
394
+ const nextIndex = (currentIndex + direction + items.length) % items.length;
395
+ return items[nextIndex] ?? null;
396
+ }
397
+ function getMountedComponents(state) {
398
+ return state.components.filter((component) => component.mountedCount > 0);
399
+ }
400
+ function getActiveMountedComponent(state) {
401
+ const mounted = getMountedComponents(state);
402
+ return mounted.find((component) => component.sourceId === state.activeSourceId) ?? mounted[0] ?? null;
403
+ }
404
+ function createVariantRuntimeController(options = {}) {
405
+ const storage = options.storage;
406
+ const listeners = /* @__PURE__ */ new Set();
407
+ const definitions = /* @__PURE__ */ new Map();
408
+ const mountedCounts = /* @__PURE__ */ new Map();
409
+ const state = {
410
+ overlayOpen: false,
411
+ activeSourceId: null,
412
+ components: [],
413
+ selections: storage?.readSelections() ?? {},
414
+ shortcutConfig: {
415
+ ...defaultShortcuts,
416
+ ...storage?.readShortcutOverrides() ?? {}
417
+ }
418
+ };
419
+ const emit = () => {
420
+ state.components = Array.from(definitions.values()).map((definition) => ({
421
+ ...definition,
422
+ selected: state.selections[definition.sourceId] ?? definition.selected,
423
+ mountedCount: mountedCounts.get(definition.sourceId) ?? 0
424
+ })).sort((left, right) => left.displayName.localeCompare(right.displayName));
425
+ const mounted = getMountedComponents(state);
426
+ if (!mounted.some((component) => component.sourceId === state.activeSourceId)) {
427
+ state.activeSourceId = mounted[0]?.sourceId ?? null;
428
+ }
429
+ for (const listener of listeners) {
430
+ listener();
431
+ }
432
+ };
433
+ const persistSelections = () => {
434
+ storage?.writeSelections(state.selections);
435
+ };
436
+ const persistShortcuts = () => {
437
+ storage?.writeShortcutOverrides(state.shortcutConfig);
438
+ };
439
+ const moveActiveComponent = (direction) => {
440
+ const mounted = getMountedComponents(state);
441
+ if (mounted.length === 0) {
442
+ return;
443
+ }
444
+ const currentIndex = mounted.findIndex((component) => component.sourceId === state.activeSourceId);
445
+ const current = currentIndex >= 0 ? currentIndex : 0;
446
+ const next = rotate(mounted, current, direction);
447
+ if (!next) {
448
+ return;
449
+ }
450
+ state.activeSourceId = next.sourceId;
451
+ emit();
452
+ };
453
+ const moveVariant = (direction) => {
454
+ const active = getActiveMountedComponent(state);
455
+ if (!active) {
456
+ return;
457
+ }
458
+ const currentIndex = active.variantNames.findIndex(
459
+ (variantName) => variantName === (state.selections[active.sourceId] ?? active.selected)
460
+ );
461
+ const current = currentIndex >= 0 ? currentIndex : 0;
462
+ const next = rotate(active.variantNames, current, direction);
463
+ if (!next) {
464
+ return;
465
+ }
466
+ state.selections[active.sourceId] = next;
467
+ persistSelections();
468
+ emit();
469
+ };
470
+ return {
471
+ define(definition) {
472
+ const existing = definitions.get(definition.sourceId);
473
+ definitions.set(definition.sourceId, {
474
+ ...definition,
475
+ displayName: definition.displayName || existing?.displayName || definition.sourceId,
476
+ variantNames: Array.from(
477
+ /* @__PURE__ */ new Set([...existing?.variantNames ?? [], ...definition.variantNames])
478
+ )
479
+ });
480
+ if (!(definition.sourceId in state.selections)) {
481
+ state.selections[definition.sourceId] = definition.selected;
482
+ persistSelections();
483
+ }
484
+ emit();
485
+ },
486
+ mount(sourceId) {
487
+ mountedCounts.set(sourceId, (mountedCounts.get(sourceId) ?? 0) + 1);
488
+ emit();
489
+ return () => {
490
+ mountedCounts.set(sourceId, Math.max((mountedCounts.get(sourceId) ?? 1) - 1, 0));
491
+ emit();
492
+ };
493
+ },
494
+ getSelectedVariant(sourceId, fallback) {
495
+ return state.selections[sourceId] ?? fallback;
496
+ },
497
+ getSnapshot() {
498
+ return {
499
+ overlayOpen: state.overlayOpen,
500
+ activeSourceId: state.activeSourceId,
501
+ components: state.components,
502
+ selections: state.selections,
503
+ shortcutConfig: state.shortcutConfig
504
+ };
505
+ },
506
+ subscribe(listener) {
507
+ listeners.add(listener);
508
+ return () => {
509
+ listeners.delete(listener);
510
+ };
511
+ },
512
+ actions: {
513
+ toggleOverlay() {
514
+ state.overlayOpen = !state.overlayOpen;
515
+ emit();
516
+ },
517
+ closeOverlay() {
518
+ if (!state.overlayOpen) {
519
+ return;
520
+ }
521
+ state.overlayOpen = false;
522
+ emit();
523
+ },
524
+ nextComponent() {
525
+ moveActiveComponent(1);
526
+ },
527
+ previousComponent() {
528
+ moveActiveComponent(-1);
529
+ },
530
+ nextVariant() {
531
+ moveVariant(1);
532
+ },
533
+ previousVariant() {
534
+ moveVariant(-1);
535
+ },
536
+ selectComponent(sourceId) {
537
+ state.activeSourceId = sourceId;
538
+ emit();
539
+ },
540
+ selectVariant(sourceId, variantName) {
541
+ const component = state.components.find((candidate) => candidate.sourceId === sourceId);
542
+ if (!component || !component.variantNames.includes(variantName)) {
543
+ return;
544
+ }
545
+ state.selections[sourceId] = variantName;
546
+ persistSelections();
547
+ emit();
548
+ },
549
+ configureShortcuts(overrides) {
550
+ if (!overrides) {
551
+ return;
552
+ }
553
+ state.shortcutConfig = {
554
+ ...state.shortcutConfig,
555
+ ...overrides
556
+ };
557
+ persistShortcuts();
558
+ emit();
559
+ }
560
+ }
561
+ };
562
+ }
563
+
564
+ // src/runtime-singleton.ts
565
+ var storageKey = "variant:component-selections";
566
+ var shortcutStorageKey = "variant:shortcut-config";
567
+ var globalControllerKey = "__variant_runtime_controller__";
568
+ function createBrowserStorage() {
569
+ return {
570
+ readSelections() {
571
+ if (typeof window === "undefined") {
572
+ return {};
573
+ }
574
+ try {
575
+ const raw = window.localStorage.getItem(storageKey);
576
+ return raw ? JSON.parse(raw) : {};
577
+ } catch {
578
+ return {};
579
+ }
580
+ },
581
+ writeSelections(selections) {
582
+ if (typeof window === "undefined") {
583
+ return;
584
+ }
585
+ window.localStorage.setItem(storageKey, JSON.stringify(selections));
586
+ },
587
+ readShortcutOverrides() {
588
+ if (typeof window === "undefined") {
589
+ return {};
590
+ }
591
+ try {
592
+ const raw = window.localStorage.getItem(shortcutStorageKey);
593
+ return raw ? JSON.parse(raw) : {};
594
+ } catch {
595
+ return {};
596
+ }
597
+ },
598
+ writeShortcutOverrides(shortcutConfig) {
599
+ if (typeof window === "undefined") {
600
+ return;
601
+ }
602
+ window.localStorage.setItem(shortcutStorageKey, JSON.stringify(shortcutConfig));
603
+ }
604
+ };
605
+ }
606
+ function getVariantRuntimeController() {
607
+ const target = globalThis;
608
+ if (!target[globalControllerKey]) {
609
+ target[globalControllerKey] = createVariantRuntimeController({
610
+ storage: createBrowserStorage()
611
+ });
612
+ }
613
+ return target[globalControllerKey];
614
+ }
615
+
616
+ // src/runtime-api.ts
617
+ function setVariantShortcuts(shortcuts) {
618
+ getVariantRuntimeController().actions.configureShortcuts(shortcuts);
619
+ }
620
+ export {
621
+ createVariantRuntimeController,
622
+ defaultShortcuts,
623
+ setVariantShortcuts,
624
+ variantPlugin
625
+ };
626
+ //# sourceMappingURL=index.js.map