<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Layout on Tarragon</title><link>https://tarrragon.github.io/blog/tags/layout/</link><description>Recent content in Layout on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/layout/index.xml" rel="self" type="application/rss+xml"/><item><title>視覺對齊用單一真實來源</title><link>https://tarrragon.github.io/blog/report/visual-alignment-single-source-of-truth/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/visual-alignment-single-source-of-truth/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>多個元素要對齊，每個元素的尺寸都需要「來源明確的數字」 — 寫死的 token 或 runtime 量測寫回變數，二選一不要混搭。&lt;/strong> 任何一個值含糊（猜的、估的、依字型自然渲染的），整條對齊基準就靠不住、修一處要找十處跟著改。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼對齊需要單一來源">為什麼對齊需要單一來源&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>對齊問題本質是&lt;strong>線性方程組&lt;/strong>：每個參與對齊的元素貢獻一個未知數，要解出對齊的 padding / margin / position 等變數，所有未知數都要有確定值。任一個未知數憑感覺給，整組無解。&lt;/p>
&lt;p>CSS 變數提供「一處定義、多處引用」的單一來源 — 改 token 只動一個值、所有引用點自動跟上。&lt;/p>
&lt;h3 id="兩種值來源各用對應的定義方法">兩種值來源、各用對應的定義方法&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>值的性質&lt;/th>
 &lt;th>確定方式&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>設計可決定的固定值&lt;/td>
 &lt;td>CSS 變數寫死&lt;/td>
 &lt;td>H1 height、icon size&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime 依字型 / 內容變動&lt;/td>
 &lt;td>ResizeObserver 量測寫回 CSS 變數&lt;/td>
 &lt;td>多行文字區塊高度、圖片自適應高度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>混搭的後果：寫死值跟實際渲染不一致時，對齊只在某些字型 / 螢幕 / 瀏覽器下成立、其他情境壞掉、且難以重現。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際應用">這次任務的實際應用&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁有四處要共用同一組視覺 token：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元素&lt;/th>
 &lt;th>為什麼需要這個值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>H1「搜尋」&lt;/td>
 &lt;td>自身 height&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pagefind search input&lt;/td>
 &lt;td>自身 height&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter sidebar &lt;code>padding-top&lt;/code>&lt;/td>
 &lt;td>對齊到 results 頂端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drawer &lt;code>margin-top&lt;/code>&lt;/td>
 &lt;td>為 scope UI 讓出空間&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>H1 與 input 的 height 是設計可決定的固定值 — 用 CSS 變數寫死。Scope UI 的 height 受字型 / 換行影響、不可預測 — 用 ResizeObserver 量測寫回。&lt;/p>
&lt;h3 id="執行css-變數定義">執行：CSS 變數定義&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 設計決定 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* pagefind input 64 + border 4，鎖定 scale=1.0 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* drawer margin-top */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c">/* --search-scope-h 由 JS 量測寫入 :root */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="執行js-量測寫回">執行：JS 量測寫回&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">56&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="執行多處引用">執行：多處引用&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-title&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__drawer&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">margin-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">calc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scope&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">padding-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">calc&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="o">+&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scope&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">gap&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="對齊問題的兩種失敗模式">對齊問題的兩種失敗模式&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗模式&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>根因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫死值與實際渲染不一致&lt;/td>
 &lt;td>字型變動或 scale 改變後對不齊&lt;/td>
 &lt;td>沒控制渲染參數（如 pagefind scale）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用估算值代替量測&lt;/td>
 &lt;td>邊界情境（短/長文字、特殊字型）壞掉&lt;/td>
 &lt;td>把不可預測的值當固定值處理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者共通的修法是：&lt;strong>確認每個值的性質、按性質選來源&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="鎖定渲染參數讓寫死值生效">鎖定渲染參數讓寫死值生效&lt;/h2>
&lt;p>當值「理論上可預測」但需要強制條件時，鎖定渲染參數。&lt;/p>
&lt;p>&lt;strong>核心定義&lt;/strong>：Pagefind input 高度 = &lt;code>64px × --pagefind-ui-scale&lt;/code>。把 scale 設成 1.0、input 自然渲染為 64px、加 border 4px 共 68px、剛好等於我們的 &lt;code>--search-form-h&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把組件提供的 scale 變數拉進自家設計系統 — 組件配合我們、不是我們配合組件。&lt;/p>
&lt;hr>
&lt;h2 id="設計取捨對齊基準的值來源策略">設計取捨：對齊基準的值來源策略&lt;/h2>
&lt;p>四種做法、各自機會成本不同。這個專案選 A（CSS 變數寫死 + var 引用）當固定值預設、B（ResizeObserver 量測寫回變數）當 runtime 值預設、其他做法是反模式。&lt;/p>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../single-source-of-truth/">#44 SSoT&lt;/a> 抽象原則在「對齊基準」這個面向的應用。&lt;/p>&lt;/blockquote>
&lt;h3 id="acss-變數寫死--var-引用這個專案對固定值的預設">A：CSS 變數寫死 + var() 引用（這個專案對固定值的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：&lt;code>body.page-search { --search-title-h: 64px }&lt;/code>、其他地方用 &lt;code>var(--search-title-h)&lt;/code> 引用&lt;/li>
&lt;li>&lt;strong>選 A 的理由&lt;/strong>：定義住址唯一、改 token 自動跟上、無 runtime 開銷&lt;/li>
&lt;li>&lt;strong>適合&lt;/strong>：設計可決定的固定值（H1 height、icon size、間距）&lt;/li>
&lt;li>&lt;strong>代價&lt;/strong>：值不能跟 runtime 內容變動 — 字型大幅變化時 layout 可能不適配&lt;/li>
&lt;/ul>
&lt;h3 id="bresizeobserver-量測寫回變數這個專案對-runtime-值的預設">B：ResizeObserver 量測寫回變數（這個專案對 runtime 值的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：JS 量測元素實際渲染高度、&lt;code>setProperty('--search-scope-h', ...)&lt;/code> 寫回變數&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：B 自動跟著實際渲染走、A 假設渲染條件穩定；B 多 JS 一層、A 純 CSS&lt;/li>
&lt;li>&lt;strong>B 比 A 好的情境&lt;/strong>：值受字型 / 換行 / 內容動態影響、無法 build time 預測&lt;/li>
&lt;/ul>
&lt;h3 id="c複製-magic-number-在多處">C：複製 magic number 在多處&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：&lt;code>padding-top: 152px&lt;/code> 在多個 selector 直接寫死&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：C 寫法直接、不用變數系統；但改一處要找全部、漏改一個就壞&lt;/li>
&lt;li>&lt;strong>C 是反模式&lt;/strong>：magic number 是「未來 debug 的潛在炸彈」（&lt;a href="../single-source-of-truth/">#44 SSoT&lt;/a>） — 改一處要找全部、漏改一個就壞&lt;/li>
&lt;/ul>
&lt;h3 id="d估算值不寫變數不量測">D：估算值（不寫變數、不量測）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：執行者依感覺寫「padding-top: 152px、應該對齊」&lt;/li>
&lt;li>&lt;strong>成本特別高的原因&lt;/strong>：估值對 = 巧合、估值錯 = 看起來對但 +/- 幾 px、後者更糟（錯誤被視覺接受、不會被發現）&lt;/li>
&lt;li>&lt;strong>D 是反模式&lt;/strong>：任何寫死值都該標明來源（fact / 鎖定條件 / 量測）— 估值對 = 巧合、估值錯 = 看起來對但實際 +/- 幾 px、ship 後在邊界情境暴露&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>可能的根因&lt;/th>
 &lt;th>第一個該檢查的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>改一個 CSS 數字、要去 N 個地方跟著改&lt;/td>
 &lt;td>缺少單一來源&lt;/td>
 &lt;td>找出複製的 magic number、提成 CSS 變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設計稿對齊但實作對不齊&lt;/td>
 &lt;td>把可變值當固定值&lt;/td>
 &lt;td>量測該元素的真實 height、決定改用 ResizeObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>字型變動或 dark mode 後對齊壞掉&lt;/td>
 &lt;td>寫死值依賴某個沒鎖定的渲染參數&lt;/td>
 &lt;td>找出該渲染參數、用 CSS 變數鎖定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對齊「大部分時候對」、邊界 case 壞掉&lt;/td>
 &lt;td>沒處理動態高度&lt;/td>
 &lt;td>把該值用 ResizeObserver 量測寫回&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：對齊不是視覺問題，是「每個參與元素是否有確定尺寸」的問題。任何一個值不確定、整組對齊就脆弱。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>多個元素要對齊，每個元素的尺寸都需要「來源明確的數字」 — 寫死的 token 或 runtime 量測寫回變數，二選一不要混搭。</strong> 任何一個值含糊（猜的、估的、依字型自然渲染的），整條對齊基準就靠不住、修一處要找十處跟著改。</p>
