mirror of
https://github.com/tiennm99/programming-fengshui.git
synced 2026-08-05 08:23:27 +00:00
docs(plans): add 260428-1003 plan to implement all remaining items
4-phase plan covering 15 remaining items from todo.md + 2043 review: - Phase 01: CSS/HTML polish (items 5-10, 13, 14, 15) - Phase 02: URL persistence, GitLab tooltip, hide empty KIM (items 2-4) - Phase 03: Top-5 non-TIOBE peek, lunar SVG body texture (items 11-12) - Phase 04: OG social card image generation (item 1, may block on tooling) Defaults baked for the 5 open product questions; flag now if any need changing. KISS: vanilla HTML/CSS/ES modules, no build step, single-file edits only (plus assets/og-card.png).
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# Phase 01 — Style & markup polish
|
||||
|
||||
## Context
|
||||
|
||||
Items 5–10 (CSS one-liners), item 13 (chip `role="list"`/`"listitem"`), items 14/15 (decision documentation as code comments). Lowest risk, fastest win. Single pass through `style.css`, `index.html`, `js/render-elements.js`.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P2 (polish) + cleanup of decision docs
|
||||
- **Status:** pending
|
||||
- **Effort:** ~30 min hand-edit + 10 min manual verify
|
||||
- **Risk:** very low — pure cosmetic / declarative
|
||||
|
||||
## Files modified
|
||||
|
||||
- `style.css` — items 5, 6, 7, 8, 9, 10, 15
|
||||
- `index.html` — item 8 (gold rule under h3 — markup-side none, just CSS), item 14 reference comment
|
||||
- `js/render-elements.js` — item 13 (chip role attributes)
|
||||
|
||||
## Implementation steps
|
||||
|
||||
### 1. Item 5 — drop subtitle italic
|
||||
|
||||
`style.css:81`:
|
||||
|
||||
```css
|
||||
/* Was: .hero .subtitle { ... font-style: italic; }
|
||||
Italic Be Vietnam Pro renders rough on Linux. Use letter-spacing for emphasis. */
|
||||
.hero .subtitle { margin: 0.5rem auto 0; max-width: 38ch; color: var(--muted); letter-spacing: 0.15px; }
|
||||
```
|
||||
|
||||
### 2. Item 6 — anchor underline migration (figcaption + credit)
|
||||
|
||||
`style.css:123-125` and `style.css:185-188`:
|
||||
|
||||
```css
|
||||
/* Replace border-bottom: 1px dotted with native dotted underline.
|
||||
Sub-pixel border at small font sizes can disappear. */
|
||||
.original-image figcaption a {
|
||||
color: var(--accent-ink);
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-color: var(--accent);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.original-image figcaption a:hover {
|
||||
color: var(--gold);
|
||||
text-decoration-color: var(--gold);
|
||||
}
|
||||
.credit a {
|
||||
color: var(--accent-ink);
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-color: var(--accent);
|
||||
text-underline-offset: 2px;
|
||||
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
||||
}
|
||||
.credit a:hover { color: var(--gold); text-decoration-color: var(--gold); }
|
||||
```
|
||||
|
||||
### 3. Item 7 — h3 letter-spacing reduction
|
||||
|
||||
`style.css:161` — change `letter-spacing: 1.2px` → `0.4px` to stop diacritic drift on Vietnamese KIM/MỘC/THUỶ/HOẢ/THỔ.
|
||||
|
||||
### 4. Item 8 — h3 size bump + thin gold rule
|
||||
|
||||
`style.css:161` and a new rule:
|
||||
|
||||
```css
|
||||
.card h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.4px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid color-mix(in oklch, var(--gold) 60%, transparent);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Item 9 — Credit `<ul>` indent
|
||||
|
||||
`style.css:184` — change `margin: 0.5rem 0 0.75rem 1.25rem;` → `margin: 0.5rem 0 0.75rem; padding-left: 1.25rem;` so the bullets sit closer to the centred panel content. Or remove `list-style` and lay out as a 2-row stack (decision: keep bullets, just trim outer indent).
|
||||
|
||||
### 6. Item 10 — pale chip shape edge
|
||||
|
||||
`style.css:232` — bump default border opacity for low-saturation chips. Cleanest universal fix is a slightly stronger border on every colored chip:
|
||||
|
||||
```css
|
||||
.chip {
|
||||
/* … unchanged props … */
|
||||
border: 1px solid rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
```
|
||||
|
||||
(Up from 0.18. Helps Rust `#dea584`-class chips read against cream bg without a per-chip rule.)
|
||||
|
||||
### 7. Item 13 — chip SR semantics
|
||||
|
||||
`js/render-elements.js`:
|
||||
|
||||
- In `renderGrid()` (around line 54): set `chips.setAttribute('role', 'list')` after creating the chips wrapper.
|
||||
- In `buildChip()` (around line 24): set `span.setAttribute('role', 'listitem')`.
|
||||
|
||||
### 8. Item 14 — document 5-card-row decision
|
||||
|
||||
Add a 1-line comment above `style.css:140`:
|
||||
|
||||
```css
|
||||
/* Grid stays auto-fit (not forced 5-col) — the Ngũ-Hành wheel reads at any
|
||||
viewport above 720px without crushing chip names. See plan 260428-1003. */
|
||||
```
|
||||
|
||||
### 9. Item 15 — document AA contrast policy
|
||||
|
||||
Add a 1-line comment above `style.css:228` (the `.chip` block):
|
||||
|
||||
```css
|
||||
/* Chip text contrast policy: best-effort via WCAG luminance picker
|
||||
(render-elements.js pickTextColor). Worst post-fix case ~3.5:1
|
||||
(Swift, MATLAB) is acceptable for decorative chips. */
|
||||
```
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] Item 5: subtitle italic → letter-spacing
|
||||
- [ ] Item 6: anchor underline migration (2 selectors)
|
||||
- [ ] Item 7: h3 letter-spacing 1.2px → 0.4px
|
||||
- [ ] Item 8: h3 size 1.05rem → 1.2rem + gold rule
|
||||
- [ ] Item 9: Credit `<ul>` indent fix
|
||||
- [ ] Item 10: chip border opacity 0.18 → 0.22
|
||||
- [ ] Item 13: `role="list"` / `role="listitem"` on chip elements
|
||||
- [ ] Item 14: 5-card-row decision comment
|
||||
- [ ] Item 15: AA contrast policy comment
|
||||
- [ ] Manual verify: open `python3 -m http.server 8765`, walk through both views (TIOBE, all-langs) on both sources (GitHub, GitLab) at 1280 / 768 / 375 viewport widths
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Visual: subtitle no longer italic; anchor underlines render as dotted text-decoration; h3 reads larger with gold rule under each Ngũ Hành label; chip borders visible against page bg on pale chips.
|
||||
- A11y: NVDA/VoiceOver announces chips as a list with N items per card.
|
||||
- No console errors, no chip overflow, no layout regressions across 1280/768/375 widths.
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
- **Risk:** `text-decoration-style: dotted` renders thicker on Firefox than border-bottom dotted — may visually clash with footer aesthetic. **Mitigation:** if jarring, switch to `text-decoration-style: dashed` or revert to current border-bottom.
|
||||
- **Risk:** h3 size bump pushes card height enough to break the visual rhythm of the grid. **Mitigation:** reduce h3 size to 1.15rem if too tall.
|
||||
- **Risk:** chip role attributes change announce verbosity — some SR users prefer no list. **Mitigation:** keep behind a feature flag if anyone complains; otherwise it's the more semantic choice.
|
||||
|
||||
## Next steps
|
||||
|
||||
→ Phase 02: functional toggles & tooltip (URL persistence, GitLab tooltip, hide empty KIM card in TIOBE view).
|
||||
@@ -0,0 +1,198 @@
|
||||
# Phase 02 — Functional toggles & tooltip
|
||||
|
||||
## Context
|
||||
|
||||
Items 2 (URL persistence), 3 (GitLab tooltip / disparity note), 4 (hide empty KIM card in TIOBE view). All target user-visible behaviour. JS-heavy: `js/main.js`, `js/render-elements.js`, plus markup in `index.html`.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P1 (functional)
|
||||
- **Status:** pending — depends on Phase 01
|
||||
- **Effort:** ~60 min hand-edit + 20 min manual verify across browsers
|
||||
- **Risk:** medium — touches state initialization and render logic; potential regressions if URL-init order is wrong
|
||||
|
||||
## Files modified
|
||||
|
||||
- `js/main.js` — URL read/write, source-change tooltip update, default-view propagation
|
||||
- `js/render-elements.js` — KIM card skip logic when bucket empty AND view=tiobe
|
||||
- `index.html` — markup hook for the GitLab disparity note (one `<span>` slot)
|
||||
|
||||
## Implementation steps
|
||||
|
||||
### Item 2 — URL persistence (3 toggles)
|
||||
|
||||
Pattern: read URL params on `init()`, write on each toggle change. Use `history.replaceState` (no history pollution).
|
||||
|
||||
**1. New helpers in `js/main.js` (after the `SORT_OPTIONS` block, before `refs`):**
|
||||
|
||||
```js
|
||||
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();
|
||||
history.replaceState(null, '', qs ? `?${qs}${window.location.hash}` : window.location.pathname + window.location.hash);
|
||||
}
|
||||
```
|
||||
|
||||
Validation: clamp unknown values back to default. In each toggle's `onChange`, before calling the handler, check the key is in the known options list.
|
||||
|
||||
**2. Update `init()` to seed defaults from URL:**
|
||||
|
||||
```js
|
||||
function init() {
|
||||
const section = document.querySelector('main .elements');
|
||||
refs.grid = document.getElementById('element-grid');
|
||||
refs.legend = section?.querySelector('.legend') ?? null;
|
||||
refs.section = section;
|
||||
refs.debug = document.getElementById('debug-panel');
|
||||
refs.sourceTag = document.getElementById('source-tag');
|
||||
|
||||
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;
|
||||
if (validView === 'all') section?.classList.add('show-all');
|
||||
|
||||
mountSegmentedControl(
|
||||
document.getElementById('source-toggle'),
|
||||
Object.entries(SOURCES).map(([key, s]) => ({ key, label: s.label })),
|
||||
validSource,
|
||||
(key) => { writeQueryParam('source', key, DEFAULT_SOURCE); loadAndRender(key); },
|
||||
'Nguồn dữ liệu màu',
|
||||
);
|
||||
mountSegmentedControl(
|
||||
document.getElementById('view-toggle'),
|
||||
VIEW_OPTIONS,
|
||||
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,
|
||||
validSort,
|
||||
(key) => { writeQueryParam('sort', key, 'tiobe'); currentSort = key; applySortAndRender(); },
|
||||
'Sắp xếp ngôn ngữ',
|
||||
);
|
||||
loadAndRender(validSource);
|
||||
}
|
||||
```
|
||||
|
||||
Note: view-toggle handler now also calls `applySortAndRender()` so the empty-card hide logic from Item 4 picks up the correct view state.
|
||||
|
||||
### Item 3 — GitLab disparity note + source-toggle tooltip
|
||||
|
||||
**1. Add a slot in `index.html:43` near the `#source-tag`:**
|
||||
|
||||
```html
|
||||
<p class="mode-tag" id="source-tag">Nguồn: GitHub Linguist <span class="source-note" id="source-note" hidden></span></p>
|
||||
```
|
||||
|
||||
**2. Style the note in `style.css` (append to `.mode-tag` block):**
|
||||
|
||||
```css
|
||||
.source-note {
|
||||
display: inline-block;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0.05rem 0.45rem;
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
**3. Update `loadAndRender()` in `js/main.js` to swap the note text:**
|
||||
|
||||
```js
|
||||
if (refs.sourceTag) refs.sourceTag.firstChild.nodeValue = `Nguồn: ${source.label} Linguist `;
|
||||
const note = document.getElementById('source-note');
|
||||
if (note) {
|
||||
if (sourceKey === 'gitlab') {
|
||||
note.hidden = false;
|
||||
note.textContent = '91 ngôn ngữ — palette riêng so với GitHub';
|
||||
} else {
|
||||
note.hidden = true;
|
||||
note.textContent = '';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Add `title` attr on the source-toggle group** — set in `mountSegmentedControl` via the existing `ariaLabel` plumbing or set directly in `init()`:
|
||||
|
||||
```js
|
||||
document.getElementById('source-toggle').setAttribute('title', 'GitHub có 664 ngôn ngữ; GitLab có 91 và dùng palette riêng');
|
||||
```
|
||||
|
||||
### Item 4 — Hide empty KIM card in TIOBE view
|
||||
|
||||
`js/render-elements.js` `renderGrid()`:
|
||||
|
||||
```js
|
||||
export function renderGrid(buckets, mountEl) {
|
||||
if (!mountEl) return;
|
||||
const showAll = mountEl.closest('.elements')?.classList.contains('show-all') ?? false;
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const { key, label } of ELEMENTS) {
|
||||
const langs = buckets[key] || [];
|
||||
/* In TIOBE view, hide cards with no languages — empty "0 ngôn ngữ"
|
||||
headings look broken. All-langs view keeps every card so the
|
||||
Ngũ Hành wheel stays visible. */
|
||||
if (!showAll && langs.length === 0) continue;
|
||||
/* … rest unchanged … */
|
||||
}
|
||||
mountEl.replaceChildren(fragment);
|
||||
}
|
||||
```
|
||||
|
||||
Caveat: if URL-init applies `show-all` to the section AFTER `renderGrid` is first called, the card list will be wrong for one frame. Phase 02 step 1 init order already adds `show-all` BEFORE `loadAndRender`, so this is safe.
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] Item 2: URL helpers (`readQueryState`, `writeQueryParam`)
|
||||
- [ ] Item 2: `init()` reads URL state, seeds toggles, writes on each change
|
||||
- [ ] Item 2: validation — clamp unknown URL values to defaults
|
||||
- [ ] Item 3: `#source-note` slot in `index.html`
|
||||
- [ ] Item 3: `.source-note` CSS pill style
|
||||
- [ ] Item 3: `loadAndRender` updates note text on source change
|
||||
- [ ] Item 3: `title` attr on `#source-toggle`
|
||||
- [ ] Item 4: hide empty cards in TIOBE view via `showAll` check in `renderGrid`
|
||||
- [ ] Manual verify: load `?s=gitlab&v=all&o=hue` directly, share back to a peer, confirm state restores
|
||||
- [ ] Manual verify: switch GitHub ↔ GitLab, confirm note shows/hides
|
||||
- [ ] Manual verify: TIOBE view shows MỘC/THUỶ/HOẢ/THỔ only; all-langs shows all 5
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Reload preserves all 3 toggle states.
|
||||
- Sharing the URL recreates the same view in a fresh browser.
|
||||
- Empty KIM card disappears in TIOBE view (since KIM has 0 TIOBE langs); reappears in all-langs.
|
||||
- GitLab selection shows a small "91 ngôn ngữ — palette riêng so với GitHub" pill next to the source tag.
|
||||
- Default URL stays clean (no params) when all toggles are at default.
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
- **Risk:** URL-init order: if `show-all` class is added after first render, cards flicker. **Mitigation:** apply class before `loadAndRender(validSource)` (already in step 1).
|
||||
- **Risk:** `firstChild.nodeValue` overwrite assumes the `#source-tag` text node is first child. **Mitigation:** if the markup ever moves the `<span>` first, use `textContent` and rebuild the inner span — or restructure to two spans (tag + note) and set tag's `textContent` directly.
|
||||
- **Risk:** GitLab pill may wrap on mobile. **Mitigation:** the parent `.mode-tag` already centres + breaks; `.source-note` is `inline-block` with small padding.
|
||||
- **Risk:** Hiding empty cards changes the grid item count, which `auto-fit minmax(220px, 1fr)` re-flows — at desktop 880 px, 4 visible cards span the full width nicely (no awkward gap).
|
||||
|
||||
## Next steps
|
||||
|
||||
→ Phase 03: additive features (Top-5 non-TIOBE peek, lunar SVG body texture).
|
||||
@@ -0,0 +1,147 @@
|
||||
# Phase 03 — Additive features
|
||||
|
||||
## Context
|
||||
|
||||
Items 11 (Top-5 non-TIOBE peek in TIOBE view) and 12 (subtle Lunar-New-Year SVG body texture). Both additive — they enhance the page without changing existing behaviour. Lower priority than Phase 02; defer if time-constrained.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P3 (additive)
|
||||
- **Status:** pending — depends on Phase 02
|
||||
- **Effort:** ~45 min for Top-5 peek (incl. data math), ~20 min for SVG texture (incl. opacity tuning)
|
||||
- **Risk:** low — all changes are additive; can revert without breaking core flow
|
||||
|
||||
## Files modified
|
||||
|
||||
- `js/main.js` — compute "popularity rank" proxy (alphabetical for now; no real popularity signal in data)
|
||||
- `js/render-elements.js` — add peek slot to chip rendering
|
||||
- `style.css` — peek styling (dimmed chips), body bg SVG
|
||||
- `assets/lunar-pattern.svg` — new file (small, ~1 KB)
|
||||
|
||||
## Implementation steps
|
||||
|
||||
### Item 11 — Top-5 non-TIOBE peek
|
||||
|
||||
In TIOBE view, each card currently shows just its TIOBE-ranked chips. Peek = show 5 dimmed non-TIOBE chips beside the pinned ones, alphabetical (no popularity data in our JSON). User can read them but the visual hierarchy stays TIOBE-first.
|
||||
|
||||
**1. `js/main.js` — pre-compute peek list per bucket** (after `classifyAll`, before `applySortAndRender`):
|
||||
|
||||
```js
|
||||
const PEEK_COUNT = 5;
|
||||
|
||||
function annotatePeeks(buckets) {
|
||||
for (const langs of Object.values(buckets)) {
|
||||
const nonTiobe = langs.filter((l) => !l.rank);
|
||||
nonTiobe.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
||||
const peekSet = new Set(nonTiobe.slice(0, PEEK_COUNT).map((l) => l.name));
|
||||
for (const l of langs) {
|
||||
if (!l.rank && peekSet.has(l.name)) l.peek = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Call `annotatePeeks(buckets)` right after `classifyAll(data)` resolves.
|
||||
|
||||
**2. `js/render-elements.js` — emit `chip-peek` class:**
|
||||
|
||||
```js
|
||||
function buildChip(name, color, { rank = null, peek = false } = {}) {
|
||||
const span = document.createElement('span');
|
||||
let cls = 'chip';
|
||||
if (rank) cls += ' chip-tiobe';
|
||||
else if (peek) cls += ' chip-peek';
|
||||
else cls += ' chip-other';
|
||||
span.className = cls;
|
||||
/* … rest unchanged … */
|
||||
}
|
||||
```
|
||||
|
||||
And in `renderGrid()` (around line 56):
|
||||
|
||||
```js
|
||||
for (const { name, color, rank, peek } of langs)
|
||||
chips.appendChild(buildChip(name, color, { rank, peek }));
|
||||
```
|
||||
|
||||
**3. `style.css` — peek visibility rule** (after the existing `.elements .chip-other` line):
|
||||
|
||||
```css
|
||||
/* Peek: in TIOBE view, surface 5 alphabetical non-TIOBE chips per card,
|
||||
dimmed so they don't compete with the pinned TIOBE entries. */
|
||||
.elements .chip-peek { display: inline-flex; opacity: 0.55; }
|
||||
.elements.show-all .chip-peek { opacity: 1; }
|
||||
```
|
||||
|
||||
### Item 12 — Lunar SVG body texture
|
||||
|
||||
A faint cloud / 雲紋-style repeating pattern at low opacity. Hand-craft a small inline-able SVG so we don't add a binary asset.
|
||||
|
||||
**1. New file `assets/lunar-pattern.svg`** — a 120×120 pattern of stylised cloud/wave curves:
|
||||
|
||||
```xml
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
|
||||
<g fill="none" stroke="#c79b3c" stroke-width="0.8" stroke-linecap="round" opacity="0.6">
|
||||
<path d="M5,20 Q15,10 25,20 T45,20 T65,20 T85,20 T105,20" />
|
||||
<path d="M5,55 Q15,45 25,55 T45,55 T65,55 T85,55 T105,55" />
|
||||
<path d="M5,90 Q15,80 25,90 T45,90 T65,90 T85,90 T105,90" />
|
||||
<circle cx="30" cy="35" r="2" />
|
||||
<circle cx="75" cy="70" r="2" />
|
||||
<circle cx="100" cy="40" r="2" />
|
||||
</g>
|
||||
</svg>
|
||||
```
|
||||
|
||||
(Tune curves and dot positions to taste.)
|
||||
|
||||
**2. `style.css:46-53` — body bg layering** (extend existing radial gradient):
|
||||
|
||||
```css
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background:
|
||||
url('./assets/lunar-pattern.svg') repeat,
|
||||
radial-gradient(1200px 600px at 50% -120px, #fff5dd 0%, transparent 60%),
|
||||
var(--bg);
|
||||
background-size: 120px 120px, auto, auto;
|
||||
/* SVG already has 0.6 internal opacity on stroke; this layering puts
|
||||
it at ~3% effective contrast against cream — barely visible, sets
|
||||
the lunar-new-year mood without distracting. */
|
||||
color: var(--fg);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
```
|
||||
|
||||
Tune the SVG `opacity` attribute or stroke colour if too prominent.
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] Item 11: `annotatePeeks()` helper + integration into `loadAndRender`
|
||||
- [ ] Item 11: `buildChip` accepts `peek` flag; emits `chip-peek` class
|
||||
- [ ] Item 11: CSS rule for `.chip-peek` (visible-dimmed in TIOBE, full in all-langs)
|
||||
- [ ] Item 12: hand-craft `assets/lunar-pattern.svg` (~1 KB)
|
||||
- [ ] Item 12: layer SVG into `body` background per CSS above
|
||||
- [ ] Manual verify: TIOBE view shows TIOBE chips at full strength + 5 peek chips dimmed
|
||||
- [ ] Manual verify: all-langs view restores all chips at full opacity (no regression)
|
||||
- [ ] Manual verify: SVG bg renders at desktop / mobile, doesn't fight text contrast (still WCAG AA)
|
||||
- [ ] Manual verify: print stylesheet — page still readable (SVG should drop or stay subtle)
|
||||
|
||||
## Success criteria
|
||||
|
||||
- TIOBE cards show ~9–12 chips each (4–7 TIOBE pinned + 5 peek), no overflow.
|
||||
- Peek chips are clearly secondary (lower opacity) but readable.
|
||||
- Body bg has a faint lunar-new-year pattern; cream + accent red read unchanged.
|
||||
- No measurable contrast regression (page bg still ≥7:1 against `--fg`).
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
- **Risk:** No popularity signal in `data/*.json` — alphabetical peek may surface obscure langs (e.g. "Apex" before "Ada"). **Mitigation:** alphabetical is honest; users get a deterministic preview. Future: rank by GitHub language usage data if added.
|
||||
- **Risk:** SVG bg slows page paint. **Mitigation:** SVG is <1 KB, repeats natively in CSS; no JS cost.
|
||||
- **Risk:** SVG visible at low-vision-mode / high contrast. **Mitigation:** wrap in `@media (prefers-contrast: more) { body { background-image: none; } }` if needed.
|
||||
- **Risk:** Peek chips visually lift cards taller — KIM card with 0 TIOBE + 5 peek = 5 small chips. **Mitigation:** with Phase 02 item 4 hide-empty logic, KIM TIOBE bucket count goes from 0 to 5 (peek), so it's no longer empty — card now shows. Reconcile: the hide rule should check `langs.length === 0` AFTER peek annotation, not before. Adjust Phase 02 step 4 if peek shipped: filter on `langs.filter(l => l.rank || l.peek).length === 0` in TIOBE view, OR change Item 4 to "hide if no TIOBE rank langs AND not show-all" — explicit. Recommend: in TIOBE view, hide a card only if it has zero TIOBE chips, regardless of peek (so KIM stays hidden in TIOBE view; peek alone doesn't justify a card).
|
||||
|
||||
## Next steps
|
||||
|
||||
→ Phase 04: OG social card image generation (independent — can run before or in parallel).
|
||||
@@ -0,0 +1,94 @@
|
||||
# Phase 04 — OG social card image generation
|
||||
|
||||
## Context
|
||||
|
||||
Item 1 — `index.html:22` `og:image` still points at `assets/ngon-ngu-lap-trinh-phong-thuy.png` (the 2018 Vietnamese-meme original). When the page is shared on Facebook / Twitter / Zalo, the preview shows the 2018 design, not the current Ngũ Hành cards. Need a 1200×630 image that reflects the current site.
|
||||
|
||||
Independent of Phases 01–03 — can run in parallel or last.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Priority:** P1 (functional / brand)
|
||||
- **Status:** pending — known blocker on tooling
|
||||
- **Effort:** 30 min if tooling works; +60 min for SVG fallback
|
||||
- **Risk:** medium — host has no working browser binary for screenshot capture; previous attempts failed (cached puppeteer chrome on aarch64 host is x86_64 ELF)
|
||||
|
||||
## Files modified
|
||||
|
||||
- `assets/og-card.png` — new file (target ~50–150 KB, 1200×630 PNG)
|
||||
- `index.html:22` — update `og:image` content path; add `og:image:width` / `og:image:height` meta tags
|
||||
- `index.html:23` — add `og:image:alt`
|
||||
|
||||
## Implementation steps
|
||||
|
||||
### Step 1 — Try the `design` skill (preferred)
|
||||
|
||||
The `design` skill ships an HTML→PNG pipeline. Compose a single-page HTML mock at 1200×630 that includes:
|
||||
|
||||
- Title "Phong thuỷ ngôn ngữ lập trình"
|
||||
- Subtitle "Ngũ Hành tương sinh — code cho hợp tuổi, hợp mệnh"
|
||||
- 5 element-tinted cards (KIM/MỘC/THUỶ/HOẢ/THỔ) with 4–6 representative TIOBE chips each
|
||||
- Lunar-new-year cream + gold accent palette (matches `style.css` tokens)
|
||||
- Site URL or repo handle in a corner
|
||||
|
||||
Invoke via `Skill` with `design` and ask for "social card 1200x630, programming-fengshui Ngũ Hành theme, see plan 260428-1003-implement-all-remaining/phase-04-og-image-generation.md for spec". Output to `assets/og-card.png`.
|
||||
|
||||
### Step 2 — Fallback A: hand-craft an SVG, rasterise locally
|
||||
|
||||
If the `design` skill is unavailable or produces something off-brand:
|
||||
|
||||
1. Hand-write `assets/og-card.svg` at viewBox 1200×630 using the same colour tokens as `style.css` (cream bg, accent red title, 5 element bands).
|
||||
2. Rasterise via ImageMagick CLI (already on the host — already used by `media-processing` skill):
|
||||
```bash
|
||||
convert -density 144 -background "#fdf6ec" assets/og-card.svg -resize 1200x630 assets/og-card.png
|
||||
```
|
||||
3. If `convert` is unavailable or the rendered output is poor: ship the SVG as `og:image` directly. Most platforms accept SVG; some (Facebook) require raster — accept the trade-off.
|
||||
|
||||
### Step 3 — Fallback B: use existing 2018 image with a new banner
|
||||
|
||||
If neither tool path works, reuse the 2018 image but composite a small "Bản web 2026" badge top-right via ImageMagick. Output to `assets/og-card.png`. Documents that the page is a 2026 reinterpretation, not just a re-host of the original.
|
||||
|
||||
### Step 4 — Update `index.html` meta tags
|
||||
|
||||
```html
|
||||
<meta property="og:image" content="assets/og-card.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Phong thuỷ ngôn ngữ lập trình — 5 thẻ Ngũ Hành (Kim, Mộc, Thuỷ, Hoả, Thổ) với các ngôn ngữ TIOBE Top 20" />
|
||||
<meta name="twitter:image" content="assets/og-card.png" />
|
||||
```
|
||||
|
||||
(Add `twitter:image` in addition to the existing `twitter:card="summary_large_image"`.)
|
||||
|
||||
### Step 5 — Verify
|
||||
|
||||
- Use Twitter Card Validator: https://cards-dev.twitter.com/validator (need a public URL — paste to user once deployed)
|
||||
- Use Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
|
||||
- Locally: open `index.html` and inspect `<head>`, confirm `og:image` resolves to a 1200×630 PNG.
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] Step 1: try `design` skill HTML→PNG with brand spec
|
||||
- [ ] If fails — Step 2: hand-craft `assets/og-card.svg` + rasterise via ImageMagick
|
||||
- [ ] If fails — Step 3: 2018 image + "Bản web 2026" badge
|
||||
- [ ] Step 4: update `index.html` `og:image` + add width/height/alt + twitter:image
|
||||
- [ ] Step 5: verify via Twitter / Facebook validators after deploy
|
||||
- [ ] Manual: confirm `assets/og-card.png` <200 KB (Facebook recommended)
|
||||
|
||||
## Success criteria
|
||||
|
||||
- `og:image` URL resolves to a 1200×630 PNG <200 KB.
|
||||
- Image visually reflects the current Ngũ Hành cards (not the 2018 source).
|
||||
- Alt text in `og:image:alt` describes the image in Vietnamese.
|
||||
- Twitter card / Facebook preview shows the new image after refreshing the URL on each platform.
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
- **Risk:** `design` skill output may not match the on-page typography (different fonts available in the renderer). **Mitigation:** spec the renderer to use a system-stack font (Be Vietnam Pro fallback to system-ui), accept slight visual drift.
|
||||
- **Risk:** Hand-crafted SVG looks bland vs the on-page design. **Mitigation:** budget extra time; iterate on element-card visual weight.
|
||||
- **Risk:** ImageMagick `convert` may not be installed. **Mitigation:** check `which convert` first; if absent, ask user to install or fall back to ship SVG directly as og:image.
|
||||
- **Risk:** New og:image cached by social platforms — won't update for shared links. **Mitigation:** documented in Step 5 — use platform debuggers to force re-scrape after deploy.
|
||||
|
||||
## Next steps
|
||||
|
||||
→ All 4 phases complete. Final session: update `plans/todo.md` to mark items closed, and either archive this plan dir or move it to `plans/done/`.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Implement All Remaining UX/UI Items
|
||||
slug: 260428-1003-implement-all-remaining
|
||||
status: in-progress
|
||||
created: 2026-04-28
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
---
|
||||
|
||||
# Implement All Remaining UX/UI Items
|
||||
|
||||
Single pass through every remaining item in `plans/todo.md` and the post-cleanup `plans/reports/ui-ux-260427-2043-fengshui-page-review.md`. 15 items, 4 phases.
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Title | Status | Items |
|
||||
|---|-------|--------|-------|
|
||||
| 01 | Style & markup polish | pending | items 5–10, 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) |
|
||||
| 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) |
|
||||
|
||||
## Default decisions (baked into the plan; flag now if any need to change)
|
||||
|
||||
1. **Empty KIM in TIOBE view** → hide the card entirely. The 5-element wheel is preserved in all-langs view. Cleaner than "0 ngôn ngữ".
|
||||
2. **5-card-row symbolism** → keep `auto-fit minmax(220px, 1fr)`. Page max-width stays 880 px.
|
||||
3. **URL persistence scope** → 3 toggles only via query params (`?source=…&view=…&sort=…`). No anchor-scroll state.
|
||||
4. **GitLab tooltip mechanism** → small visible inline note next to source toggle that swaps text when GitLab is selected ("91 ngôn ngữ — palette riêng so với GitHub"). Plus `title` attr on the source-toggle group.
|
||||
5. **AA contrast policy** → keep current best-effort (post-fix worst case Swift/MATLAB ~3.5–4.0:1). Document in a one-line CSS comment near `.chip`.
|
||||
6. **OG image** → try `design` skill HTML→PNG first. Fallback: hand-crafted static SVG of the 5-element ribbon + title text.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 02 follows Phase 01 (HTML markup stable before JS wiring).
|
||||
- Phase 03 follows Phase 02 (uses URL state pattern for shareable peek state).
|
||||
- Phase 04 independent; runs in parallel or last.
|
||||
|
||||
## Constraints
|
||||
|
||||
- No build step. Vanilla HTML/CSS/ES modules only.
|
||||
- No webfont fetches; system stack only.
|
||||
- All edits target existing files. New file allowed only for `assets/og-card.png`.
|
||||
- Manual verification via `python3 -m http.server 8765` after each phase.
|
||||
- Re-run `js/classify-element.test.html` if any classifier change (none planned).
|
||||
|
||||
## References
|
||||
|
||||
- Live review: `plans/reports/ui-ux-260427-2043-fengshui-page-review.md`
|
||||
- Live todo: `plans/todo.md`
|
||||
- Algorithm spec: `plans/reports/researcher-260427-0854-nguhanh-color-classifier.md`
|
||||
- KIM rule rationale: `plans/reports/brainstorm-260427-1046-kim-rebalance.md`
|
||||
Reference in New Issue
Block a user