<?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>Responsive Design on Tarragon</title><link>https://tarrragon.github.io/blog/tags/responsive-design/</link><description>Recent content in Responsive Design 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/responsive-design/index.xml" rel="self" type="application/rss+xml"/><item><title>跨 viewport 雙模式 UI 的物理空間預算</title><link>https://tarrragon.github.io/blog/report/viewport-dual-mode-spatial-budget/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/viewport-dual-mode-spatial-budget/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>雙模式 UI 的 breakpoint 是「物理空間預算的結果」、不是「設計選擇」。&lt;/strong> 把每個元件的固有尺寸與 gap 加總、得到「兩種模式各自能存活的最小 viewport」 — breakpoint 從這個數字往上取一個安全餘裕。憑感覺取（768、1024）會在中間過渡區看到元件擠壓、消失或溢出。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>breakpoint 數字怎麼推算&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>stateful UI 怎麼跨兩個 slot 共用同一份節點&lt;/strong>由 &lt;a href="../pattern-cross-slot-node-relocation/">#54 跨 slot 同節點搬遷 pattern&lt;/a> 處理（兩議題機制不同：本篇是數字計算、#54 是 state 一致性）&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;hr>
&lt;h2 id="breakpoint-從元件預算推算">breakpoint 從元件預算推算&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Responsive UI 的本質是「同一份內容、在不同寬度有不同的呈現方式」。每種模式（mobile / tablet / desktop）對應一個 layout、每個 layout 有自己的最小可生存寬度 — 這是元件尺寸與 gap 的加總、不是任意選擇。&lt;/p>
&lt;p>當 breakpoint 取得比實際所需大、模式切換點與「真正放得下」的點不一致、使用者在過渡區看到擠壓、溢出、或元件神秘消失。&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>列出元件之間的 gap 與 container 的 padding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>加總得到「該模式所需的最小 viewport」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>breakpoint = 最小 viewport + 安全餘裕（避免邊界情況閃爍）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="這次任務的實際預算">這次任務的實際預算&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁的 desktop layout 包含：&lt;/p>
&lt;ul>
&lt;li>main 內容欄寬度 = 70ch ≈ 720px（theme 預設）&lt;/li>
&lt;li>filter sidebar 寬度 = 400px（自訂）&lt;/li>
&lt;li>main 與 filter 之間 gap = 32px&lt;/li>
&lt;li>body 左右 padding 各 64px = 128px&lt;/li>
&lt;/ul>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 filter 放在 main 左外側、main 維持置中時、所需最小 viewport：&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">最小 viewport ≈ main + 2 × (filter + gap) + body padding
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> = 720 + 2 × (400 + 32) + 128
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> = 1712px&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但這是「左右對稱都放下」的條件。若允許 filter 溢出 body padding（仍在 viewport 內可見）、條件放寬為：&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">最小 viewport ≈ main + filter + gap + body padding
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> = 720 + 400 + 32 + 128
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> = 1280px&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>選 1280 為下限、加餘裕後取 1400px。&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="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">min-width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">1400px&lt;/span>&lt;span class="o">)&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">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">block&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 桌面寬模式 */&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下方寬度時 filter 顯示在 pagefind 原生位置（drawer 內、結果上方）、由 pagefind 自己處理。&lt;/p>
&lt;hr>
&lt;h2 id="設計取捨餘裕的取法">設計取捨：餘裕的取法&lt;/h2>
&lt;p>預算算出最小 viewport 後、breakpoint 加多少餘裕？四種做法：&lt;/p>
&lt;h3 id="a加-10-餘裕這個專案的預設">A：加 ~10% 餘裕（這個專案的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：1280 + 120 ≈ 1400&lt;/li>
&lt;li>&lt;strong>選 A 的理由&lt;/strong>：避免邊界情況閃爍（使用者視窗剛好在 1280 像素時、輕微 resize 會反覆觸發切換）&lt;/li>
&lt;li>&lt;strong>適合&lt;/strong>：一般情境&lt;/li>
&lt;li>&lt;strong>代價&lt;/strong>：1280-1400 區間其實能放下、但 CSS 仍走 mobile 模式&lt;/li>
&lt;/ul>
&lt;h3 id="b取整到常見裝置寬度">B：取整到常見裝置寬度&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：1280 → 1366（MacBook Pro 寬度）或 1440（外接螢幕常見）&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：B 對齊裝置生態、A 對齊計算結果；B 對「設計給特定裝置的網站」較合理&lt;/li>
&lt;li>&lt;strong>B 比 A 好的情境&lt;/strong>：使用者群明確（公司內部工具、特定裝置網站）&lt;/li>
&lt;/ul>
&lt;h3 id="c完全等於最小值無餘裕">C：完全等於最小值（無餘裕）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：breakpoint = 1280&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：C 把所有可放下的視窗都納入 desktop 模式、A 留空間給邊界閃爍&lt;/li>
&lt;li>&lt;strong>C 才合理的情境&lt;/strong>：實作上有 debounce 處理閃爍、且裝置寬度集中在某個值附近&lt;/li>
&lt;/ul>
&lt;h3 id="d用-container-query-取代-viewport-breakpoint">D：用 container query 取代 viewport breakpoint&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：CSS Container Queries — 元件根據父容器寬度切換、跟 viewport 解耦&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：D 更精準（容器寬度才是元件真正可用的空間）、A 簡單（viewport 是全局觀念）&lt;/li>
&lt;li>&lt;strong>D 比 A 好的情境&lt;/strong>：元件可能放在不同寬度的容器內（CMS 系統、可嵌入元件）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="不該套用物理空間預算的情境">不該套用「物理空間預算」的情境&lt;/h2>
&lt;p>預算法有適用邊界：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>雙模式 UI 的 breakpoint 是「物理空間預算的結果」、不是「設計選擇」。</strong> 把每個元件的固有尺寸與 gap 加總、得到「兩種模式各自能存活的最小 viewport」 — breakpoint 從這個數字往上取一個安全餘裕。憑感覺取（768、1024）會在中間過渡區看到元件擠壓、消失或溢出。</p>
<blockquote>
<p>本篇焦點：<strong>breakpoint 數字怎麼推算</strong>。</p>
<ul>
<li><strong>stateful UI 怎麼跨兩個 slot 共用同一份節點</strong>由 <a href="../pattern-cross-slot-node-relocation/">#54 跨 slot 同節點搬遷 pattern</a> 處理（兩議題機制不同：本篇是數字計算、#54 是 state 一致性）</li>
</ul></blockquote>
<hr>
<h2 id="breakpoint-從元件預算推算">breakpoint 從元件預算推算</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Responsive UI 的本質是「同一份內容、在不同寬度有不同的呈現方式」。每種模式（mobile / tablet / desktop）對應一個 layout、每個 layout 有自己的最小可生存寬度 — 這是元件尺寸與 gap 的加總、不是任意選擇。</p>
<p>當 breakpoint 取得比實際所需大、模式切換點與「真正放得下」的點不一致、使用者在過渡區看到擠壓、溢出、或元件神秘消失。</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>列出元件之間的 gap 與 container 的 padding</td>
      </tr>
      <tr>
          <td>3</td>
          <td>加總得到「該模式所需的最小 viewport」</td>
      </tr>
      <tr>
          <td>4</td>
          <td>breakpoint = 最小 viewport + 安全餘裕（避免邊界情況閃爍）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="這次任務的實際預算">這次任務的實際預算</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁的 desktop layout 包含：</p>