<hr>
<h2 id="為什麼對齊需要單一來源">為什麼對齊需要單一來源</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>對齊問題本質是<strong>線性方程組</strong>：每個參與對齊的元素貢獻一個未知數，要解出對齊的 padding / margin / position 等變數，所有未知數都要有確定值。任一個未知數憑感覺給，整組無解。</p>
<p>CSS 變數提供「一處定義、多處引用」的單一來源 — 改 token 只動一個值、所有引用點自動跟上。</p>
<h3 id="兩種值來源各用對應的定義方法">兩種值來源、各用對應的定義方法</h3>
<table>
  <thead>
      <tr>
          <th>值的性質</th>
          <th>確定方式</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計可決定的固定值</td>
          <td>CSS 變數寫死</td>
          <td>H1 height、icon size</td>
      </tr>
      <tr>
          <td>Runtime 依字型 / 內容變動</td>
          <td>ResizeObserver 量測寫回 CSS 變數</td>
          <td>多行文字區塊高度、圖片自適應高度</td>
      </tr>
  </tbody>
</table>
<p>混搭的後果：寫死值跟實際渲染不一致時，對齊只在某些字型 / 螢幕 / 瀏覽器下成立、其他情境壞掉、且難以重現。</p>
<hr>
<h2 id="這次任務的實際應用">這次任務的實際應用</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁有四處要共用同一組視覺 token：</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>為什麼需要這個值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>H1「搜尋」</td>
          <td>自身 height</td>
      </tr>
      <tr>
          <td>Pagefind search input</td>
          <td>自身 height</td>
      </tr>
      <tr>
          <td>Filter sidebar <code>padding-top</code></td>
          <td>對齊到 results 頂端</td>
      </tr>
      <tr>
          <td>Drawer <code>margin-top</code></td>
          <td>為 scope UI 讓出空間</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>H1 與 input 的 height 是設計可決定的固定值 — 用 CSS 變數寫死。Scope UI 的 height 受字型 / 換行影響、不可預測 — 用 ResizeObserver 量測寫回。</p>
<h3 id="執行css-變數定義">執行：CSS 變數定義</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>     <span class="c">/* 設計決定 */</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>      <span class="c">/* pagefind input 64 + border 4，鎖定 scale=1.0 */</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>         <span class="c">/* drawer margin-top */</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c">/* --search-scope-h 由 JS 量測寫入 :root */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="執行js-量測寫回">執行：JS 量測寫回</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">||</span> <span class="mi">56</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">h</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">syncScopeHeight</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">syncScopeHeight</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span></span></span></code></pre></div><h3 id="執行多處引用">執行：多處引用</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-title</span> <span class="p">{</span> <span class="k">height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">);</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">.</span><span class="nc">pagefind-ui__drawer</span> <span class="p">{</span> <span class="k">margin-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">scope</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="mi">8</span><span class="kt">px</span><span class="p">);</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">padding-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">form</span><span class="o">-</span><span class="n">h</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">scope</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="mi">8</span><span class="kt">px</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">gap</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="對齊問題的兩種失敗模式">對齊問題的兩種失敗模式</h2>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
          <th>根因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫死值與實際渲染不一致</td>
          <td>字型變動或 scale 改變後對不齊</td>
          <td>沒控制渲染參數（如 pagefind scale）</td>
      </tr>
      <tr>
          <td>用估算值代替量測</td>
          <td>邊界情境（短/長文字、特殊字型）壞掉</td>
          <td>把不可預測的值當固定值處理</td>
      </tr>
  </tbody>
