@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.
@@ -0,0 +1 @@
1
+ "use strict";var VibeflowPrototyping=(()=>{var J=Object.defineProperty;var vt=Object.getOwnPropertyDescriptor;var St=Object.getOwnPropertyNames;var Vt=Object.prototype.hasOwnProperty;var x=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,n)=>(typeof require<"u"?require:e)[n]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var Ct=(t,e)=>{for(var n in e)J(t,n,{get:e[n],enumerable:!0})},Pt=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of St(e))!Vt.call(t,o)&&o!==n&&J(t,o,{get:()=>e[o],enumerable:!(r=vt(e,o))||r.enumerable});return t};var kt=t=>Pt(J({},"__esModule",{value:!0}),t);var Mt={};Ct(Mt,{PageVariantSwitcher:()=>ft,VariantContext:()=>O,VariantDevToolbar:()=>xt,VariantProvider:()=>at,VariantSwitcher:()=>pt,clearVariantRegistry:()=>ct,getRegisteredVariant:()=>z,readUiVisibleFromStorage:()=>F,readVariantFromStorage:()=>Z,readVariantFromUrl:()=>G,registerVariant:()=>lt,removeVariantFromStorage:()=>nt,removeVariantFromUrl:()=>rt,resolveActiveVariant:()=>E,useActiveVariant:()=>P,useKeyboardShortcuts:()=>$,useVariant:()=>dt,useVariantContext:()=>m,writeUiVisibleToStorage:()=>L,writeVariantToStorage:()=>A,writeVariantToUrl:()=>_});var g=x("react");var Rt="__vf__";function q(t){return`vf[${t}]`}function G(t){return typeof window>"u"?null:new URLSearchParams(window.location.search).get(q(t))}function _(t,e){if(typeof window>"u")return;let n=new URLSearchParams(window.location.search);n.set(q(t),e);let r=n.toString(),o=`${window.location.pathname}?${r}${window.location.hash}`;window.history.pushState(null,"",o)}function rt(t){if(typeof window>"u")return;let e=new URLSearchParams(window.location.search);e.delete(q(t));let n=e.toString(),r=n?`${window.location.pathname}?${n}${window.location.hash}`:`${window.location.pathname}${window.location.hash}`;window.history.pushState(null,"",r)}function Q(t){return`${Rt}${t}`}function Z(t){if(typeof window>"u")return null;try{return window.localStorage.getItem(Q(t))}catch{return null}}function A(t,e){if(!(typeof window>"u"))try{window.localStorage.setItem(Q(t),e)}catch{}}function nt(t){if(!(typeof window>"u"))try{window.localStorage.removeItem(Q(t))}catch{}}var ot="__vf__ui_visible__";function L(t){if(!(typeof window>"u"))try{window.localStorage.setItem(ot,String(t))}catch{}}function F(){if(typeof window>"u")return null;try{let t=window.localStorage.getItem(ot);return t===null?null:t!=="false"}catch{return null}}function E(t,e,n){let r=G(t);if(r&&e.includes(r))return r;let o=Z(t);return o&&e.includes(o)?o:n}var it=x("react"),Et=[{key:"h",alt:!0},{key:"V",ctrl:!0,shift:!0}];function It(t,e){return!(t.key!==e.key||!!e.alt!==t.altKey||!!e.ctrl!==t.ctrlKey||!!e.shift!==t.shiftKey||!!e.meta!==t.metaKey)}function $({onToggleUi:t,shortcuts:e=Et}){(0,it.useEffect)(()=>{if(e===!1)return;let n=r=>{for(let o of e)if(It(r,o)){r.preventDefault(),t();return}};return window.addEventListener("keydown",n),()=>window.removeEventListener("keydown",n)},[t,e])}var st=x("react/jsx-runtime"),O=(0,g.createContext)(null);function m(){let t=(0,g.useContext)(O);if(!t)throw new Error("useVariantContext must be used inside <VariantProvider>");return t}function Ut(t,e){if(t==="always")return e??!0;let n=F();return n!==null?n:e??!0}function at({children:t,mode:e="dev",defaultVisible:n,shortcuts:r}){let[o,d]=(0,g.useState)({}),[p,S]=(0,g.useState)(()=>Ut(e,n)),C=(0,g.useRef)(new Set),k=(0,g.useCallback)((u,c)=>{d(f=>{let h=f[u];if(h&&h.variantNames.length===c.length&&h.variantNames.every((X,Y)=>X===c[Y]))return f;let D=c[0]??"default",B=E(u,c,h?.activeVariant??D);return{...f,[u]:{activeVariant:B,variantNames:c}}})},[]),R=(0,g.useCallback)(u=>o[u]?.activeVariant??"",[o]),s=(0,g.useCallback)((u,c)=>{d(f=>{let h=f[u];return!h||h.activeVariant===c?f:{...f,[u]:{...h,activeVariant:c}}}),_(u,c),A(u,c)},[]),v=(0,g.useCallback)(()=>{S(u=>{let c=!u;return L(c),c})},[]),b=(0,g.useCallback)(u=>C.current.has(u)?!1:(C.current.add(u),!0),[]),U=(0,g.useCallback)(u=>{C.current.delete(u)},[]);$({onToggleUi:v,shortcuts:r});let M={getActiveVariant:R,setActiveVariant:s,registerScope:k,registerSwitcher:b,unregisterSwitcher:U,scopes:o,uiVisible:p,toggleUiVisible:v,mode:e};return(0,st.jsx)(O.Provider,{value:M,children:t})}var T=x("react");var tt=new Map;function lt(t,e){tt.set(t,e)}function z(t){return tt.get(t)}function ct(){tt.clear()}var ut=x("react");function P(t,e){let n=m();return(0,ut.useMemo)(()=>{let r=n.getActiveVariant(t);return r&&e.includes(r)?r:E(t,e,e[0]??"default")},[n,t,e])}function dt(t,e){let n=m(),r=(0,T.useMemo)(()=>{let p=z(t);return p?{...p,...e}:e},[t,e]),o=(0,T.useMemo)(()=>Object.keys(r),[r]);(0,T.useEffect)(()=>{n.registerScope(t,o)},[n,t,o]);let d=P(t,o);return r[d]??r[o[0]??"default"]??{}}var H=x("react");var et=x("react/jsx-runtime");function ft({name:t,variants:e}){let n=m(),r=(0,H.useMemo)(()=>Object.keys(e),[e]);(0,H.useEffect)(()=>{n.registerScope(t,r)},[n,t,r]);let o=P(t,r);return!n.uiVisible||r.length<2?null:(0,et.jsx)("div",{role:"toolbar","aria-label":`Page variant switcher: ${t}`,style:{position:"fixed",top:"12px",left:"12px",zIndex:99999,display:"flex",alignItems:"center",background:"#171717",borderRadius:"8px",padding:"4px",gap:"2px",boxShadow:"0 2px 8px rgba(0,0,0,0.32)",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",fontSize:"12px",userSelect:"none"},children:r.map(d=>{let p=d===o;return(0,et.jsx)("button",{role:"radio","aria-checked":p,"aria-label":`Switch to ${d} variant`,onClick:()=>n.setActiveVariant(t,d),style:{cursor:"pointer",border:"none",outline:"none",borderRadius:"5px",padding:"5px 12px",fontSize:"12px",fontWeight:p?600:400,background:p?"#ffffff":"transparent",color:p?"#171717":"#a3a3a3",transition:"background 0.15s, color 0.15s",whiteSpace:"nowrap"},children:d},d)})})}var l=x("react");var I=x("react/jsx-runtime");function pt({name:t,variants:e,position:n="right"}){let r=m(),o=(0,l.useMemo)(()=>Object.keys(e),[e]),[d,p]=(0,l.useState)(!1),S=(0,l.useRef)(null),[C,k]=(0,l.useState)(!1);(0,l.useEffect)(()=>{let i=r.registerSwitcher(t);return k(i),()=>{r.unregisterSwitcher(t)}},[r,t]),(0,l.useEffect)(()=>{r.registerScope(t,o)},[r,t,o]);let R=P(t,o),[s,v]=(0,l.useState)(()=>{try{let i=localStorage.getItem(`vf-variant-pos-${t}`);if(i){let y=JSON.parse(i);if(typeof y.x=="number"&&typeof y.y=="number")return y}}catch{}return null}),[b,U]=(0,l.useState)(!1),[M,u]=(0,l.useState)(!1),c=(0,l.useRef)(null),f=(0,l.useRef)(null),h=(0,l.useRef)(!1),D=(0,l.useRef)(null);D.current=s,(0,l.useEffect)(()=>{if(s!==null)try{localStorage.setItem(`vf-variant-pos-${t}`,JSON.stringify(s))}catch{}},[s,t]),(0,l.useEffect)(()=>()=>{f.current!==null&&window.clearTimeout(f.current)},[]);function B(){if(S.current){let i=S.current.getBoundingClientRect();return{x:i.left,y:i.top}}return{x:window.innerWidth-48,y:window.innerHeight/2}}function X(i){let y=i.currentTarget,V=i.pointerId,W=i.clientX,N=i.clientY;h.current=!1,f.current=window.setTimeout(()=>{u(!0);let K=D.current??B();c.current={mouseX:W,mouseY:N,posX:K.x,posY:K.y},y.setPointerCapture(V)},300)}function Y(i){if(!c.current)return;let y=i.clientX-c.current.mouseX,V=i.clientY-c.current.mouseY;!b&&(Math.abs(y)>3||Math.abs(V)>3)&&U(!0),h.current=!0;let W=window.innerWidth,N=window.innerHeight,K=Math.max(8,Math.min(W-40,c.current.posX+y)),mt=Math.max(8,Math.min(N-40,c.current.posY+V));v({x:K,y:mt})}function wt(){f.current!==null&&(window.clearTimeout(f.current),f.current=null);let i=h.current;c.current=null,U(!1),u(!1),h.current=!1,i||p(!0)}function bt(){f.current!==null&&(window.clearTimeout(f.current),f.current=null),c.current=null,U(!1),u(!1),h.current=!1}let j=(0,l.useCallback)(i=>{S.current&&!S.current.contains(i.target)&&p(!1)},[]);if((0,l.useEffect)(()=>{if(d)return document.addEventListener("mousedown",j),()=>document.removeEventListener("mousedown",j)},[d,j]),(0,l.useEffect)(()=>{if(!d)return;let i=y=>{y.key==="Escape"&&p(!1)};return document.addEventListener("keydown",i),()=>document.removeEventListener("keydown",i)},[d]),!r.uiVisible||o.length<2||!C)return null;let ht=s!==null?{position:"fixed",left:s.x,top:s.y,transform:"none",zIndex:9999}:{position:"absolute",top:"50%",transform:"translateY(-50%)",...n==="left"?{left:"-24px",right:"auto"}:{right:"-24px",left:"auto"},zIndex:9999},yt=b?"Drag to reposition":M?"Drag to reposition \xB7 Release to expand":`${t} variants \u2014 click to switch \xB7 hold to drag`;return(0,I.jsx)("div",{ref:S,role:"toolbar","aria-label":`Component variant switcher: ${t}`,className:"vf-variant-switcher",style:ht,children:d?(0,I.jsx)("div",{style:{display:"flex",flexDirection:"column",gap:"3px",background:"#fff",border:"1px solid #e5e5e5",borderRadius:"4px",boxShadow:"0 1px 3px rgba(0,0,0,0.08)",padding:"3px",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"},children:o.map((i,y)=>{let V=i===R;return(0,I.jsx)("button",{role:"radio","aria-checked":V,"aria-label":`Switch to ${i} variant (${y+1})`,onClick:()=>{r.setActiveVariant(t,i),p(!1)},title:i,style:{cursor:"pointer",border:"none",outline:"none",width:"22px",height:"22px",borderRadius:"3px",fontSize:"10px",fontWeight:V?700:400,background:V?"#171717":"transparent",color:V?"#fff":"#737373",display:"flex",alignItems:"center",justifyContent:"center",transition:"background 0.12s, color 0.12s",padding:0,lineHeight:1},children:y+1},i)})}):(0,I.jsx)("button",{"aria-label":`Open variant switcher for ${t}`,title:yt,onPointerDown:X,onPointerMove:Y,onPointerUp:wt,onPointerCancel:bt,style:{cursor:b?"grabbing":M?"grab":"pointer",border:"1px solid #e5e5e5",outline:"none",width:"14px",height:"14px",borderRadius:"50%",background:b?"#e5e5e5":"#f5f5f5",padding:0,display:"flex",alignItems:"center",justifyContent:"center",boxShadow:b?"0 2px 8px rgba(0,0,0,0.14)":"0 1px 2px rgba(0,0,0,0.06)",transition:b?"none":"background 0.15s, border-color 0.15s"},onMouseEnter:i=>{b||(i.currentTarget.style.background="#e5e5e5",i.currentTarget.style.borderColor="#d4d4d4")},onMouseLeave:i=>{b||(i.currentTarget.style.background="#f5f5f5",i.currentTarget.style.borderColor="#e5e5e5")},children:(0,I.jsx)("span",{style:{width:"5px",height:"5px",borderRadius:"50%",background:"#a3a3a3"}})})})}var w=x("react");var a=x("react/jsx-runtime");function gt(){return typeof document>"u"?!1:!!document.getElementById("vibeflow-studio-root")}function xt(){let t=m(),[e,n]=(0,w.useState)(!1),[r,o]=(0,w.useState)(()=>gt()),d=(0,w.useRef)(null),p=(0,w.useRef)(e);p.current=e,(0,w.useEffect)(()=>{if(r)return;function s(){return gt()?(o(!0),!0):!1}if(s())return;let v=new MutationObserver(()=>{s()});v.observe(document.body,{childList:!0,subtree:!0});let b=setInterval(()=>{s()},500);return()=>{v.disconnect(),clearInterval(b)}},[r]);let S=(0,w.useCallback)(()=>n(!0),[]),C=(0,w.useCallback)(()=>n(!1),[]);(0,w.useEffect)(()=>{if(!r)return;let s={openPanel:S,closePanel:C,get isOpen(){return p.current}};return Object.defineProperty(window,"__vf_prototyping",{value:s,writable:!0,configurable:!0}),()=>{window.__vf_prototyping===s&&delete window.__vf_prototyping}},[r,S,C]),(0,w.useEffect)(()=>{if(!e)return;let s=v=>{v.key==="Escape"&&n(!1)};return document.addEventListener("keydown",s),()=>document.removeEventListener("keydown",s)},[e]);let k=(0,w.useCallback)(s=>{d.current&&!d.current.contains(s.target)&&n(!1)},[]);(0,w.useEffect)(()=>{if(e)return document.addEventListener("mousedown",k),()=>document.removeEventListener("mousedown",k)},[e,k]);let R=Object.entries(t.scopes);return!t.uiVisible&&!e?null:(0,a.jsxs)(a.Fragment,{children:[t.uiVisible&&!r&&(0,a.jsx)("button",{"aria-label":"Toggle variant dev toolbar","aria-expanded":e,onClick:()=>n(s=>!s),style:{position:"fixed",bottom:"16px",right:"16px",zIndex:99998,width:"40px",height:"40px",borderRadius:"50%",background:"#6366f1",color:"#fff",border:"none",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.32)",transition:"background 0.15s",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"},title:"Open variant dev toolbar (Ctrl+Shift+V)",children:(0,a.jsxs)("svg",{width:"20",height:"20",viewBox:"0 0 18 18",fill:"none","aria-hidden":"true",children:[(0,a.jsx)("rect",{x:"2.5",y:"5",width:"2",height:"8",rx:"1",fill:"currentColor",opacity:"0.7"}),(0,a.jsx)("rect",{x:"6.5",y:"2",width:"2",height:"14",rx:"1",fill:"currentColor"}),(0,a.jsx)("rect",{x:"10.5",y:"6",width:"2",height:"6",rx:"1",fill:"currentColor",opacity:"0.7"}),(0,a.jsx)("rect",{x:"14.5",y:"4",width:"2",height:"10",rx:"1",fill:"currentColor",opacity:"0.85"})]})}),e&&(0,a.jsxs)("div",{ref:d,role:"dialog","aria-label":"Variant dev toolbar",style:{position:"fixed",bottom:"68px",right:"16px",zIndex:99999,background:"#fff",border:"1px solid #e5e5e5",borderRadius:"12px",boxShadow:"0 8px 32px rgba(0,0,0,0.16)",padding:"16px",minWidth:"240px",maxWidth:"360px",maxHeight:"70vh",overflowY:"auto",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",fontSize:"13px"},children:[(0,a.jsxs)("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:"12px"},children:[(0,a.jsxs)("span",{style:{fontWeight:700,color:"#171717",fontSize:"13px",display:"flex",alignItems:"center",gap:"6px"},children:[(0,a.jsxs)("svg",{width:"14",height:"14",viewBox:"0 0 18 18",fill:"none","aria-hidden":"true",children:[(0,a.jsx)("rect",{x:"2.5",y:"5",width:"2",height:"8",rx:"1",fill:"#6366f1",opacity:"0.7"}),(0,a.jsx)("rect",{x:"6.5",y:"2",width:"2",height:"14",rx:"1",fill:"#6366f1"}),(0,a.jsx)("rect",{x:"10.5",y:"6",width:"2",height:"6",rx:"1",fill:"#6366f1",opacity:"0.7"}),(0,a.jsx)("rect",{x:"14.5",y:"4",width:"2",height:"10",rx:"1",fill:"#6366f1",opacity:"0.85"})]}),"Variant Switcher"]}),(0,a.jsx)("button",{"aria-label":"Close toolbar",onClick:()=>n(!1),style:{border:"none",background:"none",cursor:"pointer",fontSize:"16px",color:"#737373",padding:"2px 4px",lineHeight:1},children:"\xD7"})]}),(0,a.jsx)("div",{style:{fontSize:"11px",color:"#a3a3a3",marginBottom:"12px"},children:"Alt+H to toggle \u2022 Ctrl+Shift+V to open"}),R.length===0?(0,a.jsx)("div",{style:{color:"#a3a3a3",fontSize:"12px"},children:"No variant scopes registered yet."}):(0,a.jsx)("div",{style:{display:"flex",flexDirection:"column",gap:"12px"},children:R.map(([s,v])=>(0,a.jsx)(Tt,{name:s,state:v,onSelect:b=>t.setActiveVariant(s,b)},s))}),(0,a.jsx)("div",{style:{marginTop:"16px",paddingTop:"12px",borderTop:"1px solid #f0f0f0"},children:(0,a.jsx)("button",{onClick:t.toggleUiVisible,style:{width:"100%",padding:"7px 12px",border:"1px solid #e5e5e5",borderRadius:"6px",background:"none",cursor:"pointer",fontSize:"12px",color:"#737373",textAlign:"center"},children:t.uiVisible?"Hide switchers (Alt+H)":"Show switchers (Alt+H)"})})]})]})}function Tt({name:t,state:e,onSelect:n}){return(0,a.jsxs)("div",{children:[(0,a.jsx)("div",{style:{fontWeight:600,color:"#404040",marginBottom:"6px",fontSize:"12px"},children:t}),(0,a.jsx)("div",{style:{display:"flex",flexWrap:"wrap",gap:"4px"},children:e.variantNames.map(r=>{let o=r===e.activeVariant;return(0,a.jsx)("button",{role:"radio","aria-checked":o,"aria-label":`Switch ${t} to ${r}`,onClick:()=>n(r),style:{cursor:"pointer",border:o?"2px solid #171717":"1px solid #e5e5e5",borderRadius:"5px",padding:"3px 10px",fontSize:"11px",fontWeight:o?600:400,background:o?"#171717":"#fafafa",color:o?"#fff":"#404040",transition:"all 0.12s"},children:r},r)})})]})}return kt(Mt);})();
@@ -0,0 +1 @@
1
+ import{createContext as yt,useContext as mt,useState as rt,useCallback as P,useRef as vt}from"react";var pt="__vf__";function H(t){return`vf[${t}]`}function Z(t){return typeof window>"u"?null:new URLSearchParams(window.location.search).get(H(t))}function B(t,e){if(typeof window>"u")return;let n=new URLSearchParams(window.location.search);n.set(H(t),e);let r=n.toString(),i=`${window.location.pathname}?${r}${window.location.hash}`;window.history.pushState(null,"",i)}function gt(t){if(typeof window>"u")return;let e=new URLSearchParams(window.location.search);e.delete(H(t));let n=e.toString(),r=n?`${window.location.pathname}?${n}${window.location.hash}`:`${window.location.pathname}${window.location.hash}`;window.history.pushState(null,"",r)}function X(t){return`${pt}${t}`}function tt(t){if(typeof window>"u")return null;try{return window.localStorage.getItem(X(t))}catch{return null}}function Y(t,e){if(!(typeof window>"u"))try{window.localStorage.setItem(X(t),e)}catch{}}function xt(t){if(!(typeof window>"u"))try{window.localStorage.removeItem(X(t))}catch{}}var et="__vf__ui_visible__";function j(t){if(!(typeof window>"u"))try{window.localStorage.setItem(et,String(t))}catch{}}function W(){if(typeof window>"u")return null;try{let t=window.localStorage.getItem(et);return t===null?null:t!=="false"}catch{return null}}function E(t,e,n){let r=Z(t);if(r&&e.includes(r))return r;let i=tt(t);return i&&e.includes(i)?i:n}import{useEffect as wt}from"react";var bt=[{key:"h",alt:!0},{key:"V",ctrl:!0,shift:!0}];function ht(t,e){return!(t.key!==e.key||!!e.alt!==t.altKey||!!e.ctrl!==t.ctrlKey||!!e.shift!==t.shiftKey||!!e.meta!==t.metaKey)}function N({onToggleUi:t,shortcuts:e=bt}){wt(()=>{if(e===!1)return;let n=r=>{for(let i of e)if(ht(r,i)){r.preventDefault(),t();return}};return window.addEventListener("keydown",n),()=>window.removeEventListener("keydown",n)},[t,e])}import{jsx as Ct}from"react/jsx-runtime";var J=yt(null);function b(){let t=mt(J);if(!t)throw new Error("useVariantContext must be used inside <VariantProvider>");return t}function St(t,e){if(t==="always")return e??!0;let n=W();return n!==null?n:e??!0}function Vt({children:t,mode:e="dev",defaultVisible:n,shortcuts:r}){let[i,c]=rt({}),[f,h]=rt(()=>St(e,n)),m=vt(new Set),v=P((l,s)=>{c(d=>{let g=d[l];if(g&&g.variantNames.length===s.length&&g.variantNames.every((L,F)=>L===s[F]))return d;let D=s[0]??"default",A=E(l,s,g?.activeVariant??D);return{...d,[l]:{activeVariant:A,variantNames:s}}})},[]),C=P(l=>i[l]?.activeVariant??"",[i]),a=P((l,s)=>{c(d=>{let g=d[l];return!g||g.activeVariant===s?d:{...d,[l]:{...g,activeVariant:s}}}),B(l,s),Y(l,s)},[]),w=P(()=>{h(l=>{let s=!l;return j(s),s})},[]),p=P(l=>m.current.has(l)?!1:(m.current.add(l),!0),[]),R=P(l=>{m.current.delete(l)},[]);N({onToggleUi:w,shortcuts:r});let M={getActiveVariant:C,setActiveVariant:a,registerScope:v,registerSwitcher:p,unregisterSwitcher:R,scopes:i,uiVisible:f,toggleUiVisible:w,mode:e};return Ct(J.Provider,{value:M,children:t})}import{useEffect as Et,useMemo as nt}from"react";var q=new Map;function Pt(t,e){q.set(t,e)}function G(t){return q.get(t)}function kt(){q.clear()}import{useMemo as Rt}from"react";function S(t,e){let n=b();return Rt(()=>{let r=n.getActiveVariant(t);return r&&e.includes(r)?r:E(t,e,e[0]??"default")},[n,t,e])}function It(t,e){let n=b(),r=nt(()=>{let f=G(t);return f?{...f,...e}:e},[t,e]),i=nt(()=>Object.keys(r),[r]);Et(()=>{n.registerScope(t,i)},[n,t,i]);let c=S(t,i);return r[c]??r[i[0]??"default"]??{}}import{useEffect as Ut,useMemo as Tt}from"react";import{jsx as ot}from"react/jsx-runtime";function Mt({name:t,variants:e}){let n=b(),r=Tt(()=>Object.keys(e),[e]);Ut(()=>{n.registerScope(t,r)},[n,t,r]);let i=S(t,r);return!n.uiVisible||r.length<2?null:ot("div",{role:"toolbar","aria-label":`Page variant switcher: ${t}`,style:{position:"fixed",top:"12px",left:"12px",zIndex:99999,display:"flex",alignItems:"center",background:"#171717",borderRadius:"8px",padding:"4px",gap:"2px",boxShadow:"0 2px 8px rgba(0,0,0,0.32)",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",fontSize:"12px",userSelect:"none"},children:r.map(c=>{let f=c===i;return ot("button",{role:"radio","aria-checked":f,"aria-label":`Switch to ${c} variant`,onClick:()=>n.setActiveVariant(t,c),style:{cursor:"pointer",border:"none",outline:"none",borderRadius:"5px",padding:"5px 12px",fontSize:"12px",fontWeight:f?600:400,background:f?"#ffffff":"transparent",color:f?"#171717":"#a3a3a3",transition:"background 0.15s, color 0.15s",whiteSpace:"nowrap"},children:c},c)})})}import{useEffect as k,useMemo as Dt,useState as I,useCallback as Kt,useRef as U}from"react";import{jsx as T}from"react/jsx-runtime";function _t({name:t,variants:e,position:n="right"}){let r=b(),i=Dt(()=>Object.keys(e),[e]),[c,f]=I(!1),h=U(null),[m,v]=I(!1);k(()=>{let o=r.registerSwitcher(t);return v(o),()=>{r.unregisterSwitcher(t)}},[r,t]),k(()=>{r.registerScope(t,i)},[r,t,i]);let C=S(t,i),[a,w]=I(()=>{try{let o=localStorage.getItem(`vf-variant-pos-${t}`);if(o){let x=JSON.parse(o);if(typeof x.x=="number"&&typeof x.y=="number")return x}}catch{}return null}),[p,R]=I(!1),[M,l]=I(!1),s=U(null),d=U(null),g=U(!1),D=U(null);D.current=a,k(()=>{if(a!==null)try{localStorage.setItem(`vf-variant-pos-${t}`,JSON.stringify(a))}catch{}},[a,t]),k(()=>()=>{d.current!==null&&window.clearTimeout(d.current)},[]);function A(){if(h.current){let o=h.current.getBoundingClientRect();return{x:o.left,y:o.top}}return{x:window.innerWidth-48,y:window.innerHeight/2}}function L(o){let x=o.currentTarget,y=o.pointerId,O=o.clientX,z=o.clientY;g.current=!1,d.current=window.setTimeout(()=>{l(!0);let K=D.current??A();s.current={mouseX:O,mouseY:z,posX:K.x,posY:K.y},x.setPointerCapture(y)},300)}function F(o){if(!s.current)return;let x=o.clientX-s.current.mouseX,y=o.clientY-s.current.mouseY;!p&&(Math.abs(x)>3||Math.abs(y)>3)&&R(!0),g.current=!0;let O=window.innerWidth,z=window.innerHeight,K=Math.max(8,Math.min(O-40,s.current.posX+x)),ft=Math.max(8,Math.min(z-40,s.current.posY+y));w({x:K,y:ft})}function lt(){d.current!==null&&(window.clearTimeout(d.current),d.current=null);let o=g.current;s.current=null,R(!1),l(!1),g.current=!1,o||f(!0)}function ct(){d.current!==null&&(window.clearTimeout(d.current),d.current=null),s.current=null,R(!1),l(!1),g.current=!1}let $=Kt(o=>{h.current&&!h.current.contains(o.target)&&f(!1)},[]);if(k(()=>{if(c)return document.addEventListener("mousedown",$),()=>document.removeEventListener("mousedown",$)},[c,$]),k(()=>{if(!c)return;let o=x=>{x.key==="Escape"&&f(!1)};return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[c]),!r.uiVisible||i.length<2||!m)return null;let ut=a!==null?{position:"fixed",left:a.x,top:a.y,transform:"none",zIndex:9999}:{position:"absolute",top:"50%",transform:"translateY(-50%)",...n==="left"?{left:"-24px",right:"auto"}:{right:"-24px",left:"auto"},zIndex:9999},dt=p?"Drag to reposition":M?"Drag to reposition \xB7 Release to expand":`${t} variants \u2014 click to switch \xB7 hold to drag`;return T("div",{ref:h,role:"toolbar","aria-label":`Component variant switcher: ${t}`,className:"vf-variant-switcher",style:ut,children:c?T("div",{style:{display:"flex",flexDirection:"column",gap:"3px",background:"#fff",border:"1px solid #e5e5e5",borderRadius:"4px",boxShadow:"0 1px 3px rgba(0,0,0,0.08)",padding:"3px",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"},children:i.map((o,x)=>{let y=o===C;return T("button",{role:"radio","aria-checked":y,"aria-label":`Switch to ${o} variant (${x+1})`,onClick:()=>{r.setActiveVariant(t,o),f(!1)},title:o,style:{cursor:"pointer",border:"none",outline:"none",width:"22px",height:"22px",borderRadius:"3px",fontSize:"10px",fontWeight:y?700:400,background:y?"#171717":"transparent",color:y?"#fff":"#737373",display:"flex",alignItems:"center",justifyContent:"center",transition:"background 0.12s, color 0.12s",padding:0,lineHeight:1},children:x+1},o)})}):T("button",{"aria-label":`Open variant switcher for ${t}`,title:dt,onPointerDown:L,onPointerMove:F,onPointerUp:lt,onPointerCancel:ct,style:{cursor:p?"grabbing":M?"grab":"pointer",border:"1px solid #e5e5e5",outline:"none",width:"14px",height:"14px",borderRadius:"50%",background:p?"#e5e5e5":"#f5f5f5",padding:0,display:"flex",alignItems:"center",justifyContent:"center",boxShadow:p?"0 2px 8px rgba(0,0,0,0.14)":"0 1px 2px rgba(0,0,0,0.06)",transition:p?"none":"background 0.15s, border-color 0.15s"},onMouseEnter:o=>{p||(o.currentTarget.style.background="#e5e5e5",o.currentTarget.style.borderColor="#d4d4d4")},onMouseLeave:o=>{p||(o.currentTarget.style.background="#f5f5f5",o.currentTarget.style.borderColor="#e5e5e5")},children:T("span",{style:{width:"5px",height:"5px",borderRadius:"50%",background:"#a3a3a3"}})})})}import{useState as it,useEffect as _,useCallback as Q,useRef as at}from"react";import{Fragment as Ft,jsx as u,jsxs as V}from"react/jsx-runtime";function st(){return typeof document>"u"?!1:!!document.getElementById("vibeflow-studio-root")}function At(){let t=b(),[e,n]=it(!1),[r,i]=it(()=>st()),c=at(null),f=at(e);f.current=e,_(()=>{if(r)return;function a(){return st()?(i(!0),!0):!1}if(a())return;let w=new MutationObserver(()=>{a()});w.observe(document.body,{childList:!0,subtree:!0});let p=setInterval(()=>{a()},500);return()=>{w.disconnect(),clearInterval(p)}},[r]);let h=Q(()=>n(!0),[]),m=Q(()=>n(!1),[]);_(()=>{if(!r)return;let a={openPanel:h,closePanel:m,get isOpen(){return f.current}};return Object.defineProperty(window,"__vf_prototyping",{value:a,writable:!0,configurable:!0}),()=>{window.__vf_prototyping===a&&delete window.__vf_prototyping}},[r,h,m]),_(()=>{if(!e)return;let a=w=>{w.key==="Escape"&&n(!1)};return document.addEventListener("keydown",a),()=>document.removeEventListener("keydown",a)},[e]);let v=Q(a=>{c.current&&!c.current.contains(a.target)&&n(!1)},[]);_(()=>{if(e)return document.addEventListener("mousedown",v),()=>document.removeEventListener("mousedown",v)},[e,v]);let C=Object.entries(t.scopes);return!t.uiVisible&&!e?null:V(Ft,{children:[t.uiVisible&&!r&&u("button",{"aria-label":"Toggle variant dev toolbar","aria-expanded":e,onClick:()=>n(a=>!a),style:{position:"fixed",bottom:"16px",right:"16px",zIndex:99998,width:"40px",height:"40px",borderRadius:"50%",background:"#6366f1",color:"#fff",border:"none",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.32)",transition:"background 0.15s",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"},title:"Open variant dev toolbar (Ctrl+Shift+V)",children:V("svg",{width:"20",height:"20",viewBox:"0 0 18 18",fill:"none","aria-hidden":"true",children:[u("rect",{x:"2.5",y:"5",width:"2",height:"8",rx:"1",fill:"currentColor",opacity:"0.7"}),u("rect",{x:"6.5",y:"2",width:"2",height:"14",rx:"1",fill:"currentColor"}),u("rect",{x:"10.5",y:"6",width:"2",height:"6",rx:"1",fill:"currentColor",opacity:"0.7"}),u("rect",{x:"14.5",y:"4",width:"2",height:"10",rx:"1",fill:"currentColor",opacity:"0.85"})]})}),e&&V("div",{ref:c,role:"dialog","aria-label":"Variant dev toolbar",style:{position:"fixed",bottom:"68px",right:"16px",zIndex:99999,background:"#fff",border:"1px solid #e5e5e5",borderRadius:"12px",boxShadow:"0 8px 32px rgba(0,0,0,0.16)",padding:"16px",minWidth:"240px",maxWidth:"360px",maxHeight:"70vh",overflowY:"auto",fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",fontSize:"13px"},children:[V("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:"12px"},children:[V("span",{style:{fontWeight:700,color:"#171717",fontSize:"13px",display:"flex",alignItems:"center",gap:"6px"},children:[V("svg",{width:"14",height:"14",viewBox:"0 0 18 18",fill:"none","aria-hidden":"true",children:[u("rect",{x:"2.5",y:"5",width:"2",height:"8",rx:"1",fill:"#6366f1",opacity:"0.7"}),u("rect",{x:"6.5",y:"2",width:"2",height:"14",rx:"1",fill:"#6366f1"}),u("rect",{x:"10.5",y:"6",width:"2",height:"6",rx:"1",fill:"#6366f1",opacity:"0.7"}),u("rect",{x:"14.5",y:"4",width:"2",height:"10",rx:"1",fill:"#6366f1",opacity:"0.85"})]}),"Variant Switcher"]}),u("button",{"aria-label":"Close toolbar",onClick:()=>n(!1),style:{border:"none",background:"none",cursor:"pointer",fontSize:"16px",color:"#737373",padding:"2px 4px",lineHeight:1},children:"\xD7"})]}),u("div",{style:{fontSize:"11px",color:"#a3a3a3",marginBottom:"12px"},children:"Alt+H to toggle \u2022 Ctrl+Shift+V to open"}),C.length===0?u("div",{style:{color:"#a3a3a3",fontSize:"12px"},children:"No variant scopes registered yet."}):u("div",{style:{display:"flex",flexDirection:"column",gap:"12px"},children:C.map(([a,w])=>u(Lt,{name:a,state:w,onSelect:p=>t.setActiveVariant(a,p)},a))}),u("div",{style:{marginTop:"16px",paddingTop:"12px",borderTop:"1px solid #f0f0f0"},children:u("button",{onClick:t.toggleUiVisible,style:{width:"100%",padding:"7px 12px",border:"1px solid #e5e5e5",borderRadius:"6px",background:"none",cursor:"pointer",fontSize:"12px",color:"#737373",textAlign:"center"},children:t.uiVisible?"Hide switchers (Alt+H)":"Show switchers (Alt+H)"})})]})]})}function Lt({name:t,state:e,onSelect:n}){return V("div",{children:[u("div",{style:{fontWeight:600,color:"#404040",marginBottom:"6px",fontSize:"12px"},children:t}),u("div",{style:{display:"flex",flexWrap:"wrap",gap:"4px"},children:e.variantNames.map(r=>{let i=r===e.activeVariant;return u("button",{role:"radio","aria-checked":i,"aria-label":`Switch ${t} to ${r}`,onClick:()=>n(r),style:{cursor:"pointer",border:i?"2px solid #171717":"1px solid #e5e5e5",borderRadius:"5px",padding:"3px 10px",fontSize:"11px",fontWeight:i?600:400,background:i?"#171717":"#fafafa",color:i?"#fff":"#404040",transition:"all 0.12s"},children:r},r)})})]})}export{Mt as PageVariantSwitcher,J as VariantContext,At as VariantDevToolbar,Vt as VariantProvider,_t as VariantSwitcher,kt as clearVariantRegistry,G as getRegisteredVariant,W as readUiVisibleFromStorage,tt as readVariantFromStorage,Z as readVariantFromUrl,Pt as registerVariant,xt as removeVariantFromStorage,gt as removeVariantFromUrl,E as resolveActiveVariant,S as useActiveVariant,N as useKeyboardShortcuts,It as useVariant,b as useVariantContext,j as writeUiVisibleToStorage,Y as writeVariantToStorage,B as writeVariantToUrl};
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@vibeflow-tools/prototyping",
3
+ "version": "0.2.0",
4
+ "description": "In-app variant switching for React — page-level and component-level prototyping with URL persistence",
5
+ "license": "Apache-2.0",
6
+ "author": "Tomislav Zorcec <vibeflow.tools@gmail.com>",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist/",
19
+ "skills/",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup && npm run build:cdn-bundles",
25
+ "build:cdn-bundles": "esbuild src/index.ts --bundle --format=esm --platform=browser --outfile=dist/prototype-bundle.js --external:react --external:react-dom --external:react-dom/client --external:react/jsx-runtime --define:process.env.NODE_ENV=\"production\" --target=es2020 --minify && esbuild src/index.ts --bundle --format=iife --platform=browser --global-name=VibeflowPrototyping --outfile=dist/prototype-bundle.iife.js --external:react --external:react-dom --external:react/jsx-runtime --define:process.env.NODE_ENV=\"production\" --target=es2020 --minify",
26
+ "dev": "tsup --watch",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "tsc --noEmit",
32
+ "mutation": "node scripts/mutation.mjs",
33
+ "mutation:ci": "node scripts/mutation.mjs --all",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "keywords": [
37
+ "vibeflow",
38
+ "prototyping",
39
+ "variant",
40
+ "switcher",
41
+ "react",
42
+ "url-params",
43
+ "design-variants",
44
+ "a-b-testing",
45
+ "component-variants",
46
+ "dev-tools"
47
+ ],
48
+ "peerDependencies": {
49
+ "react": "^18.0.0",
50
+ "react-dom": "^18.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "esbuild": "^0.25.0",
54
+ "@stryker-mutator/core": "^9.6.1",
55
+ "@stryker-mutator/typescript-checker": "^9.6.1",
56
+ "@stryker-mutator/vitest-runner": "^9.6.1",
57
+ "@testing-library/jest-dom": "^6.9.1",
58
+ "@testing-library/react": "^16.3.0",
59
+ "@testing-library/user-event": "^14.5.2",
60
+ "@types/react": "^18.3.28",
61
+ "@types/react-dom": "^18.3.7",
62
+ "@vitest/coverage-v8": "^3.0.0",
63
+ "jsdom": "^29.0.0",
64
+ "react": "^18.3.1",
65
+ "react-dom": "^18.3.1",
66
+ "tsup": "^8.3.0",
67
+ "typescript": "^5.6.0",
68
+ "vitest": "^3.0.0"
69
+ }
70
+ }
@@ -0,0 +1,384 @@
1
+ ---
2
+ name: ui-prototyping
3
+ description: Use when building, reviewing, or integrating @vibeflow-tools/prototyping — in-app variant switching for React. Triggers on "useVariant", "PageVariantSwitcher", "VariantSwitcher", "VariantProvider", "variant switching", "prototyping components", "A/B testing variants", "design variants".
4
+ ---
5
+
6
+ # @vibeflow-tools/prototyping
7
+
8
+ In-app variant switching for React — page-level layout changes and component-level density/style changes with URL persistence and zero runtime dependencies.
9
+
10
+ ## When to Use
11
+
12
+ - **Design review** — switch between layout variants without code changes
13
+ - **A/B testing** — persist variant selection across sessions
14
+ - **Component documentation** — show all variants in a live playground
15
+ - **Prototyping** — quickly test different UI approaches
16
+
17
+ ## Quick Setup
18
+
19
+ ```tsx
20
+ import {
21
+ VariantProvider,
22
+ useVariant,
23
+ PageVariantSwitcher,
24
+ VariantSwitcher,
25
+ VariantDevToolbar,
26
+ } from '@vibeflow-tools/prototyping'
27
+
28
+ function App() {
29
+ return (
30
+ <VariantProvider>
31
+ <VariantDevToolbar /> {/* ⚡ floating button, bottom-right */}
32
+ <PageVariantSwitcher name="Layout" variants={layoutVariants} />
33
+ <YourApp />
34
+ </VariantProvider>
35
+ )
36
+ }
37
+ ```
38
+
39
+ ## Core Concepts
40
+
41
+ ### Scopes
42
+
43
+ A **scope** is a named group of variants. Use PascalCase component names:
44
+
45
+ ```tsx
46
+ // ✅ Good — descriptive, matches component
47
+ useVariant('TaskCard', { default: {}, compact: {} })
48
+ useVariant('KanbanBoard', { columns: {}, swimlane: {} })
49
+
50
+ // ❌ Bad — generic, unclear intent
51
+ useVariant('variant1', { a: {}, b: {} })
52
+ useVariant('test', { light: {}, dark: {} })
53
+ ```
54
+
55
+ ### Variant Resolution Order
56
+
57
+ 1. **URL param** — `?vf[ScopeName]=variant` (highest priority, shareable)
58
+ 2. **localStorage** — `__vf__ScopeName` (persists across sessions)
59
+ 3. **Default** — first key in the variants object
60
+
61
+ ### Two Switcher Levels
62
+
63
+ | Level | Component | Visual | Use Case |
64
+ |-------|-----------|--------|----------|
65
+ | **Page** | `PageVariantSwitcher` | Dark segmented bar, top-left | Layout changes (columns ↔ swimlane) |
66
+ | **Component** | `VariantSwitcher` | Subtle dot → click to expand | Density changes (default ↔ compact) |
67
+
68
+ ## Components
69
+
70
+ ### VariantProvider
71
+
72
+ Wraps your app (or subtree) and provides the variant system.
73
+
74
+ ```tsx
75
+ <VariantProvider mode="dev" defaultVisible={true}>
76
+ {children}
77
+ </VariantProvider>
78
+ ```
79
+
80
+ | Prop | Default | Description |
81
+ |------|---------|-------------|
82
+ | `mode` | `"dev"` | `"dev"` — hidden in production; `"always"` — always visible |
83
+ | `defaultVisible` | `true` | Initial visibility state |
84
+
85
+ ### useVariant
86
+
87
+ Core hook — returns the active variant config object.
88
+
89
+ ```tsx
90
+ const variants = {
91
+ default: { padding: 16, showMeta: true },
92
+ compact: { padding: 8, showMeta: false },
93
+ detailed: { padding: 24, showMeta: true, showComments: true },
94
+ }
95
+
96
+ function TaskCard() {
97
+ const variant = useVariant('TaskCard', variants)
98
+ return <div style={{ padding: variant.padding }}>...</div>
99
+ }
100
+ ```
101
+
102
+ **Returns:** The config object for the active variant. First key is default.
103
+
104
+ ### PageVariantSwitcher
105
+
106
+ Dark segmented bar for page-level layout switching. Fixed top-left, always visible when UI is shown.
107
+
108
+ ```tsx
109
+ const layoutVariants = {
110
+ columns: { direction: 'row' },
111
+ swimlane: { direction: 'column' },
112
+ compact: { direction: 'row', dense: true },
113
+ }
114
+
115
+ function App() {
116
+ const layout = useVariant('App', layoutVariants)
117
+ return (
118
+ <>
119
+ <PageVariantSwitcher name="App" variants={layoutVariants} />
120
+ <div className={`layout-${layout.direction}`}>
121
+ {/* content */}
122
+ </div>
123
+ </>
124
+ )
125
+ }
126
+ ```
127
+
128
+ ### VariantSwitcher
129
+
130
+ Subtle indicator dot for component-level switching. A small dot appears on the right (or left) side — click to expand the numbered-dots picker. Click outside or press Escape to collapse.
131
+
132
+ ```tsx
133
+ function TaskCard({ task }) {
134
+ const variant = useVariant('TaskCard', cardVariants)
135
+ return (
136
+ <div style={{ position: 'relative' }}>
137
+ <VariantSwitcher name="TaskCard" variants={cardVariants} />
138
+ <div className={variant.compact ? 'compact' : ''}>
139
+ {/* card content */}
140
+ </div>
141
+ </div>
142
+ )
143
+ }
144
+ ```
145
+
146
+ **Dedup:** Only one `VariantSwitcher` per scope renders. Multiple components using the same scope share one switcher.
147
+
148
+ **UX:** Small dot (14px) → click → numbered dots appear → select variant → collapses back to dot.
149
+
150
+ ### VariantDevToolbar
151
+
152
+ Floating ⚡ button (bottom-right) that opens a dialog showing all active scopes and their current variants.
153
+
154
+ **Vibeflow overlay integration:** When the Vibeflow overlay is detected (`#vibeflow-studio-root`), the standalone ⚡ button is hidden. Instead, access the toolbar via the overlay's right-click context menu → "Prototyping".
155
+
156
+ ```tsx
157
+ <VariantProvider>
158
+ <VariantDevToolbar />
159
+ {/* your app */}
160
+ </VariantProvider>
161
+ ```
162
+
163
+ ### registerVariant
164
+
165
+ Pre-register variants before the React tree (module-level).
166
+
167
+ ```tsx
168
+ import { registerVariant } from '@vibeflow-tools/prototyping'
169
+
170
+ // Call before any component renders
171
+ registerVariant('TaskCard', {
172
+ default: {},
173
+ compact: { compact: true },
174
+ detailed: { showMeta: true },
175
+ })
176
+ ```
177
+
178
+ ### Keyboard Shortcuts
179
+
180
+ | Shortcut | Action |
181
+ |----------|--------|
182
+ | `Alt + H` | Toggle all switchers visibility |
183
+ | `Ctrl + Shift + V` | Toggle dev toolbar |
184
+
185
+ ## Patterns
186
+
187
+ ### Pattern A: Page Layout Switching
188
+
189
+ One switcher controls the entire page layout.
190
+
191
+ ```tsx
192
+ const layoutVariants = {
193
+ columns: { direction: 'row', gap: 16 },
194
+ sidebar: { direction: 'row', sidebar: true, gap: 24 },
195
+ mobile: { direction: 'column', gap: 8 },
196
+ }
197
+
198
+ function App() {
199
+ const layout = useVariant('App', layoutVariants)
200
+ return (
201
+ <>
202
+ <PageVariantSwitcher name="App" variants={layoutVariants} />
203
+ <div style={{ display: 'flex', flexDirection: layout.direction, gap: layout.gap }}>
204
+ {layout.sidebar && <Sidebar />}
205
+ <Main />
206
+ </div>
207
+ </>
208
+ )
209
+ }
210
+ ```
211
+
212
+ ### Pattern B: Component Density Switching
213
+
214
+ Multiple components share one scope — all switch together.
215
+
216
+ ```tsx
217
+ const cardVariants = {
218
+ default: {},
219
+ compact: { compact: true },
220
+ detailed: { showMeta: true, showComments: true },
221
+ }
222
+
223
+ function TaskCard({ task }) {
224
+ const variant = useVariant('TaskCard', cardVariants)
225
+ return (
226
+ <div style={{ position: 'relative' }}>
227
+ <VariantSwitcher name="TaskCard" variants={cardVariants} />
228
+ <div className={variant.compact ? 'compact' : ''}>
229
+ <h3>{task.title}</h3>
230
+ {variant.showMeta && <span>{task.assignee}</span>}
231
+ {variant.showComments && <CommentList comments={task.comments} />}
232
+ </div>
233
+ </div>
234
+ )
235
+ }
236
+ ```
237
+
238
+ All `TaskCard` instances share one switcher — change one, all update.
239
+
240
+ ### Pattern C: Multiple Independent Scopes
241
+
242
+ Different components have independent variant states.
243
+
244
+ ```tsx
245
+ function App() {
246
+ return (
247
+ <VariantProvider>
248
+ <VariantDevToolbar />
249
+ <PageVariantSwitcher name="Layout" variants={layoutVariants} />
250
+ <Sidebar />
251
+ <Main />
252
+ </VariantProvider>
253
+ )
254
+ }
255
+
256
+ function Sidebar() {
257
+ const variant = useVariant('Sidebar', {
258
+ expanded: { width: 280 },
259
+ collapsed: { width: 64 },
260
+ })
261
+ return (
262
+ <aside style={{ width: variant.width, position: 'relative' }}>
263
+ <VariantSwitcher name="Sidebar" variants={sidebarVariants} />
264
+ {/* sidebar content */}
265
+ </aside>
266
+ )
267
+ }
268
+ ```
269
+
270
+ ### Pattern D: Conditional Rendering
271
+
272
+ Variants control which features are visible.
273
+
274
+ ```tsx
275
+ function Dashboard() {
276
+ const variant = useVariant('Dashboard', {
277
+ default: { showCharts: true, showTable: true, showStats: true },
278
+ analytics: { showCharts: true, showTable: true, showStats: false },
279
+ minimal: { showCharts: false, showTable: true, showStats: false },
280
+ })
281
+
282
+ return (
283
+ <>
284
+ <PageVariantSwitcher name="Dashboard" variants={dashboardVariants} />
285
+ {variant.showStats && <StatsBar />}
286
+ {variant.showCharts && <Charts />}
287
+ {variant.showTable && <DataTable />}
288
+ </>
289
+ )
290
+ }
291
+ ```
292
+
293
+ ### Pattern E: Shareable URLs
294
+
295
+ Variant state is synced to URL automatically — share the link.
296
+
297
+ ```
298
+ https://myapp.com/dashboard?vf[Layout]=sidebar&vf[TaskCard]=compact
299
+ ```
300
+
301
+ Team members opening this link see the exact same variant configuration.
302
+
303
+ ## Best Practices
304
+
305
+ ### Do
306
+
307
+ - ✅ Use PascalCase scope names matching component names
308
+ - ✅ Keep variant configs flat (no nesting)
309
+ - ✅ Use boolean flags for feature toggles: `{ compact: true }`
310
+ - ✅ Use string enums for modes: `{ layout: 'grid' }`
311
+ - ✅ Place `PageVariantSwitcher` outside content flow (fixed positioning)
312
+ - ✅ Place `VariantSwitcher` inside `position: relative` parent
313
+ - ✅ Place one `VariantSwitcher` per section (dedup handles the rest)
314
+ - ✅ Let `VariantSwitcher` deduplicate — don't manually control visibility
315
+
316
+ ### Don't
317
+
318
+ - ❌ Nest variant configs: `{ style: { padding: 16 } }` — use flat: `{ padding: 16 }`
319
+ - ❌ Use generic scope names: `variant1`, `test`, `foo`
320
+ - ❌ Place multiple `PageVariantSwitcher` for the same scope
321
+ - ❌ Place `VariantSwitcher` on every item — one per section, dedup handles it
322
+ - ❌ Skip `VariantProvider` — all hooks require it
323
+
324
+ ## Vibeflow Overlay Integration
325
+
326
+ When both `@vibeflow-tools/prototyping` and the Vibeflow overlay are present on the same page:
327
+
328
+ 1. **Detection:** `VariantDevToolbar` detects the overlay via `#vibeflow-studio-root`
329
+ 2. **Hiding:** The standalone ⚡ button is hidden (no duplicate bottom-right icons)
330
+ 3. **Registration:** The toolbar registers on `window.__vf_prototyping` with `openPanel()` / `closePanel()` methods
331
+ 4. **Overlay menu:** The overlay's right-click context menu gains a "Prototyping" option
332
+ 5. **Opening:** Clicking "Prototyping" calls `window.__vf_prototyping.openPanel()`
333
+
334
+ **No configuration needed** — integration is automatic via runtime detection.
335
+
336
+ ## Troubleshooting
337
+
338
+ ### Switcher not visible
339
+
340
+ 1. Check `mode` prop on `VariantProvider` — `"dev"` hides in production
341
+ 2. Check `uiVisible` state — press `Alt + H` to toggle
342
+ 3. Check variant count — switchers need 2+ variants
343
+ 4. Check if indicator dot is present (small 14px dot on the right side)
344
+
345
+ ### Variant not updating
346
+
347
+ 1. Verify scope name matches between `useVariant` and switcher
348
+ 2. Check URL params — they override localStorage
349
+ 3. Clear localStorage: `localStorage.removeItem('__vf__ScopeName')`
350
+
351
+ ### Multiple switchers appearing
352
+
353
+ This is expected if you're using the demo (vanilla JS). In React, `VariantSwitcher` deduplicates automatically — only one renders per scope.
354
+
355
+ ### Indicator dot not expanding
356
+
357
+ Click the dot to expand the picker. Click outside or press Escape to collapse. The picker is not hover-based — it requires a click.
358
+
359
+ ## API Reference
360
+
361
+ | Export | Type | Description |
362
+ |--------|------|-------------|
363
+ | `VariantProvider` | Component | Root provider — wraps app |
364
+ | `useVariant` | Hook | Returns active variant config |
365
+ | `PageVariantSwitcher` | Component | Dark bar, top-left |
366
+ | `VariantSwitcher` | Component | Numbered dots, hover reveal |
367
+ | `VariantDevToolbar` | Component | Floating ⚡ button + dialog |
368
+ | `registerVariant` | Function | Pre-register variants at module level |
369
+ | `getRegisteredVariant` | Function | Read registered variants |
370
+ | `clearVariantRegistry` | Function | Reset registry (testing) |
371
+ | `useKeyboardShortcuts` | Hook | Bind Alt+H / Ctrl+Shift+V |
372
+ | `useVariantContext` | Hook | Access raw context (power users) |
373
+
374
+ ### URL/localStorage Utils (Power Users)
375
+
376
+ | Export | Description |
377
+ |--------|-------------|
378
+ | `readVariantFromUrl` | Read `?vf[Name]` from URL |
379
+ | `writeVariantToUrl` | Write variant to URL |
380
+ | `removeVariantFromUrl` | Remove variant from URL |
381
+ | `readVariantFromStorage` | Read `__vf__Name` from localStorage |
382
+ | `writeVariantToStorage` | Write variant to localStorage |
383
+ | `removeVariantFromStorage` | Remove variant from localStorage |
384
+ | `resolveActiveVariant` | Resolve: URL → localStorage → default |