feat: collapse 2018 image under spoiler; segmented "Tất cả ngôn ngữ" toggle

- wrap original 2018 image in <details class="original-image"> spoiler again
- view-toggle becomes a 2-state segmented control (TIOBE Top 20 / Tất cả ngôn ngữ)
  matching the source-toggle pattern, with arrow-key roving tabindex
- label both controls ("Nguồn màu" / "Hiển thị") for discoverability
This commit is contained in:
2026-04-27 20:33:17 +07:00
parent e868002d50
commit c330a91f52
3 changed files with 104 additions and 51 deletions
+12 -6
View File
@@ -41,17 +41,23 @@
<section class="elements">
<h2>Ngũ Hành &amp; ngôn ngữ <small class="mode-tag" id="source-tag">Nguồn: GitHub Linguist</small></h2>
<div class="modern-controls">
<div id="source-toggle"></div>
<div id="view-toggle"></div>
<div class="control-group">
<span class="control-label">Nguồn màu</span>
<div id="source-toggle"></div>
</div>
<div class="control-group">
<span class="control-label">Hiển thị</span>
<div id="view-toggle"></div>
</div>
</div>
<div class="grid" id="element-grid"></div>
<p class="legend"></p>
<details id="debug-panel" hidden></details>
<p class="disclaimer">* Mặc định hiển thị TIOBE Top 20 (April 2026). Bấm "Xem tất cả ngôn ngữ" để mở rộng. Có thể đổi nguồn màu giữa GitHub Linguist và GitLab Linguist (palette khác hẳn).</p>
<p class="disclaimer">* Mặc định hiển thị TIOBE Top 20 (April 2026). Chuyển sang "Tất cả ngôn ngữ" để xem toàn bộ Linguist. Có thể đổi nguồn màu giữa GitHub Linguist và GitLab Linguist (palette khác hẳn).</p>
</section>
<section class="figure" aria-labelledby="classic-image-heading">
<h2 id="classic-image-heading" class="visually-hidden">Ảnh gốc 2018</h2>
<details class="original-image">
<summary>Ảnh gốc (2018)</summary>
<figure>
<img
src="assets/ngon-ngu-lap-trinh-phong-thuy.png"
@@ -62,7 +68,7 @@
<a href="https://toidicodedao.com/2018/01/02/top-5-ngon-ngu-lap-trinh-dang-hoc-cho-nam-2018/" target="_blank" rel="noopener noreferrer">Tôi đi code dạo</a>.
</figcaption>
</figure>
</section>
</details>
<footer class="credit">
<h2>Credit</h2>
+47 -15
View File
@@ -103,22 +103,54 @@ export function mountSourceToggle(mountEl, sources, defaultKey, onChange) {
export function mountViewToggle(mountEl, scopeEl) {
if (!mountEl || !scopeEl) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'view-toggle-btn';
const labels = {
tiobe: 'Xem tất cả ngôn ngữ',
all: 'Chỉ TIOBE Top 20',
};
function apply(view) {
scopeEl.classList.toggle('show-all', view === 'all');
btn.textContent = labels[view];
btn.dataset.view = view;
btn.setAttribute('aria-pressed', String(view === 'all'));
const views = [
{ key: 'tiobe', label: 'TIOBE Top 20' },
{ key: 'all', label: 'Tất cả ngôn ngữ' },
];
const DEFAULT_VIEW = 'tiobe';
const tablist = document.createElement('div');
tablist.className = 'view-toggle';
tablist.setAttribute('role', 'tablist');
tablist.setAttribute('aria-label', 'Phạm vi hiển thị ngôn ngữ');
const buttons = views.map(({ key, label }) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.role = 'tab';
btn.className = 'view-toggle-btn';
btn.dataset.view = key;
btn.textContent = label;
btn.setAttribute('aria-selected', String(key === DEFAULT_VIEW));
btn.tabIndex = key === DEFAULT_VIEW ? 0 : -1;
btn.addEventListener('click', () => activate(key));
tablist.appendChild(btn);
return btn;
});
tablist.addEventListener('keydown', (e) => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
e.preventDefault();
const i = buttons.indexOf(document.activeElement);
let next;
if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = buttons.length - 1;
else if (e.key === 'ArrowLeft') next = (i <= 0 ? buttons.length : i) - 1;
else next = (i + 1) % buttons.length;
buttons[next].focus();
activate(buttons[next].dataset.view);
});
function activate(key) {
for (const b of buttons) {
const active = b.dataset.view === key;
b.setAttribute('aria-selected', String(active));
b.tabIndex = active ? 0 : -1;
}
scopeEl.classList.toggle('show-all', key === 'all');
}
btn.addEventListener('click', () => apply(btn.dataset.view === 'all' ? 'tiobe' : 'all'));
apply('tiobe');
mountEl.replaceChildren(btn);
mountEl.replaceChildren(tablist);
}
export function renderError(message, mountEl) {
+45 -30
View File
@@ -90,24 +90,41 @@ body {
.hero-ribbon .r-hoa { background: var(--hoa); }
.hero-ribbon .r-tho { background: var(--tho); }
/* ---------- Original figure ---------- */
.figure { display: flex; justify-content: center; margin: 2rem 0 0; }
.figure figure {
margin: 0; background: var(--card-bg); padding: 0.85rem;
border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow); max-width: 100%;
/* ---------- Original-image disclosure ---------- */
.original-image {
margin: 2rem 0 0; background: var(--card-bg);
border: 1px solid var(--border); padding: 0.85rem 1rem;
border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.figure img {
.original-image > summary {
cursor: pointer; list-style: none; font-weight: 500;
display: inline-flex; align-items: center; gap: 0.4rem;
color: var(--accent-ink);
}
.original-image > summary::-webkit-details-marker { display: none; }
.original-image > summary::before {
content: "▸"; display: inline-block;
transition: transform 0.15s ease; color: var(--gold);
}
.original-image[open] > summary::before { transform: rotate(90deg); }
.original-image > summary:focus-visible {
outline: none; box-shadow: var(--focus-ring); border-radius: 4px;
}
.original-image figure {
margin: 0.75rem 0 0; padding: 0.85rem;
background: var(--bg-tint); border-radius: var(--radius-sm);
}
.original-image img {
display: block; max-width: 100%; height: auto; border-radius: var(--radius-sm);
}
.figure figcaption {
.original-image figcaption {
text-align: center; margin-top: 0.6rem; color: var(--muted); font-size: 0.9rem;
}
.figure figcaption a {
.original-image figcaption a {
color: var(--accent-ink); text-decoration: none;
border-bottom: 1px dotted var(--accent);
}
.figure figcaption a:hover { color: var(--gold); border-bottom-color: var(--gold); }
.original-image figcaption a:hover { color: var(--gold); border-bottom-color: var(--gold); }
/* ---------- Section headings ---------- */
.elements h2, .credit h2 {
@@ -168,15 +185,6 @@ body {
.credit a:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: 3px; }
.credit .note { font-size: 0.85rem; color: var(--muted); font-style: italic; }
/* ---------- Visually hidden (a11y) ---------- */
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
margin: -1px; padding: 0;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
/* ---------- Debug panel disclosure ---------- */
#debug-panel > summary {
cursor: pointer; list-style: none; font-weight: 500;
@@ -219,8 +227,14 @@ body {
.elements.show-all .chip-other { display: inline-flex; }
.modern-controls {
display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
gap: 0.75rem; margin: 0 0 1rem;
display: flex; flex-wrap: wrap; justify-content: center; align-items: end;
gap: 1rem 1.25rem; margin: 0 0 1rem;
}
.control-group {
display: inline-flex; flex-direction: column; align-items: center; gap: 0.3rem;
}
.control-label {
font-size: 0.78rem; color: var(--muted); letter-spacing: 0.3px; text-transform: uppercase;
}
.modern-controls #view-toggle,
.modern-controls #source-toggle { display: flex; margin: 0; }
@@ -239,17 +253,18 @@ body {
.source-toggle-btn:hover:not([aria-selected="true"]) { background: var(--bg-tint); }
.source-toggle-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.view-toggle {
display: inline-flex; border: 1px solid var(--border-strong);
border-radius: 999px; padding: 2px; background: var(--card-bg);
}
.view-toggle-btn {
font: inherit; font-size: 0.85rem;
padding: 0.4rem 0.95rem; border-radius: 999px;
border: 1px solid var(--border-strong);
background: var(--card-bg); color: var(--fg); cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.view-toggle-btn:hover { border-color: var(--accent); }
.view-toggle-btn[aria-pressed="true"] {
background: var(--accent); color: #fff; border-color: var(--accent);
font: inherit; font-size: 0.85rem; padding: 0.3rem 0.85rem;
border-radius: 999px; border: none; background: transparent;
color: var(--fg); cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.view-toggle-btn[aria-selected="true"] { background: var(--accent); color: #fff; }
.view-toggle-btn:hover:not([aria-selected="true"]) { background: var(--bg-tint); }
.view-toggle-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.legend {