</table>
<p>兩者共通的修法是：<strong>確認每個值的性質、按性質選來源</strong>。</p>
<hr>
<h2 id="鎖定渲染參數讓寫死值生效">鎖定渲染參數讓寫死值生效</h2>
<p>當值「理論上可預測」但需要強制條件時，鎖定渲染參數。</p>
<p><strong>核心定義</strong>：Pagefind input 高度 = <code>64px × --pagefind-ui-scale</code>。把 scale 設成 1.0、input 自然渲染為 64px、加 border 4px 共 68px、剛好等於我們的 <code>--search-form-h</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">{</span> <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>把組件提供的 scale 變數拉進自家設計系統 — 組件配合我們、不是我們配合組件。</p>
<hr>
<h2 id="設計取捨對齊基準的值來源策略">設計取捨：對齊基準的值來源策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（CSS 變數寫死 + var 引用）當固定值預設、B（ResizeObserver 量測寫回變數）當 runtime 值預設、其他做法是反模式。</p>
<blockquote>
<p>本篇是 <a href="../single-source-of-truth/">#44 SSoT</a> 抽象原則在「對齊基準」這個面向的應用。</p></blockquote>
<h3 id="acss-變數寫死--var-引用這個專案對固定值的預設">A：CSS 變數寫死 + var() 引用（這個專案對固定值的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>body.page-search { --search-title-h: 64px }</code>、其他地方用 <code>var(--search-title-h)</code> 引用</li>
<li><strong>選 A 的理由</strong>：定義住址唯一、改 token 自動跟上、無 runtime 開銷</li>
<li><strong>適合</strong>：設計可決定的固定值（H1 height、icon size、間距）</li>
<li><strong>代價</strong>：值不能跟 runtime 內容變動 — 字型大幅變化時 layout 可能不適配</li>
</ul>
<h3 id="bresizeobserver-量測寫回變數這個專案對-runtime-值的預設">B：ResizeObserver 量測寫回變數（這個專案對 runtime 值的預設）</h3>
<ul>
<li><strong>機制</strong>：JS 量測元素實際渲染高度、<code>setProperty('--search-scope-h', ...)</code> 寫回變數</li>
<li><strong>跟 A 的取捨</strong>：B 自動跟著實際渲染走、A 假設渲染條件穩定；B 多 JS 一層、A 純 CSS</li>
<li><strong>B 比 A 好的情境</strong>：值受字型 / 換行 / 內容動態影響、無法 build time 預測</li>
</ul>
<h3 id="c複製-magic-number-在多處">C：複製 magic number 在多處</h3>
<ul>
<li><strong>機制</strong>：<code>padding-top: 152px</code> 在多個 selector 直接寫死</li>
<li><strong>跟 A 的取捨</strong>：C 寫法直接、不用變數系統；但改一處要找全部、漏改一個就壞</li>
<li><strong>C 是反模式</strong>：magic number 是「未來 debug 的潛在炸彈」（<a href="../single-source-of-truth/">#44 SSoT</a>） — 改一處要找全部、漏改一個就壞</li>
</ul>
<h3 id="d估算值不寫變數不量測">D：估算值（不寫變數、不量測）</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺寫「padding-top: 152px、應該對齊」</li>
<li><strong>成本特別高的原因</strong>：估值對 = 巧合、估值錯 = 看起來對但 +/- 幾 px、後者更糟（錯誤被視覺接受、不會被發現）</li>
<li><strong>D 是反模式</strong>：任何寫死值都該標明來源（fact / 鎖定條件 / 量測）— 估值對 = 巧合、估值錯 = 看起來對但實際 +/- 幾 px、ship 後在邊界情境暴露</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改一個 CSS 數字、要去 N 個地方跟著改</td>
          <td>缺少單一來源</td>
          <td>找出複製的 magic number、提成 CSS 變數</td>
      </tr>
      <tr>
          <td>設計稿對齊但實作對不齊</td>
          <td>把可變值當固定值</td>
          <td>量測該元素的真實 height、決定改用 ResizeObserver</td>
      </tr>
      <tr>
          <td>字型變動或 dark mode 後對齊壞掉</td>
          <td>寫死值依賴某個沒鎖定的渲染參數</td>
          <td>找出該渲染參數、用 CSS 變數鎖定</td>
      </tr>
      <tr>
          <td>對齊「大部分時候對」、邊界 case 壞掉</td>
          <td>沒處理動態高度</td>
          <td>把該值用 ResizeObserver 量測寫回</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：對齊不是視覺問題，是「每個參與元素是否有確定尺寸」的問題。任何一個值不確定、整組對齊就脆弱。</p>
]]></content:encoded></item><item><title>量測值缺一不可：依賴未測量值會錯位</title><link>https://tarrragon.github.io/blog/report/measurement-completeness/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/measurement-completeness/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>對齊基準上的每個未知數都要解出來、整組才有解。&lt;/strong> 這跟線性方程組一樣 — 任何一個變數靠估算、整條基準線就不準。每個參與對齊的元素都需要「來源明確的數字」（寫死或量測），不能依賴「應該差不多吧」的視覺直覺。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼缺一個值整個壞">為什麼缺一個值整個壞&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>對齊不是「視覺感」、是「相對位置的數學關係」。filter 的 padding-top 要等於右側「H1 + input + gap」的總和；任何一個值不準、padding 就錯、視覺上看起來就是沒對齊。&lt;/p>
&lt;p>人眼可以分辨 1px 的差異 — 估算「大概 60px」實際上 56 或 64 都可能、視覺一眼看出。&lt;/p>
&lt;h3 id="解線性方程組需要所有變數">解線性方程組需要所有變數&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>步驟&lt;/th>
 &lt;th>動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>列出對齊基準上的所有元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>對每個元素標註「值的來源」：寫死 / 量測 / 未知&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>任何「未知」都要先解決（決定寫死或量測）才能寫對齊規則&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跳過第 2 步直接寫對齊規則 = 拿一組有未知數的方程組嘗試代入解 — 不會對。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把 filter sidebar 的內容上緣對齊到右側 results 上緣。filter 用 &lt;code>padding-top&lt;/code> 把內容下推。&lt;/p>
&lt;p>第一次嘗試：估 &lt;code>padding-top: 152px&lt;/code>（H1 64 + input 68 + gap 20）。&lt;/p>
&lt;p>實際渲染：filter 上緣比 results 上緣高了 ~10px。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>&lt;code>152px&lt;/code> 的計算用了估算的 H1 height（64px）。實際 H1 受 theme 的 &lt;code>margin-block-end&lt;/code> 影響、總高度可能 ~70px。差了 ~6px。&lt;/p>
&lt;p>進一步檢查：&lt;code>--pagefind-ui-scale: 0.8&lt;/code> 時 input 高度 = 64 × 0.8 = 51.2px、不是 68px。又差 ~17px。&lt;/p>
&lt;p>差距加總超過視覺可接受範圍。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>
&lt;p>把所有變數轉為「來源明確」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元素&lt;/th>
 &lt;th>解決方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>H1&lt;/td>
 &lt;td>寫死 height + line-height + margin: 0，強制等於 token&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pagefind input&lt;/td>
 &lt;td>設 &lt;code>--pagefind-ui-scale: 1.0&lt;/code>，加 border 共 68px、強制等於 token&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope UI（高度受字型換行影響）&lt;/td>
 &lt;td>用 ResizeObserver 量測寫回 CSS 變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Gap（drawer margin-top）&lt;/td>
 &lt;td>從 pagefind CSS 取得固定值 20px&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>padding-top&lt;/code> 用 &lt;code>calc()&lt;/code> 加總所有變數、永遠跟著走。&lt;/p>
&lt;hr>
&lt;h2 id="內在屬性比較值的來源分類">內在屬性比較：值的「來源」分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>來源&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>維護負擔&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Design token（CSS 變數寫死）&lt;/td>
 &lt;td>設計可決定的固定值&lt;/td>
 &lt;td>低 — 改一處全部跟上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件提供的 hook（如 pagefind scale）&lt;/td>
 &lt;td>透過組件 API 鎖定渲染參數&lt;/td>
 &lt;td>低 — 跟組件升級走&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime 量測（ResizeObserver）&lt;/td>
 &lt;td>內容動態決定的值&lt;/td>
 &lt;td>中 — JS 程式要寫對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>估算 / magic number&lt;/td>
 &lt;td>不適用 — 永遠錯&lt;/td>
 &lt;td>不該存在&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>不要把「估算 / magic number」當作來源&lt;/strong>。每個 magic number 都是未來 debug 的潛在炸彈。&lt;/p>
