const $ = (sel,root=document) => root.querySelector(sel); const $$ = (sel,root=document) => Array.from(root.querySelectorAll(sel)); /* ───── Smooth scroll with ease-in-out ───── */ function smoothScrollTo(el, offset = 0) { const start = window.scrollY; const end = el.getBoundingClientRect().top + start + offset; const duration = 400; const startTime = performance.now(); function step(now) { const elapsed = (now - startTime) / duration; const t = Math.min(elapsed, 1); const ease = t < .5 ? 2*t*t : -1+(4-2*t)*t; // easeInOutQuad window.scrollTo(0, start + (end - start) * ease); if (t < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } /* ───── Build chips ───── */ function buildChips(){ const ecoBox = $("#eco-chips"); ecoBox.innerHTML = ""; ECOSYSTEMS.forEach(e=>{ const cnt = SKILLS.filter(x=>x.ecosystem===e).length; const b = document.createElement("button"); b.className = "chip"; b.setAttribute("aria-pressed","false"); b.dataset.eco = e; b.dataset.filterVal = e; b.innerHTML = `${uniqueSvg(GLYPHS[e])}${e}${cnt}`; b.addEventListener("click", ()=>toggle(state.ecosystems,e,b)); ecoBox.appendChild(b); }); const catBox = $("#cat-chips"); catBox.innerHTML = ""; CATEGORIES.forEach(c=>{ const cnt = SKILLS.filter(x=>x.category===c).length; const b = document.createElement("button"); b.className = "chip"; b.setAttribute("aria-pressed","false"); b.dataset.filterVal = c; b.innerHTML = `${c}${cnt}`; b.addEventListener("click", ()=>toggle(state.categories,c,b)); catBox.appendChild(b); }); const stBox = $("#status-chips"); stBox.innerHTML = ""; STATUSES.forEach(s=>{ const cnt = SKILLS.filter(x=>x.status===s).length; const b = document.createElement("button"); b.className = "chip"; b.setAttribute("aria-pressed","false"); b.dataset.filterVal = s; b.innerHTML = `${s}${cnt}`; b.addEventListener("click", ()=>toggle(state.statuses,s,b)); stBox.appendChild(b); }); } function toggle(set, value, el){ if(set.has(value)){ set.delete(value); el.setAttribute("aria-pressed","false"); } else{ set.add(value); el.setAttribute("aria-pressed","true"); } render(); } /* ───── Grid render with asymmetric spans ───── */ const SPAN_PATTERN = [4, 4, 4, 6, 6, 4, 4, 4, 8, 4, 4, 4, 4, 6, 6]; function spanFor(i){ return SPAN_PATTERN[i % SPAN_PATTERN.length]; } function matchesFilters(s){ if(state.favOnly && !favorites.has(s.id)) return false; if(state.newOnly && !NEW_IDS.has(s.id)) return false; if(state.ecosystems.size && !state.ecosystems.has(s.ecosystem)) return false; if(state.categories.size && !state.categories.has(s.category)) return false; if(state.statuses.size && !state.statuses.has(s.status)) return false; if(state.query){ const q = state.query.toLowerCase(); const blob = (s.name+" "+s.description+" "+s.trigger+" "+s.example+" "+s.category+" "+s.ecosystem+" "+(s.howto||HOWTO[s.id]||"")).toLowerCase(); if(!blob.includes(q)) return false; } return true; } function render(){ const grid = $("#grid"); grid.innerHTML = ""; let filtered = SKILLS.filter(matchesFilters); // sort if(state.sort === "recent"){ const rank = s => (s.status === "Beta" ? 0 : (s.status === "Stable" ? 1 : 2)); filtered = filtered.slice().sort((a,b)=> rank(a) - rank(b)); } lastFiltered = filtered; focusedCardIdx = -1; $("#count").textContent = filtered.length; const totalEl = $("#total-count"); if(totalEl) totalEl.textContent = SKILLS.length; /* Only update the hero counter after the first load — boot uses animateCount() instead */ const heroCount = $("#hero-skill-count"); if(heroCount && render._firstDone) heroCount.textContent = SKILLS.length; renderActiveFilters(); if(filtered.length === 0){ const qLabel = state.query ? `"${escapeHTML(state.query)}"` : "that combination"; grid.innerHTML = `
Nothing matched ${qLabel}.Try broader terms · or fewer filters
`; return; } filtered.forEach((s, i)=>{ const span = spanFor(i); const featured = (span >= 8) || (i === 0); const isSaved = favorites.has(s.id); const card = document.createElement("article"); card.className = `card ${span===6?"span-6":""} ${span===8?"span-8":""} ${span===12?"span-12":""} ${featured?"featured":""}`; card.tabIndex = 0; card.setAttribute("role","button"); card.setAttribute("aria-label", `${s.name} — ${s.ecosystem} — open details`); card.id = `skill-${s.id}`; card.dataset.id = s.id; card.dataset.eco = s.ecosystem; card.dataset.trigger = s.trigger; card.dataset.example = s.example; card.dataset.source = s.source; const nameHTML = s.name.replace(/(\S+)$/, '$1'); const isNew = NEW_IDS.has(s.id); card.innerHTML = `
${uniqueSvg(GLYPHS[s.ecosystem])}
${s.id}${s._custom?'custom':''}${isNew?'new':''}
${s.ecosystem} № ${String(SKILLS.indexOf(s)+1).padStart(3,'0')} / ${String(SKILLS.length).padStart(3,'0')}

${nameHTML}

${codeBacktick(escapeHTML(s.description))}

${codeBacktick(escapeHTML(s.trigger))}

${s.category} ${s.status}
`; card.addEventListener("click", (e)=>{ if(e.target.closest(".anchor") || e.target.closest(".fav-btn")) return; openSheet(s); }); card.addEventListener("keydown", e=>{ if((e.key==="Enter"||e.key===" ") && !e.target.classList.contains("anchor") && !e.target.classList.contains("fav-btn")){ e.preventDefault(); openSheet(s); } }); card.querySelector(".anchor").addEventListener("click", (e)=>{ e.stopPropagation(); copyDeepLink(s.id); }); card.querySelector(".fav-btn").addEventListener("click", (e)=>{ e.stopPropagation(); const was = favorites.has(s.id); was ? favorites.delete(s.id) : favorites.add(s.id); saveFavs(); updateFavCount(); const btn = e.currentTarget; btn.classList.toggle("saved", !was); btn.textContent = !was ? "♥" : "♡"; btn.title = !was ? "Remove from saved" : "Save this skill"; if(state.favOnly) render(); }); card.querySelector(".trigger").addEventListener("click", (e)=>{ e.stopPropagation(); navigator.clipboard && navigator.clipboard.writeText(s.trigger).catch(()=>{}); showToast("trigger copied"); }); grid.appendChild(card); }); // staggered fade-in — fast stagger on filter, gentle on first load requestAnimationFrame(()=>{ const step = render._firstDone ? 8 : 18; $$("#grid .card").forEach((c,i)=>{ setTimeout(()=>c.classList.add("in"), i*step); }); render._firstDone = true; }); } /* ───── Sheet ───── */ function openSheet(s, opts){ opts = opts || {}; const sheet = $("#sheet"); const nameHTML = s.name.replace(/(\S+)$/, '$1'); currentSheetIdx = lastFiltered.findIndex(x => x.id === s.id); const hasPrev = currentSheetIdx > 0; const hasNext = currentSheetIdx < lastFiltered.length - 1; const isSaved = favorites.has(s.id); sheet.innerHTML = `
${uniqueSvg(GLYPHS[s.ecosystem])}
${s.ecosystem}  ·  ${s.category}  ·  ${s.status}
id: ${s.id}  ·  entry № ${String(SKILLS.indexOf(s)+1).padStart(3,'0')} of ${SKILLS.length}  ·  copy deep link

${nameHTML}

${codeBacktick(escapeHTML(s.description))}

Trigger

${splitTriggers(codeBacktick(escapeHTML(s.trigger)))}

How to use

${codeBacktick(escapeHTML(s.howto || HOWTO[s.id] || "See the source link below for setup instructions."))}

Worked example

${codeBacktick(escapeHTML(s.example))}

Source & citation

${escapeHTML(s.source)}
${(()=>{ const related = SKILLS.filter(r => r.id !== s.id && ( r.category === s.category || (r.ecosystem === s.ecosystem && r.id !== s.id) ) ).slice(0,4); if(!related.length) return ""; return `

Related skills

`; })()}
${currentSheetIdx+1} of ${lastFiltered.length}
`; $("#scrim").classList.add("open"); document.body.style.overflow = "hidden"; sheet.scrollTop = 0; $("#closeBtn").addEventListener("click", closeSheet); $("#sheetCopy").addEventListener("click", (e)=>{ e.preventDefault(); copyDeepLink(s.id); }); if(hasPrev) $("#prevSkill").addEventListener("click", ()=>openSheet(lastFiltered[currentSheetIdx-1])); if(hasNext) $("#nextSkill").addEventListener("click", ()=>openSheet(lastFiltered[currentSheetIdx+1])); $("#favSheetBtn").addEventListener("click", ()=>{ const was = favorites.has(s.id); was ? favorites.delete(s.id) : favorites.add(s.id); saveFavs(); updateFavCount(); const fb = $("#favSheetBtn"); fb.className = favorites.has(s.id) ? "is-saved" : ""; fb.textContent = favorites.has(s.id) ? "♥ Saved" : "♡ Save"; fb.setAttribute("aria-label", favorites.has(s.id) ? "Remove from saved" : "Save this skill"); const cardFav = document.querySelector(`[data-fav-id="${s.id}"]`); if(cardFav){ cardFav.classList.toggle("saved", favorites.has(s.id)); cardFav.textContent = favorites.has(s.id)?"♥":"♡"; } if(state.favOnly) render(); }); $("#shareBtn").addEventListener("click", ()=>{ const url = location.origin + location.pathname + location.search + "#" + s.id; if(navigator.share){ navigator.share({title:`${s.name} — SkillsAllYouNeed`, url}).catch(()=>copyDeepLink(s.id)); } else { copyDeepLink(s.id); } }); $("#copyTriggerBtn").addEventListener("click", ()=>{ navigator.clipboard && navigator.clipboard.writeText(s.trigger).catch(()=>{}); showToast("trigger copied"); }); $$(".related-chip", sheet).forEach(btn => { btn.addEventListener("click", ()=>{ const target = SKILLS.find(x=>x.id === btn.dataset.relatedId); if(target) openSheet(target); }); }); if(s._custom){ const delBtn = document.createElement("button"); delBtn.className = "is-delete"; delBtn.textContent = "Delete ✕"; delBtn.setAttribute("aria-label","Delete this custom skill"); delBtn.addEventListener("click", ()=>{ if(!confirm(`Delete "${s.name}"? This cannot be undone.`)) return; const idx = SKILLS.findIndex(x=>x.id===s.id); if(idx > -1) SKILLS.splice(idx, 1); const remaining = JSON.parse(localStorage.getItem("skills-custom")||"[]").filter(x=>x.id!==s.id); localStorage.setItem("skills-custom", JSON.stringify(remaining)); closeSheet(); render(); showToast(`"${s.name}" deleted`); }); sheet.querySelector(".sheet-actions").appendChild(delBtn); } if(opts.pushHash !== false && location.hash !== "#"+s.id){ history.replaceState(null, "", "#"+s.id); } } function closeSheet(){ $("#scrim").classList.remove("open"); document.body.style.overflow = ""; if(location.hash){ history.replaceState(null, "", location.pathname + location.search); } } $("#scrim").addEventListener("click", e=>{ if(e.target.id === "scrim") closeSheet(); }); /* ───── Deep link / toast ───── */ function copyDeepLink(id){ const url = location.origin + location.pathname + location.search + "#" + id; const fallback = () => { const ta = document.createElement("textarea"); ta.value = url; ta.style.position="fixed"; ta.style.left="-9999px"; document.body.appendChild(ta); ta.select(); try{ document.execCommand("copy"); }catch(_){} document.body.removeChild(ta); }; if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(url).catch(fallback); } else { fallback(); } showToast("link copied · ¶ " + id); } let toastTimer; function showToast(msg){ const t = $("#toast"); t.textContent = msg; t.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(()=>t.classList.remove("show"), 1800); } /* ───── Ecosystem deep-dive strip ───── */ function buildEcoStrip(){ const wrap = $("#eco-strip"); wrap.innerHTML = ""; ECOSYSTEMS.forEach(eco => { const count = SKILLS.filter(s => s.ecosystem === eco).length; const cell = document.createElement("button"); cell.className = "eco-cell"; cell.dataset.eco = eco; cell.setAttribute("aria-label", `Filter to ${eco} — ${count} entries`); cell.innerHTML = `
${uniqueSvg(GLYPHS[eco])}
${String(count).padStart(2,'0')}entries

${eco.replace(/(\w+)$/, '$1')}

${ECO_BLURB[eco]}

`; cell.addEventListener("click", ()=>{ // single-select this ecosystem state.ecosystems.clear(); state.ecosystems.add(eco); $$("#eco-chips .chip").forEach(c=>{ c.setAttribute("aria-pressed", c.dataset.eco === eco ? "true" : "false"); }); render(); smoothScrollTo($("#grid"), -20); }); wrap.appendChild(cell); }); } /* ───── Shortcuts modal ───── */ function openShortcuts(){ $("#shortcuts").classList.add("open"); } function closeShortcuts(){ $("#shortcuts").classList.remove("open"); } $("#hint-key").addEventListener("click", ()=>{ const s = $("#shortcuts"); s.classList.toggle("open"); }); $("#closeFn").addEventListener("click", closeShortcuts); /* ───── Global keyboard shortcuts (unified) ───── */ document.addEventListener("keydown", (e)=>{ const tag = (e.target && e.target.tagName || "").toLowerCase(); const typing = tag === "input" || tag === "textarea" || e.target.isContentEditable; const sheetOpen = $("#scrim").classList.contains("open"); // Escape closes whatever is open if(e.key === "Escape"){ if(sheetOpen) { closeSheet(); return; } if(document.getElementById("add-scrim").classList.contains("open")){ closeAddSheet(); return; } if($("#shortcuts").classList.contains("open")){ closeShortcuts(); return; } return; } // Arrow navigation inside open sheet if(sheetOpen){ if(e.key === "ArrowLeft" && currentSheetIdx > 0){ e.preventDefault(); openSheet(lastFiltered[currentSheetIdx-1]); return; } if(e.key === "ArrowRight" && currentSheetIdx < lastFiltered.length-1){ e.preventDefault(); openSheet(lastFiltered[currentSheetIdx+1]); return; } } if(typing) return; if(e.key === "/"){ e.preventDefault(); $("#q").focus(); $("#q").select(); } else if(e.key === "?" || (e.shiftKey && e.key === "/")){ e.preventDefault(); $("#shortcuts").classList.toggle("open"); } else if((e.key === "j" || e.key === "k") && !sheetOpen){ e.preventDefault(); const cards = $$("#grid .card"); if(!cards.length) return; focusedCardIdx = e.key==="j" ? Math.min(focusedCardIdx+1, cards.length-1) : Math.max(focusedCardIdx-1, 0); cards[focusedCardIdx].focus(); cards[focusedCardIdx].scrollIntoView({block:"nearest", behavior:"smooth"}); } else if(e.key === "f"){ if(sheetOpen){ const fb = $("#favSheetBtn"); if(fb) fb.click(); } else { const focused = document.activeElement; if(focused && focused.classList.contains("card")){ const btn = focused.querySelector(".fav-btn"); if(btn) btn.click(); } } } else if(e.key === "r" && !sheetOpen){ if(lastFiltered.length) openSheet(lastFiltered[Math.floor(Math.random()*lastFiltered.length)]); } }); /* ───── Sort toggle ───── */ function setSort(mode){ state.sort = mode; $("#sort-index").classList.toggle("on", mode === "index"); $("#sort-recent").classList.toggle("on", mode === "recent"); render(); } $("#sort-index").addEventListener("click", ()=>setSort("index")); $("#sort-recent").addEventListener("click", ()=>setSort("recent")); ["sort-index","sort-recent"].forEach(id=>{ $("#"+id).addEventListener("keydown", e=>{ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); setSort(id==="sort-index"?"index":"recent"); } }); }); /* ───── Active filters bar ───── */ function renderActiveFilters(){ const bar = $("#active-filters"); if(!bar) return; const tags = []; state.ecosystems.forEach(e => tags.push({type:"eco", val:e})); state.categories.forEach(c => tags.push({type:"cat", val:c})); state.statuses.forEach(s => tags.push({type:"stat", val:s})); if(state.favOnly) tags.push({type:"fav", val:"Saved only"}); if(state.newOnly) tags.push({type:"newOnly", val:"New additions"}); if(state.query) tags.push({type:"q", val:`"${state.query}"`}); if(!tags.length){ bar.style.display="none"; return; } bar.style.display = "flex"; bar.innerHTML = `Active:` + tags.map(t=> `` ).join(""); $$(".active-filter-tag", bar).forEach(tag => { tag.addEventListener("click", ()=>{ const {type, val} = tag.dataset; if(type==="eco"){ state.ecosystems.delete(val); $$(`#eco-chips .chip`).forEach(c=>{ if(c.dataset.filterVal===val) c.setAttribute("aria-pressed","false"); }); } else if(type==="cat"){ state.categories.delete(val); $$(`#cat-chips .chip`).forEach(c=>{ if(c.dataset.filterVal===val) c.setAttribute("aria-pressed","false"); }); } else if(type==="stat"){ state.statuses.delete(val); $$(`#status-chips .chip`).forEach(c=>{ if(c.dataset.filterVal===val) c.setAttribute("aria-pressed","false"); }); } else if(type==="fav"){ state.favOnly = false; $("#fav-filter-btn")?.setAttribute("aria-pressed","false"); } else if(type==="newOnly"){ state.newOnly = false; $$(".preset-btn").forEach(b=>{ if(b.dataset.preset==="new") b.classList.remove("active"); }); } else if(type==="q"){ state.query = ""; $("#q").value = ""; } render(); }); }); } /* ───── Scroll-to-top ───── */ window.addEventListener("scroll", ()=>{ const btn = $("#scroll-top"); if(btn) btn.classList.toggle("visible", window.scrollY > 500); }, {passive:true}); $("#scroll-top").addEventListener("click", ()=>window.scrollTo({top:0, behavior:"smooth"})); /* ───── Grid toolbar ───── */ $("#surprise-btn").addEventListener("click", ()=>{ if(!lastFiltered.length) return; openSheet(lastFiltered[Math.floor(Math.random()*lastFiltered.length)]); }); $("#fav-filter-btn").addEventListener("click", ()=>{ state.favOnly = !state.favOnly; $("#fav-filter-btn").setAttribute("aria-pressed", state.favOnly?"true":"false"); render(); }); $("#export-btn").addEventListener("click", ()=>{ const data = lastFiltered.map(s=>{ const h = s.howto || HOWTO[s.id] || ""; return {id:s.id,name:s.name,ecosystem:s.ecosystem,category:s.category,status:s.status,description:s.description,trigger:s.trigger,howto:h,example:s.example,source:s.source}; }); const blob = new Blob([JSON.stringify(data,null,2)], {type:"application/json"}); const url = URL.createObjectURL(blob); const a = Object.assign(document.createElement("a"), {href:url, download:`skills-${Date.now()}.json`}); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`exported ${data.length} entr${data.length===1?"y":"ies"}`); }); /* ───── Hash routing ───── */ function openFromHash(){ const id = location.hash.replace("#",""); if(!id) return; const s = SKILLS.find(x => x.id === id); if(s){ openSheet(s, {pushHash:false}); const card = document.getElementById("skill-"+id); if(card){ card.classList.add("hashed"); setTimeout(()=>card.classList.remove("hashed"), 2400); } } } window.addEventListener("hashchange", openFromHash); /* ───── Matrix ───── */ function buildMatrix(){ const t = $("#matrix"); $("#matrix-count").textContent = MATRIX_ROWS.length; $("#matrix-ecosystem-count").textContent = ECOSYSTEMS.length; const headRow = ` Capability ${ECOSYSTEMS.map(e=>`
${uniqueSvg(GLYPHS[e])}${e}
`).join("")} `; const body = ` ${MATRIX_ROWS.map((row, i)=>{ const [label, ...cells] = row; return ` ${String(i+1).padStart(2,'0')}${label} ${cells.map(c=>{ if(c==="y") return `✓`; if(c==="n") return `✗`; return `partial`; }).join("")} `; }).join("")} `; t.insertAdjacentHTML("beforeend", headRow); t.insertAdjacentHTML("beforeend", body); } /* ───── References ───── */ function buildRefs(){ const ol = $("#refs-list"); ol.innerHTML = REFS.map(r=>{ const isUrl = r.venue && !r.venue.includes(' '); const venueHTML = isUrl ? `${r.venue}` : r.venue; return `
  • ${r.who}. ${r.title}. ${r.year}.${venueHTML}
  • `; }).join(""); } /* ───── Wire search ───── */ $("#q").addEventListener("input", e=>{ state.query = e.target.value.trim(); render(); }); $("#reset").addEventListener("click", ()=>{ state.query = ""; state.ecosystems.clear(); state.categories.clear(); state.statuses.clear(); state.favOnly = false; state.newOnly = false; $("#q").value = ""; $$(".chip[aria-pressed='true']").forEach(c=>c.setAttribute("aria-pressed","false")); $$(".preset-btn").forEach(b=>b.classList.remove("active")); render(); }); /* ───── Add Skill modal ───── */ function genId(eco, name){ const prefix = ECO_PREFIX[eco] || "xx"; const slug = name.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""); return prefix + "-" + slug; } function openAddSheet(){ document.getElementById("add-scrim").classList.add("open"); document.body.style.overflow = "hidden"; setTimeout(()=>document.getElementById("f-name").focus(), 60); } function closeAddSheet(){ document.getElementById("add-scrim").classList.remove("open"); document.body.style.overflow = ""; document.getElementById("add-skill-form").reset(); document.getElementById("id-preview").innerHTML = "id: —"; } document.getElementById("add-skill-btn").addEventListener("click", openAddSheet); document.getElementById("add-close-btn").addEventListener("click", closeAddSheet); document.getElementById("add-cancel-btn").addEventListener("click", closeAddSheet); document.getElementById("add-scrim").addEventListener("click", e=>{ if(e.target.id==="add-scrim") closeAddSheet(); }); // Live ID preview as user types name / picks ecosystem ["f-name","f-ecosystem"].forEach(fid=>{ document.getElementById(fid).addEventListener("input", ()=>{ const name = document.getElementById("f-name").value; const eco = document.getElementById("f-ecosystem").value; const el = document.getElementById("id-preview"); el.innerHTML = (name && eco) ? `id: ${genId(eco,name)}` : "id: —"; }); }); document.getElementById("add-skill-form").addEventListener("submit", e=>{ e.preventDefault(); const val = id => document.getElementById(id).value.trim(); const name = val("f-name"); const ecosystem = val("f-ecosystem"); const category = val("f-category"); const status = val("f-status"); const description = val("f-desc"); const trigger = val("f-trigger"); const howto = val("f-howto"); const example = val("f-example"); const source = val("f-source") || "#"; if(!name||!ecosystem||!category||!status||!description||!trigger||!howto||!example){ showToast("please fill in all required fields"); return; } const id = genId(ecosystem, name); if(SKILLS.find(x=>x.id===id)){ showToast("a skill with that name already exists"); return; } const skill = {id,name,ecosystem,category,status,description,trigger,howto,example,source,_custom:true}; SKILLS.push(skill); const saved = JSON.parse(localStorage.getItem("skills-custom")||"[]"); saved.push(skill); localStorage.setItem("skills-custom", JSON.stringify(saved)); closeAddSheet(); render(); showToast(`"${name}" added`); requestAnimationFrame(()=>openSheet(skill)); }); /* ───── Boot ───── */ buildChips(); buildEcoStrip(); buildMatrix(); render(); openFromHash(); updateFavCount(); /* Wire corpus download link in footer */ const _corpusLink = document.getElementById("export-corpus-link"); if(_corpusLink) _corpusLink.addEventListener("click", (e)=>{ e.preventDefault(); $("#export-btn").click(); }); /* ═══════════════════════════════════════════════════════════ DARK MODE TOGGLE ═══════════════════════════════════════════════════════════ */ (function(){ const btn = document.getElementById("dark-toggle"); if(!btn) return; function applyIcon(){ const dark = document.documentElement.getAttribute("data-theme") === "dark"; btn.textContent = dark ? "○" : "●"; btn.title = dark ? "Switch to light mode" : "Switch to dark mode"; btn.setAttribute("aria-label", dark ? "Switch to light mode" : "Switch to dark mode"); } applyIcon(); btn.addEventListener("click", ()=>{ const isDark = document.documentElement.getAttribute("data-theme") === "dark"; document.documentElement.setAttribute("data-theme", isDark ? "light" : "dark"); localStorage.setItem("sayn-theme", isDark ? "light" : "dark"); applyIcon(); }); })(); /* ═══════════════════════════════════════════════════════════ ANIMATED HERO COUNTERS ═══════════════════════════════════════════════════════════ */ function animateCount(el, target, duration){ if(!el) return; duration = duration || 1100; const startTime = performance.now(); function tick(now){ const t = Math.min((now - startTime) / duration, 1); const eased = 1 - Math.pow(1 - t, 3); /* ease-out cubic */ el.textContent = Math.round(eased * target); if(t < 1) requestAnimationFrame(tick); else el.textContent = target; } requestAnimationFrame(tick); } /* Run after boot (boot is synchronous above, so this fires right after) */ setTimeout(()=>{ animateCount(document.getElementById("hero-skill-count"), SKILLS.length, 1100); }, 80); /* ═══════════════════════════════════════════════════════════ GITHUB STAR COUNT — always fetches live on every page load so the count reflects new stars immediately. GitHub allows 60 unauthenticated req/hr per IP — fine for a static site where each visitor has their own quota. ═══════════════════════════════════════════════════════════ */ (function(){ const REPO = "kishormorol/SkillsAllYouNeed"; function fmtStars(n){ if(n >= 1000) return (n/1000).toFixed(1).replace(/\.0$/,"") + "k"; return String(n); } function applyCount(n){ const navCount = document.getElementById("star-count"); const ctaCount = document.getElementById("star-count-cta"); if(n < 1){ return; } /* keep hidden until at least 1 star */ const formatted = fmtStars(n); if(navCount){ navCount.textContent = formatted; navCount.style.display = ""; } if(ctaCount){ ctaCount.textContent = formatted; ctaCount.classList.add("loaded"); } } fetch("https://api.github.com/repos/" + REPO, {cache:"no-store"}) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(data => applyCount(data.stargazers_count || 0)) .catch(() => {}); /* fail silently — button still works */ })(); /* ═══════════════════════════════════════════════════════════ QUICK PRESET FILTERS ═══════════════════════════════════════════════════════════ */ (function(){ function resetAll(){ state.ecosystems.clear(); state.categories.clear(); state.statuses.clear(); state.query = ""; state.favOnly = false; state.newOnly = false; document.getElementById("q").value = ""; document.querySelectorAll(".chip[aria-pressed='true']").forEach(c=>c.setAttribute("aria-pressed","false")); document.querySelectorAll(".preset-btn").forEach(b=>b.classList.remove("active")); } document.querySelectorAll(".preset-btn").forEach(btn=>{ btn.addEventListener("click", ()=>{ const p = btn.dataset.preset; resetAll(); if(p === "claude-code"){ state.ecosystems.add("Claude Code"); document.querySelector(`#eco-chips .chip[data-eco="Claude Code"]`)?.setAttribute("aria-pressed","true"); btn.classList.add("active"); } else if(p === "agentic"){ state.categories.add("Agentic"); document.querySelector(`#cat-chips .chip[data-filter-val="Agentic"]`)?.setAttribute("aria-pressed","true"); btn.classList.add("active"); } else if(p === "stable"){ state.statuses.add("Stable"); document.querySelector(`#status-chips .chip[data-filter-val="Stable"]`)?.setAttribute("aria-pressed","true"); btn.classList.add("active"); } else if(p === "new"){ state.newOnly = true; btn.classList.add("active"); } /* "clear" just resets — already done above */ render(); smoothScrollTo(document.getElementById("catalog-section"), -20); }); }); /* Deactivate preset buttons when user manually changes filters */ document.getElementById("q").addEventListener("input", ()=>{ document.querySelectorAll(".preset-btn").forEach(b=>b.classList.remove("active")); }); document.querySelectorAll(".chip").forEach(c=>{ c.addEventListener("click", ()=>{ document.querySelectorAll(".preset-btn").forEach(b=>b.classList.remove("active")); }); }); })(); /* ═══════════════════════════════════════════════════════════ ROTATING SEARCH PLACEHOLDER ═══════════════════════════════════════════════════════════ */ (function(){ const hints = [ "search names, descriptions, triggers…", "try: memory", "try: code review", "try: generate image", "try: webhook", "try: artifacts", "try: voice mode", "try: Claude Code", "try: export to docx", "try: browser automation", ]; let idx = 0; const q = document.getElementById("q"); setInterval(()=>{ if(document.activeElement !== q && !state.query){ idx = (idx + 1) % hints.length; q.setAttribute("placeholder", hints[idx]); } }, 3200); })(); /* ═══════════════════════════════════════════════════════════ ONBOARDING STRIP — show once, dismiss to localStorage ═══════════════════════════════════════════════════════════ */ (function(){ const strip = document.getElementById("onboarding-strip"); if(!strip) return; if(!localStorage.getItem("sayn-onboarding-seen")){ strip.style.display = "block"; } document.getElementById("onboarding-dismiss").addEventListener("click", ()=>{ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; strip.classList.add("is-leaving"); setTimeout(()=>{ strip.style.display = "none"; }, reduced ? 0 : 180); localStorage.setItem("sayn-onboarding-seen", "1"); }); })(); /* ───── Smooth-scroll all internal anchor links ───── */ document.querySelectorAll('a[href^="#"]').forEach(link=>{ link.addEventListener("click", (e)=>{ const id = link.getAttribute("href").slice(1); const target = document.getElementById(id); if(target){ e.preventDefault(); target.scrollIntoView({behavior:"smooth", block:"start"}); } }); }); /* ───── Chip: reset all on double-click of already-active chip ───── */ // (chips already toggle on single click; this just adds polish) /* ───── Make add-skill-btn also reachable via keyboard Enter/Space ───── */ document.getElementById("add-skill-btn").addEventListener("keydown", e=>{ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); openAddSheet(); } }); document.getElementById("surprise-btn").addEventListener("keydown", e=>{ if(e.key === "Enter" || e.key === " "){ e.preventDefault(); if(lastFiltered.length) openSheet(lastFiltered[Math.floor(Math.random()*lastFiltered.length)]); } });