<ul>
<li>main 內容欄寬度 = 70ch ≈ 720px（theme 預設）</li>
<li>filter sidebar 寬度 = 400px（自訂）</li>
<li>main 與 filter 之間 gap = 32px</li>
<li>body 左右 padding 各 64px = 128px</li>
</ul>
<h3 id="判讀">判讀</h3>
<p>把 filter 放在 main 左外側、main 維持置中時、所需最小 viewport：</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">最小 viewport ≈ main + 2 × (filter + gap) + body padding
</span></span><span class="line"><span class="ln">2</span><span class="cl">             = 720 + 2 × (400 + 32) + 128
</span></span><span class="line"><span class="ln">3</span><span class="cl">             = 1712px</span></span></code></pre></div><p>但這是「左右對稱都放下」的條件。若允許 filter 溢出 body padding（仍在 viewport 內可見）、條件放寬為：</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">最小 viewport ≈ main + filter + gap + body padding
</span></span><span class="line"><span class="ln">2</span><span class="cl">             = 720 + 400 + 32 + 128
</span></span><span class="line"><span class="ln">3</span><span class="cl">             = 1280px</span></span></code></pre></div><p>選 1280 為下限、加餘裕後取 1400px。</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="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">2</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="c">/* 桌面寬模式 */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>下方寬度時 filter 顯示在 pagefind 原生位置（drawer 內、結果上方）、由 pagefind 自己處理。</p>
<hr>
<h2 id="設計取捨餘裕的取法">設計取捨：餘裕的取法</h2>
<p>預算算出最小 viewport 後、breakpoint 加多少餘裕？四種做法：</p>
<h3 id="a加-10-餘裕這個專案的預設">A：加 ~10% 餘裕（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：1280 + 120 ≈ 1400</li>
<li><strong>選 A 的理由</strong>：避免邊界情況閃爍（使用者視窗剛好在 1280 像素時、輕微 resize 會反覆觸發切換）</li>
<li><strong>適合</strong>：一般情境</li>
<li><strong>代價</strong>：1280-1400 區間其實能放下、但 CSS 仍走 mobile 模式</li>
</ul>
<h3 id="b取整到常見裝置寬度">B：取整到常見裝置寬度</h3>
<ul>
<li><strong>機制</strong>：1280 → 1366（MacBook Pro 寬度）或 1440（外接螢幕常見）</li>
<li><strong>跟 A 的取捨</strong>：B 對齊裝置生態、A 對齊計算結果；B 對「設計給特定裝置的網站」較合理</li>
<li><strong>B 比 A 好的情境</strong>：使用者群明確（公司內部工具、特定裝置網站）</li>
</ul>
<h3 id="c完全等於最小值無餘裕">C：完全等於最小值（無餘裕）</h3>
<ul>
<li><strong>機制</strong>：breakpoint = 1280</li>
<li><strong>跟 A 的取捨</strong>：C 把所有可放下的視窗都納入 desktop 模式、A 留空間給邊界閃爍</li>
<li><strong>C 才合理的情境</strong>：實作上有 debounce 處理閃爍、且裝置寬度集中在某個值附近</li>
</ul>
<h3 id="d用-container-query-取代-viewport-breakpoint">D：用 container query 取代 viewport breakpoint</h3>
<ul>
<li><strong>機制</strong>：CSS Container Queries — 元件根據父容器寬度切換、跟 viewport 解耦</li>
<li><strong>跟 A 的取捨</strong>：D 更精準（容器寬度才是元件真正可用的空間）、A 簡單（viewport 是全局觀念）</li>
<li><strong>D 比 A 好的情境</strong>：元件可能放在不同寬度的容器內（CMS 系統、可嵌入元件）</li>
</ul>
<hr>
<h2 id="不該套用物理空間預算的情境">不該套用「物理空間預算」的情境</h2>
<p>預算法有適用邊界：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內容靜態、不依設計尺寸（純文字段落）</td>
          <td>內容自然 reflow、不需要 breakpoint</td>
      </tr>
      <tr>
          <td>流體 layout（純 % / fr 單位）</td>
          <td>元件自動撐滿可用空間、無「最小寬度」概念</td>
      </tr>
      <tr>
          <td>完全 mobile-first 設計</td>
          <td>沒有「desktop 模式」這個分支</td>
      </tr>
      <tr>
          <td>元件尺寸 runtime 才知道</td>
          <td>用 ResizeObserver 動態調整、不是 breakpoint</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：兩種模式之間有「明確的視覺結構切換」嗎？是 → 用預算法；否 → 用流體 layout 或動態量測。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>抽象層原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSoT</a></td>
          <td>breakpoint 數字是 fact、CSS 變數住址唯一才能集中管理</td>
      </tr>
      <tr>
          <td><a href="../measurement-completeness/">#7 量測值缺一不可</a></td>
          <td>預算的每個分量都要有明確來源（寫死 / 量測）、不能估算</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>中間寬度時 UI 元件擠壓或重疊</td>
          <td>breakpoint 比實際所需小</td>
          <td>算物理空間預算、確認 breakpoint 對應的最小 viewport</td>
      </tr>
      <tr>
          <td>元件在某些寬度下消失但 CSS <code>display</code> 是 <code>block</code></td>
          <td>元件被 absolute 定位推出 viewport</td>
          <td>檢查 absolute 元件相對 viewport 的座標、是否為負</td>
      </tr>
      <tr>
          <td>Breakpoint 取常見值（768 / 1024）就壞</td>
          <td>那些值跟你的元件尺寸無關</td>
          <td>重算預算、不要用「常見」值</td>
      </tr>
      <tr>
          <td>Resize 過 breakpoint 時 layout 跳動</td>
          <td>沒加餘裕、邊界震盪</td>
          <td>加 10% 餘裕避開閃爍區</td>
      </tr>
  </tbody>