&lt;hr>
&lt;h2 id="把對齊看成方程組的步驟">把對齊看成方程組的步驟&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">基準線 P 的位置 = sum(每個前置元素的 height + margin + padding + gap)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例：filter 的 &lt;code>padding-top&lt;/code> = &lt;code>H1.height + input.height + drawer.margin-top&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>對齊基準上的每個未知數都要解出來、整組才有解。</strong> 這跟線性方程組一樣 — 任何一個變數靠估算、整條基準線就不準。每個參與對齊的元素都需要「來源明確的數字」（寫死或量測），不能依賴「應該差不多吧」的視覺直覺。</p>
<hr>
<h2 id="為什麼缺一個值整個壞">為什麼缺一個值整個壞</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>對齊不是「視覺感」、是「相對位置的數學關係」。filter 的 padding-top 要等於右側「H1 + input + gap」的總和；任何一個值不準、padding 就錯、視覺上看起來就是沒對齊。</p>
<p>人眼可以分辨 1px 的差異 — 估算「大概 60px」實際上 56 或 64 都可能、視覺一眼看出。</p>
<h3 id="解線性方程組需要所有變數">解線性方程組需要所有變數</h3>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>列出對齊基準上的所有元素</td>
      </tr>
      <tr>
          <td>2</td>
          <td>對每個元素標註「值的來源」：寫死 / 量測 / 未知</td>
      </tr>
      <tr>
          <td>3</td>
          <td>任何「未知」都要先解決（決定寫死或量測）才能寫對齊規則</td>
      </tr>
  </tbody>
</table>
<p>跳過第 2 步直接寫對齊規則 = 拿一組有未知數的方程組嘗試代入解 — 不會對。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>要把 filter sidebar 的內容上緣對齊到右側 results 上緣。filter 用 <code>padding-top</code> 把內容下推。</p>
<p>第一次嘗試：估 <code>padding-top: 152px</code>（H1 64 + input 68 + gap 20）。</p>
<p>實際渲染：filter 上緣比 results 上緣高了 ~10px。</p>
<h3 id="判讀">判讀</h3>
<p><code>152px</code> 的計算用了估算的 H1 height（64px）。實際 H1 受 theme 的 <code>margin-block-end</code> 影響、總高度可能 ~70px。差了 ~6px。</p>
<p>進一步檢查：<code>--pagefind-ui-scale: 0.8</code> 時 input 高度 = 64 × 0.8 = 51.2px、不是 68px。又差 ~17px。</p>
<p>差距加總超過視覺可接受範圍。</p>
<h3 id="執行">執行</h3>
<p>把所有變數轉為「來源明確」：</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>解決方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>H1</td>
          <td>寫死 height + line-height + margin: 0，強制等於 token</td>
      </tr>
      <tr>
          <td>Pagefind input</td>
          <td>設 <code>--pagefind-ui-scale: 1.0</code>，加 border 共 68px、強制等於 token</td>
      </tr>
      <tr>
          <td>Scope UI（高度受字型換行影響）</td>
          <td>用 ResizeObserver 量測寫回 CSS 變數</td>
      </tr>
      <tr>
          <td>Gap（drawer margin-top）</td>
          <td>從 pagefind CSS 取得固定值 20px</td>
      </tr>
  </tbody>
</table>
<p><code>padding-top</code> 用 <code>calc()</code> 加總所有變數、永遠跟著走。</p>
<hr>
<h2 id="內在屬性比較值的來源分類">內在屬性比較：值的「來源」分類</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>適用情境</th>
          <th>維護負擔</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Design token（CSS 變數寫死）</td>
          <td>設計可決定的固定值</td>
          <td>低 — 改一處全部跟上</td>
      </tr>
      <tr>
          <td>組件提供的 hook（如 pagefind scale）</td>
          <td>透過組件 API 鎖定渲染參數</td>
          <td>低 — 跟組件升級走</td>
      </tr>
      <tr>
          <td>Runtime 量測（ResizeObserver）</td>
          <td>內容動態決定的值</td>
          <td>中 — JS 程式要寫對</td>
      </tr>
      <tr>
          <td>估算 / magic number</td>
          <td>不適用 — 永遠錯</td>
          <td>不該存在</td>
      </tr>
  </tbody>
</table>
<p><strong>不要把「估算 / magic number」當作來源</strong>。每個 magic number 都是未來 debug 的潛在炸彈。</p>
<hr>
<h2 id="把對齊看成方程組的步驟">把對齊看成方程組的步驟</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">基準線 P 的位置 = sum(每個前置元素的 height + margin + padding + gap)</span></span></code></pre></div><p>例：filter 的 <code>padding-top</code> = <code>H1.height + input.height + drawer.margin-top</code>。</p>
<p>把每個變數列出、確認來源、用 CSS <code>calc()</code> + 變數寫成 single source。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">padding-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">)</span>        <span class="c">/* 寫死 64px */</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">form</span><span class="o">-</span><span class="n">h</span><span class="p">)</span>       <span class="c">/* 鎖 scale=1.0、寫死 68px */</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">scope</span><span class="o">-</span><span class="n">h</span><span class="p">)</span>      <span class="c">/* ResizeObserver 量測 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="o">+</span> <span class="mi">8</span><span class="kt">px</span>                        <span class="c">/* 固定 padding */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">gap</span><span class="p">)</span>          <span class="c">/* pagefind drawer margin 20px */</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="設計取捨對齊基準上每個值的來源">設計取捨：對齊基準上每個值的來源</h2>
<p>四種做法、各自機會成本不同。預設選擇取決於值的可預測性 — 設計可決定 → A、組件提供 hook → B、內容動態 → C、估算永遠不是答案。</p>
<h3 id="adesign-tokencss-變數寫死">A：Design token（CSS 變數寫死）</h3>
<ul>
<li><strong>機制</strong>：<code>--search-title-h: 64px</code> 寫成設計系統 token</li>
<li><strong>選 A 的理由</strong>：build time 確定、純 CSS、改 token 全部跟上</li>
<li><strong>適合</strong>：設計可決定的固定值（spacing、typography scale、icon size）</li>
<li><strong>代價</strong>：值無法跟 runtime 內容變動 — 字型大幅變化時可能不適配</li>
</ul>
<h3 id="b組件提供的-hook如-pagefind-scale">B：組件提供的 hook（如 pagefind scale）</h3>
<ul>
<li><strong>機制</strong>：<code>.search-shell { --pagefind-ui-scale: 1.0 }</code>、透過組件 API 鎖定渲染參數</li>
<li><strong>跟 A 的取捨</strong>：B 把組件納入自家設計系統、A 自己決定值；B 在「組件渲染參數可調」時最乾淨</li>
<li><strong>B 比 A 好的情境</strong>：值由組件決定但組件提供 hook 可控（例如 vendor library 的 size variant）</li>
</ul>
<h3 id="cruntime-量測resizeobserver-寫回-css-變數">C：Runtime 量測（ResizeObserver 寫回 CSS 變數）</h3>
<ul>
<li><strong>機制</strong>：JS 量元素實際渲染尺寸、寫回 CSS 變數、其他元素引用</li>
<li><strong>跟 A/B 的取捨</strong>：C 自動跟著實際走、A/B 假設條件穩定；C 多 JS 一層</li>
<li><strong>C 比 A 好的情境</strong>：值受字型 / 換行 / 內容動態影響、無法 build time 預測</li>
</ul>
<h3 id="d估算--magic-number">D：估算 / magic number</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺給數字、不寫變數、不量測</li>
<li><strong>成本特別高的原因</strong>：未來 debug 的潛在炸彈、估錯時錯誤被視覺接受不被發現、跨情境（字型 / scale / theme）必壞</li>
<li><strong>D 是反模式</strong>：任何「沒來源」的值都是 unsolved 變數、會在邊界情境爆掉</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺對齊「看起來差幾 px」</td>
          <td>某個元素的高度估算不準</td>
          <td>量測該元素真實 height、跟假設值比對</td>
      </tr>
      <tr>
          <td>換 viewport 對齊壞掉</td>
          <td>某個值依賴 viewport 但沒處理</td>
          <td>找出該值、改用響應式變數</td>
      </tr>
      <tr>
          <td>換字型 / 縮放後對齊壞掉</td>
          <td>某個值受字型影響但寫死了</td>
          <td>改用 ResizeObserver 量測</td>
      </tr>
      <tr>
          <td>改某個 token 要去多處跟改</td>
          <td>沒用 CSS 變數</td>
          <td>把 magic number 提成變數、calc 串起來</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：對齊問題的根因永遠是「某個變數沒解出來」。先找出那個變數、確定來源、再寫對齊規則。</p>
