@whichly/core 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,71 @@
1
+ # @whichly/core
2
+
3
+ Framework-agnostic DOM runtime for Whichly variant previews.
4
+
5
+ `@whichly/core` scans server-rendered HTML for Whichly data attributes, keeps variant state in the URL, applies the active variant to the DOM, and renders the floating picker inside a Shadow DOM.
6
+
7
+ It is intended for framework integrations such as `@whichly/astro`, but can also be used directly on any static or server-rendered page.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm i @whichly/core
13
+ # or
14
+ pnpm add @whichly/core
15
+ ```
16
+
17
+ ## Markup contract
18
+
19
+ ```html
20
+ <div data-whichly-block="hero">
21
+ <div data-whichly-variant="simple">
22
+ <h1>Simple hero</h1>
23
+ </div>
24
+
25
+ <div data-whichly-variant="bold">
26
+ <h1>Bold hero</h1>
27
+ </div>
28
+ </div>
29
+ ```
30
+
31
+ The first variant is active by default. Active variants use `display: contents`; inactive variants use `display: none`.
32
+
33
+ ## Runtime
34
+
35
+ ```ts
36
+ import { createWhichlyRuntime } from "@whichly/core";
37
+
38
+ const runtime = createWhichlyRuntime();
39
+ ```
40
+
41
+ Options:
42
+
43
+ ```ts
44
+ createWhichlyRuntime({
45
+ floating: true,
46
+ param: "vp",
47
+ root: document,
48
+ });
49
+ ```
50
+
51
+ ## URL state
52
+
53
+ Variant selections are encoded in the URL:
54
+
55
+ ```txt
56
+ ?vp=hero:bold,pricing:enterprise
57
+ ```
58
+
59
+ ## API
60
+
61
+ ```ts
62
+ runtime.scan();
63
+ runtime.select("hero", "bold");
64
+ runtime.reset("hero");
65
+ runtime.subscribe(() => {
66
+ console.log(runtime.state);
67
+ });
68
+ runtime.destroy();
69
+ ```
70
+
71
+ The runtime also observes DOM mutations, so blocks added later by islands or client-rendered components are picked up automatically.
package/dist/index.cjs ADDED
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const DEFAULT_URL_PARAM = "vp";
4
+ function decodePart(value) {
5
+ try {
6
+ return decodeURIComponent(value);
7
+ } catch {
8
+ return value;
9
+ }
10
+ }
11
+ function encodePart(value) {
12
+ return encodeURIComponent(value);
13
+ }
14
+ function parseUrl(href = location.href, param = DEFAULT_URL_PARAM) {
15
+ try {
16
+ const raw = new URL(href).searchParams.get(param);
17
+ if (!raw) return {};
18
+ const state = {};
19
+ for (const pair of raw.split(",")) {
20
+ const idx = pair.indexOf(":");
21
+ if (idx <= 0) continue;
22
+ const block = decodePart(pair.slice(0, idx));
23
+ const variant = decodePart(pair.slice(idx + 1));
24
+ if (block && variant) state[block] = variant;
25
+ }
26
+ return state;
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+ function serializeUrl(state) {
32
+ return Object.entries(state).map(([block, variant]) => `${encodePart(block)}:${encodePart(variant)}`).join(",");
33
+ }
34
+ function syncURL(state, param = DEFAULT_URL_PARAM) {
35
+ try {
36
+ const url = new URL(location.href);
37
+ const serialized = serializeUrl(state);
38
+ if (serialized) {
39
+ url.searchParams.set(param, serialized);
40
+ } else {
41
+ url.searchParams.delete(param);
42
+ }
43
+ history.replaceState(history.state, "", url.toString());
44
+ } catch {
45
+ }
46
+ }
47
+ const BLOCK_SELECTOR = "[data-whichly-block], [data-vp-block]";
48
+ const VARIANT_SELECTOR = "[data-whichly-variant], [data-vp-variant]";
49
+ const pickerCss = `
50
+ :host { all: initial; color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
51
+ * { box-sizing: border-box; }
52
+ .whichly-wrap { position: fixed; left: 0; right: 0; bottom: 5vh; z-index: 2147483647; display: flex; justify-content: center; padding: 0 16px; pointer-events: none; }
53
+ .whichly-dock { pointer-events: auto; display: flex; min-width: 0; max-width: 100%; align-items: center; gap: 14px; border-radius: 999px; background: #09090b; color: #fafafa; padding: 6px 8px 6px 16px; box-shadow: 0 16px 48px rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.1); }
54
+ .whichly-title { font-size: 14px; font-weight: 700; white-space: nowrap; }
55
+ .whichly-list { display: flex; min-width: 0; flex: 1; align-items: center; gap: 10px; overflow-x: auto; padding: 2px 0; scrollbar-width: none; }
56
+ .whichly-list::-webkit-scrollbar { display: none; }
57
+ .whichly-stepper { display: inline-flex; flex-shrink: 0; align-items: center; gap: 4px; border-radius: 999px; border: 1px solid rgba(255,255,255,.12); background: #18181b; padding: 4px 6px 4px 12px; }
58
+ .whichly-block { max-width: 96px; overflow: hidden; padding-right: 6px; color: #a1a1aa; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
59
+ .whichly-current { min-width: 80px; max-width: 160px; overflow: hidden; padding: 0 2px; text-align: center; color: #fafafa; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
60
+ button { appearance: none; border: 0; border-radius: 999px; background: transparent; color: #fafafa; cursor: pointer; display: inline-grid; height: 28px; min-width: 28px; place-items: center; padding: 0 8px; font: inherit; }
61
+ button:hover { background: rgba(255,255,255,.09); }
62
+ button.whichly-reset:hover { background: rgba(239,68,68,.2); color: #fecaca; }
63
+ button.whichly-collapse { flex-shrink: 0; }
64
+ .whichly-fab { position: fixed; right: 16px; bottom: 5vh; z-index: 2147483647; width: 48px; height: 48px; border: 1px solid rgba(255,255,255,.1); background: #18181b; box-shadow: 0 16px 48px rgba(0,0,0,.35); pointer-events: auto; }
65
+ @media (max-width: 640px) { .whichly-wrap { bottom: max(12px, env(safe-area-inset-bottom)); padding: 0 8px; } .whichly-title { display: none; } .whichly-dock { gap: 8px; padding: 8px; } .whichly-fab { bottom: max(12px, env(safe-area-inset-bottom)); } }
66
+ `;
67
+ function getBlockName(el) {
68
+ return el.dataset.whichlyBlock ?? el.dataset.vpBlock;
69
+ }
70
+ function getVariantName(el) {
71
+ return el.dataset.whichlyVariant ?? el.dataset.vpVariant;
72
+ }
73
+ function wrap(list, current, dir) {
74
+ const len = list.length;
75
+ if (len === 0) return current;
76
+ const idx = list.indexOf(current);
77
+ const start = idx === -1 ? 0 : idx;
78
+ return list[(start + dir + len) % len] ?? current;
79
+ }
80
+ function isNestedInOtherBlock(variant, block) {
81
+ var _a;
82
+ const parentBlock = (_a = variant.parentElement) == null ? void 0 : _a.closest(BLOCK_SELECTOR);
83
+ return parentBlock !== block;
84
+ }
85
+ function createWhichlyRuntime(options = {}) {
86
+ const root = options.root ?? document;
87
+ const param = options.param ?? DEFAULT_URL_PARAM;
88
+ const floating = options.floating ?? true;
89
+ const blocks = /* @__PURE__ */ new Map();
90
+ let state = typeof location === "undefined" ? {} : parseUrl(location.href, param);
91
+ const listeners = /* @__PURE__ */ new Set();
92
+ let observer;
93
+ let host;
94
+ let shadow;
95
+ let collapsed = false;
96
+ function emit() {
97
+ for (const listener of listeners) listener();
98
+ }
99
+ function getActive(block) {
100
+ const variants = blocks.get(block);
101
+ if (!variants || variants.length === 0) return void 0;
102
+ const preferred = state[block];
103
+ if (preferred && variants.includes(preferred)) return preferred;
104
+ return variants[0];
105
+ }
106
+ function applyState() {
107
+ for (const blockEl of root.querySelectorAll(BLOCK_SELECTOR)) {
108
+ const block = getBlockName(blockEl);
109
+ if (!block) continue;
110
+ const active = getActive(block);
111
+ for (const variantEl of blockEl.querySelectorAll(VARIANT_SELECTOR)) {
112
+ if (isNestedInOtherBlock(variantEl, blockEl)) continue;
113
+ const variant = getVariantName(variantEl);
114
+ variantEl.style.display = variant === active ? "contents" : "none";
115
+ }
116
+ }
117
+ }
118
+ function renderPicker() {
119
+ if (!shadow) return;
120
+ const existing = shadow.querySelector(".whichly-wrap, .whichly-fab");
121
+ existing == null ? void 0 : existing.remove();
122
+ if (blocks.size === 0) return;
123
+ if (collapsed) {
124
+ const button = document.createElement("button");
125
+ button.className = "whichly-fab";
126
+ button.type = "button";
127
+ button.setAttribute("aria-label", "Expand Whichly picker");
128
+ button.textContent = "⌁";
129
+ button.addEventListener("click", () => {
130
+ collapsed = false;
131
+ renderPicker();
132
+ });
133
+ shadow.appendChild(button);
134
+ return;
135
+ }
136
+ const wrapEl = document.createElement("div");
137
+ wrapEl.className = "whichly-wrap";
138
+ const dock = document.createElement("div");
139
+ dock.className = "whichly-dock";
140
+ dock.setAttribute("role", "toolbar");
141
+ dock.setAttribute("aria-label", "Whichly variant picker");
142
+ const title = document.createElement("div");
143
+ title.className = "whichly-title";
144
+ title.textContent = "Whichly";
145
+ dock.appendChild(title);
146
+ const list = document.createElement("div");
147
+ list.className = "whichly-list";
148
+ for (const [block, variants] of blocks) {
149
+ const current = getActive(block) ?? "";
150
+ const stepper = document.createElement("div");
151
+ stepper.className = "whichly-stepper";
152
+ const label = document.createElement("div");
153
+ label.className = "whichly-block";
154
+ label.title = block;
155
+ label.textContent = block;
156
+ stepper.appendChild(label);
157
+ const prev = document.createElement("button");
158
+ prev.type = "button";
159
+ prev.setAttribute("aria-label", `Previous ${block} variant`);
160
+ prev.textContent = "‹";
161
+ prev.addEventListener("click", () => runtime.select(block, wrap(variants, current, -1)));
162
+ stepper.appendChild(prev);
163
+ const value = document.createElement("div");
164
+ value.className = "whichly-current";
165
+ value.title = current;
166
+ value.setAttribute("aria-live", "polite");
167
+ value.textContent = current;
168
+ stepper.appendChild(value);
169
+ const next = document.createElement("button");
170
+ next.type = "button";
171
+ next.setAttribute("aria-label", `Next ${block} variant`);
172
+ next.textContent = "›";
173
+ next.addEventListener("click", () => runtime.select(block, wrap(variants, current, 1)));
174
+ stepper.appendChild(next);
175
+ const reset = document.createElement("button");
176
+ reset.type = "button";
177
+ reset.className = "whichly-reset";
178
+ reset.setAttribute("aria-label", `Reset ${block} variant`);
179
+ reset.textContent = "↺";
180
+ reset.addEventListener("click", () => runtime.reset(block));
181
+ stepper.appendChild(reset);
182
+ list.appendChild(stepper);
183
+ }
184
+ dock.appendChild(list);
185
+ const collapse = document.createElement("button");
186
+ collapse.type = "button";
187
+ collapse.className = "whichly-collapse";
188
+ collapse.setAttribute("aria-label", "Minimize picker");
189
+ collapse.textContent = "−";
190
+ collapse.addEventListener("click", () => {
191
+ collapsed = true;
192
+ renderPicker();
193
+ });
194
+ dock.appendChild(collapse);
195
+ wrapEl.appendChild(dock);
196
+ shadow.appendChild(wrapEl);
197
+ }
198
+ function scan() {
199
+ blocks.clear();
200
+ for (const blockEl of root.querySelectorAll(BLOCK_SELECTOR)) {
201
+ const block = getBlockName(blockEl);
202
+ if (!block) continue;
203
+ const variants = blocks.get(block) ?? [];
204
+ for (const variantEl of blockEl.querySelectorAll(VARIANT_SELECTOR)) {
205
+ if (isNestedInOtherBlock(variantEl, blockEl)) continue;
206
+ const variant = getVariantName(variantEl);
207
+ if (variant && !variants.includes(variant)) variants.push(variant);
208
+ }
209
+ if (variants.length > 0) blocks.set(block, variants);
210
+ }
211
+ applyState();
212
+ renderPicker();
213
+ emit();
214
+ }
215
+ const runtime = {
216
+ get blocks() {
217
+ return new Map(blocks);
218
+ },
219
+ get state() {
220
+ return { ...state };
221
+ },
222
+ scan,
223
+ select(block, variant) {
224
+ var _a;
225
+ if (!((_a = blocks.get(block)) == null ? void 0 : _a.includes(variant))) return;
226
+ state = { ...state, [block]: variant };
227
+ syncURL(state, param);
228
+ applyState();
229
+ renderPicker();
230
+ emit();
231
+ },
232
+ reset(block) {
233
+ var _a;
234
+ const first = (_a = blocks.get(block)) == null ? void 0 : _a[0];
235
+ if (!first) return;
236
+ state = { ...state, [block]: first };
237
+ syncURL(state, param);
238
+ applyState();
239
+ renderPicker();
240
+ emit();
241
+ },
242
+ subscribe(listener) {
243
+ listeners.add(listener);
244
+ return () => listeners.delete(listener);
245
+ },
246
+ destroy() {
247
+ observer == null ? void 0 : observer.disconnect();
248
+ host == null ? void 0 : host.remove();
249
+ listeners.clear();
250
+ blocks.clear();
251
+ }
252
+ };
253
+ if (floating && typeof document !== "undefined") {
254
+ host = document.createElement("div");
255
+ host.id = "__whichly_picker_host";
256
+ shadow = host.attachShadow({ mode: "open" });
257
+ const style = document.createElement("style");
258
+ style.textContent = pickerCss;
259
+ shadow.appendChild(style);
260
+ document.body.appendChild(host);
261
+ }
262
+ scan();
263
+ if (typeof MutationObserver !== "undefined") {
264
+ observer = new MutationObserver(() => scan());
265
+ observer.observe(root, {
266
+ childList: true,
267
+ subtree: true,
268
+ attributes: true,
269
+ attributeFilter: [
270
+ "data-whichly-block",
271
+ "data-whichly-variant",
272
+ "data-vp-block",
273
+ "data-vp-variant"
274
+ ]
275
+ });
276
+ }
277
+ return runtime;
278
+ }
279
+ exports.DEFAULT_URL_PARAM = DEFAULT_URL_PARAM;
280
+ exports.createWhichlyRuntime = createWhichlyRuntime;
281
+ exports.parseUrl = parseUrl;
282
+ exports.serializeUrl = serializeUrl;
283
+ exports.syncURL = syncURL;
284
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/url.ts","../src/runtime.ts"],"sourcesContent":["export type State = Record<string, string>;\n\nexport const DEFAULT_URL_PARAM = \"vp\";\n\nfunction decodePart(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction encodePart(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport function parseUrl(href: string = location.href, param = DEFAULT_URL_PARAM): State {\n try {\n const raw = new URL(href).searchParams.get(param);\n if (!raw) return {};\n\n const state: State = {};\n for (const pair of raw.split(\",\")) {\n const idx = pair.indexOf(\":\");\n if (idx <= 0) continue;\n const block = decodePart(pair.slice(0, idx));\n const variant = decodePart(pair.slice(idx + 1));\n if (block && variant) state[block] = variant;\n }\n return state;\n } catch {\n return {};\n }\n}\n\nexport function serializeUrl(state: State): string {\n return Object.entries(state)\n .map(([block, variant]) => `${encodePart(block)}:${encodePart(variant)}`)\n .join(\",\");\n}\n\nexport function syncURL(state: State, param = DEFAULT_URL_PARAM): void {\n try {\n const url = new URL(location.href);\n const serialized = serializeUrl(state);\n if (serialized) {\n url.searchParams.set(param, serialized);\n } else {\n url.searchParams.delete(param);\n }\n history.replaceState(history.state, \"\", url.toString());\n } catch {\n // location can be cross-origin restricted in sandboxed iframes; ignore\n }\n}\n","import { DEFAULT_URL_PARAM, type State, parseUrl, syncURL } from \"./url\";\n\nexport interface WhichlyRuntimeOptions {\n root?: ParentNode;\n floating?: boolean;\n param?: string;\n}\n\nexport interface WhichlyRuntime {\n readonly blocks: Map<string, string[]>;\n readonly state: State;\n scan(): void;\n select(block: string, variant: string): void;\n reset(block: string): void;\n subscribe(listener: () => void): () => void;\n destroy(): void;\n}\n\ntype Listener = () => void;\n\nconst BLOCK_SELECTOR = \"[data-whichly-block], [data-vp-block]\";\nconst VARIANT_SELECTOR = \"[data-whichly-variant], [data-vp-variant]\";\n\nconst pickerCss = `\n:host { all: initial; color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; }\n* { box-sizing: border-box; }\n.whichly-wrap { position: fixed; left: 0; right: 0; bottom: 5vh; z-index: 2147483647; display: flex; justify-content: center; padding: 0 16px; pointer-events: none; }\n.whichly-dock { pointer-events: auto; display: flex; min-width: 0; max-width: 100%; align-items: center; gap: 14px; border-radius: 999px; background: #09090b; color: #fafafa; padding: 6px 8px 6px 16px; box-shadow: 0 16px 48px rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.1); }\n.whichly-title { font-size: 14px; font-weight: 700; white-space: nowrap; }\n.whichly-list { display: flex; min-width: 0; flex: 1; align-items: center; gap: 10px; overflow-x: auto; padding: 2px 0; scrollbar-width: none; }\n.whichly-list::-webkit-scrollbar { display: none; }\n.whichly-stepper { display: inline-flex; flex-shrink: 0; align-items: center; gap: 4px; border-radius: 999px; border: 1px solid rgba(255,255,255,.12); background: #18181b; padding: 4px 6px 4px 12px; }\n.whichly-block { max-width: 96px; overflow: hidden; padding-right: 6px; color: #a1a1aa; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }\n.whichly-current { min-width: 80px; max-width: 160px; overflow: hidden; padding: 0 2px; text-align: center; color: #fafafa; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }\nbutton { appearance: none; border: 0; border-radius: 999px; background: transparent; color: #fafafa; cursor: pointer; display: inline-grid; height: 28px; min-width: 28px; place-items: center; padding: 0 8px; font: inherit; }\nbutton:hover { background: rgba(255,255,255,.09); }\nbutton.whichly-reset:hover { background: rgba(239,68,68,.2); color: #fecaca; }\nbutton.whichly-collapse { flex-shrink: 0; }\n.whichly-fab { position: fixed; right: 16px; bottom: 5vh; z-index: 2147483647; width: 48px; height: 48px; border: 1px solid rgba(255,255,255,.1); background: #18181b; box-shadow: 0 16px 48px rgba(0,0,0,.35); pointer-events: auto; }\n@media (max-width: 640px) { .whichly-wrap { bottom: max(12px, env(safe-area-inset-bottom)); padding: 0 8px; } .whichly-title { display: none; } .whichly-dock { gap: 8px; padding: 8px; } .whichly-fab { bottom: max(12px, env(safe-area-inset-bottom)); } }\n`;\n\nfunction getBlockName(el: Element): string | undefined {\n return (el as HTMLElement).dataset.whichlyBlock ?? (el as HTMLElement).dataset.vpBlock;\n}\n\nfunction getVariantName(el: Element): string | undefined {\n return (el as HTMLElement).dataset.whichlyVariant ?? (el as HTMLElement).dataset.vpVariant;\n}\n\nfunction wrap(list: string[], current: string, dir: 1 | -1): string {\n const len = list.length;\n if (len === 0) return current;\n const idx = list.indexOf(current);\n const start = idx === -1 ? 0 : idx;\n return list[(start + dir + len) % len] ?? current;\n}\n\nfunction isNestedInOtherBlock(variant: Element, block: Element): boolean {\n const parentBlock = variant.parentElement?.closest(BLOCK_SELECTOR);\n return parentBlock !== block;\n}\n\nexport function createWhichlyRuntime(options: WhichlyRuntimeOptions = {}): WhichlyRuntime {\n const root = options.root ?? document;\n const param = options.param ?? DEFAULT_URL_PARAM;\n const floating = options.floating ?? true;\n const blocks = new Map<string, string[]>();\n let state: State = typeof location === \"undefined\" ? {} : parseUrl(location.href, param);\n const listeners = new Set<Listener>();\n let observer: MutationObserver | undefined;\n let host: HTMLDivElement | undefined;\n let shadow: ShadowRoot | undefined;\n let collapsed = false;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function getActive(block: string): string | undefined {\n const variants = blocks.get(block);\n if (!variants || variants.length === 0) return undefined;\n const preferred = state[block];\n if (preferred && variants.includes(preferred)) return preferred;\n return variants[0];\n }\n\n function applyState(): void {\n for (const blockEl of root.querySelectorAll<HTMLElement>(BLOCK_SELECTOR)) {\n const block = getBlockName(blockEl);\n if (!block) continue;\n const active = getActive(block);\n for (const variantEl of blockEl.querySelectorAll<HTMLElement>(VARIANT_SELECTOR)) {\n if (isNestedInOtherBlock(variantEl, blockEl)) continue;\n const variant = getVariantName(variantEl);\n variantEl.style.display = variant === active ? \"contents\" : \"none\";\n }\n }\n }\n\n function renderPicker(): void {\n if (!shadow) return;\n const existing = shadow.querySelector(\".whichly-wrap, .whichly-fab\");\n existing?.remove();\n\n if (blocks.size === 0) return;\n\n if (collapsed) {\n const button = document.createElement(\"button\");\n button.className = \"whichly-fab\";\n button.type = \"button\";\n button.setAttribute(\"aria-label\", \"Expand Whichly picker\");\n button.textContent = \"⌁\";\n button.addEventListener(\"click\", () => {\n collapsed = false;\n renderPicker();\n });\n shadow.appendChild(button);\n return;\n }\n\n const wrapEl = document.createElement(\"div\");\n wrapEl.className = \"whichly-wrap\";\n\n const dock = document.createElement(\"div\");\n dock.className = \"whichly-dock\";\n dock.setAttribute(\"role\", \"toolbar\");\n dock.setAttribute(\"aria-label\", \"Whichly variant picker\");\n\n const title = document.createElement(\"div\");\n title.className = \"whichly-title\";\n title.textContent = \"Whichly\";\n dock.appendChild(title);\n\n const list = document.createElement(\"div\");\n list.className = \"whichly-list\";\n\n for (const [block, variants] of blocks) {\n const current = getActive(block) ?? \"\";\n const stepper = document.createElement(\"div\");\n stepper.className = \"whichly-stepper\";\n\n const label = document.createElement(\"div\");\n label.className = \"whichly-block\";\n label.title = block;\n label.textContent = block;\n stepper.appendChild(label);\n\n const prev = document.createElement(\"button\");\n prev.type = \"button\";\n prev.setAttribute(\"aria-label\", `Previous ${block} variant`);\n prev.textContent = \"‹\";\n prev.addEventListener(\"click\", () => runtime.select(block, wrap(variants, current, -1)));\n stepper.appendChild(prev);\n\n const value = document.createElement(\"div\");\n value.className = \"whichly-current\";\n value.title = current;\n value.setAttribute(\"aria-live\", \"polite\");\n value.textContent = current;\n stepper.appendChild(value);\n\n const next = document.createElement(\"button\");\n next.type = \"button\";\n next.setAttribute(\"aria-label\", `Next ${block} variant`);\n next.textContent = \"›\";\n next.addEventListener(\"click\", () => runtime.select(block, wrap(variants, current, 1)));\n stepper.appendChild(next);\n\n const reset = document.createElement(\"button\");\n reset.type = \"button\";\n reset.className = \"whichly-reset\";\n reset.setAttribute(\"aria-label\", `Reset ${block} variant`);\n reset.textContent = \"↺\";\n reset.addEventListener(\"click\", () => runtime.reset(block));\n stepper.appendChild(reset);\n\n list.appendChild(stepper);\n }\n\n dock.appendChild(list);\n\n const collapse = document.createElement(\"button\");\n collapse.type = \"button\";\n collapse.className = \"whichly-collapse\";\n collapse.setAttribute(\"aria-label\", \"Minimize picker\");\n collapse.textContent = \"−\";\n collapse.addEventListener(\"click\", () => {\n collapsed = true;\n renderPicker();\n });\n dock.appendChild(collapse);\n\n wrapEl.appendChild(dock);\n shadow.appendChild(wrapEl);\n }\n\n function scan(): void {\n blocks.clear();\n for (const blockEl of root.querySelectorAll<HTMLElement>(BLOCK_SELECTOR)) {\n const block = getBlockName(blockEl);\n if (!block) continue;\n const variants = blocks.get(block) ?? [];\n for (const variantEl of blockEl.querySelectorAll<HTMLElement>(VARIANT_SELECTOR)) {\n if (isNestedInOtherBlock(variantEl, blockEl)) continue;\n const variant = getVariantName(variantEl);\n if (variant && !variants.includes(variant)) variants.push(variant);\n }\n if (variants.length > 0) blocks.set(block, variants);\n }\n applyState();\n renderPicker();\n emit();\n }\n\n const runtime: WhichlyRuntime = {\n get blocks() {\n return new Map(blocks);\n },\n get state() {\n return { ...state };\n },\n scan,\n select(block, variant) {\n if (!blocks.get(block)?.includes(variant)) return;\n state = { ...state, [block]: variant };\n syncURL(state, param);\n applyState();\n renderPicker();\n emit();\n },\n reset(block) {\n const first = blocks.get(block)?.[0];\n if (!first) return;\n state = { ...state, [block]: first };\n syncURL(state, param);\n applyState();\n renderPicker();\n emit();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n destroy() {\n observer?.disconnect();\n host?.remove();\n listeners.clear();\n blocks.clear();\n },\n };\n\n if (floating && typeof document !== \"undefined\") {\n host = document.createElement(\"div\");\n host.id = \"__whichly_picker_host\";\n shadow = host.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n style.textContent = pickerCss;\n shadow.appendChild(style);\n document.body.appendChild(host);\n }\n\n scan();\n\n if (typeof MutationObserver !== \"undefined\") {\n observer = new MutationObserver(() => scan());\n observer.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\n \"data-whichly-block\",\n \"data-whichly-variant\",\n \"data-vp-block\",\n \"data-vp-variant\",\n ],\n });\n }\n\n return runtime;\n}\n"],"names":[],"mappings":";;AAEO,MAAM,oBAAoB;AAEjC,SAAS,WAAW,OAAuB;AACzC,MAAI;AACF,WAAO,mBAAmB,KAAK;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WAAW,OAAuB;AACzC,SAAO,mBAAmB,KAAK;AACjC;AAEO,SAAS,SAAS,OAAe,SAAS,MAAM,QAAQ,mBAA0B;AACvF,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,IAAI,EAAE,aAAa,IAAI,KAAK;AAChD,QAAI,CAAC,IAAK,QAAO,CAAA;AAEjB,UAAM,QAAe,CAAA;AACrB,eAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,YAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,UAAI,OAAO,EAAG;AACd,YAAM,QAAQ,WAAW,KAAK,MAAM,GAAG,GAAG,CAAC;AAC3C,YAAM,UAAU,WAAW,KAAK,MAAM,MAAM,CAAC,CAAC;AAC9C,UAAI,SAAS,QAAS,OAAM,KAAK,IAAI;AAAA,IACvC;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,CAAA;AAAA,EACT;AACF;AAEO,SAAS,aAAa,OAAsB;AACjD,SAAO,OAAO,QAAQ,KAAK,EACxB,IAAI,CAAC,CAAC,OAAO,OAAO,MAAM,GAAG,WAAW,KAAK,CAAC,IAAI,WAAW,OAAO,CAAC,EAAE,EACvE,KAAK,GAAG;AACb;AAEO,SAAS,QAAQ,OAAc,QAAQ,mBAAyB;AACrE,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,SAAS,IAAI;AACjC,UAAM,aAAa,aAAa,KAAK;AACrC,QAAI,YAAY;AACd,UAAI,aAAa,IAAI,OAAO,UAAU;AAAA,IACxC,OAAO;AACL,UAAI,aAAa,OAAO,KAAK;AAAA,IAC/B;AACA,YAAQ,aAAa,QAAQ,OAAO,IAAI,IAAI,UAAU;AAAA,EACxD,QAAQ;AAAA,EAER;AACF;AClCA,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AAEzB,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBlB,SAAS,aAAa,IAAiC;AACrD,SAAQ,GAAmB,QAAQ,gBAAiB,GAAmB,QAAQ;AACjF;AAEA,SAAS,eAAe,IAAiC;AACvD,SAAQ,GAAmB,QAAQ,kBAAmB,GAAmB,QAAQ;AACnF;AAEA,SAAS,KAAK,MAAgB,SAAiB,KAAqB;AAClE,QAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,MAAM,KAAK,QAAQ,OAAO;AAChC,QAAM,QAAQ,QAAQ,KAAK,IAAI;AAC/B,SAAO,MAAM,QAAQ,MAAM,OAAO,GAAG,KAAK;AAC5C;AAEA,SAAS,qBAAqB,SAAkB,OAAyB;;AACvE,QAAM,eAAc,aAAQ,kBAAR,mBAAuB,QAAQ;AACnD,SAAO,gBAAgB;AACzB;AAEO,SAAS,qBAAqB,UAAiC,IAAoB;AACxF,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,6BAAa,IAAA;AACnB,MAAI,QAAe,OAAO,aAAa,cAAc,CAAA,IAAK,SAAS,SAAS,MAAM,KAAK;AACvF,QAAM,gCAAgB,IAAA;AACtB,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI,YAAY;AAEhB,WAAS,OAAa;AACpB,eAAW,YAAY,UAAW,UAAA;AAAA,EACpC;AAEA,WAAS,UAAU,OAAmC;AACpD,UAAM,WAAW,OAAO,IAAI,KAAK;AACjC,QAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO;AAC/C,UAAM,YAAY,MAAM,KAAK;AAC7B,QAAI,aAAa,SAAS,SAAS,SAAS,EAAG,QAAO;AACtD,WAAO,SAAS,CAAC;AAAA,EACnB;AAEA,WAAS,aAAmB;AAC1B,eAAW,WAAW,KAAK,iBAA8B,cAAc,GAAG;AACxE,YAAM,QAAQ,aAAa,OAAO;AAClC,UAAI,CAAC,MAAO;AACZ,YAAM,SAAS,UAAU,KAAK;AAC9B,iBAAW,aAAa,QAAQ,iBAA8B,gBAAgB,GAAG;AAC/E,YAAI,qBAAqB,WAAW,OAAO,EAAG;AAC9C,cAAM,UAAU,eAAe,SAAS;AACxC,kBAAU,MAAM,UAAU,YAAY,SAAS,aAAa;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AAEA,WAAS,eAAqB;AAC5B,QAAI,CAAC,OAAQ;AACb,UAAM,WAAW,OAAO,cAAc,6BAA6B;AACnE,yCAAU;AAEV,QAAI,OAAO,SAAS,EAAG;AAEvB,QAAI,WAAW;AACb,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,aAAO,YAAY;AACnB,aAAO,OAAO;AACd,aAAO,aAAa,cAAc,uBAAuB;AACzD,aAAO,cAAc;AACrB,aAAO,iBAAiB,SAAS,MAAM;AACrC,oBAAY;AACZ,qBAAA;AAAA,MACF,CAAC;AACD,aAAO,YAAY,MAAM;AACzB;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,cAAc,KAAK;AAC3C,WAAO,YAAY;AAEnB,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,YAAY;AACjB,SAAK,aAAa,QAAQ,SAAS;AACnC,SAAK,aAAa,cAAc,wBAAwB;AAExD,UAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,UAAM,YAAY;AAClB,UAAM,cAAc;AACpB,SAAK,YAAY,KAAK;AAEtB,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,YAAY;AAEjB,eAAW,CAAC,OAAO,QAAQ,KAAK,QAAQ;AACtC,YAAM,UAAU,UAAU,KAAK,KAAK;AACpC,YAAM,UAAU,SAAS,cAAc,KAAK;AAC5C,cAAQ,YAAY;AAEpB,YAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,YAAM,YAAY;AAClB,YAAM,QAAQ;AACd,YAAM,cAAc;AACpB,cAAQ,YAAY,KAAK;AAEzB,YAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,WAAK,OAAO;AACZ,WAAK,aAAa,cAAc,YAAY,KAAK,UAAU;AAC3D,WAAK,cAAc;AACnB,WAAK,iBAAiB,SAAS,MAAM,QAAQ,OAAO,OAAO,KAAK,UAAU,SAAS,EAAE,CAAC,CAAC;AACvF,cAAQ,YAAY,IAAI;AAExB,YAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,YAAM,YAAY;AAClB,YAAM,QAAQ;AACd,YAAM,aAAa,aAAa,QAAQ;AACxC,YAAM,cAAc;AACpB,cAAQ,YAAY,KAAK;AAEzB,YAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,WAAK,OAAO;AACZ,WAAK,aAAa,cAAc,QAAQ,KAAK,UAAU;AACvD,WAAK,cAAc;AACnB,WAAK,iBAAiB,SAAS,MAAM,QAAQ,OAAO,OAAO,KAAK,UAAU,SAAS,CAAC,CAAC,CAAC;AACtF,cAAQ,YAAY,IAAI;AAExB,YAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,YAAM,OAAO;AACb,YAAM,YAAY;AAClB,YAAM,aAAa,cAAc,SAAS,KAAK,UAAU;AACzD,YAAM,cAAc;AACpB,YAAM,iBAAiB,SAAS,MAAM,QAAQ,MAAM,KAAK,CAAC;AAC1D,cAAQ,YAAY,KAAK;AAEzB,WAAK,YAAY,OAAO;AAAA,IAC1B;AAEA,SAAK,YAAY,IAAI;AAErB,UAAM,WAAW,SAAS,cAAc,QAAQ;AAChD,aAAS,OAAO;AAChB,aAAS,YAAY;AACrB,aAAS,aAAa,cAAc,iBAAiB;AACrD,aAAS,cAAc;AACvB,aAAS,iBAAiB,SAAS,MAAM;AACvC,kBAAY;AACZ,mBAAA;AAAA,IACF,CAAC;AACD,SAAK,YAAY,QAAQ;AAEzB,WAAO,YAAY,IAAI;AACvB,WAAO,YAAY,MAAM;AAAA,EAC3B;AAEA,WAAS,OAAa;AACpB,WAAO,MAAA;AACP,eAAW,WAAW,KAAK,iBAA8B,cAAc,GAAG;AACxE,YAAM,QAAQ,aAAa,OAAO;AAClC,UAAI,CAAC,MAAO;AACZ,YAAM,WAAW,OAAO,IAAI,KAAK,KAAK,CAAA;AACtC,iBAAW,aAAa,QAAQ,iBAA8B,gBAAgB,GAAG;AAC/E,YAAI,qBAAqB,WAAW,OAAO,EAAG;AAC9C,cAAM,UAAU,eAAe,SAAS;AACxC,YAAI,WAAW,CAAC,SAAS,SAAS,OAAO,EAAG,UAAS,KAAK,OAAO;AAAA,MACnE;AACA,UAAI,SAAS,SAAS,EAAG,QAAO,IAAI,OAAO,QAAQ;AAAA,IACrD;AACA,eAAA;AACA,iBAAA;AACA,SAAA;AAAA,EACF;AAEA,QAAM,UAA0B;AAAA,IAC9B,IAAI,SAAS;AACX,aAAO,IAAI,IAAI,MAAM;AAAA,IACvB;AAAA,IACA,IAAI,QAAQ;AACV,aAAO,EAAE,GAAG,MAAA;AAAA,IACd;AAAA,IACA;AAAA,IACA,OAAO,OAAO,SAAS;;AACrB,UAAI,GAAC,YAAO,IAAI,KAAK,MAAhB,mBAAmB,SAAS,UAAU;AAC3C,cAAQ,EAAE,GAAG,OAAO,CAAC,KAAK,GAAG,QAAA;AAC7B,cAAQ,OAAO,KAAK;AACpB,iBAAA;AACA,mBAAA;AACA,WAAA;AAAA,IACF;AAAA,IACA,MAAM,OAAO;;AACX,YAAM,SAAQ,YAAO,IAAI,KAAK,MAAhB,mBAAoB;AAClC,UAAI,CAAC,MAAO;AACZ,cAAQ,EAAE,GAAG,OAAO,CAAC,KAAK,GAAG,MAAA;AAC7B,cAAQ,OAAO,KAAK;AACpB,iBAAA;AACA,mBAAA;AACA,WAAA;AAAA,IACF;AAAA,IACA,UAAU,UAAU;AAClB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM,UAAU,OAAO,QAAQ;AAAA,IACxC;AAAA,IACA,UAAU;AACR,2CAAU;AACV,mCAAM;AACN,gBAAU,MAAA;AACV,aAAO,MAAA;AAAA,IACT;AAAA,EAAA;AAGF,MAAI,YAAY,OAAO,aAAa,aAAa;AAC/C,WAAO,SAAS,cAAc,KAAK;AACnC,SAAK,KAAK;AACV,aAAS,KAAK,aAAa,EAAE,MAAM,QAAQ;AAC3C,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,cAAc;AACpB,WAAO,YAAY,KAAK;AACxB,aAAS,KAAK,YAAY,IAAI;AAAA,EAChC;AAEA,OAAA;AAEA,MAAI,OAAO,qBAAqB,aAAa;AAC3C,eAAW,IAAI,iBAAiB,MAAM,MAAM;AAC5C,aAAS,QAAQ,MAAM;AAAA,MACrB,WAAW;AAAA,MACX,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,iBAAiB;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IACF,CACD;AAAA,EACH;AAEA,SAAO;AACT;;;;;;"}
@@ -0,0 +1,29 @@
1
+ export declare function createWhichlyRuntime(options?: WhichlyRuntimeOptions): WhichlyRuntime;
2
+
3
+ export declare const DEFAULT_URL_PARAM = "vp";
4
+
5
+ export declare function parseUrl(href?: string, param?: string): State;
6
+
7
+ export declare function serializeUrl(state: State): string;
8
+
9
+ export declare type State = Record<string, string>;
10
+
11
+ export declare function syncURL(state: State, param?: string): void;
12
+
13
+ export declare interface WhichlyRuntime {
14
+ readonly blocks: Map<string, string[]>;
15
+ readonly state: State;
16
+ scan(): void;
17
+ select(block: string, variant: string): void;
18
+ reset(block: string): void;
19
+ subscribe(listener: () => void): () => void;
20
+ destroy(): void;
21
+ }
22
+
23
+ export declare interface WhichlyRuntimeOptions {
24
+ root?: ParentNode;
25
+ floating?: boolean;
26
+ param?: string;
27
+ }
28
+
29
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ const DEFAULT_URL_PARAM = "vp";
2
+ function decodePart(value) {
3
+ try {
4
+ return decodeURIComponent(value);
5
+ } catch {
6
+ return value;
7
+ }
8
+ }
9
+ function encodePart(value) {
10
+ return encodeURIComponent(value);
11
+ }
12
+ function parseUrl(href = location.href, param = DEFAULT_URL_PARAM) {
13
+ try {
14
+ const raw = new URL(href).searchParams.get(param);
15
+ if (!raw) return {};
16
+ const state = {};
17
+ for (const pair of raw.split(",")) {
18
+ const idx = pair.indexOf(":");
19
+ if (idx <= 0) continue;
20
+ const block = decodePart(pair.slice(0, idx));
21
+ const variant = decodePart(pair.slice(idx + 1));
22
+ if (block && variant) state[block] = variant;
23
+ }
24
+ return state;
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+ function serializeUrl(state) {
30
+ return Object.entries(state).map(([block, variant]) => `${encodePart(block)}:${encodePart(variant)}`).join(",");
31
+ }
32
+ function syncURL(state, param = DEFAULT_URL_PARAM) {
33
+ try {
34
+ const url = new URL(location.href);
35
+ const serialized = serializeUrl(state);
36
+ if (serialized) {
37
+ url.searchParams.set(param, serialized);
38
+ } else {
39
+ url.searchParams.delete(param);
40
+ }
41
+ history.replaceState(history.state, "", url.toString());
42
+ } catch {
43
+ }
44
+ }
45
+ const BLOCK_SELECTOR = "[data-whichly-block], [data-vp-block]";
46
+ const VARIANT_SELECTOR = "[data-whichly-variant], [data-vp-variant]";
47
+ const pickerCss = `
48
+ :host { all: initial; color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
49
+ * { box-sizing: border-box; }
50
+ .whichly-wrap { position: fixed; left: 0; right: 0; bottom: 5vh; z-index: 2147483647; display: flex; justify-content: center; padding: 0 16px; pointer-events: none; }
51
+ .whichly-dock { pointer-events: auto; display: flex; min-width: 0; max-width: 100%; align-items: center; gap: 14px; border-radius: 999px; background: #09090b; color: #fafafa; padding: 6px 8px 6px 16px; box-shadow: 0 16px 48px rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.1); }
52
+ .whichly-title { font-size: 14px; font-weight: 700; white-space: nowrap; }
53
+ .whichly-list { display: flex; min-width: 0; flex: 1; align-items: center; gap: 10px; overflow-x: auto; padding: 2px 0; scrollbar-width: none; }
54
+ .whichly-list::-webkit-scrollbar { display: none; }
55
+ .whichly-stepper { display: inline-flex; flex-shrink: 0; align-items: center; gap: 4px; border-radius: 999px; border: 1px solid rgba(255,255,255,.12); background: #18181b; padding: 4px 6px 4px 12px; }
56
+ .whichly-block { max-width: 96px; overflow: hidden; padding-right: 6px; color: #a1a1aa; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
57
+ .whichly-current { min-width: 80px; max-width: 160px; overflow: hidden; padding: 0 2px; text-align: center; color: #fafafa; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
58
+ button { appearance: none; border: 0; border-radius: 999px; background: transparent; color: #fafafa; cursor: pointer; display: inline-grid; height: 28px; min-width: 28px; place-items: center; padding: 0 8px; font: inherit; }
59
+ button:hover { background: rgba(255,255,255,.09); }
60
+ button.whichly-reset:hover { background: rgba(239,68,68,.2); color: #fecaca; }
61
+ button.whichly-collapse { flex-shrink: 0; }
62
+ .whichly-fab { position: fixed; right: 16px; bottom: 5vh; z-index: 2147483647; width: 48px; height: 48px; border: 1px solid rgba(255,255,255,.1); background: #18181b; box-shadow: 0 16px 48px rgba(0,0,0,.35); pointer-events: auto; }
63
+ @media (max-width: 640px) { .whichly-wrap { bottom: max(12px, env(safe-area-inset-bottom)); padding: 0 8px; } .whichly-title { display: none; } .whichly-dock { gap: 8px; padding: 8px; } .whichly-fab { bottom: max(12px, env(safe-area-inset-bottom)); } }
64
+ `;
65
+ function getBlockName(el) {
66
+ return el.dataset.whichlyBlock ?? el.dataset.vpBlock;
67
+ }
68
+ function getVariantName(el) {
69
+ return el.dataset.whichlyVariant ?? el.dataset.vpVariant;
70
+ }
71
+ function wrap(list, current, dir) {
72
+ const len = list.length;
73
+ if (len === 0) return current;
74
+ const idx = list.indexOf(current);
75
+ const start = idx === -1 ? 0 : idx;
76
+ return list[(start + dir + len) % len] ?? current;
77
+ }
78
+ function isNestedInOtherBlock(variant, block) {
79
+ var _a;
80
+ const parentBlock = (_a = variant.parentElement) == null ? void 0 : _a.closest(BLOCK_SELECTOR);
81
+ return parentBlock !== block;
82
+ }
83
+ function createWhichlyRuntime(options = {}) {
84
+ const root = options.root ?? document;
85
+ const param = options.param ?? DEFAULT_URL_PARAM;
86
+ const floating = options.floating ?? true;
87
+ const blocks = /* @__PURE__ */ new Map();
88
+ let state = typeof location === "undefined" ? {} : parseUrl(location.href, param);
89
+ const listeners = /* @__PURE__ */ new Set();
90
+ let observer;
91
+ let host;
92
+ let shadow;
93
+ let collapsed = false;
94
+ function emit() {
95
+ for (const listener of listeners) listener();
96
+ }
97
+ function getActive(block) {
98
+ const variants = blocks.get(block);
99
+ if (!variants || variants.length === 0) return void 0;
100
+ const preferred = state[block];
101
+ if (preferred && variants.includes(preferred)) return preferred;
102
+ return variants[0];
103
+ }
104
+ function applyState() {
105
+ for (const blockEl of root.querySelectorAll(BLOCK_SELECTOR)) {
106
+ const block = getBlockName(blockEl);
107
+ if (!block) continue;
108
+ const active = getActive(block);
109
+ for (const variantEl of blockEl.querySelectorAll(VARIANT_SELECTOR)) {
110
+ if (isNestedInOtherBlock(variantEl, blockEl)) continue;
111
+ const variant = getVariantName(variantEl);
112
+ variantEl.style.display = variant === active ? "contents" : "none";
113
+ }
114
+ }
115
+ }
116
+ function renderPicker() {
117
+ if (!shadow) return;
118
+ const existing = shadow.querySelector(".whichly-wrap, .whichly-fab");
119
+ existing == null ? void 0 : existing.remove();
120
+ if (blocks.size === 0) return;
121
+ if (collapsed) {
122
+ const button = document.createElement("button");
123
+ button.className = "whichly-fab";
124
+ button.type = "button";
125
+ button.setAttribute("aria-label", "Expand Whichly picker");
126
+ button.textContent = "⌁";
127
+ button.addEventListener("click", () => {
128
+ collapsed = false;
129
+ renderPicker();
130
+ });
131
+ shadow.appendChild(button);
132
+ return;
133
+ }
134
+ const wrapEl = document.createElement("div");
135
+ wrapEl.className = "whichly-wrap";
136
+ const dock = document.createElement("div");
137
+ dock.className = "whichly-dock";
138
+ dock.setAttribute("role", "toolbar");
139
+ dock.setAttribute("aria-label", "Whichly variant picker");
140
+ const title = document.createElement("div");
141
+ title.className = "whichly-title";
142
+ title.textContent = "Whichly";
143
+ dock.appendChild(title);
144
+ const list = document.createElement("div");
145
+ list.className = "whichly-list";
146
+ for (const [block, variants] of blocks) {
147
+ const current = getActive(block) ?? "";
148
+ const stepper = document.createElement("div");
149
+ stepper.className = "whichly-stepper";
150
+ const label = document.createElement("div");
151
+ label.className = "whichly-block";
152
+ label.title = block;
153
+ label.textContent = block;
154
+ stepper.appendChild(label);
155
+ const prev = document.createElement("button");
156
+ prev.type = "button";
157
+ prev.setAttribute("aria-label", `Previous ${block} variant`);
158
+ prev.textContent = "‹";
159
+ prev.addEventListener("click", () => runtime.select(block, wrap(variants, current, -1)));
160
+ stepper.appendChild(prev);
161
+ const value = document.createElement("div");
162
+ value.className = "whichly-current";
163
+ value.title = current;
164
+ value.setAttribute("aria-live", "polite");
165
+ value.textContent = current;
166
+ stepper.appendChild(value);
167
+ const next = document.createElement("button");
168
+ next.type = "button";
169
+ next.setAttribute("aria-label", `Next ${block} variant`);
170
+ next.textContent = "›";
171
+ next.addEventListener("click", () => runtime.select(block, wrap(variants, current, 1)));
172
+ stepper.appendChild(next);
173
+ const reset = document.createElement("button");
174
+ reset.type = "button";
175
+ reset.className = "whichly-reset";
176
+ reset.setAttribute("aria-label", `Reset ${block} variant`);
177
+ reset.textContent = "↺";
178
+ reset.addEventListener("click", () => runtime.reset(block));
179
+ stepper.appendChild(reset);
180
+ list.appendChild(stepper);
181
+ }
182
+ dock.appendChild(list);
183
+ const collapse = document.createElement("button");
184
+ collapse.type = "button";
185
+ collapse.className = "whichly-collapse";
186
+ collapse.setAttribute("aria-label", "Minimize picker");
187
+ collapse.textContent = "−";
188
+ collapse.addEventListener("click", () => {
189
+ collapsed = true;
190
+ renderPicker();
191
+ });
192
+ dock.appendChild(collapse);
193
+ wrapEl.appendChild(dock);
194
+ shadow.appendChild(wrapEl);
195
+ }
196
+ function scan() {
197
+ blocks.clear();
198
+ for (const blockEl of root.querySelectorAll(BLOCK_SELECTOR)) {
199
+ const block = getBlockName(blockEl);
200
+ if (!block) continue;
201
+ const variants = blocks.get(block) ?? [];
202
+ for (const variantEl of blockEl.querySelectorAll(VARIANT_SELECTOR)) {
203
+ if (isNestedInOtherBlock(variantEl, blockEl)) continue;
204
+ const variant = getVariantName(variantEl);
205
+ if (variant && !variants.includes(variant)) variants.push(variant);
206
+ }
207
+ if (variants.length > 0) blocks.set(block, variants);
208
+ }
209
+ applyState();
210
+ renderPicker();
211
+ emit();
212
+ }
213
+ const runtime = {
214
+ get blocks() {
215
+ return new Map(blocks);
216
+ },
217
+ get state() {
218
+ return { ...state };
219
+ },
220
+ scan,
221
+ select(block, variant) {
222
+ var _a;
223
+ if (!((_a = blocks.get(block)) == null ? void 0 : _a.includes(variant))) return;
224
+ state = { ...state, [block]: variant };
225
+ syncURL(state, param);
226
+ applyState();
227
+ renderPicker();
228
+ emit();
229
+ },
230
+ reset(block) {
231
+ var _a;
232
+ const first = (_a = blocks.get(block)) == null ? void 0 : _a[0];
233
+ if (!first) return;
234
+ state = { ...state, [block]: first };
235
+ syncURL(state, param);
236
+ applyState();
237
+ renderPicker();
238
+ emit();
239
+ },
240
+ subscribe(listener) {
241
+ listeners.add(listener);
242
+ return () => listeners.delete(listener);
243
+ },
244
+ destroy() {
245
+ observer == null ? void 0 : observer.disconnect();
246
+ host == null ? void 0 : host.remove();
247
+ listeners.clear();
248
+ blocks.clear();
249
+ }
250
+ };
251
+ if (floating && typeof document !== "undefined") {
252
+ host = document.createElement("div");
253
+ host.id = "__whichly_picker_host";
254
+ shadow = host.attachShadow({ mode: "open" });
255
+ const style = document.createElement("style");
256
+ style.textContent = pickerCss;
257
+ shadow.appendChild(style);
258
+ document.body.appendChild(host);
259
+ }
260
+ scan();
261
+ if (typeof MutationObserver !== "undefined") {
262
+ observer = new MutationObserver(() => scan());
263
+ observer.observe(root, {
264
+ childList: true,
265
+ subtree: true,
266
+ attributes: true,
267
+ attributeFilter: [
268
+ "data-whichly-block",
269
+ "data-whichly-variant",
270
+ "data-vp-block",
271
+ "data-vp-variant"
272
+ ]
273
+ });
274
+ }
275
+ return runtime;
276
+ }
277
+ export {
278
+ DEFAULT_URL_PARAM,
279
+ createWhichlyRuntime,
280
+ parseUrl,
281
+ serializeUrl,
282
+ syncURL
283
+ };
284
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/url.ts","../src/runtime.ts"],"sourcesContent":["export type State = Record<string, string>;\n\nexport const DEFAULT_URL_PARAM = \"vp\";\n\nfunction decodePart(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction encodePart(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport function parseUrl(href: string = location.href, param = DEFAULT_URL_PARAM): State {\n try {\n const raw = new URL(href).searchParams.get(param);\n if (!raw) return {};\n\n const state: State = {};\n for (const pair of raw.split(\",\")) {\n const idx = pair.indexOf(\":\");\n if (idx <= 0) continue;\n const block = decodePart(pair.slice(0, idx));\n const variant = decodePart(pair.slice(idx + 1));\n if (block && variant) state[block] = variant;\n }\n return state;\n } catch {\n return {};\n }\n}\n\nexport function serializeUrl(state: State): string {\n return Object.entries(state)\n .map(([block, variant]) => `${encodePart(block)}:${encodePart(variant)}`)\n .join(\",\");\n}\n\nexport function syncURL(state: State, param = DEFAULT_URL_PARAM): void {\n try {\n const url = new URL(location.href);\n const serialized = serializeUrl(state);\n if (serialized) {\n url.searchParams.set(param, serialized);\n } else {\n url.searchParams.delete(param);\n }\n history.replaceState(history.state, \"\", url.toString());\n } catch {\n // location can be cross-origin restricted in sandboxed iframes; ignore\n }\n}\n","import { DEFAULT_URL_PARAM, type State, parseUrl, syncURL } from \"./url\";\n\nexport interface WhichlyRuntimeOptions {\n root?: ParentNode;\n floating?: boolean;\n param?: string;\n}\n\nexport interface WhichlyRuntime {\n readonly blocks: Map<string, string[]>;\n readonly state: State;\n scan(): void;\n select(block: string, variant: string): void;\n reset(block: string): void;\n subscribe(listener: () => void): () => void;\n destroy(): void;\n}\n\ntype Listener = () => void;\n\nconst BLOCK_SELECTOR = \"[data-whichly-block], [data-vp-block]\";\nconst VARIANT_SELECTOR = \"[data-whichly-variant], [data-vp-variant]\";\n\nconst pickerCss = `\n:host { all: initial; color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; }\n* { box-sizing: border-box; }\n.whichly-wrap { position: fixed; left: 0; right: 0; bottom: 5vh; z-index: 2147483647; display: flex; justify-content: center; padding: 0 16px; pointer-events: none; }\n.whichly-dock { pointer-events: auto; display: flex; min-width: 0; max-width: 100%; align-items: center; gap: 14px; border-radius: 999px; background: #09090b; color: #fafafa; padding: 6px 8px 6px 16px; box-shadow: 0 16px 48px rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.1); }\n.whichly-title { font-size: 14px; font-weight: 700; white-space: nowrap; }\n.whichly-list { display: flex; min-width: 0; flex: 1; align-items: center; gap: 10px; overflow-x: auto; padding: 2px 0; scrollbar-width: none; }\n.whichly-list::-webkit-scrollbar { display: none; }\n.whichly-stepper { display: inline-flex; flex-shrink: 0; align-items: center; gap: 4px; border-radius: 999px; border: 1px solid rgba(255,255,255,.12); background: #18181b; padding: 4px 6px 4px 12px; }\n.whichly-block { max-width: 96px; overflow: hidden; padding-right: 6px; color: #a1a1aa; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }\n.whichly-current { min-width: 80px; max-width: 160px; overflow: hidden; padding: 0 2px; text-align: center; color: #fafafa; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }\nbutton { appearance: none; border: 0; border-radius: 999px; background: transparent; color: #fafafa; cursor: pointer; display: inline-grid; height: 28px; min-width: 28px; place-items: center; padding: 0 8px; font: inherit; }\nbutton:hover { background: rgba(255,255,255,.09); }\nbutton.whichly-reset:hover { background: rgba(239,68,68,.2); color: #fecaca; }\nbutton.whichly-collapse { flex-shrink: 0; }\n.whichly-fab { position: fixed; right: 16px; bottom: 5vh; z-index: 2147483647; width: 48px; height: 48px; border: 1px solid rgba(255,255,255,.1); background: #18181b; box-shadow: 0 16px 48px rgba(0,0,0,.35); pointer-events: auto; }\n@media (max-width: 640px) { .whichly-wrap { bottom: max(12px, env(safe-area-inset-bottom)); padding: 0 8px; } .whichly-title { display: none; } .whichly-dock { gap: 8px; padding: 8px; } .whichly-fab { bottom: max(12px, env(safe-area-inset-bottom)); } }\n`;\n\nfunction getBlockName(el: Element): string | undefined {\n return (el as HTMLElement).dataset.whichlyBlock ?? (el as HTMLElement).dataset.vpBlock;\n}\n\nfunction getVariantName(el: Element): string | undefined {\n return (el as HTMLElement).dataset.whichlyVariant ?? (el as HTMLElement).dataset.vpVariant;\n}\n\nfunction wrap(list: string[], current: string, dir: 1 | -1): string {\n const len = list.length;\n if (len === 0) return current;\n const idx = list.indexOf(current);\n const start = idx === -1 ? 0 : idx;\n return list[(start + dir + len) % len] ?? current;\n}\n\nfunction isNestedInOtherBlock(variant: Element, block: Element): boolean {\n const parentBlock = variant.parentElement?.closest(BLOCK_SELECTOR);\n return parentBlock !== block;\n}\n\nexport function createWhichlyRuntime(options: WhichlyRuntimeOptions = {}): WhichlyRuntime {\n const root = options.root ?? document;\n const param = options.param ?? DEFAULT_URL_PARAM;\n const floating = options.floating ?? true;\n const blocks = new Map<string, string[]>();\n let state: State = typeof location === \"undefined\" ? {} : parseUrl(location.href, param);\n const listeners = new Set<Listener>();\n let observer: MutationObserver | undefined;\n let host: HTMLDivElement | undefined;\n let shadow: ShadowRoot | undefined;\n let collapsed = false;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function getActive(block: string): string | undefined {\n const variants = blocks.get(block);\n if (!variants || variants.length === 0) return undefined;\n const preferred = state[block];\n if (preferred && variants.includes(preferred)) return preferred;\n return variants[0];\n }\n\n function applyState(): void {\n for (const blockEl of root.querySelectorAll<HTMLElement>(BLOCK_SELECTOR)) {\n const block = getBlockName(blockEl);\n if (!block) continue;\n const active = getActive(block);\n for (const variantEl of blockEl.querySelectorAll<HTMLElement>(VARIANT_SELECTOR)) {\n if (isNestedInOtherBlock(variantEl, blockEl)) continue;\n const variant = getVariantName(variantEl);\n variantEl.style.display = variant === active ? \"contents\" : \"none\";\n }\n }\n }\n\n function renderPicker(): void {\n if (!shadow) return;\n const existing = shadow.querySelector(\".whichly-wrap, .whichly-fab\");\n existing?.remove();\n\n if (blocks.size === 0) return;\n\n if (collapsed) {\n const button = document.createElement(\"button\");\n button.className = \"whichly-fab\";\n button.type = \"button\";\n button.setAttribute(\"aria-label\", \"Expand Whichly picker\");\n button.textContent = \"⌁\";\n button.addEventListener(\"click\", () => {\n collapsed = false;\n renderPicker();\n });\n shadow.appendChild(button);\n return;\n }\n\n const wrapEl = document.createElement(\"div\");\n wrapEl.className = \"whichly-wrap\";\n\n const dock = document.createElement(\"div\");\n dock.className = \"whichly-dock\";\n dock.setAttribute(\"role\", \"toolbar\");\n dock.setAttribute(\"aria-label\", \"Whichly variant picker\");\n\n const title = document.createElement(\"div\");\n title.className = \"whichly-title\";\n title.textContent = \"Whichly\";\n dock.appendChild(title);\n\n const list = document.createElement(\"div\");\n list.className = \"whichly-list\";\n\n for (const [block, variants] of blocks) {\n const current = getActive(block) ?? \"\";\n const stepper = document.createElement(\"div\");\n stepper.className = \"whichly-stepper\";\n\n const label = document.createElement(\"div\");\n label.className = \"whichly-block\";\n label.title = block;\n label.textContent = block;\n stepper.appendChild(label);\n\n const prev = document.createElement(\"button\");\n prev.type = \"button\";\n prev.setAttribute(\"aria-label\", `Previous ${block} variant`);\n prev.textContent = \"‹\";\n prev.addEventListener(\"click\", () => runtime.select(block, wrap(variants, current, -1)));\n stepper.appendChild(prev);\n\n const value = document.createElement(\"div\");\n value.className = \"whichly-current\";\n value.title = current;\n value.setAttribute(\"aria-live\", \"polite\");\n value.textContent = current;\n stepper.appendChild(value);\n\n const next = document.createElement(\"button\");\n next.type = \"button\";\n next.setAttribute(\"aria-label\", `Next ${block} variant`);\n next.textContent = \"›\";\n next.addEventListener(\"click\", () => runtime.select(block, wrap(variants, current, 1)));\n stepper.appendChild(next);\n\n const reset = document.createElement(\"button\");\n reset.type = \"button\";\n reset.className = \"whichly-reset\";\n reset.setAttribute(\"aria-label\", `Reset ${block} variant`);\n reset.textContent = \"↺\";\n reset.addEventListener(\"click\", () => runtime.reset(block));\n stepper.appendChild(reset);\n\n list.appendChild(stepper);\n }\n\n dock.appendChild(list);\n\n const collapse = document.createElement(\"button\");\n collapse.type = \"button\";\n collapse.className = \"whichly-collapse\";\n collapse.setAttribute(\"aria-label\", \"Minimize picker\");\n collapse.textContent = \"−\";\n collapse.addEventListener(\"click\", () => {\n collapsed = true;\n renderPicker();\n });\n dock.appendChild(collapse);\n\n wrapEl.appendChild(dock);\n shadow.appendChild(wrapEl);\n }\n\n function scan(): void {\n blocks.clear();\n for (const blockEl of root.querySelectorAll<HTMLElement>(BLOCK_SELECTOR)) {\n const block = getBlockName(blockEl);\n if (!block) continue;\n const variants = blocks.get(block) ?? [];\n for (const variantEl of blockEl.querySelectorAll<HTMLElement>(VARIANT_SELECTOR)) {\n if (isNestedInOtherBlock(variantEl, blockEl)) continue;\n const variant = getVariantName(variantEl);\n if (variant && !variants.includes(variant)) variants.push(variant);\n }\n if (variants.length > 0) blocks.set(block, variants);\n }\n applyState();\n renderPicker();\n emit();\n }\n\n const runtime: WhichlyRuntime = {\n get blocks() {\n return new Map(blocks);\n },\n get state() {\n return { ...state };\n },\n scan,\n select(block, variant) {\n if (!blocks.get(block)?.includes(variant)) return;\n state = { ...state, [block]: variant };\n syncURL(state, param);\n applyState();\n renderPicker();\n emit();\n },\n reset(block) {\n const first = blocks.get(block)?.[0];\n if (!first) return;\n state = { ...state, [block]: first };\n syncURL(state, param);\n applyState();\n renderPicker();\n emit();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n destroy() {\n observer?.disconnect();\n host?.remove();\n listeners.clear();\n blocks.clear();\n },\n };\n\n if (floating && typeof document !== \"undefined\") {\n host = document.createElement(\"div\");\n host.id = \"__whichly_picker_host\";\n shadow = host.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n style.textContent = pickerCss;\n shadow.appendChild(style);\n document.body.appendChild(host);\n }\n\n scan();\n\n if (typeof MutationObserver !== \"undefined\") {\n observer = new MutationObserver(() => scan());\n observer.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\n \"data-whichly-block\",\n \"data-whichly-variant\",\n \"data-vp-block\",\n \"data-vp-variant\",\n ],\n });\n }\n\n return runtime;\n}\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@whichly/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic DOM runtime for Whichly variant previews.",
5
+ "license": "MIT",
6
+ "author": "kapishdima <kapishdima@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kapishdima/whichly.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://github.com/kapishdima/whichly/tree/main/packages/core#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/kapishdima/whichly/issues"
15
+ },
16
+ "keywords": ["whichly", "variants", "ab-test", "staging", "client-review", "astro"],
17
+ "type": "module",
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "files": ["dist"],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "sideEffects": false,
33
+ "scripts": {
34
+ "dev": "vite build --watch",
35
+ "build": "vite build",
36
+ "typecheck": "tsc --noEmit",
37
+ "lint": "biome check .",
38
+ "prepublishOnly": "pnpm build"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.6.3",
42
+ "vite": "^6.0.0",
43
+ "vite-plugin-dts": "^4.4.0"
44
+ }
45
+ }