<?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>Mobile on Tarragon</title><link>https://tarrragon.github.io/blog/tags/mobile/</link><description>Recent content in Mobile on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/mobile/index.xml" rel="self" type="application/rss+xml"/><item><title>Mobile 導航模式分類</title><link>https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/mobile-navigation-taxonomy/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/mobile-navigation-taxonomy/</guid><description>&lt;p>Mobile 導航模式決定使用者如何在畫面之間移動。每種模式對應不同的使用者心理模型 — 使用者期望按 back 會發生什麼、期望首頁在哪裡、期望平行功能如何切換。選擇導航模式的依據是 app 的資訊架構和使用者的操作路徑。&lt;/p>
&lt;h2 id="pushpop-stack堆疊導航">Push/pop stack（堆疊導航）&lt;/h2>
&lt;p>堆疊導航是最基本的模式。每次導航把新畫面推入堆疊頂端，按 back 彈出頂端畫面回到前一頁。使用者的心理模型是「深入 → 返回」的線性路徑。&lt;/p>
&lt;p>適合場景：層級式的資訊結構（列表 → 詳細 → 編輯）、步驟式流程（填表 → 確認 → 完成）。&lt;/p>
&lt;p>堆疊導航的限制是「只有一條軸」— 使用者只能在深度方向移動（往下鑽或往上回），無法在同層級的平行功能之間橫向切換。&lt;/p>
&lt;h2 id="declarative-router宣告式路由">Declarative router（宣告式路由）&lt;/h2>
&lt;p>Declarative router 用 URL 或路由路徑表示畫面狀態。Flutter 的 GoRouter、React Router、Vue Router 都屬於這個模式。導航操作是「把 URL 設成 /settings」而非「push SettingsScreen」。&lt;/p>
&lt;p>Declarative router 的優勢是路由狀態和畫面狀態分離 — 路由邏輯集中管理，支援 deep link，支援動態重建導航堆疊（例如從 deep link 恢復完整的 back 堆疊）。&lt;/p>
&lt;p>適合場景：需要 deep link 支援的 app、URL 驅動的 web app、複雜的條件式導航（根據使用者狀態決定顯示哪個畫面）。&lt;/p>
&lt;h2 id="tab-bar標籤列導航">Tab bar（標籤列導航）&lt;/h2>
&lt;p>畫面底部的標籤列讓使用者在平行的頂層功能之間橫向切換。每個 tab 是獨立的導航堆疊 — 在 tab A 深入到第三層，切換到 tab B 再切回 tab A，回到 tab A 的第三層。&lt;/p>
&lt;p>適合場景：3-5 個平行的主要功能（首頁、搜尋、通知、個人檔案）。使用者頻繁在這些功能之間切換。&lt;/p>
&lt;p>Tab bar 的限制是 tab 數量。超過 5 個 tab 在手機螢幕上過於擁擠。超過 5 個頂層功能時，次要功能放進「更多」tab 或改用 drawer。&lt;/p>
&lt;h2 id="drawer抽屜導航">Drawer（抽屜導航）&lt;/h2>
&lt;p>從螢幕邊緣滑出的側邊選單，列出所有導航選項。使用者需要打開 drawer 才能看到選項，日常操作中 drawer 是隱藏的。&lt;/p>
&lt;p>適合場景：頂層功能超過 5 個、功能之間的切換頻率低、或需要顯示使用者資訊（帳號、設定）。&lt;/p>
&lt;p>Drawer 的缺點是功能的可見性低 — 隱藏在側邊的功能不如 tab bar 上的功能容易被發現。不常用的功能適合放 drawer，核心功能應該放在更可見的位置。&lt;/p>
&lt;h2 id="組合使用">組合使用&lt;/h2>
&lt;p>多數 app 組合使用多種導航模式。Tab bar 做頂層橫向導航，每個 tab 內部用 push/pop 做縱向深入，drawer 放使用者設定和次要功能。&lt;/p>
&lt;p>組合使用時的注意點：back 按鈕的行為在不同模式下需要一致。在 tab A 的第三層按 back 應該回到第二層（push/pop 行為），而非切換到上一個 tab。&lt;/p>
&lt;p>分類建立後，在 Flutter 的實作層面用 &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/flutter-gorouter/" data-link-title="Flutter GoRouter 導航設計" data-link-desc="GoRouter 的路由定義、導航 API（go / push / pushReplacement）、redirect 機制和 ShellRoute 的使用場景">GoRouter 導航設計&lt;/a>把導航模式對應到具體的 router 設定。不同導航操作（go / push / pushReplacement）的 UX 語意差異在 &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/go-push-semantics/" data-link-title="go vs push vs pushReplacement 的 UX 語意表" data-link-desc="三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便">go vs push vs pushReplacement 語意表&lt;/a>中逐一比對。確認導航實作正確後，用&lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性檢查&lt;/a>驗證所有路由從 UI 可達。&lt;/p></description><content:encoded><![CDATA[<p>Mobile 導航模式決定使用者如何在畫面之間移動。每種模式對應不同的使用者心理模型 — 使用者期望按 back 會發生什麼、期望首頁在哪裡、期望平行功能如何切換。選擇導航模式的依據是 app 的資訊架構和使用者的操作路徑。</p>
<h2 id="pushpop-stack堆疊導航">Push/pop stack（堆疊導航）</h2>
<p>堆疊導航是最基本的模式。每次導航把新畫面推入堆疊頂端，按 back 彈出頂端畫面回到前一頁。使用者的心理模型是「深入 → 返回」的線性路徑。</p>
<p>適合場景：層級式的資訊結構（列表 → 詳細 → 編輯）、步驟式流程（填表 → 確認 → 完成）。</p>
<p>堆疊導航的限制是「只有一條軸」— 使用者只能在深度方向移動（往下鑽或往上回），無法在同層級的平行功能之間橫向切換。</p>
<h2 id="declarative-router宣告式路由">Declarative router（宣告式路由）</h2>
<p>Declarative router 用 URL 或路由路徑表示畫面狀態。Flutter 的 GoRouter、React Router、Vue Router 都屬於這個模式。導航操作是「把 URL 設成 /settings」而非「push SettingsScreen」。</p>
<p>Declarative router 的優勢是路由狀態和畫面狀態分離 — 路由邏輯集中管理，支援 deep link，支援動態重建導航堆疊（例如從 deep link 恢復完整的 back 堆疊）。</p>
<p>適合場景：需要 deep link 支援的 app、URL 驅動的 web app、複雜的條件式導航（根據使用者狀態決定顯示哪個畫面）。</p>
<h2 id="tab-bar標籤列導航">Tab bar（標籤列導航）</h2>
<p>畫面底部的標籤列讓使用者在平行的頂層功能之間橫向切換。每個 tab 是獨立的導航堆疊 — 在 tab A 深入到第三層，切換到 tab B 再切回 tab A，回到 tab A 的第三層。</p>
<p>適合場景：3-5 個平行的主要功能（首頁、搜尋、通知、個人檔案）。使用者頻繁在這些功能之間切換。</p>
<p>Tab bar 的限制是 tab 數量。超過 5 個 tab 在手機螢幕上過於擁擠。超過 5 個頂層功能時，次要功能放進「更多」tab 或改用 drawer。</p>
<h2 id="drawer抽屜導航">Drawer（抽屜導航）</h2>
<p>從螢幕邊緣滑出的側邊選單，列出所有導航選項。使用者需要打開 drawer 才能看到選項，日常操作中 drawer 是隱藏的。</p>
<p>適合場景：頂層功能超過 5 個、功能之間的切換頻率低、或需要顯示使用者資訊（帳號、設定）。</p>
<p>Drawer 的缺點是功能的可見性低 — 隱藏在側邊的功能不如 tab bar 上的功能容易被發現。不常用的功能適合放 drawer，核心功能應該放在更可見的位置。</p>
<h2 id="組合使用">組合使用</h2>
<p>多數 app 組合使用多種導航模式。Tab bar 做頂層橫向導航，每個 tab 內部用 push/pop 做縱向深入，drawer 放使用者設定和次要功能。</p>
<p>組合使用時的注意點：back 按鈕的行為在不同模式下需要一致。在 tab A 的第三層按 back 應該回到第二層（push/pop 行為），而非切換到上一個 tab。</p>
<p>分類建立後，在 Flutter 的實作層面用 <a href="/blog/ux-design/05-navigation-patterns/flutter-gorouter/" data-link-title="Flutter GoRouter 導航設計" data-link-desc="GoRouter 的路由定義、導航 API（go / push / pushReplacement）、redirect 機制和 ShellRoute 的使用場景">GoRouter 導航設計</a>把導航模式對應到具體的 router 設定。不同導航操作（go / push / pushReplacement）的 UX 語意差異在 <a href="/blog/ux-design/05-navigation-patterns/go-push-semantics/" data-link-title="go vs push vs pushReplacement 的 UX 語意表" data-link-desc="三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便">go vs push vs pushReplacement 語意表</a>中逐一比對。確認導航實作正確後，用<a href="/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性檢查</a>驗證所有路由從 UI 可達。</p>
]]></content:encoded></item><item><title>U.C1 Terminal 畫面五個狀態零個退出路徑</title><link>https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/</guid><description>&lt;p>這個案例的核心責任是說明「每個畫面每個狀態都需要退出路徑」這個原則為什麼容易在企劃階段被遺漏，以及用什麼工具能系統性地捕捉這類缺口。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 Terminal 畫面用一個 &lt;code>TerminalScreenUiState&lt;/code> enum 管理五個狀態。實機測試前，五個狀態的 UI 實作如下：&lt;/p>
&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>idle&lt;/td>
 &lt;td>空白（自動連線）&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connecting&lt;/td>
 &lt;td>進度指示&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connected&lt;/td>
 &lt;td>終端機 + 工具列&lt;/td>
 &lt;td>打字、特殊鍵&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>錯誤訊息 + 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>disconnected&lt;/td>
 &lt;td>「連線中斷」+ 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>使用者從首頁點 Connect Terminal 進入後，無論處於哪個狀態都無法返回首頁。唯一退出方式是殺掉 app。&lt;/p>
&lt;p>W2-001 修復後加入 back 按鈕的狀態：error、disconnected、connecting。但 idle 和 connected 仍缺退出路徑。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>企劃文件的「前端引導」欄位只描述顯示，不描述操作和退出&lt;/strong>。操作盤點表的「前端引導」欄位寫了「連線失敗顯示無法連線」— 覆蓋了 error 狀態的顯示，但沒回答「能做什麼」和「怎麼離開」。從 BDD 操作盤點到 UI 實作之間，缺少把「情境」展開成「畫面 × 狀態 × 操作 × 退出」矩陣的步驟。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>開發者假設使用者只走 happy path&lt;/strong>。「connected 後使用者不會想回首頁」是開發者的隱性假設。實際上使用者可能想：切換到配對畫面重新配對、暫時離開終端機做其他事、遇到問題想重新開始。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>error 和 disconnected 有重連按鈕但沒有 back，也是半成品&lt;/strong>。重連失敗時使用者被困在 error → retry → error 的循環裡。加 back 按鈕讓使用者有第二條路。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>畫面狀態矩陣作為設計產物&lt;/strong>：把每個畫面的每個狀態展開成四欄表格（顯示 / 可用操作 / 進入條件 / 退出路徑）。退出路徑欄位為空 = UX 死胡同，10 分鐘能查完所有畫面。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>退出路徑是預設要求&lt;/strong>：每個畫面的每個狀態至少要有一條退出路徑。即使是 connecting 這種過渡狀態，使用者也應該能取消。這跟 iOS HIG 和 Material Design 對 modal 畫面的 dismiss 要求一致。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Widget test 覆蓋退出路徑&lt;/strong>：狀態矩陣直接轉成 test case — 每個狀態找到 back 按鈕、tap、斷言導航到首頁。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想用狀態矩陣設計畫面 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>&lt;/li>
&lt;li>想建 widget test 覆蓋導航 → &lt;a href="https://tarrragon.github.io/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">模組四：自動化 UI 驗證&lt;/a>&lt;/li>
&lt;li>類似案例（Gate fallback）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2 biometricOnly 無 fallback&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「每個畫面每個狀態都需要退出路徑」這個原則為什麼容易在企劃階段被遺漏，以及用什麼工具能系統性地捕捉這類缺口。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 Terminal 畫面用一個 <code>TerminalScreenUiState</code> enum 管理五個狀態。實機測試前，五個狀態的 UI 實作如下：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>空白（自動連線）</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>進度指示</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>connected</td>
          <td>終端機 + 工具列</td>
          <td>打字、特殊鍵</td>
          <td>無</td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連按鈕</td>
          <td>重新連線</td>
          <td>無</td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>「連線中斷」+ 重連按鈕</td>
          <td>重新連線</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>使用者從首頁點 Connect Terminal 進入後，無論處於哪個狀態都無法返回首頁。唯一退出方式是殺掉 app。</p>
