feat: expand skills review navigation and catalog
This commit is contained in:
+34
-8
@@ -15,6 +15,11 @@ const copy = async (content) => {
|
||||
};
|
||||
const packageFiles = (entry = state.selected) => files[entry.id] || [{ name: 'SKILL.md', path: entry.path, kind: 'skill' }];
|
||||
const packageSearchText = (entry) => packageFiles(entry).map((file) => `${file.name} ${file.kind}`).join(' ');
|
||||
function packageSummary(entry) {
|
||||
const counts = packageFiles(entry).reduce((all, file) => { all[file.kind] = (all[file.kind] || 0) + 1; return all; }, {});
|
||||
const labels = { skill:'skill', reference:'ref', script:'script', template:'template', data:'data', asset:'asset' };
|
||||
return Object.entries(counts).map(([kind, count]) => `${count} ${labels[kind] || kind}${count === 1 ? '' : 's'}`).join(' · ');
|
||||
}
|
||||
const unchangedDraft = (file) => `# ${file.name}\n\n> Kept as-is in the improved package\n\nThis ${file.kind} file was not rewritten. Select **Change lens** to see why the improved draft concentrates its changes in the main skill contract.`;
|
||||
const currentSource = () => state.sourceByPath.get(state.file.path);
|
||||
const currentContent = () => state.preview === 'original' ? (currentSource() || 'Loading original file…') : (state.file.improved || (state.file.name === 'SKILL.md' ? state.selected.improved : (currentSource() ? `# ${state.file.name}\n\n> Kept as-is in the improved package\n\n${currentSource()}` : unchangedDraft(state.file))));
|
||||
@@ -26,7 +31,7 @@ const inlineMarkdown = (value) => escape(value)
|
||||
const tableCells = (line) => line.trim().replace(/^\||\|$/g, '').split('|').map((cell) => cell.trim());
|
||||
const isTableDivider = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
|
||||
function markdownMarkup(markdown) {
|
||||
const lines = markdown.replace(/\r/g, '').split('\n'); let index = 0; const out = [];
|
||||
const lines = markdown.replace(/\r/g, '').split('\n'); const headings = markdownHeadings(markdown); let headingIndex = 0; let index = 0; const out = [];
|
||||
if (lines[0] === '---') {
|
||||
const end = lines.indexOf('---', 1);
|
||||
if (end > 0) { out.push(`<dl class="markdown-frontmatter">${lines.slice(1, end).map((line) => { const [key, ...rest] = line.split(':'); return rest.length ? `<dt>${escape(key)}</dt><dd>${inlineMarkdown(rest.join(':').trim())}</dd>` : ''; }).join('')}</dl>`); index = end + 1; }
|
||||
@@ -36,7 +41,7 @@ function markdownMarkup(markdown) {
|
||||
const line = lines[index];
|
||||
if (!line.trim()) { index += 1; continue; }
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (heading) { const level = heading[1].length; out.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`); index += 1; continue; }
|
||||
if (heading) { const level = heading[1].length; const item = headings[headingIndex++]; out.push(`<h${level} id="${item.id}">${inlineMarkdown(heading[2])}</h${level}>`); index += 1; continue; }
|
||||
if (/^```/.test(line)) { const language = line.slice(3).trim(); const code = []; index += 1; while (index < lines.length && !/^```/.test(lines[index])) code.push(lines[index++]); if (index < lines.length) index += 1; out.push(`<pre><code${language ? ` data-language="${escape(language)}"` : ''}>${escape(code.join('\n'))}</code></pre>`); continue; }
|
||||
if (line.includes('|') && isTableDivider(lines[index + 1] || '')) { const headings = tableCells(line); index += 2; const rows = []; while (index < lines.length && lines[index].includes('|') && lines[index].trim()) rows.push(tableCells(lines[index++])); out.push(`<div class="markdown-table-wrap"><table><thead><tr>${headings.map((cell) => `<th>${inlineMarkdown(cell)}</th>`).join('')}</tr></thead><tbody>${rows.map((row) => `<tr>${headings.map((_, cell) => `<td>${inlineMarkdown(row[cell] || '')}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`); continue; }
|
||||
const list = line.match(/^([-*+]|\d+\.)\s+(.+)$/);
|
||||
@@ -47,6 +52,19 @@ function markdownMarkup(markdown) {
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
function markdownHeadings(markdown) {
|
||||
const used = new Map(); let fenced = false;
|
||||
return markdown.replace(/\r/g, '').split('\n').flatMap((line) => {
|
||||
if (/^```/.test(line)) { fenced = !fenced; return []; }
|
||||
const match = !fenced && line.match(/^(#{1,6})\s+(.+)$/); if (!match) return [];
|
||||
const text = match[2].replace(/[`*_\[\]]/g, '').trim(); const base = text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '-').replace(/(^-|-$)/g, '') || 'section'; const seen = used.get(base) || 0; used.set(base, seen + 1);
|
||||
return [{ level: match[1].length, text, id: seen ? `${base}-${seen + 1}` : base }];
|
||||
});
|
||||
}
|
||||
function markdownToc(markdown) {
|
||||
const headings = markdownHeadings(markdown); if (headings.length < 2) return '';
|
||||
return `<nav class="markdown-toc" aria-label="On this page"><span>ON THIS PAGE</span><ol>${headings.map((heading) => `<li class="level-${heading.level}"><a href="#${heading.id}">${escape(heading.text)}</a></li>`).join('')}</ol></nav>`;
|
||||
}
|
||||
const changeRows = (entry) => entry.improve.map((why, index) => ({
|
||||
kind: ['SAFETY', 'SCOPE', 'EVIDENCE', 'STRUCTURE'][index] || 'CLARITY',
|
||||
before: index === 0 ? 'The submitted guidance leaves a material decision implicit.' : 'The submitted package carries detail without a clear boundary.',
|
||||
@@ -84,10 +102,12 @@ function selectFromUrl() {
|
||||
function renderList() {
|
||||
const items = visible();
|
||||
$('#count').textContent = state.searching ? `Searching package files… ${items.length} of ${catalog.length}` : `${items.length} of ${catalog.length} reviewed`;
|
||||
$('#skill-list').innerHTML = items.map((item) => `<button role="option" aria-selected="${item.id === state.selected.id}" class="${item.id === state.selected.id ? 'active' : ''}" data-id="${item.id}"><span>AUTHOR · ${escape(item.author)}</span><strong>${escape(item.title)}</strong><small>SKILL · ${escape(item.id)} · ${escape(item.status)}</small></button>`).join('');
|
||||
$('#skill-list').querySelectorAll('button').forEach((button) => button.addEventListener('click', () => {
|
||||
state.selected = catalog.find((item) => item.id === button.dataset.id); state.file = packageFiles()[0]; state.preview = 'original'; state.lens = false; state.rendered = false; state.diff = false; syncUrl(); renderList(); renderDetail(); loadSelectedFile();
|
||||
}));
|
||||
$('#skill-list').innerHTML = items.map((item) => `<button role="option" aria-selected="${item.id === state.selected.id}" class="${item.id === state.selected.id ? 'active' : ''}" data-id="${item.id}"><span>AUTHOR · ${escape(item.author)}</span><strong>${escape(item.title)}</strong><small>SKILL · ${escape(item.id)} · ${escape(item.status)}</small><em>${escape(packageSummary(item))}</em></button>`).join('');
|
||||
$('#skill-list').querySelectorAll('button').forEach((button) => button.addEventListener('click', () => selectSkill(button.dataset.id)));
|
||||
}
|
||||
function selectSkill(id, focus = false) {
|
||||
state.selected = catalog.find((item) => item.id === id) || state.selected; state.file = packageFiles()[0]; state.preview = 'original'; state.lens = false; state.rendered = false; state.diff = false; syncUrl(); renderList(); renderDetail(); loadSelectedFile();
|
||||
if (focus) $('#skill-list').querySelector(`[data-id="${state.selected.id}"]`)?.focus();
|
||||
}
|
||||
async function fetchSource(entry, file) {
|
||||
if (state.sourceByPath.has(file.path)) return state.sourceByPath.get(file.path);
|
||||
@@ -133,8 +153,8 @@ function previewMarkup(entry, available) {
|
||||
if (state.preview === 'improved' && state.lens) return lensMarkup(entry);
|
||||
if (state.diff) return diffMarkup(entry);
|
||||
const label = state.preview === 'original' ? 'ORIGINAL / SAFETY-REDACTED WHERE NEEDED' : 'IMPROVED DRAFT / PACKAGE-AWARE';
|
||||
const body = state.rendered ? `<div class="markdown-preview" aria-label="Rendered Markdown preview">${markdownMarkup(currentContent())}</div>` : `<pre><code>${escape(currentContent())}</code></pre>`;
|
||||
return `<section class="preview"><header><span>${label}</span><div>${state.preview === 'improved' ? '<button data-lens aria-pressed="false">Change lens</button>' : ''}<button data-diff aria-pressed="false">Diff</button><button data-render aria-pressed="${state.rendered}">${state.rendered ? 'Source' : 'Preview'}</button><button data-copy>Copy</button><button data-download>Download</button></div></header><nav class="file-tabs" aria-label="Skill package files">${available.map((item) => `<button class="${item.name === state.file.name ? 'active' : ''}" data-file="${escape(item.name)}"><span>${escape(item.kind)}</span>${escape(item.name)}</button>`).join('')}</nav>${body}</section>`;
|
||||
const content = currentContent(); const body = state.rendered ? `<div class="markdown-preview" aria-label="Rendered Markdown preview">${markdownToc(content)}${markdownMarkup(content)}</div>` : `<pre><code>${escape(content)}</code></pre>`;
|
||||
return `<section class="preview"><header><div class="preview-title"><span>FILE PREVIEW</span><small>${label}</small></div><div>${state.preview === 'improved' ? '<button data-lens aria-pressed="false">Change lens</button>' : ''}<button data-diff aria-pressed="false">Diff</button><button class="preview-markdown" data-render aria-pressed="${state.rendered}">${state.rendered ? 'View source' : 'Preview Markdown'}</button><button data-copy>Copy</button><button data-download>Download</button></div></header><nav class="file-tabs" aria-label="Skill package files">${available.map((item) => `<button class="${item.name === state.file.name ? 'active' : ''}" data-file="${escape(item.name)}"><span>${escape(item.kind)}</span>${escape(item.name)}</button>`).join('')}</nav>${body}</section>`;
|
||||
}
|
||||
function renderDetail() {
|
||||
const entry = state.selected; const available = packageFiles(entry);
|
||||
@@ -149,5 +169,11 @@ function renderDetail() {
|
||||
}
|
||||
|
||||
$('#skill-filter').addEventListener('input', (event) => { state.query = event.target.value.toLowerCase().trim(); renderList(); schedulePackageSearch(); });
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.metaKey || event.ctrlKey || event.altKey || /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || '')) return;
|
||||
const items = visible(); const current = items.findIndex((item) => item.id === state.selected.id);
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); const offset = event.key === 'ArrowDown' ? 1 : -1; selectSkill(items[(current + offset + items.length) % items.length]?.id, true); }
|
||||
if (event.key.toLowerCase() === 'p') { event.preventDefault(); $('[data-render]')?.click(); }
|
||||
});
|
||||
window.addEventListener('popstate', () => { selectFromUrl(); renderList(); renderDetail(); loadSelectedFile(); });
|
||||
selectFromUrl(); renderList(); renderDetail(); loadSelectedFile();
|
||||
|
||||
Reference in New Issue
Block a user