<?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>Svelte on Tarragon</title><link>https://tarrragon.github.io/blog/tags/svelte/</link><description>Recent content in Svelte 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/svelte/index.xml" rel="self" type="application/rss+xml"/><item><title>在外部組件上加客製功能：以邊界為中心的方法選擇</title><link>https://tarrragon.github.io/blog/report/external-component-customization/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-component-customization/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>客製外部組件的穩定性與「離組件邊界多近」成正比。&lt;/strong> 組件作者維護組件內部的一致性；客製貼著組件的對外介面（CLI 參數、CSS reset 邊界、CSS layer）做，組件升級不會打到客製。深入組件內部（注入子節點、改 source 行為）的客製則仰賴組件的內部實作，作者不保證下個版本還相容。&lt;/p>
&lt;p>這條原則對應到 Pagefind 整合時的三個具體情境，本文逐一拆解。&lt;/p>
&lt;hr>
&lt;h2 id="邊界概念為什麼用邊界看外部組件">邊界概念：為什麼用「邊界」看外部組件&lt;/h2>
&lt;h3 id="這類問題的本質商業邏輯">這類問題的本質（商業邏輯）&lt;/h3>
&lt;p>組件 = 一組對外契約 + 內部實作。對外契約是作者保證的東西（CLI 參數、CSS class name、CSS variable hooks），內部實作是達成契約的手段、可能在版本之間變動。&lt;/p>
&lt;p>客製貼著對外契約做，等於跟作者站在同一邊；客製動到內部實作，等於跟作者每個版本對抗。&lt;/p>
&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>組件對外介面&lt;/td>
 &lt;td>介面契約（穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件 DOM 邊界外&lt;/td>
 &lt;td>邊界元素的 class / id 名稱（半穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件 DOM 邊界內&lt;/td>
 &lt;td>內部 DOM 結構（隨框架渲染週期變動）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件內部邏輯&lt;/td>
 &lt;td>source code（升級時必衝突）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="這次任務涉及的邊界case">這次任務涉及的邊界（CASE）&lt;/h3>
&lt;p>Pagefind 提供三條值得辨識的邊界：&lt;/p>
&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>&lt;code>--root-selector&lt;/code> 參數&lt;/td>
 &lt;td>「組件看到什麼資料」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重置邊界&lt;/td>
 &lt;td>&lt;code>.pagefind-ui--reset&lt;/code> class&lt;/td>
 &lt;td>對 UA 樣式的覆寫範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>specificity 邊界&lt;/td>
 &lt;td>svelte hash class（雙寫到 30）&lt;/td>
 &lt;td>組件 CSS 的權重起點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每條邊界各有對應的客製介面 — 緊貼這些介面比繞道進內部安全得多。&lt;/p>
