diff options
Diffstat (limited to 'skills.html')
| -rw-r--r-- | skills.html | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/skills.html b/skills.html new file mode 100644 index 0000000..a606aca --- /dev/null +++ b/skills.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> +<meta name="theme-color" content="#0a0a14"> +<title>WebX11 — Skills</title> +<script>!function(){var v=localStorage.getItem('webx11_cache_ver');if(!v)return;var m=v.match(/v([\d.]+)/);if(!m)return;document.title='WebX11 v'+m[1]+' — Skills';document.querySelector('.badge-v')&&(document.querySelector('.badge-v').textContent='v'+m[1])}()</script> +<style> +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent} +:root{--bg:#0a0a14;--bg2:#141428;--bg3:#1c1c36;--bg4:#24244a;--text:#e8e8ff;--text2:#b0b0cc;--accent:#818cf8;--accent2:#a78bfa;--green:#34d399;--cyan:#22d3ee;--amber:#fbbf24;--rose:#fb7185;--pink:#e879f9;--border:#3a3a5a;--radius:12px;--font-mono:'JetBrains Mono','Fira Code','Cascadia Code','Courier New',monospace;--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter','SF Pro',Roboto,sans-serif} +body{background:var(--bg);color:var(--text);font-family:var(--font-sans);line-height:1.6;padding:0 16px 80px;max-width:960px;margin:0 auto;min-height:100vh} +header{padding:24px 0 16px;border-bottom:1px solid var(--border);margin-bottom:24px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} +h1{font-size:1.5rem;font-weight:600;letter-spacing:-0.02em} +.badge-v{background:var(--bg4);color:var(--accent);font-family:var(--font-mono);font-size:.75rem;padding:2px 10px;border-radius:20px;border:1px solid var(--border)} +.search-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;align-items:center} +#search{flex:1;min-width:180px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 14px;color:var(--text);font-size:.95rem;font-family:var(--font-sans);outline:none;transition:border-color .2s} +#search:focus{border-color:var(--accent)} +#search::placeholder{color:var(--text2)} +.cat-filters{display:flex;gap:6px;flex-wrap:wrap} +.cat-btn{background:var(--bg3);color:var(--text2);border:1px solid var(--border);border-radius:20px;padding:6px 16px;font-size:.82rem;cursor:pointer;transition:all .2s;font-family:inherit} +.cat-btn:hover{border-color:var(--accent);color:var(--text)} +.cat-btn.active{background:var(--accent);color:#fff;border-color:var(--accent)} +.stats{color:var(--text2);font-size:.85rem;margin-bottom:16px} +.skills-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px} +.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;transition:border-color .2s,transform .1s} +.card:hover{border-color:var(--accent);transform:translateY(-1px)} +.card h3{margin-bottom:6px;font-size:1rem} +.card h3 a{color:var(--accent);text-decoration:none} +.card h3 a:hover{text-decoration:underline} +.card .desc{color:var(--text2);font-size:.85rem;line-height:1.5;margin-bottom:10px} +.card .tags{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px} +.tag{background:var(--bg3);color:var(--cyan);font-size:.72rem;padding:2px 8px;border-radius:10px;font-family:var(--font-mono)} +.tag.cat-system{color:var(--cyan)} +.tag.cat-dev{color:var(--green)} +.tag.cat-research{color:var(--amber)} +.tag.cat-productivity{color:var(--pink)} +.card .meta{color:var(--text2);font-size:.75rem;display:flex;justify-content:space-between} +.error-banner{background:#4a1a1a;border:1px solid var(--rose);border-radius:var(--radius);padding:16px;text-align:center;color:var(--rose);margin-bottom:16px} +.error-banner button{background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 20px;margin-top:8px;cursor:pointer;font-family:inherit} +.empty-state{text-align:center;color:var(--text2);padding:40px 20px;font-size:.9rem} +#loading{text-align:center;color:var(--text2);padding:40px} +footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--border);text-align:center;font-size:.8rem;color:var(--text2)} +footer a{color:var(--accent);text-decoration:none} +.hidden{display:none!important} +@media(max-width:600px){.skills-grid{grid-template-columns:1fr}body{padding:0 12px 60px}} +</style> +</head> +<body> +<header> + <h1>🧠 <span class="badge-v"></span> Skills</h1> +</header> +<div class="search-row"> + <input type="text" id="search" placeholder="Search skills by name, description, or tag..."> + <div class="cat-filters" id="catFilters"></div> +</div> +<div class="stats" id="stats"></div> +<div id="loading">Loading skills...</div> +<div id="error" class="error-banner hidden"> + <div id="errorMsg">Failed to load skills index.</div> + <button onclick="loadSkills()">Retry</button> +</div> +<div id="empty" class="empty-state hidden">No skills match your filters.</div> +<div class="skills-grid" id="grid"></div> +<footer><a href="https://webx11.duckdns.org/spaces/newos4/skills/">Skills in VFS</a> · <a href="help.html">Help</a> · <a href="modules.html">Modules</a></footer> +<script> +const INDEX_URL = '/spaces/newos4/skills/index.json'; +const VFS_BASE = '/spaces/newos4/skills/'; + +let allSkills = []; +let currentCategory = 'all'; +let currentQuery = ''; + +function getUniqueSkills(data) { + const seen = {}; + Object.values(data).forEach(s => { + if (!seen[s.name]) seen[s.name] = s; + }); + return Object.values(seen); +} + +async function loadSkills() { + const grid = document.getElementById('grid'); + const loading = document.getElementById('loading'); + const error = document.getElementById('error'); + const empty = document.getElementById('empty'); + loading.classList.remove('hidden'); + error.classList.add('hidden'); + empty.classList.add('hidden'); + grid.innerHTML = ''; + + try { + const resp = await fetch(INDEX_URL + '?v=' + (localStorage.getItem('webx11_cache_ver') || '')); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const data = await resp.json(); + allSkills = getUniqueSkills(data); + renderCatFilters(); + applyFilters(); + loading.classList.add('hidden'); + } catch(e) { + loading.classList.add('hidden'); + document.getElementById('errorMsg').textContent = 'Failed to load skills: ' + e.message; + error.classList.remove('hidden'); + } +} + +function renderCatFilters() { + const cats = ['all', ...new Set(allSkills.map(s => s.category || 'general'))]; + const container = document.getElementById('catFilters'); + container.innerHTML = cats.map(c => + `<button class="cat-btn${c === currentCategory ? ' active' : ''}" data-cat="${c}" onclick="setCategory('${c}')">${c}</button>` + ).join(''); +} + +function setCategory(cat) { + currentCategory = cat; + document.querySelectorAll('.cat-btn').forEach(b => b.classList.toggle('active', b.dataset.cat === cat)); + applyFilters(); +} + +function applyFilters() { + const q = document.getElementById('search').value.toLowerCase().trim(); + currentQuery = q; + + let filtered = allSkills; + if (currentCategory !== 'all') { + filtered = filtered.filter(s => (s.category || 'general') === currentCategory); + } + if (q) { + filtered = filtered.filter(s => + s.name.toLowerCase().includes(q) || + (s.description || '').toLowerCase().includes(q) || + (s.tags || []).some(t => t.toLowerCase().includes(q)) + ); + } + + filtered.sort((a, b) => (a.category || '').localeCompare(b.category || '') || a.name.localeCompare(b.name)); + + const grid = document.getElementById('grid'); + const stats = document.getElementById('stats'); + const empty = document.getElementById('empty'); + + stats.textContent = filtered.length + ' of ' + allSkills.length + ' skills'; + + if (filtered.length === 0) { + grid.innerHTML = ''; + empty.classList.remove('hidden'); + return; + } + empty.classList.add('hidden'); + + const categoryColors = { system: 'cat-system', dev: 'cat-dev', research: 'cat-research', productivity: 'cat-productivity' }; + grid.innerHTML = filtered.map(s => { + const dateStr = s.updated ? new Date(s.updated).toLocaleDateString() : ''; + return `<div class="card"> + <h3><a href="${VFS_BASE}${encodeURIComponent(s.name)}.md" target="_blank" title="View skill file">${s.name}</a></h3> + <div class="desc">${s.description || ''}</div> + <div class="tags">${(s.tags || []).map(t => `<span class="tag ${categoryColors[s.category] || ''}">${t}</span>`).join('')}</div> + <div class="meta"><span class="cat-label">${s.category || 'general'}</span>${dateStr ? '<span>Updated '+dateStr+'</span>' : ''}</div> + </div>`; + }).join(''); +} + +document.getElementById('search').addEventListener('input', applyFilters); +document.addEventListener('DOMContentLoaded', loadSkills); +</script> +</body> +</html>
\ No newline at end of file |