</table>
<p><strong>順序</strong>：看到擠壓或消失先量空間、不要立刻調 breakpoint 數字。數字背後有計算才不會反覆試錯。</p>
]]></content:encoded></item><item><title>Pattern：跨 slot 同節點搬遷</title><link>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&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">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&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">function&lt;/span> &lt;span class="nx">place&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&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"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">desktopSlot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&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 class="k">else&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="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&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;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始化
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。&lt;/p>
&lt;p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Filter UI 跨 viewport 切換顯示位置&lt;/td>
 &lt;td>checkbox state 跟著節點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Modal 內容 vs 側邊抽屜&lt;/td>
 &lt;td>同一份表單在兩種展示方式間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab UI 跨 desktop / mobile 重新組織&lt;/td>
 &lt;td>各 tab 內 state 不重置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任何「同 UI、不同位置」的 responsive 切換&lt;/td>
 &lt;td>不需要 state 同步邏輯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。&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>兩個位置展示的是不同元件（雖然視覺類似）&lt;/td>
 &lt;td>搬遷會把錯誤元件搬到錯位置&lt;/td>
 &lt;td>各自獨立掛載、不搬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 純 stateless（純圖示、純文字）&lt;/td>
 &lt;td>複製兩份成本低、無 state 風險&lt;/td>
 &lt;td>CSS-only 雙顯示 + display 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 管的節點&lt;/td>
 &lt;td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）&lt;/td>
 &lt;td>必須搬整節點、不複製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩個位置視覺差異大&lt;/td>
 &lt;td>搬遷後 UI 不適配新位置&lt;/td>
 &lt;td>各自獨立元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="appendchild-是搬遷不是複製">&lt;code>appendChild&lt;/code> 是搬遷、不是複製&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="nx">parentA&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// node 從原位置消失、出現在 parentA
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM API 的 &lt;code>appendChild&lt;/code> / &lt;code>insertBefore&lt;/code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。&lt;/p>
&lt;h3 id="初始放在哪">初始放在哪&lt;/h3>





