Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e1be055
feat: add UX gap analysis reports and DevExperience site
MrCoder May 10, 2026
75a639a
fix(ux): rename Settings modal title from 'Editor' to 'Settings' with…
MrCoder May 10, 2026
8ab73ff
fix(ux): move 'Preserve console logs' to collapsible Advanced/Develop…
MrCoder May 10, 2026
2a7ce09
fix(ux): replace 'was forked' jargon toast with plain language; renam…
MrCoder May 10, 2026
9f0d12f
fix(ux): update default example comment to reference left sidebar boo…
MrCoder May 10, 2026
66e6e4c
fix(ux): replace 'Emmet code completion' jargon with 'Auto-complete s…
MrCoder May 10, 2026
d3b80b9
fix(ux): add Undo/Redo shortcuts (Cmd+Z / Cmd+Shift+Z) to keyboard sh…
MrCoder May 10, 2026
51af2e6
fix(ux): add title and aria-label to all 8 toolbar buttons for hover …
MrCoder May 10, 2026
130f9ec
fix(ux): add visual group separators to toolbar — Participants / Mess…
MrCoder May 10, 2026
9149931
fix(ux): remove marketing tweet solicitation from creation modal — wr…
MrCoder May 10, 2026
523f9b9
fix(ux): replace ambiguous timestamp title 'Untitled 9-5-23:24' with …
MrCoder May 10, 2026
c2c0444
fix(ux): improve Library empty state with actionable Cmd+S hint; add …
MrCoder May 10, 2026
65e77a9
fix(ux): add aria-label to all sidebar icon buttons; aria-hidden on i…
MrCoder May 10, 2026
632ee10
fix(ux): fix typo 'Asyc message'; make cheat sheet scrollable; add do…
MrCoder May 10, 2026
125fe38
fix(ux): truncate long titles with ellipsis from start; add full-titl…
MrCoder May 10, 2026
cc801d1
fix(ux): unify Present button title/aria-label; add auth hint to PNG …
MrCoder May 10, 2026
1cf5514
fix(ux): show lock icon on PNG export buttons for unauthenticated use…
MrCoder May 10, 2026
cad9fa5
fix(ux): add contextual reason to login modal explaining why auth is …
MrCoder May 10, 2026
151aee5
fix(ux): 'Start from scratch' now creates truly blank diagram; rename…
MrCoder May 10, 2026
e74d50d
fix(ux): add 'Reset to defaults' button to Settings modal [GAP-08-004]
MrCoder May 10, 2026
46e26a0
fix(ux): add placeholder text to ZenUML code editor when empty [GAP-0…
MrCoder May 10, 2026
a97a6b8
fix(ux): add WCAG 2.4.11-compliant focus-visible ring (2px blue) for …
MrCoder May 10, 2026
c09fe66
fix(ux): darken primary button color from #6786f7 to #4355CC for WCAG…
MrCoder May 10, 2026
9c53765
fix(ux): add skip-to-editor link for WCAG 2.4.1 keyboard navigation; …
MrCoder May 10, 2026
a9e1144
fix: remove dead updateSetting (had typo bug) and unused RadioGroup i…
MrCoder May 10, 2026
54bfc8a
fix(ux): add Cmd+S saved toast and unsaved-changes dot indicator in h…
MrCoder May 10, 2026
28f4490
fix(ux): trap focus in login modal — focus first auth button on open …
MrCoder May 10, 2026
7696af4
fix(ux): add context-specific reasons to all login modal triggers [GA…
MrCoder May 10, 2026
1837b7c
fix(ux): close Library panel when switching to Code Editor tab [GAP-1…
MrCoder May 10, 2026
97bc2d1
fix(ux): make split-pane gutter visible with hover/active states and …
MrCoder May 10, 2026
8e97879
fix(ux): add double-click to rename page tabs inline [GAP-13-001] [GA…
MrCoder May 10, 2026
680d94b
fix(ux): guard window.saveBtn null to prevent TypeError on every Nth …
MrCoder May 10, 2026
ad1df76
fix(ux): make diagram title rename more discoverable — cursor-text, h…
MrCoder May 10, 2026
2c9a3f2
fix(ux): add Alt+E shortcut to focus code editor; document in shortcu…
MrCoder May 10, 2026
245dfda
fix(ux): show lock icon on CSS tab for unauthenticated users with too…
MrCoder May 10, 2026
892abb5
fix(ux): replace 'lost forever' language in delete confirmation with …
MrCoder May 10, 2026
8db6eb4
fix(ux): raise CodeMirror line-number contrast to WCAG AA (monokai 0.…
MrCoder May 10, 2026
ee19a19
fix(ux): keep code editor visible alongside library panel instead of …
MrCoder May 10, 2026
49b9f25
fix(ux): rewrite onboarding modal with actionable getting-started ste…
MrCoder May 10, 2026
916daf7
fix(ux): strengthen active tab indicator — border-b-2, font-bold, hov…
MrCoder May 10, 2026
a094d65
fix(ux): focus editor after toolbar button insert so typing continues…
MrCoder May 10, 2026
094008e
fix(ux): add visual divider between panel icons and utility icons in …
MrCoder May 10, 2026
7bad865
fix(ux): change default split to 45/55; add double-click gutter to re…
MrCoder May 10, 2026
4bc6b75
fix(ux): wire error gutter to iframe parse-error messages from zenuml…
MrCoder May 10, 2026
9ca41f5
fix(ux): add Alt+P shortcut for new page; document in keyboard shortc…
MrCoder May 10, 2026
0acb881
fix(ux): extend font size range from 10-24px (was 12-18px) for access…
MrCoder May 10, 2026
be52169
fix(ux): set default editor font size to 14px
MrCoder May 10, 2026
04981fb
chore: move dev server to port 23000
MrCoder May 10, 2026
50b3049
fix(ux): style iframe portal controls
MrCoder May 10, 2026
48c76e8
fix: pin pnpm for Node 20 CI
MrCoder May 10, 2026
f28a960
fix: update workflows for Node 24 actions
MrCoder May 10, 2026
68501fb
fix: emit zenuml core asset in production
MrCoder May 10, 2026
31bc2f0
chore: update zenuml core
MrCoder May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ jobs:
node-version: [20.x]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: latest
version: 10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:
node-version: [20.x]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: latest
version: 10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
Expand All @@ -39,7 +39,7 @@ jobs:
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-key.json
pnpm deploy:staging
- name: Upload artifacts # Find artifacts under actions/jobs
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: chrome-extension
path: extension
17 changes: 11 additions & 6 deletions .github/workflows/remove-old-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ jobs:

steps:
- name: Remove old artifacts
uses: c-hive/gha-remove-artifacts@v1
with:
age: '1 month' # '<number> <unit>', e.g. 5 days, 2 years, 90 seconds, parsed by Moment.js
# Optional inputs
# skip-tags: true
# skip-recent: 5
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
cutoff="$(date -u -d '1 month ago' +%Y-%m-%dT%H:%M:%SZ)"

gh api --paginate "repos/$REPO/actions/artifacts" \
--jq ".artifacts[] | select(.created_at < \"$cutoff\") | .id" |
while read -r artifact_id; do
gh api --method DELETE "repos/$REPO/actions/artifacts/$artifact_id"
done
1,278 changes: 1,278 additions & 0 deletions dev-experience/agent-prompts.json

Large diffs are not rendered by default.

224 changes: 224 additions & 0 deletions dev-experience/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
(function() {
'use strict';

const state = {
data: null,
filteredGaps: [],
filters: { severity: 'all', case: 'all', principle: 'all', search: '' },
};

const el = (id) => document.getElementById(id);

fetch('gaps.json', { cache: 'no-store' })
.then(r => r.json())
.then(init)
.catch(err => {
el('gap-list').innerHTML = `<div class="empty"><h3>Failed to load gaps.json</h3><p>${err.message}</p></div>`;
});

function init(data) {
state.data = data;

// Flatten gaps with case context
const flatGaps = [];
for (const c of data.cases) {
for (const g of c.gaps) {
flatGaps.push({
...g,
caseNumber: c.number,
caseTitle: c.title,
caseSlug: c.slug,
caseGif: c.gif,
});
}
}
state.allGaps = flatGaps;

// KPIs
el('kpi-total').textContent = flatGaps.length;
el('kpi-high').textContent = data.totals.high;
el('kpi-medium').textContent = data.totals.medium;
el('kpi-low').textContent = data.totals.low;
el('kpi-cases').textContent = data.cases.length;

const principleSet = new Set();
for (const g of flatGaps) for (const p of g.principles) principleSet.add(p);
const principles = [...principleSet].sort();
el('kpi-principles').textContent = principles.length;

el('footer-cases').textContent = data.cases.length;
el('footer-gaps').textContent = flatGaps.length;
el('generated-date').textContent = data.generated.slice(0, 10);

// Populate dropdowns
const caseSel = el('filter-case');
caseSel.innerHTML = '<option value="all">All cases</option>' +
data.cases.map(c => `<option value="${c.number}">Case ${String(c.number).padStart(2,'0')} — ${escapeHtml(c.title || '')}</option>`).join('');

const principleSel = el('filter-principle');
principleSel.innerHTML = '<option value="all">All principles</option>' +
principles.map(p => `<option value="${escapeAttr(p)}">${escapeHtml(p)}</option>`).join('');

// Wire up filters
document.querySelectorAll('#filter-severity .pill').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#filter-severity .pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.filters.severity = btn.dataset.value;
syncSeverityKpis();
render();
});
});

document.querySelectorAll('.kpi[data-sev]').forEach(kpi => {
kpi.addEventListener('click', () => {
const sev = kpi.dataset.sev;
const isActive = kpi.classList.contains('active-sev');
const target = isActive ? 'all' : sev;
document.querySelectorAll('#filter-severity .pill').forEach(b => {
b.classList.toggle('active', b.dataset.value === target);
});
state.filters.severity = target;
syncSeverityKpis();
render();
});
});

caseSel.addEventListener('change', () => {
state.filters.case = caseSel.value;
render();
});
principleSel.addEventListener('change', () => {
state.filters.principle = principleSel.value;
render();
});

el('search-input').addEventListener('input', debounce(e => {
state.filters.search = e.target.value.trim().toLowerCase();
render();
}, 120));

el('reset-filters').addEventListener('click', () => {
state.filters = { severity: 'all', case: 'all', principle: 'all', search: '' };
el('search-input').value = '';
caseSel.value = 'all';
principleSel.value = 'all';
document.querySelectorAll('#filter-severity .pill').forEach(b => {
b.classList.toggle('active', b.dataset.value === 'all');
});
syncSeverityKpis();
render();
});

// Modal close
const modal = el('gap-modal');
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.close();
});

render();
}

function syncSeverityKpis() {
document.querySelectorAll('.kpi[data-sev]').forEach(kpi => {
kpi.classList.toggle('active-sev', kpi.dataset.sev === state.filters.severity);
});
}

function render() {
const f = state.filters;
const filtered = state.allGaps.filter(g => {
if (f.severity !== 'all' && g.severity !== f.severity) return false;
if (f.case !== 'all' && String(g.caseNumber) !== f.case) return false;
if (f.principle !== 'all' && !g.principles.includes(f.principle)) return false;
if (f.search) {
const hay = (g.title + ' ' + g.paragraphs.join(' ') + ' ' + (g.fix || '') + ' ' + (g.evidence || '') + ' ' + g.principles.join(' ')).toLowerCase();
if (!hay.includes(f.search)) return false;
}
return true;
});

state.filteredGaps = filtered;

// Sort: high first, then medium, low; within severity, by case number
const sevOrder = { high: 0, medium: 1, low: 2 };
filtered.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity] || a.caseNumber - b.caseNumber);