<p>W2-001 修復後加入 back 按鈕的狀態：error、disconnected、connecting。但 idle 和 connected 仍缺退出路徑。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>企劃文件的「前端引導」欄位只描述顯示，不描述操作和退出</strong>。操作盤點表的「前端引導」欄位寫了「連線失敗顯示無法連線」— 覆蓋了 error 狀態的顯示，但沒回答「能做什麼」和「怎麼離開」。從 BDD 操作盤點到 UI 實作之間，缺少把「情境」展開成「畫面 × 狀態 × 操作 × 退出」矩陣的步驟。</p>
</li>
<li>
<p><strong>開發者假設使用者只走 happy path</strong>。「connected 後使用者不會想回首頁」是開發者的隱性假設。實際上使用者可能想：切換到配對畫面重新配對、暫時離開終端機做其他事、遇到問題想重新開始。</p>
</li>
<li>
<p><strong>error 和 disconnected 有重連按鈕但沒有 back，也是半成品</strong>。重連失敗時使用者被困在 error → retry → error 的循環裡。加 back 按鈕讓使用者有第二條路。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>畫面狀態矩陣作為設計產物</strong>：把每個畫面的每個狀態展開成四欄表格（顯示 / 可用操作 / 進入條件 / 退出路徑）。退出路徑欄位為空 = UX 死胡同，10 分鐘能查完所有畫面。</p>
</li>
<li>
<p><strong>退出路徑是預設要求</strong>：每個畫面的每個狀態至少要有一條退出路徑。即使是 connecting 這種過渡狀態，使用者也應該能取消。這跟 iOS HIG 和 Material Design 對 modal 畫面的 dismiss 要求一致。</p>
</li>
<li>
<p><strong>Widget test 覆蓋退出路徑</strong>：狀態矩陣直接轉成 test case — 每個狀態找到 back 按鈕、tap、斷言導航到首頁。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想用狀態矩陣設計畫面 → <a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a></li>
<li>想建 widget test 覆蓋導航 → <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">模組四：自動化 UI 驗證</a></li>
<li>類似案例（Gate fallback）→ <a href="/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2 biometricOnly 無 fallback</a></li>
</ul>
]]></content:encoded></item><item><title>輸入機制決策表</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/</guid><description>&lt;p>輸入機制是設計產物，在功能規格階段決定，和 API schema、畫面狀態矩陣同級。手機鍵盤的行為由多個參數控制，每個參數都是一個設計決策，影響使用者體驗、UI layout 和通訊協議。&lt;/p>
&lt;h2 id="四個決策維度">四個決策維度&lt;/h2>
&lt;h3 id="keyboard-type顯示哪種鍵盤">Keyboard type：顯示哪種鍵盤&lt;/h3>
&lt;p>Keyboard type 決定使用者按下輸入框時出現什麼鍵盤。數字鍵盤、email 鍵盤、URL 鍵盤、一般文字鍵盤 — 每種鍵盤的按鍵配置和自動行為不同。&lt;/p>
&lt;p>選擇判斷依據是「使用者要輸入什麼內容」。email 地址用 email 鍵盤（有 &lt;code>@&lt;/code> 鍵），電話號碼用數字鍵盤，密碼或 CLI 指令用 &lt;code>visiblePassword&lt;/code> 型別（避免自動校正和建議）。&lt;/p>
&lt;p>app_tunnel 的 terminal 輸入框用 &lt;code>TextInputType.visiblePassword&lt;/code> — 因為 CLI 指令包含路徑分隔符、flag 縮寫等非自然語言內容，一般文字鍵盤會嘗試自動校正 &lt;code>ls -la&lt;/code> 或 &lt;code>/usr/bin/&lt;/code> 成其他東西（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>）。&lt;/p>
&lt;h3 id="submit-model怎麼送出輸入">Submit model：怎麼送出輸入&lt;/h3>
&lt;p>Submit model 決定使用者輸入的內容何時傳送給系統。兩個基本選項：整行送出（使用者按 Enter/Send 後一次傳送整行）和逐字元送出（每個按鍵即時傳送）。&lt;/p>
&lt;p>這個決策直接影響通訊協議設計（本章合成，UF-8 Derive）。整行送出代表每次傳送一個完整指令字串（&lt;code>ls -la\n&lt;/code>），server 端按行處理。逐字元送出代表每個按鍵產生一個 WebSocket frame（&lt;code>l&lt;/code>、&lt;code>s&lt;/code>、&lt;code> &lt;/code>、&lt;code>-&lt;/code>、&lt;code>l&lt;/code>、&lt;code>a&lt;/code>），server 端需要處理單字元輸入，包括 Tab 補全和 Ctrl+C 這類立即回應的操作。&lt;/p>
&lt;p>app_tunnel 選擇整行送出（&lt;code>onSubmitted&lt;/code>），代表 Tab 補全在 client 端無法觸發（因為 Tab 不會單獨送出），但實作成本較低且協議設計較簡單。逐字元送出支援 Tab 補全和命令編輯，但 protocol 複雜度顯著提高。&lt;/p>
&lt;h3 id="ime-policy輸入法的行為控制">IME policy：輸入法的行為控制&lt;/h3>
&lt;p>IME（Input Method Editor）policy 控制手機輸入法的自動行為：自動校正、建議列、個人化學習。每個行為在某些輸入場景是幫助，在另一些場景是干擾或安全風險。&lt;/p>
&lt;p>三個控制項各自有獨立的影響：&lt;/p>
&lt;ul>
&lt;li>&lt;code>autocorrect&lt;/code>：自動校正把輸入替換成字典中的詞。CLI 指令和路徑不是自然語言，自動校正會破壞輸入內容。&lt;/li>
&lt;li>&lt;code>enableSuggestions&lt;/code>：建議列在鍵盤上方顯示候選詞。在 terminal 場景中建議列遮擋畫面底部的終端機輸出。&lt;/li>
&lt;li>&lt;code>enableIMEPersonalizedLearning&lt;/code>：IME 從使用者輸入中學習新詞，跨 app 適用。CLI 輸入可能包含密碼和路徑 — 這是安全問題，見 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制 checklist&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h3 id="special-keys特殊按鍵的處理">Special keys：特殊按鍵的處理&lt;/h3>
&lt;p>手機鍵盤沒有桌面鍵盤的 Esc、Tab、Ctrl、方向鍵。如果應用需要這些按鍵，必須自建 UI 元件提供。&lt;/p>
&lt;p>app_tunnel 用底部工具列提供 Esc/Tab/Ctrl/方向鍵。這個工具列的設計（按鈕大小、排列、長按行為）是 UX 決策，不是實作細節。&lt;/p>
&lt;h2 id="決策表作為設計產物">決策表作為設計產物&lt;/h2>
&lt;p>四個維度的決策應該在功能規格中以表格形式記錄，讓 code review 時可以逐項對照實作是否符合規格。&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>Keyboard&lt;/td>
 &lt;td>visiblePassword&lt;/td>
 &lt;td>CLI 指令不適用自動校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Submit&lt;/td>
 &lt;td>整行送出&lt;/td>
 &lt;td>protocol 簡單，犧牲 Tab 補全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IME&lt;/td>
 &lt;td>全關&lt;/td>
 &lt;td>安全考量 + 非自然語言輸入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Special keys&lt;/td>
 &lt;td>底部工具列&lt;/td>
 &lt;td>手機無實體 Esc/Tab/Ctrl&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>app_tunnel 的六個 TextField 參數全是 W2 hotfix 事後補上的，沒有一個是事前規劃。每個參數都有 gotcha — 漏掉 &lt;code>enableIMEPersonalizedLearning: false&lt;/code> 就是安全漏洞，漏掉 &lt;code>autocorrect: false&lt;/code> 就是 UX 問題。事先決策並記錄在規格中，code review 時逐項勾選，比事後逐一發現問題的成本低。&lt;/p>
&lt;p>四個維度在不同場景下的具體決策各有不同。CLI 場景的特殊需求見 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計&lt;/a>，安全敏感欄位的 IME 控制逐項列在 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist&lt;/a>。Submit model 的選擇（整行 vs 逐字元）直接影響通訊協議的設計 — 這個交叉影響在 &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試&lt;/a>中從 test 的角度分析。&lt;/p></description><content:encoded><![CDATA[<p>輸入機制是設計產物，在功能規格階段決定，和 API schema、畫面狀態矩陣同級。手機鍵盤的行為由多個參數控制，每個參數都是一個設計決策，影響使用者體驗、UI layout 和通訊協議。</p>
<h2 id="四個決策維度">四個決策維度</h2>
<h3 id="keyboard-type顯示哪種鍵盤">Keyboard type：顯示哪種鍵盤</h3>
<p>Keyboard type 決定使用者按下輸入框時出現什麼鍵盤。數字鍵盤、email 鍵盤、URL 鍵盤、一般文字鍵盤 — 每種鍵盤的按鍵配置和自動行為不同。</p>
<p>選擇判斷依據是「使用者要輸入什麼內容」。email 地址用 email 鍵盤（有 <code>@</code> 鍵），電話號碼用數字鍵盤，密碼或 CLI 指令用 <code>visiblePassword</code> 型別（避免自動校正和建議）。</p>
<p>app_tunnel 的 terminal 輸入框用 <code>TextInputType.visiblePassword</code> — 因為 CLI 指令包含路徑分隔符、flag 縮寫等非自然語言內容，一般文字鍵盤會嘗試自動校正 <code>ls -la</code> 或 <code>/usr/bin/</code> 成其他東西（<a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a>）。</p>
<h3 id="submit-model怎麼送出輸入">Submit model：怎麼送出輸入</h3>
<p>Submit model 決定使用者輸入的內容何時傳送給系統。兩個基本選項：整行送出（使用者按 Enter/Send 後一次傳送整行）和逐字元送出（每個按鍵即時傳送）。</p>
<p>這個決策直接影響通訊協議設計（本章合成，UF-8 Derive）。整行送出代表每次傳送一個完整指令字串（<code>ls -la\n</code>），server 端按行處理。逐字元送出代表每個按鍵產生一個 WebSocket frame（<code>l</code>、<code>s</code>、<code> </code>、<code>-</code>、<code>l</code>、<code>a</code>），server 端需要處理單字元輸入，包括 Tab 補全和 Ctrl+C 這類立即回應的操作。</p>
<p>app_tunnel 選擇整行送出（<code>onSubmitted</code>），代表 Tab 補全在 client 端無法觸發（因為 Tab 不會單獨送出），但實作成本較低且協議設計較簡單。逐字元送出支援 Tab 補全和命令編輯，但 protocol 複雜度顯著提高。</p>
<h3 id="ime-policy輸入法的行為控制">IME policy：輸入法的行為控制</h3>
<p>IME（Input Method Editor）policy 控制手機輸入法的自動行為：自動校正、建議列、個人化學習。每個行為在某些輸入場景是幫助，在另一些場景是干擾或安全風險。</p>
<p>三個控制項各自有獨立的影響：</p>
<ul>
<li><code>autocorrect</code>：自動校正把輸入替換成字典中的詞。CLI 指令和路徑不是自然語言，自動校正會破壞輸入內容。</li>
<li><code>enableSuggestions</code>：建議列在鍵盤上方顯示候選詞。在 terminal 場景中建議列遮擋畫面底部的終端機輸出。</li>
<li><code>enableIMEPersonalizedLearning</code>：IME 從使用者輸入中學習新詞，跨 app 適用。CLI 輸入可能包含密碼和路徑 — 這是安全問題，見 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制 checklist</a>。</li>
</ul>
<h3 id="special-keys特殊按鍵的處理">Special keys：特殊按鍵的處理</h3>
<p>手機鍵盤沒有桌面鍵盤的 Esc、Tab、Ctrl、方向鍵。如果應用需要這些按鍵，必須自建 UI 元件提供。</p>
<p>app_tunnel 用底部工具列提供 Esc/Tab/Ctrl/方向鍵。這個工具列的設計（按鈕大小、排列、長按行為）是 UX 決策，不是實作細節。</p>
<h2 id="決策表作為設計產物">決策表作為設計產物</h2>
<p>四個維度的決策應該在功能規格中以表格形式記錄，讓 code review 時可以逐項對照實作是否符合規格。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>選項</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyboard</td>
          <td>visiblePassword</td>
          <td>CLI 指令不適用自動校正</td>
      </tr>
      <tr>
          <td>Submit</td>
          <td>整行送出</td>
          <td>protocol 簡單，犧牲 Tab 補全</td>
      </tr>
      <tr>
          <td>IME</td>
          <td>全關</td>
          <td>安全考量 + 非自然語言輸入</td>
      </tr>
      <tr>
          <td>Special keys</td>
          <td>底部工具列</td>
          <td>手機無實體 Esc/Tab/Ctrl</td>
      </tr>
  </tbody>
</table>
<p>app_tunnel 的六個 TextField 參數全是 W2 hotfix 事後補上的，沒有一個是事前規劃。每個參數都有 gotcha — 漏掉 <code>enableIMEPersonalizedLearning: false</code> 就是安全漏洞，漏掉 <code>autocorrect: false</code> 就是 UX 問題。事先決策並記錄在規格中，code review 時逐項勾選，比事後逐一發現問題的成本低。</p>
<p>四個維度在不同場景下的具體決策各有不同。CLI 場景的特殊需求見 <a href="/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計</a>，安全敏感欄位的 IME 控制逐項列在 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist</a>。Submit model 的選擇（整行 vs 逐字元）直接影響通訊協議的設計 — 這個交叉影響在 <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a>中從 test 的角度分析。</p>
]]></content:encoded></item><item><title>Terminal app 輸入設計</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/</guid><description>&lt;p>Terminal app 在手機上的輸入需求和一般文字輸入有根本差異。CLI 指令是結構化語法，路徑分隔符、flag 縮寫、管線符號都有精確語意 — 手機鍵盤為自然語言設計的自動行為（校正、建議、學習）在 CLI 場景中全部變成干擾。&lt;/p>
&lt;h2 id="cli-輸入的特殊性">CLI 輸入的特殊性&lt;/h2>
&lt;p>桌面終端機的鍵盤直接傳送按鍵事件，沒有中間的輸入法處理層。使用者按 &lt;code>l&lt;/code> 就是 &lt;code>l&lt;/code>，按 Tab 就是 Tab，按 Ctrl+C 就是 interrupt signal。&lt;/p>
&lt;p>手機鍵盤在使用者和 app 之間插入了 IME 層。使用者按 &lt;code>l&lt;/code> 時，IME 可能等待後續按鍵組合成完整詞彙再傳送；使用者按的按鍵可能被自動校正替換；使用者的輸入被記錄到 IME 詞庫供跨 app 學習。&lt;/p>
&lt;p>Terminal app 需要繞過或控制 IME 層的這些行為。app_tunnel 的 TextField 用 &lt;code>TextInputType.visiblePassword&lt;/code> + &lt;code>autocorrect: false&lt;/code> + &lt;code>enableSuggestions: false&lt;/code> + &lt;code>enableIMEPersonalizedLearning: false&lt;/code> 四個參數關閉 IME 的自動行為（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>）。&lt;/p>
&lt;h2 id="整行送出-vs-逐字元protocol-層的影響">整行送出 vs 逐字元：protocol 層的影響&lt;/h2>
&lt;p>整行送出和逐字元送出在 UI 層看起來只是「按 Enter 送出整行」和「每個按鍵即時送出」的差別，但在 protocol 層是兩種不同的通訊模式。&lt;/p>
&lt;h3 id="整行送出">整行送出&lt;/h3>
&lt;p>Client 端累積使用者輸入，使用者按 Enter 時傳送完整指令字串加換行符（&lt;code>ls -la\n&lt;/code>）。Server 端收到完整行後處理。&lt;/p>
&lt;p>Protocol 設計簡單：每個 WebSocket frame 是一個完整指令。Server 不需要管理部分輸入的狀態，也不需要即時回應 Tab 或方向鍵。&lt;/p>
&lt;p>代價：使用者無法在手機上使用 Tab 補全（Tab 被 IME 攔截或不存在）、無法用方向鍵在指令中移動游標（移動的是 TextField 的游標，不是 server 端的 readline 游標）。&lt;/p>
&lt;h3 id="逐字元送出">逐字元送出&lt;/h3>
&lt;p>Client 端每個按鍵即時傳送一個 WebSocket frame。Server 端的 shell 即時處理每個字元，包括 Tab 補全（server 回傳補全結果）、Ctrl+C（server 中斷當前程序）、方向鍵（server 端 readline 移動游標）。&lt;/p>
&lt;p>Protocol 設計複雜：每個按鍵一個 frame，frame 內容是單一字元或控制序列。Server 端必須維護 readline 狀態。Client 端必須正確編碼控制字元（Ctrl+C = 0x03, Tab = 0x09）。&lt;/p>
&lt;p>代價：protocol 複雜度高，每個按鍵都有網路延遲。在高延遲網路上輸入體驗差（打字後要等 round-trip 才看到回顯）。&lt;/p>
&lt;h3 id="決策在-protocol-層做">決策在 protocol 層做&lt;/h3>
&lt;p>app_tunnel 選擇整行送出，犧牲 Tab 補全換取簡單的 protocol 設計。這個決策應該在 protocol spec 階段做 — 因為它影響 server 端（ttyd）的行為預期和 client 端的 frame 格式。在 UI 實作時才臨時決定，可能和 server 端的行為預期不一致。&lt;/p>
&lt;h2 id="特殊按鍵的-ui-方案">特殊按鍵的 UI 方案&lt;/h2>
&lt;p>手機沒有 Esc、Tab、Ctrl、方向鍵。Terminal app 需要額外的 UI 元件提供這些按鍵。&lt;/p>
&lt;h3 id="底部工具列">底部工具列&lt;/h3>
&lt;p>固定在鍵盤上方的一排按鈕，提供常用特殊鍵。app_tunnel 的工具列包含 Esc、Tab、Ctrl、四個方向鍵。&lt;/p>
&lt;p>工具列的設計考量：按鈕大小（手指能精確觸碰的最小尺寸約 44x44 pt）、排列順序（最常用的放中間）、長按行為（長按 Ctrl 是否支援 Ctrl 組合鍵）。&lt;/p>
&lt;h3 id="ctrl-組合鍵">Ctrl 組合鍵&lt;/h3>
&lt;p>Ctrl+C（中斷）、Ctrl+D（EOF）、Ctrl+Z（暫停）在 CLI 操作中頻繁使用。手機上的實作方式通常是：按下 Ctrl 按鈕後進入「Ctrl 模式」，下一個按鍵自動加 Ctrl 前綴。&lt;/p></description><content:encoded><![CDATA[<p>Terminal app 在手機上的輸入需求和一般文字輸入有根本差異。CLI 指令是結構化語法，路徑分隔符、flag 縮寫、管線符號都有精確語意 — 手機鍵盤為自然語言設計的自動行為（校正、建議、學習）在 CLI 場景中全部變成干擾。</p>
<h2 id="cli-輸入的特殊性">CLI 輸入的特殊性</h2>
<p>桌面終端機的鍵盤直接傳送按鍵事件，沒有中間的輸入法處理層。使用者按 <code>l</code> 就是 <code>l</code>，按 Tab 就是 Tab，按 Ctrl+C 就是 interrupt signal。</p>
<p>手機鍵盤在使用者和 app 之間插入了 IME 層。使用者按 <code>l</code> 時，IME 可能等待後續按鍵組合成完整詞彙再傳送；使用者按的按鍵可能被自動校正替換；使用者的輸入被記錄到 IME 詞庫供跨 app 學習。</p>
<p>Terminal app 需要繞過或控制 IME 層的這些行為。app_tunnel 的 TextField 用 <code>TextInputType.visiblePassword</code> + <code>autocorrect: false</code> + <code>enableSuggestions: false</code> + <code>enableIMEPersonalizedLearning: false</code> 四個參數關閉 IME 的自動行為（<a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a>）。</p>
<h2 id="整行送出-vs-逐字元protocol-層的影響">整行送出 vs 逐字元：protocol 層的影響</h2>
<p>整行送出和逐字元送出在 UI 層看起來只是「按 Enter 送出整行」和「每個按鍵即時送出」的差別，但在 protocol 層是兩種不同的通訊模式。</p>
<h3 id="整行送出">整行送出</h3>
<p>Client 端累積使用者輸入，使用者按 Enter 時傳送完整指令字串加換行符（<code>ls -la\n</code>）。Server 端收到完整行後處理。</p>
<p>Protocol 設計簡單：每個 WebSocket frame 是一個完整指令。Server 不需要管理部分輸入的狀態，也不需要即時回應 Tab 或方向鍵。</p>
<p>代價：使用者無法在手機上使用 Tab 補全（Tab 被 IME 攔截或不存在）、無法用方向鍵在指令中移動游標（移動的是 TextField 的游標，不是 server 端的 readline 游標）。</p>
<h3 id="逐字元送出">逐字元送出</h3>
<p>Client 端每個按鍵即時傳送一個 WebSocket frame。Server 端的 shell 即時處理每個字元，包括 Tab 補全（server 回傳補全結果）、Ctrl+C（server 中斷當前程序）、方向鍵（server 端 readline 移動游標）。</p>
<p>Protocol 設計複雜：每個按鍵一個 frame，frame 內容是單一字元或控制序列。Server 端必須維護 readline 狀態。Client 端必須正確編碼控制字元（Ctrl+C = 0x03, Tab = 0x09）。</p>
<p>代價：protocol 複雜度高，每個按鍵都有網路延遲。在高延遲網路上輸入體驗差（打字後要等 round-trip 才看到回顯）。</p>
<h3 id="決策在-protocol-層做">決策在 protocol 層做</h3>
<p>app_tunnel 選擇整行送出，犧牲 Tab 補全換取簡單的 protocol 設計。這個決策應該在 protocol spec 階段做 — 因為它影響 server 端（ttyd）的行為預期和 client 端的 frame 格式。在 UI 實作時才臨時決定，可能和 server 端的行為預期不一致。</p>
<h2 id="特殊按鍵的-ui-方案">特殊按鍵的 UI 方案</h2>
<p>手機沒有 Esc、Tab、Ctrl、方向鍵。Terminal app 需要額外的 UI 元件提供這些按鍵。</p>
<h3 id="底部工具列">底部工具列</h3>
<p>固定在鍵盤上方的一排按鈕，提供常用特殊鍵。app_tunnel 的工具列包含 Esc、Tab、Ctrl、四個方向鍵。</p>
<p>工具列的設計考量：按鈕大小（手指能精確觸碰的最小尺寸約 44x44 pt）、排列順序（最常用的放中間）、長按行為（長按 Ctrl 是否支援 Ctrl 組合鍵）。</p>
<h3 id="ctrl-組合鍵">Ctrl 組合鍵</h3>
<p>Ctrl+C（中斷）、Ctrl+D（EOF）、Ctrl+Z（暫停）在 CLI 操作中頻繁使用。手機上的實作方式通常是：按下 Ctrl 按鈕後進入「Ctrl 模式」，下一個按鍵自動加 Ctrl 前綴。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>四維度決策表 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>安全敏感輸入框的 IME 控制 → <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist</a></li>
<li>表單場景的輸入設計 → <a href="/blog/ux-design/03-input-mechanism/form-ux-pattern/" data-link-title="表單 UX 模式" data-link-desc="表單輸入的驗證時機、auto-fill 支援、錯誤回饋設計 — 和 terminal 輸入的決策維度相同但選項不同">表單 UX 模式</a></li>
</ul>
]]></content:encoded></item><item><title>U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField</title><link>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</guid><description>&lt;p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。&lt;/p>
&lt;p>W2-001 修復時加入的 &lt;code>TextField&lt;/code> 及其參數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">TextField&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="nl">keyboardType:&lt;/span> &lt;span class="n">TextInputType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">visiblePassword&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 避免自動校正
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">enableSuggestions:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉建議列
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">autocorrect:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉自動校正
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">enableIMEPersonalizedLearning:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉 IME 個人化學習
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">onSubmitted:&lt;/span> &lt;span class="n">_submitInput&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Enter 送出整行
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">textInputAction:&lt;/span> &lt;span class="n">TextInputAction&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">send&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 鍵盤顯示「傳送」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計決策&lt;/th>
 &lt;th>事前規劃&lt;/th>
 &lt;th>事後 hotfix 的風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>visiblePassword&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用預設 &lt;code>text&lt;/code>，iOS 會自動校正 &lt;code>ls -la&lt;/code> 成其他東西&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableSuggestions: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>建議列遮擋終端機畫面下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>autocorrect: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>路徑 &lt;code>/usr/bin/&lt;/code> 可能被校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableIMEPersonalizedLearning: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>CLI 輸入含密碼和路徑，IME 學習是安全風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>onSubmitted&lt;/code>（整行送出）&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>TextInputAction.send&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用 &lt;code>newline&lt;/code>，使用者按 Enter 會換行不送出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>輸入設計影響 UI layout 和 protocol&lt;/strong>。&lt;code>onSubmitted&lt;/code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 &lt;code>command\n&lt;/code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>IME 控制有安全意涵&lt;/strong>。&lt;code>enableIMEPersonalizedLearning: false&lt;/code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>事後 hotfix 的六個參數每個都有 gotcha&lt;/strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 &lt;code>autocorrect: false&lt;/code> 但忘了 &lt;code>enableIMEPersonalizedLearning: false&lt;/code>），漏掉的那個就成為安全漏洞。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>功能規格新增「輸入機制決策表」&lt;/strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。&lt;/li>
