從最小可驗證單位起步、加變數一次只加一個、範圍從窄到寬擴張。

適用:UI layout debug、對齊問題、selector / MutationObserver root / JS 操作邊界的設計。 不適用:純內部演算法(沒有視覺、沒有範圍選擇)。

自包含聲明:閱讀本文件不需要先讀其他 reference。本文件涵蓋 placeholder 漸進、measurement 完整性、最小必要範圍三個共生原則。


Test-First 補充:當「漸進」的方式是「寫測試固化」時、必須走 RED → GREEN 兩個訊號才算驗證 — 詳見 #69 Test-First:先看到 RED 才相信 GREEN。沒看過 RED 的測試 = 未驗證的訊號、不能信任。

何時參閱本文件

訊號該做的第一件事
開始 UI layout debug、不知道從哪一步起從色塊 placeholder 起步
對齊規則寫了結果歪掉、不知道哪裡錯列方程組、確認每個變數有來源
設計 selector / observer / JS 操作的範圍從最小起、有證據再擴張
想用 document.querySelectorAll('*')subtree: true停 — 範圍可能過寬、補上限制條件
Layout debug 一次改了 5 個變數、改完不知道哪個生效退回去、一次只動一個

為什麼這三個原則合併在一份 reference

三個原則服務同一個讀者群體(正在開始一個新工作、還沒卡關)、回答同一類問題(該從多大的範圍 / 多少變數起步)。

  • Placeholder 漸進 = 視覺面的「一次一個變數」
  • Measurement 完整性 = 對齊問題的「方程組必須完整」
  • Minimum scope = JS / CSS 範圍的「窄起來再放寬」

共同精神:先窄後寬、有證據再擴張。「先寬後縮」的問題是分不出哪個寬度是刻意的;「先窄後寬」每次擴張都有原因可追。


原則 1:Placeholder 漸進除錯

UI debug 從色塊起步、加東西一次加一個。

起步:純色塊

1<div style="width: 200px; height: 100px; background: red; border: 2px solid black;"></div>

沒文字、沒樣式、沒互動。唯一目的:確認位置、尺寸、grid / flex / absolute 的定位邏輯對。

階段順序

階段加入驗證
1純色塊(固定尺寸 + 顯眼邊框)位置、grid cell、stacking 對
2占位文字(單行、無樣式)文字基線對、line-height 沒影響
3真實內容(多行、含長字串)換行、溢出、文字裁切對
4視覺樣式(color、font、padding)視覺層次對
5互動行為(hover、click、focus)互動狀態對、focus 不跑掉

每階段只引入一個變數、發現問題能立刻定位。跳階段 = 失敗時不知道是哪個變數錯。

典型反例

1<!-- 第 1 步直接寫真實內容 + 完整樣式 -->
2<div class="card">
3  <h3>Search results</h3>
4  <p>Showing {{count}} matches for "{{query}}"</p>
5  <ul>...</ul>
6</div>

CSS 寫了 30 條、結果 .card 沒在預期位置。是 grid 錯?font-size 影響?margin-collapse?line-height?無法定位。


原則 2:Measurement 完整性

對齊問題的本質是線性方程組:

1target_y = anchor_y + offset
2total_height = h1_height + form_height + gap + scope_height + ...

每個變數都要有明確來源 — 任一個未知 → 整組無解。

變數來源的三種類型

類型說明範例
Hardcoded寫死在 design token / config--gap: 16px--h1-height: 48px
Component hook框架 / vendor 提供的 APIpagefind.options.height、CSS var
Runtime measuredJS 執行時量測(getBoundingClientRect)form.getBoundingClientRect().height

反例:靠估值補方程式

1/* 假設 form 大概 60px、加 gap 20px、總共 80px */
2.scope { top: 80px; }

實際 form 高度是 72px(隨字型 / line-height 變動)→ scope 跑位 8px。

對例:每個變數有來源

1const formHeight = form.getBoundingClientRect().height;  // measured
2const gap = parseFloat(getComputedStyle(form).marginBottom);  // measured
3scope.style.top = `${formHeight + gap}px`;

或全部用 design token:

1.scope { top: calc(var(--form-height) + var(--gap)); }
2/* var 在某處有單一定義、不是分散估值 */