el('result-count').textContent = `${filtered.length} of ${state.allGaps.length} gaps`;

const list = el('gap-list');
if (filtered.length === 0) {
list.innerHTML = `<div class="empty"><h3>No gaps match these filters</h3><p>Try removing a filter or clearing the search.</p></div>`;
return;
}

list.innerHTML = filtered.map((g, i) => renderCard(g, i)).join('');
list.querySelectorAll('.gap').forEach(card => {
card.addEventListener('click', () => {
const idx = parseInt(card.dataset.idx, 10);
openModal(filtered[idx]);
});
});
}

function renderCard(g, idx) {
const snippet = (g.paragraphs && g.paragraphs[0]) ? g.paragraphs[0] : '';
const principles = (g.principles || []).slice(0, 3).map(p => `<span class="principle-chip">${escapeHtml(p)}</span>`).join('');
const thumb = g.id ? `<img class="gap-thumb" src="screenshots/${encodeURIComponent(g.id)}.png" alt="" loading="lazy" onerror="this.style.display='none'">` : '';
return `
<article class="gap gap-${g.severity}" data-idx="${idx}">
${thumb}
<div class="gap-top">
<span class="gap-id">${escapeHtml(g.id || '—')}</span>
<span class="sev-badge sev-${g.severity}">${g.severity}</span>
<span class="case-tag">Case ${String(g.caseNumber).padStart(2,'0')}</span>
</div>
<h3 class="gap-title">${escapeHtml(g.title || 'Untitled')}</h3>
<p class="gap-snippet">${escapeHtml(snippet)}</p>
<div class="gap-principles">${principles}</div>
</article>
`;
}