&lt;li>&lt;strong>輸入機制跟 protocol 一起設計&lt;/strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。&lt;/li>
&lt;li>&lt;strong>安全敏感參數強制列入 review checklist&lt;/strong>：&lt;code>enableIMEPersonalizedLearning&lt;/code>、&lt;code>autocorrect&lt;/code> 在處理 secret 的輸入框中是安全要求，不是可選項。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計 mobile 輸入機制 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表&lt;/a>&lt;/li>
&lt;li>想看 protocol 跟輸入的關聯 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type&lt;/a>（sendData 的型別決策）&lt;/li>
&lt;li>想做安全審查 → 待補：CLI 輸入安全 checklist&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。</p>
<p>W2-001 修復時加入的 <code>TextField</code> 及其參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">TextField</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">keyboardType:</span> <span class="n">TextInputType</span><span class="p">.</span><span class="n">visiblePassword</span><span class="p">,</span>   <span class="c1">// 避免自動校正
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nl">enableSuggestions:</span> <span class="kc">false</span><span class="p">,</span>                       <span class="c1">// 關閉建議列
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nl">autocorrect:</span> <span class="kc">false</span><span class="p">,</span>                             <span class="c1">// 關閉自動校正
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nl">enableIMEPersonalizedLearning:</span> <span class="kc">false</span><span class="p">,</span>           <span class="c1">// 關閉 IME 個人化學習
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nl">onSubmitted:</span> <span class="n">_submitInput</span><span class="p">,</span>                      <span class="c1">// Enter 送出整行
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nl">textInputAction:</span> <span class="n">TextInputAction</span><span class="p">.</span><span class="n">send</span><span class="p">,</span>          <span class="c1">// 鍵盤顯示「傳送」
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">)</span></span></span></code></pre></div><p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。</p>
<table>
  <thead>
      <tr>
          <th>設計決策</th>
          <th>事前規劃</th>
          <th>事後 hotfix 的風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>visiblePassword</code></td>
          <td>沒有</td>
          <td>如果用預設 <code>text</code>，iOS 會自動校正 <code>ls -la</code> 成其他東西</td>
      </tr>
      <tr>
          <td><code>enableSuggestions: false</code></td>
          <td>沒有</td>
          <td>建議列遮擋終端機畫面下方</td>
      </tr>
      <tr>
          <td><code>autocorrect: false</code></td>
          <td>沒有</td>
          <td>路徑 <code>/usr/bin/</code> 可能被校正</td>
      </tr>
      <tr>
          <td><code>enableIMEPersonalizedLearning: false</code></td>
          <td>沒有</td>
          <td>CLI 輸入含密碼和路徑，IME 學習是安全風險</td>
      </tr>
      <tr>
          <td><code>onSubmitted</code>（整行送出）</td>
          <td>沒有</td>
          <td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計</td>
      </tr>
      <tr>
          <td><code>TextInputAction.send</code></td>
          <td>沒有</td>
          <td>如果用 <code>newline</code>，使用者按 Enter 會換行不送出</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>輸入設計影響 UI layout 和 protocol</strong>。<code>onSubmitted</code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 <code>command\n</code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。</p>
</li>
<li>
<p><strong>IME 控制有安全意涵</strong>。<code>enableIMEPersonalizedLearning: false</code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。</p>
</li>
<li>
<p><strong>事後 hotfix 的六個參數每個都有 gotcha</strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 <code>autocorrect: false</code> 但忘了 <code>enableIMEPersonalizedLearning: false</code>），漏掉的那個就成為安全漏洞。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>功能規格新增「輸入機制決策表」</strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。</li>
<li><strong>輸入機制跟 protocol 一起設計</strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。</li>
<li><strong>安全敏感參數強制列入 review checklist</strong>：<code>enableIMEPersonalizedLearning</code>、<code>autocorrect</code> 在處理 secret 的輸入框中是安全要求，不是可選項。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 mobile 輸入機制 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>想看 protocol 跟輸入的關聯 → <a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type</a>（sendData 的型別決策）</li>
<li>想做安全審查 → 待補：CLI 輸入安全 checklist</li>
</ul>
]]></content:encoded></item><item><title>表單 UX 模式</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/form-ux-pattern/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/form-ux-pattern/</guid><description>&lt;p>表單輸入和 terminal 輸入用同一套四維度決策框架（keyboard type / submit model / IME policy / special keys），但每個維度的選項和取捨方向不同。表單場景的使用者輸入的是結構化但自然語言為主的內容 — 姓名、email、地址 — 手機鍵盤的自動行為在這個場景中大部分是幫助。&lt;/p>
&lt;h2 id="keyboard-type-的選擇">Keyboard type 的選擇&lt;/h2>
&lt;p>表單的每個欄位應該使用最適合該欄位內容的鍵盤。正確的 keyboard type 減少使用者在鍵盤上找按鍵的時間，也讓自動填入和驗證更準確。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>Keyboard type&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Email&lt;/td>
 &lt;td>emailAddress&lt;/td>
 &lt;td>有 &lt;code>@&lt;/code> 和 &lt;code>.&lt;/code> 快捷鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>電話號碼&lt;/td>
 &lt;td>phone&lt;/td>
 &lt;td>只顯示數字和 &lt;code>+&lt;/code> &lt;code>-&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>URL&lt;/td>
 &lt;td>url&lt;/td>
 &lt;td>有 &lt;code>.com&lt;/code> 快捷鍵和 &lt;code>/&lt;/code> 鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>密碼&lt;/td>
 &lt;td>visiblePassword&lt;/td>
 &lt;td>關閉自動校正，保留字元可見控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>搜尋&lt;/td>
 &lt;td>text&lt;/td>
 &lt;td>一般文字，可搭配 &lt;code>textInputAction: search&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數字金額&lt;/td>
 &lt;td>numberWithOptions(decimal: true)&lt;/td>
 &lt;td>數字鍵盤加小數點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="驗證時機">驗證時機&lt;/h2>
&lt;p>表單驗證的時機影響使用者的操作流暢度和錯誤修正效率。&lt;/p>
&lt;h3 id="即時驗證on-change">即時驗證（on change）&lt;/h3>
&lt;p>每次輸入變化時驗證。適合格式明確的欄位（email 格式、手機號碼長度）。即時驗證在使用者輸入過程中就能回饋格式錯誤，不需要等到送出。&lt;/p>
&lt;p>即時驗證的風險是過早報錯。使用者正在輸入 email 地址 &lt;code>user@&lt;/code> 時，缺少 domain 部分 — 這個時候報「email 格式錯誤」對使用者沒有幫助。解法是在欄位失去焦點（on blur）時才顯示錯誤，輸入過程中只顯示通過的驗證（例如勾號表示格式正確）。&lt;/p>
&lt;h3 id="送出時驗證on-submit">送出時驗證（on submit）&lt;/h3>
&lt;p>使用者按送出按鈕時統一驗證所有欄位。適合需要多欄位交叉驗證的場景（密碼確認、日期範圍）。&lt;/p>
&lt;p>送出時驗證的風險是使用者填完整張表單才知道哪裡有問題。在欄位多的表單中，使用者需要回頭找到錯誤欄位修正 — 用 scroll 定位和欄位 highlight 減輕這個成本。&lt;/p>
&lt;h3 id="混合策略">混合策略&lt;/h3>
&lt;p>格式驗證用即時（on blur）、業務邏輯驗證用送出時。Email 格式在失去焦點時檢查，「email 是否已被註冊」在送出時呼叫 API 檢查。&lt;/p>
&lt;h2 id="auto-fill-支援">Auto-fill 支援&lt;/h2>
&lt;p>手機系統（iOS AutoFill、Android Autofill）可以自動填入使用者已儲存的資訊（姓名、地址、信用卡）。App 需要正確標記每個欄位的語意類型（&lt;code>autofillHints&lt;/code>），系統才能匹配正確的儲存值。&lt;/p>
&lt;p>正確標記的欄位讓使用者一鍵填入而非手動輸入 — 在 mobile 上減少的打字量直接轉化為轉換率提升。&lt;/p>
&lt;h2 id="錯誤回饋">錯誤回饋&lt;/h2>
&lt;p>錯誤訊息的位置和內容影響使用者修正錯誤的效率。&lt;/p>
&lt;p>&lt;strong>位置&lt;/strong>：錯誤訊息應該緊跟在對應欄位下方，而非集中在表單頂部或底部。使用者需要在看到錯誤的同時看到對應的欄位，不需要來回比對。&lt;/p>
&lt;p>&lt;strong>內容&lt;/strong>：錯誤訊息應該說明「期望什麼」而非「哪裡錯了」。「請輸入有效的 email 地址」比「email 格式無效」提供更多行動指引。&lt;/p>
&lt;p>&lt;strong>視覺&lt;/strong>：錯誤欄位的邊框變色（通常紅色）讓使用者在視覺掃描時快速定位。搭配錯誤文字使用，不要只靠顏色（色盲使用者）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>搜尋場景的輸入設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/search-ux-pattern/" data-link-title="搜尋 UX 模式" data-link-desc="Debounce / instant / suggestion 三種搜尋模式的取捨 — 和輸入機制的 submit model 維度直接相關">搜尋 UX 模式&lt;/a>&lt;/li>
&lt;li>四維度決策表總覽 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表&lt;/a>&lt;/li>
&lt;li>安全敏感欄位的 IME 控制 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>表單輸入和 terminal 輸入用同一套四維度決策框架（keyboard type / submit model / IME policy / special keys），但每個維度的選項和取捨方向不同。表單場景的使用者輸入的是結構化但自然語言為主的內容 — 姓名、email、地址 — 手機鍵盤的自動行為在這個場景中大部分是幫助。</p>
<h2 id="keyboard-type-的選擇">Keyboard type 的選擇</h2>
<p>表單的每個欄位應該使用最適合該欄位內容的鍵盤。正確的 keyboard type 減少使用者在鍵盤上找按鍵的時間，也讓自動填入和驗證更準確。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Keyboard type</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Email</td>
          <td>emailAddress</td>
          <td>有 <code>@</code> 和 <code>.</code> 快捷鍵</td>
      </tr>
      <tr>
          <td>電話號碼</td>
          <td>phone</td>
          <td>只顯示數字和 <code>+</code> <code>-</code></td>
      </tr>
      <tr>
          <td>URL</td>
          <td>url</td>
          <td>有 <code>.com</code> 快捷鍵和 <code>/</code> 鍵</td>
      </tr>
      <tr>
          <td>密碼</td>
          <td>visiblePassword</td>
          <td>關閉自動校正，保留字元可見控制</td>
      </tr>
      <tr>
          <td>搜尋</td>
          <td>text</td>
          <td>一般文字，可搭配 <code>textInputAction: search</code></td>
      </tr>
      <tr>
          <td>數字金額</td>
          <td>numberWithOptions(decimal: true)</td>
          <td>數字鍵盤加小數點</td>
      </tr>
  </tbody>