混搭策略要全選同一邊:對齊基準上要嘛全寫死、要嘛全量測、不要 hardcoded + 估值混用。


原則 3:Minimum Necessary Scope

Selector / MutationObserver / JS 操作的範圍從最小起、擴張要有證據。

Selector 範圍

寬度範例風險
最小(精準)#search-form .scope-toggle安全、變化時要更新 selector
中等.scope-toggle可能命中其他頁面的同名元素
過寬[class*="scope"] / * > .toggle命中無關元素、副作用未知

預設用最小、有證據(多個地方確實要 match)再擴張。

MutationObserver 範圍

三個維度:root、options、頻率。

1// 過寬
2observer.observe(document.body, { childList: true, subtree: true, attributes: true });
3// → 監聽整個 page、每個 attribute 變動都觸發、CPU 100%
4
5// 最小
6observer.observe(searchForm, { childList: true });
7// → 只監聽 form 直接子節點變動

JS 操作邊界

改一個元素的範圍從小到大:

範圍風險
改 inline style安全、僅自家管的元素
改 attribute中 — framework 可能 reconcile 清掉
改 textContent中 — 同上
改 innerHTML高 — 子節點全重建、event listener 失效
reparent 整節點高但可控 — 整節點搬遷、framework 通常不會還原

從「改 inline style」起步、不行才升級。


三個原則的共同精神

從最小可驗證單位起步、有證據再擴張

  • Placeholder:色塊 → 文字 → 樣式(一次加一層)
  • Measurement:每個變數先確認來源、再寫對齊規則
  • Scope:最窄的 selector / observer / JS 邊界、要擴張要有具體 case

「先寬後縮」的反模式:寫一個包山包海的 selector、之後試著加 :not(...) 排除 → 永遠不知道哪些 match 是刻意的。


Wrong vs Right 對照

範例 1:UI debug 起步

任務:把搜尋結果卡片做成兩欄 grid

 1<!-- 直接寫完整版本 -->
 2<div class="results-grid">
 3  <article class="result-card">
 4    <h3><a href="...">Title</a></h3>
 5    <p class="excerpt">{{excerpt}}</p>
 6    <div class="meta"><span class="tag">tag</span> · <time>date</time></div>
 7  </article>
 8</div>
 9
10<style>
11.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
12.result-card { padding: 16px; border: 1px solid; }
13.result-card h3 { font-size: 18px; margin-bottom: 8px; }
14/* ... */
15</style>

跑出來、卡片高度不一致、grid-auto-rows 沒設、第二欄擠到第一欄底下。debug 困難 — 是 grid 設定錯?卡片內容差異?margin-collapse?

1<!-- 階段 1:純色塊驗證 grid -->
2<div class="results-grid">
3  <div style="height: 100px; background: red;"></div>
4  <div style="height: 100px; background: blue;"></div>
5  <div style="height: 100px; background: green;"></div>
6  <div style="height: 100px; background: yellow;"></div>
7</div>

確認 grid 兩欄正常後、再進階段 2(加占位文字)。

範例 2:MutationObserver root

任務:當 search results 出現時、注入客製 UI

1new MutationObserver(...).observe(document.body, { subtree: true, childList: true });
2// 整個 page 任何變動都觸發、callback 跑 1000+ 次/秒

1const container = document.querySelector('.pagefind-ui__results-area');
2new MutationObserver(...).observe(container, { childList: true });
3// 只監聽 results area 的直接子節點變動

如果之後發現 .pagefind-ui__results-area 內部 nested 變動也要監聽 → 那時再加 subtree: true、加之前能說出「為什麼需要」。


自檢清單(dogfooding)

開始一個新工作前:

  • UI debug:第 1 階段是不是純色塊(沒文字、沒樣式)?
  • 對齊規則寫之前:是不是每個變數都列出來源(hardcoded / hook / measured)?
  • Selector:起步是不是最精準的版本?
  • MutationObserver:root / options 是不是最窄的?
  • JS 改元素:是不是從「改 inline style」起、不行才升級?

任一項打勾失敗 → 退回最小、重新起步。


延伸閱讀

對應的事後檢討(在 content/report/):


Last Updated: 2026-04-26 Version: 0.1.0