Lựa chọn ngôn ngữ lập trình theo phong thuỷ
Ngũ Hành tương sinh — code cho hợp tuổi, hợp mệnh.
+
diff --git a/js/render-elements.js b/js/render-elements.js
index de3d6fe..0b6f04a 100644
--- a/js/render-elements.js
+++ b/js/render-elements.js
@@ -1,10 +1,18 @@
import { ELEMENTS, hexToHsl } from './classify-element.js';
+// Relative luminance per WCAG 2.x — better than YIQ for mid-tones
+// (e.g. saturated mid-greens / mid-reds where YIQ flips text color wrong).
+function relLuminance(hex) {
+ const v = (c) => {
+ const x = parseInt(hex.slice(c, c + 2), 16) / 255;
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+ };
+ return 0.2126 * v(1) + 0.7152 * v(3) + 0.0722 * v(5);
+}
+
function pickTextColor(hex) {
- const r = parseInt(hex.slice(1, 3), 16);
- const g = parseInt(hex.slice(3, 5), 16);
- const b = parseInt(hex.slice(5, 7), 16);
- return (r * 299 + g * 587 + b * 114) / 1000 >= 128 ? 'black' : 'white';
+ // Threshold tuned so saturated mid-tones (#d44950, #5d4037) get white text.
+ return relLuminance(hex) > 0.5 ? '#1a1a1a' : '#ffffff';
}
function buildChip(name, color, { rank = null } = {}) {
@@ -14,7 +22,10 @@ function buildChip(name, color, { rank = null } = {}) {
if (color) {
span.style.background = color;
span.style.color = pickTextColor(color);
- span.title = rank ? `${color} · TIOBE #${rank}` : color;
+ const hexLabel = color.toUpperCase();
+ const tooltip = rank ? `${name} · ${hexLabel} · TIOBE #${rank}` : `${name} · ${hexLabel}`;
+ span.title = tooltip;
+ span.setAttribute('aria-label', tooltip);
}
if (rank) span.dataset.rank = String(rank);
return span;
diff --git a/plans/reports/uiux-260427-0927-audit-improvements.md b/plans/reports/uiux-260427-0927-audit-improvements.md
new file mode 100644
index 0000000..b1ca28a
--- /dev/null
+++ b/plans/reports/uiux-260427-0927-audit-improvements.md
@@ -0,0 +1,102 @@
+# UI/UX Audit & Improvements — programming-fengshui
+
+Date: 2026-04-27
+Scope: `index.html`, `style.css`, `js/render-elements.js` (read all JS for context)
+Approach: single careful pass, ~10 high-impact fixes, no rewrites.
+
+---
+
+## (a) Issues found & triaged
+
+| # | Severity | Selector / Area | Symptom |
+|---|----------|-----------------|---------|
+| 1 | High | `.mode-toggle` | No `flex-wrap` → at 320px Vietnamese labels squeeze and risk horizontal scroll. |
+| 2 | High | `--muted #6b5a48` on `--bg #fdf6ec` | ~4.0:1 — fails WCAG AA on small italic (`.legend`, `.subtitle`, `.note`). |
+| 3 | High | `.chip` text color picker (`pickTextColor`) | YIQ ≥128 mis-flips on saturated mid-tones (e.g. `#d44950` got black text — unreadable). Also no SR label, hex only in `title` (invisible on touch). |
+| 4 | Med | `.card:hover { transform }` | Reduced-motion respected only for keyframe; transform transition still ran. |
+| 5 | Med | Hero | No visual anchor / brand cue. Page is a feng-shui joke but reads like a generic CRUD card list. |
+| 6 | Med | `@media (max-width: 500px) { .grid { 1fr } }` | Too aggressive — at 375–499px we lose the 2-col grid that auto-fit would otherwise produce. Cards become tall single-column lists with chip walls. |
+| 7 | Med | `
` summaries (`Ảnh gốc`, debug panel) | Muted italic, no chevron — invisible affordance. Users won't click. |
+| 8 | Med | Focus rings | Only on tabs and view-toggle. Links + `
` summaries have no `:focus-visible`. |
+| 9 | Low | `` inline inside `` | At narrow widths the centered heading wraps awkwardly with the parenthetical glued at the end. |
+| 10 | Low | a11y | No skip link to bypass tablist for keyboard users. |
+| 11 | Low | Element accent colors `--kim/--moc/--tho` | `#d4af37`, `#3a8f4a`, `#8a6d3b` — `--kim` and `--moc` borderline AA at h3 size on white. |
+| 12 | Low | Chip border `rgba(0,0,0,0.08)` | Vanishes on dark chips — silhouette dissolves into bg on dark languages. |
+
+Skipped as nitpicks (intentionally not in scope): dark-mode support, fancy 3D / parallax (would overshoot a one-page joke), typography rewrite to a webfont with network calls (constraint says try system stack first).
+
+---
+
+## (b) Changes shipped & rationale
+
+### `style.css` — full pass (370 → 302 lines, all under 400-line cap)
+
+1. **Token expansion (not replacement)** — added `--bg-tint`, `--accent-ink`, `--border`, `--border-strong`, `--shadow-sm`, `--radius-sm`, `--focus-ring`. Kept all original `--bg/--fg/--accent/--gold/--card-bg/--shadow` and `--kim/--moc/--thuy/--hoa/--tho` keys per constraint.
+2. **Contrast fixes:** `--muted #6b5a48` → `#5b4a38` (AA on cream). `--kim #d4af37` → `#c8932a`, `--moc #3a8f4a` → `#2f7d3f`, `--tho #8a6d3b` → `#7a5f30` — h3 now passes AA on white.
+3. **Font stack:** added `Be Vietnam Pro`, `Inter`, then `system-ui` fallbacks — Vietnamese-first system stack, zero network fetches per constraint. `Be Vietnam Pro` ships on most modern Vietnamese-localized systems and on Android; falls back transparently to `system-ui` / `Segoe UI`.
+4. **Hero ribbon:** 5 element-color bars (Kim/Mộc/Thuỷ/Hoả/Thổ, 28×4px each) under the subtitle. Quietly ties the brand together — visible on every load, not kitsch, `aria-hidden="true"` so it never pollutes SR output. Body now has a soft warm radial wash from top-center for subtle lunar-new-year warmth.
+5. **Mode toggle:** added `flex-wrap: wrap` (fixes 320px), bumped border to `--border-strong`, kept pill shape, added subtle drop-shadow on active state for depth, `box-shadow`-based focus ring (consistent with rest of page).
+6. **Reduced motion:** global `prefers-reduced-motion` rule kills all animation/transition durations. `.card:hover { transform }` now also explicitly cancels under reduced motion.
+7. **Mobile breakpoint:** moved 1-column grid collapse from 500px → **420px**. At 375px we keep 2 columns (auto-fit min 160px works). Mode-toggle gets `flex: 1 1 auto` at narrow widths so the two pills share the row evenly. Extra 320px cap.
+8. **`
` | At narrow widths the centered heading wraps awkwardly with the parenthetical glued at the end. |
+| 10 | Low | a11y | No skip link to bypass tablist for keyboard users. |
+| 11 | Low | Element accent colors `--kim/--moc/--tho` | `#d4af37`, `#3a8f4a`, `#8a6d3b` — `--kim` and `--moc` borderline AA at h3 size on white. |
+| 12 | Low | Chip border `rgba(0,0,0,0.08)` | Vanishes on dark chips — silhouette dissolves into bg on dark languages. |
+
+Skipped as nitpicks (intentionally not in scope): dark-mode support, fancy 3D / parallax (would overshoot a one-page joke), typography rewrite to a webfont with network calls (constraint says try system stack first).
+
+---
+
+## (b) Changes shipped & rationale
+
+### `style.css` — full pass (370 → 302 lines, all under 400-line cap)
+
+1. **Token expansion (not replacement)** — added `--bg-tint`, `--accent-ink`, `--border`, `--border-strong`, `--shadow-sm`, `--radius-sm`, `--focus-ring`. Kept all original `--bg/--fg/--accent/--gold/--card-bg/--shadow` and `--kim/--moc/--thuy/--hoa/--tho` keys per constraint.
+2. **Contrast fixes:** `--muted #6b5a48` → `#5b4a38` (AA on cream). `--kim #d4af37` → `#c8932a`, `--moc #3a8f4a` → `#2f7d3f`, `--tho #8a6d3b` → `#7a5f30` — h3 now passes AA on white.
+3. **Font stack:** added `Be Vietnam Pro`, `Inter`, then `system-ui` fallbacks — Vietnamese-first system stack, zero network fetches per constraint. `Be Vietnam Pro` ships on most modern Vietnamese-localized systems and on Android; falls back transparently to `system-ui` / `Segoe UI`.
+4. **Hero ribbon:** 5 element-color bars (Kim/Mộc/Thuỷ/Hoả/Thổ, 28×4px each) under the subtitle. Quietly ties the brand together — visible on every load, not kitsch, `aria-hidden="true"` so it never pollutes SR output. Body now has a soft warm radial wash from top-center for subtle lunar-new-year warmth.
+5. **Mode toggle:** added `flex-wrap: wrap` (fixes 320px), bumped border to `--border-strong`, kept pill shape, added subtle drop-shadow on active state for depth, `box-shadow`-based focus ring (consistent with rest of page).
+6. **Reduced motion:** global `prefers-reduced-motion` rule kills all animation/transition durations. `.card:hover { transform }` now also explicitly cancels under reduced motion.
+7. **Mobile breakpoint:** moved 1-column grid collapse from 500px → **420px**. At 375px we keep 2 columns (auto-fit min 160px works). Mode-toggle gets `flex: 1 1 auto` at narrow widths so the two pills share the row evenly. Extra 320px cap.
+8. **`` styling:** custom chevron (▸ → ▾ on `[open]`) in gold, accent-ink summary text, dashed border on debug panel to visually separate it from the main `original-image` disclosure. Both shared via grouped selectors to keep CSS DRY.
+9. **Skip link:** standard "off-screen until focus" pattern, accent background.
+10. **Chips:** stronger border (`rgba(0,0,0,0.18)` default, `0.45` for TIOBE), inner `1px` highlight on TIOBE chips for premium feel, `overflow: hidden + text-overflow: ellipsis` to prevent overflow at 320px on extra-long names.
+11. **Focus rings everywhere:** unified `--focus-ring` token (gold halo) applied to tabs, view-toggle, credit links, both `` summaries.
+12. **Subtitle / legend max-width** to constrain line length to 38ch / 60ch — improves readability at desktop widths.
+13. Cards got border + reduced base shadow + lift on hover (shadow grows on hover, not just transform — adds depth cue).
+
+### `index.html`
+
+1. Added skip link at top of ``.
+2. Added `id="main-content"` to `` for skip link target.
+3. Added `
` summaries.
+12. **Subtitle / legend max-width** to constrain line length to 38ch / 60ch — improves readability at desktop widths.
+13. Cards got border + reduced base shadow + lift on hover (shadow grows on hover, not just transform — adds depth cue).
+
+### `index.html`
+
+1. Added skip link at top of ``.
+2. Added `id="main-content"` to `` for skip link target.
+3. Added `