&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="c">&amp;lt;!-- 預設位置（mobile / fallback）--&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">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui&amp;#34;&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;drawer&amp;#34;&lt;/span>&lt;span class="p">&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">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-panel&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">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;span class="line">&lt;span class="ln">6&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;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 桌面 slot（空、等待搬入）--&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">aside&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;desktop-filter-slot&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">aside&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。&lt;/p>
&lt;h3 id="跨-slot-切換的時機">跨 slot 切換的時機&lt;/h3>
&lt;p>&lt;code>matchMedia&lt;/code> event 是 viewport 跨過 breakpoint 的瞬間：&lt;/p>





&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">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&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="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&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="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始也跑一次
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。&lt;code>matchMedia&lt;/code> 只在 cross 的瞬間觸發。&lt;/p>
&lt;h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity&lt;/h3>
&lt;p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</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><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始化
</span></span></span></code></pre></div><p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。</p>
<p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter UI 跨 viewport 切換顯示位置</td>
          <td>checkbox state 跟著節點</td>
      </tr>
      <tr>
          <td>Modal 內容 vs 側邊抽屜</td>
          <td>同一份表單在兩種展示方式間</td>
      </tr>
      <tr>
          <td>Tab UI 跨 desktop / mobile 重新組織</td>
          <td>各 tab 內 state 不重置</td>
      </tr>
      <tr>
          <td>任何「同 UI、不同位置」的 responsive 切換</td>
          <td>不需要 state 同步邏輯</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩個位置展示的是不同元件（雖然視覺類似）</td>
          <td>搬遷會把錯誤元件搬到錯位置</td>
          <td>各自獨立掛載、不搬</td>
      </tr>
      <tr>
          <td>UI 純 stateless（純圖示、純文字）</td>
          <td>複製兩份成本低、無 state 風險</td>
          <td>CSS-only 雙顯示 + display 切換</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）</td>
          <td>必須搬整節點、不複製</td>
      </tr>
      <tr>
          <td>兩個位置視覺差異大</td>
          <td>搬遷後 UI 不適配新位置</td>
          <td>各自獨立元件</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="appendchild-是搬遷不是複製"><code>appendChild</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="nx">parentA</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>  <span class="c1">// node 從原位置消失、出現在 parentA
