aboutsummaryrefslogtreecommitdiff
path: root/skills.html
diff options
context:
space:
mode:
Diffstat (limited to 'skills.html')
-rw-r--r--skills.html168
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