feat: focus modern grid on TIOBE Top 20 with 'Xem tất cả' expand toggle

Default view shows only TIOBE-ranked languages (19 chips total) for a cleaner
first impression. A single button at the top of the modern panel toggles to
the full 664-language list. Card counts now read 'X TIOBE · Y khác'. Chips
of TIOBE-ranked languages get a slightly heavier border to stand out.

- js/tiobe-top.js: TIOBE Top 20 (April 2026) keyed by GH Linguist names
- js/main.js: tag entries with rank, sort TIOBE-first then alphabetical
- js/render-elements.js: chip distinction + view-toggle mount
- index.html: view-toggle slot + updated disclaimer
- style.css: .chip-tiobe / .chip-other / .view-toggle-btn rules
This commit is contained in:
2026-04-27 09:24:25 +07:00
parent 5efccffede
commit e1ac51c31f
5 changed files with 108 additions and 9 deletions
+2 -1
View File
@@ -67,10 +67,11 @@
<section id="panel-modern" role="tabpanel" aria-labelledby="tab-modern" hidden>
<section class="elements">
<h2>Ngũ Hành &amp; ngôn ngữ <small class="mode-tag">(tự phân loại theo màu GitHub)</small></h2>
<div id="view-toggle"></div>
<div class="grid" id="element-grid"></div>
<p class="legend"></p>
<details id="debug-panel" hidden></details>
<p class="disclaimer">* Phân loại tự động theo màu chính thức GitHub Linguist + quy tắc HSL.</p>
<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.</p>
</section>
<details class="original-image">
<summary>Ảnh gốc</summary>
+11 -3
View File
@@ -1,5 +1,6 @@
import { classify } from './classify-element.js';
import { renderGrid, renderError, renderDebugPanel, isBorderline } from './render-elements.js';
import { renderGrid, renderError, renderDebugPanel, isBorderline, mountViewToggle } from './render-elements.js';
import { TIOBE_TOP } from './tiobe-top.js';
const HEX_RE = /^#[0-9a-fA-F]{6}$/;
const LEGEND_TEXT =
@@ -27,15 +28,22 @@ async function init() {
continue;
}
const element = classify(color);
buckets[element].push({ name, color });
const rank = TIOBE_TOP[name] || null;
buckets[element].push({ name, color, rank });
if (isBorderline(color)) borderline.push({ name, color, element });
}
for (const key of Object.keys(buckets)) {
buckets[key].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
buckets[key].sort((a, b) => {
if (a.rank && b.rank) return a.rank - b.rank;
if (a.rank) return -1;
if (b.rank) return 1;
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
});
}
renderGrid(buckets, gridEl);
mountViewToggle(document.getElementById('view-toggle'), document.querySelector('#panel-modern .elements'));
if (legendEl) legendEl.textContent = LEGEND_TEXT;
renderDebugPanel({ skipped, borderline }, debugEl);
} catch (err) {
+29 -5
View File
@@ -7,15 +7,16 @@ function pickTextColor(hex) {
return (r * 299 + g * 587 + b * 114) / 1000 >= 128 ? 'black' : 'white';
}
function buildChip(name, color) {
function buildChip(name, color, { rank = null } = {}) {
const span = document.createElement('span');
span.className = 'chip';
span.className = 'chip' + (rank ? ' chip-tiobe' : ' chip-other');
span.textContent = name;
if (color) {
span.style.background = color;
span.style.color = pickTextColor(color);
span.title = color;
span.title = rank ? `${color} · TIOBE #${rank}` : color;
}
if (rank) span.dataset.rank = String(rank);
return span;
}
@@ -24,22 +25,45 @@ export function renderGrid(buckets, mountEl) {
const fragment = document.createDocumentFragment();
for (const { key, label } of ELEMENTS) {
const langs = buckets[key] || [];
const tiobeCount = langs.filter((l) => l.rank).length;
const card = document.createElement('article');
card.className = `card ${key}`;
const h3 = document.createElement('h3');
h3.textContent = label;
const count = document.createElement('small');
count.className = 'card-count';
count.textContent = `${langs.length} ngôn ngữ`;
count.textContent = tiobeCount
? `${tiobeCount} TIOBE · ${langs.length - tiobeCount} khác`
: `${langs.length} ngôn ngữ`;
const chips = document.createElement('div');
chips.className = 'chips';
for (const { name, color } of langs) chips.appendChild(buildChip(name, color));
for (const { name, color, rank } of langs) chips.appendChild(buildChip(name, color, { rank }));
card.append(h3, count, chips);
fragment.appendChild(card);
}
mountEl.replaceChildren(fragment);
}
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'));
}
btn.addEventListener('click', () => apply(btn.dataset.view === 'all' ? 'tiobe' : 'all'));
apply('tiobe');
mountEl.replaceChildren(btn);
}
export function renderError(message, mountEl) {
if (!mountEl) return;
const p = document.createElement('p');
+25
View File
@@ -0,0 +1,25 @@
// TIOBE Index Top 20, April 2026 snapshot.
// Keys are exact GitHub Linguist names. Scratch is omitted (not tracked by Linguist).
// Source: https://www.tiobe.com/tiobe-index/
export const TIOBE_TOP = {
'Python': 1,
'C': 2,
'C++': 3,
'Java': 4,
'C#': 5,
'JavaScript': 6,
'Visual Basic .NET': 7,
'SQL': 8,
'R': 9,
'Pascal': 10,
'Perl': 12,
'Fortran': 13,
'PHP': 14,
'Go': 15,
'Rust': 16,
'MATLAB': 17,
'Assembly': 18,
'Swift': 19,
'Ada': 20,
};
+41
View File
@@ -278,6 +278,47 @@ body {
color: var(--fg);
}
.chip-tiobe {
border-color: rgba(0, 0, 0, 0.35);
font-weight: 600;
}
.elements .chip-other { display: none; }
.elements.show-all .chip-other { display: inline-block; }
#view-toggle {
display: flex;
justify-content: center;
margin: 0 0 1rem;
}
.view-toggle-btn {
font: inherit;
font-size: 0.85rem;
padding: 0.35rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--muted);
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);
}
.view-toggle-btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.legend {
text-align: center;
font-style: italic;