</table>
<h2 id="驗證時機">驗證時機</h2>
<p>表單驗證的時機影響使用者的操作流暢度和錯誤修正效率。</p>
<h3 id="即時驗證on-change">即時驗證（on change）</h3>
<p>每次輸入變化時驗證。適合格式明確的欄位（email 格式、手機號碼長度）。即時驗證在使用者輸入過程中就能回饋格式錯誤，不需要等到送出。</p>
<p>即時驗證的風險是過早報錯。使用者正在輸入 email 地址 <code>user@</code> 時，缺少 domain 部分 — 這個時候報「email 格式錯誤」對使用者沒有幫助。解法是在欄位失去焦點（on blur）時才顯示錯誤，輸入過程中只顯示通過的驗證（例如勾號表示格式正確）。</p>
<h3 id="送出時驗證on-submit">送出時驗證（on submit）</h3>
<p>使用者按送出按鈕時統一驗證所有欄位。適合需要多欄位交叉驗證的場景（密碼確認、日期範圍）。</p>
<p>送出時驗證的風險是使用者填完整張表單才知道哪裡有問題。在欄位多的表單中，使用者需要回頭找到錯誤欄位修正 — 用 scroll 定位和欄位 highlight 減輕這個成本。</p>
<h3 id="混合策略">混合策略</h3>
<p>格式驗證用即時（on blur）、業務邏輯驗證用送出時。Email 格式在失去焦點時檢查，「email 是否已被註冊」在送出時呼叫 API 檢查。</p>
<h2 id="auto-fill-支援">Auto-fill 支援</h2>
<p>手機系統（iOS AutoFill、Android Autofill）可以自動填入使用者已儲存的資訊（姓名、地址、信用卡）。App 需要正確標記每個欄位的語意類型（<code>autofillHints</code>），系統才能匹配正確的儲存值。</p>
<p>正確標記的欄位讓使用者一鍵填入而非手動輸入 — 在 mobile 上減少的打字量直接轉化為轉換率提升。</p>
<h2 id="錯誤回饋">錯誤回饋</h2>
<p>錯誤訊息的位置和內容影響使用者修正錯誤的效率。</p>
<p><strong>位置</strong>：錯誤訊息應該緊跟在對應欄位下方，而非集中在表單頂部或底部。使用者需要在看到錯誤的同時看到對應的欄位，不需要來回比對。</p>
<p><strong>內容</strong>：錯誤訊息應該說明「期望什麼」而非「哪裡錯了」。「請輸入有效的 email 地址」比「email 格式無效」提供更多行動指引。</p>
<p><strong>視覺</strong>：錯誤欄位的邊框變色（通常紅色）讓使用者在視覺掃描時快速定位。搭配錯誤文字使用，不要只靠顏色（色盲使用者）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>搜尋場景的輸入設計 → <a href="/blog/ux-design/03-input-mechanism/search-ux-pattern/" data-link-title="搜尋 UX 模式" data-link-desc="Debounce / instant / suggestion 三種搜尋模式的取捨 — 和輸入機制的 submit model 維度直接相關">搜尋 UX 模式</a></li>
<li>四維度決策表總覽 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>安全敏感欄位的 IME 控制 → <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist</a></li>
</ul>
]]></content:encoded></item><item><title>模組三：輸入機制設計</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/</guid><description>&lt;p>回答「使用者怎麼輸入資料」。手機鍵盤和桌面鍵盤的差異比想像的大。&lt;/p>
&lt;h2 id="對應-findings">對應 findings&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>UF-6&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>&lt;/td>
 &lt;td>6 個 TextField 參數全是事後 hotfix — &lt;strong>本模組主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UF-7&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>&lt;/td>
 &lt;td>enableIMEPersonalizedLearning 有安全意涵（secret → IME 詞庫）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UF-8&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>&lt;/td>
 &lt;td>整行送出 vs 逐字元影響 protocol 設計 — &lt;strong>SSoT 主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 輸入機制四維度決策表（keyboard type / submit model / IME policy / special keys）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Terminal app 輸入設計（CLI 特殊需求）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 表單 UX 模式（validate / auto-fill / error feedback）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 搜尋 UX 模式（debounce / instant / suggestion）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 安全敏感輸入框的 IME 控制 checklist&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試&lt;/a>：整行 vs 逐字元影響 protocol test 斷言&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安&lt;/a>：IME 個人化學習 = secret 洩漏風險&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「使用者怎麼輸入資料」。手機鍵盤和桌面鍵盤的差異比想像的大。</p>
<h2 id="對應-findings">對應 findings</h2>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>來源</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>UF-6</td>
          <td><a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a></td>
          <td>6 個 TextField 參數全是事後 hotfix — <strong>本模組主寫</strong></td>
      </tr>
      <tr>
          <td>UF-7</td>
          <td><a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a></td>
          <td>enableIMEPersonalizedLearning 有安全意涵（secret → IME 詞庫）</td>
      </tr>
      <tr>
          <td>UF-8</td>
          <td><a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a></td>
          <td>整行送出 vs 逐字元影響 protocol 設計 — <strong>SSoT 主寫</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> 輸入機制四維度決策表（keyboard type / submit model / IME policy / special keys）</li>
<li><input checked="" disabled="" type="checkbox"> Terminal app 輸入設計（CLI 特殊需求）</li>
<li><input checked="" disabled="" type="checkbox"> 表單 UX 模式（validate / auto-fill / error feedback）</li>
<li><input checked="" disabled="" type="checkbox"> 搜尋 UX 模式（debounce / instant / suggestion）</li>
<li><input checked="" disabled="" type="checkbox"> 安全敏感輸入框的 IME 控制 checklist</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a>：整行 vs 逐字元影響 protocol test 斷言</li>
<li>→ <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a>：IME 個人化學習 = secret 洩漏風險</li>
</ul>
]]></content:encoded></item><item><title>Permission 請求時機與措辭</title><link>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/permission-request-timing/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/permission-request-timing/</guid><description>&lt;p>系統權限（相機、位置、通知、麥克風）的請求對話框由作業系統控制，app 只能決定「什麼時候觸發」和「觸發前顯示什麼說明」。使用者拒絕後，再次請求不會彈出系統對話框 — 必須引導使用者到系統設定手動開啟。這意味著第一次請求的時機和說明內容直接影響授權率。&lt;/p>
&lt;h2 id="請求時機">請求時機&lt;/h2>
&lt;h3 id="首次開啟時一次性請求">首次開啟時一次性請求&lt;/h3>
&lt;p>App 首次啟動時依序請求所有需要的權限。優點是使用者只被打斷一次；缺點是使用者尚未使用任何功能，不理解每個權限的用途，傾向拒絕。&lt;/p>
&lt;p>這個模式適合權限數量少（1-2 個）且和 app 核心功能直接相關的情境。相機 app 在首次開啟時請求相機權限，使用者能直覺理解原因。&lt;/p>
&lt;h3 id="功能使用時即時請求">功能使用時即時請求&lt;/h3>
&lt;p>使用者點擊需要權限的功能時才請求。優點是使用者在操作 context 中，能理解為什麼需要這個權限；缺點是操作流程被打斷。&lt;/p>
&lt;p>這個模式適合權限和特定功能綁定的情境。掃描 QR code 時請求相機權限，使用者正在嘗試掃描，理解為什麼需要相機。&lt;/p>
&lt;h3 id="推薦策略">推薦策略&lt;/h3>
&lt;p>功能使用時即時請求是多數場景的推薦策略。使用者有操作 context，授權率較高。打斷可以透過 pre-permission 說明畫面降低突兀感。&lt;/p>
&lt;h2 id="pre-permission-說明畫面">Pre-permission 說明畫面&lt;/h2>
&lt;p>在觸發系統權限對話框之前，app 先顯示自己的說明畫面，解釋為什麼需要這個權限和用途。&lt;/p>
&lt;p>說明畫面的設計要點：&lt;/p>
&lt;p>&lt;strong>說明用途而非技術細節&lt;/strong>。「需要相機來掃描裝置上的 QR code」比「app 需要存取 AVCaptureDevice」更有用。使用者關心的是「為什麼」，不是「用什麼 API」。&lt;/p>
&lt;p>&lt;strong>提供「稍後再說」選項&lt;/strong>。使用者可能想先了解 app 再決定是否授權。強制授權（沒有跳過選項）會讓使用者選擇拒絕。&lt;/p>
&lt;p>&lt;strong>視覺化說明&lt;/strong>。用截圖或圖示展示「授權後這個功能長什麼樣」，讓使用者預覽授權的價值。&lt;/p>
&lt;h2 id="拒絕後的處理">拒絕後的處理&lt;/h2>
&lt;p>使用者拒絕權限後，app 需要：&lt;/p>
&lt;p>&lt;strong>記住拒絕狀態&lt;/strong>。不要在每次使用者操作同一功能時都顯示 pre-permission 說明（使用者已經表達不想授權，反覆詢問是騷擾）。&lt;/p>
&lt;p>&lt;strong>提供功能降級&lt;/strong>。如果可能，提供不需要權限的替代方案。掃描 QR code 可以改成手動輸入配對碼。&lt;/p>
&lt;p>&lt;strong>在適當時機再提醒&lt;/strong>。使用者多次使用需要權限的功能但都因為沒有權限而失敗時，用非侵入式提示（Snackbar）說明「開啟相機權限可以使用掃描功能」加設定連結。&lt;/p>
&lt;p>&lt;strong>引導到系統設定&lt;/strong>。一旦使用者在系統對話框中選擇「不再詢問」（Android）或拒絕（iOS 拒絕後系統不再彈窗），唯一的路徑是引導使用者到系統設定手動開啟。提供直接跳轉到 app 設定頁面的按鈕。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Gate 設計的通用方法論 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>網路 gate 的處理策略 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/network-offline-ux/" data-link-title="網路斷線 UX 模式" data-link-desc="Offline-first / retry / degraded mode 三種網路 gate 的處理策略 — 取決於功能是否依賴即時連線">網路斷線 UX 模式&lt;/a>&lt;/li>
&lt;li>開發環境遮蔽 gate 問題 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/dev-vs-real-gate-behavior/" data-link-title="開發環境 vs 真機的 gate 行為差異表" data-link-desc="模擬器、debug build、test 環境中的 gate 行為和真機 release build 不同 — 差異表讓開發者在上機前知道哪些 gate 還沒被真實驗證">開發環境 vs 真機的 gate 行為差異表&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>系統權限（相機、位置、通知、麥克風）的請求對話框由作業系統控制，app 只能決定「什麼時候觸發」和「觸發前顯示什麼說明」。使用者拒絕後，再次請求不會彈出系統對話框 — 必須引導使用者到系統設定手動開啟。這意味著第一次請求的時機和說明內容直接影響授權率。</p>
<h2 id="請求時機">請求時機</h2>
<h3 id="首次開啟時一次性請求">首次開啟時一次性請求</h3>
<p>App 首次啟動時依序請求所有需要的權限。優點是使用者只被打斷一次；缺點是使用者尚未使用任何功能，不理解每個權限的用途，傾向拒絕。</p>
<p>這個模式適合權限數量少（1-2 個）且和 app 核心功能直接相關的情境。相機 app 在首次開啟時請求相機權限，使用者能直覺理解原因。</p>
<h3 id="功能使用時即時請求">功能使用時即時請求</h3>
<p>使用者點擊需要權限的功能時才請求。優點是使用者在操作 context 中，能理解為什麼需要這個權限；缺點是操作流程被打斷。</p>
<p>這個模式適合權限和特定功能綁定的情境。掃描 QR code 時請求相機權限，使用者正在嘗試掃描，理解為什麼需要相機。</p>
<h3 id="推薦策略">推薦策略</h3>
<p>功能使用時即時請求是多數場景的推薦策略。使用者有操作 context，授權率較高。打斷可以透過 pre-permission 說明畫面降低突兀感。</p>
<h2 id="pre-permission-說明畫面">Pre-permission 說明畫面</h2>
<p>在觸發系統權限對話框之前，app 先顯示自己的說明畫面，解釋為什麼需要這個權限和用途。</p>
<p>說明畫面的設計要點：</p>
<p><strong>說明用途而非技術細節</strong>。「需要相機來掃描裝置上的 QR code」比「app 需要存取 AVCaptureDevice」更有用。使用者關心的是「為什麼」，不是「用什麼 API」。</p>
<p><strong>提供「稍後再說」選項</strong>。使用者可能想先了解 app 再決定是否授權。強制授權（沒有跳過選項）會讓使用者選擇拒絕。</p>
<p><strong>視覺化說明</strong>。用截圖或圖示展示「授權後這個功能長什麼樣」，讓使用者預覽授權的價值。</p>
<h2 id="拒絕後的處理">拒絕後的處理</h2>
<p>使用者拒絕權限後，app 需要：</p>
<p><strong>記住拒絕狀態</strong>。不要在每次使用者操作同一功能時都顯示 pre-permission 說明（使用者已經表達不想授權，反覆詢問是騷擾）。</p>
<p><strong>提供功能降級</strong>。如果可能，提供不需要權限的替代方案。掃描 QR code 可以改成手動輸入配對碼。</p>
<p><strong>在適當時機再提醒</strong>。使用者多次使用需要權限的功能但都因為沒有權限而失敗時，用非侵入式提示（Snackbar）說明「開啟相機權限可以使用掃描功能」加設定連結。</p>
<p><strong>引導到系統設定</strong>。一旦使用者在系統對話框中選擇「不再詢問」（Android）或拒絕（iOS 拒絕後系統不再彈窗），唯一的路徑是引導使用者到系統設定手動開啟。提供直接跳轉到 app 設定頁面的按鈕。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Gate 設計的通用方法論 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>網路 gate 的處理策略 → <a href="/blog/ux-design/02-gate-fallback/network-offline-ux/" data-link-title="網路斷線 UX 模式" data-link-desc="Offline-first / retry / degraded mode 三種網路 gate 的處理策略 — 取決於功能是否依賴即時連線">網路斷線 UX 模式</a></li>
<li>開發環境遮蔽 gate 問題 → <a href="/blog/ux-design/02-gate-fallback/dev-vs-real-gate-behavior/" data-link-title="開發環境 vs 真機的 gate 行為差異表" data-link-desc="模擬器、debug build、test 環境中的 gate 行為和真機 release build 不同 — 差異表讓開發者在上機前知道哪些 gate 還沒被真實驗證">開發環境 vs 真機的 gate 行為差異表</a></li>
</ul>
]]></content:encoded></item><item><title>搜尋 UX 模式</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/search-ux-pattern/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/search-ux-pattern/</guid><description>&lt;p>搜尋輸入的核心決策是「使用者輸入到什麼程度觸發搜尋」。這和 terminal 輸入的 submit model 維度相同 — 差別在 terminal 場景的選項是「整行 vs 逐字元」，搜尋場景的選項是「按送出 vs 即時 vs debounce」。&lt;/p>
&lt;h2 id="三種觸發模式">三種觸發模式&lt;/h2>
&lt;h3 id="按送出觸發">按送出觸發&lt;/h3>
&lt;p>使用者打完搜尋詞、按搜尋按鈕後觸發一次搜尋。最簡單的模式 — 一次搜尋、一次 API 呼叫、一次結果顯示。&lt;/p>
&lt;p>適合搜尋成本高的場景：資料庫全文搜尋、外部 API 呼叫（有速率限制或費用）、搜尋結果需要複雜運算。&lt;/p>
&lt;h3 id="即時觸發instant">即時觸發（instant）&lt;/h3>
&lt;p>使用者每輸入一個字元就觸發搜尋。結果即時更新，使用者可以在輸入過程中看到搜尋結果逐漸精確。&lt;/p>
&lt;p>適合搜尋成本低的場景：client 端的本地過濾、記憶體內的資料集篩選、已快取的少量資料。&lt;/p>
&lt;p>即時觸發在搜尋成本高的場景會產生問題：使用者輸入 &lt;code>hello&lt;/code> 的過程中觸發五次 API 呼叫（&lt;code>h&lt;/code>、&lt;code>he&lt;/code>、&lt;code>hel&lt;/code>、&lt;code>hell&lt;/code>、&lt;code>hello&lt;/code>），前四次的結果在使用者看到之前就被覆蓋。浪費的 API 呼叫增加 server 負載和使用者的網路流量。&lt;/p>
&lt;h3 id="debounce-觸發">Debounce 觸發&lt;/h3>
&lt;p>使用者停止輸入一段時間後（通常 300-500ms）觸發搜尋。平衡即時回饋和 API 呼叫次數 — 使用者連續打字時不觸發，停下來時觸發一次。&lt;/p>
&lt;p>Debounce 是遠端搜尋場景的常見選擇。延遲時間的設定是 UX trade-off：太短（100ms）接近即時觸發，API 呼叫次數多；太長（1000ms）使用者感覺到明顯延遲。300-500ms 是多數場景的合理區間。&lt;/p>
&lt;h2 id="搜尋結果的顯示">搜尋結果的顯示&lt;/h2>
&lt;h3 id="suggestion-list建議列表">Suggestion list（建議列表）&lt;/h3>
&lt;p>在搜尋框下方即時顯示候選結果。使用者可以點選候選項完成搜尋，不需要打完整個搜尋詞。&lt;/p>
&lt;p>Suggestion list 適合搜尋詞有限且可列舉的場景（城市名、產品名、使用者名）。搜尋詞無限（全文搜尋）時 suggestion list 的候選項品質依賴搜尋演算法。&lt;/p>
&lt;h3 id="結果頁">結果頁&lt;/h3>
&lt;p>使用者送出搜尋後導航到獨立的結果頁面。適合結果量大、需要分頁、每筆結果需要較多空間展示的場景。&lt;/p>
&lt;h3 id="即時過濾filter">即時過濾（filter）&lt;/h3>
&lt;p>在現有列表上即時隱藏不符合搜尋條件的項目，不導航到新頁面。適合「在已經看得到的清單中找到特定項目」的場景。&lt;/p>
&lt;h2 id="keyboard-type-和-textinputaction">keyboard type 和 textInputAction&lt;/h2>
&lt;p>搜尋框的 keyboard type 通常用 &lt;code>text&lt;/code>（一般文字），搭配 &lt;code>textInputAction: search&lt;/code> 讓鍵盤的 Enter 鍵顯示搜尋圖示（放大鏡）而非換行或送出圖示。&lt;/p>
&lt;p>這個細節影響使用者的操作直覺 — 看到搜尋圖示的按鈕，使用者知道按下去會觸發搜尋；看到換行圖示，使用者可能猶豫按下去會不會換行。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>四維度決策表總覽 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表&lt;/a>&lt;/li>
&lt;li>Terminal 場景的 submit model → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計&lt;/a>&lt;/li>
&lt;li>表單場景的驗證設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/form-ux-pattern/" data-link-title="表單 UX 模式" data-link-desc="表單輸入的驗證時機、auto-fill 支援、錯誤回饋設計 — 和 terminal 輸入的決策維度相同但選項不同">表單 UX 模式&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>搜尋輸入的核心決策是「使用者輸入到什麼程度觸發搜尋」。這和 terminal 輸入的 submit model 維度相同 — 差別在 terminal 場景的選項是「整行 vs 逐字元」，搜尋場景的選項是「按送出 vs 即時 vs debounce」。</p>
<h2 id="三種觸發模式">三種觸發模式</h2>
<h3 id="按送出觸發">按送出觸發</h3>
<p>使用者打完搜尋詞、按搜尋按鈕後觸發一次搜尋。最簡單的模式 — 一次搜尋、一次 API 呼叫、一次結果顯示。</p>
<p>適合搜尋成本高的場景：資料庫全文搜尋、外部 API 呼叫（有速率限制或費用）、搜尋結果需要複雜運算。</p>
<h3 id="即時觸發instant">即時觸發（instant）</h3>
<p>使用者每輸入一個字元就觸發搜尋。結果即時更新，使用者可以在輸入過程中看到搜尋結果逐漸精確。</p>
<p>適合搜尋成本低的場景：client 端的本地過濾、記憶體內的資料集篩選、已快取的少量資料。</p>
<p>即時觸發在搜尋成本高的場景會產生問題：使用者輸入 <code>hello</code> 的過程中觸發五次 API 呼叫（<code>h</code>、<code>he</code>、<code>hel</code>、<code>hell</code>、<code>hello</code>），前四次的結果在使用者看到之前就被覆蓋。浪費的 API 呼叫增加 server 負載和使用者的網路流量。</p>
<h3 id="debounce-觸發">Debounce 觸發</h3>
<p>使用者停止輸入一段時間後（通常 300-500ms）觸發搜尋。平衡即時回饋和 API 呼叫次數 — 使用者連續打字時不觸發，停下來時觸發一次。</p>
<p>Debounce 是遠端搜尋場景的常見選擇。延遲時間的設定是 UX trade-off：太短（100ms）接近即時觸發，API 呼叫次數多；太長（1000ms）使用者感覺到明顯延遲。300-500ms 是多數場景的合理區間。</p>
<h2 id="搜尋結果的顯示">搜尋結果的顯示</h2>
<h3 id="suggestion-list建議列表">Suggestion list（建議列表）</h3>
<p>在搜尋框下方即時顯示候選結果。使用者可以點選候選項完成搜尋，不需要打完整個搜尋詞。</p>
<p>Suggestion list 適合搜尋詞有限且可列舉的場景（城市名、產品名、使用者名）。搜尋詞無限（全文搜尋）時 suggestion list 的候選項品質依賴搜尋演算法。</p>
<h3 id="結果頁">結果頁</h3>
<p>使用者送出搜尋後導航到獨立的結果頁面。適合結果量大、需要分頁、每筆結果需要較多空間展示的場景。</p>
<h3 id="即時過濾filter">即時過濾（filter）</h3>
<p>在現有列表上即時隱藏不符合搜尋條件的項目，不導航到新頁面。適合「在已經看得到的清單中找到特定項目」的場景。</p>
<h2 id="keyboard-type-和-textinputaction">keyboard type 和 textInputAction</h2>
<p>搜尋框的 keyboard type 通常用 <code>text</code>（一般文字），搭配 <code>textInputAction: search</code> 讓鍵盤的 Enter 鍵顯示搜尋圖示（放大鏡）而非換行或送出圖示。</p>
<p>這個細節影響使用者的操作直覺 — 看到搜尋圖示的按鈕，使用者知道按下去會觸發搜尋；看到換行圖示，使用者可能猶豫按下去會不會換行。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>四維度決策表總覽 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>Terminal 場景的 submit model → <a href="/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計</a></li>
<li>表單場景的驗證設計 → <a href="/blog/ux-design/03-input-mechanism/form-ux-pattern/" data-link-title="表單 UX 模式" data-link-desc="表單輸入的驗證時機、auto-fill 支援、錯誤回饋設計 — 和 terminal 輸入的決策維度相同但選項不同">表單 UX 模式</a></li>
</ul>
]]></content:encoded></item><item><title>安全敏感輸入框的 IME 控制 checklist</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/</guid><description>&lt;p>IME（Input Method Editor）的個人化學習功能會從使用者輸入中學習新詞彙，存入 IME 詞庫，跨 app 適用。在處理 secret 的輸入框中，這個功能把密碼、API key、伺服器路徑等敏感資訊存入了 IME 的持久化儲存 — 其他 app 的使用者在輸入時可能在建議列表中看到這些內容。&lt;/p>
&lt;h2 id="為什麼是安全問題">為什麼是安全問題&lt;/h2>
&lt;p>&lt;code>enableIMEPersonalizedLearning&lt;/code> 控制的是 IME 是否從當前輸入框的內容學習新詞彙。預設值是 &lt;code>true&lt;/code> — IME 會學習使用者輸入的所有內容。&lt;/p>
&lt;p>在一般文字輸入場景中（聊天、筆記、email），IME 學習使用者的常用詞彙是合理的 — 提高打字效率，減少重複輸入。&lt;/p>
&lt;p>在 CLI 場景中（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>），使用者可能輸入：&lt;/p>
&lt;ul>
&lt;li>資料庫密碼：&lt;code>mysql -p'MySecret123'&lt;/code>&lt;/li>
&lt;li>API key：&lt;code>curl -H 'Authorization: Bearer sk-abc123...'&lt;/code>&lt;/li>
&lt;li>伺服器路徑：&lt;code>ssh admin@192.168.1.100&lt;/code>&lt;/li>
&lt;li>環境變數：&lt;code>export DB_PASSWORD=secret&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>IME 學習這些輸入後，使用者在其他 app 打字時，IME 可能在建議列表中顯示 &lt;code>MySecret123&lt;/code> 或 &lt;code>sk-abc123&lt;/code> — 任何看到螢幕的人都能看到。&lt;/p>
&lt;p>這個風險和密碼外洩的傳統路徑不同。傳統密碼外洩通常是資料庫被入侵或傳輸被攔截；IME 學習造成的洩漏是使用者在日常打字時被動暴露，使用者不會意識到 IME 記住了他們在另一個 app 輸入的密碼。&lt;/p>
&lt;h2 id="checklist">Checklist&lt;/h2>
&lt;p>處理以下任何一類內容的輸入框，應全部通過此 checklist：&lt;/p>
&lt;ul>
&lt;li>密碼、PIN 碼&lt;/li>
&lt;li>API key、token、secret&lt;/li>
&lt;li>伺服器位址、連線字串&lt;/li>
&lt;li>CLI 指令（可能包含上述任何一類）&lt;/li>
&lt;li>信用卡號碼&lt;/li>
&lt;li>任何標示為 confidential 的欄位&lt;/li>
&lt;/ul>
&lt;h3 id="必須關閉的-ime-控制">必須關閉的 IME 控制&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>&lt;code>enableIMEPersonalizedLearning: false&lt;/code>&lt;/td>
 &lt;td>防止 secret 進入 IME 詞庫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自動校正&lt;/td>
 &lt;td>&lt;code>autocorrect: false&lt;/code>&lt;/td>
 &lt;td>防止 secret 被替換成字典詞&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>輸入建議&lt;/td>
 &lt;td>&lt;code>enableSuggestions: false&lt;/code>&lt;/td>
 &lt;td>防止 secret 出現在建議列表&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="建議的-keyboard-type">建議的 keyboard type&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Keyboard type&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>密碼&lt;/td>
 &lt;td>visiblePassword&lt;/td>
 &lt;td>關閉自動校正，可選顯示/隱藏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CLI 指令&lt;/td>
 &lt;td>visiblePassword&lt;/td>
 &lt;td>需要精確輸入，不要自動校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>信用卡號碼&lt;/td>
 &lt;td>number&lt;/td>
 &lt;td>只需要數字鍵盤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>連線字串&lt;/td>
 &lt;td>url&lt;/td>
 &lt;td>有 &lt;code>.&lt;/code>、&lt;code>/&lt;/code>、&lt;code>:&lt;/code> 快捷鍵&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="code-review-檢查點">Code review 檢查點&lt;/h3>