</span></span></span></code></pre></div><p>DOM API 的 <code>appendChild</code> / <code>insertBefore</code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。</p>
<h3 id="初始放在哪">初始放在哪</h3>





<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="c">&lt;!-- 預設位置（mobile / fallback）--&gt;</span>
</span></span><span class="line"><span class="ln">2</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;pagefind-ui&#34;</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;drawer&#34;</span><span class="p">&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">class</span><span class="o">=</span><span class="s">&#34;filter-panel&#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">5</span><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c">&lt;!-- 桌面 slot（空、等待搬入）--&gt;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">&lt;</span><span class="nt">aside</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;desktop-filter-slot&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">aside</span><span class="p">&gt;</span></span></span></code></pre></div><p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。</p>
<h3 id="跨-slot-切換的時機">跨 slot 切換的時機</h3>
<p><code>matchMedia</code> event 是 viewport 跨過 breakpoint 的瞬間：</p>





<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">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始也跑一次
</span></span></span></code></pre></div><p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。<code>matchMedia</code> 只在 cross 的瞬間觸發。</p>
<h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity</h3>
<p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。</p>
<p>詳細安全規則由 <a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a> 處理。</p>
<h3 id="focus-跟著搬">Focus 跟著搬</h3>
<p>搬遷可能讓鍵盤 focus 暫時失去（視瀏覽器）— 加 save/restore：</p>