&lt;hr>
&lt;h2 id="索引邊界用-root-selector-限縮搜尋範圍">索引邊界：用 root-selector 限縮搜尋範圍&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：Pagefind 的索引流程透過 &lt;code>--root-selector&lt;/code> 參數控制「哪些 HTML 進索引」。這是組件預期的客製介面。&lt;/p>
&lt;h3 id="這次的觀察">這次的觀察&lt;/h3>
&lt;p>theme 的 site header 包含 &lt;code>&amp;lt;h1&amp;gt;{{ .Site.Title }}&amp;lt;/h1&amp;gt;&lt;/code>，每一頁 DOM 上的第一個 h1 都是站名「Tarragon」。Pagefind 預設取頁面第一個 h1 當搜尋結果 title — 結果所有結果都顯示「Tarragon」。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第一個 h1 是站名而非文章名 — 索引「看到」的範圍跟我們以為的不同。要修的不是 theme（影響其他頁面），是 Pagefind 的 input 範圍。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">npx -y pagefind --site public --root-selector main&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 內才包含文章 h1；site header 在 &lt;code>&amp;lt;body&amp;gt;&lt;/code> 直接子節點、不在 &lt;code>&amp;lt;main&amp;gt;&lt;/code> 內，自然被排除。&lt;/p>
&lt;p>&lt;strong>完成標準&lt;/strong>：每一筆結果的 title 顯示文章 h1，而非站名。&lt;/p>
&lt;hr>
&lt;h2 id="重置邊界在落腳處重建-ua-樣式">重置邊界：在落腳處重建 UA 樣式&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：CSS reset 的有效範圍與 ancestor class 綁定。元素換位置就換 reset context — 這是 CSS 的基本機制，不是 bug。&lt;/p>
&lt;h3 id="這次的觀察-1">這次的觀察&lt;/h3>
&lt;p>Filter UI（&lt;code>.pagefind-ui__filter-panel&lt;/code>，本質是 &lt;code>&amp;lt;fieldset&amp;gt;&lt;/code>）需要從 &lt;code>.pagefind-ui&lt;/code> 搬到外部 aside（左側 sidebar）。搬完後 fieldset 的瀏覽器預設邊框冒出來。&lt;/p>
&lt;h3 id="判讀-1">判讀&lt;/h3>
&lt;p>&lt;code>.pagefind-ui--reset&lt;/code> 用 &lt;code>all: unset&lt;/code> 重置所有後代元素的 UA 樣式。fieldset 搬出 &lt;code>.pagefind-ui&lt;/code> 後，這個 ancestor 不在了，UA 樣式（包括 fieldset 預設邊框）回到原樣。&lt;/p>
&lt;h3 id="執行-1">執行&lt;/h3>
&lt;p>在 fieldset 落腳處（aside 內）重新關掉 UA 樣式：&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-filter-slot&lt;/span> &lt;span class="nt">fieldset&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">border&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">3&lt;/span>&lt;span class="cl"> &lt;span class="k">padding&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">4&lt;/span>&lt;span class="cl"> &lt;span class="k">margin&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">5&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;strong>完成標準&lt;/strong>：fieldset 在新位置看起來跟在 &lt;code>.pagefind-ui&lt;/code> 內一樣 — 沒有多餘邊框。&lt;/p>
&lt;hr>
&lt;h2 id="specificity-邊界跳出線性比較戰場">specificity 邊界：跳出線性比較戰場&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：當組件透過 hash class 把 specificity 拉高到一般客製寫法蓋不過時，邊界落在 CSS 樣式分層機制 — 不在個別 selector 數字。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>客製外部組件的穩定性與「離組件邊界多近」成正比。</strong> 組件作者維護組件內部的一致性；客製貼著組件的對外介面（CLI 參數、CSS reset 邊界、CSS layer）做，組件升級不會打到客製。深入組件內部（注入子節點、改 source 行為）的客製則仰賴組件的內部實作，作者不保證下個版本還相容。</p>
<p>這條原則對應到 Pagefind 整合時的三個具體情境，本文逐一拆解。</p>
<hr>
<h2 id="邊界概念為什麼用邊界看外部組件">邊界概念：為什麼用「邊界」看外部組件</h2>
<h3 id="這類問題的本質商業邏輯">這類問題的本質（商業邏輯）</h3>
<p>組件 = 一組對外契約 + 內部實作。對外契約是作者保證的東西（CLI 參數、CSS class name、CSS variable hooks），內部實作是達成契約的手段、可能在版本之間變動。</p>
<p>客製貼著對外契約做，等於跟作者站在同一邊；客製動到內部實作，等於跟作者每個版本對抗。</p>
<p>「邊界」這個概念把組件分成「契約面」與「實作面」 — 客製選位置時，依離邊界遠近排序：</p>
<table>
  <thead>
      <tr>
          <th>客製位置</th>
          <th>跨越時的依賴</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件對外介面</td>
          <td>介面契約（穩定）</td>
      </tr>
      <tr>
          <td>組件 DOM 邊界外</td>
          <td>邊界元素的 class / id 名稱（半穩定）</td>
      </tr>
      <tr>
          <td>組件 DOM 邊界內</td>
          <td>內部 DOM 結構（隨框架渲染週期變動）</td>
      </tr>
      <tr>
          <td>組件內部邏輯</td>
          <td>source code（升級時必衝突）</td>
      </tr>
  </tbody>
</table>
<h3 id="這次任務涉及的邊界case">這次任務涉及的邊界（CASE）</h3>
<p>Pagefind 提供三條值得辨識的邊界：</p>
<table>
  <thead>
      <tr>
          <th>邊界名稱</th>
          <th>介面形式</th>
          <th>範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引邊界</td>
          <td><code>--root-selector</code> 參數</td>
          <td>「組件看到什麼資料」</td>
      </tr>
      <tr>
          <td>重置邊界</td>
          <td><code>.pagefind-ui--reset</code> class</td>
          <td>對 UA 樣式的覆寫範圍</td>
      </tr>
      <tr>
          <td>specificity 邊界</td>
          <td>svelte hash class（雙寫到 30）</td>
          <td>組件 CSS 的權重起點</td>
      </tr>
  </tbody>
</table>
<p>每條邊界各有對應的客製介面 — 緊貼這些介面比繞道進內部安全得多。</p>
<hr>
<h2 id="索引邊界用-root-selector-限縮搜尋範圍">索引邊界：用 root-selector 限縮搜尋範圍</h2>
<p><strong>核心定義</strong>：Pagefind 的索引流程透過 <code>--root-selector</code> 參數控制「哪些 HTML 進索引」。這是組件預期的客製介面。</p>
<h3 id="這次的觀察">這次的觀察</h3>
<p>theme 的 site header 包含 <code>&lt;h1&gt;{{ .Site.Title }}&lt;/h1&gt;</code>，每一頁 DOM 上的第一個 h1 都是站名「Tarragon」。Pagefind 預設取頁面第一個 h1 當搜尋結果 title — 結果所有結果都顯示「Tarragon」。</p>
<h3 id="判讀">判讀</h3>
<p>第一個 h1 是站名而非文章名 — 索引「看到」的範圍跟我們以為的不同。要修的不是 theme（影響其他頁面），是 Pagefind 的 input 範圍。</p>
<h3 id="執行">執行</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">npx -y pagefind --site public --root-selector main</span></span></code></pre></div><p><code>&lt;main&gt;</code> 內才包含文章 h1；site header 在 <code>&lt;body&gt;</code> 直接子節點、不在 <code>&lt;main&gt;</code> 內，自然被排除。</p>
<p><strong>完成標準</strong>：每一筆結果的 title 顯示文章 h1，而非站名。</p>
<hr>
<h2 id="重置邊界在落腳處重建-ua-樣式">重置邊界：在落腳處重建 UA 樣式</h2>
<p><strong>核心定義</strong>：CSS reset 的有效範圍與 ancestor class 綁定。元素換位置就換 reset context — 這是 CSS 的基本機制，不是 bug。</p>
<h3 id="這次的觀察-1">這次的觀察</h3>
<p>Filter UI（<code>.pagefind-ui__filter-panel</code>，本質是 <code>&lt;fieldset&gt;</code>）需要從 <code>.pagefind-ui</code> 搬到外部 aside（左側 sidebar）。搬完後 fieldset 的瀏覽器預設邊框冒出來。</p>
<h3 id="判讀-1">判讀</h3>
<p><code>.pagefind-ui--reset</code> 用 <code>all: unset</code> 重置所有後代元素的 UA 樣式。fieldset 搬出 <code>.pagefind-ui</code> 後，這個 ancestor 不在了，UA 樣式（包括 fieldset 預設邊框）回到原樣。</p>
<h3 id="執行-1">執行</h3>
<p>在 fieldset 落腳處（aside 內）重新關掉 UA 樣式：</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="nt">fieldset</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>完成標準</strong>：fieldset 在新位置看起來跟在 <code>.pagefind-ui</code> 內一樣 — 沒有多餘邊框。</p>
<hr>
<h2 id="specificity-邊界跳出線性比較戰場">specificity 邊界：跳出線性比較戰場</h2>
<p><strong>核心定義</strong>：當組件透過 hash class 把 specificity 拉高到一般客製寫法蓋不過時，邊界落在 CSS 樣式分層機制 — 不在個別 selector 數字。</p>
<h3 id="這次的觀察-2">這次的觀察</h3>
<p>Pagefind 透過 svelte 把 class name 加 hash 重複寫進 selector（<code>.x.svelte-y.svelte-y</code>），把 specificity 從 10 提升到 30。一般客製 CSS specificity 10-20、蓋不過去。</p>
<h3 id="判讀-2">判讀</h3>
<p>Specificity 是線性數字比較 — 跟組件作者比 specificity 是無贏的軍備競賽。要真正解這類覆寫戰、需要跳出「線性比較」這個維度本身。</p>
<h3 id="執行-2">執行</h3>
<p>具體做法（<code>@import url(...) layer(...)</code>）與升級兼容性、其他外部組件的 layer 策略，由 <a href="../css-layers-over-specificity/">#24 CSS Layers 取代 specificity 戰</a> 完整展開。在邊界辨識上、本篇要記住的是：<strong>遇到 specificity 30+ 的覆寫戰、不要往 <code>!important</code> / <code>.x.x</code> 雙寫的方向加碼、改去看 layer 維度</strong>。</p>
<p><strong>完成標準</strong>：所有原本需要 <code>!important</code> 或 <code>.x.x</code> 雙寫的覆寫，可以用單純的 class selector 寫。</p>
<hr>
<h2 id="客製深度的內在屬性比較">客製深度的內在屬性比較</h2>
<p>選擇客製位置時，用三個<strong>內在屬性</strong>比較 — 不用「實作要多久」這類時間維度：</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>依賴前提</th>
          <th>升級風險</th>
          <th>可逆性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件對外介面</td>
          <td>介面 stable across 版本</td>
          <td>低 — 介面是公開契約</td>
          <td>高 — 改一個參數即可還原</td>
      </tr>
      <tr>
          <td>DOM 邊界外（class hooks）</td>
          <td>邊界元素 class / id 穩定</td>
          <td>中 — class 改名才打破</td>
          <td>中 — 改選擇器即可</td>
      </tr>
      <tr>
          <td>DOM 邊界內</td>
          <td>內部 DOM 結構穩定</td>
          <td>高 — 框架重繪可能即時打破</td>
          <td>低 — 客製跟內部結構深度耦合</td>
      </tr>
      <tr>
          <td>內部邏輯（fork / patch）</td>
          <td>整個組件 source</td>
          <td>最高 — 升級必衝突</td>
          <td>最低 — 重新 merge</td>
      </tr>
  </tbody>
</table>
<p><strong>選擇順序</strong>：先試最外層；不夠用再往內推一層。每往內一層、依賴前提增加、升級風險上升、可逆性下降。</p>
<hr>
<h2 id="設計取捨客製位置的層次選擇">設計取捨：客製位置的層次選擇</h2>
<p>四種做法、各自機會成本不同。預設選最外層（A）、不夠用才往內推一層。</p>
<blockquote>
<p>本篇是 <a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a> 抽象原則在「客製位置選擇」這個面向的應用。</p></blockquote>
<h3 id="a組件公開介面層最佳">A：組件公開介面層（最佳）</h3>
<ul>
<li><strong>機制</strong>：用組件提供的 CLI 參數、props、CSS variable hook、event API 客製</li>
<li><strong>選 A 的理由</strong>：作者保證跨版本相容、客製跟組件升級無關</li>
<li><strong>適合</strong>：能被介面覆蓋的客製需求（多數常見場景）</li>
<li><strong>代價</strong>：受介面範圍限制、超出範圍只能往內推</li>
</ul>
<h3 id="b鄰接層class-hooksreset-邊界這個專案的次選">B：鄰接層（class hooks、reset 邊界）（這個專案的次選）</h3>
<ul>
<li><strong>機制</strong>：用組件邊界元素的 class / id / data attribute 寫客製 CSS、注意 CSS reset 邊界</li>
<li><strong>跟 A 的取捨</strong>：B 介面外但仍貼著組件邊界、A 在介面內；B 在 class 名穩定時 OK、改名就壞</li>
<li><strong>B 比 A 好的情境</strong>：組件未提供對應介面、但有穩定的 class 邊界可掛勾</li>
</ul>
<h3 id="c邊界內-dom-操作">C：邊界內 DOM 操作</h3>
<ul>
<li><strong>機制</strong>：JS 改組件內部 DOM 結構 / 節點屬性 / 注入元素</li>
<li><strong>跟 A/B 的取捨</strong>：C 直接操控、跟 framework reconciliation 競爭；風險顯著上升</li>
<li><strong>C 才合理的情境</strong>：A/B 都無法達成、且能容忍每次升級重做（<a href="../component-boundary-and-js-impact/">#13</a> 的安全規則必看）</li>
</ul>
<h3 id="dfork--patch-組件-source">D：Fork / patch 組件 source</h3>
<ul>
<li><strong>機制</strong>：fork 整個組件、改 source code、自家維護版本</li>
<li><strong>成本特別高的原因</strong>：每次升級都要 merge upstream、客製永久綁在 fork</li>
<li><strong>D 才合理的情境</strong>：客製需求超過所有其他選項、且願意承擔 fork 維護成本</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>下次遇到組件整合任務、看到這些訊號就走「找邊界」的路：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>對應邊界類型</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件抓到的資料超過你想要的範圍</td>
          <td>索引邊界</td>
          <td>找組件的 <code>root</code> / <code>scope</code> / <code>selector</code> 參數</td>
      </tr>
      <tr>
          <td>同一段 CSS 在不同位置表現不一致</td>
          <td>重置邊界</td>
          <td>比對兩個位置的 ancestor class、看 reset 是否還在</td>
      </tr>
      <tr>
          <td><code>!important</code> 寫了還是沒蓋過</td>
          <td>specificity 邊界</td>
          <td>看組件 CSS 是否用 hash class 提升 specificity；考慮 CSS Layers</td>
      </tr>
      <tr>
          <td>組件渲染後客製 UI 消失</td>
          <td>DOM 結構邊界</td>
          <td>把客製 UI 搬出組件、用 CSS 控制視覺位置</td>
      </tr>
      <tr>
          <td>組件升級後客製失效</td>
          <td>內部邏輯邊界</td>
          <td>把客製重寫到組件介面層</td>
      </tr>
  </tbody>
</table>
<p><strong>使用順序</strong>：訊號出現 → 對應邊界 → 嘗試該邊界提供的介面 → 介面不夠用、才考慮往內推一層。</p>
]]></content:encoded></item><item><title>客製 UI 留 framework 邊界外、用 CSS 控制視覺位置</title><link>https://tarrragon.github.io/blog/report/coexisting-with-framework-managed-dom/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/coexisting-with-framework-managed-dom/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>客製 UI 留在 framework 管轄的 DOM 邊界外、用 CSS（absolute、margin spacer、grid）達成想要的視覺位置。&lt;/strong> 注入 framework 子樹的客製元素會被 reconciliation 清掉、跟渲染週期競爭、行為不可預測。邊界外的客製跟 framework 解耦、命運由我們自己決定。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：客製 UI 該放哪。&lt;strong>framework 元件本身需要動（搬節點、改順序、改 attribute）的安全規則&lt;/strong>由 &lt;a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則&lt;/a> 處理。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼-framework-管轄區會清外來節點">為什麼 framework 管轄區會清外來節點&lt;/h2>
&lt;h3 id="reconciliation-機制">Reconciliation 機制&lt;/h3>
&lt;p>Svelte / React 等框架透過「component tree → DOM tree」的 reconciliation 機制保持 UI 與 state 同步。框架在 patch 時：&lt;/p>
&lt;ol>
&lt;li>比對 component tree 的當前狀態與目標狀態&lt;/li>
&lt;li>計算 DOM 需要的最小變動&lt;/li>
&lt;li>套用變動到實際 DOM&lt;/li>
&lt;/ol>
&lt;p>關鍵是步驟 2：&lt;strong>框架只認得自己 create 的節點&lt;/strong>。外來節點（我們手動 appendChild 進去的）不在它的 component tree 裡、被視為「該節點不該存在」、清掉。&lt;/p>
&lt;p>這不是 bug、是 reconciliation 的正常行為 — 框架要保證 DOM 跟 component state 一致、外來節點屬於不一致的部分。&lt;/p>
&lt;h3 id="外來節點的命運是不可預測的">外來節點的命運是不可預測的&lt;/h3>
&lt;p>不同框架 / 不同 reconciliation 策略對外來節點的處理：&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>Svelte&lt;/td>
 &lt;td>多數情境清掉、視 patch 點而定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>React&lt;/td>
 &lt;td>通常清掉（Virtual DOM diff 時）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vue&lt;/td>
 &lt;td>通常清掉、但 v-pre 包裹可保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Web Components&lt;/td>
 &lt;td>由 component 內部邏輯決定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>「不可預測」本身就是問題&lt;/strong> — 即使某次測試沒清、下次升級或 patch 時可能清。設計時不該依賴未明確保證的行為。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的具體情境">這次任務的具體情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把搜尋範圍切換 UI（scope radio group）放在「pagefind 搜尋輸入框與結果之間」 — 視覺上希望它就在 form 與 drawer 中間。&lt;/p>