&lt;p>Review 安全敏感輸入框的 TextField 實作時，逐項確認：&lt;/p>
&lt;ol>
&lt;li>&lt;code>enableIMEPersonalizedLearning&lt;/code> 是否明確設為 &lt;code>false&lt;/code>（不依賴預設值）&lt;/li>
&lt;li>&lt;code>autocorrect&lt;/code> 是否設為 &lt;code>false&lt;/code>&lt;/li>
&lt;li>&lt;code>enableSuggestions&lt;/code> 是否設為 &lt;code>false&lt;/code>&lt;/li>
&lt;li>&lt;code>keyboardType&lt;/code> 是否選擇了不會觸發自動行為的類型&lt;/li>
&lt;li>如果是密碼欄位，&lt;code>obscureText&lt;/code> 是否按需求設定&lt;/li>
&lt;/ol>
&lt;h2 id="平台差異">平台差異&lt;/h2>
&lt;p>&lt;code>enableIMEPersonalizedLearning&lt;/code> 是 Flutter 的 API，對應到不同平台的不同機制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>iOS&lt;/strong>：對應 &lt;code>UITextField.spellCheckingType = .no&lt;/code> 和相關 attribute。iOS 的 QuickType 學習機制由系統控制，app 只能建議不強制。&lt;/li>
&lt;li>&lt;strong>Android&lt;/strong>：對應 &lt;code>InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS&lt;/code> 等 flag。不同 IME app（Gboard、Samsung Keyboard、搜狗）對 flag 的遵守程度不一。&lt;/li>
&lt;/ul>
&lt;p>平台差異意味著 app 端的控制是「盡力而為」— 設定正確的 flag 是必要條件，但不保證所有 IME 都會遵守。安全敏感場景中，除了 IME 控制外，還應考慮 secure text entry（&lt;code>obscureText: true&lt;/code>）讓畫面上不顯示明文。&lt;/p></description><content:encoded><![CDATA[<p>IME（Input Method Editor）的個人化學習功能會從使用者輸入中學習新詞彙，存入 IME 詞庫，跨 app 適用。在處理 secret 的輸入框中，這個功能把密碼、API key、伺服器路徑等敏感資訊存入了 IME 的持久化儲存 — 其他 app 的使用者在輸入時可能在建議列表中看到這些內容。</p>
<h2 id="為什麼是安全問題">為什麼是安全問題</h2>
<p><code>enableIMEPersonalizedLearning</code> 控制的是 IME 是否從當前輸入框的內容學習新詞彙。預設值是 <code>true</code> — IME 會學習使用者輸入的所有內容。</p>
<p>在一般文字輸入場景中（聊天、筆記、email），IME 學習使用者的常用詞彙是合理的 — 提高打字效率，減少重複輸入。</p>
<p>在 CLI 場景中（<a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a>），使用者可能輸入：</p>
<ul>
<li>資料庫密碼：<code>mysql -p'MySecret123'</code></li>
<li>API key：<code>curl -H 'Authorization: Bearer sk-abc123...'</code></li>
<li>伺服器路徑：<code>ssh admin@192.168.1.100</code></li>
<li>環境變數：<code>export DB_PASSWORD=secret</code></li>
</ul>
<p>IME 學習這些輸入後，使用者在其他 app 打字時，IME 可能在建議列表中顯示 <code>MySecret123</code> 或 <code>sk-abc123</code> — 任何看到螢幕的人都能看到。</p>
<p>這個風險和密碼外洩的傳統路徑不同。傳統密碼外洩通常是資料庫被入侵或傳輸被攔截；IME 學習造成的洩漏是使用者在日常打字時被動暴露，使用者不會意識到 IME 記住了他們在另一個 app 輸入的密碼。</p>
<h2 id="checklist">Checklist</h2>
<p>處理以下任何一類內容的輸入框，應全部通過此 checklist：</p>
<ul>
<li>密碼、PIN 碼</li>
<li>API key、token、secret</li>
<li>伺服器位址、連線字串</li>
<li>CLI 指令（可能包含上述任何一類）</li>
<li>信用卡號碼</li>
<li>任何標示為 confidential 的欄位</li>
</ul>
<h3 id="必須關閉的-ime-控制">必須關閉的 IME 控制</h3>
<table>
  <thead>
      <tr>
          <th>控制項</th>
          <th>參數</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>個人化學習</td>
          <td><code>enableIMEPersonalizedLearning: false</code></td>
          <td>防止 secret 進入 IME 詞庫</td>
      </tr>
      <tr>
          <td>自動校正</td>
          <td><code>autocorrect: false</code></td>
          <td>防止 secret 被替換成字典詞</td>
      </tr>
      <tr>
          <td>輸入建議</td>
          <td><code>enableSuggestions: false</code></td>
          <td>防止 secret 出現在建議列表</td>
      </tr>
  </tbody>
</table>
<h3 id="建議的-keyboard-type">建議的 keyboard type</h3>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Keyboard type</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>密碼</td>
          <td>visiblePassword</td>
          <td>關閉自動校正，可選顯示/隱藏</td>
      </tr>
      <tr>
          <td>CLI 指令</td>
          <td>visiblePassword</td>
          <td>需要精確輸入，不要自動校正</td>
      </tr>
      <tr>
          <td>信用卡號碼</td>
          <td>number</td>
          <td>只需要數字鍵盤</td>
      </tr>
      <tr>
          <td>連線字串</td>
          <td>url</td>
          <td>有 <code>.</code>、<code>/</code>、<code>:</code> 快捷鍵</td>
      </tr>
  </tbody>
</table>
<h3 id="code-review-檢查點">Code review 檢查點</h3>
<p>Review 安全敏感輸入框的 TextField 實作時，逐項確認：</p>
<ol>
<li><code>enableIMEPersonalizedLearning</code> 是否明確設為 <code>false</code>（不依賴預設值）</li>
<li><code>autocorrect</code> 是否設為 <code>false</code></li>
<li><code>enableSuggestions</code> 是否設為 <code>false</code></li>
<li><code>keyboardType</code> 是否選擇了不會觸發自動行為的類型</li>
<li>如果是密碼欄位，<code>obscureText</code> 是否按需求設定</li>
</ol>
<h2 id="平台差異">平台差異</h2>
<p><code>enableIMEPersonalizedLearning</code> 是 Flutter 的 API，對應到不同平台的不同機制：</p>
<ul>
<li><strong>iOS</strong>：對應 <code>UITextField.spellCheckingType = .no</code> 和相關 attribute。iOS 的 QuickType 學習機制由系統控制，app 只能建議不強制。</li>
<li><strong>Android</strong>：對應 <code>InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS</code> 等 flag。不同 IME app（Gboard、Samsung Keyboard、搜狗）對 flag 的遵守程度不一。</li>
</ul>
<p>平台差異意味著 app 端的控制是「盡力而為」— 設定正確的 flag 是必要條件，但不保證所有 IME 都會遵守。安全敏感場景中，除了 IME 控制外，還應考慮 secure text entry（<code>obscureText: true</code>）讓畫面上不顯示明文。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>四維度決策表總覽 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>IME 個人化學習在 monitoring 中的安全考量 → <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a></li>
<li>Terminal 場景的完整輸入設計 → <a href="/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計</a></li>
</ul>
]]></content:encoded></item><item><title>模組五：導航模式</title><link>https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/</guid><description>&lt;p>回答「畫面之間怎麼跳」。&lt;/p>
&lt;h2 id="對應-findings">對應 findings&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>UF-10&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4&lt;/a>&lt;/td>
 &lt;td>go vs push 語意差異影響 UX — &lt;strong>本模組主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Mobile 導航模式分類（push/pop / declarative router / tab / drawer）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Flutter GoRouter 導航設計&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> iOS HIG vs Material Design 導航差異&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Deep link 設計&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> go vs push vs pushReplacement 的 UX 語意表&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>← &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一&lt;/a>：狀態矩陣的「退出路徑」欄位決定用 go 還是 push&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四&lt;/a>：導航路徑需要 widget test 驗證&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「畫面之間怎麼跳」。</p>
