Files
ai-for-dummies/skills-review/app.js
T
Marcos Paulo 47fff7d289 revert(motion): drop the legacy-file edits, keep the Astro ones
The motion pass edited five legacy files. Two changes were behaviour
regressions on the live site, and all five were out of scope: the brief
asks for an inventory of `src/`, and the legacy pages are what visitors
see until task 20 cuts over.

The confirmed break: `styles.css` changed the reading-progress bar to
`width:100%; transform:scaleX(0)`, but `app.js:406` still sets
`style.width = "N%"`. The inline width applies, the scaleX(0) is never
updated, and the bar renders at 0px on every legacy page. Verified in a
headless browser at 1100px, scrolled halfway: computed transform
`matrix(0, 0, 0, 1, 0, 0)`, bounding width `0`. The Astro island was
updated to set `transform` and is correct -- its half-scrolled bar
measures 595px -- so only the legacy half was broken.

The route-meter change (`height:var(--score)` to
`transform:scaleY(var(--score))` in `responsive.css`) does work:
`--score` is a percentage and Chromium accepts a percentage in
`scaleY()`. It is reverted anyway, because it belongs in the component.
`responsive.css` is being retired by task 15e, which is porting these
rules into the components that need them; that is where the meter should
stop animating a layout property. Noted in the 15e brief.

The three review-desk animations added to `skills-review/` are not
broken, but they change live-site behaviour ahead of the cutover and the
same motion already exists on the Astro side. Reverted for the same
reason.

Kept: every `src/` change -- the ReadingProgress transform, the guide
panel swap, the change-lens and package-preview easing, and the vote
widget tally pop.
2026-09-05 23:31:18 +00:00

546 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { catalog as legacyCatalog } from './catalog.js';
import { files } from './files.js';
import { renderVoteWidget } from './vote.js';
// The Astro review-desk route serializes the typed content collection before
// this client module loads. Keeping the legacy fallback preserves the vanilla
// page until cutover removes it.
const catalog = window.__SKILLS_REVIEW_CATALOG || legacyCatalog;
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) => {
const safe = value
.replace(/(NDO_PASS[^\n=]*[=:]\s*["']?)[^\n"']+/gi, '$1[REDACTED]')
.replace(/(password["']?\s*[:=]\s*["']?)[^\n"']+/gi, '$1[REDACTED]')
.replace(/\b[\w.+-]+@[\w.-]+\.[a-z]{2,}\b/gi, '[REDACTED SERVICE ACCOUNT]')
.replace(/\/home\/[A-Za-z0-9._-]+(?=\/)/g, '[REDACTED LOCAL USER]')
.replace(/display\/~[A-Za-z0-9._-]+/gi, 'display/~[REDACTED USER]');
return entry.id === 'ndo-repro'
? safe
.replace(/https?:\/\/[^\s)>]+/gi, '[REDACTED URL]')
.replace(/\b(?:[\w-]+\.)*netcracker\.[\w.-]+\b/gi, '[REDACTED HOST]')
.replace(/\bpedro[._ -]?aranha\b/gi, '[REDACTED CONTRIBUTOR]')
: safe;
};
const download = (name, content) => {
const url = URL.createObjectURL(new Blob([content], { type: 'text/markdown' }));
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
};
const copy = async (content) => {
if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(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(' ');
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));
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');
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;
}
}
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;
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+(.+)$/);
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('');
}
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.',
after:
index === 0
? 'The improved draft makes the operating rule explicit.'
: 'The improved draft moves the decision into a smaller, reviewable contract.',
why,
}));
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);
url.searchParams.set('skill', state.selected.id);
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() {
const params = new URLSearchParams(window.location.search);
const author = params.get('author');
const id = params.get('skill');
const view = params.get('view');
const byAuthor =
author && catalog.filter((item) => item.author.toLowerCase() === author.toLowerCase());
const byId = id && catalog.find((item) => item.id === id);
state.selected = byId || byAuthor?.[0] || catalog[0];
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 = 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><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);
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';
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);
$('#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 id="vote-widget"></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)}`;
renderVoteWidget($('#vote-widget'), entry.id);
$('#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();
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();