@vibeflow-tools/prototyping 0.2.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/CHANGELOG.md +12 -0
- package/README.md +341 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +890 -0
- package/dist/prototype-bundle.iife.js +1 -0
- package/dist/prototype-bundle.js +1 -0
- package/package.json +70 -0
- package/skills/SKILL.md +384 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// src/utils.ts
|
|
11
|
+
var URL_PARAM_PREFIX = "vf";
|
|
12
|
+
var LS_PREFIX = "__vf__";
|
|
13
|
+
function encodeUrlKey(name) {
|
|
14
|
+
return `${URL_PARAM_PREFIX}[${name}]`;
|
|
15
|
+
}
|
|
16
|
+
function readVariantFromUrl(name) {
|
|
17
|
+
if (typeof window === "undefined") return null;
|
|
18
|
+
const params = new URLSearchParams(window.location.search);
|
|
19
|
+
return params.get(encodeUrlKey(name));
|
|
20
|
+
}
|
|
21
|
+
function writeVariantToUrl(name, variant) {
|
|
22
|
+
if (typeof window === "undefined") return;
|
|
23
|
+
const params = new URLSearchParams(window.location.search);
|
|
24
|
+
params.set(encodeUrlKey(name), variant);
|
|
25
|
+
const newSearch = params.toString();
|
|
26
|
+
const newUrl = `${window.location.pathname}?${newSearch}${window.location.hash}`;
|
|
27
|
+
window.history.pushState(null, "", newUrl);
|
|
28
|
+
}
|
|
29
|
+
function removeVariantFromUrl(name) {
|
|
30
|
+
if (typeof window === "undefined") return;
|
|
31
|
+
const params = new URLSearchParams(window.location.search);
|
|
32
|
+
params.delete(encodeUrlKey(name));
|
|
33
|
+
const newSearch = params.toString();
|
|
34
|
+
const newUrl = newSearch ? `${window.location.pathname}?${newSearch}${window.location.hash}` : `${window.location.pathname}${window.location.hash}`;
|
|
35
|
+
window.history.pushState(null, "", newUrl);
|
|
36
|
+
}
|
|
37
|
+
function lsKey(name) {
|
|
38
|
+
return `${LS_PREFIX}${name}`;
|
|
39
|
+
}
|
|
40
|
+
function readVariantFromStorage(name) {
|
|
41
|
+
if (typeof window === "undefined") return null;
|
|
42
|
+
try {
|
|
43
|
+
return window.localStorage.getItem(lsKey(name));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function writeVariantToStorage(name, variant) {
|
|
49
|
+
if (typeof window === "undefined") return;
|
|
50
|
+
try {
|
|
51
|
+
window.localStorage.setItem(lsKey(name), variant);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function removeVariantFromStorage(name) {
|
|
56
|
+
if (typeof window === "undefined") return;
|
|
57
|
+
try {
|
|
58
|
+
window.localStorage.removeItem(lsKey(name));
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var UI_VISIBLE_KEY = "__vf__ui_visible__";
|
|
63
|
+
function writeUiVisibleToStorage(visible) {
|
|
64
|
+
if (typeof window === "undefined") return;
|
|
65
|
+
try {
|
|
66
|
+
window.localStorage.setItem(UI_VISIBLE_KEY, String(visible));
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function readUiVisibleFromStorage() {
|
|
71
|
+
if (typeof window === "undefined") return null;
|
|
72
|
+
try {
|
|
73
|
+
const val = window.localStorage.getItem(UI_VISIBLE_KEY);
|
|
74
|
+
if (val === null) return null;
|
|
75
|
+
return val !== "false";
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function resolveActiveVariant(name, variantKeys, defaultKey) {
|
|
81
|
+
const fromUrl = readVariantFromUrl(name);
|
|
82
|
+
if (fromUrl && variantKeys.includes(fromUrl)) return fromUrl;
|
|
83
|
+
const fromStorage = readVariantFromStorage(name);
|
|
84
|
+
if (fromStorage && variantKeys.includes(fromStorage)) return fromStorage;
|
|
85
|
+
return defaultKey;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/useKeyboardShortcuts.ts
|
|
89
|
+
import { useEffect } from "react";
|
|
90
|
+
var DEFAULT_SHORTCUTS = [
|
|
91
|
+
{ key: "h", alt: true },
|
|
92
|
+
{ key: "V", ctrl: true, shift: true }
|
|
93
|
+
];
|
|
94
|
+
function matchesShortcut(e, shortcut) {
|
|
95
|
+
if (e.key !== shortcut.key) return false;
|
|
96
|
+
if (!!shortcut.alt !== e.altKey) return false;
|
|
97
|
+
if (!!shortcut.ctrl !== e.ctrlKey) return false;
|
|
98
|
+
if (!!shortcut.shift !== e.shiftKey) return false;
|
|
99
|
+
if (!!shortcut.meta !== e.metaKey) return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function useKeyboardShortcuts({
|
|
103
|
+
onToggleUi,
|
|
104
|
+
shortcuts = DEFAULT_SHORTCUTS
|
|
105
|
+
}) {
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (shortcuts === false) return;
|
|
108
|
+
const handler = (e) => {
|
|
109
|
+
for (const shortcut of shortcuts) {
|
|
110
|
+
if (matchesShortcut(e, shortcut)) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
onToggleUi();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
window.addEventListener("keydown", handler);
|
|
118
|
+
return () => window.removeEventListener("keydown", handler);
|
|
119
|
+
}, [onToggleUi, shortcuts]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/context.tsx
|
|
123
|
+
import { jsx } from "react/jsx-runtime";
|
|
124
|
+
var VariantContext = createContext(null);
|
|
125
|
+
function useVariantContext() {
|
|
126
|
+
const ctx = useContext(VariantContext);
|
|
127
|
+
if (!ctx) {
|
|
128
|
+
throw new Error("useVariantContext must be used inside <VariantProvider>");
|
|
129
|
+
}
|
|
130
|
+
return ctx;
|
|
131
|
+
}
|
|
132
|
+
function resolveInitialUiVisible(mode, defaultVisible) {
|
|
133
|
+
if (mode === "always") {
|
|
134
|
+
return defaultVisible ?? true;
|
|
135
|
+
}
|
|
136
|
+
const persisted = readUiVisibleFromStorage();
|
|
137
|
+
if (persisted !== null) return persisted;
|
|
138
|
+
return defaultVisible ?? true;
|
|
139
|
+
}
|
|
140
|
+
function VariantProvider({
|
|
141
|
+
children,
|
|
142
|
+
mode = "dev",
|
|
143
|
+
defaultVisible,
|
|
144
|
+
shortcuts
|
|
145
|
+
}) {
|
|
146
|
+
const [scopes, setScopes] = useState({});
|
|
147
|
+
const [uiVisible, setUiVisible] = useState(
|
|
148
|
+
() => resolveInitialUiVisible(mode, defaultVisible)
|
|
149
|
+
);
|
|
150
|
+
const registeredSwitchers = useRef(/* @__PURE__ */ new Set());
|
|
151
|
+
const registerScope = useCallback(
|
|
152
|
+
(name, variantNames) => {
|
|
153
|
+
setScopes((prev) => {
|
|
154
|
+
const existing = prev[name];
|
|
155
|
+
if (existing && existing.variantNames.length === variantNames.length && existing.variantNames.every((v, i) => v === variantNames[i])) {
|
|
156
|
+
return prev;
|
|
157
|
+
}
|
|
158
|
+
const firstKey = variantNames[0] ?? "default";
|
|
159
|
+
const activeVariant = resolveActiveVariant(
|
|
160
|
+
name,
|
|
161
|
+
variantNames,
|
|
162
|
+
existing?.activeVariant ?? firstKey
|
|
163
|
+
);
|
|
164
|
+
return { ...prev, [name]: { activeVariant, variantNames } };
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
[]
|
|
168
|
+
);
|
|
169
|
+
const getActiveVariant = useCallback(
|
|
170
|
+
(name) => {
|
|
171
|
+
return scopes[name]?.activeVariant ?? "";
|
|
172
|
+
},
|
|
173
|
+
[scopes]
|
|
174
|
+
);
|
|
175
|
+
const setActiveVariant = useCallback(
|
|
176
|
+
(name, variant) => {
|
|
177
|
+
setScopes((prev) => {
|
|
178
|
+
const existing = prev[name];
|
|
179
|
+
if (!existing) return prev;
|
|
180
|
+
if (existing.activeVariant === variant) return prev;
|
|
181
|
+
return { ...prev, [name]: { ...existing, activeVariant: variant } };
|
|
182
|
+
});
|
|
183
|
+
writeVariantToUrl(name, variant);
|
|
184
|
+
writeVariantToStorage(name, variant);
|
|
185
|
+
},
|
|
186
|
+
[]
|
|
187
|
+
);
|
|
188
|
+
const toggleUiVisible = useCallback(() => {
|
|
189
|
+
setUiVisible((prev) => {
|
|
190
|
+
const next = !prev;
|
|
191
|
+
writeUiVisibleToStorage(next);
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
194
|
+
}, []);
|
|
195
|
+
const registerSwitcher = useCallback((name) => {
|
|
196
|
+
if (registeredSwitchers.current.has(name)) return false;
|
|
197
|
+
registeredSwitchers.current.add(name);
|
|
198
|
+
return true;
|
|
199
|
+
}, []);
|
|
200
|
+
const unregisterSwitcher = useCallback((name) => {
|
|
201
|
+
registeredSwitchers.current.delete(name);
|
|
202
|
+
}, []);
|
|
203
|
+
useKeyboardShortcuts({
|
|
204
|
+
onToggleUi: toggleUiVisible,
|
|
205
|
+
shortcuts
|
|
206
|
+
});
|
|
207
|
+
const value = {
|
|
208
|
+
getActiveVariant,
|
|
209
|
+
setActiveVariant,
|
|
210
|
+
registerScope,
|
|
211
|
+
registerSwitcher,
|
|
212
|
+
unregisterSwitcher,
|
|
213
|
+
scopes,
|
|
214
|
+
uiVisible,
|
|
215
|
+
toggleUiVisible,
|
|
216
|
+
mode
|
|
217
|
+
};
|
|
218
|
+
return /* @__PURE__ */ jsx(VariantContext.Provider, { value, children });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/useVariant.ts
|
|
222
|
+
import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
|
|
223
|
+
|
|
224
|
+
// src/registry.ts
|
|
225
|
+
var registry = /* @__PURE__ */ new Map();
|
|
226
|
+
function registerVariant(name, variants) {
|
|
227
|
+
registry.set(name, variants);
|
|
228
|
+
}
|
|
229
|
+
function getRegisteredVariant(name) {
|
|
230
|
+
return registry.get(name);
|
|
231
|
+
}
|
|
232
|
+
function clearVariantRegistry() {
|
|
233
|
+
registry.clear();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/useActiveVariant.ts
|
|
237
|
+
import { useMemo } from "react";
|
|
238
|
+
function useActiveVariant(name, variantKeys) {
|
|
239
|
+
const ctx = useVariantContext();
|
|
240
|
+
return useMemo(() => {
|
|
241
|
+
const current = ctx.getActiveVariant(name);
|
|
242
|
+
if (current && variantKeys.includes(current)) return current;
|
|
243
|
+
return resolveActiveVariant(
|
|
244
|
+
name,
|
|
245
|
+
variantKeys,
|
|
246
|
+
variantKeys[0] ?? "default"
|
|
247
|
+
);
|
|
248
|
+
}, [ctx, name, variantKeys]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/useVariant.ts
|
|
252
|
+
function useVariant(name, variants) {
|
|
253
|
+
const ctx = useVariantContext();
|
|
254
|
+
const mergedVariants = useMemo2(() => {
|
|
255
|
+
const registered = getRegisteredVariant(name);
|
|
256
|
+
return registered ? { ...registered, ...variants } : variants;
|
|
257
|
+
}, [name, variants]);
|
|
258
|
+
const variantKeys = useMemo2(
|
|
259
|
+
() => Object.keys(mergedVariants),
|
|
260
|
+
[mergedVariants]
|
|
261
|
+
);
|
|
262
|
+
useEffect2(() => {
|
|
263
|
+
ctx.registerScope(name, variantKeys);
|
|
264
|
+
}, [ctx, name, variantKeys]);
|
|
265
|
+
const activeKey = useActiveVariant(name, variantKeys);
|
|
266
|
+
return mergedVariants[activeKey] ?? mergedVariants[variantKeys[0] ?? "default"] ?? {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/PageVariantSwitcher.tsx
|
|
270
|
+
import { useEffect as useEffect3, useMemo as useMemo3 } from "react";
|
|
271
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
272
|
+
function PageVariantSwitcher({
|
|
273
|
+
name,
|
|
274
|
+
variants
|
|
275
|
+
}) {
|
|
276
|
+
const ctx = useVariantContext();
|
|
277
|
+
const variantKeys = useMemo3(() => Object.keys(variants), [variants]);
|
|
278
|
+
useEffect3(() => {
|
|
279
|
+
ctx.registerScope(name, variantKeys);
|
|
280
|
+
}, [ctx, name, variantKeys]);
|
|
281
|
+
const activeKey = useActiveVariant(name, variantKeys);
|
|
282
|
+
if (!ctx.uiVisible) return null;
|
|
283
|
+
if (variantKeys.length < 2) return null;
|
|
284
|
+
return /* @__PURE__ */ jsx2(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
role: "toolbar",
|
|
288
|
+
"aria-label": `Page variant switcher: ${name}`,
|
|
289
|
+
style: {
|
|
290
|
+
position: "fixed",
|
|
291
|
+
top: "12px",
|
|
292
|
+
left: "12px",
|
|
293
|
+
zIndex: 99999,
|
|
294
|
+
display: "flex",
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
background: "#171717",
|
|
297
|
+
borderRadius: "8px",
|
|
298
|
+
padding: "4px",
|
|
299
|
+
gap: "2px",
|
|
300
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.32)",
|
|
301
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
302
|
+
fontSize: "12px",
|
|
303
|
+
userSelect: "none"
|
|
304
|
+
},
|
|
305
|
+
children: variantKeys.map((key) => {
|
|
306
|
+
const isActive = key === activeKey;
|
|
307
|
+
return /* @__PURE__ */ jsx2(
|
|
308
|
+
"button",
|
|
309
|
+
{
|
|
310
|
+
role: "radio",
|
|
311
|
+
"aria-checked": isActive,
|
|
312
|
+
"aria-label": `Switch to ${key} variant`,
|
|
313
|
+
onClick: () => ctx.setActiveVariant(name, key),
|
|
314
|
+
style: {
|
|
315
|
+
cursor: "pointer",
|
|
316
|
+
border: "none",
|
|
317
|
+
outline: "none",
|
|
318
|
+
borderRadius: "5px",
|
|
319
|
+
padding: "5px 12px",
|
|
320
|
+
fontSize: "12px",
|
|
321
|
+
fontWeight: isActive ? 600 : 400,
|
|
322
|
+
background: isActive ? "#ffffff" : "transparent",
|
|
323
|
+
color: isActive ? "#171717" : "#a3a3a3",
|
|
324
|
+
transition: "background 0.15s, color 0.15s",
|
|
325
|
+
whiteSpace: "nowrap"
|
|
326
|
+
},
|
|
327
|
+
children: key
|
|
328
|
+
},
|
|
329
|
+
key
|
|
330
|
+
);
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/VariantSwitcher.tsx
|
|
337
|
+
import { useEffect as useEffect4, useMemo as useMemo4, useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
338
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
339
|
+
function VariantSwitcher({
|
|
340
|
+
name,
|
|
341
|
+
variants,
|
|
342
|
+
position = "right"
|
|
343
|
+
}) {
|
|
344
|
+
const ctx = useVariantContext();
|
|
345
|
+
const variantKeys = useMemo4(() => Object.keys(variants), [variants]);
|
|
346
|
+
const [expanded, setExpanded] = useState2(false);
|
|
347
|
+
const containerRef = useRef2(null);
|
|
348
|
+
const [isPrimary, setIsPrimary] = useState2(false);
|
|
349
|
+
useEffect4(() => {
|
|
350
|
+
const primary = ctx.registerSwitcher(name);
|
|
351
|
+
setIsPrimary(primary);
|
|
352
|
+
return () => {
|
|
353
|
+
ctx.unregisterSwitcher(name);
|
|
354
|
+
};
|
|
355
|
+
}, [ctx, name]);
|
|
356
|
+
useEffect4(() => {
|
|
357
|
+
ctx.registerScope(name, variantKeys);
|
|
358
|
+
}, [ctx, name, variantKeys]);
|
|
359
|
+
const activeKey = useActiveVariant(name, variantKeys);
|
|
360
|
+
const [pos, setPos] = useState2(() => {
|
|
361
|
+
try {
|
|
362
|
+
const saved = localStorage.getItem(`vf-variant-pos-${name}`);
|
|
363
|
+
if (saved) {
|
|
364
|
+
const parsed = JSON.parse(saved);
|
|
365
|
+
if (typeof parsed.x === "number" && typeof parsed.y === "number")
|
|
366
|
+
return parsed;
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
});
|
|
372
|
+
const [isDragging, setIsDragging] = useState2(false);
|
|
373
|
+
const [isHolding, setIsHolding] = useState2(false);
|
|
374
|
+
const dragOrigin = useRef2(null);
|
|
375
|
+
const holdTimer = useRef2(null);
|
|
376
|
+
const didDrag = useRef2(false);
|
|
377
|
+
const posRef = useRef2(null);
|
|
378
|
+
posRef.current = pos;
|
|
379
|
+
useEffect4(() => {
|
|
380
|
+
if (pos !== null) {
|
|
381
|
+
try {
|
|
382
|
+
localStorage.setItem(`vf-variant-pos-${name}`, JSON.stringify(pos));
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}, [pos, name]);
|
|
387
|
+
useEffect4(() => {
|
|
388
|
+
return () => {
|
|
389
|
+
if (holdTimer.current !== null) window.clearTimeout(holdTimer.current);
|
|
390
|
+
};
|
|
391
|
+
}, []);
|
|
392
|
+
function getInitialPos() {
|
|
393
|
+
if (containerRef.current) {
|
|
394
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
395
|
+
return { x: rect.left, y: rect.top };
|
|
396
|
+
}
|
|
397
|
+
return { x: window.innerWidth - 48, y: window.innerHeight / 2 };
|
|
398
|
+
}
|
|
399
|
+
function onDotPointerDown(e) {
|
|
400
|
+
const target = e.currentTarget;
|
|
401
|
+
const pointerId = e.pointerId;
|
|
402
|
+
const clientX = e.clientX;
|
|
403
|
+
const clientY = e.clientY;
|
|
404
|
+
didDrag.current = false;
|
|
405
|
+
holdTimer.current = window.setTimeout(() => {
|
|
406
|
+
setIsHolding(true);
|
|
407
|
+
const startPos = posRef.current ?? getInitialPos();
|
|
408
|
+
dragOrigin.current = {
|
|
409
|
+
mouseX: clientX,
|
|
410
|
+
mouseY: clientY,
|
|
411
|
+
posX: startPos.x,
|
|
412
|
+
posY: startPos.y
|
|
413
|
+
};
|
|
414
|
+
target.setPointerCapture(pointerId);
|
|
415
|
+
}, 300);
|
|
416
|
+
}
|
|
417
|
+
function onDotPointerMove(e) {
|
|
418
|
+
if (!dragOrigin.current) return;
|
|
419
|
+
const dx = e.clientX - dragOrigin.current.mouseX;
|
|
420
|
+
const dy = e.clientY - dragOrigin.current.mouseY;
|
|
421
|
+
if (!isDragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3))
|
|
422
|
+
setIsDragging(true);
|
|
423
|
+
didDrag.current = true;
|
|
424
|
+
const bw = window.innerWidth;
|
|
425
|
+
const bh = window.innerHeight;
|
|
426
|
+
const x = Math.max(8, Math.min(bw - 40, dragOrigin.current.posX + dx));
|
|
427
|
+
const y = Math.max(8, Math.min(bh - 40, dragOrigin.current.posY + dy));
|
|
428
|
+
setPos({ x, y });
|
|
429
|
+
}
|
|
430
|
+
function onDotPointerUp() {
|
|
431
|
+
if (holdTimer.current !== null) {
|
|
432
|
+
window.clearTimeout(holdTimer.current);
|
|
433
|
+
holdTimer.current = null;
|
|
434
|
+
}
|
|
435
|
+
const wasDragged = didDrag.current;
|
|
436
|
+
dragOrigin.current = null;
|
|
437
|
+
setIsDragging(false);
|
|
438
|
+
setIsHolding(false);
|
|
439
|
+
didDrag.current = false;
|
|
440
|
+
if (!wasDragged) setExpanded(true);
|
|
441
|
+
}
|
|
442
|
+
function onDotPointerCancel() {
|
|
443
|
+
if (holdTimer.current !== null) {
|
|
444
|
+
window.clearTimeout(holdTimer.current);
|
|
445
|
+
holdTimer.current = null;
|
|
446
|
+
}
|
|
447
|
+
dragOrigin.current = null;
|
|
448
|
+
setIsDragging(false);
|
|
449
|
+
setIsHolding(false);
|
|
450
|
+
didDrag.current = false;
|
|
451
|
+
}
|
|
452
|
+
const handleClickOutside = useCallback2((e) => {
|
|
453
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
454
|
+
setExpanded(false);
|
|
455
|
+
}
|
|
456
|
+
}, []);
|
|
457
|
+
useEffect4(() => {
|
|
458
|
+
if (expanded) {
|
|
459
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
460
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
461
|
+
}
|
|
462
|
+
}, [expanded, handleClickOutside]);
|
|
463
|
+
useEffect4(() => {
|
|
464
|
+
if (!expanded) return;
|
|
465
|
+
const handleKeyDown = (e) => {
|
|
466
|
+
if (e.key === "Escape") setExpanded(false);
|
|
467
|
+
};
|
|
468
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
469
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
470
|
+
}, [expanded]);
|
|
471
|
+
if (!ctx.uiVisible) return null;
|
|
472
|
+
if (variantKeys.length < 2) return null;
|
|
473
|
+
if (!isPrimary) return null;
|
|
474
|
+
const sideStyle = position === "left" ? { left: "-24px", right: "auto" } : { right: "-24px", left: "auto" };
|
|
475
|
+
const containerStyle = pos !== null ? {
|
|
476
|
+
position: "fixed",
|
|
477
|
+
left: pos.x,
|
|
478
|
+
top: pos.y,
|
|
479
|
+
transform: "none",
|
|
480
|
+
zIndex: 9999
|
|
481
|
+
} : {
|
|
482
|
+
position: "absolute",
|
|
483
|
+
top: "50%",
|
|
484
|
+
transform: "translateY(-50%)",
|
|
485
|
+
...sideStyle,
|
|
486
|
+
zIndex: 9999
|
|
487
|
+
};
|
|
488
|
+
const dotTitle = isDragging ? "Drag to reposition" : isHolding ? "Drag to reposition \xB7 Release to expand" : `${name} variants \u2014 click to switch \xB7 hold to drag`;
|
|
489
|
+
return /* @__PURE__ */ jsx3(
|
|
490
|
+
"div",
|
|
491
|
+
{
|
|
492
|
+
ref: containerRef,
|
|
493
|
+
role: "toolbar",
|
|
494
|
+
"aria-label": `Component variant switcher: ${name}`,
|
|
495
|
+
className: "vf-variant-switcher",
|
|
496
|
+
style: containerStyle,
|
|
497
|
+
children: expanded ? /* @__PURE__ */ jsx3(
|
|
498
|
+
"div",
|
|
499
|
+
{
|
|
500
|
+
style: {
|
|
501
|
+
display: "flex",
|
|
502
|
+
flexDirection: "column",
|
|
503
|
+
gap: "3px",
|
|
504
|
+
background: "#fff",
|
|
505
|
+
border: "1px solid #e5e5e5",
|
|
506
|
+
borderRadius: "4px",
|
|
507
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
|
|
508
|
+
padding: "3px",
|
|
509
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
|
510
|
+
},
|
|
511
|
+
children: variantKeys.map((key, index) => {
|
|
512
|
+
const isActive = key === activeKey;
|
|
513
|
+
return /* @__PURE__ */ jsx3(
|
|
514
|
+
"button",
|
|
515
|
+
{
|
|
516
|
+
role: "radio",
|
|
517
|
+
"aria-checked": isActive,
|
|
518
|
+
"aria-label": `Switch to ${key} variant (${index + 1})`,
|
|
519
|
+
onClick: () => {
|
|
520
|
+
ctx.setActiveVariant(name, key);
|
|
521
|
+
setExpanded(false);
|
|
522
|
+
},
|
|
523
|
+
title: key,
|
|
524
|
+
style: {
|
|
525
|
+
cursor: "pointer",
|
|
526
|
+
border: "none",
|
|
527
|
+
outline: "none",
|
|
528
|
+
width: "22px",
|
|
529
|
+
height: "22px",
|
|
530
|
+
borderRadius: "3px",
|
|
531
|
+
fontSize: "10px",
|
|
532
|
+
fontWeight: isActive ? 700 : 400,
|
|
533
|
+
background: isActive ? "#171717" : "transparent",
|
|
534
|
+
color: isActive ? "#fff" : "#737373",
|
|
535
|
+
display: "flex",
|
|
536
|
+
alignItems: "center",
|
|
537
|
+
justifyContent: "center",
|
|
538
|
+
transition: "background 0.12s, color 0.12s",
|
|
539
|
+
padding: 0,
|
|
540
|
+
lineHeight: 1
|
|
541
|
+
},
|
|
542
|
+
children: index + 1
|
|
543
|
+
},
|
|
544
|
+
key
|
|
545
|
+
);
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
) : (
|
|
549
|
+
/* Collapsed: draggable indicator dot */
|
|
550
|
+
/* @__PURE__ */ jsx3(
|
|
551
|
+
"button",
|
|
552
|
+
{
|
|
553
|
+
"aria-label": `Open variant switcher for ${name}`,
|
|
554
|
+
title: dotTitle,
|
|
555
|
+
onPointerDown: onDotPointerDown,
|
|
556
|
+
onPointerMove: onDotPointerMove,
|
|
557
|
+
onPointerUp: onDotPointerUp,
|
|
558
|
+
onPointerCancel: onDotPointerCancel,
|
|
559
|
+
style: {
|
|
560
|
+
cursor: isDragging ? "grabbing" : isHolding ? "grab" : "pointer",
|
|
561
|
+
border: "1px solid #e5e5e5",
|
|
562
|
+
outline: "none",
|
|
563
|
+
width: "14px",
|
|
564
|
+
height: "14px",
|
|
565
|
+
borderRadius: "50%",
|
|
566
|
+
background: isDragging ? "#e5e5e5" : "#f5f5f5",
|
|
567
|
+
padding: 0,
|
|
568
|
+
display: "flex",
|
|
569
|
+
alignItems: "center",
|
|
570
|
+
justifyContent: "center",
|
|
571
|
+
boxShadow: isDragging ? "0 2px 8px rgba(0,0,0,0.14)" : "0 1px 2px rgba(0,0,0,0.06)",
|
|
572
|
+
transition: isDragging ? "none" : "background 0.15s, border-color 0.15s"
|
|
573
|
+
},
|
|
574
|
+
onMouseEnter: (e) => {
|
|
575
|
+
if (isDragging) return;
|
|
576
|
+
e.currentTarget.style.background = "#e5e5e5";
|
|
577
|
+
e.currentTarget.style.borderColor = "#d4d4d4";
|
|
578
|
+
},
|
|
579
|
+
onMouseLeave: (e) => {
|
|
580
|
+
if (isDragging) return;
|
|
581
|
+
e.currentTarget.style.background = "#f5f5f5";
|
|
582
|
+
e.currentTarget.style.borderColor = "#e5e5e5";
|
|
583
|
+
},
|
|
584
|
+
children: /* @__PURE__ */ jsx3(
|
|
585
|
+
"span",
|
|
586
|
+
{
|
|
587
|
+
style: {
|
|
588
|
+
width: "5px",
|
|
589
|
+
height: "5px",
|
|
590
|
+
borderRadius: "50%",
|
|
591
|
+
background: "#a3a3a3"
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/VariantDevToolbar.tsx
|
|
603
|
+
import { useState as useState3, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
604
|
+
import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
605
|
+
function isOverlayPresent() {
|
|
606
|
+
if (typeof document === "undefined") return false;
|
|
607
|
+
return !!document.getElementById("vibeflow-studio-root");
|
|
608
|
+
}
|
|
609
|
+
function VariantDevToolbar() {
|
|
610
|
+
const ctx = useVariantContext();
|
|
611
|
+
const [isOpen, setIsOpen] = useState3(false);
|
|
612
|
+
const [overlayDetected, setOverlayDetected] = useState3(() => isOverlayPresent());
|
|
613
|
+
const panelRef = useRef3(null);
|
|
614
|
+
const isOpenRef = useRef3(isOpen);
|
|
615
|
+
isOpenRef.current = isOpen;
|
|
616
|
+
useEffect5(() => {
|
|
617
|
+
if (overlayDetected) return;
|
|
618
|
+
function detect() {
|
|
619
|
+
if (isOverlayPresent()) {
|
|
620
|
+
setOverlayDetected(true);
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
if (detect()) return;
|
|
626
|
+
const observer = new MutationObserver(() => {
|
|
627
|
+
detect();
|
|
628
|
+
});
|
|
629
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
630
|
+
const interval = setInterval(() => {
|
|
631
|
+
detect();
|
|
632
|
+
}, 500);
|
|
633
|
+
return () => {
|
|
634
|
+
observer.disconnect();
|
|
635
|
+
clearInterval(interval);
|
|
636
|
+
};
|
|
637
|
+
}, [overlayDetected]);
|
|
638
|
+
const openPanel = useCallback3(() => setIsOpen(true), []);
|
|
639
|
+
const closePanel = useCallback3(() => setIsOpen(false), []);
|
|
640
|
+
useEffect5(() => {
|
|
641
|
+
if (!overlayDetected) return;
|
|
642
|
+
const api = { openPanel, closePanel, get isOpen() {
|
|
643
|
+
return isOpenRef.current;
|
|
644
|
+
} };
|
|
645
|
+
Object.defineProperty(window, "__vf_prototyping", {
|
|
646
|
+
value: api,
|
|
647
|
+
writable: true,
|
|
648
|
+
configurable: true
|
|
649
|
+
});
|
|
650
|
+
return () => {
|
|
651
|
+
if (window.__vf_prototyping === api) {
|
|
652
|
+
delete window.__vf_prototyping;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}, [overlayDetected, openPanel, closePanel]);
|
|
656
|
+
useEffect5(() => {
|
|
657
|
+
if (!isOpen) return;
|
|
658
|
+
const handleKeyDown = (e) => {
|
|
659
|
+
if (e.key === "Escape") setIsOpen(false);
|
|
660
|
+
};
|
|
661
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
662
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
663
|
+
}, [isOpen]);
|
|
664
|
+
const handleClickOutside = useCallback3((e) => {
|
|
665
|
+
if (panelRef.current && !panelRef.current.contains(e.target)) {
|
|
666
|
+
setIsOpen(false);
|
|
667
|
+
}
|
|
668
|
+
}, []);
|
|
669
|
+
useEffect5(() => {
|
|
670
|
+
if (!isOpen) return;
|
|
671
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
672
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
673
|
+
}, [isOpen, handleClickOutside]);
|
|
674
|
+
const scopeEntries = Object.entries(ctx.scopes);
|
|
675
|
+
if (!ctx.uiVisible && !isOpen) return null;
|
|
676
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
677
|
+
ctx.uiVisible && !overlayDetected && /* @__PURE__ */ jsx4(
|
|
678
|
+
"button",
|
|
679
|
+
{
|
|
680
|
+
"aria-label": "Toggle variant dev toolbar",
|
|
681
|
+
"aria-expanded": isOpen,
|
|
682
|
+
onClick: () => setIsOpen((p) => !p),
|
|
683
|
+
style: {
|
|
684
|
+
position: "fixed",
|
|
685
|
+
bottom: "16px",
|
|
686
|
+
right: "16px",
|
|
687
|
+
zIndex: 99998,
|
|
688
|
+
width: "40px",
|
|
689
|
+
height: "40px",
|
|
690
|
+
borderRadius: "50%",
|
|
691
|
+
background: "#6366f1",
|
|
692
|
+
color: "#fff",
|
|
693
|
+
border: "none",
|
|
694
|
+
cursor: "pointer",
|
|
695
|
+
display: "flex",
|
|
696
|
+
alignItems: "center",
|
|
697
|
+
justifyContent: "center",
|
|
698
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.32)",
|
|
699
|
+
transition: "background 0.15s",
|
|
700
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
|
701
|
+
},
|
|
702
|
+
title: "Open variant dev toolbar (Ctrl+Shift+V)",
|
|
703
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: [
|
|
704
|
+
/* @__PURE__ */ jsx4("rect", { x: "2.5", y: "5", width: "2", height: "8", rx: "1", fill: "currentColor", opacity: "0.7" }),
|
|
705
|
+
/* @__PURE__ */ jsx4("rect", { x: "6.5", y: "2", width: "2", height: "14", rx: "1", fill: "currentColor" }),
|
|
706
|
+
/* @__PURE__ */ jsx4("rect", { x: "10.5", y: "6", width: "2", height: "6", rx: "1", fill: "currentColor", opacity: "0.7" }),
|
|
707
|
+
/* @__PURE__ */ jsx4("rect", { x: "14.5", y: "4", width: "2", height: "10", rx: "1", fill: "currentColor", opacity: "0.85" })
|
|
708
|
+
] })
|
|
709
|
+
}
|
|
710
|
+
),
|
|
711
|
+
isOpen && /* @__PURE__ */ jsxs(
|
|
712
|
+
"div",
|
|
713
|
+
{
|
|
714
|
+
ref: panelRef,
|
|
715
|
+
role: "dialog",
|
|
716
|
+
"aria-label": "Variant dev toolbar",
|
|
717
|
+
style: {
|
|
718
|
+
position: "fixed",
|
|
719
|
+
bottom: "68px",
|
|
720
|
+
right: "16px",
|
|
721
|
+
zIndex: 99999,
|
|
722
|
+
background: "#fff",
|
|
723
|
+
border: "1px solid #e5e5e5",
|
|
724
|
+
borderRadius: "12px",
|
|
725
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.16)",
|
|
726
|
+
padding: "16px",
|
|
727
|
+
minWidth: "240px",
|
|
728
|
+
maxWidth: "360px",
|
|
729
|
+
maxHeight: "70vh",
|
|
730
|
+
overflowY: "auto",
|
|
731
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
732
|
+
fontSize: "13px"
|
|
733
|
+
},
|
|
734
|
+
children: [
|
|
735
|
+
/* @__PURE__ */ jsxs(
|
|
736
|
+
"div",
|
|
737
|
+
{
|
|
738
|
+
style: {
|
|
739
|
+
display: "flex",
|
|
740
|
+
alignItems: "center",
|
|
741
|
+
justifyContent: "space-between",
|
|
742
|
+
marginBottom: "12px"
|
|
743
|
+
},
|
|
744
|
+
children: [
|
|
745
|
+
/* @__PURE__ */ jsxs("span", { style: { fontWeight: 700, color: "#171717", fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }, children: [
|
|
746
|
+
/* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: [
|
|
747
|
+
/* @__PURE__ */ jsx4("rect", { x: "2.5", y: "5", width: "2", height: "8", rx: "1", fill: "#6366f1", opacity: "0.7" }),
|
|
748
|
+
/* @__PURE__ */ jsx4("rect", { x: "6.5", y: "2", width: "2", height: "14", rx: "1", fill: "#6366f1" }),
|
|
749
|
+
/* @__PURE__ */ jsx4("rect", { x: "10.5", y: "6", width: "2", height: "6", rx: "1", fill: "#6366f1", opacity: "0.7" }),
|
|
750
|
+
/* @__PURE__ */ jsx4("rect", { x: "14.5", y: "4", width: "2", height: "10", rx: "1", fill: "#6366f1", opacity: "0.85" })
|
|
751
|
+
] }),
|
|
752
|
+
"Variant Switcher"
|
|
753
|
+
] }),
|
|
754
|
+
/* @__PURE__ */ jsx4(
|
|
755
|
+
"button",
|
|
756
|
+
{
|
|
757
|
+
"aria-label": "Close toolbar",
|
|
758
|
+
onClick: () => setIsOpen(false),
|
|
759
|
+
style: {
|
|
760
|
+
border: "none",
|
|
761
|
+
background: "none",
|
|
762
|
+
cursor: "pointer",
|
|
763
|
+
fontSize: "16px",
|
|
764
|
+
color: "#737373",
|
|
765
|
+
padding: "2px 4px",
|
|
766
|
+
lineHeight: 1
|
|
767
|
+
},
|
|
768
|
+
children: "\xD7"
|
|
769
|
+
}
|
|
770
|
+
)
|
|
771
|
+
]
|
|
772
|
+
}
|
|
773
|
+
),
|
|
774
|
+
/* @__PURE__ */ jsx4(
|
|
775
|
+
"div",
|
|
776
|
+
{
|
|
777
|
+
style: {
|
|
778
|
+
fontSize: "11px",
|
|
779
|
+
color: "#a3a3a3",
|
|
780
|
+
marginBottom: "12px"
|
|
781
|
+
},
|
|
782
|
+
children: "Alt+H to toggle \u2022 Ctrl+Shift+V to open"
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
scopeEntries.length === 0 ? /* @__PURE__ */ jsx4("div", { style: { color: "#a3a3a3", fontSize: "12px" }, children: "No variant scopes registered yet." }) : /* @__PURE__ */ jsx4("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: scopeEntries.map(([scopeName, state]) => /* @__PURE__ */ jsx4(
|
|
786
|
+
ScopeControl,
|
|
787
|
+
{
|
|
788
|
+
name: scopeName,
|
|
789
|
+
state,
|
|
790
|
+
onSelect: (variant) => ctx.setActiveVariant(scopeName, variant)
|
|
791
|
+
},
|
|
792
|
+
scopeName
|
|
793
|
+
)) }),
|
|
794
|
+
/* @__PURE__ */ jsx4(
|
|
795
|
+
"div",
|
|
796
|
+
{
|
|
797
|
+
style: {
|
|
798
|
+
marginTop: "16px",
|
|
799
|
+
paddingTop: "12px",
|
|
800
|
+
borderTop: "1px solid #f0f0f0"
|
|
801
|
+
},
|
|
802
|
+
children: /* @__PURE__ */ jsx4(
|
|
803
|
+
"button",
|
|
804
|
+
{
|
|
805
|
+
onClick: ctx.toggleUiVisible,
|
|
806
|
+
style: {
|
|
807
|
+
width: "100%",
|
|
808
|
+
padding: "7px 12px",
|
|
809
|
+
border: "1px solid #e5e5e5",
|
|
810
|
+
borderRadius: "6px",
|
|
811
|
+
background: "none",
|
|
812
|
+
cursor: "pointer",
|
|
813
|
+
fontSize: "12px",
|
|
814
|
+
color: "#737373",
|
|
815
|
+
textAlign: "center"
|
|
816
|
+
},
|
|
817
|
+
children: ctx.uiVisible ? "Hide switchers (Alt+H)" : "Show switchers (Alt+H)"
|
|
818
|
+
}
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
)
|
|
822
|
+
]
|
|
823
|
+
}
|
|
824
|
+
)
|
|
825
|
+
] });
|
|
826
|
+
}
|
|
827
|
+
function ScopeControl({ name, state, onSelect }) {
|
|
828
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
829
|
+
/* @__PURE__ */ jsx4(
|
|
830
|
+
"div",
|
|
831
|
+
{
|
|
832
|
+
style: {
|
|
833
|
+
fontWeight: 600,
|
|
834
|
+
color: "#404040",
|
|
835
|
+
marginBottom: "6px",
|
|
836
|
+
fontSize: "12px"
|
|
837
|
+
},
|
|
838
|
+
children: name
|
|
839
|
+
}
|
|
840
|
+
),
|
|
841
|
+
/* @__PURE__ */ jsx4("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px" }, children: state.variantNames.map((key) => {
|
|
842
|
+
const isActive = key === state.activeVariant;
|
|
843
|
+
return /* @__PURE__ */ jsx4(
|
|
844
|
+
"button",
|
|
845
|
+
{
|
|
846
|
+
role: "radio",
|
|
847
|
+
"aria-checked": isActive,
|
|
848
|
+
"aria-label": `Switch ${name} to ${key}`,
|
|
849
|
+
onClick: () => onSelect(key),
|
|
850
|
+
style: {
|
|
851
|
+
cursor: "pointer",
|
|
852
|
+
border: isActive ? "2px solid #171717" : "1px solid #e5e5e5",
|
|
853
|
+
borderRadius: "5px",
|
|
854
|
+
padding: "3px 10px",
|
|
855
|
+
fontSize: "11px",
|
|
856
|
+
fontWeight: isActive ? 600 : 400,
|
|
857
|
+
background: isActive ? "#171717" : "#fafafa",
|
|
858
|
+
color: isActive ? "#fff" : "#404040",
|
|
859
|
+
transition: "all 0.12s"
|
|
860
|
+
},
|
|
861
|
+
children: key
|
|
862
|
+
},
|
|
863
|
+
key
|
|
864
|
+
);
|
|
865
|
+
}) })
|
|
866
|
+
] });
|
|
867
|
+
}
|
|
868
|
+
export {
|
|
869
|
+
PageVariantSwitcher,
|
|
870
|
+
VariantContext,
|
|
871
|
+
VariantDevToolbar,
|
|
872
|
+
VariantProvider,
|
|
873
|
+
VariantSwitcher,
|
|
874
|
+
clearVariantRegistry,
|
|
875
|
+
getRegisteredVariant,
|
|
876
|
+
readUiVisibleFromStorage,
|
|
877
|
+
readVariantFromStorage,
|
|
878
|
+
readVariantFromUrl,
|
|
879
|
+
registerVariant,
|
|
880
|
+
removeVariantFromStorage,
|
|
881
|
+
removeVariantFromUrl,
|
|
882
|
+
resolveActiveVariant,
|
|
883
|
+
useActiveVariant,
|
|
884
|
+
useKeyboardShortcuts,
|
|
885
|
+
useVariant,
|
|
886
|
+
useVariantContext,
|
|
887
|
+
writeUiVisibleToStorage,
|
|
888
|
+
writeVariantToStorage,
|
|
889
|
+
writeVariantToUrl
|
|
890
|
+
};
|