Skip to content

Instantly share code, notes, and snippets.

@btahir
Created December 25, 2025 23:49
Show Gist options
  • Select an option

  • Save btahir/f331db51333c4bdadee24cdd31f963ae to your computer and use it in GitHub Desktop.

Select an option

Save btahir/f331db51333c4bdadee24cdd31f963ae to your computer and use it in GitHub Desktop.
Bookmarklet that stacks related fal.ai model variants into hoverable card decks
javascript:(()=>{const getGroupKey=(txt)=>{const m=txt.match(/\b([a-z0-9_-]+(?:\/[a-z0-9_.-]+)+)/i);if(!m)return null;const seg=m[1].toLowerCase().split(%27/%27);if(seg.length<2)return null;return seg.slice(0,-1).join(%27/%27)};const COLLAPSED_SHIFT=4,COLLAPSED_LIFT=3,EXPAND_GAP=12,EXPAND_DELAY=400,MAX_VISIBLE_CARDS=3;if(window.__falDeckCleanup)try{window.__falDeckCleanup()}catch{}const cleanupFns=[];window.__falDeckCleanup=()=>{cleanupFns.splice(0).reverse().forEach(fn=>{try{fn()}catch{}});delete window.__falDeckCleanup};const style=document.createElement("style");style.id="__fal_deck_style";style.textContent=`.__falDeckWrap{position:relative;display:block;border-radius:16px;isolation:isolate;z-index:1}.__falDeckWrap.__falExpanded{z-index:100}.__falDeckCard{background:var(--__falCardBg,rgba(18,18,20,.98))!important}.__falDeckWrap .__falDeckCard{position:absolute!important;top:0!important;left:0!important;width:var(--__falCardW,100%)!important;height:var(--__falCardH,100%)!important;transform-origin:center center;transition:transform 400ms cubic-bezier(.25,.1,.25,1),box-shadow 300ms ease,opacity 300ms ease;box-shadow:0 2px 8px rgba(0,0,0,.15);border-radius:inherit;overflow:hidden}.__falDeckWrap:not(.__falExpanded) .__falDeckCard{pointer-events:none}.__falDeckWrap:not(.__falExpanded) .__falDeckCard.__falTop{pointer-events:auto}.__falDeckWrap.__falExpanded .__falDeckCard{pointer-events:auto;box-shadow:0 4px 16px rgba(0,0,0,.2)}.__falDeckWrap.__falExpanded .__falDeckCard:hover{box-shadow:0 6px 20px rgba(0,0,0,.25)}.__falDeckBadge{position:absolute;bottom:8px;right:8px;z-index:9999;font:500 10px/1 ui-sans-serif,system-ui,sans-serif;padding:4px 7px;border-radius:6px;background:rgba(0,0,0,.5);color:rgba(255,255,255,.85);pointer-events:none;transition:opacity 300ms ease}.__falDeckWrap.__falExpanded .__falDeckBadge{opacity:0}.__falDeckScroller{position:absolute;top:0;left:50%;transform:translateX(-50%);display:flex;gap:${EXPAND_GAP}px;padding:8px;overflow-x:auto;overflow-y:hidden;opacity:0;pointer-events:none;transition:opacity 300ms ease;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;border-radius:16px}.__falDeckScroller::-webkit-scrollbar{height:6px}.__falDeckScroller::-webkit-scrollbar-track{background:transparent}.__falDeckScroller::-webkit-scrollbar-thumb{background:rgba(255,255,255,.3);border-radius:3px}.__falDeckWrap.__falExpanded .__falDeckScroller{opacity:1;pointer-events:auto}.__falDeckScroller .__falDeckCard{position:relative!important;flex-shrink:0;top:auto!important;left:auto!important}`;document.documentElement.appendChild(style);cleanupFns.push(()=>style.remove());const pageBg=getComputedStyle(document.body).backgroundColor;document.documentElement.style.setProperty("--__falCardBg",pageBg&&pageBg!=="rgba(0, 0, 0, 0)"?pageBg:"rgba(18,18,20,.98)");const processedCards=new WeakSet();const madeWraps=[];let hoverTimer=null,activeWrap=null;const collapseWrap=(wrap)=>{wrap.classList.remove("__falExpanded");const scroller=wrap.querySelector(".__falDeckScroller");const deckCards=Array.from(scroller?.querySelectorAll(".__falDeckCard")||[]);deckCards.forEach((c,i)=>{wrap.appendChild(c);c.style.transform=`translate(${i*COLLAPSED_SHIFT}px,${i*-COLLAPSED_LIFT}px)`;c.style.zIndex=String(10+i)})};const processCards=()=>{const allLinks=Array.from(document.querySelectorAll("a[href]"));const candidateLinks=allLinks.filter(a=>{if(processedCards.has(a)||a.closest(".__falDeckWrap"))return false;const txt=(a.innerText||"").trim();if(!txt)return false;const r=a.getBoundingClientRect();if(r.width<200||r.height<90)return false;return getGroupKey(txt)!==null});if(candidateLinks.length<1)return;const parentCounts=new Map();for(const a of candidateLinks){const p=a.parentElement;if(p&&!p.classList.contains("__falDeckWrap"))parentCounts.set(p,(parentCounts.get(p)||0)+1)}const gridParent=[...parentCounts.entries()].sort((a,b)=>b[1]-a[1])[0]?.[0];if(!gridParent)return;const cards=candidateLinks.filter(a=>gridParent.contains(a));const groups=new Map(),order=[];for(const card of cards){const key=getGroupKey(card.innerText);if(!key)continue;processedCards.add(card);if(!groups.has(key)){groups.set(key,[]);order.push(key)}groups.get(key).push(card)}const deckGroups=order.map(k=>[k,groups.get(k)]).filter(([,arr])=>arr&&arr.length>=2);for(const[key,arr]of deckGroups){const groupCards=arr.slice();const first=groupCards[0];const firstRect=first.getBoundingClientRect();const firstParent=first.parentElement;if(!firstParent||firstParent.classList.contains("__falDeckWrap"))continue;const cardW=Math.round(firstRect.width);const cardH=Math.round(firstRect.height);const wrap=document.createElement("div");wrap.className="__falDeckWrap";wrap.style.width=`${cardW}px`;wrap.style.height=`${cardH}px`;wrap.style.setProperty("--__falCardW",`${cardW}px`);wrap.style.setProperty("--__falCardH",`${cardH}px`);const maxScrollerW=MAX_VISIBLE_CARDS*cardW+(MAX_VISIBLE_CARDS-1)*EXPAND_GAP+16;const scroller=document.createElement("div");scroller.className="__falDeckScroller";scroller.style.maxWidth=`${maxScrollerW}px`;scroller.style.height=`${cardH+16}px`;wrap.appendChild(scroller);const badge=document.createElement("div");badge.className="__falDeckBadge";badge.textContent=`+${groupCards.length-1}`;wrap.appendChild(badge);firstParent.insertBefore(wrap,first);groupCards.forEach(c=>wrap.appendChild(c));const deckCards=groupCards;deckCards.forEach((c,i)=>{c.classList.add("__falDeckCard");c.style.zIndex=String(10+i);if(i===deckCards.length-1)c.classList.add("__falTop")});const applyCollapsed=()=>deckCards.forEach((c,i)=>{c.style.transform=`translate(${i*COLLAPSED_SHIFT}px,${i*-COLLAPSED_LIFT}px)`});applyCollapsed();const onEnter=()=>{if(hoverTimer)clearTimeout(hoverTimer);hoverTimer=setTimeout(()=>{if(activeWrap&&activeWrap!==wrap)collapseWrap(activeWrap);activeWrap=wrap;wrap.classList.add("__falExpanded");deckCards.forEach((c,i)=>{c.style.transform="";c.style.zIndex=String(100+i);scroller.appendChild(c)})},EXPAND_DELAY)};const onLeave=()=>{if(hoverTimer){clearTimeout(hoverTimer);hoverTimer=null}collapseWrap(wrap);if(activeWrap===wrap)activeWrap=null};wrap.addEventListener("mouseenter",onEnter);wrap.addEventListener("mouseleave",onLeave);cleanupFns.push(()=>{wrap.removeEventListener("mouseenter",onEnter);wrap.removeEventListener("mouseleave",onLeave)});madeWraps.push({wrap,firstParent,cards:groupCards})}};processCards();const observer=new MutationObserver(()=>{setTimeout(processCards,100)});observer.observe(document.body,{childList:true,subtree:true});cleanupFns.push(()=>observer.disconnect());let scrollTimer;const onScroll=()=>{clearTimeout(scrollTimer);scrollTimer=setTimeout(processCards,200)};window.addEventListener("scroll",onScroll,{passive:true});cleanupFns.push(()=>window.removeEventListener("scroll",onScroll));cleanupFns.push(()=>{if(hoverTimer)clearTimeout(hoverTimer);for(const{wrap,firstParent,cards}of madeWraps.reverse()){for(const c of cards){c.classList.remove("__falDeckCard","__falTop");c.style.transform="";c.style.zIndex="";firstParent.insertBefore(c,wrap)}wrap.remove()}});console.log("[fal-deck] Running with smart grouping")})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment