@minhspark/spacing-skill 2.12.1 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +18 -18
- package/.claude-plugin/marketplace.json +20 -20
- package/.claude-plugin/plugin.json +22 -22
- package/.codex-plugin/plugin.json +6 -6
- package/CHANGELOG.md +27 -14
- package/LEARNINGS.md +233 -188
- package/LICENSE +21 -21
- package/README.md +245 -223
- package/gemini-extension.json +6 -6
- package/package.json +22 -22
- package/skills/spacing-skill/SKILL.md +16 -0
package/LEARNINGS.md
CHANGED
|
@@ -1,188 +1,233 @@
|
|
|
1
|
-
# Field Learnings — `design-spacing-rhythm`
|
|
2
|
-
|
|
3
|
-
Append-only journal for the **§15 Continuous-Improvement loop** in
|
|
4
|
-
`skills/spacing-skill/SKILL.md`. Every real spacing/alignment edit lands here as
|
|
5
|
-
one dated entry; lessons that generalize are distilled into the skill (with a
|
|
6
|
-
`CHANGELOG.md` bump) and then pruned here to a one-line pointer.
|
|
7
|
-
|
|
8
|
-
> Accessibility floors (§9) outrank any preference recorded in this file. Log
|
|
9
|
-
> only edits you actually made — **evidence, not vibes.**
|
|
10
|
-
|
|
11
|
-
## How to add an entry
|
|
12
|
-
|
|
13
|
-
1. Make (and ideally render) the spacing/alignment edit.
|
|
14
|
-
2. Append one entry at the **top** of *Entries* using the template below.
|
|
15
|
-
3. If the verdict is `gap` or `refinement` **and** the lesson generalizes, fold
|
|
16
|
-
it into the right section of `SKILL.md`, bump `CHANGELOG.md`, then replace the
|
|
17
|
-
entry here with a one-line pointer: `→ folded into §n @ vX.Y.Z`.
|
|
18
|
-
|
|
19
|
-
### 2026-09-
|
|
20
|
-
|
|
21
|
-
- Space Read:
|
|
22
|
-
- Did:
|
|
23
|
-
- Taught:
|
|
24
|
-
- Verdict: gap(§
|
|
25
|
-
- Action: folded into §
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- Space Read:
|
|
78
|
-
- Did:
|
|
79
|
-
- Taught:
|
|
80
|
-
- Verdict: gap(§4.
|
|
81
|
-
- Action: folded into §4.
|
|
82
|
-
|
|
83
|
-
### 2026-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
188
|
-
-
|
|
1
|
+
# Field Learnings — `design-spacing-rhythm`
|
|
2
|
+
|
|
3
|
+
Append-only journal for the **§15 Continuous-Improvement loop** in
|
|
4
|
+
`skills/spacing-skill/SKILL.md`. Every real spacing/alignment edit lands here as
|
|
5
|
+
one dated entry; lessons that generalize are distilled into the skill (with a
|
|
6
|
+
`CHANGELOG.md` bump) and then pruned here to a one-line pointer.
|
|
7
|
+
|
|
8
|
+
> Accessibility floors (§9) outrank any preference recorded in this file. Log
|
|
9
|
+
> only edits you actually made — **evidence, not vibes.**
|
|
10
|
+
|
|
11
|
+
## How to add an entry
|
|
12
|
+
|
|
13
|
+
1. Make (and ideally render) the spacing/alignment edit.
|
|
14
|
+
2. Append one entry at the **top** of *Entries* using the template below.
|
|
15
|
+
3. If the verdict is `gap` or `refinement` **and** the lesson generalizes, fold
|
|
16
|
+
it into the right section of `SKILL.md`, bump `CHANGELOG.md`, then replace the
|
|
17
|
+
entry here with a one-line pointer: `→ folded into §n @ vX.Y.Z`.
|
|
18
|
+
|
|
19
|
+
### 2026-09-24 — CongCuDoiTen / WinForms results table with a new image column — a resize is a round trip: sizes the toolkit measured while narrow stay cached when the table widens → folded into §8.I
|
|
20
|
+
|
|
21
|
+
- Space Read: offline WinForms tool, results table (checkbox · corner-number picture · catalog no. · name · status · file · score) beside a preview and a catalog pane · balanced · rows 54 px (48 px picture + 6) · DENSITY 6 · RIGOR 8 · every row the same height unless its text truly needs more; the three text columns keep their ratio 46 : 30 : 18 at every window size from 1000×600 to 1680×940.
|
|
22
|
+
- Did: added a 120×48 picture column that shows at full size when the table has room, draws zoomed with a short header when it is tight, and hides below 72 px; the thresholds use the table's client width minus the vertical scrollbar, so the column does not flip when the scrollbar appears. Then drove a narrow → wide → narrow round trip at five window sizes and measured every row, including rows scrolled out of view. Two cached sizes survived: (1) rows that grew to 78 px at 1000 px kept 78 px at 1680 px where they needed the 54 px floor — `AutoResizeRows(DisplayedCellsExceptHeaders)` changed nothing, because while an autosize mode is active WinForms only caches heights set through the public API; (2) the fill columns came back as 216 · 151 · 112 px instead of 226 · 157 · 96 px at the same 1366 px, because the File column hit its minimum at 1000 px and the grid kept the adjusted working ratio. Fix: one pass 150 ms after a text column stops changing width — nudge one FillWeight up and back (the grid redistributes from the weights; now 235 · 153 · 91 → 234 · 153 · 92, ≤ 2 px of rounding), then autosize mode to None, `AutoResizeRows(AllCellsExceptHeaders)`, mode back (39 ms at 50 rows, 131 ms at 300; a drag step stays ~15 ms). Two follow-ups from an adversarial review: resetting the ratio on every resize step made window drags ~58% slower at 300 rows, so it moved into the settle pass; and the first version of that pass re-armed itself through its own width-changed events, firing every 150 ms forever (the harness waiting for an idle table timed out at 240 s) — the pass now ignores the events it raises and re-measures only when widths really moved. Hiding the picture column while it held the current cell also cleared the grid's current row (F8 and arrow keys restarted from the ends), so the current cell moves to the name cell first, keeping selection and scroll.
|
|
23
|
+
- Taught: a layout checked once at each size can still be wrong after the user resizes, because toolkits keep what they measured while narrow: row heights of rows that were visible, auto-sized controls that only grow, fill ratios adjusted at a minimum width. Virtualized lists on the web cache measured heights the same way. Re-measure every item, off-screen ones included, once the width settles; test the round trip, not each size; and make sure the settle pass cannot trigger itself.
|
|
24
|
+
- Verdict: gap(§8) — second CongCuDoiTen datapoint for "sizes stay cached across a resize" (the first was the `GrowOnly` toolbar in the entry below).
|
|
25
|
+
- Action: folded into §8.I (new), the §13.C Responsive checklist and anti-pattern #25 @ v2.13.0. Verified by the tool's harness, 58/58 at 1000×600, 1200×700, 1280×720, 1366×768 and 1680×940.
|
|
26
|
+
### 2026-09-24 — CongCuDoiTen / WinForms three-pane window (table · preview · catalog) — a label that grows with data needs its widest text reserved, and "fits?" must be measured against the slot, not the auto-sized control
|
|
27
|
+
|
|
28
|
+
- Space Read: offline WinForms tool, three panes side by side under one toolbar band · balanced · STEP 3 px control margins, 12 px outer gutter · DENSITY 6 · RIGOR 8 · pane headings and second rows must share one line across all three panes; nothing may clip or wrap at 1000×600 (minimum) … 1680×940.
|
|
29
|
+
- Did: measured control bounds in the running form at 1000/1366/1680/maximized instead of eyeballing. At 1680×940 every outer edge sits at x=12 / x=1652, the toolbar and both pane titles share centre y=134, second rows start at y=157, picture and catalog box end at y=790. Three dynamic-text fixes: (1) the table toolbar wrapped (42 → 80 px, table header dropped 38 px below the other panes) as soon as 2 rows were selected, because `Chọn loại tài liệu…` became `…cho 2 dòng…` (127 → 193 px) and New-Nut buttons use AutoSizeMode `GrowOnly`, so the bar stayed wrapped after the selection went back to one row; kept the label constant (count moved to the tooltip) and pinned the other counter button to the width of its widest caption (`▶ Cần xem: 999 dòng (F8)`). (2) The step strip, the name-column header and the footer count line got short variants chosen by measuring the long text against the width the parent cell allots (`TableLayoutPanel.GetColumnWidths`), not against the control's own auto-sized width. (3) A rules line measured 255 px by `TextRenderer.MeasureText` still wrapped under `MaximumSize.Width = 258`, because an AutoSize `Label` adds ~7 px around its text; the fit test now adds 8 px.
|
|
30
|
+
- Taught: (a) text that changes with data (counts, selection size) inside a no-wrap row must either stay constant or reserve its widest value, and `GrowOnly` autosize never gives the space back, so one transient long label breaks alignment for the rest of the session. (b) Choosing a short text variant by comparing with the control's own auto-sized width is self-referential: once short, the control shrinks and the long text never fits again; compare with the slot the container gives. (c) A text-fits check must include the host control's own padding, not just the renderer's glyph width.
|
|
31
|
+
- Verdict: covered(§4.C — one shared band across panes, measured) · refinement(§9.D/§14 — verify dynamic text at its widest value, and measure fit against the allotted slot including control padding).
|
|
32
|
+
- Action: pending pattern — first datapoint for the autosize-`GrowOnly` and self-referential-fit traps; per §15.D fold into §9.D on a repeat. Verified by the tool's harness (43/43 at 1366×768 and 1680×940) plus probes at 1000–1680 px wide. → the `GrowOnly` half repeated the same day (grid rows and fill ratios kept across a resize) and was folded into §8.I @ v2.13.0.
|
|
33
|
+
### 2026-09-22 — PCC4SH / kho-vat-tu.xlsm bảng lịch sử hiện tại chỗ dưới bảng tồn kho và dưới tờ phiếu — "below the last filled cell" is not "below the layout"
|
|
34
|
+
|
|
35
|
+
- Space Read: Excel workbook; a button pastes a hidden history table underneath whatever the reader is looking at (stock list or a 02-VT invoice form) and pastes it away again on the next press · balanced · STEP 1 row.
|
|
36
|
+
- Did: placed the pasted table `last used row + 2 blank rows + 1`, where "last used" came from `Cells.Find("*")`. On the invoice sheet that landed the table on row 36 — the signer-name row, which is *inside the print area* but empty on the blank template — and collapsing the table deleted that row. Fixed by taking `max(last filled row, bottom of PageSetup.PrintArea)` as the anchor (invoice: 37 → table starts at 39). Second bug: the italic hint under the title inherited a right-aligned column A on the stock sheet, so the overflow ran left off the sheet and read as "bấm để thu gọn bảng này." (front half missing); forced `xlLeft`.
|
|
37
|
+
- Taught: the bottom of a form is defined by its **print/layout box**, not by its last non-empty cell — a template's reserved rows are blank by design. And overflow direction follows text alignment: a long label in a narrow left column must be left-aligned or it spills into nothing.
|
|
38
|
+
- Verdict: refinement(§4 — a reserved-but-empty slot still occupies space; anchor "after" on the declared box) · covered(§9/§14 — render with real data; the blank template hid both bugs, the COM run with three filled rows exposed them).
|
|
39
|
+
- Action: `ModKho.HienBangTaiCho` anchors on `max(Find, PrintArea)`, hint cell `HorizontalAlignment = xlLeft`; verified by running the macros through Excel COM and `CopyPicture` (PR #425). Not bumping the skill version — folding into §4 wording later if it recurs.
|
|
40
|
+
|
|
41
|
+
### 2026-09-22 — PCC4SH / kho-vat-tu.xlsm trang Tồn kho + nút quay về — a width table keyed by column letter drifts from a header list keyed by name
|
|
42
|
+
|
|
43
|
+
- Space Read: Excel workbook the warehouse staff download from the web (Rules §0.49: must mirror the web table) · balanced · STEP 1 char (Excel column units) · DENSITY 5 · RIGOR 7 · column widths by content, buttons 150×22pt in a 26pt row
|
|
44
|
+
- Did: the owner reported "vẫn còn mấy chỗ bị mất chữ". Measured every button caption against its box (TextRenderer vs `btn.Width`) and every cell in the first 12 rows on all 22 sheets: no caption was cut, no title clipped. Then screenshotted each sheet through Excel COM (`Range.CopyPicture`, which includes form buttons the PDF export drops) and the defect was on **Tồn kho**: the name column ("Tên nhãn hiệu và quy cách") was 14 chars wide and "ĐVT" was 50 — the `Set-Widths @{A=6;B=18;C=14;D=50;…}` table had been written for an older header order and never re-read when a column was removed, so it was off by one. Set `C=50, D=8, E=14, F=12, G=18, H=16` and re-aimed the alignment ranges the same way (names left, numbers right, ĐVT centre). Also the "◀ VỀ TỒN KHO" button on the ledger sheets was 20pt tall around 11pt bold text (26px box, 20px glyphs): raised to 22pt, still inside the 26pt row.
|
|
45
|
+
- Taught: a width table addressed **positionally** (column letters) next to a header list addressed **by name** is two sources of truth, and removing one header silently shifts every width after it by one column. The template looked fine empty — headers wrap, so the narrow name column still read "Tên nhãn hiệu và quy cách" on two lines — and only the *filled* download showed names truncated. The same file already had a by-name `Get-KhoColSpec` table for the other sheets; Tồn kho bypassed it. Second: measuring text-vs-box catches clipped captions but not *wrong* boxes; a screenshot of the rendered artifact with real content is the only check that sees a column that is merely too narrow for what will go in it.
|
|
46
|
+
- Verdict: covered(§1 — widths by content, not one number) · refinement(§14 — verify on the rendered artifact with real content; an empty template hides a narrow column)
|
|
47
|
+
- Action: pinned in `_kho-xlsm.test.ts` (name column ≥40, ĐVT ≤10, name widest). Switching the sheet to by-name specs is the follow-up, not done yet. No SKILL bump — §14 already says render-and-measure; the new wrinkle is only that an empty template is not the artifact.
|
|
48
|
+
|
|
49
|
+
### 2026-09-20 — spacing-skill / video giới thiệu repo (một template, hai khung 1920×1080 + 1080×1920) — an auto-margin with no slack is `0`, and a burnt-in caption is a safe area
|
|
50
|
+
|
|
51
|
+
- Space Read: title-and-panel slide deck for a 2-minute repo video, one HTML template rendered at both aspect ratios · balanced · STEP 8 · DENSITY 3 · RIGOR 7 · 64px background grid; ladder 8 → 24 (inside panels) → 64 (column gap)
|
|
52
|
+
- Did: three spacing bugs, each caught only by looking at the rendered artifact. (1) Titles were set `line-height:1.08`; on Vietnamese all-caps two-liners the `Ề` of "ĐỀU NHAU" collided with "PADDING" above it — the skill's own anti-pattern #15, committed while building a video that *teaches* anti-pattern #15. Raised to `1.15`. (2) The copy column's footer used `margin-top:auto`; it rendered 44px clear in the landscape build and **0px** in the portrait build, footer jammed against the subtitle — portrait made that column `flex:none`, so there was no free space for `auto` to eat. Replaced with a literal `44px`. (3) Burnt-in ASS captions (46px, `MarginV 56`) landed on top of the panels: the page reserved only 72px of block-end padding, so the caption box covered the last two lines of the SLOP panel's note and, in portrait, the `line-height 1.15` badge — the exact evidence the slide existed to show. Reserved a real band: block-end padding 72 → **184px** (landscape) and 88 → **208px** (portrait), caption `MarginV` 56/64.
|
|
53
|
+
- Taught: two failure modes that share one shape — *a spacing value that is computed from leftover space, or from an assumption about empty space, is not a spacing value.* `margin-top:auto` is the first: it is a claim on free space, so the identical declaration yields 44px or 0px depending on whether an ancestor happened to stretch the box, and it fails silently in the variant you did not look at. A composited overlay is the second: `env(safe-area-inset-*)` covers the notch but knows nothing about a caption, HUD, or control bar the renderer paints later, so the layout verifies clean in the browser and ships broken in the frame. Both are invisible to every check performed on the page rather than on the finished output. Third, smaller, lesson: shipping a skill's own anti-pattern inside that skill's marketing is the cheapest possible proof the rule is worth having — and it took a rendered screenshot, not a code review, to catch.
|
|
54
|
+
- Verdict: gap(§4.C, §8.E) · covered(§5.A — #15 fired exactly as written)
|
|
55
|
+
- Action: folded into §4.C (auto-margin needs slack, with the 44/0 measurement) and §8.E (composited overlays are safe areas, with the 72 → 184px measurement) @ v2.12.2, plus anti-patterns #23 and #24. Both are failure-mode warnings from real defects, hardened on first occurrence per the §15.D carve-out.
|
|
56
|
+
|
|
57
|
+
### 2026-09-09 — PCC4SH / màn Cài đặt + menu tài khoản — `w-auto` loses to the Stack, and a leading icon moves the spine
|
|
58
|
+
|
|
59
|
+
- Space Read: settings screen (PWA mobile-first, also used on desktop) · balanced · STEP 4 (inherited Tailwind v4 + PCC4SH tokens) · DENSITY 5 · RIGOR 8 · ladder 8 → 16 → 24; every tap target ≥48px and ≥12px apart (the brief's a11y floor outranks every dial)
|
|
60
|
+
- Did: the screen was four white cards stacked, each holding exactly one full-bleed navy 64px button, so nothing read as more or less important than anything else — textbook anti-pattern #7 (a card per group) plus #1 (uniform weight). Collapsed them into three labelled cards ("Tài khoản" / "Trên máy này" / "Thoát tài khoản") whose members are rows: 44px icon tile, title + description, then the action. Secondary actions moved to a new 48px `inline` size (`w-full sm:w-auto`) instead of the 64px full-bleed primary. Two alignment bugs surfaced only after rendering: (1) at 1280px the `inline` buttons were still 640px wide — `sm:w-auto` was set but the row is a `flex-col`, so `align-items:stretch` overrode it; `sm:self-start` fixed it to 184px. (2) the action sat at the card inset while its own description started 56px in, behind the icon tile — added `sm:pl-14` (44 + 12) so title, copy and control share one left edge (measured descLeft 554 = btnLeft 554).
|
|
61
|
+
- Taught: `width` is a main-axis word. In a vertical Stack the cross axis belongs to `align-items`, so every "hug your content" utility is a no-op until an `align-self` backs it — and the failure is invisible on mobile, where full-bleed happened to be the intent, then appears at the first wide breakpoint. Second: a leading icon silently defines a text column, and any control that is a sibling of the icon block re-aligns to the container inset instead. The two edges are 56px apart, which reads as sloppy rather than as a deliberate outdent.
|
|
62
|
+
- Verdict: gap(§4.C)
|
|
63
|
+
- Action: folded into §4.C @ v2.11.0 — both bullets. Visible-misalignment failure modes caught on first render, so hardened on first occurrence per the §15.D carve-out.
|
|
64
|
+
- Follow-up (same day, after the owner asked for a different look): the row/button split was dropped entirely for the **whole row as the tap target** — 64px rows carrying icon tile, one-line label and a chevron, with the long explanation moved to a note at the foot of the group. Measured 341×65px per target versus 279×48px before, ~1.7× the area, and the §4.C indent problem stopped existing because there is no separate control to align. Kept both §4.C bullets: the `align-self` trap is general, and the measurement stands. Added the row-as-target rule to §10.B @ v2.12.0.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### 2026-09-05 — PCC4SH / Mẫu 02-VT print margins — the page margin box is also the browser's header/footer canvas
|
|
68
|
+
|
|
69
|
+
- Space Read: printed A4 accounting form · balanced · STEP 4 · RIGOR 8 · paper margin must stay ~10mm, but nothing may print inside it
|
|
70
|
+
- Did: the user saw `11:52 5/9/26 · Hệ thống quản lý công trình` across the top of every printed slip and the page URL across the bottom. Those are Chrome's own header/footer, drawn **inside the `@page` margin box** — not content, so no selector reaches them. Moved the slip to a named page `@page phieu-mot-trang { margin: 0 }` and put the 10mm back as `padding` on the print root (`main[data-print-page='phieu-kho']`). Kept the default `@page { margin: 10mm }` for the multi-page reports.
|
|
71
|
+
- Taught: `@page` margin is doing **two** jobs — paper whitespace *and* the browser chrome's canvas. Zeroing it is the only CSS lever that removes the chrome, and the whitespace then has to be re-created as padding on the content. The catch that decides where you can apply it: **body/root padding is not per-page.** Left/right padding repeats on every sheet, but top/bottom lands only on the first and last page — so a multi-page document with `margin: 0` prints its middle pages edge-to-edge and the printer's hardware margin clips the top and bottom rows. Safe for a one-page form, a defect for a long report. Scope the named page accordingly, and prefer the failure that adds whitespace (named page ignored ⇒ 10mm margin + 10mm padding) over the one that removes it.
|
|
72
|
+
- Verdict: gap(§12)
|
|
73
|
+
- Action: folded into §12 @ v2.10.0 — added the print-margin dual-role rule and anti-pattern #22. A user-visible defect with no selector-based fix, so hardened on first occurrence per §15.D's failure-mode carve-out.
|
|
74
|
+
|
|
75
|
+
### 2026-09-05 — PCC4SH / Mẫu 02-VT slip signature row — bottom-aligned boxes ≠ bottom-aligned text → folded into §4.C
|
|
76
|
+
|
|
77
|
+
- Space Read: printed accounting form, 4-column signature block · balanced · STEP 4 · DENSITY 4 · RIGOR 8 · four signer names must land on one baseline; the gap above them is hand-signing space
|
|
78
|
+
- Did: signature row was a 4-track grid, each column `flex-col` with `margin-top:auto` on the name. Two columns held `<input class="mau02vt-input">` (`min-height:44px`, `padding:.3rem .2rem`), two held a bare `<span>` (~22px, line-height 1.3 × 1.05rem). Both flush at the bottom, yet the input text sat ~11px higher — the user rejected it on sight. Fix: `.mau02vt-signature-name { min-height:44px; padding:.3rem .2rem; text-align:center }` on **all four**, plus `span.mau02vt-signature-name { display:flex; align-items:center; justify-content:center }`. Print keeps only the spans, so `@media print` resets `min-height:0; padding:0`.
|
|
79
|
+
- Taught: `margin-top:auto` bottom-aligns the **box**; the reader sees the **text**. Any row mixing an editable control with static text inherits the control's touch-target `min-height` (44/48px) as an alignment constraint — the control centers its text inside that tall box, the span fills its short one. Invisible until the two sit side by side. The a11y floor (§9) silently authored a layout rule.
|
|
80
|
+
- Verdict: gap(§4.C)
|
|
81
|
+
- Action: folded into §4.C @ v2.9.1 — added the mixed-control/text bullet and anti-pattern #21. A visible misalignment the user caught on first render → hardened on first occurrence, per the §15.D carve-out for failure-mode warnings.
|
|
82
|
+
|
|
83
|
+
### 2026-09-01 — PCC4SH / shell taskbar — a centered clock needs balanced grid tracks and a compact mobile face
|
|
84
|
+
|
|
85
|
+
- Space Read: authenticated app shell header · balanced · STEP 8 (Tailwind 4px utilities, 8px layout rhythm) · DENSITY 4 · RIGOR 8 · title/control groups stay at the edges while the clock owns the center track
|
|
86
|
+
- Did: changed the header row from one flex line to `grid-template-columns: minmax(0,1fr) auto minmax(0,1fr)` with `gap-2` (8px) below `sm` and `gap-3` (12px) from `sm` upward. Measured the built CSS at 320px: header inner row 288px after 16px side padding, clock 65.86px, clock center delta **−0.0078px**, edge overlap **0px**; the clock keeps only `HH:mm` visually while its DOM/accessible label retains seconds. Measured the desktop shell at 1440px with the 368px navigation rail: taskbar content center x=904, clock center x=904, delta **0px**, overlap **0px**, inner row 1040px.
|
|
87
|
+
- Taught: the first symmetric grid was mathematically centered but left only 92px per side at 320px while the sync icon + avatar cluster needed 104px, creating a 4px collision. A responsive clock face is the smallest change that preserves the center track, the existing controls, and the 48px touch floor; keep the full seconds display where the side groups have room.
|
|
88
|
+
- Verdict: covered
|
|
89
|
+
- Action: none
|
|
90
|
+
|
|
91
|
+
### 2026-08-30 — PCC4SH / login screen — unlayered critical CSS beats `@layer utilities` → folded into §12
|
|
92
|
+
|
|
93
|
+
- Space Read: `auth form · balanced · STEP 4 (Tailwind inherited) · DENSITY 4 · RIGOR 8 · label↔input 8 < field↔field 20 < group↔group 32`
|
|
94
|
+
- Did: user reported the login field's text "starts small, then grows a moment later". Measured by disabling the built stylesheet in the live page: field `13.33px` (UA default for `<input>`; box 22px tall) vs `22px` with CSS (box 60px) — the only element on the screen whose size visibly changes, because the UA sheet renders body text at 16px but form controls at ~13.3px. Fixed delivery two ways: `experimental.inlineCss` (production; CSS ships inside the HTML, no render-blocking round trip) plus a two-line critical `<style>` in the root layout for dev/stale-cache/404-chunk paths. Then rebuilt the screen's ladder: dropped a stray `my-auto` on the `<h1>` that fought the action block's `mt-auto` for the leftover height, so two sibling gaps had been landing at a *random* 46px/47px; now a fixed 32px each. Field internals `gap-1.5` (6px, off a 4-grid) → `gap-2` (8px). Repo-wide: 84 half-step spacing utilities snapped onto the grid, and 153 ad-hoc `text-[…rem]` magic sizes folded into named tokens so one type scale remains.
|
|
95
|
+
- Taught: **the critical block itself was the regression.** Written unlayered, `input,button,select,textarea{font:inherit}` outranked `.text-input{font-size:1.375rem}` — Tailwind v4 puts utilities in `@layer utilities`, and layer order is resolved *before* specificity, so a bare element selector wins over a class. Field silently rendered 18px instead of 22px with the class still on the element, the rule still in the stylesheet, and all 901 tests green. Wrapping it in `@layer base { … }` restored 22px while keeping the 18px floor when the stylesheet is missing. Second, smaller lesson: because only `<input>` moves visibly, users describe late-CSS as a *field* bug, so the hunt starts in the wrong component.
|
|
96
|
+
- Verdict: gap(§12)
|
|
97
|
+
- Action: folded into §12 @ v2.9.0 — added the unlayered-CSS rule + the "only inputs visibly resize" note, and anti-pattern #20. A visible, silent, green-tests failure mode → hardened on first occurrence (§15.D's wait-for-a-repeat gates default changes, not failure-mode warnings).
|
|
98
|
+
|
|
99
|
+
### Template
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
### YYYY-MM-DD — <project / surface> — <one-line lesson>
|
|
103
|
+
- Space Read: <surface · airy|balanced|packed · STEP · DENSITY · RIGOR · the ratio that mattered>
|
|
104
|
+
- Did: <the change + the real numbers>
|
|
105
|
+
- Taught: <what the situation revealed>
|
|
106
|
+
- Verdict: covered | gap(§n) | refinement(§n)
|
|
107
|
+
- Action: none | folded into §n @ vX.Y.Z | pending pattern
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Entries
|
|
111
|
+
|
|
112
|
+
<!-- newest first -->
|
|
113
|
+
|
|
114
|
+
### 2026-08-17 — PCC4SH / login screen — a lockup is centred by its box, not by its ink → folded into §4.E
|
|
115
|
+
- Space Read: login-form · balanced · STEP 4 (Tailwind inherited) · DENSITY 4 · RIGOR 6 · one 400px measure for fields and buttons alike; the logo group must shrink to its rendered text before centring means anything
|
|
116
|
+
- Did: two edits on `apps/web`. (1) `Logo.tsx` — the company name's two parts became `block` spans for the `surface` tone, so the span's `max-content` equals its longer line (205.6px) instead of the whole name on one line. The lockup went from filling the column to shrink-wrapping at 321.6px. Measured left/right gaps before → after: 375px wide `0/21.4` → `10.7/10.7`; 674px wide `0/126.4` → `63.2/63.2`; 360px `3.2/3.2`; 320px still wraps (the existing `min-[360px]` breakpoint already concedes that) with no h-scroll. (2) `dang-nhap/page.tsx` — the button column was `w-full` (448px) against a `max-w-[400px]` field column, **24px proud each side** at 674px and matching only by accident on phones where both hit the viewport cap; both now read one `FORM_COLUMN` constant, re-measured identical at `137 → 537`.
|
|
117
|
+
- Taught: §4.E lists four centring strategies but assumes the item's box *is* its content. When `max-content` exceeds the container the item is clamped to the container, the text wraps inside it, and the trailing dead space becomes pure offset — `align-items:center` then centres a box wider than anything it paints. The failure is invisible on a phone (leftover ≈ 10px reads as a rendering quirk) and screams on a laptop (63px), so the usual mobile-first check certifies it as fine. It is a *sizing* bug wearing an alignment costume: no nudge, `justify-*` or `text-align` fixes it, because every one of them still operates on the oversized box. Sibling columns that must share a measure have the same shape — two literals of the same width match by luck until one viewport separates them.
|
|
118
|
+
- Verdict: gap(§4.E)
|
|
119
|
+
- Action: folded into §4.E @ v2.8.0 — also added anti-pattern #19. Hardened on first occurrence: a visible, measurable rendering bug rather than a baseline-default change, same precedent as §9.D @ v2.2.1 and §5.E @ v2.7.0.
|
|
120
|
+
|
|
121
|
+
### 2026-08-13 — Personal-Web / checkout method list — a fixed-width icon rail overflows onto its own label → folded into §5.E
|
|
122
|
+
- Space Read: payment-method-list (modal) · balanced · STEP 8 (Tailwind inherited) · DENSITY 5 · RIGOR 8 · the logo rail must be a floor, not a clamp
|
|
123
|
+
- Did: logo rail `w-16` → `min-w-16` on both method rows in `src/PaymentModal.tsx`. The rail held one VietQR badge (58px) and fit 64px; a second row added Visa (52px) + `gap-1` (4) + Mastercard (40) = 96px into the same 64px. Measured in the browser: box 64 / content 96 → **32px overflow**, logo content edge at x=127 against a text edge at x=107 = **20px of glyph-on-glyph overlap**. After: box 96 = content 96, overflow 0, logo↔text gap back to the row's own `gap-3` (12). Re-measured at button widths 268/300/340px (320px viewport upward): overflow 0, gap 12, no in-button h-scroll.
|
|
124
|
+
- Taught: §5.E already says *use a floor (`min-height`), never a clamp (`height`)* — but states it only for the **block** axis on controls, so it does not fire when the fixed dimension is an inline-axis **slot** (icon rail, logo strip, avatar column, badge tray). The inline case is worse than the block case: a too-short box clips or scrolls, whereas a too-narrow slot whose children are `shrink-0` with `max-w-none` **paints over the next flex sibling** — no clip, no scrollbar, no console warning. It also stays invisible until someone adds item N+1 to *one* row, so it reads as "that one row is broken" rather than a systemic sizing error, and the row that still has N items keeps looking correct.
|
|
125
|
+
- Verdict: refinement(§5.E)
|
|
126
|
+
- Action: folded into §5.E @ v2.7.0 — also added anti-pattern #18. Hardened on first occurrence (visible rendering bug / failure-mode warning, not a baseline-default change), same precedent as §9.D @ v2.2.1.
|
|
127
|
+
|
|
128
|
+
### 2026-08-06 — EduPortal / sweeping 90 inline grids — the conversion is a judgement per site (covered; §12 clarified)
|
|
129
|
+
- Space Read: `whole app, mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · each two-column grid stacks or stays two-up by what its narrower cell holds`
|
|
130
|
+
- Did: converted every inline two-column grid in the app after v2.6.0 named the defect — **82 sites in 29 files**, plus **8 fixed rails** (`1fr 340px`, `1fr 300px`) found on the way. 59 took a collapsing class, 23 a stay-two-up class, 33 more were correctly left alone (`auto-fit` already reflows, three-plus columns are not splits, one column count is driven by state). 14 sites had the wide cell second and kept their proportions via `--split-main: 0.667fr` rather than being flipped.
|
|
131
|
+
- Taught: two things, neither a new rule. First, the fixed rails are **already forbidden** — §4.A says column widths are fluid and §9.D says no fixed-px widths on layout containers — and they were still there on eight screens, leaving the main column **7px** on a 375px phone after the pane padding. A rule that exists and is violated eight times is evidence about how the value spreads: one inline layout property gets copy-pasted, and neither the author nor a reviewer sees a breakpoint failing because nothing fails loudly. Second, and worth adding to §12: the conversion cannot be mechanical. Sending all 82 to the collapsing class would reproduce anti-pattern #16, so the class you convert to has to carry §8.B's judgement, which means you need at least two classes before you start.
|
|
132
|
+
- Verdict: covered
|
|
133
|
+
- Action: one clarifying sentence into §12 @ v2.6.1 tying the mechanism to §8.B's judgement, with the 59/23 split as the evidence. No new rule — §4.A, §9.D, §12 and §8.B each already say their half.
|
|
134
|
+
|
|
135
|
+
### 2026-08-06 — EduPortal / admin + teacher dashboards on a phone — an inline style outranks every media query → folded into §12
|
|
136
|
+
- Space Read: `dashboard mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · content column and side column must stack; every control clears 44px`
|
|
137
|
+
- Did: the main split of four dashboards was `style={{ gridTemplateColumns: '1.5fr 1fr' }}` inline, so `@media (max-width: 760px)` never reached it — admin, teacher, parent and pupil all stayed two-column on a phone, the narrow side measuring **107px** on the teacher screen. Nine call sites in all. Replaced with a `.ds-split` class whose ratio comes from `var(--split-main, 1.5fr)`, so a screen sets a **variable** and CSS keeps the **property**. Measured the targets at the same time: **eleven** controls at 33px tall on the admin screen (the tab row), 42px buttons and fixed 40×40 icon buttons on the teacher screen. `.btn`, `.icon-btn`, `.tab-btn` floored at 44 on the phone breakpoint. After: admin 0 controls under 44, no grid still split.
|
|
138
|
+
- Taught: the responsive section of this skill assumes the breakpoint can reach the value. In a JSX codebase it frequently cannot, and the failure is **silent in both directions** — the rule is written and correct, the breakpoint fires, the layout does not move, and nothing in devtools flags it as a conflict because there is no conflict: inline simply wins. `!important` "fixes" it and is the wrong tool, since it then wins at every width rather than the one you meant. The custom-property indirection is the fix that keeps both: inline expresses intent, CSS keeps authority.
|
|
139
|
+
- Verdict: gap(§12)
|
|
140
|
+
- Action: folded into §12 @ v2.6.0 — also added anti-pattern #17. Per §15.D this is a failure-mode warning with a measured before and after, not a baseline-dial change, so the wait-for-a-repeat guardrail does not gate it.
|
|
141
|
+
|
|
142
|
+
### 2026-08-06 — EduPortal / mobile command dock — size the children, don't pick a `justify-content` (covered; §4.C refinement pending)
|
|
143
|
+
- Space Read: `command-dock mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · six icon targets share the bar width, 44px touch floor does not yield`
|
|
144
|
+
- Did: a full-width bar of six icon buttons sat `justify-content: flex-start` with fixed-width children — ink ended at 315px of a 512px bar, **187px empty**. Measured the buttons at the same time: **44×38**, so width cleared the touch floor and height did not. Put `flex: 1 1 0; min-width: 44px; min-height: 44px` on the actions: 75×44 each, gap 8, dead space 187 → **1px**, floor met on all six.
|
|
145
|
+
- Taught: nothing the skill lacks — §9.A and §10.A both put the mobile tap target at 44 hard, and §4.C already says `space-evenly` for a segmented control, which is what a six-action dock is. Logged because the *reason* the wrong value was there is worth naming: `flex-start` is correct for the **overflow** case this bar also has (`overflow-x: auto`), and it had been chosen for that case and left to govern the case where everything fits. One alignment cannot answer both.
|
|
146
|
+
- Refinement worth watching: for a row that is scrollable *and* usually fits, put the sizing on the **children** (`flex: 1 1 0` + `min-width:` the a11y floor) rather than choosing a `justify-content` on the parent. Children fill the space when there is room, hold their floor when there is not, and the overflow scrolls — one declaration covers both states, where §4.C's `justify-content` table only answers the fits-case. Minimum width is then arithmetic and checkable: `n × floor + (n−1) × gap + padding` (here 6×44 + 5×8 + 20 = 324px).
|
|
147
|
+
- Verdict: covered
|
|
148
|
+
- Action: none for §9/§10 — the floor was already written and was simply violated. The §4.C sizing-vs-alignment point is a single datapoint; per §15.D, harden into §4.C only on a repeat.
|
|
149
|
+
|
|
150
|
+
### 2026-08-06 — EduPortal / student + admin dashboards on a phone — a blanket one-column collapse spends the screen → folded into §8.B
|
|
151
|
+
- Space Read: `dashboard mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · four one-number stat cards must not each take a phone's width; ladder 8→16→24`
|
|
152
|
+
- Did: the only phone breakpoint was `@media (max-width:760px) { .cols-2,.cols-3,.cols-4 { 1fr } }`, so a four-up stat row became four full-width blocks — measured **822px** of stat cards (4 × 189 + gaps) before the first real content on a 375px screen. Added `.ds-grid:has(> .card.stat) { repeat(2,1fr) }` in its own rule so a browser without `:has()` falls back to the old one-column behaviour rather than a broken one: **371px** after. Also `.card` 22→16 padding, `.stat .stat-val` 2.1rem→1.55rem, `.page-title` 2rem→1.5rem (32px of Fredoka took two lines for a greeting and pushed the page's own action below the fold), `.page-head` wrap→`flex-direction:column` (`align-items:flex-end` only reads as alignment while the row fits), and `.sidebar` `100vh`→`100dvh`.
|
|
153
|
+
- Taught: **reflow is "no horizontal scroll", not "one column"** — §9.D's collapse language invites a blanket single-column mobile rule, and §8.B's compressible-vs-not test was written only about *gaps*. The same test governs column count, and the deciding fact is what the narrowest **cell** holds, not what the grid is: one number + label wants half a phone's width, a table or prose wants all of it. There is no repo-wide scale here (values ran 7/12/13/14/16/20/22/52), so §0.A signal 1 gave nothing to conform to — which is exactly when the magic numbers creep in.
|
|
154
|
+
- Verdict: gap(§8.B)
|
|
155
|
+
- Action: folded into §8.B @ v2.5.0 — also added anti-pattern #16. Per §15.D this is a failure-mode warning with a measured before/after, not a baseline-dial change, so the wait-for-a-repeat guardrail does not gate it.
|
|
156
|
+
|
|
157
|
+
### 2026-08-06 — EduPortal / same edit, self-audit — I shipped three magic numbers into the fix (covered, no skill gap)
|
|
158
|
+
- Space Read: as above
|
|
159
|
+
- Did: the first pass of that same breakpoint wrote `.ds-grid{gap:14px}`, `.stat{gap:10px}`, `.page-head{margin-bottom:18px}`. All three are off-scale. Snapped per §11 triage (`10–15→8/12/16`, `17–22→16`) to `16 / 8 / 24`, which also restores a clean ladder: intra-card **8** → grid gutter **16** → head↔content **24** (2×, then 1.5× — §6.B's floor, not its target).
|
|
160
|
+
- Taught: nothing the skill does not already say — §2 and §11 cover this exactly. Worth logging as evidence rather than as a lesson: the numbers appeared because I set them to "feel tighter on a phone" *before* deriving the ramp, i.e. I skipped §13.A step 3. The failure mode is reaching for a value while thinking about the viewport instead of about the tier.
|
|
161
|
+
- Verdict: covered
|
|
162
|
+
- Action: none. Logged because §15 asks for evidence, and a violation of a rule that exists is evidence about the operator, not about the skill.
|
|
163
|
+
|
|
164
|
+
### 2026-07-04 — Personal-Web / policy hub — mobile pill-nav blew out the whole page → folded into §9.D
|
|
165
|
+
- Space Read: docs/policy hub · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · aside pill-nav must scroll-x inside its box, page contained at 320
|
|
166
|
+
- Did: policy grid `grid gap-8 lg:grid-cols-[240px_1fr_280px]` → added base `grid-cols-1` + `min-w-0` on the nav `<aside>`. The mobile pill-nav (10 `shrink-0` items in an `overflow-x-auto` row) had inflated the implicit `auto` grid column to ~2390px → page h-scroll at EVERY width <1024px (both VI & EN). After: 0 overflow at 320–1280 both langs; the nav scrolls internally.
|
|
167
|
+
- Taught: an `overflow-x-auto` scroller does NOT contain itself when its wrapper sits in an `auto`-sized grid/flex track — the track grows to the scroller's max-content and the page overflows. A grid defined with only `lg:grid-cols-[…]` falls back to an implicit `auto` column on mobile. Cap the track (`grid-cols-1` = `minmax(0,1fr)`) + `min-w-0` on the item so the scroll engages. Extends §9.D from nowrap-text to scroll-regions.
|
|
168
|
+
- Verdict: gap(§9.D)
|
|
169
|
+
- Action: folded into §9.D @ v2.4.1
|
|
170
|
+
|
|
171
|
+
### 2026-07-04 — Personal-Web / mobile header — enlarge tap targets without moving the row → folded into §9.A
|
|
172
|
+
- Space Read: mobile app-bar (nav utility) · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · header controls on a fixed `gap-4`; targets must reach 44 without shifting the row
|
|
173
|
+
- Did: cart icon-button 20×20 → 40×40, hamburger 24×18 → 44×38, VN/EN toggle 16 → 36, close-× 28 → 48 — all via net-zero `-m-2.5 p-2.5` (`-my-2.5 py-2.5` on the inline VN/EN); wrapped the cart icon+badge in an inner `relative` span so the count badge stayed on the glyph, not the padded corner. Verified 0 layout shift (before/after screenshots identical) and no 320–390px overflow.
|
|
174
|
+
- Taught: §9.A said "pad the hit area" but was silent on the tight-cluster case where padding visibly spreads a fixed-`gap` row. An equal negative margin cancels the padding's margin-box, so the target grows at zero layout cost; and an `absolute` overlay must be re-anchored to an inner icon wrapper or it drifts to the enlarged box's corner.
|
|
175
|
+
- Verdict: refinement(§9.A)
|
|
176
|
+
- Action: folded into §9.A @ v2.4.0
|
|
177
|
+
|
|
178
|
+
### 2026-06-30 — Personal-Web / policy hub → scroll-spy (all sections, one scroll) — §8 scroll-margin under sticky aside (covered)
|
|
179
|
+
- Space Read: docs/policy hub · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · sticky aside + sections stacked `gap-8`, active follows scroll
|
|
180
|
+
- Did: switched from tab (1 section) to all sections stacked; IntersectionObserver `rootMargin: "-25% 0px -60% 0px"` picks the topmost in-view section to highlight the sticky menu; nav anchors smooth-scroll with `scroll-mt-24` so the target clears the top edge; footer `#hash` still lands correctly.
|
|
181
|
+
- Taught: nothing new — §8 scroll-margin/scroll-padding under a sticky element + §4 sticky-aside. The rootMargin band (top ~25–40%) is the standard scroll-spy trigger zone; keep `scroll-mt` ≥ the visual top offset so anchored sections aren't clipped.
|
|
182
|
+
- Verdict: covered
|
|
183
|
+
- Action: none
|
|
184
|
+
|
|
185
|
+
### 2026-06-30 — Personal-Web / policy page → sidebar policy hub — §4 grid + sticky sidebar + tab nav (covered)
|
|
186
|
+
- Space Read: docs/policy hub · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · two-col `[280px_1fr]`, sticky menu (top-24), one section shown at a time
|
|
187
|
+
- Did: redesigned `/chinh-sach` to breadcrumb + title, `lg:grid-cols-[280px_1fr]` gap-12, sticky left menu (`lg:sticky lg:top-24 lg:self-start`) with numbered active-highlight items, content card `p-10`. Menu collapses to horizontal pill tabs on mobile (`flex overflow-x-auto` → `lg:flex-col`). Active section tied to URL hash so footer `#hoan-tien` opens the right tab.
|
|
188
|
+
- Taught: nothing new — §4 layout grid + sticky-aside pattern + §10 nav/section recipe applied to a CellphoneS-style policy hub. Mobile menu = compressible layout region (§8.B) so it drops to a scroll-row.
|
|
189
|
+
- Verdict: covered
|
|
190
|
+
- Action: none
|
|
191
|
+
|
|
192
|
+
### 2026-06-30 — Personal-Web / shared footer + policy page — §6 ladder + §10 footer recipe + §4 measure (covered)
|
|
193
|
+
- Space Read: site footer (utility) · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · group↔group 40px > intra 8–12px; policy = long-form, measure ~70ch
|
|
194
|
+
- Did: shared Footer (brand / 3 policy links / 6 payment logos / copyright) with proximity ladder — link list `gap-2.5` (10) < column groups `gap-10` (40) < copyright divider `pt-6` (24) after `border-t`; `py-12` footer band (lighter than the `py-24` CTA above it). Policy page `max-w-3xl` (~70ch), section `gap-14`, bullet list `gap-3.5`, heading↔items `mt-5`, `scroll-mt-28` for anchored links.
|
|
195
|
+
- Taught: nothing new — textbook §6 proximity ladder, §10 footer/section recipe, §4 measure for the long-form policy. No skill gap.
|
|
196
|
+
- Verdict: covered
|
|
197
|
+
- Action: none
|
|
198
|
+
|
|
199
|
+
### 2026-06-30 — Personal-Web / footer CTA heading — tight display leading collides Vietnamese diacritics → folded into §5.A
|
|
200
|
+
- Space Read: marketing closing-CTA · airy · STEP 8 · DENSITY 2 · RIGOR 4 · two-line VN display heading must clear stacked tone marks
|
|
201
|
+
- Did: heading `leading-[0.95]` → `leading-[1.1]` across the footer CTA + 4 section headings (footer later nudged to `1.2`); the dot-below on line 1 (`TẠO`/`TUYỆT`) was touching the circumflex/breve on line 2 (`ĐIỀU`/`Ờ`/`Ệ`).
|
|
202
|
+
- Taught: Vietnamese (and Thai / Arabic / Devanagari) caps stack marks both above AND below; sub-1.05 display leading that is fine for Latin all-caps collides marks between wrapped lines. The floor is set by content language, not aesthetics, and stays latent until the heading wraps to two lines.
|
|
203
|
+
- Verdict: gap(§5.A)
|
|
204
|
+
- Action: folded into §5.A @ v2.3.0 — also added anti-pattern #15.
|
|
205
|
+
|
|
206
|
+
### 2026-06-29 — Personal-Web / stat-card grid — `whitespace-nowrap` values overflow narrow columns → folded into §9.D
|
|
207
|
+
- Space Read: stat-card grid · packed · STEP 4 · DENSITY 6 · RIGOR 8 · value must fit its column at the *narrowest* breakpoint
|
|
208
|
+
- Did: capped value font `md:text-3xl`(30) → `sm:text-2xl`(24); stat grid `sm:grid-cols-3` → `+ lg:grid-cols-2` (both `MY_CREATED_SKILLS` & `shownAiSkills` blocks sit in `lg:grid-cols-2` cards). Measured 8 values overflowing a 102px column at 1024px (Telegraphic +63, AI→Human +53, SKILL.md +32, Universal +30, …); after fix 0/52 overflow at 375/900/1024/1280.
|
|
209
|
+
- Taught: `whitespace-nowrap` + a multi-col stat grid inside a half-width (2-col) card = guaranteed edge overflow at desktop. It hides at wide viewports (columns wide) and bites at the `lg` breakpoint where cards halve but font/columns don't. Always verify at the narrowest column, not the widest.
|
|
210
|
+
- Verdict: gap(§9.D)
|
|
211
|
+
- Action: folded into §9.D @ v2.2.1 — a clear, visible bug → hardened on first occurrence; it's a failure-mode warning, not a baseline-default change, so §15.D's "wait for a repeat" doesn't gate it.
|
|
212
|
+
|
|
213
|
+
### 2026-06-29 — Personal-Web / project stat-card grid — bottom-align captions with `flex-1` value, not bare `mt-auto`
|
|
214
|
+
- Space Read: stat-card grid · balanced · STEP 4 · DENSITY 6 · RIGOR 8 · captions must baseline-align across the row; tile inset on-scale & uncramped
|
|
215
|
+
- Did: per-card `px-2 py-3` → `p-3` (kept `sm:p-4`); added `flex flex-col` to the card and `flex-1` to the value block; value↔label `mt-1` → `mt-2` (4 → 8px). Applied to both stat-grid blocks in `src/App.tsx`.
|
|
216
|
+
- Taught: to bottom-align captions across a row of unequal-length values, `flex-col` card + `flex-1` on the value keeps a fixed value↔label min-gap, whereas the bare §4.C `mt-auto`-on-footer collapses that gap to 0 when a row's cards are already equal height (no free space for the auto-margin to absorb).
|
|
217
|
+
- Verdict: refinement(§4.C)
|
|
218
|
+
- Action: pending pattern — single datapoint; per §15.D, harden into §4.C/§10 only on a repeat.
|
|
219
|
+
|
|
220
|
+
### 2026-09-05 — PCC4SH / Mẫu 02-VT slip grid — read-only cells were the ones breaking the row's shared box
|
|
221
|
+
- Space Read: data-entry grid row · packed · STEP 3 · DENSITY 7 · RIGOR 9 · every cell in a row must sit on one baseline; gloved outdoor use sets a 44px floor
|
|
222
|
+
- Did: converted the 3 computed cells (ĐVT, Thực xuất, Thành tiền) from bare `<td>{text}` to `<input class="mau02vt-input">`, the same class the row's other 4 cells already used (`min-height: 44px`). Changed **no** spacing value — the 44px floor and cell padding were already there.
|
|
223
|
+
- Taught: in a grid that mixes input cells and read-only cells, the **read-only cells are the defect**, not the inputs. Each input carries its own 44px min-height box; a text-only `<td>` is one line tall and rides on the cell's vertical centring instead, so its content sits on a different optical line from its neighbours — visible as a wobble that reads like a random rendering bug rather than a spacing decision. Making the row homogeneous (every cell the same box) fixes alignment as a side effect of fixing the affordance. Second datapoint for the same rule as the 2026-09-05 signature-row case: **the touch-target floor only aligns a row if every cell in that row is subject to it.**
|
|
224
|
+
- Verdict: covered(§4.C + §9)
|
|
225
|
+
- Action: no skill change — §4.C already says the shared box governs the row, and §9's 44px floor already applies per control. Logged as the second occurrence in a different surface (grid row, not signature row); if a third appears, promote to an explicit anti-pattern "read-only cell inside an input row".
|
|
226
|
+
- CORRECTION, same day: the inputs were reverted — the warehouse model forbids editing those three cells (a DB `CHECK (issued_quantity = 1)`, and the issue/cancel flow moves exactly one slot). Final fix is the *general* form of the lesson, and it is the better one: the cells stay read-only but get a `min-height:44px` flex box of their own (`.mau02vt-computed`), so the row is homogeneous **without** making anything editable. Turning them into inputs was never the point — **matching the box was**. Second-order win: with the input fill colour now removed from exactly the non-editable cells, the on-screen instruction "type into the coloured cells" became literally true again, so the affordance and the alignment were one fix, not two.
|
|
227
|
+
|
|
228
|
+
### 2026-09-17 — PCC4SH / app shell chrome — a rule drawn by two sibling elements only reads as one line when both weight AND colour match
|
|
229
|
+
- Space Read: app shell chrome (fixed sidebar + sticky header) · balanced · STEP 8 · DENSITY 6 · RIGOR 9 · the vertical rule beside the logo and the horizontal rule under the title are drawn by two different components and must meet flush at the corner
|
|
230
|
+
- Did: three borders went from `border-b border-line` (1px `#c6ccd9`) to `border-b-[3px] border-accent` (`#303070`, the token the sidebar already fills with) — logo strip bottom, sticky header bottom, mobile drawer header bottom — plus a new `border-r-[3px]` on the logo strip. Changed **no** padding, gap or height: the strip stays `h-[76px]`, the header `min-h-[76px]`, and with border-box the touch targets are untouched (e2e `vung-cham` 130/130 across 11 screens confirms).
|
|
231
|
+
- Taught: two things, both latent until the corner is inspected. (1) Rules drawn by *separate* elements read as one continuous frame only when weight and colour both match; the original had matching geometry but a pale border meeting a navy one, and the eye reads that as a broken seam, not as two deliberate lines. The fix is not "add a divider" but "make every element contributing to the frame use the same pen". (2) When one element needs two edges in the same colour, spend **one** colour class (`border-accent`) rather than per-edge colours (`border-b-line` + `border-r-accent`): both are single-class specificity, so which edge wins is decided by the framework's CSS emit order — a coin-flip that can silently change on a Tailwind upgrade. Per-edge colour is only justified when the edges genuinely differ.
|
|
232
|
+
- Verdict: refinement(§4.C) — pending pattern, single datapoint
|
|
233
|
+
- Action: not folded into SKILL.md. §15.D gates hardening on a repeat, and this is one occurrence; it is also not a visible bug in production (the pale seam was merely ugly, not broken), so the first-occurrence exception used for the 2026-06-29 overflow case does not apply. If a second surface shows the same broken-seam signature, promote to §4.C as "a frame is one pen" plus an anti-pattern for per-edge colour classes.
|