<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">place</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">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</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><p>詳細處理由 <a href="../focus-management-on-dom-move/">#37 動態 DOM 移動時的 focus 管理</a> 處理。</p>
<hr>
<h2 id="設計取捨兩個-slot-的-stateful-ui-共用">設計取捨：兩個 slot 的 stateful UI 共用</h2>
<p>四種做法、各自機會成本不同。預設選 A（搬同節點）、其他做法在特定情境合理。</p>
<h3 id="a搬同一節點這個專案的預設">A：搬同一節點（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>matchMedia + appendChild</code> 在兩 slot 間搬同一份節點</li>
<li><strong>選 A 的理由</strong>：state 跟著節點、切換無感、不需要 sync 邏輯</li>
<li><strong>適合</strong>：stateful UI、需要在兩個位置展示同樣內容</li>
<li><strong>代價</strong>：搬遷 callback 在 viewport 跨 breakpoint 時觸發、需要處理 focus / 動畫</li>
<li><strong>詳細</strong>：本卡片</li>
</ul>
<h3 id="bcss-only-雙顯示--display-切換">B：CSS-only 雙顯示 + display 切換</h3>
<ul>
<li><strong>機制</strong>：兩個位置都放同一份節點 (寫兩遍 HTML)、用 <code>@media + display: none</code> 切換顯示</li>
<li><strong>跟 A 的取捨</strong>：B 純 CSS 簡單、A 需要 JS；但 B 對 stateful UI 失敗（兩份 state 各自獨立）</li>
<li><strong>B 比 A 好的情境</strong>：UI 純 stateless（純圖示）、純 CSS 解就夠</li>
</ul>
<h3 id="ccss-only--js-同步-state">C：CSS-only + JS 同步 state</h3>
<ul>
<li><strong>機制</strong>：兩份節點 + JS 監聽 state 變動同步</li>
<li><strong>跟 A 的取捨</strong>：C 比 B 解 state 問題、但同步邏輯複雜（雙向更新、避免循環）</li>
<li><strong>C 比 A 好的情境</strong>：兩個位置的 UI 視覺需要差異（不只是位置不同）</li>
</ul>
<h3 id="djs-完全重建-ui">D：JS 完全重建 UI</h3>
<ul>
<li><strong>機制</strong>：viewport 變動時拆掉舊 UI、在新位置重建一份</li>
<li><strong>成本特別高的原因</strong>：state 在重建時遺失、UI 閃爍、輸入中斷</li>
<li><strong>D 才合理的情境</strong>：UI 是 stateless 的、且重建成本低</li>
</ul>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 pattern 的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、本卡片是「跨 slot 搬遷」這個專門情境的補充：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應 pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 的起點</td>
          <td><a href="../pattern-document-query/">#46 document</a> / <a href="../pattern-component-root/">#47 元件根變數</a> / <a href="../pattern-root-as-parameter/">#48 起點當參數</a> / <a href="../pattern-closest-lookup/">#49 closest 反向</a></td>
      </tr>
      <tr>
          <td>Idempotency 過濾</td>
          <td><a href="../pattern-attribute-idempotency-marker/">#50 attribute 標記</a> / <a href="../pattern-weakmap-idempotency-record/">#51 WeakMap</a></td>
      </tr>
      <tr>
          <td>跨 slot 搬遷（本卡片）</td>
          <td>同節點 vs 雙節點 + state 同步</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="應用範例跨-viewport-filter-切換">應用範例：跨 viewport filter 切換</h2>





<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">setupResponsiveFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">breakpoint</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">filter</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">desktopSlot</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-filter-slot&#39;</span><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="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">filter</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span> <span class="o">||</span> <span class="o">!</span><span class="nx">desktopSlot</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: &#39;</span> <span class="o">+</span> <span class="nx">breakpoint</span> <span class="o">+</span> <span class="s1">&#39;px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>完整 pattern：取元件根 + matchMedia + 搬遷 + focus 處理。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩份節點各自 state、用 sync 邏輯保持一致</td>
          <td>是 — 改成搬同節點、移除 sync</td>
      </tr>
      <tr>
          <td>Stateful UI 在 mobile / desktop 兩種 layout 間</td>
          <td>是 — 直接的應用</td>
      </tr>
      <tr>
          <td>切換 viewport 時 UI 閃爍 / 重建</td>
          <td>是 — 改成搬而非重建</td>
      </tr>
      <tr>
          <td>兩個位置展示完全不同的 UI（不是同邏輯）</td>
          <td>否 — 各自獨立元件</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>是 — 整節點搬安全、但要遵守 <a href="../component-boundary-and-js-impact/">#13</a> 的規則</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Stateful UI 的兩個展示位置共用同一份節點、state 自然跟著走 — 比「兩份節點 + sync 邏輯」乾淨。複製兩份是「state 來源從一變二」的隱形多源（違反 <a href="../single-source-of-truth/">#44 SSoT</a>）。</p>
]]></content:encoded></item></channel></rss>