<h2 id="對應-findings">對應 findings</h2>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>來源</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>UF-10</td>
          <td><a href="/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4</a></td>
          <td>go vs push 語意差異影響 UX — <strong>本模組主寫</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> Mobile 導航模式分類（push/pop / declarative router / tab / drawer）</li>
<li><input checked="" disabled="" type="checkbox"> Flutter GoRouter 導航設計</li>
<li><input checked="" disabled="" type="checkbox"> iOS HIG vs Material Design 導航差異</li>
<li><input checked="" disabled="" type="checkbox"> Deep link 設計</li>
<li><input checked="" disabled="" type="checkbox"> go vs push vs pushReplacement 的 UX 語意表</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>← <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一</a>：狀態矩陣的「退出路徑」欄位決定用 go 還是 push</li>
<li>→ <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四</a>：導航路徑需要 widget test 驗證</li>
</ul>
]]></content:encoded></item><item><title>Motor 可達性：hit target、間距、誤點防護</title><link>https://tarrragon.github.io/blog/report/motor-accessibility-hit-target/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/motor-accessibility-hit-target/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Hit target ≥ 44×44px、相鄰互動元素之間有間距、避免「精準瞄準」需求。&lt;/strong> Motor accessibility 處理的不是視覺、是「手能否準確點擊」 — 行動裝置使用者、年長使用者、motor 障礙使用者都受益。設計時優先擴大 padding、不是縮小視覺。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>motor 可達性&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>視覺呈現面的 a11y&lt;/strong>由 &lt;a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>鍵盤使用者的 a11y&lt;/strong>由 &lt;a href="../keyboard-accessibility/">#52 鍵盤可達性&lt;/a> 處理&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼-motor-可達性需要獨立盤點">為什麼 motor 可達性需要獨立盤點&lt;/h2>
&lt;h3 id="使用者類型">使用者類型&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>使用者&lt;/th>
 &lt;th>為什麼 hit target 重要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>行動裝置使用者&lt;/td>
 &lt;td>手指比滑鼠粗、需要更大目標&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>年長使用者&lt;/td>
 &lt;td>手部精準度下降&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Motor 障礙使用者&lt;/td>
 &lt;td>Tremor / 手部協調困難&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>暫時受限使用者（拿東西單手操作、晃動環境）&lt;/td>
 &lt;td>短期內精準度下降&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最後一類包含「正常使用者在某些情境」 — motor a11y 的設計對全體使用者都有價值。&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>Hit target &amp;lt; 24px&lt;/td>
 &lt;td>行動裝置上難點&lt;/td>
 &lt;td>多數行動使用者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>相鄰互動元素間距不足&lt;/td>
 &lt;td>誤觸隔壁&lt;/td>
 &lt;td>手指粗 / motor 障礙者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要精準 drag / pinch&lt;/td>
 &lt;td>部分 motor 障礙者無法&lt;/td>
 &lt;td>motor 障礙者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>短時間內需多次精準操作&lt;/td>
 &lt;td>tremor 使用者無法&lt;/td>
 &lt;td>tremor 使用者&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="wcag-標準">WCAG 標準&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>2.5.5 Target Size&lt;/td>
 &lt;td>互動元素 ≥ 44×44 CSS px&lt;/td>
 &lt;td>AAA&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2.5.8 Target Size (Minimum)&lt;/td>
 &lt;td>互動元素 ≥ 24×24 CSS px（除非有間距足夠的等價替代）&lt;/td>
 &lt;td>AA（WCAG 2.2 新增）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2.5.7 Dragging Movements&lt;/td>
 &lt;td>拖拽動作有單擊替代&lt;/td>
 &lt;td>AA（WCAG 2.2 新增）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>WCAG 2.2 把 motor a11y 從 AAA 拉到部分 AA — 顯示這類問題的重要性提升。&lt;/p>
&lt;hr>
&lt;h2 id="風險點-1hit-target-太小">風險點 1：Hit target 太小&lt;/h2>
&lt;p>&lt;strong>位置&lt;/strong>：scope UI 的 radio buttons、filter checkbox。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>WCAG 2.5.5（AAA）建議互動元素 hit target ≥ 44×44px&lt;/li>
&lt;li>Native &lt;code>&amp;lt;input type=&amp;quot;radio&amp;quot;&amp;gt;&lt;/code> 在桌面 ~13×13px、行動裝置 24×24px&lt;/li>
&lt;li>label 包住 input + 文字、整個 label 可點 — 提升 hit target&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：行動裝置使用者點擊精準度不足、誤點旁邊選項。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：量 label 整體（含 padding）的高度與寬度。&lt;/p>
&lt;p>&lt;strong>修正方向&lt;/strong>：&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="c">/* 較差 — input 視覺很小、label 文字緊鄰 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nt">label&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">inline-block&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="nt">input&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;radio&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">13&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">13&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"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c">/* 好 — label 整個區域可點、padding 撐到 44px */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nt">label&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">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">inline-flex&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">align-items&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">center&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">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.625&lt;/span>&lt;span class="kt">rem&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">rem&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 約 44px 高 */&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">cursor&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">pointer&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;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nt">input&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;radio&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">margin-right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.5&lt;/span>&lt;span class="kt">rem&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>關鍵不是把 input 視覺變大、是把可點區域擴大（padding）— 視覺保持精緻、可點區域達標。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Hit target ≥ 44×44px、相鄰互動元素之間有間距、避免「精準瞄準」需求。</strong> Motor accessibility 處理的不是視覺、是「手能否準確點擊」 — 行動裝置使用者、年長使用者、motor 障礙使用者都受益。設計時優先擴大 padding、不是縮小視覺。</p>
<blockquote>
<p>本篇焦點：<strong>motor 可達性</strong>。</p>
<ul>
<li><strong>視覺呈現面的 a11y</strong>由 <a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助</a> 處理</li>
<li><strong>鍵盤使用者的 a11y</strong>由 <a href="../keyboard-accessibility/">#52 鍵盤可達性</a> 處理</li>
</ul></blockquote>
<hr>
<h2 id="為什麼-motor-可達性需要獨立盤點">為什麼 motor 可達性需要獨立盤點</h2>
<h3 id="使用者類型">使用者類型</h3>
<table>
  <thead>
      <tr>
          <th>使用者</th>
          <th>為什麼 hit target 重要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>行動裝置使用者</td>
          <td>手指比滑鼠粗、需要更大目標</td>
      </tr>
      <tr>
          <td>年長使用者</td>
          <td>手部精準度下降</td>
      </tr>
      <tr>
          <td>Motor 障礙使用者</td>
          <td>Tremor / 手部協調困難</td>
      </tr>
      <tr>
          <td>暫時受限使用者（拿東西單手操作、晃動環境）</td>
          <td>短期內精準度下降</td>
      </tr>
  </tbody>
</table>
<p>最後一類包含「正常使用者在某些情境」 — motor a11y 的設計對全體使用者都有價值。</p>
<h3 id="失敗模式">失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗</th>
          <th>表現</th>
          <th>影響範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hit target &lt; 24px</td>
          <td>行動裝置上難點</td>
          <td>多數行動使用者</td>
      </tr>
      <tr>
          <td>相鄰互動元素間距不足</td>
          <td>誤觸隔壁</td>
          <td>手指粗 / motor 障礙者</td>
      </tr>
      <tr>
          <td>需要精準 drag / pinch</td>
          <td>部分 motor 障礙者無法</td>
          <td>motor 障礙者</td>
      </tr>
      <tr>
          <td>短時間內需多次精準操作</td>
          <td>tremor 使用者無法</td>
          <td>tremor 使用者</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="wcag-標準">WCAG 標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>要求</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2.5.5 Target Size</td>
          <td>互動元素 ≥ 44×44 CSS px</td>
          <td>AAA</td>
      </tr>
      <tr>
          <td>2.5.8 Target Size (Minimum)</td>
          <td>互動元素 ≥ 24×24 CSS px（除非有間距足夠的等價替代）</td>
          <td>AA（WCAG 2.2 新增）</td>
      </tr>
      <tr>
          <td>2.5.7 Dragging Movements</td>
          <td>拖拽動作有單擊替代</td>
          <td>AA（WCAG 2.2 新增）</td>
      </tr>
  </tbody>
</table>
<p>WCAG 2.2 把 motor a11y 從 AAA 拉到部分 AA — 顯示這類問題的重要性提升。</p>
<hr>
<h2 id="風險點-1hit-target-太小">風險點 1：Hit target 太小</h2>
<p><strong>位置</strong>：scope UI 的 radio buttons、filter checkbox。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>WCAG 2.5.5（AAA）建議互動元素 hit target ≥ 44×44px</li>
<li>Native <code>&lt;input type=&quot;radio&quot;&gt;</code> 在桌面 ~13×13px、行動裝置 24×24px</li>
<li>label 包住 input + 文字、整個 label 可點 — 提升 hit target</li>
</ul>
<p><strong>症狀</strong>：行動裝置使用者點擊精準度不足、誤點旁邊選項。</p>
<p><strong>第一個該查的</strong>：量 label 整體（含 padding）的高度與寬度。</p>
<p><strong>修正方向</strong>：</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="c">/* 較差 — input 視覺很小、label 文字緊鄰 */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">label</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">inline-block</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s2">&#34;radio&#34;</span><span class="o">]</span> <span class="p">{</span> <span class="k">width</span><span class="p">:</span> <span class="mi">13</span><span class="kt">px</span><span class="p">;</span> <span class="k">height</span><span class="p">:</span> <span class="mi">13</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c">/* 好 — label 整個區域可點、padding 撐到 44px */</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">label</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">inline-flex</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">align-items</span><span class="p">:</span> <span class="kc">center</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mf">0.625</span><span class="kt">rem</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>  <span class="c">/* 約 44px 高 */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">cursor</span><span class="p">:</span> <span class="kc">pointer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s2">&#34;radio&#34;</span><span class="o">]</span> <span class="p">{</span> <span class="k">margin-right</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">rem</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>關鍵不是把 input 視覺變大、是把可點區域擴大（padding）— 視覺保持精緻、可點區域達標。</p>
<hr>
<h2 id="風險點-2相鄰互動元素間距不足">風險點 2：相鄰互動元素間距不足</h2>
<p><strong>位置</strong>：filter checkbox 列、scope radio 列。</p>
<p><strong>判讀</strong>：</p>
<p>兩個 hit target 緊鄰、即使各自達 44px、相鄰時仍可能誤觸 — WCAG 2.5.8 要求「目標之間有足夠間距」。</p>
<p><strong>症狀</strong>：使用者想點 A 但點到旁邊的 B。</p>
<p><strong>修正方向</strong>：</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="c">/* 加 gap 確保相鄰元素間距 */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">.</span><span class="nc">filter-list</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">flex</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">flex-direction</span><span class="p">:</span> <span class="kc">column</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">gap</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>  <span class="c">/* 至少 8px 間距 */</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></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c">/* 或用 padding 撐開 */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">.</span><span class="nc">filter-list</span> <span class="nt">label</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mf">0.625</span><span class="kt">rem</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">4</span><span class="kt">px</span><span class="p">;</span>  <span class="c">/* 加總間距達 8px+ */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>預設 8px 間距 — 比視覺需求多一點、避免誤觸。</p>
<hr>
<h2 id="風險點-3需要精準-drag--pinch-的操作">風險點 3：需要精準 drag / pinch 的操作</h2>
<p><strong>位置</strong>：搜尋頁未實作 drag 互動、但若未來加（例如拖拽結果排序、pinch 縮放圖片）。</p>
<p><strong>判讀</strong>：</p>
<p>WCAG 2.5.7（AA）要求 drag 動作有單擊替代 — 例如「拖拽排序」要有「上移 / 下移」按鈕作為替代。</p>
<p><strong>症狀</strong>：motor 障礙使用者無法完成 drag 操作。</p>
<p><strong>修正方向</strong>：</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="c">&lt;!-- 主互動：drag --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">li</span> <span class="na">draggable</span><span class="o">=</span><span class="s">&#34;true&#34;</span><span class="p">&gt;</span>項目 A<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c">&lt;!-- 必須提供：button 替代 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  項目 A
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;上移&#34;</span><span class="p">&gt;</span>↑<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;下移&#34;</span><span class="p">&gt;</span>↓<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span></span></span></code></pre></div><p>對搜尋頁當前實作不適用、但未來加互動時的預警。</p>
<hr>
<h2 id="設計取捨擴大-hit-target-的策略">設計取捨：擴大 hit target 的策略</h2>
<p>當「視覺精緻度」與「hit target 大小」衝突、四種做法：</p>
<h3 id="a視覺保持小padding-擴大可點區這個專案的預設">A：視覺保持小、padding 擴大可點區（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：input 視覺 13px、label padding 撐到 44px</li>
<li><strong>選 A 的理由</strong>：視覺精緻 + a11y 達標、兩全</li>
<li><strong>適合</strong>：絕大多數互動元素</li>
<li><strong>代價</strong>：UI 整體高度增加（每行 44px+）</li>
</ul>
<h3 id="b視覺直接放大到-44px">B：視覺直接放大到 44px</h3>
<ul>
<li><strong>機制</strong>：input width: 44px; height: 44px;</li>
<li><strong>跟 A 的取捨</strong>：B 視覺粗、A 視覺精緻；B 在「需要清楚看到」的情境（年長使用者）有價值</li>
<li><strong>B 比 A 好的情境</strong>：使用者主要是年長者、視覺辨識比精緻重要</li>
</ul>
<h3 id="c視覺小不擴-padding不滿足-a11y">C：視覺小、不擴 padding（不滿足 a11y）</h3>
<ul>
<li><strong>機制</strong>：input 13px、label 緊鄰文字、無 padding</li>
<li><strong>成本特別高的原因</strong>：行動使用者誤點、motor 障礙者無法用、違反 WCAG 2.5.8</li>
<li><strong>C 才合理的情境</strong>：純 desktop 應用 + 確認使用者群不含行動 / motor — 通常不該假設</li>
</ul>
<h3 id="d用-hover-area-擴大命中hover-才放大">D：用 hover area 擴大命中（hover 才放大）</h3>
<ul>
<li><strong>機制</strong>：預設視覺小、hover 時擴大可點區</li>
<li><strong>跟 A 的取捨</strong>：D 在 desktop 視覺精緻、hover 反饋也好；行動裝置沒有 hover、D 失敗</li>
<li><strong>D 比 A 好的情境</strong>：純 desktop 工具</li>
</ul>
<hr>
<h2 id="設計取捨誤點防護機制">設計取捨：誤點防護機制</h2>
<p>對「誤點代價高」的操作（刪除 / 提交 / 付款）、四種做法：</p>
<h3 id="a直接觸發--後續-undo這個專案的預設若有此類操作">A：直接觸發 + 後續 undo（這個專案的預設、若有此類操作）</h3>
<ul>
<li><strong>機制</strong>：點擊立刻執行、提供 undo 機制（例如 toast「已刪除、5 秒內可復原」）</li>
<li><strong>選 A 的理由</strong>：常見操作流暢、誤點有救</li>
<li><strong>適合</strong>：可逆操作（刪除、移動、隱藏）</li>
<li><strong>代價</strong>：實作 undo 機制需要儲狀態</li>
</ul>
<h3 id="b點擊--確認對話框">B：點擊 → 確認對話框</h3>
<ul>
<li><strong>機制</strong>：點擊出 confirm dialog「確定要 X 嗎？」</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>：需要長按 1 秒才觸發、誤點不會</li>
<li><strong>跟 A/B 的取捨</strong>：C 對 motor 障礙不友善（需要持續按）、且不直觀</li>
<li><strong>C 是反模式</strong>：對 motor 障礙不友善（需要持續按 1 秒）— 不直觀、違反 a11y 預期互動</li>
</ul>
<h3 id="d拖到確認區">D：拖到「確認區」</h3>
<ul>
<li><strong>機制</strong>：滑動到特定區域才觸發（iOS 拖刪除）</li>
<li><strong>跟 A/B 的取捨</strong>：D 對非典型互動使用者不直觀、違反 WCAG 2.5.7（需 button 替代）</li>
<li><strong>D 才合理的情境</strong>：搭配 button 替代（drag + button 兩種途徑都行）</li>
</ul>
<hr>
<h2 id="開發階段檢查清單">開發階段檢查清單</h2>
<table>
  <thead>
      <tr>
          <th>檢查</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hit target ≥ 44px</td>
          <td>DevTools Box Model 量 interactive 元素的 padding box</td>
      </tr>
      <tr>
          <td>相鄰元素間距 ≥ 8px</td>
          <td>DevTools 看 gap / margin</td>
      </tr>
      <tr>
          <td>行動裝置實測</td>
          <td>DevTools Device Mode + 實機測試</td>
      </tr>
      <tr>
          <td>不可逆操作有確認</td>
          <td>點擊「刪除」看是否有 confirm</td>
      </tr>
      <tr>
          <td>Drag 操作有 button 替代</td>
          <td>任何 drag 互動都有對應 button</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助</a></td>
          <td>互補 — 視覺面 vs 操作面、不同使用者群</td>
      </tr>
      <tr>
          <td><a href="../keyboard-accessibility/">#52 鍵盤可達性</a></td>
          <td>互補 — 鍵盤是 motor a11y 的一個面向（鍵盤精準度 &gt; 滑鼠 &gt; 觸控）、本篇處理觸控 / 點擊面</td>
      </tr>
      <tr>
          <td><a href="../native-html-over-aria-role/">#39 Native HTML 優先於 ARIA role</a></td>
          <td>用 native button / input 自動獲得合理 hit area、不需自行設計</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>行動使用者反映誤點</td>
          <td>量 hit target、&lt; 44px 加 padding</td>
      </tr>
      <tr>
          <td>「我這個介面只給 desktop 用」</td>
          <td>行動使用者比例可能比想像高、量化驗證</td>
      </tr>
      <tr>
          <td>Drag 互動沒有 button 替代</td>
          <td>加 button、達 WCAG 2.5.7</td>
      </tr>
      <tr>
          <td>不可逆操作沒有 confirm</td>
          <td>加 confirm dialog</td>
      </tr>
      <tr>
          <td>Filter list 元素緊鄰、容易誤觸</td>
          <td>加 gap ≥ 8px</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Motor a11y 是「手能否準確點擊」 — 不只給 motor 障礙使用者、行動使用者 / 年長者 / 暫時受限使用者都受益。預設 padding 擴 44px、間距 8px、不可逆操作加 confirm — 這些是基礎、不是優化。</p>
]]></content:encoded></item><item><title>每個畫面都需要出口：畫面狀態機設計與 UX 導航的系統性方法</title><link>https://tarrragon.github.io/blog/work-log/%E6%AF%8F%E5%80%8B%E7%95%AB%E9%9D%A2%E9%83%BD%E9%9C%80%E8%A6%81%E5%87%BA%E5%8F%A3%E7%95%AB%E9%9D%A2%E7%8B%80%E6%85%8B%E6%A9%9F%E8%A8%AD%E8%A8%88%E8%88%87-ux-%E5%B0%8E%E8%88%AA%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%96%B9%E6%B3%95/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E6%AF%8F%E5%80%8B%E7%95%AB%E9%9D%A2%E9%83%BD%E9%9C%80%E8%A6%81%E5%87%BA%E5%8F%A3%E7%95%AB%E9%9D%A2%E7%8B%80%E6%85%8B%E6%A9%9F%E8%A8%AD%E8%A8%88%E8%88%87-ux-%E5%B0%8E%E8%88%AA%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%96%B9%E6%B3%95/</guid><description>&lt;h2 id="這篇要解決什麼">這篇要解決什麼&lt;/h2>
&lt;blockquote>
&lt;p>使用者連上遠端終端機後、無法返回首頁。&lt;/p>&lt;/blockquote>
&lt;p>這是設計遺漏。Terminal 畫面的 &lt;code>connected&lt;/code> 狀態沒有 disconnect 按鈕也沒有 back 按鈕。&lt;code>error&lt;/code> 和 &lt;code>disconnected&lt;/code> 狀態也沒有。使用者被困在畫面裡，唯一的出路是殺掉 app。&lt;/p>
&lt;p>這不是「忘記加按鈕」的問題。回頭看企劃文件，操作盤點段確實列了「連線失敗顯示無法連線」這個失敗情境，但沒有系統性地問：&lt;strong>這個畫面有幾個狀態？每個狀態能做什麼操作？怎麼離開？&lt;/strong>&lt;/p>
&lt;p>本文整理畫面狀態機設計的方法、示範用狀態矩陣捕捉導航缺口、歸納 mobile app UX 的三個設計原則。&lt;/p>
&lt;hr>
&lt;h2 id="實際案例terminal-畫面的五個狀態">實際案例：Terminal 畫面的五個狀態&lt;/h2>
&lt;p>Terminal 畫面有一個 &lt;code>TerminalScreenUiState&lt;/code> enum 定義了五個狀態：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">enum&lt;/span> &lt;span class="n">TerminalScreenUiState&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">idle&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">connecting&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">connected&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">disconnected&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機測試前、這五個狀態各自的 UI 長這樣：&lt;/p>
&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>idle&lt;/td>
 &lt;td>空白（自動開始連線）&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connecting&lt;/td>
 &lt;td>「連線中&amp;hellip;」進度指示&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connected&lt;/td>
 &lt;td>終端機畫面 + 工具列&lt;/td>
 &lt;td>打字、Esc/Tab/Ctrl/方向鍵&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>錯誤訊息 + 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>disconnected&lt;/td>
 &lt;td>「連線中斷」+ 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>&lt;strong>無&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>五個狀態、零個退出路徑。使用者一旦進入 Terminal 畫面就出不去。&lt;/p>