function openModal(g) {
const paragraphs = (g.paragraphs || []).map(p => `<p>${escapeHtml(p)}</p>`).join('');
const evidence = g.evidence ? `<div class="evidence">${escapeHtml(g.evidence)}</div>` : '';
const fix = g.fix ? `<div class="fix-block"><strong>Suggested fix</strong>${escapeHtml(g.fix)}</div>` : '';
const principles = (g.principles || []).map(p => `<span class="principle-chip">${escapeHtml(p)}</span>`).join('');
const sourceLink = `<a class="source-link" href="../ux-gap-reports/${g.caseSlug}.html" target="_blank" rel="noopener">View Case ${String(g.caseNumber).padStart(2,'0')} (with screen recording) →</a>`;

const screenshot = g.id ? `<img class="modal-screenshot" src="screenshots/${encodeURIComponent(g.id)}.png" alt="Annotated screenshot for ${escapeAttr(g.id)}" onerror="this.style.display='none'">` : '';
const html = `
<button class="modal-close" id="modal-close-btn" aria-label="Close">×</button>
<h2>${escapeHtml(g.title || '')}</h2>
<div class="meta-row">
<span class="gap-id">${escapeHtml(g.id || '—')}</span>
<span class="sev-badge sev-${g.severity}">${g.severity}</span>
<span class="case-tag">Case ${String(g.caseNumber).padStart(2,'0')} — ${escapeHtml(g.caseTitle || '')}</span>
</div>
${screenshot}
${paragraphs}
${evidence}
${fix}
<div class="principles">${principles}</div>
${sourceLink}
`;

const modal = el('gap-modal');
el('modal-content').innerHTML = html;
document.getElementById('modal-close-btn').addEventListener('click', () => modal.close());
if (typeof modal.showModal === 'function') {
modal.showModal();
} else {
modal.setAttribute('open', '');
}
}

function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
function escapeAttr(s) { return escapeHtml(s); }
function debounce(fn, ms) {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}
})();
Loading