@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
|
@@ -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
|
+
}
|
package/skills/SKILL.md
ADDED
|
@@ -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 |
|