&lt;p>第一次嘗試：JS 把 scope element 用 &lt;code>form.insertAdjacentElement('afterend', scopeEl)&lt;/code> 注入 &lt;code>.pagefind-ui&lt;/code> 內部。&lt;/p>
&lt;p>結果：使用者打字後 scope 消失。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>Pagefind 用 svelte 構建 UI、reactivity 監聽 search query 變動。Query 改變時 svelte 會 patch &lt;code>.pagefind-ui&lt;/code> 的子樹 — 我們注入的 scope 不是 svelte 認得的節點、被視為差異清掉。&lt;/p>
&lt;h3 id="執行邊界外--css-控制位置">執行：邊界外 + CSS 控制位置&lt;/h3>
&lt;p>策略改為「scope 留在 &lt;code>.search-shell&lt;/code> 裡（framework 邊界外）、用 CSS absolute 浮在 form 上」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-shell&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- 邊界外、永不被清 --&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- pagefind 進來這裡 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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="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-scope&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="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">4&lt;/span>&lt;span class="cl"> &lt;span class="k">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">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 class="o">+&lt;/span> &lt;span class="mi">4&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">5&lt;/span>&lt;span class="cl"> &lt;span class="c">/* ... */&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;span class="line">&lt;span class="ln">7&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>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &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="c">/* 為 scope 讓位 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>scope 不在 svelte 管轄區、永遠不被清；視覺位置靠 absolute + drawer 的 margin-top 共同決定。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>客製 UI 留在 framework 管轄的 DOM 邊界外、用 CSS（absolute、margin spacer、grid）達成想要的視覺位置。</strong> 注入 framework 子樹的客製元素會被 reconciliation 清掉、跟渲染週期競爭、行為不可預測。邊界外的客製跟 framework 解耦、命運由我們自己決定。</p>
<blockquote>
<p>本篇焦點：客製 UI 該放哪。<strong>framework 元件本身需要動（搬節點、改順序、改 attribute）的安全規則</strong>由 <a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a> 處理。</p></blockquote>
<hr>
<h2 id="為什麼-framework-管轄區會清外來節點">為什麼 framework 管轄區會清外來節點</h2>
<h3 id="reconciliation-機制">Reconciliation 機制</h3>
<p>Svelte / React 等框架透過「component tree → DOM tree」的 reconciliation 機制保持 UI 與 state 同步。框架在 patch 時：</p>
<ol>
<li>比對 component tree 的當前狀態與目標狀態</li>
<li>計算 DOM 需要的最小變動</li>
<li>套用變動到實際 DOM</li>
</ol>
<p>關鍵是步驟 2：<strong>框架只認得自己 create 的節點</strong>。外來節點（我們手動 appendChild 進去的）不在它的 component tree 裡、被視為「該節點不該存在」、清掉。</p>
<p>這不是 bug、是 reconciliation 的正常行為 — 框架要保證 DOM 跟 component state 一致、外來節點屬於不一致的部分。</p>
<h3 id="外來節點的命運是不可預測的">外來節點的命運是不可預測的</h3>
<p>不同框架 / 不同 reconciliation 策略對外來節點的處理：</p>
<table>
  <thead>
      <tr>
          <th>框架</th>
          <th>外來節點命運</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Svelte</td>
          <td>多數情境清掉、視 patch 點而定</td>
      </tr>
      <tr>
          <td>React</td>
          <td>通常清掉（Virtual DOM diff 時）</td>
      </tr>
      <tr>
          <td>Vue</td>
          <td>通常清掉、但 v-pre 包裹可保留</td>
      </tr>
      <tr>
          <td>Web Components</td>
          <td>由 component 內部邏輯決定</td>
      </tr>
  </tbody>
</table>
<p><strong>「不可預測」本身就是問題</strong> — 即使某次測試沒清、下次升級或 patch 時可能清。設計時不該依賴未明確保證的行為。</p>
<hr>
<h2 id="這次任務的具體情境">這次任務的具體情境</h2>
<h3 id="觀察">觀察</h3>
<p>要把搜尋範圍切換 UI（scope radio group）放在「pagefind 搜尋輸入框與結果之間」 — 視覺上希望它就在 form 與 drawer 中間。</p>
<p>第一次嘗試：JS 把 scope element 用 <code>form.insertAdjacentElement('afterend', scopeEl)</code> 注入 <code>.pagefind-ui</code> 內部。</p>
<p>結果：使用者打字後 scope 消失。</p>
<h3 id="判讀">判讀</h3>
<p>Pagefind 用 svelte 構建 UI、reactivity 監聽 search query 變動。Query 改變時 svelte 會 patch <code>.pagefind-ui</code> 的子樹 — 我們注入的 scope 不是 svelte 認得的節點、被視為差異清掉。</p>
<h3 id="執行邊界外--css-控制位置">執行：邊界外 + CSS 控制位置</h3>
<p>策略改為「scope 留在 <code>.search-shell</code> 裡（framework 邊界外）、用 CSS absolute 浮在 form 上」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-shell&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>      <span class="c">&lt;!-- 邊界外、永不被清 --&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;search&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>               <span class="c">&lt;!-- pagefind 進來這裡 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div>