<p>「估值補方程式」是便利（用感覺寫死）、「找變數真實來源」是對齊（量測或從 token 算）— 同 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。對齊問題的 silent 失敗常常是估值的後果。</p>
]]></content:encoded></item><item><title>置中元件與絕對定位元件並存：用疊層而非排擠</title><link>https://tarrragon.github.io/blog/report/centered-and-positioned-coexistence/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/centered-and-positioned-coexistence/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>置中元件與指定位置元件共存，正確做法是讓兩者位於不同 layout 層。&lt;/strong> Layout 流負責「以內容驅動的尺寸與置中」；絕對定位負責「貼在 layout 流之上的固定位置元件」。兩者用疊層共存、不互相排擠。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼排擠式做法行不通">為什麼排擠式做法行不通&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>排擠式：把兩個元件放進同一個 grid / flex container、各佔一個欄位。問題在於「容器寬度有限、加一個欄位就壓縮另一個」 — 中央欄想置中時，加 sidebar 後整個 layout 重新分佈、中央欄被推到非置中位置。&lt;/p>
&lt;p>疊層式：sidebar 用 &lt;code>position: absolute&lt;/code> 從 layout 流跳出 — 中央欄看不到 sidebar、繼續按自己的規則置中。&lt;/p>
&lt;h3 id="兩種共存模式比較">兩種共存模式比較&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模式&lt;/th>
 &lt;th>中央欄置中&lt;/th>
 &lt;th>維護成本&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>排擠式（同 layout 流）&lt;/td>
 &lt;td>受 sidebar 影響、需要重算&lt;/td>
 &lt;td>低 — 純 CSS&lt;/td>
 &lt;td>兩個元件都要自然撐開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>疊層式（absolute）&lt;/td>
 &lt;td>不受影響、永遠按 viewport 置中&lt;/td>
 &lt;td>中 — absolute 需要定位基準&lt;/td>
 &lt;td>中央欄要嚴格置中、sidebar 是「附加」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇疊層式的時機：&lt;strong>中央欄的位置是設計重點、sidebar 是補充&lt;/strong>。本案搜尋頁的 main 是內容主體、filter 是輔助 — 適合疊層。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際做法">這次任務的實際做法&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁 desktop layout：&lt;/p>
&lt;ul>
&lt;li>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 寬度 70ch、theme 預設 &lt;code>margin-inline: auto&lt;/code> 置中&lt;/li>
&lt;li>想加 filter sidebar 在 main 左外側、寬度 400px&lt;/li>
&lt;/ul>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 filter 放進 main 的 grid（變成 main 的子 column），main 內容會被推到右半邊、不再置中。&lt;/p>
&lt;p>讓 filter 用 &lt;code>position: absolute&lt;/code> 相對 main 定位 — main 完全不知道 filter 存在、繼續置中。filter 在 main 外側「貼著」main 的左邊緣。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="nt">main&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="nn">main-content&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">relative&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* filter 的 offset parent */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="c">/* main 的 max-width: 70ch 與 margin-inline: auto 由 theme 提供，不動 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">calc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="kt">%&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">rem&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c">/* main 的左外側、間距 2rem */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>right: calc(100% + 2rem)&lt;/code> 的含義：filter 的右邊緣 = main 左邊緣 - 2rem。filter 從這個 anchor 往左展開 400px。&lt;/p>
&lt;p>main 永遠按 viewport 置中、filter 永遠貼在 main 左外側。&lt;/p>
&lt;hr>
&lt;h2 id="疊層共存的三個關鍵要素">疊層共存的三個關鍵要素&lt;/h2>
&lt;h3 id="1-offset-parent-的選擇">1. Offset parent 的選擇&lt;/h3>
&lt;p>絕對定位元件的座標相對於「最近的 positioned ancestor」。要讓 sidebar 跟著 main 一起移動（不要跟 viewport 走），就把 main 設為 &lt;code>position: relative&lt;/code> 當作 offset parent。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="nt">main&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">relative&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 相對 main */&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-anchor-點的選擇">2. Anchor 點的選擇&lt;/h3>
&lt;p>&lt;code>right: calc(100% + 2rem)&lt;/code>、&lt;code>left: -432px&lt;/code>、&lt;code>right: 100%; margin-right: 2rem&lt;/code> 三種寫法視覺上等價。選擇可讀性最高的 — 通常是 &lt;code>right: calc(100% + 2rem)&lt;/code>，意義最直接（「我的右緣 = parent 寬度 + 2rem 之外」）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>置中元件與指定位置元件共存，正確做法是讓兩者位於不同 layout 層。</strong> Layout 流負責「以內容驅動的尺寸與置中」；絕對定位負責「貼在 layout 流之上的固定位置元件」。兩者用疊層共存、不互相排擠。</p>
<hr>
<h2 id="為什麼排擠式做法行不通">為什麼排擠式做法行不通</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>排擠式：把兩個元件放進同一個 grid / flex container、各佔一個欄位。問題在於「容器寬度有限、加一個欄位就壓縮另一個」 — 中央欄想置中時，加 sidebar 後整個 layout 重新分佈、中央欄被推到非置中位置。</p>
<p>疊層式：sidebar 用 <code>position: absolute</code> 從 layout 流跳出 — 中央欄看不到 sidebar、繼續按自己的規則置中。</p>
<h3 id="兩種共存模式比較">兩種共存模式比較</h3>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>中央欄置中</th>
          <th>維護成本</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>排擠式（同 layout 流）</td>
          <td>受 sidebar 影響、需要重算</td>
          <td>低 — 純 CSS</td>
          <td>兩個元件都要自然撐開</td>
      </tr>
      <tr>
          <td>疊層式（absolute）</td>
          <td>不受影響、永遠按 viewport 置中</td>
          <td>中 — absolute 需要定位基準</td>
          <td>中央欄要嚴格置中、sidebar 是「附加」</td>
      </tr>
  </tbody>
