feat(ui): phase-02 url state, gitlab tooltip, hide empty kim card

- Persist source/view/sort toggles via URL query params (?s=…&v=…&o=…).
  Short keys keep shared URLs readable; defaults are stripped to keep
  the bare URL clean. history.replaceState (no history pollution).
  Init seeds toggles from URL with clamp-to-default validation.
- View class is applied BEFORE first render so renderGrid sees the
  correct show-all state on direct deep-link loads.
- Add #source-note pill next to the source tag; shows
  "91 ngôn ngữ — palette riêng so với GitHub" when GitLab selected,
  hidden otherwise. Plus title attr on the source-toggle group with
  the full GitHub-vs-GitLab disparity.
- In TIOBE view, hide cards with zero TIOBE-ranked langs (currently
  KIM has 0 of 19) — all-langs view still shows every Ngũ Hành card
  so the wheel stays intact.
- source-tag firstChild text-node overwrite preserves the source-note
  span sibling.

Closes phase-02 of plan 260428-1003-implement-all-remaining (items
2, 3, 4).
This commit is contained in:
2026-04-28 11:45:44 +07:00
parent 51d2b0515f
commit b8502fd3fc
5 changed files with 84 additions and 11 deletions
+1 -1
View File
@@ -40,7 +40,7 @@
<section class="elements">
<h2>Ngũ Hành &amp; ngôn ngữ</h2>
<p class="mode-tag" id="source-tag">Nguồn: GitHub Linguist</p>
<p class="mode-tag" id="source-tag">Nguồn: GitHub Linguist <span class="source-note" id="source-note" hidden></span></p>
<div class="modern-controls">
<div class="control-group">
<span class="control-label">Nguồn màu</span>
+65 -9
View File
@@ -28,12 +28,34 @@ const SORT_OPTIONS = [
{ key: 'hue', label: 'Cầu vồng' },
];
// Short keys keep the URL readable when shared.
const QUERY_KEYS = { source: 's', view: 'v', sort: 'o' };
function readQueryState() {
const params = new URLSearchParams(window.location.search);
return {
source: params.get(QUERY_KEYS.source) || DEFAULT_SOURCE,
view: params.get(QUERY_KEYS.view) || 'tiobe',
sort: params.get(QUERY_KEYS.sort) || 'tiobe',
};
}
function writeQueryParam(key, value, defaultValue) {
const params = new URLSearchParams(window.location.search);
if (value === defaultValue) params.delete(QUERY_KEYS[key]);
else params.set(QUERY_KEYS[key], value);
const qs = params.toString();
const next = qs ? `?${qs}${window.location.hash}` : window.location.pathname + window.location.hash;
history.replaceState(null, '', next);
}
const refs = {
grid: null,
legend: null,
section: null,
debug: null,
sourceTag: null,
sourceNote: null,
};
let lastBuckets = null;
@@ -98,7 +120,22 @@ async function loadAndRender(sourceKey) {
lastBuckets = buckets;
applySortAndRender();
if (refs.legend) refs.legend.textContent = LEGEND_TEXT;
if (refs.sourceTag) refs.sourceTag.textContent = `Nguồn: ${source.label} Linguist`;
if (refs.sourceTag) {
// First child is the text node; preserve the `#source-note` span sibling.
const first = refs.sourceTag.firstChild;
const tagText = `Nguồn: ${source.label} Linguist `;
if (first && first.nodeType === Node.TEXT_NODE) first.nodeValue = tagText;
else refs.sourceTag.prepend(document.createTextNode(tagText));
}
if (refs.sourceNote) {
if (sourceKey === 'gitlab') {
refs.sourceNote.hidden = false;
refs.sourceNote.textContent = '91 ngôn ngữ — palette riêng so với GitHub';
} else {
refs.sourceNote.hidden = true;
refs.sourceNote.textContent = '';
}
}
renderDebugPanel({ skipped, borderline }, refs.debug);
} catch (err) {
console.error('[programming-fengshui] failed to render modern grid:', err);
@@ -116,29 +153,48 @@ function init() {
refs.section = section;
refs.debug = document.getElementById('debug-panel');
refs.sourceTag = document.getElementById('source-tag');
refs.sourceNote = document.getElementById('source-note');
// Seed toggle defaults from URL query params; clamp unknown values.
const initial = readQueryState();
const validSource = SOURCES[initial.source] ? initial.source : DEFAULT_SOURCE;
const validView = VIEW_OPTIONS.some((o) => o.key === initial.view) ? initial.view : 'tiobe';
const validSort = SORT_OPTIONS.some((o) => o.key === initial.sort) ? initial.sort : 'tiobe';
currentSort = validSort;
// Apply view class BEFORE first render so renderGrid sees the right state
// (drives the empty-card hide logic in render-elements.js).
if (validView === 'all') section?.classList.add('show-all');
const sourceToggleEl = document.getElementById('source-toggle');
sourceToggleEl?.setAttribute('title', 'GitHub có 664 ngôn ngữ; GitLab có 91 và dùng palette riêng');
mountSegmentedControl(
document.getElementById('source-toggle'),
sourceToggleEl,
Object.entries(SOURCES).map(([key, s]) => ({ key, label: s.label })),
DEFAULT_SOURCE,
loadAndRender,
validSource,
(key) => { writeQueryParam('source', key, DEFAULT_SOURCE); loadAndRender(key); },
'Nguồn dữ liệu màu',
);
mountSegmentedControl(
document.getElementById('view-toggle'),
VIEW_OPTIONS,
'tiobe',
(key) => section?.classList.toggle('show-all', key === 'all'),
validView,
(key) => {
writeQueryParam('view', key, 'tiobe');
section?.classList.toggle('show-all', key === 'all');
applySortAndRender();
},
'Phạm vi hiển thị ngôn ngữ',
);
mountSegmentedControl(
document.getElementById('sort-toggle'),
SORT_OPTIONS,
'tiobe',
(key) => { currentSort = key; applySortAndRender(); },
validSort,
(key) => { writeQueryParam('sort', key, 'tiobe'); currentSort = key; applySortAndRender(); },
'Sắp xếp ngôn ngữ',
);
loadAndRender(DEFAULT_SOURCE);
loadAndRender(validSource);
}
if (document.readyState === 'loading') {
+5
View File
@@ -39,10 +39,15 @@ function buildChip(name, color, { rank = null } = {}) {
export function renderGrid(buckets, mountEl) {
if (!mountEl) return;
// In TIOBE view, skip cards with zero TIOBE-ranked langs — empty
// "0 ngôn ngữ" headings look broken. All-langs view always shows
// every element so the Ngũ Hành wheel stays intact.
const showAll = mountEl.closest('.elements')?.classList.contains('show-all') ?? false;
const fragment = document.createDocumentFragment();
for (const { key, label } of ELEMENTS) {
const langs = buckets[key] || [];
const tiobeCount = langs.filter((l) => l.rank).length;
if (!showAll && tiobeCount === 0) continue;
const card = document.createElement('article');
card.className = `card ${key}`;
const h3 = document.createElement('h3');
@@ -16,7 +16,7 @@ Single pass through every remaining item in `plans/todo.md` and the post-cleanup
| # | Title | Status | Items |
|---|-------|--------|-------|
| 01 | Style & markup polish | **done** | items 510, 13, 14, 15 (CSS one-liners + chip role + decision docs) |
| 02 | Functional toggles & tooltip | pending | items 2, 3, 4 (URL persistence, GitLab tooltip, hide empty KIM in TIOBE view) |
| 02 | Functional toggles & tooltip | **done** | items 2, 3, 4 (URL persistence, GitLab tooltip, hide empty KIM in TIOBE view) |
| 03 | Additive features | pending | items 11, 12 (Top-5 non-TIOBE peek, lunar SVG body texture) |
| 04 | OG social card image | pending | item 1 (generate `assets/og-card.png`, may block on tooling) |
+12
View File
@@ -138,6 +138,18 @@ body {
text-align: center; font-size: 0.85rem; font-style: italic;
color: var(--muted); font-weight: 400; margin: -0.5rem 0 1rem;
}
.source-note {
display: inline-block;
margin-left: 0.4rem;
padding: 0.05rem 0.5rem;
border-radius: 999px;
background: var(--bg-tint);
color: var(--accent-ink);
font-size: 0.7rem;
font-style: normal;
font-weight: 600;
letter-spacing: 0.2px;
}
/* ---------- Card grid ---------- */
/* Grid stays auto-fit (not forced 5-col) — the Ngũ-Hành wheel reads at any