Created
December 25, 2025 23:49
-
-
Save btahir/f331db51333c4bdadee24cdd31f963ae to your computer and use it in GitHub Desktop.
Bookmarklet that stacks related fal.ai model variants into hoverable card decks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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