</table>
<p>選擇疊層式的時機：<strong>中央欄的位置是設計重點、sidebar 是補充</strong>。本案搜尋頁的 main 是內容主體、filter 是輔助 — 適合疊層。</p>
<hr>
<h2 id="這次任務的實際做法">這次任務的實際做法</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁 desktop layout：</p>
<ul>
<li><code>&lt;main&gt;</code> 寬度 70ch、theme 預設 <code>margin-inline: auto</code> 置中</li>
<li>想加 filter sidebar 在 main 左外側、寬度 400px</li>
</ul>
<h3 id="判讀">判讀</h3>
<p>把 filter 放進 main 的 grid（變成 main 的子 column），main 內容會被推到右半邊、不再置中。</p>
<p>讓 filter 用 <code>position: absolute</code> 相對 main 定位 — main 完全不知道 filter 存在、繼續置中。filter 在 main 外側「貼著」main 的左邊緣。</p>
<h3 id="執行">執行</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="nt">main</span><span class="p">#</span><span class="nn">main-content</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">relative</span><span class="p">;</span>   <span class="c">/* filter 的 offset parent */</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c">/* main 的 max-width: 70ch 與 margin-inline: auto 由 theme 提供，不動 */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">right</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="mi">100</span><span class="kt">%</span> <span class="o">+</span> <span class="mi">2</span><span class="kt">rem</span><span class="p">);</span>   <span class="c">/* main 的左外側、間距 2rem */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">width</span><span class="p">:</span> <span class="mi">400</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>right: calc(100% + 2rem)</code> 的含義：filter 的右邊緣 = main 左邊緣 - 2rem。filter 從這個 anchor 往左展開 400px。</p>
<p>main 永遠按 viewport 置中、filter 永遠貼在 main 左外側。</p>
<hr>
<h2 id="疊層共存的三個關鍵要素">疊層共存的三個關鍵要素</h2>
<h3 id="1-offset-parent-的選擇">1. Offset parent 的選擇</h3>
<p>絕對定位元件的座標相對於「最近的 positioned ancestor」。要讓 sidebar 跟著 main 一起移動（不要跟 viewport 走），就把 main 設為 <code>position: relative</code> 當作 offset parent。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="nt">main</span> <span class="p">{</span> <span class="k">position</span><span class="p">:</span> <span class="kc">relative</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span> <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span> <span class="c">/* 相對 main */</span> <span class="p">}</span></span></span></code></pre></div><h3 id="2-anchor-點的選擇">2. Anchor 點的選擇</h3>
<p><code>right: calc(100% + 2rem)</code>、<code>left: -432px</code>、<code>right: 100%; margin-right: 2rem</code> 三種寫法視覺上等價。選擇可讀性最高的 — 通常是 <code>right: calc(100% + 2rem)</code>，意義最直接（「我的右緣 = parent 寬度 + 2rem 之外」）。</p>
<h3 id="3-物理空間檢查">3. 物理空間檢查</h3>
<p>絕對定位不檢查 viewport 邊界 — sidebar 可能被推到 viewport 外。需要在 breakpoint 確認「viewport 夠寬時才顯示 sidebar」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">min-width</span><span class="o">:</span> <span class="nt">1400px</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>物理空間預算的細節參見〈跨 viewport 雙模式 UI 的物理空間預算〉。</p>
<hr>
<h2 id="內在屬性比較兩種共存做法">內在屬性比較：兩種共存做法</h2>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>排擠式（grid / flex）</th>
          <th>疊層式（absolute）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>中央欄位置</td>
          <td>隨 sidebar 寬度變動</td>
          <td>不受影響、嚴格置中</td>
      </tr>
      <tr>
          <td>Sidebar 寬度限制</td>
          <td>來自 grid container</td>
          <td>來自 viewport（需 breakpoint 控制）</td>
      </tr>
      <tr>
          <td>Layout 重算成本</td>
          <td>改 sidebar 寬度時 main 跟著動</td>
          <td>main 永遠不動</td>
      </tr>
      <tr>
          <td>適用情境</td>
          <td>兩個元件都要自然撐開</td>
          <td>中央嚴格置中、sidebar 附加</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>先確認中央欄的位置要求</strong>。要嚴格置中 → 疊層式；可隨 sidebar 浮動 → 排擠式。</p>
<hr>
<h2 id="設計取捨兩元件共存的-layout-策略">設計取捨：兩元件共存的 layout 策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（疊層 absolute）當預設、其他做法在特定情境合理。</p>
<h3 id="a疊層式中央-layout-flow--附加-absolute這個專案的預設">A：疊層式（中央 layout flow + 附加 absolute）（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：嚴格置中的元件留 layout flow、附加元件 <code>position: absolute</code> 跳出 flow</li>
<li><strong>選 A 的理由</strong>：中央位置不受附加元件影響、改附加元件不需重算中央</li>
<li><strong>適合</strong>：中央嚴格置中 + sidebar 是「附加」的場景（搜尋頁、文章閱讀頁）</li>
<li><strong>代價</strong>：absolute 需要明確 offset parent、要做物理空間檢查避免溢出</li>
</ul>
<h3 id="b排擠式同-layout-flowgrid--flex">B：排擠式（同 layout flow、grid / flex）</h3>
<ul>
<li><strong>機制</strong>：把兩元件放進同一 grid / flex container、各佔一個欄位</li>
<li><strong>跟 A 的取捨</strong>：B 兩元件都自然撐開、A 中央嚴格置中；B 改一邊另一邊跟著動、A 完全解耦</li>
<li><strong>B 比 A 好的情境</strong>：兩元件都是內容主體、需要互相適應寬度（dashboard 多面板）</li>
</ul>
<h3 id="cfixed相對-viewport">C：Fixed（相對 viewport）</h3>
<ul>
<li><strong>機制</strong>：附加元件 <code>position: fixed</code>、永遠在 viewport 同位置</li>
<li><strong>跟 A 的取捨</strong>：C 永遠可見（隨 scroll 不動）、A 跟內容一起 scroll；C 適合 always-on UI</li>
<li><strong>C 比 A 好的情境</strong>：浮動操作鈕、頂部 nav、使用者隨時要 access 的元件</li>
</ul>
<h3 id="d重新設計-layout取消共存需求">D：重新設計 layout（取消共存需求）</h3>
<ul>
<li><strong>機制</strong>：把附加元件搬到完全不同位置（footer、modal、抽屜）</li>
<li><strong>跟 A/B/C 的取捨</strong>：D 完全避開共存問題、A/B/C 解決共存</li>
<li><strong>D 比 A 好的情境</strong>：附加元件使用頻率低（offset 很大價值低）— 例如 settings 改放 modal 內</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>加了 sidebar 後中央內容不再置中</td>
          <td>用了排擠式、中央欄被推</td>
          <td>改用 absolute、main 設 <code>position: relative</code></td>
      </tr>
      <tr>
          <td>Absolute sidebar 跟著 viewport 跑、不貼 main</td>
          <td>沒設 offset parent</td>
          <td>給 main 加 <code>position: relative</code></td>
      </tr>
      <tr>
          <td>Sidebar 在窄 viewport 溢出畫面</td>
          <td>沒做物理空間檢查</td>
          <td>加 breakpoint、寬度不夠時 <code>display: none</code></td>
      </tr>
      <tr>
          <td>改 sidebar 寬度時要回頭調 main 樣式</td>
          <td>排擠式造成 layout 耦合</td>
          <td>改用疊層、main 永遠不需要因 sidebar 改</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：兩個元件的視覺關係用疊層描述、不用排擠描述。疊層 = 兩層獨立 = 改一邊不影響另一邊。</p>
]]></content:encoded></item><item><title>同一個元件在三種互動狀態下顯示位置不同的 root cause</title><link>https://tarrragon.github.io/blog/report/component-tristate-root-cause/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/component-tristate-root-cause/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>元件位置 = 定位基準（anchor）+ 相對基準的偏移。元件「跟著狀態飄」不是元件本身的問題、是它的 anchor 隨狀態在動。&lt;/strong> Debug 時把元件位置拆成「找錨點 → 算偏移」兩層、確認哪一層在隨狀態變化。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼狀態化錯位的根因不在元件本身">為什麼狀態化錯位的根因不在元件本身&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS 計算元件位置時，元件總是「相對某個 reference」 — block flow 是「上一個 sibling 的下緣」、absolute 是 offset parent、grid item 是 grid container。&lt;strong>這個 reference 才是元件位置的決定因素&lt;/strong>。&lt;/p>
&lt;p>當 reference 在不同狀態下尺寸或位置變動，元件被動跟著動 — 看起來是元件「自己飄」，根因卻在 reference。&lt;/p>
&lt;h3 id="三層拆解-debug-法">三層拆解 debug 法&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>問題&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. 元件本身&lt;/td>
 &lt;td>元件 CSS 規則錯了？&lt;/td>
 &lt;td>看元件的 computed style&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. 元件的 reference&lt;/td>
 &lt;td>reference 在動嗎？尺寸隨狀態變動？&lt;/td>
 &lt;td>量 reference 在每個狀態下的 bounding rect&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. Reference 的 reference&lt;/td>
 &lt;td>上一層也在動嗎？&lt;/td>
 &lt;td>一層一層往上追&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數狀態化錯位的根因在第 2 或第 3 層、不在第 1 層。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>新加 scope UI（搜尋範圍 radio group）後出現三個狀態的位置不一致：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>scope UI 位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>初始載入（pagefind 還沒 mount）&lt;/td>
 &lt;td>緊接 H1 下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>點擊 input（focus、空查詢）&lt;/td>
 &lt;td>在 input 與 results 區之間（如預期）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>輸入查詢（results 載入後）&lt;/td>
 &lt;td>跑到所有結果的最下方&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第一輪猜測：scope UI 自己的 CSS 在不同狀態下不同 — 用 playwright 看 computed style，發現三狀態下 scope 的 grid-row 都是 3、CSS 屬性沒變。&lt;/p>