&lt;hr>
&lt;h2 id="問題不在按鈕在設計方法">問題不在按鈕、在設計方法&lt;/h2>
&lt;p>加 back 按鈕是 5 分鐘的事。真正的問題是：&lt;strong>企劃階段沒有工具強制你為每個狀態想退出路徑。&lt;/strong>&lt;/p>
&lt;p>操作盤點表長這樣：&lt;/p>
&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>日常連線&lt;/td>
 &lt;td>Face ID → 讀憑證 → WS 連線 → 雙向 I/O&lt;/td>
 &lt;td>辨識失敗；Tailscale 離線；ttyd 認證失敗&lt;/td>
 &lt;td>辨識失敗不讀憑證；連線失敗顯示「無法連線」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「前端引導」只有一句話。它沒有被展開成畫面狀態。「連線失敗顯示無法連線」這句話覆蓋了 &lt;code>error&lt;/code> 狀態的&lt;strong>顯示&lt;/strong>，但沒有回答&lt;strong>操作&lt;/strong>（重連？返回？）和&lt;strong>退出&lt;/strong>（怎麼離開這個畫面？）。&lt;/p>
&lt;hr>
&lt;h2 id="畫面狀態矩陣">畫面狀態矩陣&lt;/h2>
&lt;p>把狀態機設計變成一張表，強制回答每個狀態的四個面向：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>畫面.狀態&lt;/th>
 &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>Terminal.idle&lt;/td>
 &lt;td>空白&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>從首頁導航進入&lt;/td>
 &lt;td>back → 首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.connecting&lt;/td>
 &lt;td>進度指示&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>自動觸發連線&lt;/td>
 &lt;td>back → 首頁（取消連線）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.connected&lt;/td>
 &lt;td>終端機 + 工具列&lt;/td>
 &lt;td>打字、特殊鍵&lt;/td>
 &lt;td>WS 連線成功&lt;/td>
 &lt;td>disconnect → idle；back → 首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.error&lt;/td>
 &lt;td>錯誤訊息&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>連線失敗&lt;/td>
 &lt;td>back → 首頁；retry → connecting&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Terminal.disconnected&lt;/td>
 &lt;td>「連線中斷」&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>WS 斷線&lt;/td>
 &lt;td>back → 首頁；retry → connecting&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>表格的威力在「退出路徑」欄位：&lt;strong>如果這格是空的，這就是一個 UX 死胡同。&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="三個-mobile-app-ux-設計原則">三個 Mobile App UX 設計原則&lt;/h2>
&lt;p>從這個案例提煉出的三個原則，適用於所有 mobile app：&lt;/p>
&lt;h3 id="原則-1每個畫面的每個狀態都需要退出路徑">原則 1：每個畫面的每個狀態都需要退出路徑&lt;/h3>
&lt;p>沒有例外。即使是「connecting」這種過渡狀態，使用者也可能想取消。iOS 的 HIG 和 Material Design 都要求 modal 畫面提供 dismiss 機制 — 如果使用者進不了某個狀態的下一步（連線失敗、timeout、服務無回應），他至少得能退出。&lt;/p>
&lt;p>&lt;strong>反模式&lt;/strong>：假設使用者只走 happy path。「connected 之後使用者不會想回首頁」是開發者的假設，不是使用者的需求。&lt;/p>
&lt;h3 id="原則-2gate-必須有-fallback">原則 2：Gate 必須有 fallback&lt;/h3>
&lt;p>Gate = 使用者必須通過的關卡（biometric、network、auth）。每個 gate 的設計不只是「成功時怎麼做」，還包含「失敗時的替代路徑」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Gate&lt;/th>
 &lt;th>成功&lt;/th>
 &lt;th>失敗 fallback&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Biometric（Face ID / 指紋）&lt;/td>
 &lt;td>讀取憑證、繼續連線&lt;/td>
 &lt;td>密碼 fallback（&lt;code>biometricOnly: false&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network（Tailscale VPN）&lt;/td>
 &lt;td>WS 連線&lt;/td>
 &lt;td>顯示「網路不可用」+ 重試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auth（ttyd basic auth）&lt;/td>
 &lt;td>進入終端機&lt;/td>
 &lt;td>顯示「認證失敗」+ 建議重新配對&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>biometricOnly: true&lt;/code> 就是缺少 fallback 的典型案例 — Face ID 不可用（戴口罩、光線差、指紋模糊）時使用者直接被擋住，沒有替代方案。改為 &lt;code>biometricOnly: false&lt;/code> 讓系統提供密碼 fallback。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要解決什麼">這篇要解決什麼</h2>
<blockquote>
<p>使用者連上遠端終端機後、無法返回首頁。</p></blockquote>
<p>這是設計遺漏。Terminal 畫面的 <code>connected</code> 狀態沒有 disconnect 按鈕也沒有 back 按鈕。<code>error</code> 和 <code>disconnected</code> 狀態也沒有。使用者被困在畫面裡，唯一的出路是殺掉 app。</p>
<p>這不是「忘記加按鈕」的問題。回頭看企劃文件，操作盤點段確實列了「連線失敗顯示無法連線」這個失敗情境，但沒有系統性地問：<strong>這個畫面有幾個狀態？每個狀態能做什麼操作？怎麼離開？</strong></p>
<p>本文整理畫面狀態機設計的方法、示範用狀態矩陣捕捉導航缺口、歸納 mobile app UX 的三個設計原則。</p>
<hr>
<h2 id="實際案例terminal-畫面的五個狀態">實際案例：Terminal 畫面的五個狀態</h2>
<p>Terminal 畫面有一個 <code>TerminalScreenUiState</code> enum 定義了五個狀態：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">enum</span> <span class="n">TerminalScreenUiState</span> <span class="p">{</span> <span class="n">idle</span><span class="p">,</span> <span class="n">connecting</span><span class="p">,</span> <span class="n">connected</span><span class="p">,</span> <span class="n">error</span><span class="p">,</span> <span class="n">disconnected</span> <span class="p">}</span></span></span></code></pre></div><p>實機測試前、這五個狀態各自的 UI 長這樣：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>空白（自動開始連線）</td>
          <td>無</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>「連線中&hellip;」進度指示</td>
          <td>無</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>connected</td>
          <td>終端機畫面 + 工具列</td>
          <td>打字、Esc/Tab/Ctrl/方向鍵</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連按鈕</td>
          <td>重新連線</td>
          <td><strong>無</strong></td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>「連線中斷」+ 重連按鈕</td>
          <td>重新連線</td>
          <td><strong>無</strong></td>
      </tr>
  </tbody>
</table>
<p>五個狀態、零個退出路徑。使用者一旦進入 Terminal 畫面就出不去。</p>
<hr>
<h2 id="問題不在按鈕在設計方法">問題不在按鈕、在設計方法</h2>
<p>加 back 按鈕是 5 分鐘的事。真正的問題是：<strong>企劃階段沒有工具強制你為每個狀態想退出路徑。</strong></p>
<p>操作盤點表長這樣：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>主情境</th>
          <th>失敗情境</th>
          <th>前端引導</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日常連線</td>
          <td>Face ID → 讀憑證 → WS 連線 → 雙向 I/O</td>
          <td>辨識失敗；Tailscale 離線；ttyd 認證失敗</td>
          <td>辨識失敗不讀憑證；連線失敗顯示「無法連線」</td>
      </tr>
  </tbody>
</table>
<p>「前端引導」只有一句話。它沒有被展開成畫面狀態。「連線失敗顯示無法連線」這句話覆蓋了 <code>error</code> 狀態的<strong>顯示</strong>，但沒有回答<strong>操作</strong>（重連？返回？）和<strong>退出</strong>（怎麼離開這個畫面？）。</p>
<hr>
<h2 id="畫面狀態矩陣">畫面狀態矩陣</h2>
<p>把狀態機設計變成一張表，強制回答每個狀態的四個面向：</p>
<table>
  <thead>
      <tr>
          <th>畫面.狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>進入條件</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Terminal.idle</td>
          <td>空白</td>
          <td>—</td>
          <td>從首頁導航進入</td>
          <td>back → 首頁</td>
      </tr>
      <tr>
          <td>Terminal.connecting</td>
          <td>進度指示</td>
          <td>—</td>
          <td>自動觸發連線</td>
          <td>back → 首頁（取消連線）</td>
      </tr>
      <tr>
          <td>Terminal.connected</td>
          <td>終端機 + 工具列</td>
          <td>打字、特殊鍵</td>
          <td>WS 連線成功</td>
          <td>disconnect → idle；back → 首頁</td>
      </tr>
      <tr>
          <td>Terminal.error</td>
          <td>錯誤訊息</td>
          <td>重新連線</td>
          <td>連線失敗</td>
          <td>back → 首頁；retry → connecting</td>
      </tr>
      <tr>
          <td>Terminal.disconnected</td>
          <td>「連線中斷」</td>
          <td>重新連線</td>
          <td>WS 斷線</td>
          <td>back → 首頁；retry → connecting</td>
      </tr>
  </tbody>
