feat: add skill preview search and diff

This commit is contained in:
Marcos Silva
2026-09-04 09:45:03 -03:00
parent 5756dceb5a
commit 96771dfbd6
5 changed files with 91 additions and 15 deletions
+4 -2
View File
@@ -47,7 +47,7 @@ if (rulesHtml.includes('script src="http') || rulesHtml.includes('rel="styleshee
for (const token of ['@media(min-width:2200px)','@media(max-width:900px)','@media(max-width:600px)','prefers-reduced-motion']) if (!rulesCss.includes(token)) throw new Error(`missing rules responsive contract ${token}`);
console.log('rules standalone verification passed');
for (const token of ['id="catalog"','id="skill-filter"','id="skill-list"','id="detail"','?author=Name&skill=skill-id&view=improved']) if (!reviewHtml.includes(token)) throw new Error(`missing review page content ${token}`);
for (const token of ["from './catalog.js'", "from './files.js'",'function renderList','function renderDetail','loadSelectedFile','function selectFromUrl','function syncUrl','URLSearchParams','navigator.clipboard','document.execCommand','download','data-file','searchParams.set(\'file\'']) if (!reviewJs.includes(token)) throw new Error(`missing review interaction ${token}`);
for (const token of ["from './catalog.js'", "from './files.js'",'function renderList','function renderDetail','loadSelectedFile','schedulePackageSearch','fetchSource','packageSearchText','diffMarkup','diffRows','data-diff','searchParams.set(\'compare\'','markdownMarkup','data-render','searchParams.set(\'render\'','AUTHOR ·','SKILL ·','function selectFromUrl','function syncUrl','URLSearchParams','navigator.clipboard','document.execCommand','download','data-file','searchParams.set(\'file\'']) if (!reviewJs.includes(token)) throw new Error(`missing review interaction ${token}`);
for (const token of ['ndo-repro','gfiber-logging','confluence-page','diagram-plantuml','page-reviewer','unslop','reference.md','files =']) if (!`${reviewFiles}\n${read('skills-review/submitted-files.js')}`.includes(token)) throw new Error(`missing review file manifest ${token}`);
if ((reviewCatalog.match(/id:'/g) || []).length + (read('skills-review/submitted-catalog.js').match(/id:'/g) || []).length !== 21) throw new Error('review catalog does not cover all submissions');
if (!reviewCatalog.includes('hardcoded password') || !reviewCatalog.includes('safety-redacted') || !reviewJs.includes('[REDACTED]') || !reviewJs.includes('[REDACTED LOCAL USER]') || !reviewJs.includes('[REDACTED USER]')) throw new Error('review catalog does not record secret safety handling');
@@ -78,5 +78,7 @@ for (const token of ['currentContent','unchangedDraft','state.preview = button.d
if (reviewJs.includes("state.preview = 'original'; syncUrl(); renderDetail(); loadSelectedFile();")) throw new Error('file selection still resets improved mode');
console.log('review file-mode verification passed');
for (const token of ['const changeRows','function lensMarkup','data-lens','CHANGE LENS','What changed — and why.']) if (!reviewJs.includes(token)) throw new Error(`review change-lens contract missing ${token}`);
for (const token of ['.change-lens','.change-rows','@media(max-width:620px)','prefers-reduced-motion']) if (!reviewLensCss.includes(token)) throw new Error(`review change-lens CSS missing ${token}`);
for (const token of ['.change-lens','.change-rows','.skill-diff','.diff-lines','@media(max-width:620px)','prefers-reduced-motion']) if (!reviewLensCss.includes(token)) throw new Error(`review change-lens CSS missing ${token}`);
console.log('review change-lens verification passed');
for (const token of ['.markdown-preview','max-height:540px','.markdown-table-wrap','.markdown-frontmatter']) if (!read('skills-review/styles.css').includes(token)) throw new Error(`review markdown preview contract missing ${token}`);
console.log('review markdown preview verification passed');
+80 -12
View File
@@ -1,7 +1,7 @@
import { catalog } from './catalog.js';
import { files } from './files.js';
const state = { selected: catalog[0], query: '', preview: 'original', file: null, sourceByPath: new Map(), lens: false };
const state = { selected: catalog[0], query: '', preview: 'original', file: null, sourceByPath: new Map(), lens: false, rendered: false, diff: false, searching: false, contentMatches: new Set(), searchTimer: null, searchRequest: 0 };
const $ = (selector) => document.querySelector(selector);
const escape = (value) => value.replace(/[&<>"']/g, (character) => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#039;' })[character]);
const redact = (value, entry) => {
@@ -14,9 +14,39 @@ const copy = async (content) => {
const textarea = document.createElement('textarea'); textarea.value = content; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.append(textarea); textarea.select(); document.execCommand('copy'); textarea.remove();
};
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(' ');
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))));
const inlineMarkdown = (value) => escape(value)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1 ↗</a>');
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 = [];
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; }
}
const startsBlock = (line, next) => !line || /^#{1,6}\s+/.test(line) || /^```/.test(line) || /^[-*+]\s+/.test(line) || /^\d+\.\s+/.test(line) || /^>\s?/.test(line) || /^---+$/.test(line) || (line.includes('|') && isTableDivider(next || ''));
while (index < lines.length) {
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 (/^```/.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+(.+)$/);
if (list) { const ordered = /\d+\./.test(list[1]); const items = []; while (index < lines.length) { const item = lines[index].match(ordered ? /^\d+\.\s+(.+)$/ : /^[-*+]\s+(.+)$/); if (!item) break; items.push(`<li>${inlineMarkdown(item[1])}</li>`); index += 1; } out.push(`<${ordered ? 'ol' : 'ul'}>${items.join('')}</${ordered ? 'ol' : 'ul'}>`); continue; }
if (/^>\s?/.test(line)) { const quote = []; while (index < lines.length && /^>\s?/.test(lines[index])) quote.push(lines[index++].replace(/^>\s?/, '')); out.push(`<blockquote>${inlineMarkdown(quote.join(' '))}</blockquote>`); continue; }
if (/^---+$/.test(line)) { out.push('<hr>'); index += 1; continue; }
const paragraph = [line]; index += 1; while (index < lines.length && !startsBlock(lines[index], lines[index + 1])) paragraph.push(lines[index++]); out.push(`<p>${inlineMarkdown(paragraph.join(' '))}</p>`);
}
return out.join('');
}
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.',
@@ -24,7 +54,7 @@ const changeRows = (entry) => entry.improve.map((why, index) => ({
why
}));
function visible() { return catalog.filter((item) => `${item.author} ${item.title} ${item.focus}`.toLowerCase().includes(state.query)); }
function visible() { return catalog.filter((item) => `${item.author} ${item.title} ${item.id} ${item.focus} ${packageSearchText(item)}`.toLowerCase().includes(state.query) || state.contentMatches.has(item.id)); }
function syncUrl() {
const url = new URL(window.location.href);
url.searchParams.set('author', state.selected.author);
@@ -32,6 +62,8 @@ function syncUrl() {
url.searchParams.set('view', state.preview);
if (state.file && state.file.name !== 'SKILL.md') url.searchParams.set('file', state.file.name); else url.searchParams.delete('file');
if (state.preview === 'improved' && state.lens) url.searchParams.set('lens', 'changes'); else url.searchParams.delete('lens');
if (state.rendered) url.searchParams.set('render', 'preview'); else url.searchParams.delete('render');
if (state.diff) url.searchParams.set('compare', 'diff'); else url.searchParams.delete('compare');
history.replaceState({}, '', url);
}
function selectFromUrl() {
@@ -43,43 +75,79 @@ function selectFromUrl() {
state.query = byAuthor ? state.selected.author.toLowerCase() : '';
state.preview = view === 'improved' ? 'improved' : 'original';
state.lens = state.preview === 'improved' && params.get('lens') === 'changes';
state.rendered = params.get('render') === 'preview';
state.diff = params.get('compare') === 'diff';
state.file = packageFiles().find((item) => item.name === params.get('file')) || packageFiles()[0];
$('#skill-filter').value = byAuthor ? state.selected.author : '';
$('#submission-count').textContent = `${catalog.length} submissions`;
}
function renderList() {
const items = visible();
$('#count').textContent = `${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>${escape(item.author)}</span><strong>${escape(item.title)}</strong><small>${escape(item.status)}</small></button>`).join('');
$('#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; syncUrl(); renderList(); renderDetail(); loadSelectedFile();
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();
}));
}
async function loadSelectedFile() {
const entry = state.selected; const file = state.file;
async function fetchSource(entry, file) {
if (state.sourceByPath.has(file.path)) return state.sourceByPath.get(file.path);
try { state.sourceByPath.set(file.path, redact(await (await fetch(file.path)).text(), entry)); }
catch { state.sourceByPath.set(file.path, '# Original preview unavailable\n\nServe this site from the repository root to load the submitted source.'); }
return state.sourceByPath.get(file.path);
}
async function loadSelectedFile() {
const entry = state.selected; const file = state.file;
await fetchSource(entry, file);
if (state.selected.id === entry.id && state.file.path === file.path) renderDetail();
return state.sourceByPath.get(file.path);
}
function schedulePackageSearch() {
clearTimeout(state.searchTimer); const query = state.query; const request = ++state.searchRequest; state.contentMatches.clear();
if (query.length < 3) { state.searching = false; renderList(); return; }
state.searchTimer = setTimeout(async () => {
state.searching = true; renderList();
await Promise.all(catalog.flatMap((entry) => packageFiles(entry).map((file) => fetchSource(entry, file))));
if (request !== state.searchRequest) return;
state.contentMatches = new Set(catalog.filter((entry) => packageFiles(entry).some((file) => state.sourceByPath.get(file.path)?.toLowerCase().includes(query))).map((entry) => entry.id));
state.searching = false; renderList();
}, 180);
}
function lensMarkup(entry) {
return `<section class="change-lens" aria-label="Why this improved draft changed"><header><div><span>CHANGE LENS</span><h3>What changed — and why.</h3></div><button data-lens aria-pressed="true">Back to draft</button></header><p>The improved draft keeps the job, but narrows the decisions an agent must make from memory.</p><div class="change-rows">${changeRows(entry).map((change, index) => `<article><span>0${index + 1} / ${change.kind}</span><div><b> Before</b><p>${escape(change.before)}</p></div><div><b>+ After</b><p>${escape(change.after)}</p></div><aside><b>Why</b><p>${escape(change.why)}</p></aside></article>`).join('')}</div></section>`;
}
function diffRows(before, after) {
const oldLines = before.split('\n'); const newLines = after.split('\n'); const rows = []; let oldIndex = 0; let newIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
if (oldLines[oldIndex] === newLines[newIndex]) { rows.push(`<p class="same"><span>${oldIndex + 1}</span>${escape(oldLines[oldIndex] || '')}</p>`); oldIndex += 1; newIndex += 1; continue; }
const oldAhead = oldLines.slice(oldIndex + 1, oldIndex + 9).indexOf(newLines[newIndex]); const newAhead = newLines.slice(newIndex + 1, newIndex + 9).indexOf(oldLines[oldIndex]);
if (newIndex < newLines.length && (oldIndex >= oldLines.length || (oldAhead === -1 && newAhead !== -1) || newAhead < oldAhead)) { rows.push(`<p class="added"><span>+</span>${escape(newLines[newIndex++])}</p>`); continue; }
if (oldIndex < oldLines.length) { rows.push(`<p class="removed"><span></span>${escape(oldLines[oldIndex++])}</p>`); continue; }
}
return rows.join('');
}
function diffMarkup(entry) {
if (state.file.name !== 'SKILL.md') return `<section class="skill-diff" aria-label="Draft comparison"><header><div><span>PACKAGE DIFF</span><h3>Supporting file unchanged.</h3></div><button data-diff aria-pressed="true">Back to draft</button></header><p>This review only rewrites the main skill contract. The selected ${escape(state.file.kind)} file remains available in its original form.</p></section>`;
return `<section class="skill-diff" aria-label="Original and improved skill comparison"><header><div><span>SKILL DIFF</span><h3>Original → improved draft</h3></div><button data-diff aria-pressed="true">Back to draft</button></header><p>Green lines are additions; red lines are removals. Unmarked lines are shared context.</p><div class="diff-lines">${diffRows(currentSource() || 'Loading original Markdown…', entry.improved)}</div></section>`;
}
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';
return `<section class="preview"><header><span>${label}</span><div>${state.preview === 'improved' ? '<button data-lens aria-pressed="false">Change lens</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><pre><code>${escape(currentContent())}</code></pre></section>`;
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>`;
}
function renderDetail() {
const entry = state.selected; const available = packageFiles(entry);
$('#detail').innerHTML = `<header><div><span class="status">${escape(entry.status)}</span><h2>${escape(entry.title)}</h2><p>Submitted by <a class="author-link" href="?author=${encodeURIComponent(entry.author)}">${escape(entry.author)}</a> · <a class="share-link" href="?author=${encodeURIComponent(entry.author)}&skill=${encodeURIComponent(entry.id)}&view=${state.preview}">share review ↗</a></p></div><div class="switch" role="group" aria-label="Preview version"><button class="${state.preview === 'original' ? 'active' : ''}" data-preview="original">Original</button><button class="${state.preview === 'improved' ? 'active' : ''}" data-preview="improved">Improved draft</button></div></header><div class="purpose"><span>THE JOB</span><p>${escape(entry.focus)}</p></div><div class="review-grid"><section><span>WHAT'S ALREADY WORKING</span><ul>${entry.wins.map((item) => `<li>${escape(item)}</li>`).join('')}</ul></section><section><span>HIGHEST-VALUE IMPROVEMENTS</span><ul>${entry.improve.map((item) => `<li>${escape(item)}</li>`).join('')}</ul></section></div><aside class="extras"><span>GOOD NEXT ADDITION</span><p>${escape(entry.extras)}</p></aside>${previewMarkup(entry, available)}`;
$('#detail').querySelectorAll('[data-file]').forEach((button) => button.addEventListener('click', () => { state.file = available.find((item) => item.name === button.dataset.file) || available[0]; syncUrl(); renderDetail(); loadSelectedFile(); }));
$('#detail').querySelectorAll('[data-preview]').forEach((button) => button.addEventListener('click', () => { state.preview = button.dataset.preview; state.lens = false; syncUrl(); renderDetail(); loadSelectedFile(); }));
$('#detail').querySelectorAll('[data-lens]').forEach((button) => button.addEventListener('click', () => { state.lens = !state.lens; syncUrl(); renderDetail(); }));
$('#detail').querySelectorAll('[data-file]').forEach((button) => button.addEventListener('click', () => { state.file = available.find((item) => item.name === button.dataset.file) || available[0]; state.rendered = false; state.diff = false; syncUrl(); renderDetail(); loadSelectedFile(); }));
$('#detail').querySelectorAll('[data-preview]').forEach((button) => button.addEventListener('click', () => { state.preview = button.dataset.preview; state.lens = false; state.rendered = false; state.diff = false; syncUrl(); renderDetail(); loadSelectedFile(); }));
$('#detail').querySelectorAll('[data-lens]').forEach((button) => button.addEventListener('click', () => { state.lens = !state.lens; state.rendered = false; state.diff = false; syncUrl(); renderDetail(); }));
$('#detail').querySelectorAll('[data-diff]').forEach((button) => button.addEventListener('click', async () => { await loadSelectedFile(); state.diff = !state.diff; state.rendered = false; state.lens = false; syncUrl(); renderDetail(); }));
$('[data-render]')?.addEventListener('click', async () => { await loadSelectedFile(); state.rendered = !state.rendered; syncUrl(); renderDetail(); });
$('[data-copy]')?.addEventListener('click', async () => { await loadSelectedFile(); await copy(currentContent()); $('[data-copy]').textContent = 'Copied'; });
$('[data-download]')?.addEventListener('click', async () => { await loadSelectedFile(); download(`${entry.id}-${state.file.name.replaceAll('/', '-')}-${state.preview}.md`, currentContent()); });
}
$('#skill-filter').addEventListener('input', (event) => { state.query = event.target.value.toLowerCase().trim(); renderList(); });
$('#skill-filter').addEventListener('input', (event) => { state.query = event.target.value.toLowerCase().trim(); renderList(); schedulePackageSearch(); });
window.addEventListener('popstate', () => { selectFromUrl(); renderList(); renderDetail(); loadSelectedFile(); });
selectFromUrl(); renderList(); renderDetail(); loadSelectedFile();
+3
View File
@@ -1 +1,4 @@
.change-lens{border:1px solid var(--ink);background:#123042;color:var(--paper);animation:lens-enter .28s ease both}.change-lens>header{display:flex;justify-content:space-between;gap:20px;align-items:start;padding:22px 24px;border-bottom:1px solid #466274}.change-lens span{color:var(--gold);font:700 10px ui-monospace,monospace;letter-spacing:.1em}.change-lens h3{margin:7px 0 0;font-size:clamp(24px,3vw,40px);line-height:1.02;letter-spacing:-.05em}.change-lens>header button{padding:9px 11px;border:1px solid #557080;color:var(--paper);background:transparent;cursor:pointer;font:700 10px ui-monospace,monospace}.change-lens>header button:hover{color:var(--ink);background:var(--gold)}.change-lens>p{max-width:67ch;margin:0;padding:19px 24px;color:#c6d2d7}.change-rows{display:grid;gap:1px;background:#466274}.change-rows article{display:grid;grid-template-columns:120px minmax(0,1fr) minmax(0,1fr) minmax(220px,.85fr);gap:1px;background:#466274}.change-rows article>*{min-width:0;margin:0;padding:17px;background:#173b4f}.change-rows article>span{color:var(--gold);font:700 10px/1.4 ui-monospace,monospace}.change-rows b{font:700 10px ui-monospace,monospace;letter-spacing:.07em;text-transform:uppercase}.change-rows div:first-of-type b{color:#e89a8e}.change-rows div:nth-of-type(2) b{color:#9bcba7}.change-rows aside{background:#1d455b}.change-rows aside b{color:var(--gold)}.change-rows p{margin:7px 0 0;color:#d4dfe3;font-size:12px;line-height:1.55}.preview header [data-lens]{color:var(--gold);border-color:var(--gold)}@keyframes lens-enter{from{opacity:.15;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@media(max-width:1000px){.change-rows article{grid-template-columns:100px 1fr 1fr}.change-rows aside{grid-column:2/-1}}@media(max-width:620px){.change-lens>header{display:block}.change-lens>header button{margin-top:14px}.change-rows article{grid-template-columns:1fr}.change-rows article>span{padding-bottom:6px}.change-rows aside{grid-column:auto}.change-lens>p{padding:17px}.change-lens>header{padding:18px}.change-rows p{font-size:13px}}@media(prefers-reduced-motion:reduce){.change-lens{animation:none}}
/* A compact, scrollable comparison keeps long skill revisions reviewable. */
.skill-diff{border:1px solid var(--ink);background:#102b3a;color:var(--paper);animation:lens-enter .28s ease both}.skill-diff>header{display:flex;justify-content:space-between;gap:20px;align-items:start;padding:22px 24px;border-bottom:1px solid #466274}.skill-diff span{color:var(--gold);font:700 10px ui-monospace,monospace;letter-spacing:.1em}.skill-diff h3{margin:7px 0 0;font-size:clamp(24px,3vw,40px);line-height:1.02;letter-spacing:-.05em}.skill-diff>header button{padding:9px 11px;border:1px solid #557080;color:var(--paper);background:transparent;cursor:pointer;font:700 10px ui-monospace,monospace}.skill-diff>header button:hover{color:var(--ink);background:var(--gold)}.skill-diff>p{margin:0;padding:17px 24px;color:#c6d2d7}.diff-lines{max-height:540px;overflow:auto;border-top:1px solid #466274;font:12px/1.55 ui-monospace,monospace}.diff-lines p{display:grid;grid-template-columns:42px minmax(0,1fr);gap:11px;margin:0;padding:4px 16px;white-space:pre-wrap;overflow-wrap:anywhere}.diff-lines span{color:#91aab7}.diff-lines .added{color:#d5f1d6;background:#1a4b42}.diff-lines .added span{color:#a9e3ae}.diff-lines .removed{color:#ffd7d0;background:#572f32}.diff-lines .removed span{color:#ffb5a8}.preview header [data-diff]{color:#c6d2d7;border-color:#557080}@media(max-width:620px){.skill-diff>header{display:block}.skill-diff>header button{margin-top:14px}.diff-lines p{grid-template-columns:30px minmax(0,1fr);padding:4px 12px}}@media(prefers-reduced-motion:reduce){.skill-diff{animation:none}}
+1 -1
View File
@@ -10,7 +10,7 @@
</head>
<body>
<main>
<header class="topbar"><a href="../" class="back">← field guide</a><span>SUBMITTED SKILLS / REVIEW DESK</span><a href="#catalog">16 submissions</a></header>
<header class="topbar"><a href="../" class="back">← field guide</a><span>SUBMITTED SKILLS / REVIEW DESK</span><a href="#catalog" id="submission-count">submissions</a></header>
<section class="hero">
<p class="eyebrow">A friendly path from draft to dependable</p>
<h1>Every skill deserves<br><em>a clear job.</em></h1>
File diff suppressed because one or more lines are too long