&lt;p>第二輪：用 playwright &lt;code>getBoundingClientRect()&lt;/code> 量 scope 的位置，發現 y 座標確實在三狀態下不同。&lt;/p>
&lt;p>第三輪：往上一層看「scope 的 grid container 是誰、container 的 grid template 在不同狀態下變了嗎」。發現 search-shell 的 grid template-rows 是 &lt;code>auto&lt;/code>、自動依子元素內容撐開。&lt;/p>
&lt;p>關鍵發現：&lt;strong>&lt;code>.pagefind-ui__drawer&lt;/code> 不是 &lt;code>.pagefind-ui&lt;/code> 的直接子節點 — 它在 &lt;code>&amp;lt;form&amp;gt;&lt;/code> 內&lt;/strong>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">.pagefind-ui (display: contents)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">└── form.pagefind-ui__form (grid-row: 2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └── div.pagefind-ui__drawer (grid-row: 4 設了沒生效)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>於是：&lt;/p>
&lt;ul>
&lt;li>初始：form 只含 input、row 2 矮、scope 在 row 3 緊接 row 2 下。&lt;/li>
&lt;li>輸入後：form 含 input + drawer（187 個結果）、row 2 撐到全頁高。grid-row 4 比 row 2 後 — 但 drawer 被 form 包住、整個 form 在 row 2 — scope（row 3）在 form 之後 = 結果之後。&lt;/li>
&lt;/ul>
&lt;p>scope 的 anchor（grid container 的 row 排列）在 form 撐開時改變 — anchor 在動、scope 跟著動。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>元件位置 = 定位基準（anchor）+ 相對基準的偏移。元件「跟著狀態飄」不是元件本身的問題、是它的 anchor 隨狀態在動。</strong> Debug 時把元件位置拆成「找錨點 → 算偏移」兩層、確認哪一層在隨狀態變化。</p>
<hr>
<h2 id="為什麼狀態化錯位的根因不在元件本身">為什麼狀態化錯位的根因不在元件本身</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS 計算元件位置時，元件總是「相對某個 reference」 — block flow 是「上一個 sibling 的下緣」、absolute 是 offset parent、grid item 是 grid container。<strong>這個 reference 才是元件位置的決定因素</strong>。</p>
<p>當 reference 在不同狀態下尺寸或位置變動，元件被動跟著動 — 看起來是元件「自己飄」，根因卻在 reference。</p>
<h3 id="三層拆解-debug-法">三層拆解 debug 法</h3>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>問題</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 元件本身</td>
          <td>元件 CSS 規則錯了？</td>
          <td>看元件的 computed style</td>
      </tr>
      <tr>
          <td>2. 元件的 reference</td>
          <td>reference 在動嗎？尺寸隨狀態變動？</td>
          <td>量 reference 在每個狀態下的 bounding rect</td>
      </tr>
      <tr>
          <td>3. Reference 的 reference</td>
          <td>上一層也在動嗎？</td>
          <td>一層一層往上追</td>
      </tr>
  </tbody>
</table>
<p>多數狀態化錯位的根因在第 2 或第 3 層、不在第 1 層。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>新加 scope UI（搜尋範圍 radio group）後出現三個狀態的位置不一致：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>scope UI 位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>初始載入（pagefind 還沒 mount）</td>
          <td>緊接 H1 下方</td>
      </tr>
      <tr>
          <td>點擊 input（focus、空查詢）</td>
          <td>在 input 與 results 區之間（如預期）</td>
      </tr>
      <tr>
          <td>輸入查詢（results 載入後）</td>
          <td>跑到所有結果的最下方</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>第一輪猜測：scope UI 自己的 CSS 在不同狀態下不同 — 用 playwright 看 computed style，發現三狀態下 scope 的 grid-row 都是 3、CSS 屬性沒變。</p>
<p>第二輪：用 playwright <code>getBoundingClientRect()</code> 量 scope 的位置，發現 y 座標確實在三狀態下不同。</p>
<p>第三輪：往上一層看「scope 的 grid container 是誰、container 的 grid template 在不同狀態下變了嗎」。發現 search-shell 的 grid template-rows 是 <code>auto</code>、自動依子元素內容撐開。</p>
<p>關鍵發現：<strong><code>.pagefind-ui__drawer</code> 不是 <code>.pagefind-ui</code> 的直接子節點 — 它在 <code>&lt;form&gt;</code> 內</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">.pagefind-ui (display: contents)
</span></span><span class="line"><span class="ln">2</span><span class="cl">└── form.pagefind-ui__form (grid-row: 2)
</span></span><span class="line"><span class="ln">3</span><span class="cl">    └── div.pagefind-ui__drawer (grid-row: 4 設了沒生效)</span></span></code></pre></div><p>於是：</p>
<ul>
<li>初始：form 只含 input、row 2 矮、scope 在 row 3 緊接 row 2 下。</li>
<li>輸入後：form 含 input + drawer（187 個結果）、row 2 撐到全頁高。grid-row 4 比 row 2 後 — 但 drawer 被 form 包住、整個 form 在 row 2 — scope（row 3）在 form 之後 = 結果之後。</li>
</ul>
<p>scope 的 anchor（grid container 的 row 排列）在 form 撐開時改變 — anchor 在動、scope 跟著動。</p>
<h3 id="執行">執行</h3>
<p>確認 anchor 問題後改用 absolute 定位：scope 浮在 form 之上、drawer 用 margin-top 讓位。scope 的 anchor 改為 <code>.search-shell</code> 的 <code>position: relative</code>、不再依賴 form 的尺寸。三狀態下位置一致。</p>
<hr>
<h2 id="拆解-anchor-的四個工具">拆解 anchor 的四個工具</h2>
<h3 id="1-找元件的-reference">1. 找元件的 reference</h3>
<table>
  <thead>
      <tr>
          <th>元件的 position</th>
          <th>Reference</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>static（預設）</td>
          <td>上一個 sibling 的下緣 / 父 container</td>
      </tr>
      <tr>
          <td>relative</td>
          <td>元件原本在 flow 中的位置</td>
      </tr>
      <tr>
          <td>absolute</td>
          <td>最近的 positioned ancestor</td>
      </tr>
      <tr>
          <td>fixed</td>
          <td>viewport</td>
      </tr>
      <tr>
          <td>sticky</td>
          <td>滾動容器</td>
      </tr>
      <tr>
          <td>Grid item</td>
          <td>Grid container 的 cell</td>
      </tr>
      <tr>
          <td>Flex item</td>
          <td>Flex container 的軸線</td>
      </tr>
  </tbody>
</table>
<h3 id="2-用-getboundingclientrect-量">2. 用 <code>getBoundingClientRect</code> 量</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">getBoundingClientRect</span><span class="p">());</span></span></span></code></pre></div><p>在三個狀態下分別量、比對 y 座標。差異對應到「reference 在動」。</p>
<h3 id="3-往上追-ancestor-chain">3. 往上追 ancestor chain</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">let</span> <span class="nx">parents</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">let</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">parents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">+</span> <span class="s1">&#39;.&#39;</span> <span class="o">+</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">el</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">parents</span><span class="p">);</span></span></span></code></pre></div><p>找出 reference 是誰、reference 的 reference 是誰、一層一層追到「不會動」的元素。</p>
<h3 id="4-computed-style-vs-dom-tree-一起看">4. Computed style vs DOM tree 一起看</h3>
<p>CSS 規則在 computed style 顯示為「我設了什麼」、DOM tree 顯示「實際巢狀關係」。兩者一起看才知道規則為什麼沒生效。</p>
<hr>
<h2 id="內在屬性比較三種定位策略對狀態化錯位的抵抗">內在屬性比較：三種定位策略對狀態化錯位的抵抗</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>Anchor 穩定性</th>
          <th>狀態化飄移風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Static / block flow</td>
          <td>低 — 任何前置元素變動都影響</td>
          <td>高 — sibling 撐高就被推下去</td>
      </tr>
      <tr>
          <td>Grid / Flex item</td>
          <td>中 — 跟 container 設計綁定</td>
          <td>中 — container row 撐開時跟著動</td>
      </tr>
      <tr>
          <td>Absolute（自定義 offset parent）</td>
          <td>高 — anchor 是固定 ancestor</td>
          <td>低 — anchor 不變則元件不動</td>
      </tr>
      <tr>
          <td>Fixed</td>
          <td>最高 — anchor 是 viewport</td>
          <td>不會因內容變動飄移、但會因捲動變化</td>
      </tr>
  </tbody>
</table>
<p>當一個元件需要在多種狀態下保持固定位置 — 優先 absolute（搭配明確的 offset parent）。</p>
<hr>
<h2 id="設計取捨對抗狀態化飄移的定位策略">設計取捨：對抗狀態化飄移的定位策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（absolute + 自定義 offset parent）當預設、其他做法在特定情境合理。</p>
<h3 id="aabsolute--穩定-offset-parent這個專案的預設">A：Absolute + 穩定 offset parent（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：元件 <code>position: absolute</code>、選定一個尺寸不隨狀態變動的 ancestor 作為 offset parent</li>
<li><strong>選 A 的理由</strong>：anchor 不變則元件不動、跨所有互動狀態位置一致</li>
<li><strong>適合</strong>：需要在多狀態下保持固定位置的元件</li>
<li><strong>代價</strong>：跳出 layout flow、附近元件需要手動讓位（margin spacer）</li>
</ul>
<h3 id="bgrid--flex-item">B：Grid / Flex item</h3>
<ul>
<li><strong>機制</strong>：把元件當 grid / flex container 的子項、用 grid-row / flex-order 排</li>
<li><strong>跟 A 的取捨</strong>：B 自然 reflow、A 完全 anchor-driven；B 在 container 內容隨狀態撐開時、grid 排序跟著重算</li>
<li><strong>B 比 A 好的情境</strong>：container 尺寸不隨狀態變動的場景（純 layout、內容靜態）</li>
</ul>
<h3 id="cstatic--block-flow預設-layout">C：Static / block flow（預設 layout）</h3>
<ul>
<li><strong>機制</strong>：不設 position、跟 sibling 自然排</li>
<li><strong>跟 A/B 的取捨</strong>：C 最簡單、A/B 主動處理 anchor；C 完全受前置 sibling 影響、狀態化飄移風險最高</li>
<li><strong>C 才合理的情境</strong>：頁面內容極穩定、無狀態切換 — 否則第 N 個元素位置受前 N-1 個元素影響</li>
</ul>
<h3 id="dfixed相對-viewport">D：Fixed（相對 viewport）</h3>
<ul>
<li><strong>機制</strong>：<code>position: fixed</code>、anchor 是 viewport</li>
<li><strong>跟 A 的取捨</strong>：D 永遠在 viewport 同位置、A 跟著內容；D 對「導航類元件」合理、對「內容相關元件」不合理</li>
<li><strong>D 比 A 好的情境</strong>：永遠可見的功能元件（toolbar、scroll-to-top button）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件位置在不同互動狀態下不同</td>
          <td>Anchor 隨狀態變動</td>
          <td>用 playwright 量三個狀態下的 bounding rect</td>
      </tr>
      <tr>
          <td>Computed style 三狀態下都一樣、但位置不同</td>
          <td>Reference 元素的尺寸在動</td>
          <td>量 reference 元素的尺寸、確認哪個狀態下變大</td>
      </tr>
      <tr>
          <td>改元件 CSS 一個狀態好了、另一個壞</td>
          <td>用了 reference-dependent layout</td>
          <td>改用 absolute、選擇穩定的 offset parent</td>
      </tr>
      <tr>
          <td>元件初始正確、互動後跑掉</td>
          <td>Reference 因 reactivity 撐開</td>
          <td>找出該 reference、用 absolute 跳出其影響</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：元件「會飄」不是元件的個性、是它依賴的東西在飄。先找飄的源頭，不要追著元件改。</p>
]]></content:encoded></item></channel></rss>