mirror of
https://github.com/tiennm99/programming-fengshui.git
synced 2026-08-05 16:25:21 +00:00
chore(plans): drop dual-mode plan (superseded), add todo.md for next session
- delete plans/260427-0854-color-element-mapping/ — feature shipped, classic panel later removed; plan no longer reflects the codebase. - plans/todo.md captures: OG image regen, open questions from the latest UI/UX review, polish leftovers, and which reports are still load-bearing.
This commit is contained in:
@@ -1,155 +0,0 @@
|
||||
---
|
||||
title: "Phase 01 — Vendor color data + HSL classifier module"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: 60m
|
||||
---
|
||||
|
||||
## Context Links
|
||||
|
||||
- Plan overview: [plan.md](plan.md)
|
||||
- Color data report: [../reports/researcher-260427-0855-github-language-colors.md](../reports/researcher-260427-0855-github-language-colors.md)
|
||||
- Classifier algorithm report: [../reports/researcher-260427-0854-nguhanh-color-classifier.md](../reports/researcher-260427-0854-nguhanh-color-classifier.md)
|
||||
- Existing files: `index.html`, `style.css`
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P2 (foundation for phases 02–04)
|
||||
- **Status:** pending
|
||||
- **Description:** Vendor `ozh/github-colors` JSON locally and implement a pure-function HSL classifier (`hex → element`) with an in-browser sanity-check harness.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Report #1 confirms `ozh/github-colors` JSON is CORS-friendly + 78KB, but vendoring locally avoids any runtime third-party dependency and keeps the page working offline / on `file://`.
|
||||
- Report #2 provides a deterministic HSL algorithm with explicit Step-2 grayscale handling and Step-4 edge-case refinements. Translate Python pseudocode to JS literally — do not invent variations.
|
||||
- Report #2 §3 lists 12 sample languages with expected outputs; report §4 lists 10 canonical edge colors. These are the test fixtures.
|
||||
- Element keys must be lowercase (`kim`, `moc`, `thuy`, `hoa`, `tho`) so they match the existing CSS class names in `style.css` (lines 124–133). Reports use uppercase Vietnamese — translate at module boundary.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- Vendor `data/github-colors.json` (~78KB) at the documented schema: `{ "<Language>": { "color": "#hex"|null, "url": "..." } }`.
|
||||
- Provide ES module `js/classify-element.js` exporting:
|
||||
- `hexToHsl(hex: string): { h: number, s: number, l: number }` — H ∈ [0, 360), S/L ∈ [0, 100]
|
||||
- `classify(hex: string): 'kim' | 'moc' | 'thuy' | 'hoa' | 'tho'` — implements report §2 Steps 2–4 verbatim
|
||||
- `ELEMENTS` constant: ordered list `['kim', 'moc', 'thuy', 'hoa', 'tho']` with display labels (`KIM`, `MỘC`, `THUỶ`, `HOẢ`, `THỔ`)
|
||||
- Provide `js/classify-element.test.html` — opens in any browser, runs assertions, prints green PASS / red FAIL list. No test framework. No build.
|
||||
|
||||
### Non-functional
|
||||
- File size budget: `classify-element.js` ≤ 120 lines, `classify-element.test.html` ≤ 100 lines (project rule: <200 lines/file).
|
||||
- Pure functions: no I/O, no DOM, no globals. Importable from Node for future tooling without changes.
|
||||
- Hex input tolerated: `#RRGGBB` and `#rrggbb`. Throw on malformed input (don't silently mis-classify).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data flow
|
||||
```
|
||||
data/github-colors.json (static)
|
||||
│
|
||||
▼ (fetched in Phase 02)
|
||||
{ "JavaScript": { color: "#f1e05a", url: "..." }, ... }
|
||||
│
|
||||
▼ (Phase 02 calls classify(entry.color))
|
||||
classify-element.js ──▶ 'thy' | 'kim' | ...
|
||||
```
|
||||
|
||||
In Phase 01, the classifier is exercised only by the test harness — no integration yet.
|
||||
|
||||
### Module contract (`js/classify-element.js`)
|
||||
|
||||
```js
|
||||
// Input: '#RRGGBB' or '#rrggbb'
|
||||
// Output: 'kim' | 'moc' | 'thuy' | 'hoa' | 'tho'
|
||||
// Throws: TypeError if input is null/undefined/not a 7-char hex string.
|
||||
export function classify(hex) { ... }
|
||||
|
||||
export function hexToHsl(hex) { ... } // exported for test visibility
|
||||
|
||||
export const ELEMENTS = [
|
||||
{ key: 'kim', label: 'KIM' },
|
||||
{ key: 'moc', label: 'MỘC' },
|
||||
{ key: 'thuy', label: 'THUỶ' },
|
||||
{ key: 'hoa', label: 'HOẢ' },
|
||||
{ key: 'tho', label: 'THỔ' },
|
||||
];
|
||||
```
|
||||
|
||||
### Classifier rules (mirror report #2 §2)
|
||||
|
||||
1. Parse `#RRGGBB` → R, G, B ∈ [0, 255] → normalize → HSL.
|
||||
2. Grayscale (`S < 5`):
|
||||
- `L < 20` → `thuy`
|
||||
- `20 ≤ L < 70` → `tho`
|
||||
- `L ≥ 70` → `kim`
|
||||
3. Hue ranges:
|
||||
- `[0, 20)` → `hoa`
|
||||
- `[20, 40)` → `hoa` if `S ≥ 60 && L ≥ 50` else `tho`
|
||||
- `[40, 70)` → `tho`
|
||||
- `[70, 200)` → `moc` (covers green + jade/cyan)
|
||||
- `[200, 260)` → `thuy`
|
||||
- `[260, 360)` → `hoa`
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Create
|
||||
- `data/github-colors.json` (vendored, ~78KB)
|
||||
- `js/classify-element.js`
|
||||
- `js/classify-element.test.html`
|
||||
|
||||
### Modify
|
||||
- None.
|
||||
|
||||
### Delete
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Fetch the JSON once: `curl -fsSL https://raw.githubusercontent.com/ozh/github-colors/master/colors.json -o data/github-colors.json`. Verify file is well-formed JSON (`python3 -m json.tool data/github-colors.json | head` is fine for a smoke check).
|
||||
2. Create `js/classify-element.js`:
|
||||
- Implement `hexToHsl(hex)` — port report #2 pseudocode line by line. Handle `max == min` achromatic case (S=0, H=0).
|
||||
- Implement `classify(hex)` — call `hexToHsl`, then run Step 2 then Step 3, return lowercase string.
|
||||
- Export `ELEMENTS` array.
|
||||
- Add a leading file comment citing the algorithm source: `// Algorithm: plans/reports/researcher-260427-0854-nguhanh-color-classifier.md §2`.
|
||||
3. Create `js/classify-element.test.html`:
|
||||
- Plain HTML with `<script type="module">` importing `./classify-element.js`.
|
||||
- Define a `cases` array with the 12 samples from report §3 + 10 edge colors from report §4.
|
||||
- Loop, compare actual vs expected, append `<li>` to a `<ul>` with PASS (green) / FAIL (red).
|
||||
- Display total counts at the bottom.
|
||||
4. Open `js/classify-element.test.html` in a browser; confirm all 22 cases PASS. If any fail, fix algorithm — do NOT edit expected values.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Download and vendor `data/github-colors.json`
|
||||
- [ ] Implement `hexToHsl(hex)` in `js/classify-element.js`
|
||||
- [ ] Implement `classify(hex)` with Step 2 + Step 3 rules
|
||||
- [ ] Export `ELEMENTS` constant with `key` + `label`
|
||||
- [ ] Build `js/classify-element.test.html` harness
|
||||
- [ ] Add 12 sample-language cases (report §3)
|
||||
- [ ] Add 10 edge-case cases (report §4)
|
||||
- [ ] Run harness in browser; confirm 22/22 PASS
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `data/github-colors.json` exists, parses, contains ≥600 entries with non-null colors.
|
||||
- `node --input-type=module -e "import {classify} from './js/classify-element.js'; console.log(classify('#f1e05a'))"` prints `tho`.
|
||||
- Opening `js/classify-element.test.html` in a browser shows all 22 PASS lines, zero FAIL.
|
||||
- Running `wc -l js/classify-element.js` returns ≤ 120.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| RGB→HSL math bug (off-by-one on hue) | Med | High (mis-classifies many languages) | Test harness with 22 cases catches it; mirror pseudocode literally |
|
||||
| JSON download is rate-limited or moved | Low | Med | Try once; fall back to manually downloading via browser. URL is documented in report #1. |
|
||||
| Vendored JSON gets stale | Med | Low | This is a joke site; staleness is acceptable. Document refresh command in `data/README.md` (one-liner) — defer if YAGNI |
|
||||
| Edge case Swift `#ffac45` (report flagged as borderline) | Low | Low | Report §3 resolves it: classifies as HOẢ via current rules. Test case enforces this. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- No secrets, no auth, no user input — pure data + pure function.
|
||||
- JSON is third-party data; treat as untrusted: validate that color values match `/^#[0-9a-fA-F]{6}$/` before classifying. Skip on mismatch.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Phase 02 imports `classify` and `ELEMENTS` to render the grid. Do not wire up rendering here.
|
||||
- If the test harness fails on a case, fix the classifier — do not change the expected value without updating report #2 first.
|
||||
@@ -1,248 +0,0 @@
|
||||
---
|
||||
title: "Phase 02 — Dual-mode shell: toggle, panels, hash persistence"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: 50m
|
||||
---
|
||||
|
||||
## Context Links
|
||||
|
||||
- Plan overview: [plan.md](plan.md)
|
||||
- Next phase (consumes `#panel-modern`): [phase-03-render-modern-grid.md](phase-03-render-modern-grid.md)
|
||||
- Style polish (toggle CSS): [phase-04-style-polish.md](phase-04-style-polish.md)
|
||||
- Existing files: `index.html` (current single-view markup), `style.css`
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P2 (blocks Phase 03 + 04)
|
||||
- **Status:** pending
|
||||
- **Description:** Restructure `index.html` from a single-view page into a two-panel SPA-style page with a segmented Classic / Modern toggle. Move the existing static image + 5 cards verbatim into `#panel-classic`. Scaffold an empty `#panel-modern` that Phase 03 will populate. Add `js/mode-toggle.js` to handle hash-based persistence + keyboard navigation.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- The 5 published static cards (current `index.html` lines 27–52) and the original image (lines 17–25) are the **canonical Classic content**. They must be moved byte-for-byte; nothing rewritten, nothing reworded. They become the reference comparison view for Modern.
|
||||
- The toggle is the only new interactive control on the page. Everything else stays static. Keep `mode-toggle.js` minimal — under ~80 lines.
|
||||
- URL hash is the persistence channel. Hash also enables sharing (`?` query string would mean a separate URL identity per mode and might confuse GH Pages routing — hash is safer).
|
||||
- Both panels must exist in DOM at all times (locked decision). Toggling `hidden` is preferred over `display:none` in JS because it's both a CSS hook (`[hidden]`) AND a semantic signal to AT.
|
||||
- Default = Classic. If hash is anything other than `#modern`, render Classic.
|
||||
- Phase 04 owns the visual styling of the toggle + the panel fade transition. This phase ships a functional but unstyled toggle — that's fine.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
- Restructure `index.html`:
|
||||
- Hero `<header>` unchanged.
|
||||
- **New** segmented toggle directly after the hero, before any panel.
|
||||
- **`#panel-classic`** wraps the existing `<section class="figure">` (image) + `<section class="elements">` (5 hardcoded cards). Add a small label "(theo bài gốc, 2018)" near the section heading. No content changes inside.
|
||||
- **`#panel-modern`** is a sibling section, currently empty except for an h2 heading and the mount points Phase 03 will use (`<div id="element-grid" class="grid"></div>`, `<p class="legend"></p>`, `<p class="disclaimer">…</p>`). Place `<details><summary>Ảnh gốc</summary>…image…</details>` near the bottom (collapsed by default). The image inside is a **second `<img>` tag** with the same `src` — duplication is intentional and KISS-compliant (no JS needed to move the image between panels).
|
||||
- Footer credit unchanged.
|
||||
- Add `<script type="module" src="./js/mode-toggle.js"></script>` before `</body>`.
|
||||
- `js/mode-toggle.js` responsibilities:
|
||||
- On `DOMContentLoaded`: read `location.hash`, normalize (`#modern` → modern, anything else → classic), call `setMode(mode)`.
|
||||
- `setMode(mode)` sets `hidden` on the inactive panel, removes from active, updates `aria-selected` + `tabindex` on the two `<button>`s, updates `location.hash` (using `history.replaceState` on initial load to avoid pushing a history entry; `location.hash = ...` on user clicks).
|
||||
- Click handler on each tab → `setMode(button.dataset.mode)`.
|
||||
- Keyboard handler on the `<div role="tablist">`: `ArrowLeft` / `ArrowRight` move focus between the two buttons and call `setMode` on the newly focused tab (auto-activate; standard tab pattern). `Home` / `End` jump to first / last (only 2 tabs, but harmless). `Enter` / `Space` no-op extra needed since clicking already works.
|
||||
- Listen to `hashchange` so browser back/forward + manual hash edits also switch modes.
|
||||
|
||||
### Non-functional
|
||||
|
||||
- `js/mode-toggle.js` ≤ 90 lines.
|
||||
- `index.html` total ≤ 130 lines after restructure (currently 78).
|
||||
- Zero console errors. Zero external requests added.
|
||||
- No layout shift between Classic load and Modern toggle beyond the natural content swap.
|
||||
- Works on `file://` (no fetch in this phase).
|
||||
|
||||
## Architecture
|
||||
|
||||
### DOM contract (post-Phase-02)
|
||||
|
||||
```html
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<h1>Lựa chọn ngôn ngữ lập trình theo phong thuỷ</h1>
|
||||
<p class="subtitle">Ngũ Hành tương sinh — code cho hợp tuổi, hợp mệnh.</p>
|
||||
</header>
|
||||
|
||||
<div class="mode-toggle" role="tablist" aria-label="Chế độ hiển thị">
|
||||
<button type="button" role="tab" id="tab-classic" data-mode="classic"
|
||||
aria-controls="panel-classic" aria-selected="true" tabindex="0">
|
||||
Cổ điển (bài gốc)
|
||||
</button>
|
||||
<button type="button" role="tab" id="tab-modern" data-mode="modern"
|
||||
aria-controls="panel-modern" aria-selected="false" tabindex="-1">
|
||||
Hiện đại (tự phân loại)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section id="panel-classic" role="tabpanel" aria-labelledby="tab-classic">
|
||||
<!-- existing <section class="figure"> moved here verbatim -->
|
||||
<section class="figure">
|
||||
<figure>
|
||||
<img src="assets/ngon-ngu-lap-trinh-phong-thuy.png" alt="..." />
|
||||
<figcaption>Sơ đồ ngôn ngữ lập trình xếp theo Ngũ Hành.</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
<!-- existing <section class="elements"> moved here verbatim, with label tweak -->
|
||||
<section class="elements">
|
||||
<h2>Ngũ Hành & ngôn ngữ <small class="mode-tag">(theo bài gốc, 2018)</small></h2>
|
||||
<div class="grid">
|
||||
<article class="card kim"><h3>KIM</h3><p>JavaScript, Objective-C, Python</p></article>
|
||||
<article class="card thuy"><h3>THUỶ</h3><p>C#, PHP</p></article>
|
||||
<article class="card moc"><h3>MỘC</h3><p>Android, C#</p></article>
|
||||
<article class="card hoa"><h3>HOẢ</h3><p>Scala, HTML5, Java, Node.js</p></article>
|
||||
<article class="card tho"><h3>THỔ</h3><p>JavaScript, Go, Ruby</p></article>
|
||||
</div>
|
||||
<p class="disclaimer">* Bảng phân loại mang tính giải trí, lấy từ ảnh gốc.</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="panel-modern" role="tabpanel" aria-labelledby="tab-modern" hidden>
|
||||
<section class="elements">
|
||||
<h2>Ngũ Hành & ngôn ngữ <small class="mode-tag">(tự phân loại theo màu GitHub)</small></h2>
|
||||
<div class="grid" id="element-grid"></div>
|
||||
<p class="legend"></p>
|
||||
<p class="disclaimer">* Phân loại tự động theo màu chính thức GitHub Linguist + quy tắc HSL.</p>
|
||||
</section>
|
||||
<details class="original-image">
|
||||
<summary>Ảnh gốc</summary>
|
||||
<figure>
|
||||
<img src="assets/ngon-ngu-lap-trinh-phong-thuy.png" alt="Sơ đồ phong thuỷ ngôn ngữ lập trình theo Ngũ Hành" />
|
||||
<figcaption>Sơ đồ ngôn ngữ lập trình xếp theo Ngũ Hành.</figcaption>
|
||||
</figure>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<footer class="credit"><!-- unchanged --></footer>
|
||||
</main>
|
||||
<script type="module" src="./js/mode-toggle.js"></script>
|
||||
</body>
|
||||
```
|
||||
|
||||
### Module contract (`js/mode-toggle.js`)
|
||||
|
||||
```js
|
||||
// Public surface: none (self-initialising entry point).
|
||||
// Side effects: mutates DOM (hidden, aria-selected, tabindex), listens to clicks, keys, hashchange.
|
||||
|
||||
const MODES = ['classic', 'modern'];
|
||||
const DEFAULT_MODE = 'classic';
|
||||
|
||||
function readHash() {
|
||||
const h = (location.hash || '').replace('#', '');
|
||||
return MODES.includes(h) ? h : DEFAULT_MODE;
|
||||
}
|
||||
|
||||
function setMode(mode, { writeHash = true, push = false } = {}) {
|
||||
// toggle [hidden] on panels, aria-selected + tabindex on tabs,
|
||||
// update hash via history.replaceState (push=false) or location.hash (push=true)
|
||||
}
|
||||
|
||||
function onTabClick(e) { setMode(e.currentTarget.dataset.mode, { push: true }); }
|
||||
function onTabListKeydown(e) { /* ←/→ Home End cycle + setMode */ }
|
||||
function onHashChange() { setMode(readHash(), { writeHash: false }); }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// wire listeners; initial setMode(readHash(), { writeHash: false })
|
||||
});
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
```
|
||||
|
||||
### Data flow
|
||||
|
||||
```
|
||||
[page load]
|
||||
│
|
||||
▼
|
||||
mode-toggle.js DOMContentLoaded
|
||||
├── readHash() → 'classic' | 'modern'
|
||||
└── setMode(mode, { writeHash:false })
|
||||
├── #panel-classic [hidden=mode!=='classic']
|
||||
├── #panel-modern [hidden=mode!=='modern']
|
||||
├── #tab-classic aria-selected, tabindex=0|-1
|
||||
└── #tab-modern aria-selected, tabindex=0|-1
|
||||
|
||||
[tab click] → setMode(mode, { push:true }) → location.hash = '#mode'
|
||||
[hashchange] → setMode(readHash(), { writeHash:false })
|
||||
[arrow key on tablist] → focus next tab + setMode(...)
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Create
|
||||
- `js/mode-toggle.js`
|
||||
|
||||
### Modify
|
||||
- `index.html` — restructure: insert toggle, wrap existing image+cards into `#panel-classic`, add scaffolded `#panel-modern`, add module script tag. **The 5 cards inside `#panel-classic` must be byte-identical copies of current `index.html` lines 30–49.**
|
||||
|
||||
### Delete
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Open `index.html`. Capture the exact text of lines 17–52 (figure section + elements section) — these blocks move into `#panel-classic` unchanged except for the heading `<small class="mode-tag">` insertion.
|
||||
2. Replace the body `<main>` content with the structure shown in the DOM contract above:
|
||||
- Hero unchanged.
|
||||
- Insert `<div class="mode-toggle" role="tablist">` with two buttons.
|
||||
- Wrap the captured figure + elements blocks inside `<section id="panel-classic" role="tabpanel" aria-labelledby="tab-classic">`. Add `<small class="mode-tag">(theo bài gốc, 2018)</small>` inside the existing `<h2>`.
|
||||
- Add `<section id="panel-modern" role="tabpanel" aria-labelledby="tab-modern" hidden>` with the empty grid mount, legend `<p>`, modern disclaimer, and the `<details><summary>Ảnh gốc</summary>…</details>` block (duplicate `<img>` reusing the same `src`).
|
||||
- Footer credit untouched.
|
||||
3. Add `<script type="module" src="./js/mode-toggle.js"></script>` immediately before `</body>`.
|
||||
4. Create `js/mode-toggle.js` per the contract:
|
||||
- Implement `readHash`, `setMode`, click handler, keydown handler, `hashchange` handler.
|
||||
- On `DOMContentLoaded`, call `setMode(readHash(), { writeHash: false })` so initial render does not pollute history.
|
||||
- Use `history.replaceState(null, '', '#' + mode)` when `writeHash: true && push: false`, `location.hash = mode` when `push: true`.
|
||||
- Arrow key handler: compute next tab index, focus it (`.focus()`), call `setMode` to activate.
|
||||
5. Smoke test (no server needed for this phase since no `fetch`):
|
||||
- Open `index.html` directly. Default view = Classic, image + 5 cards render exactly as before.
|
||||
- Click "Hiện đại" — Classic panel disappears, Modern panel appears (empty grid, legend empty, but `<details>Ảnh gốc</details>` is visible and collapsible).
|
||||
- Refresh page — URL is `#modern`, Modern persists.
|
||||
- Press ← arrow key while a tab has focus — focus and panel switch back to Classic; URL becomes `#classic`.
|
||||
- Open `index.html#modern` directly — opens in Modern.
|
||||
- Open `index.html#bogus` directly — falls back to Classic.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Capture verbatim copy of current `index.html` lines 17–52 for `#panel-classic`
|
||||
- [ ] Insert toggle markup (`<div role="tablist">` + two `<button role="tab">`)
|
||||
- [ ] Wrap figure + elements blocks into `#panel-classic` with `mode-tag` heading addition
|
||||
- [ ] Scaffold `#panel-modern` with empty grid, legend `<p>`, disclaimer, and collapsed `<details>Ảnh gốc</details>` containing duplicate `<img>`
|
||||
- [ ] Add `<script type="module" src="./js/mode-toggle.js">` before `</body>`
|
||||
- [ ] Create `js/mode-toggle.js`: `readHash`, `setMode`, tab click handler
|
||||
- [ ] Add arrow-key navigation (`←`/`→`/`Home`/`End`) on the tablist
|
||||
- [ ] Add `hashchange` listener for back/forward
|
||||
- [ ] Verify default = Classic; refresh persistence; deep link `#modern`; invalid hash fallback
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `index.html` opens with Classic active by default; image + 5 cards visually identical to pre-Phase-02 state.
|
||||
- Clicking "Hiện đại" hides classic panel, shows modern panel (still empty pending Phase 03), URL hash becomes `#modern`.
|
||||
- Refreshing page keeps the active mode.
|
||||
- Browser back button after a toggle restores previous mode (works because we use `location.hash =` on user clicks).
|
||||
- `aria-selected` and `tabindex` on the two `<button>`s reflect state at all times.
|
||||
- Keyboard ←/→ cycles tabs and activates them.
|
||||
- No console errors. `wc -l js/mode-toggle.js` ≤ 90.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Verbatim copy of static cards drifts (typo introduced during move) | Med | High (acceptance test #1 fails) | Copy block as-is, do not retype. Diff against original commit before merging. |
|
||||
| Hash collision with future anchor links (`#section-id`) | Low | Med | Reserve `#classic` / `#modern` only; document in mode-toggle.js comment. |
|
||||
| Loading at `index.html#modern` flashes Classic before swap | Med | Low | Set initial `hidden` on `#panel-modern` in HTML; JS `setMode` runs on `DOMContentLoaded` which fires before paint of dynamic content. If FOUC observed, move the `setMode` call inline-blocking before render. |
|
||||
| Arrow-key handler steals focus when tabs are not focused | Low | Low | Attach keydown listener to the `<div role="tablist">` element only, not `document`. |
|
||||
| Duplicate `<img>` in Modern panel double-loads the asset | Low | Low | Browser caches; same `src` resolves to one network request. Acceptable. |
|
||||
| Browser ignores `<script type="module">` on `file://` (Firefox/old Chrome) | Low | Med | README already recommends `python3 -m http.server`. Document the limitation in commit body. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- No user input is taken anywhere. Hash value is normalized against an allowlist (`MODES`) before any DOM mutation — no injection vector.
|
||||
- All DOM mutations use property assignment (`el.hidden`, `el.setAttribute`, `el.tabIndex`); no `innerHTML`.
|
||||
- Static file additions only.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Phase 03 mounts the dynamic grid into `#panel-modern .grid` (already scaffolded here) and populates the legend `<p>`.
|
||||
- Phase 04 adds the visual styling of the toggle (segmented control look, focus ring, fade transition between panels).
|
||||
- Phase 05 (optional) appends a `<details id="debug-panel">` inside `#panel-modern` after the legend.
|
||||
@@ -1,214 +0,0 @@
|
||||
---
|
||||
title: "Phase 03 — Render dynamic element grid into #panel-modern"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: 45m
|
||||
---
|
||||
|
||||
## Context Links
|
||||
|
||||
- Plan overview: [plan.md](plan.md)
|
||||
- Phase 01 (classifier dependency): [phase-01-data-and-classifier.md](phase-01-data-and-classifier.md)
|
||||
- Phase 02 (panel scaffold dependency): [phase-02-dual-mode-shell.md](phase-02-dual-mode-shell.md)
|
||||
- Style polish (consumes the chip markup produced here): [phase-04-style-polish.md](phase-04-style-polish.md)
|
||||
- Existing files: `index.html` (post-Phase-02, with `#panel-modern .grid` scaffolded), `style.css` (existing `.card.kim/.thuy/.moc/.hoa/.tho`)
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Populate the empty grid in `#panel-modern` (created by Phase 02) with 5 cards driven by `data/github-colors.json` and the Phase 01 classifier. Each card lists its languages as colored chips. Modern-only — Classic panel stays untouched. The collapsed `<details>Ảnh gốc</details>` block already exists in markup; this phase does not modify it.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Mount point is `#panel-modern .grid` (i.e. `document.getElementById('element-grid')`), **not** the legacy `<section class="elements">` block — that selector now exists inside both panels and only the Modern one should be populated by JS.
|
||||
- The Classic cards (`#panel-classic .grid`) are static and must not be touched by this phase. This is a hard rule — JS queries by id, not by class, to avoid accidentally hitting Classic.
|
||||
- Original image, hero, footer credit must stay byte-identical (Phase 02 already kept them so).
|
||||
- Chips must be readable on their own background → compute YIQ contrast at render time and pick black or white text. Only branching needed for accessibility (WCAG-ish).
|
||||
- Alphabetical sort is the only natural default; "popularity" is not in the JSON. YAGNI on ranking.
|
||||
- Module loading on `file://` is restricted in some browsers (e.g. Chrome blocks `fetch` of local JSON). Document `python3 -m http.server 8080` in README — already present.
|
||||
- Render runs on every page load regardless of active mode. Cost is acceptable (~664 chips into a hidden DOM node), and avoids a re-render race when the user switches to Modern.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- On page load (`DOMContentLoaded`), fetch `./data/github-colors.json`.
|
||||
- Filter entries with non-null, valid hex `color` (regex `/^#[0-9a-fA-F]{6}$/`).
|
||||
- For each language: call `classify(color)` → bucket into one of 5 element groups.
|
||||
- Sort each bucket alphabetically by language name (case-insensitive).
|
||||
- Render 5 cards into `#element-grid` (KIM, MỘC, THUỶ, HOẢ, THỔ in that order). Each card shows:
|
||||
- Heading (existing `<h3>` style)
|
||||
- Language count: `<small class="card-count">123 ngôn ngữ</small>`
|
||||
- Chip list: `<div class="chips">` containing one `<span class="chip" style="background:#hex; color:[black|white]" title="#hex">LangName</span>` per language.
|
||||
- Skipped languages (null/invalid color): not rendered. No "unknown" bucket.
|
||||
- Populate `<p class="legend">` (the empty `<p>` already in the markup) with the rule summary: `"Phân loại theo tông màu HSL: đỏ/tím/cam đậm → HOẢ, xanh lá/cyan → MỘC, xanh dương → THUỶ, vàng/nâu → THỔ, trắng/xám sáng → KIM."`
|
||||
- Disclaimer paragraph already exists in the Modern panel markup; do not touch.
|
||||
- The `<details>Ảnh gốc</details>` element exists in markup from Phase 02; this phase does not modify it.
|
||||
|
||||
### Non-functional
|
||||
- File size budget: `js/render-elements.js` ≤ 130 lines, `js/main.js` ≤ 60 lines.
|
||||
- Render must complete <100ms on a typical laptop (664 chips is trivial DOM work).
|
||||
- No console errors. No external network requests at runtime (only relative-path fetch to vendored JSON).
|
||||
- Graceful failure: if fetch fails, replace the legend `<p>` text and prepend an inline `.render-error` element inside `#panel-modern .elements` section. Do not break the rest of the page or affect Classic mode.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data flow
|
||||
|
||||
```
|
||||
[page load]
|
||||
│
|
||||
▼
|
||||
js/main.js (DOMContentLoaded)
|
||||
├── fetch('./data/github-colors.json')
|
||||
│ │
|
||||
│ ▼ (resolved to colors object)
|
||||
│ ┌────────────────────────────┐
|
||||
│ │ for each [name, {color}]: │
|
||||
│ │ if !color → skip │
|
||||
│ │ element = classify(color)│ (from classify-element.js)
|
||||
│ │ buckets[element].push(…) │
|
||||
│ └────────────────────────────┘
|
||||
│
|
||||
└──▶ render-elements.js: renderGrid(buckets, mountEl)
|
||||
└─ writes 5 cards into #element-grid (inside #panel-modern)
|
||||
└──▶ sets legend text on document.querySelector('#panel-modern .legend')
|
||||
```
|
||||
|
||||
### DOM contract (the markup this phase mounts into — created by Phase 02)
|
||||
|
||||
```html
|
||||
<section id="panel-modern" role="tabpanel" aria-labelledby="tab-modern" hidden>
|
||||
<section class="elements">
|
||||
<h2>Ngũ Hành & ngôn ngữ <small class="mode-tag">(tự phân loại theo màu GitHub)</small></h2>
|
||||
<div class="grid" id="element-grid"><!-- this phase populates --></div>
|
||||
<p class="legend"><!-- this phase populates --></p>
|
||||
<p class="disclaimer">* Phân loại tự động theo màu chính thức GitHub Linguist + quy tắc HSL.</p>
|
||||
</section>
|
||||
<details class="original-image">
|
||||
<summary>Ảnh gốc</summary>
|
||||
<figure>
|
||||
<img src="assets/ngon-ngu-lap-trinh-phong-thuy.png" alt="..." />
|
||||
<figcaption>Sơ đồ ngôn ngữ lập trình xếp theo Ngũ Hành.</figcaption>
|
||||
</figure>
|
||||
</details>
|
||||
</section>
|
||||
```
|
||||
|
||||
This phase **does not** modify `index.html`.
|
||||
|
||||
### Module contracts
|
||||
|
||||
```js
|
||||
// js/render-elements.js
|
||||
export function renderGrid(buckets, mountEl)
|
||||
// buckets: { kim: [{name, color}], moc: [...], thuy: [...], hoa: [...], tho: [...] }
|
||||
// mountEl: HTMLElement to render cards into (#element-grid)
|
||||
|
||||
export function renderError(message, mountEl)
|
||||
// inline error fallback; mountEl is the .elements <section> inside #panel-modern
|
||||
|
||||
// js/main.js (entry)
|
||||
// 1. fetch JSON
|
||||
// 2. classify into buckets
|
||||
// 3. call renderGrid(buckets, document.getElementById('element-grid'))
|
||||
// 4. set legend text on document.querySelector('#panel-modern .legend')
|
||||
// 5. on error: renderError into document.querySelector('#panel-modern .elements')
|
||||
```
|
||||
|
||||
### Contrast helper
|
||||
|
||||
Inline in `render-elements.js` (≤ 6 lines):
|
||||
```js
|
||||
// YIQ-based: returns 'black' or 'white'
|
||||
function pickTextColor(hex) {
|
||||
const r = parseInt(hex.slice(1,3),16),
|
||||
g = parseInt(hex.slice(3,5),16),
|
||||
b = parseInt(hex.slice(5,7),16);
|
||||
return (r*299 + g*587 + b*114) / 1000 >= 128 ? 'black' : 'white';
|
||||
}
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Create
|
||||
- `js/render-elements.js`
|
||||
- `js/main.js`
|
||||
|
||||
### Modify
|
||||
- None. (`index.html` already has the mount points from Phase 02.)
|
||||
|
||||
### Delete
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `js/render-elements.js`:
|
||||
- Implement `pickTextColor(hex)` helper.
|
||||
- Implement `renderGrid(buckets, mountEl)`:
|
||||
- Build `DocumentFragment`. For each `{key, label}` in `ELEMENTS`:
|
||||
- `<article class="card ${key}">` with `<h3>${label}</h3>`, `<small class="card-count">${langs.length} ngôn ngữ</small>`, and `<div class="chips">` containing one `<span class="chip">` per language. Use `textContent = name` (never `innerHTML`); set `style.background = color; style.color = pickTextColor(color); title = color`.
|
||||
- Clear `mountEl` (`mountEl.replaceChildren(fragment)`).
|
||||
- Implement `renderError(msg, mountEl)` → prepend `<p class="render-error">${msg}</p>` to `mountEl`.
|
||||
- Export `renderGrid`, `renderError`. Import `ELEMENTS` from `./classify-element.js`.
|
||||
2. Create `js/main.js`:
|
||||
- Import `classify`, `ELEMENTS` from `./classify-element.js`, and `renderGrid`, `renderError` from `./render-elements.js`.
|
||||
- On `DOMContentLoaded`:
|
||||
- `fetch('./data/github-colors.json')` → `.json()`.
|
||||
- Build `buckets = { kim:[], moc:[], thuy:[], hoa:[], tho:[] }`.
|
||||
- Iterate entries: skip if `color` null or fails `/^#[0-9a-fA-F]{6}$/`. Otherwise push `{name, color}` into `buckets[classify(color)]`.
|
||||
- Sort each bucket alphabetically (case-insensitive `localeCompare`).
|
||||
- `renderGrid(buckets, document.getElementById('element-grid'))`.
|
||||
- Set legend: `document.querySelector('#panel-modern .legend').textContent = "Phân loại theo tông màu HSL: đỏ/tím/cam đậm → HOẢ, xanh lá/cyan → MỘC, xanh dương → THUỶ, vàng/nâu → THỔ, trắng/xám sáng → KIM."`
|
||||
- Wrap in try/catch → on failure call `renderError(msg, document.querySelector('#panel-modern .elements'))`.
|
||||
- Add module script tag to `index.html`? **No** — Phase 02 added `js/mode-toggle.js`. Add a second tag `<script type="module" src="./js/main.js"></script>` before `</body>`. *Correction: this is the only `index.html` edit this phase makes — adding the second `<script type="module">` line.* Update file ownership accordingly.
|
||||
3. Edit `index.html` (one line): add `<script type="module" src="./js/main.js"></script>` immediately after the existing `<script type="module" src="./js/mode-toggle.js"></script>`.
|
||||
4. Local smoke test: `python3 -m http.server 8080`, open `http://localhost:8080`, switch to Modern via toggle, confirm:
|
||||
- Modern grid: 5 cards render with chips
|
||||
- JavaScript chip is yellow with black text and lives under THỔ
|
||||
- Python chip is blue with white text under THUỶ
|
||||
- C# chip is green under MỘC
|
||||
- Classic panel still shows verbatim 5 cards from the 2018 mapping (untouched)
|
||||
- `<details>Ảnh gốc</details>` expands and shows the original image
|
||||
- No console errors
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Create `js/render-elements.js` with `renderGrid`, `renderError`, contrast helper
|
||||
- [ ] Create `js/main.js` entry point (fetch + classify + render into `#element-grid`)
|
||||
- [ ] Set legend text on `#panel-modern .legend`
|
||||
- [ ] Add `<script type="module" src="./js/main.js">` to `index.html`
|
||||
- [ ] Smoke test via `python3 -m http.server 8080` in both modes
|
||||
- [ ] Verify the 12 sample languages from report §3 land in expected elements (Modern panel)
|
||||
- [ ] Verify Classic panel unchanged
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Modern panel: all 5 cards render with ≥5 chips each.
|
||||
- Each chip has `style="background:#hex; color:black|white"` and `title="#hex"`.
|
||||
- Sample language placement matches Phase 01 acceptance list (12 languages).
|
||||
- Total Modern render time <100ms (eyeball; no flash of empty content beyond network latency).
|
||||
- Classic panel: visually identical to post-Phase-02 state — JavaScript/Objective-C/Python in KIM card, etc.
|
||||
- View-source after switching to Modern shows populated chip markup inside `#element-grid`.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| `fetch('./data/...')` fails on `file://` (Chrome) | High | Med | Document `python3 -m http.server` in README (already done); show inline error if it happens (does not affect Classic mode) |
|
||||
| 600+ chips cause horizontal scroll on mobile | Med | Med | Phase 04 handles wrap + chip sizing |
|
||||
| `getElementById('element-grid')` collides with Classic if Phase 02 misnamed the Classic grid | Med | High | Phase 02 contract: only `#panel-modern .grid` carries `id="element-grid"`. Verify before Phase 03 starts. |
|
||||
| Sort by name puts emoji/Unicode names oddly | Low | Low | `localeCompare` default is fine. |
|
||||
| Module script load order: `main.js` runs before `mode-toggle.js` finishes | Low | Low | Both listen to `DOMContentLoaded`; order is irrelevant. Each operates on disjoint DOM nodes. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All chip text comes from JSON keys; the JSON is vendored from a trusted source. Still, set `chipEl.textContent = name` (never `innerHTML`) — defends against malformed entries.
|
||||
- Style attribute uses validated hex only (regex-checked in `main.js`). No CSS injection vector.
|
||||
- No `eval`, no `innerHTML` with interpolation.
|
||||
- This phase only writes inside `#panel-modern`; Classic panel is read-never-write.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Phase 04 styles the chips for readability + responsive flow + the toggle / panel transition CSS.
|
||||
- Phase 05 (optional) appends a debug panel inside `#panel-modern` listing skipped + borderline languages.
|
||||
@@ -1,272 +0,0 @@
|
||||
---
|
||||
title: "Phase 04 — Toggle, panel transitions, chip styling, legend, responsive"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: 35m
|
||||
---
|
||||
|
||||
## Context Links
|
||||
|
||||
- Plan overview: [plan.md](plan.md)
|
||||
- Phase 02 (toggle / panel structure dependency): [phase-02-dual-mode-shell.md](phase-02-dual-mode-shell.md)
|
||||
- Phase 03 (chip markup dependency): [phase-03-render-modern-grid.md](phase-03-render-modern-grid.md)
|
||||
- Existing file: `style.css` (current `.card`, `.grid`, color vars)
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** Append CSS for the new dual-mode UI: segmented toggle (`.mode-toggle`, tabs, focus ring), panel `[hidden]` + opacity transition, plus the chip / count / legend / error / responsive rules originally planned. Keep all existing rules.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Two distinct concerns in this phase: **(A)** the toggle + panel transition (new in this revision), **(B)** chip / legend / responsive (carried over from the original Phase 03 plan). Bundled because they live in the same `style.css` file and share design tokens — splitting would create two trivial phases.
|
||||
- The existing grid uses `auto-fit, minmax(160px, 1fr)`. With long chip lists in Modern, cards become tall — fine, but on mobile (single column) we want them readable, not a wall of text.
|
||||
- Existing CSS vars (`--kim`, `--thuy`, etc.) remain the right element accents. Chips use the language hex; card border-top stays element-coloured.
|
||||
- Both panels coexist in DOM; toggling `[hidden]` is the canonical hide. CSS reinforces with `[hidden] { display: none; }` (browsers honor this, but explicitness helps when overriding for transitions).
|
||||
- Opacity fade on switch: simple — apply `transition: opacity 150ms ease` to the panel, set `opacity: 0` while hidden via attribute, but `[hidden]` already removes from layout. KISS: skip cross-fade choreography. Just fade-in the becoming-visible panel via animation. Implementation: keep `[hidden] { display:none }`; when newly shown, the panel naturally appears. Add `@keyframes fadeIn` and apply to non-hidden panels. Cheap, no JS coordination.
|
||||
- KISS: no animation beyond the existing `:hover translateY` and the new fade-in. No dark mode. No hover tooltips beyond browser-native `title`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
#### A. Toggle + panel transitions
|
||||
- `.mode-toggle` is a centered horizontal flex container; small gap; max-width matches the page content.
|
||||
- Each `[role="tab"]` button: pill-shaped, neutral background when inactive (`var(--card-bg)` or transparent), accent background when `aria-selected="true"` (`var(--accent)` background, white text). Border, rounded corners, padding.
|
||||
- Focus ring: visible 2px outline (`outline: 2px solid var(--gold); outline-offset: 2px`) on `:focus-visible` only.
|
||||
- `[role="tabpanel"][hidden]` → `display: none` (default browser behavior; reinforced explicitly).
|
||||
- `[role="tabpanel"]:not([hidden])` → fade in via `@keyframes fadeIn` (opacity 0 → 1 over 150ms). No layout shift.
|
||||
- `.mode-tag` (the `<small>` inside h2): muted, italic, `0.85rem`.
|
||||
|
||||
#### B. Chip / legend / responsive (originally planned)
|
||||
- `.chips` is a flex container that wraps.
|
||||
- `.chip` is a small inline-block with rounded corners, padded, font-size ~0.78rem. No `text-shadow` (we already pick contrast color).
|
||||
- `.card-count` is a muted small label under `<h3>`.
|
||||
- `.legend` is centered, italic, `var(--muted)`, `0.85rem`.
|
||||
- `.render-error` is centered, `var(--accent)` color, padded, dashed border.
|
||||
- `.original-image` (the `<details>` block in Modern panel): inherit existing figure styles; `<summary>` should be readable (cursor pointer, muted color).
|
||||
- Mobile (≤500px): chips slightly smaller; grid collapses to single column; toggle buttons shrink padding/font-size to stay on one row.
|
||||
|
||||
### Non-functional
|
||||
- No new CSS file. Append to existing `style.css`. Total file should remain ≤ 280 lines (currently 175).
|
||||
- No CSS frameworks, no preprocessors.
|
||||
- Chips must remain readable on extreme colors (e.g. `#000000` Linguist entries). The `pickTextColor` from Phase 03 already handles this; CSS does not need to override.
|
||||
- Animation respects `prefers-reduced-motion`: wrap `@keyframes fadeIn` usage in `@media (prefers-reduced-motion: no-preference)`.
|
||||
|
||||
## Architecture
|
||||
|
||||
No structural change. Just additive selectors.
|
||||
|
||||
### Selectors to add
|
||||
|
||||
```css
|
||||
/* A. Toggle + panels */
|
||||
.mode-toggle /* flex tablist wrapper */
|
||||
.mode-toggle [role="tab"] /* tab button base */
|
||||
.mode-toggle [role="tab"][aria-selected="true"] /* active tab */
|
||||
.mode-toggle [role="tab"]:focus-visible /* focus ring */
|
||||
[role="tabpanel"][hidden] /* explicit display:none */
|
||||
.mode-tag /* "(theo bài gốc, 2018)" small tag in h2 */
|
||||
.original-image /* <details> wrapping the duplicate image in Modern */
|
||||
.original-image summary /* clickable summary styling */
|
||||
@keyframes fadeIn /* opacity 0 → 1 */
|
||||
|
||||
/* B. Grid contents */
|
||||
.chips /* flex wrap container inside .card */
|
||||
.chip /* individual language pill */
|
||||
.card-count /* small count under <h3> */
|
||||
.legend /* one-line rule explanation */
|
||||
.render-error /* fetch failure inline message */
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 500px) { ... }
|
||||
@media (prefers-reduced-motion: no-preference) { /* fade-in */ }
|
||||
```
|
||||
|
||||
### Visual hierarchy (modern panel)
|
||||
|
||||
```
|
||||
.card
|
||||
├── h3 (existing element-color title)
|
||||
├── .card-count ← small muted "{n} ngôn ngữ"
|
||||
└── .chips ← flex wrap
|
||||
└── .chip ×N ← background = language hex, color = computed contrast
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Create
|
||||
- None.
|
||||
|
||||
### Modify
|
||||
- `style.css` — append new selectors at the end.
|
||||
|
||||
### Delete
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Open `style.css`, append a "Dual-mode toggle + panels" block after the existing `.card.tho h3 { ... }` rules (around line 133):
|
||||
|
||||
```css
|
||||
/* ===== Dual-mode toggle + panels ===== */
|
||||
.mode-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0 1.5rem;
|
||||
}
|
||||
.mode-toggle [role="tab"] {
|
||||
font: inherit;
|
||||
padding: 0.45rem 1rem;
|
||||
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;
|
||||
}
|
||||
.mode-toggle [role="tab"][aria-selected="true"] {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.mode-toggle [role="tab"]:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.mode-toggle [role="tab"]:focus-visible {
|
||||
outline: 2px solid var(--gold);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
[role="tabpanel"][hidden] { display: none; }
|
||||
.mode-tag {
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
font-weight: normal;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
.original-image {
|
||||
margin: 2rem 0 0;
|
||||
background: var(--card-bg);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.original-image summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.original-image figure { margin: 0.75rem 0 0; }
|
||||
.original-image img { display: block; max-width: 100%; height: auto; border-radius: 8px; }
|
||||
.original-image figcaption {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
[role="tabpanel"]:not([hidden]) { animation: fadeIn 150ms ease; }
|
||||
}
|
||||
```
|
||||
|
||||
2. Append the chip / legend / error block:
|
||||
|
||||
```css
|
||||
/* ===== Chip grid (modern panel) ===== */
|
||||
.card-count {
|
||||
display: block;
|
||||
margin: 0 0 0.6rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
padding: 0.18rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.legend {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
.render-error {
|
||||
text-align: center;
|
||||
color: var(--accent);
|
||||
padding: 1rem;
|
||||
border: 1px dashed var(--accent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.chip { font-size: 0.72rem; padding: 0.15rem 0.45rem; }
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
.mode-toggle [role="tab"] { padding: 0.35rem 0.75rem; font-size: 0.9rem; }
|
||||
}
|
||||
```
|
||||
|
||||
3. Reload `http://localhost:8080`. Verify:
|
||||
- Toggle: two pill buttons centered above the panels; active tab is accent-coloured.
|
||||
- Tab focus ring: gold outline appears when tabbing in via keyboard.
|
||||
- Switching tabs: the becoming-visible panel fades in (≤150ms).
|
||||
- Modern panel: chips wrap nicely inside cards; yellow JavaScript chip has black text; dark blue Python chip has white text.
|
||||
- Mobile (Chrome devtools, 375px width): one card per row; tabs still on one line; chips readable.
|
||||
- Classic panel: image + 5 cards visually unchanged from pre-Phase-04.
|
||||
- `<details>Ảnh gốc</details>` (in Modern): summary line muted/italic; expanding shows the image card-styled.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Append `.mode-toggle` flex container + button base + active + hover + focus-visible rules
|
||||
- [ ] Append `[role="tabpanel"][hidden] { display:none }` reinforcement
|
||||
- [ ] Append `.mode-tag` muted-italic style
|
||||
- [ ] Append `.original-image` + `summary` + nested `figure/img/figcaption` styles
|
||||
- [ ] Append `@keyframes fadeIn` inside `prefers-reduced-motion: no-preference` guard
|
||||
- [ ] Append `.card-count` rule
|
||||
- [ ] Append `.chips` flex wrap container rule
|
||||
- [ ] Append `.chip` pill rule (no color/background — set inline in JS)
|
||||
- [ ] Append `.legend` and `.render-error` rules
|
||||
- [ ] Append `@media (max-width: 500px)` block (chip + grid + toggle adjustments)
|
||||
- [ ] Visual smoke test desktop + 375px mobile, both modes, with reduced-motion on/off
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Toggle visually reads as a segmented control; active tab is unambiguous.
|
||||
- Keyboard focus ring visible on tab focus (Tab key navigation).
|
||||
- Panel switch produces a soft fade-in (no layout flash) with reduced-motion respected.
|
||||
- Chips are rounded pills; do not overlap; wrap to next line when card width exhausted.
|
||||
- Card with 100+ chips remains within page width (no horizontal scroll on a 320px viewport).
|
||||
- Legend renders centered, italic, muted under the grid.
|
||||
- Classic panel layout/colors visually identical to before this phase.
|
||||
- `style.css` total length ≤ 280 lines.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Long card heights in Modern cause uneven grid rows | High | Low | Acceptable; grid auto-fit handles it. Don't force equal heights. |
|
||||
| Chip with `#000` looks like a hole | Med | Low | Border on chip provides outline (rule already includes 1px border) |
|
||||
| Mobile toggle wraps to two lines on very narrow screens | Med | Low | `@media` block reduces button padding. If still wraps at <320px, acceptable. |
|
||||
| Fade-in animation runs on the *initial* visible panel on page load (Classic) | Med | Low | Acceptable; users see Classic fade in once, looks intentional. If undesired, gate with `:not([data-initial])` flag set by `mode-toggle.js`. |
|
||||
| `prefers-reduced-motion` users still see fade due to a missed media query | Low | Low | Wrapping the keyframes definition + usage inside `@media (prefers-reduced-motion: no-preference)` ensures reduced-motion users get instant switches. |
|
||||
| Active-tab contrast (white on `--accent`) fails AA on bright accents | Low | Med | Current `--accent` is `#b8312f` (dark red); white text passes AA. If accent changes, re-check. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Pure CSS, no security implications. No `url(...)`, no `@import` from external.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Phase 05 (optional) adds a debug panel inside `#panel-modern` — its `.render-error` style is already provided here; debug panel reuses standard `<details>` plus existing `.chip` styling.
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
title: "Phase 05 (optional) — Debug verify panel for unclassified / borderline cases"
|
||||
status: pending
|
||||
priority: P3
|
||||
effort: 30m
|
||||
---
|
||||
|
||||
## Context Links
|
||||
|
||||
- Plan overview: [plan.md](plan.md)
|
||||
- Phase 02 (panel scaffold): [phase-02-dual-mode-shell.md](phase-02-dual-mode-shell.md)
|
||||
- Phase 03 (grid render dependency): [phase-03-render-modern-grid.md](phase-03-render-modern-grid.md)
|
||||
- Phase 04 (parallel-OK): [phase-04-style-polish.md](phase-04-style-polish.md)
|
||||
- Algorithm reference: [../reports/researcher-260427-0854-nguhanh-color-classifier.md](../reports/researcher-260427-0854-nguhanh-color-classifier.md) §4 edge cases
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P3 (optional; ship if time allows)
|
||||
- **Status:** pending
|
||||
- **Description:** Add a collapsible `<details>` debug panel inside `#panel-modern` (after the legend, before the disclaimer) that lists (a) skipped languages (no color), (b) borderline-classified languages near hue boundaries. Future contributors can sanity-check classifier output without rerunning the test harness. Modern-only — Classic panel is unaffected.
|
||||
|
||||
## Key Insights
|
||||
|
||||
- Borderline = classifier is using a "tipping rule" rather than a deep-bucket hue. Concretely: `H ∈ [18, 22)`, `H ∈ [38, 42)`, `H ∈ [68, 72)`, `H ∈ [198, 202)`, `H ∈ [258, 262)`. Width: ±2°. Anything inside = borderline.
|
||||
- Grayscale bucket is also borderline-prone at `S ∈ [4, 6]` and at `L ∈ [18, 22] / [68, 72]`.
|
||||
- This panel is for humans, not the build. Hidden by default via `<details>`.
|
||||
- Reusing `.chip` styles from Phase 04 keeps CSS additions to zero.
|
||||
- Mount point is **inside `#panel-modern`** only — never inside Classic. Hard rule.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
- After Modern grid renders, count + list:
|
||||
1. **Skipped** (color is null/invalid): show count + a hidden chip list inside `<details>`.
|
||||
2. **Borderline** (within ±2° of any hue boundary, OR `S ∈ [4,6]`, OR grayscale `L ∈ [18,22]/[68,72]`): show count + chip list with both the language hex and the assigned element label appended (e.g., `Foo (#a8e0c5 → moc)`).
|
||||
- Both lists hidden by default; click summary to expand.
|
||||
- If both lists are empty, hide the entire panel (no `<details>` rendered visibly — set `hidden`).
|
||||
|
||||
### Non-functional
|
||||
- Append to `js/render-elements.js` (do NOT create a new file). Stay under 200 lines total.
|
||||
- No CSS additions — reuse `.chip` and small text styles.
|
||||
- Zero performance cost when the panel is collapsed (it's just plain DOM, not lazy).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data flow
|
||||
|
||||
```
|
||||
js/main.js (Phase 03)
|
||||
│
|
||||
├── classifies all entries
|
||||
│ └── also collects:
|
||||
│ skipped: [{name}] (color was null/invalid)
|
||||
│ borderline: [{name, color, element}] (HSL near boundary)
|
||||
│
|
||||
└── renderGrid(...) (existing, into #element-grid)
|
||||
└── renderDebugPanel({ skipped, borderline }, mountEl) ← NEW
|
||||
mountEl = document.getElementById('debug-panel') (inside #panel-modern)
|
||||
```
|
||||
|
||||
### New module additions
|
||||
|
||||
```js
|
||||
// js/render-elements.js (append)
|
||||
export function renderDebugPanel({ skipped, borderline }, mountEl) { ... }
|
||||
export function isBorderline(hex) { ... }
|
||||
|
||||
// js/main.js (modify)
|
||||
// extend the per-entry loop to also push to skipped[] and borderline[] arrays
|
||||
// after renderGrid, call renderDebugPanel(diag, document.getElementById('debug-panel'))
|
||||
```
|
||||
|
||||
### Borderline detector (inline, ≤ 12 lines)
|
||||
|
||||
```js
|
||||
import { hexToHsl } from './classify-element.js';
|
||||
|
||||
export function isBorderline(hex) {
|
||||
const { h, s, l } = hexToHsl(hex);
|
||||
if (s >= 4 && s < 6) return true; // grayscale tip
|
||||
if (s < 5) return l >= 18 && l <= 22 || l >= 68 && l <= 72;
|
||||
const boundaries = [20, 40, 70, 200, 260]; // 360-wrap not needed; 0/360 is identical
|
||||
return boundaries.some(b => Math.abs(h - b) <= 2);
|
||||
}
|
||||
```
|
||||
|
||||
### DOM contract (inside #panel-modern)
|
||||
|
||||
```html
|
||||
<section id="panel-modern" role="tabpanel" aria-labelledby="tab-modern" hidden>
|
||||
<section class="elements">
|
||||
<h2>...</h2>
|
||||
<div class="grid" id="element-grid">...</div>
|
||||
<p class="legend">...</p>
|
||||
<details id="debug-panel" hidden>
|
||||
<summary>Kiểm tra tự động (N skipped · M borderline)</summary>
|
||||
<h4>Bỏ qua (không có màu)</h4>
|
||||
<div class="chips">…skipped chips…</div>
|
||||
<h4>Trường hợp ranh giới</h4>
|
||||
<div class="chips">…borderline chips…</div>
|
||||
</details>
|
||||
<p class="disclaimer">...</p>
|
||||
</section>
|
||||
<details class="original-image">...</details>
|
||||
</section>
|
||||
```
|
||||
|
||||
The `hidden` attribute on `#debug-panel` is removed by JS only when at least one list is non-empty.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
### Create
|
||||
- None.
|
||||
|
||||
### Modify
|
||||
- `js/render-elements.js` — append `renderDebugPanel` + `isBorderline` exports.
|
||||
- `js/main.js` — collect `skipped` + `borderline` while iterating; call `renderDebugPanel` after `renderGrid`.
|
||||
- `index.html` — add `<details id="debug-panel" hidden></details>` inside `#panel-modern .elements`, after the `<p class="legend">` and before `<p class="disclaimer">`.
|
||||
|
||||
### Delete
|
||||
- None.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. In `js/render-elements.js` (append):
|
||||
- Import `hexToHsl` from `./classify-element.js`.
|
||||
- Add `isBorderline(hex)` per the snippet above.
|
||||
- Add `renderDebugPanel({ skipped, borderline }, mountEl)`:
|
||||
- If both empty → set `mountEl.hidden = true; return;`.
|
||||
- Else build `<summary>` with counts.
|
||||
- Build two `.chips` containers; for `skipped`, plain `<span class="chip">name</span>` (no color, default styling); for `borderline`, `<span class="chip" style="background:#hex;color:..." title="#hex → element">{name}</span>`.
|
||||
- Set `mountEl.hidden = false`.
|
||||
2. In `js/main.js`, during the iteration loop:
|
||||
- When skipping (null/invalid color) → push `{ name }` to `skipped[]`.
|
||||
- After `classify(color)`: if `isBorderline(color)`, push `{ name, color, element }` to `borderline[]`.
|
||||
- After `renderGrid`, call `renderDebugPanel({ skipped, borderline }, document.getElementById('debug-panel'))`.
|
||||
- Import `isBorderline` and `renderDebugPanel` from `./render-elements.js`.
|
||||
3. In `index.html`, inside `#panel-modern .elements`, insert `<details id="debug-panel" hidden></details>` between the legend `<p>` and the disclaimer `<p>`.
|
||||
4. Smoke test: open page, switch to Modern, expand the panel, eyeball the lists. Expected: ~58 skipped (data/markup formats — JSON, YAML, etc.); a small handful of borderline. Verify Classic panel unchanged.
|
||||
|
||||
## Todo List
|
||||
|
||||
- [ ] Add `isBorderline(hex)` helper in `render-elements.js`
|
||||
- [ ] Add `renderDebugPanel(...)` exported function
|
||||
- [ ] Extend `main.js` iteration to collect `skipped` + `borderline`
|
||||
- [ ] Wire `renderDebugPanel` after `renderGrid`
|
||||
- [ ] Add `<details id="debug-panel" hidden>` inside `#panel-modern .elements`
|
||||
- [ ] Verify panel hides when both lists empty
|
||||
- [ ] Verify "skipped" count ≈ 58 (matches report #1 §5)
|
||||
- [ ] Verify Classic panel still unchanged (no debug panel leaks across)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Debug panel exists in Modern only and is collapsed by default.
|
||||
- Expanding shows two sub-sections with counts in `<summary>`.
|
||||
- Skipped count ≈ 58 (report #1 expectation).
|
||||
- Borderline list is non-empty but reasonable (say, < 50 entries).
|
||||
- If you set `data/github-colors.json` to a synthetic dataset with no skipped + no borderline, the `<details>` element stays hidden.
|
||||
- Classic panel: `#panel-classic` shows no debug panel, no chip clutter.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Borderline count too noisy (>200) | Med | Low | ±2° window already narrow; if too noisy, narrow to ±1° in a follow-up |
|
||||
| Adding panel changes layout when collapsed | Low | Low | `<details>` collapses to a single line; visually negligible |
|
||||
| Importing `hexToHsl` re-exposes internals | Low | None | `hexToHsl` is already exported from Phase 01 contract |
|
||||
| Debug panel mount selector accidentally targets Classic | Low | High | Use `getElementById('debug-panel')`; only Modern has that id (verify in HTML edit) |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Panel content is the same chip/textContent path as Phase 03. No new injection surface.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- If borderline list reveals systematic issues → revisit classifier rules in `classify-element.js` and update the test cases in `classify-element.test.html` (and report #2 §3).
|
||||
- This phase is the final one. Mark feature complete after manual review of the panel.
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
title: "Dual-mode (Classic / Modern) Ngũ Hành language page"
|
||||
description: "Toggle between the published 2018 mapping and an HSL-classifier-driven dynamic grid."
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: 3h45m
|
||||
branch: main
|
||||
tags: [static-site, frontend, classifier, dual-mode, gh-pages]
|
||||
created: 2026-04-27
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Extend `index.html` from a single static view into a dual-mode page: **Classic** (the 2018 published image + 5 hardcoded cards, verbatim) and **Modern** (collapsed source image + dynamic grid driven by GitHub Linguist colors classified into 5 Ngũ Hành elements via deterministic HSL rules). No build step. Single SPA, vanilla ES modules, deployed via existing GH Pages workflow.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Purpose | Contents |
|
||||
|------|---------|----------|
|
||||
| **Classic** *(default)* | Faithful reproduction of the 2018 source | Full-size original image + 5 hardcoded element cards (JS/Obj-C/Python in KIM, etc.) labelled "(theo bài gốc, 2018)" |
|
||||
| **Modern** | Value-add extension | Dynamic grid + legend rendered from `data/github-colors.json` via classifier; original image collapsed into `<details>Ảnh gốc</details>` near bottom |
|
||||
|
||||
**Default = Classic.** Persisted via URL hash (`#classic` / `#modern`); shareable, no localStorage. Both panels live in the DOM at all times; toggle sets `hidden` on the inactive one with a CSS opacity fade.
|
||||
|
||||
## Architectural decisions (locked)
|
||||
|
||||
- **Toggle UI:** segmented control, two `<button role="tab">` inside `<div role="tablist">`. Keyboard: ←/→ cycle, Enter/Space activate. Placed between hero and panels.
|
||||
- **Persistence:** URL hash only. Read on load; write on click. Invalid/missing → Classic.
|
||||
- **Single DOM, two panels:** `#panel-classic` + `#panel-modern`, both `role="tabpanel"`. Toggle never re-renders.
|
||||
- **Data source:** vendor `data/github-colors.json` from `ozh/github-colors`. `fetch('./data/github-colors.json')` (relative, same-origin, offline-friendly).
|
||||
- **Module strategy:** vanilla ES modules; no bundler, no npm.
|
||||
- **Classifier contract:** pure `classify(hex) → 'kim'|'moc'|'thuy'|'hoa'|'tho'` (lowercase to match existing CSS classes).
|
||||
- **Animation:** CSS `opacity` transition only.
|
||||
- **Out of scope:** framework, build tooling, npm install, GitHub API, popularity ranking, search, filter, dark mode, localStorage, tests beyond classifier harness.
|
||||
|
||||
## Phases
|
||||
|
||||
| # | File | Status | Effort | Owner files |
|
||||
|---|------|--------|--------|-------------|
|
||||
| 01 | [phase-01-data-and-classifier.md](phase-01-data-and-classifier.md) | pending | 60m | `data/github-colors.json` (new), `js/classify-element.js` (new), `js/classify-element.test.html` (new) |
|
||||
| 02 | [phase-02-dual-mode-shell.md](phase-02-dual-mode-shell.md) | pending | 50m | `index.html` (restructure), `js/mode-toggle.js` (new) |
|
||||
| 03 | [phase-03-render-modern-grid.md](phase-03-render-modern-grid.md) | pending | 45m | `js/render-elements.js` (new), `js/main.js` (new) |
|
||||
| 04 | [phase-04-style-polish.md](phase-04-style-polish.md) | pending | 35m | `style.css` (modify) |
|
||||
| 05 | [phase-05-debug-verify-panel.md](phase-05-debug-verify-panel.md) | pending (optional) | 30m | `js/render-elements.js` (modify), `index.html` (modify, modern panel only) |
|
||||
|
||||
Total: 3h10m core, +30m optional.
|
||||
|
||||
## Dependency graph
|
||||
|
||||
```
|
||||
phase-01 ──▶ phase-03 ──┬──▶ phase-04
|
||||
└──▶ phase-05 (optional)
|
||||
phase-02 ──▶ phase-03
|
||||
phase-02 ──▶ phase-04 (toggle/panel CSS)
|
||||
```
|
||||
|
||||
Phase 02 is independent of Phase 01 but blocks Phase 03 (provides `#panel-modern` mount). Phase 04 needs both Phase 02 (toggle markup) and Phase 03 (grid markup) to exist.
|
||||
|
||||
## File ownership (no overlap between concurrent phases)
|
||||
|
||||
- Phase 01 owns: `data/`, `js/classify-element.js`, `js/classify-element.test.html`
|
||||
- Phase 02 owns: `index.html` (full restructure: hero stays, classic panel wraps the existing image+cards, modern panel scaffolded empty, toggle inserted), `js/mode-toggle.js`
|
||||
- Phase 03 owns: `js/render-elements.js`, `js/main.js`. Reads `#panel-modern .grid` (created by Phase 02). Does **not** edit `index.html`.
|
||||
- Phase 04 owns: `style.css` (toggle, panel transitions, chip, legend, responsive)
|
||||
- Phase 05 owns: appends to `js/render-elements.js`, adds `<details id="debug-panel">` inside `#panel-modern` only
|
||||
|
||||
`index.html` is touched by Phase 02 (structural) and Phase 05 (debug mount inside `#panel-modern`) — sequential, no overlap.
|
||||
|
||||
## Acceptance criteria (whole feature)
|
||||
|
||||
1. Page loads at `index.html` with **Classic** mode active by default; original image full-size and the 5 published-mapping cards (JS/Obj-C/Python in KIM, C#/PHP in THUỶ, Android/C# in MỘC, Scala/HTML5/Java/Node.js in HOẢ, JS/Go/Ruby in THỔ) render verbatim.
|
||||
2. Clicking **Modern** toggle: classic panel hides, modern panel shows; dynamic grid populated by classifier with ≥5 chips per element; original image visible only after expanding `<details>Ảnh gốc</details>`.
|
||||
3. URL hash updates to `#classic` / `#modern` on toggle. Refreshing the page restores the active mode. Sharing the URL with `#modern` opens directly in Modern.
|
||||
4. Keyboard: Tab focuses the toggle; ←/→ cycle between tabs; Enter/Space activates the focused tab; `aria-selected` reflects state.
|
||||
5. The 12 sample languages classify deterministically per the HSL rules (algorithm-verified): JS→THỔ, Python→THUỶ, Rust→THỔ, Go→MỘC, Ruby→HOẢ, Java→THỔ, C#→MỘC, TS→THUỶ, PHP→THUỶ, Swift→HOẢ, Kotlin→KIM, HTML→HOẢ. (Note: Rust/Java moved to THỔ vs report §3's preliminary table because their orange-brown hues fall below the L≥50 / S≥60 brightness gate that promotes [20,40) hues to HOẢ; Go moves to MỘC because its cyan hue H≈192 sits in the [70,200) MỘC range per the locked rule "MỘC covers green + jade/cyan"; Kotlin lands in KIM under the L≥70 "metallic shine" rule introduced 2026-04-27 to rebalance the empty KIM bucket.)
|
||||
6. Languages with `color: null` are absent from the Modern grid.
|
||||
7. Hero, footer credit unchanged. No console errors. No external network requests at runtime.
|
||||
8. `js/classify-element.test.html` opens in browser and prints PASS for all 22 cases.
|
||||
|
||||
## Rollback
|
||||
|
||||
Phase commits revert in reverse order (05 → 04 → 03 → 02 → 01). **Phase 02 is the only destructive change to `index.html`** (restructures the published markup into panels). The full pre-Phase-02 markup must be quoted verbatim in the Phase 02 commit body for restoration. After Phase 02 revert, Classic mode markup must reappear as the original single-view layout with no toggle.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
1. Cap on chips per element in Modern? Default = show all alphabetical. Confirm if too noisy after first render.
|
||||
2. Should hash changes via browser back/forward animate the panel switch? Default = yes (same `hashchange` listener).
|
||||
@@ -0,0 +1,47 @@
|
||||
# TODO — programming-fengshui
|
||||
|
||||
Last session ended: 2026-04-27 21:14 (Asia/Saigon). Branch: `main`.
|
||||
|
||||
Pick up here next time.
|
||||
|
||||
## Context for next session
|
||||
|
||||
- Site is single-mode now (classic panel removed); modern grid is the only view, 2018 source image lives under a `<details>` spoiler.
|
||||
- Three segmented controls live above the grid: **Nguồn màu** (GitHub / GitLab), **Hiển thị** (TIOBE Top 20 / Tất cả ngôn ngữ), **Sắp xếp** (Mặc định / A–Z / Cầu vồng).
|
||||
- Cards have element-tinted backgrounds, count line picks up element color.
|
||||
- Most recent UI/UX review and unresolved questions: `plans/reports/ui-ux-260427-2043-fengshui-page-review.md`.
|
||||
|
||||
## Open items (in priority order)
|
||||
|
||||
### 1. OG / social card image
|
||||
- `index.html:21` `og:image` still points at the 2018 source image. Replace with a render of the current Ngũ Hành cards.
|
||||
- Options: screenshot the live page at 1200×630 (need a working browser on this aarch64 host first — last attempt failed because cached puppeteer chrome is x86_64 ELF), or build an HTML→PNG via the `design` skill.
|
||||
- Output: `assets/og-card.png`, then update the meta tag.
|
||||
|
||||
### 2. Resolve the open questions in the UI/UX review
|
||||
From `plans/reports/ui-ux-260427-2043-fengshui-page-review.md` §7:
|
||||
1. **5-card-row symbolism** — keep `auto-fit minmax(220px, 1fr)` (current) or force back to 5-col with a wider `--page-width`? The 5-element wheel is part of the joke; auto-fit may collapse to 2–3 cols on common viewports.
|
||||
2. **Empty KIM bucket in TIOBE view** — KIM has 0 TIOBE Top 20 entries. Should we hide the empty card or keep the heading visible? Current state: heading + "0 ngôn ngữ" count.
|
||||
3. **Persist user toggle choices** — view + sort + source currently reset on every load. Wire to URL query params (`?source=gitlab&view=all&sort=hue`) for shareable state. Probably P3 — small win, low cost.
|
||||
4. **GitLab vs GitHub palette disparity tooltip** — GitLab has 91 entries vs GitHub's 664. Add a small note when GitLab is selected so users don't think it's broken.
|
||||
5. **Strict AA vs decorative chip contrast** — review §2 brought worst cases above AA via the new `pickTextColor`, but a few still hover at 3.5–4.0:1 (Swift, MATLAB). Acceptable for decorative chips? Document the policy.
|
||||
|
||||
### 3. Polish leftovers from the review (none of them blocking)
|
||||
- ARIA radio pattern is in place on the segmented controls — recheck with NVDA / VoiceOver if you have access.
|
||||
- Anchor link underlines (`.credit a`, `.figure figcaption a`) use `border-bottom: 1px dotted` — at small sizes this can render sub-pixel. Migrate to native `text-decoration: underline; text-decoration-style: dotted;` when convenient.
|
||||
- `.hero .subtitle` is italic — Be Vietnam Pro italic at small sizes can look slanted-rough on Linux. Consider dropping italic in favour of letter-spacing.
|
||||
- Per-card "Top 5 non-TIOBE peek" when in TIOBE view — additive feature, see review §6.
|
||||
- Subtle Lunar-New-Year SVG texture at ~3 % opacity for body bg — ditto §6.
|
||||
|
||||
### 4. Tests / verification
|
||||
- The `classify-element.test.html` harness still passes 22/22 (last verified). Re-run when you make any classifier rule changes.
|
||||
- No automated browser test for the page itself — if doing meaningful changes, spin up `python3 -m http.server 8765` and walk through each toggle by hand.
|
||||
|
||||
## Reference reports (do NOT delete)
|
||||
|
||||
- `plans/reports/researcher-260427-0854-nguhanh-color-classifier.md` — algorithm spec, referenced in `js/classify-element.js:1`.
|
||||
- `plans/reports/researcher-260427-0855-github-language-colors.md` — GitHub Linguist data source rationale.
|
||||
- `plans/reports/researcher-260427-1024-gitlab-colors-source.md` — GitLab data source.
|
||||
- `plans/reports/brainstorm-260427-1046-kim-rebalance.md` — explains the current L≥70 "metallic shine" KIM rule.
|
||||
- `plans/reports/uiux-260427-0927-audit-improvements.md` — first-pass UI audit (most items shipped).
|
||||
- `plans/reports/ui-ux-260427-2043-fengshui-page-review.md` — most recent review; top-3 critical fixes shipped, polish items partly shipped.
|
||||
Reference in New Issue
Block a user