</table>
<p>表格的威力在「退出路徑」欄位：<strong>如果這格是空的，這就是一個 UX 死胡同。</strong></p>
<hr>
<h2 id="三個-mobile-app-ux-設計原則">三個 Mobile App UX 設計原則</h2>
<p>從這個案例提煉出的三個原則，適用於所有 mobile app：</p>
<h3 id="原則-1每個畫面的每個狀態都需要退出路徑">原則 1：每個畫面的每個狀態都需要退出路徑</h3>
<p>沒有例外。即使是「connecting」這種過渡狀態，使用者也可能想取消。iOS 的 HIG 和 Material Design 都要求 modal 畫面提供 dismiss 機制 — 如果使用者進不了某個狀態的下一步（連線失敗、timeout、服務無回應），他至少得能退出。</p>
<p><strong>反模式</strong>：假設使用者只走 happy path。「connected 之後使用者不會想回首頁」是開發者的假設，不是使用者的需求。</p>
<h3 id="原則-2gate-必須有-fallback">原則 2：Gate 必須有 fallback</h3>
<p>Gate = 使用者必須通過的關卡（biometric、network、auth）。每個 gate 的設計不只是「成功時怎麼做」，還包含「失敗時的替代路徑」。</p>
<table>
  <thead>
      <tr>
          <th>Gate</th>
          <th>成功</th>
          <th>失敗 fallback</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Biometric（Face ID / 指紋）</td>
          <td>讀取憑證、繼續連線</td>
          <td>密碼 fallback（<code>biometricOnly: false</code>）</td>
      </tr>
      <tr>
          <td>Network（Tailscale VPN）</td>
          <td>WS 連線</td>
          <td>顯示「網路不可用」+ 重試</td>
      </tr>
      <tr>
          <td>Auth（ttyd basic auth）</td>
          <td>進入終端機</td>
          <td>顯示「認證失敗」+ 建議重新配對</td>
      </tr>
  </tbody>
</table>
<p><code>biometricOnly: true</code> 就是缺少 fallback 的典型案例 — Face ID 不可用（戴口罩、光線差、指紋模糊）時使用者直接被擋住，沒有替代方案。改為 <code>biometricOnly: false</code> 讓系統提供密碼 fallback。</p>
<h3 id="原則-3輸入機制是設計產物不是實作細節">原則 3：輸入機制是設計產物，不是實作細節</h3>
<p>「手機打字操作 CLI」的輸入設計決策比想像的多：</p>
<table>
  <thead>
      <tr>
          <th>設計決策</th>
          <th>選項</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyboard type</td>
          <td><code>visiblePassword</code>（無自動校正）vs <code>text</code>（有校正）</td>
          <td>CLI 命令不需要自動校正，<code>visiblePassword</code> 避免系統「幫忙」修改輸入</td>
      </tr>
      <tr>
          <td>Submit model</td>
          <td>Enter 送出整行 vs 逐字元即時送出</td>
          <td>整行送出減少網路來回，但沒有即時 tab 補全回饋</td>
      </tr>
      <tr>
          <td>IME policy</td>
          <td>關閉建議、關閉自動校正、關閉個人化學習</td>
          <td>CLI 輸入內容可能包含密碼和路徑，IME 學習是安全風險</td>
      </tr>
      <tr>
          <td>Special keys</td>
          <td>Esc / Tab / Ctrl 組合鍵</td>
          <td>手機鍵盤沒有這些鍵，需要自訂工具列</td>
      </tr>
  </tbody>
</table>
<p>這些決策在企劃階段就應該做，因為它們影響 UI layout（是否需要輸入框？工具列放什麼鍵？）和 protocol 設計（逐字元還是整行？）。事後補的 <code>TextField</code> 參數列表（<code>enableSuggestions: false, autocorrect: false, enableIMEPersonalizedLearning: false</code>）全是散落的 hotfix，不是設計產物。</p>
<hr>
<h2 id="系統性方法從操作盤點到畫面狀態矩陣">系統性方法：從操作盤點到畫面狀態矩陣</h2>
<p>操作盤點是 BDD 的起點（使用者做什麼、成功時發生什麼、失敗時發生什麼）。但盤點到「前端引導」就停了 — 它回答了「顯示什麼」但沒回答「能做什麼」「怎麼離開」。</p>
<p>補上的步驟：</p>
<ol>
<li><strong>從操作盤點列出所有畫面</strong>：每個操作涉及哪些畫面？（首頁 → 配對畫面 → QR 掃描 → 終端機畫面）</li>
<li><strong>每個畫面列出所有狀態</strong>：這個畫面有哪些 enum 值或邏輯分支？</li>
<li><strong>填畫面狀態矩陣</strong>：顯示 / 可用操作 / 進入條件 / 退出路徑。退出路徑欄位為空 = UX 死胡同</li>
<li><strong>每個 gate 標注 fallback</strong>：biometric / network / auth 各有什麼替代方案？</li>
<li><strong>輸入機制列決策表</strong>：keyboard type / submit model / IME policy / special keys</li>
</ol>
<p>這是操作盤點本來就該產出的下一層。一張表能在 10 分鐘內暴露所有 UX 死胡同，省掉實機測試才發現的成本。</p>
<h2 id="延伸閱讀">延伸閱讀</h2>
<p>本文的觀察和判讀在 <a href="/blog/ux-design/" data-link-title="UX 設計實務指南" data-link-desc="整理畫面狀態機、導航設計、Gate fallback、輸入機制與使用者行為驗證 — 從「使用者被困在畫面裡出不去」的結構性遺漏出發，建立系統性的 UX 設計方法">UX Design 畫面設計</a> 教學系列中展開為系統性的教學模組：<a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a>、<a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a>、<a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a>。</p>
]]></content:encoded></item><item><title>SQLite Mobile / Desktop Embedded Store</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 mobile、desktop、CLI 與 embedded device；本文聚焦 &lt;em>device-local formal state 的資料責任、backup、privacy 與 sync boundary&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite embedded store 的核心責任是讓 application process 在本機持有正式狀態。Mobile app、desktop app、browser profile、CLI tool 與 embedded device 常用 SQLite 保存 local data；這些資料可能只是 cache，也可能是使用者唯一資料來源。教學上要先判斷它是否承擔 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，再決定 backup、sync、privacy 與 migration 責任。&lt;/p>
&lt;p>本文的判讀錨點是：embedded SQLite 的 production boundary 在 device lifecycle，database server 層的邊界在這裡不適用。OS backup、app upgrade、device loss、profile corruption、local PII、multi-device sync 與 user export / delete 都是資料庫責任的一部分。&lt;/p>
&lt;h2 id="embedded-state-model">Embedded state model&lt;/h2>
&lt;p>Embedded state model 的核心責任是把 local database file 放回 application lifecycle。SQLite 是典型的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/embedded-database/" data-link-title="Embedded Database" data-link-desc="說明嵌入式資料庫如何隨 application process 運作，並把檔案生命週期責任交回應用">embedded database&lt;/a>：database file 通常跟著 app sandbox、user profile、CLI config directory 或 device storage 存在，它的 owner 是 application，而非獨立 DBA。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>SQLite 資料角色&lt;/th>
 &lt;th>主要風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mobile app&lt;/td>
 &lt;td>offline state、draft、cache、local profile&lt;/td>
 &lt;td>app upgrade、device loss、cloud backup leakage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Desktop app&lt;/td>
 &lt;td>user profile、history、settings&lt;/td>
 &lt;td>profile corruption、manual file copy、multi-version app&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CLI tool&lt;/td>
 &lt;td>local index、metadata、state cache&lt;/td>
 &lt;td>command interruption、portable file path&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser / profile&lt;/td>
 &lt;td>cookies、history、bookmark 類資料&lt;/td>
 &lt;td>privacy、profile migration、lock collision&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedded device&lt;/td>
 &lt;td>offline event、sensor / config state&lt;/td>
 &lt;td>power loss、flash wear、delayed sync&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是資料角色而非產品名稱。同樣是 SQLite file，cache 可以清掉重建；draft、local-only note、sensor event 或 user history 可能需要正式 backup / export / delete。&lt;/p>
&lt;h2 id="backup-與-export">Backup 與 export&lt;/h2>
&lt;p>Embedded backup 的核心責任是讓使用者或服務能從 device / profile failure 復原。Mobile / desktop / CLI 的 backup 路徑常和 OS backup、app export、cloud sync 或手動複製混在一起；SQLite file lifecycle 要明確。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 mobile、desktop、CLI 與 embedded device；本文聚焦 <em>device-local formal state 的資料責任、backup、privacy 與 sync boundary</em>。</p></blockquote>
<p>SQLite embedded store 的核心責任是讓 application process 在本機持有正式狀態。Mobile app、desktop app、browser profile、CLI tool 與 embedded device 常用 SQLite 保存 local data；這些資料可能只是 cache，也可能是使用者唯一資料來源。教學上要先判斷它是否承擔 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，再決定 backup、sync、privacy 與 migration 責任。</p>
<p>本文的判讀錨點是：embedded SQLite 的 production boundary 在 device lifecycle，database server 層的邊界在這裡不適用。OS backup、app upgrade、device loss、profile corruption、local PII、multi-device sync 與 user export / delete 都是資料庫責任的一部分。</p>
<h2 id="embedded-state-model">Embedded state model</h2>
<p>Embedded state model 的核心責任是把 local database file 放回 application lifecycle。SQLite 是典型的 <a href="/blog/backend/knowledge-cards/embedded-database/" data-link-title="Embedded Database" data-link-desc="說明嵌入式資料庫如何隨 application process 運作，並把檔案生命週期責任交回應用">embedded database</a>：database file 通常跟著 app sandbox、user profile、CLI config directory 或 device storage 存在，它的 owner 是 application，而非獨立 DBA。</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>SQLite 資料角色</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mobile app</td>
          <td>offline state、draft、cache、local profile</td>
          <td>app upgrade、device loss、cloud backup leakage</td>
      </tr>
      <tr>
          <td>Desktop app</td>
          <td>user profile、history、settings</td>
          <td>profile corruption、manual file copy、multi-version app</td>
      </tr>
      <tr>
          <td>CLI tool</td>
          <td>local index、metadata、state cache</td>
          <td>command interruption、portable file path</td>
      </tr>
      <tr>
          <td>Browser / profile</td>
          <td>cookies、history、bookmark 類資料</td>
          <td>privacy、profile migration、lock collision</td>
      </tr>
      <tr>
          <td>Embedded device</td>
          <td>offline event、sensor / config state</td>
          <td>power loss、flash wear、delayed sync</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是資料角色而非產品名稱。同樣是 SQLite file，cache 可以清掉重建；draft、local-only note、sensor event 或 user history 可能需要正式 backup / export / delete。</p>
<h2 id="backup-與-export">Backup 與 export</h2>
<p>Embedded backup 的核心責任是讓使用者或服務能從 device / profile failure 復原。Mobile / desktop / CLI 的 backup 路徑常和 OS backup、app export、cloud sync 或手動複製混在一起；SQLite file lifecycle 要明確。</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>適合資料</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OS / device backup</td>
          <td>user-owned local state</td>
          <td>local PII、encryption、restore compatibility</td>
      </tr>
      <tr>
          <td>App export</td>
          <td>使用者可攜資料</td>
          <td>schema version、format stability、privacy</td>
      </tr>
      <tr>
          <td><code>.backup</code> / snapshot</td>
          <td>application-managed backup</td>
          <td>live DB consistency、WAL sidecar handling</td>
      </tr>
      <tr>
          <td>Cloud sync</td>
          <td>multi-device state</td>
          <td>conflict、server authority、delete propagation</td>
      </tr>
  </tbody>
</table>
<p>Backup 設計要先決定 restore target。Restore 到同 app version、未來 app version、或不同 device，會帶來不同 schema compatibility 與 privacy requirement。</p>
<h2 id="privacy-與-local-pii">Privacy 與 local PII</h2>
<p>Embedded SQLite 的 privacy 責任是治理 device-local data。資料在 server DB 中通常有 access log、IAM、DLP 與 retention policy；進入 SQLite file 後，風險轉到 device encryption、app sandbox、backup retention、debug export 與 support bundle。</p>
<table>
  <thead>
      <tr>
          <th>風險</th>
          <th>真實情境</th>
          <th>控制方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local PII</td>
          <td>profile、token、message、draft</td>
          <td>最小化欄位、加密敏感值、限制 export</td>
      </tr>
      <tr>
          <td>Backup leakage</td>
          <td>OS cloud backup 含 database file</td>
          <td>設定 backup exclusion 或加密</td>
      </tr>
      <tr>
          <td>Support bundle</td>
          <td>使用者回報問題附上 DB</td>
          <td>scrub / redaction、只匯出必要 table</td>
      </tr>
      <tr>
          <td>Delete request</td>
          <td>server 刪除但 device local 留存</td>
          <td>sync delete、local purge、retention evidence</td>
      </tr>
  </tbody>
</table>
<p>SQLite file 要進入資料保護盤點。若 local DB 保存敏感資料，應連到 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a> 與 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 的相同問題，只是控制面改在 device / app。</p>
<h2 id="app-upgrade-與-schema-compatibility">App upgrade 與 schema compatibility</h2>
<p>App upgrade 的核心責任是保證新版 binary 能安全打開舊 database file。Mobile / desktop app 的使用者不會按照 backend deployment order 升級；同一時間可能存在多個 app version 與多個 DB schema version。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>設計策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 app 打舊 DB</td>
          <td>startup migration、<code>user_version</code>、backup before migration</td>
      </tr>
      <tr>
          <td>舊 app 打新 DB</td>
          <td>backward-compatible column、feature gate、minimum supported version</td>
      </tr>
      <tr>
          <td>使用者降版</td>
          <td>export / import、read-only fallback、no-downgrade notice</td>
      </tr>
      <tr>
          <td>多裝置不同版本</td>
          <td>sync protocol version、server-side compatibility</td>
      </tr>
  </tbody>
</table>
<p>這些策略要和 <a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a> 對齊。Embedded app 的 migration failure 通常直接影響使用者啟動體驗，因此 migration 要能快速、可恢復、可診斷。</p>
<h2 id="sync-boundary">Sync boundary</h2>
<p>Sync boundary 的核心責任是把 single-device SQLite 和 multi-device state 分開。SQLite 保存本地狀態；跨裝置同步需要 transport、identity、conflict resolution、delete propagation 與 server authority。</p>
<table>
  <thead>
      <tr>
          <th>Sync 需求</th>
          <th>SQLite 角色</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單裝置 offline</td>
          <td>local source of truth</td>
          <td>SQLite + backup / export</td>
      </tr>
      <tr>
          <td>多裝置同步</td>
          <td>local replica / cache</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary</a></td>
      </tr>
      <tr>
          <td>即時多人協作</td>
          <td>local working copy</td>
          <td>server authority、CRDT、event log</td>
      </tr>
      <tr>
          <td>Server reporting</td>
          <td>local data upload / ETL</td>
          <td>API sync、queue、analytics store</td>
      </tr>
  </tbody>
</table>
<p>當 sync 需求出現時，SQLite 仍可作為 local store，但不再單獨承擔完整資料一致性。完整性要由 sync protocol 與 server-side validation 補上。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1把-cache-當正式資料">Case 1：把 cache 當正式資料</h3>
<p>Cache 被誤當正式資料的核心風險是清除 local DB 會造成不可恢復資料損失。許多 app 初期把 SQLite 當 cache；後來加入 draft、offline action 或 local-only setting，資料責任就改變了。</p>
<p>修正方向是逐 table 標示資料角色。Cache table 可清；formal state table 要 backup、migration、export 與 delete policy。</p>
<h3 id="case-2os-backup-帶走敏感資料">Case 2：OS backup 帶走敏感資料</h3>
<p>OS backup 的核心風險是 device-local PII 進入使用者或平台雲端備份。Server 端已刪除的資料，可能仍存在 device backup。</p>
<p>修正方向是決定哪些資料可被備份。Token、secret、敏感 PII 可排除或加密；user-owned content 則要提供 export / restore 語意。</p>
<h3 id="case-3app-upgrade-migration-失敗讓使用者卡在啟動頁">Case 3：App upgrade migration 失敗讓使用者卡在啟動頁</h3>
<p>Startup migration 失敗的核心風險是使用者卡在 app 啟動前，且修復能力有限。SQLite file 在使用者裝置上，SRE 通常需要透過 app update、support bundle 或 restore flow 處理。</p>
<p>修正方向是保留 pre-migration snapshot、提供 safe mode、收集匿名 schema / error evidence，並避免長 migration 放在 cold start。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>Embedded SQLite 設計要回答：</p>
<ol>
<li>每張 table 是 cache、formal state、derived state 還是 sync queue。</li>
<li>Database file 在 app / OS 的哪個 storage boundary。</li>
<li>OS backup 是否包含 database file。</li>
<li>敏感欄位是否加密、排除或可清除。</li>
<li>App upgrade migration 是否有 pre-migration backup。</li>
<li>使用者 export / delete / support bundle 如何處理 SQLite data。</li>
<li>Multi-device sync 是否有 conflict 與 server authority 設計。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first Sync Boundary</a>、<a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></li>
<li>官方：<a href="https://www.sqlite.org/whentouse.html">SQLite Appropriate Uses</a>、<a href="https://www.sqlite.org/backup.html">SQLite Backup API</a></li>
</ul>
]]></content:encoded></item></channel></rss>