<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="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-scope</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</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">4</span><span class="cl">  <span class="k">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">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 class="o">+</span> <span class="mi">4</span><span class="kt">px</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c">/* ... */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</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></span><span class="line"><span class="ln">8</span><span class="cl">  <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="c">/* 為 scope 讓位 */</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>scope 不在 svelte 管轄區、永遠不被清；視覺位置靠 absolute + drawer 的 margin-top 共同決定。</p>
<hr>
<h2 id="css-達成視覺位置的設計工具">CSS 達成視覺位置的設計工具</h2>
<h3 id="工具-1absolute--容器-relative">工具 1：Absolute + 容器 relative</h3>
<p>把客製 UI 設 <code>position: absolute</code>、容器設 <code>position: relative</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="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-custom</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="k">top</span><span class="p">:</span> <span class="o">...</span><span class="p">;</span> <span class="k">left</span><span class="p">:</span> <span class="o">...</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>客製 UI 跟 framework 元素脫離 layout flow、各自獨立。</p>
<h3 id="工具-2margin-spacer-推開-framework-元素">工具 2：Margin spacer 推開 framework 元素</h3>
<p>要在 framework 元素之間插入空間放客製 UI、改 framework 元素的 margin / padding 推出空間：</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">framework-element</span> <span class="p">{</span> <span class="k">margin-top</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">custom</span><span class="o">-</span><span class="n">height</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">custom-ui</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="k">top</span><span class="p">:</span> <span class="mi">0</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">custom</span><span class="o">-</span><span class="n">height</span><span class="p">);</span> <span class="p">}</span></span></span></code></pre></div><p>framework 元素留出空間、客製 UI 浮在空間上。</p>
<h3 id="工具-3grid-容器讓-framework-元件當-grid-item">工具 3：Grid 容器讓 framework 元件當 grid item</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-shell</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">grid-template-rows</span><span class="p">:</span> <span class="kc">auto</span> <span class="kc">auto</span> <span class="mi">1</span><span class="n">fr</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="p">.</span><span class="nc">search-shell</span> <span class="o">&gt;</span> <span class="p">.</span><span class="nc">search-scope</span> <span class="p">{</span> <span class="k">grid-row</span><span class="p">:</span> <span class="mi">2</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="o">&gt;</span> <span class="p">#</span><span class="nn">search</span> <span class="p">{</span> <span class="k">grid-row</span><span class="p">:</span> <span class="mi">3</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>把 framework 元件當 grid 的一個 item — grid 控制 layout、framework 不知道有 grid 在外層、繼續管它的子樹。</p>
<h3 id="工具-4用-css-variables-共享尺寸">工具 4：用 CSS variables 共享尺寸</h3>
<p>framework 元素的尺寸需要參考客製 UI 時、用 CSS variable 傳遞：</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="nd">root</span> <span class="p">{</span> <span class="nv">--custom-height</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</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">framework-element</span> <span class="p">{</span> <span class="k">margin-top</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">custom</span><span class="o">-</span><span class="n">height</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">custom-ui</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">custom</span><span class="o">-</span><span class="n">height</span><span class="p">);</span> <span class="p">}</span></span></span></code></pre></div><p>或用 ResizeObserver 量測寫回 variable（<a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a>）。</p>
<hr>
<h2 id="設計取捨客製-ui-的位置選擇">設計取捨：客製 UI 的位置選擇</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（邊界外 + CSS）當預設、其他做法在特定情境合理。</p>
<h3 id="aframework-邊界外--css-視覺定位這個專案的預設">A：framework 邊界外 + CSS 視覺定位（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：客製 UI 放在 framework 元件的 sibling 位置、用 CSS absolute / grid / margin spacer 達成視覺位置</li>
<li><strong>選 A 的理由</strong>：跟 framework reconciliation 完全解耦、命運由自己決定、升級不影響</li>
<li><strong>適合</strong>：絕大多數需要在 framework UI 旁 / 上 / 下加客製內容的情境</li>
<li><strong>代價</strong>：CSS 定位邏輯比 DOM 巢狀複雜、需要正確處理 stacking context / z-index</li>
</ul>
<h3 id="bframework-邊界外--js-量測位置">B：framework 邊界外 + JS 量測位置</h3>
<ul>
<li><strong>機制</strong>：用 ResizeObserver 量 framework 元素的 bounding rect、JS 算出客製 UI 該擺哪</li>
<li><strong>跟 A 的取捨</strong>：A 用 CSS 表達靜態關係、B 處理 runtime 才知道的尺寸；B 多一層 JS、但能達成 CSS 表達不出的精確定位</li>
<li><strong>B 比 A 好的情境</strong>：客製 UI 位置依賴 framework 元件的 runtime 尺寸（內容換行、字型變化）</li>
</ul>
<h3 id="cframework-邊界內注入">C：framework 邊界內注入</h3>
<ul>
<li><strong>機制</strong>：JS 把客製 element 直接 appendChild 到 framework 子樹內</li>
<li><strong>跟 A 的取捨</strong>：C 看似省事（少一層 wrapper）、實際把客製命運綁在 framework reconciliation 上</li>
<li><strong>C 才合理的情境</strong>：該 framework 子樹確認「不會被 reconcile」（極罕見、需要讀框架 source 確認）</li>
<li><strong>代價</strong>：客製可能在任何 patch 時消失、需要 MutationObserver 補打、跟渲染週期賽跑</li>
</ul>
<h3 id="dfork-framework-source">D：Fork framework source</h3>
<ul>
<li><strong>機制</strong>：fork 整個 framework、改 reconciliation 行為讓它認得我們的客製</li>
<li><strong>成本特別高的原因</strong>：每次升級都要重新 merge、客製永久綁在 fork 版本</li>
<li><strong>D 才合理的情境</strong>：framework 已停止維護、且客製需求超過所有其他選項</li>
</ul>
<hr>
<h2 id="不該套用邊界外的情境">不該套用「邊界外」的情境</h2>
<p>A 是預設、但不是萬靈丹：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不適合 A</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客製內容必須在 framework 元件的內部視覺脈絡內（共享 inline flow）</td>
          <td>Absolute 跳出 flow、達不到 inline 的視覺效果</td>
      </tr>
      <tr>
          <td>Framework 元件本身就是要客製化（改 row、改 cell）</td>
          <td>動的是 framework 本身、不是「在旁邊加東西」</td>
      </tr>
      <tr>
          <td>Framework 提供了官方擴展介面（slot、render prop）</td>
          <td>用官方介面更穩、不需要邊界外 hack</td>
      </tr>
      <tr>
          <td>客製需要訪問 framework 的內部 state</td>
          <td>邊界外的客製跟內部 state 隔離、訪問成本高</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：客製是「在 framework 旁邊加東西」還是「改 framework 本身」？前者用本策略、後者另想辦法。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>抽象層原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a></td>
          <td>「邊界外 + CSS」是「不要挖 framework 內部」的具體應用 — 客製貼著外部介面（DOM sibling）做、不挖內部</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>第 1 次注入失敗（被清掉）= 第 2 次該換策略到邊界外、不該繼續嘗試「換種方式注入」</td>
      </tr>
      <tr>
          <td><a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a></td>
          <td>互補關係 — 本篇處理「客製 UI 該放哪」、#13 處理「framework 元件本身要動時怎麼動」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該怎麼處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>注入 framework DOM 的元素在使用者互動後消失</td>
          <td>把該元素搬出 framework 邊界、用 CSS 控制視覺位置</td>
      </tr>
      <tr>
          <td>客製 UI 在 framework 更新後 attribute 被 revert</td>
          <td>客製 UI 不該在 framework 內、wrapper 在外、attribute 套 wrapper</td>
      </tr>
      <tr>
          <td>看不出哪些 DOM 是 framework 管的</td>
          <td>讀 framework 的 mount root、從那裡往內都是管轄區</td>
      </tr>
      <tr>
          <td>Stacking context 衝突、z-index 失靈</td>
          <td>確認 absolute 的 containing block 是預期的 relative parent</td>
      </tr>
      <tr>
          <td>Framework 元件位置不固定、客製 UI 對不齊</td>
          <td>用 ResizeObserver 量 framework 元素、寫回 CSS variable</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：客製 UI 的存活壽命 = 「離 framework 管轄區多遠」。最遠 = 永遠不被清；注入內部 = 隨時可能消失。預設選邊界外、不要為了「省一層 wrapper」進入 framework 領地。</p>
]]></content:encoded></item></channel></rss>