Skip to content

[UX-B] 行動版收尾與可及性:兩個 P0 + 21 項 (#44) - #52

Merged
poterpan merged 23 commits into
mainfrom
ux/44-mobile-a11y
Aug 30, 2026
Merged

poterpan merged 23 commits into
mainfrom
ux/44-mobile-a11y

Conversation

@poterpan

Copy link
Copy Markdown
Owner

Closes #44

UX 健檢批次 B(行動版收尾與可及性),純前端。owner 決策:--line 紙感保留,只加深 input/textarea/select 的邊框至 3:1。

兩個 P0

編號 問題 修法
P0-9 Manage 四表在 375px 下,列動作 100% 在畫面外 .tbl-cards 卡片化(補 data-label)
P0-10 721–932px 死區——退回桌面版佈局,8 欄表的動作鈕跑到畫面外(正好是 iPad 直向 768/820 與橫向手機 844/932) 斷點 720px → 1000px;>1000px 則以 sticky 首欄/動作欄收尾

選 1000 而非 980:無 viewport meta 的 in-app WebView 首次載入會退回 980px 版面視窗,必須落在行動版這側。

其餘 21 項

  • B1 成員頁繳費渠道下拉原本是 152×19px、13.3px 的瀏覽器預設——App.tsx 用的 .field 是 admin 專屬 class,web 從來沒定義過。這是成員唯一會打開的那一頁上最顯眼的缺陷。
  • B2 表單控件 14px → 16px,解除 iOS Safari 聚焦後永久放大
  • B3 行動版分頁條 sticky + 自動捲到目前分頁 + 邊緣漸層(原本藏掉 41% 且隨頁捲走)
  • B4/B5/B16 Modal 補 role="dialog"、aria-modal、焦點進入/trap/還原、Escape 關閉、背景捲動鎖
  • B6/B7/B18 對比一次修齊,16 組色值全部照計劃的計算表(--muted 6.00:1、--line-strong 3.43:1 等),沒有自創顏色
  • B8/B20 可及名稱與文件語意:8 張表補 <caption> + scope="col"、Settings 三處輸入補 htmlFor/id、.topbar → <header>、成員頁補 <h1>/<h2> 與備註欄 aria-label
  • B9 繳費工具列從 183px 降下來——四顆期別工具收進 .toolbar__acts,手機上與狀態膠囊都變單行橫捲
  • B10 推播狀態卡片化(三顆鈕原本從畫面外開始);各方案首欄 sticky
  • B11 繳費列可用鍵盤開啟明細(tabIndex + Enter/Space + aria-label)
  • B12 名單搜尋:>12 個選項才出現搜尋框(借用 Discord 端 >25 人降級的既有思路)
  • B13 載入失敗給得出重試的路——useAsync 一直有 reload,但沒有任何錯誤 UI 用過它
  • B14 訂閱表單改日期選擇器,結帳日補 1–28 界限與「短月會落空」說明
  • B15/B19 寬表格 sticky 首欄/動作欄 + 橫向捲動邊緣陰影
  • B17 四處小於 24px 的觸控目標補齊
  • B21 清掉死 CSS:.btn--sm(用了 6 次、從未定義)與 web 的 .plans(Discord-first 改版後 markup 早已不存在)

刻意不做

role="button" 加在 <tr> 上(會破壞螢幕閱讀器賴以理解表格的列/儲存格關係);定義 .btn--sm(那會縮小七顆按鈕,反噬 B17 的觸控目標工作);/payments 的 LIMIT 與排序(那是 worker/API 改動,屬批次 E)。

沒有動到的

按鈕文案、確認流程、btn--danger 樣式——那些是批次 A(#43)的兩段式危險動作設計。本批次只做版面與可及性。

驗收

pnpm -r typecheck 綠(三個 package);admin + web build 綠;worker 測試 382 passed,與 main 基準一致(本批純前端,零 worker 改動)。

⚠️ 驗收限制:計劃檔設計的 CDP 瀏覽器探針($SCRATCH/ux44/*)在此環境不存在,改以 typecheck + build + 原始碼與 minify 後 CSS 的結構性 grep 驗收。sticky 分隔線、捲動陰影、Escape、背景捲動鎖、焦點環等視覺/互動行為未經真機驗證,建議合併前在 dev server 或真機上看一眼。

README 徽章未動:main 目前寫 355、實際 382(PR #50 漏改),PR #51 已修正為 423——留給它避免衝突。

審查註記

Codex 跨引擎審查因額度用盡尚未執行,owner 指定 23:15 後補送。

P0-9:成員 18/18、訂閱 18/18、方案 6/6、渠道 9/9 顆按鈕的 left >= innerWidth
→ 全部 0/N。動作格改為 2-3 顆等寬 42px 按鈕(沿用既有 .tbl-cards 規則)。
P0-10:721–932px 之間會退回桌面版佈局(232px 側欄 + 489–700px 內容欄),
導致 8 欄表格的列動作在 iPad 直向(768/820)與橫向手機(844/932)跑到畫面外。

四個 media query 區塊一起改(不同步會造成佈局錯位):
- max-width: 1000px — shell(.app)、settings/savebar、.tbl-cards + bottom sheet
- min-width: 1001px — .mrow 兩欄

選 1000 而非 980:無 viewport meta 的 in-app WebView 首次載入會退回
980px 版面視窗,必須落在行動版這側。
PaymentDetail 過去只能用滑鼠點列的背景才打得開——鍵盤使用者雖然有
成員 → MemberReview → 完整資訊 這條繞路,但沒有人會找到它。

列本身變成 tabIndex={0} + Enter/Space 開啟,並補上描述性的 aria-label。
刻意不加 role="button":那會破壞螢幕閱讀器賴以理解表格的列/儲存格關係。
列內按鈕保有自己的 tab stop 與 stopPropagation,事件不會重複觸發
(e.target !== e.currentTarget 時直接 return)。
起算日原本是純文字框,用 placeholder 教格式——App 其他地方的日期一律是
用選的(Settings 與繳費彈窗都用 type="month"),這裡沒有理由例外。

結帳日補上 min/max(1–28)與說明:29–31 在短月會落空,所以上限是 28。
自訂週期補上後果說明:勾選後依自己的結帳日出帳,排程只會在該日產生帳單——
原本只有一個沒有解釋的勾選框。
1024/1280 桌機寬度下,8 欄的繳費表(1058px)寬過內容欄,成員(這列是誰)
與動作欄(能對它做什麼)會被推到畫面外——使用者得先橫捲才知道自己在看哪一列、
也才按得到驗證。改成把首欄 sticky 在左、動作欄 sticky 在右,橫捲就只影響
中間的資訊欄,兩個真正重要的東西一直在。以 class opt-in(.tbl--pin-first /
.tbl--pin-last),3 欄的小表不會平白被釘住;≤1000px 這些表是卡片,規則不適用。

table 由 border-collapse: collapse 改 separate + border-spacing 0:collapse 下
邊框屬於 table 而非 cell,sticky cell 在 WebKit 會畫不出分隔線。這裡只設過
border-bottom,視覺完全相同。

.content max-width 1100 → 1180(B19):1440px 螢幕不必再橫捲。
.tbl 加四層背景漸層(B15):只有真的溢出時才出現邊緣陰影,捲到底自動消失,
純 CSS 無需 JS。
useAsync 一直有回傳 reload,但沒有任何錯誤 UI 用過它——載入失敗只留下
一條紅線,使用者沒有任何前進的方式。

新增 ErrorNote:載入失敗處(Dashboard/Payments/Manage 四表/Settings)
一律附「重試」按鈕。401/403 例外——api.ts 會轉成「未授權,請重新登入」,
而 Cloudflare Access 只在整頁重載時重發 session,所以那個情況給的是
「重新登入」(走 location.reload),不是無效的重試。

彈窗內回報「動作失敗」的 err 區塊維持原樣:那些不是載入失敗,
重試的按鈕就是使用者本來按的那顆。
行動版把側欄壓成一條橫向捲動的分頁條後,留下兩個問題:

一、375px 下約四成分頁在畫面外,而分頁條不會自己捲——進到 方案/支付渠道/設定
這三頁時,被標示為 on 的那顆按鈕在右側看不見的地方,使用者看不出自己在哪一頁。
改成每次 view 變更就把 active tab 捲到可視範圍中央。用 scrollLeft 而非
scrollIntoView:後者會連帶把整份文件往下捲。

二、分頁條原本 position: static,設定頁 2500px 高,想換頁就得先捲回最上面;
真正被釘住的反而是只顯示頁名的 .topbar——導覽捲走、標題留下,剛好是反的。
改成分頁條 sticky top 0(z-index 6 蓋過 topbar),.topbar 在行動版改 static,
避免兩條 sticky 搶同一個上緣互相重疊。

分頁條另加四層背景漸層(與 .tbl 同一招,配深色底重新調色):原本沒有任何線索
提示右邊還有分頁。
App.tsx 一直用 .field / .field__label —— 但那是 admin 專屬的 class,
web/styles.css 從來沒有定義過,也沒有任何規則套用到裸 select。結果成員
唯一會打開的那一頁上,渠道選單是 152x19px、13.3px 的瀏覽器預設,夾在
完整設計過的拖放區與送出按鈕之間。

只加 CSS,markup 不動。尺寸對齊 .note 與 .submit:16px(iOS 不會自動放大)、
最小高度 48px(拇指可點)、--line-strong 邊框滿足 WCAG 1.4.11 非文字對比。
保留原生 appearance,iOS 仍畫自己的 chevron。

順帶把 --line-strong 加進 web 的 :root(值與 admin 端一致)。
推播狀態的三顆鈕(重發開繳通知/催繳未繳成員/重置催繳發送紀錄)在 375px
下從畫面外開始——最右邊那顆完全看不到、也點不到。

改為 .tbl-cards(≤1000px 堆疊成卡片,每格由 data-label 標示)+ .tbl--pin-last
(>1000px 時動作欄 sticky)。純版面調整:按鈕文案、確認流程與顏色屬批次 A 的
兩段式危險動作設計,一律未動。

各方案表的首欄 sticky 已由 Task 4 完成(.tbl--pin-first),右滑讀應收時
不再失去方案名。
B4、B5、B16 都落在同一個十幾行的元件上,所以一起改:

語意(B4):原本只是兩層 div,輔助技術完全不知道畫面上跳出了東西。補
role="dialog" + aria-modal="true" + aria-labelledby 指向標題,讀螢幕時
才會唸出「這是對話框、標題是什麼」,也才會把背景內容視為不可及。

焦點(B4):開啟後焦點仍留在背後的表格上,連按十幾次 Tab 都走不進彈窗。
改成開啟時把焦點移進 sheet、Tab/Shift+Tab 在內部循環、關閉時還原到原本
觸發的那顆按鈕。FOCUSABLE 特意排除 [disabled]:本 App 每個彈窗送出時都會
把控制項 disable,若把 disabled 的按鈕算進循環會憑空吃掉一次 Tab。

Escape(B5):原本只能點 ✕ 或背景,鍵盤使用者沒有退路。

背景捲動鎖(B16):375px 下 bottom sheet 上方仍留約 357px 可觸背景,拖它
會捲動底下的頁面,看起來像彈窗自己在飄。鎖在 <html> 上可同時涵蓋兩個捲動
容器(行動版是 body,桌機是 .main)。用 refcount 而非開關,因為彈窗會疊
(PaymentDetail 從 MemberReview 開),內層關閉不該提早解鎖外層。

公開 props 未變(title / onClose / children),十四個呼叫端一行都不用改。
B13 的兩個呼叫點:前一個 commit 只帶到 ui.tsx / Manage.tsx / Settings.tsx,
Dashboard 與 Payments 的置換因為與並行工作的暫存交錯而漏掉。
375px 下工具列高 183px——期別一行、狀態膠囊兩行、四顆按鈕各自一行——
首屏只剩 2 列繳費紀錄。

四顆一次性期別工具收進 .toolbar__acts;≤1000px 時它與 .pills 都變成
單行橫向捲動(沿用 .tbl 的邊緣漸層),>1000px 維持原本的平鋪 flex 列、外觀不變。
按鈕的文案、順序、title、btn--danger 樣式全部未動——那是批次 A 的地盤。
Discord-first 改版之後,成員頁不再逐一勾選方案(一次送出就結清當期
所有訂閱),這 11 行樣式對應的 markup 早就不存在。

已確認 App.tsx 沒有任何 .plan/.plans 的使用點才刪。
Settings.tsx 的 btn--sm(B21 另一半)留待 Task 9 之後處理,避免與並行工作衝突。
本 commit 刻意為空。Task 7(對比與 focus 一次修齊)的 CSS 變更在我編輯期間,被同一個
worktree 內並行作業的其他 agent 以 `git add packages/.../styles.css` 一併帶進了
0549a06(admin 半邊)與 5b8c40f(web 半邊),因此已無獨立 diff 可 commit。

內容已核對無誤且完整保留:--muted 6.00/5.31、--amber 5.94、.tag 4.88、
badge--pending/chip--off 5.15、sidebar__foot 6.25、全域 :focus-visible 6.12
(側欄 10.59)、--line-strong 3.43 只套用在 input/select/textarea,
web --muted 5.75/5.32 與 .note 邊框/focus。色值與計劃表逐項相符。
留此標記以說明為何 B6/B7/B18 沒有自己的 diff;不改寫歷史以免影響並行作業。
iOS Safari 只要聚焦的控件字級低於 16px 就會把 viewport 放大,且失焦後不會還原,
所以在手機上點一次後台任何欄位,整個後台就永久停在放大+偏移的狀態。

admin:.field input/select/textarea 與 MemberReview 退回原因輸入框(涵蓋設定頁與全部 modal
的 number/text/mono/textarea/select/month)14px → 16px;.toolbar 的控件本來就靠 font: inherit
拿到 16px,這正是「不需要重新設計視覺也能改」的證據。web:.note 15px → 16px。

.grid2 的兩欄底部對齊(.grid2 .field input { margin-top: auto })不受影響:
grid 項目預設 stretch 等高,margin-top:auto 仍把控件釘在欄位底部。
B8:設定頁 Bark/Webhook/自訂通知文字三個控件原本只有 placeholder,螢幕閱讀器念不出欄位名稱
→ 改用 label + htmlFor(不能直接換成 Field 元件:那一列裡有「送出測試」按鈕,按鈕包在 label
裡會被指向 label 的點擊誤觸發)。另補 MemberReview 退回原因輸入框的 aria-label——計劃檔沒列,
但它是同一類缺陷。

B20:.topbar 改 <header>(後台 header landmark 0 → 1);八張表全部補 sr-only <caption>
與 th[scope="col"],動作欄的空 <th> 改放 sr-only 文字而不是留白;Manage 三張卡片標題
成員/訂閱/方案 → 成員名單/訂閱清單/方案清單,讓 topbar 的 H1 與卡片的 H2 不再字字相同
(純結構去重,不是概念改名;Batch D 的術語工作勿覆蓋)。

新增 .sr-only 工具類別(styles.css,接在 .mono 之後)。
借用 Discord >25 的降級思路(handler.ts:306-366):選項超過 12 個時在 select 前面加篩選框,
未超過就維持原生 select(FilterSelect,用於新增訂閱的成員/方案、手動補登的訂閱、
產生上傳連結的成員四處);成員/訂閱兩張表的卡片頭加搜尋與 N / M 計數(TableFilter),
並補上「沒有符合的…」空狀態。篩掉目前選中的項目時仍把它留在 option 裡,避免 select 空白
而下一次送出偷偷送舊值。無新增相依。

/payments 沒有 LIMIT 與表格排序屬 worker 變更,留在批次 E。
Task 9 的 Step 6-7:先前因檔案分工限制而略過,現在補上。

- .brand → <h1>、.stub__hi → <h2>:成員頁原本沒有任何標題元素,
  螢幕閱讀器的文件大綱是空的。.brand 補 margin: 0(原本是 div,
  沒有 UA 的標題邊距;.stub__hi 本來就有自己的 margin)
- 備註 textarea 只有 placeholder 沒有可及名稱,補 aria-label="備註"
  (這一區沒有可見標籤,用 aria-label 正確)
- 移除永遠走不到的錯誤分支:送出按鈕以同一個 canSubmit 條件 disabled,
  那句錯誤訊息不可能出現。保留 guard 本身當保險。
完整資訊 52×19 → ≥32px(.mrow__facts .linkbtn 補上繳費卡片早就有的處理);
details > summary 19px → 32px 的整條;checkbox glyph 13×13 → 18×18,外層 label 由恰好
24px 變成有餘裕;bottom sheet 主行動由 58×42 靠左 → 滿寬 42px,.btn-row 內則等分。

兩處與計劃檔的小調整:summary 改 flex 後 ::before 的尾隨空白會被 trim 掉,所以把
content 的兩個空白改成 gap: 6px 保住箭頭與文字的間距;checkbox 加 flex: 0 0 auto,
免得它在 display:flex 的 label 裡被長文字擠扁。
.btn--sm 在 Settings.tsx 用了六次卻從未定義(目前反而讓那六顆按鈕維持全尺寸)→
移除用法而非補定義:補定義會把設定頁六顆一次性動作鈕縮小,跟剛做完的 B17 觸控目標相衝。
兩處同時帶 btn--danger 的維持原樣。渲染結果完全不變,只是把陷阱從檔案裡拿掉。

web 半邊(.plans / .plan* 死 CSS)已於 5b8c40f 完成。
依 Codex 審查(PR #52):

- 註解寫「五個區塊」,實際是六個(Task 14 又加了 filter/card-head 區塊)。
  這份清單正是後人用來「一起改」的依據,過時就會漏改——直接修正並標註要維護它。
- 補充說明為什麼寫成 1000/1001 而不是 range:小數寬度(縮放顯示器可能出現
  1000 < w < 1001)兩側都不命中,會落回基礎規則——也就是可橫捲的版面,
  降級但不會壞掉。這是刻意接受的取捨,不是疏漏。
三處衝突都是「兩邊各自新增、要的是聯集」:

- Manage.tsx 成員頁:#44 的搜尋框/sticky 欄/caption/ErrorNote
  與 #45 的未綁定 pills 並存。兩個篩選器串接——未綁定先縮小集合,
  搜尋再縮一次;空狀態文案同時涵蓋兩種篩選。
- Manage.tsx 新增訂閱:#44 的 FilterSelect 與日期選擇器
  + #45 的通知勾選框。
- Settings.tsx / import:兩邊各加一個符號,取聯集。
- web/App.tsx:保留 #44 移除死分支的版本——#45 只是給那句訊息補句號,
  但該分支本來就走不到(送出鈕以同一條件 disabled)。

驗證:426 tests、三個 typecheck、admin + web build 全綠。
@poterpan
poterpan merged commit 943dcb0 into main Aug 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[UX-B] 行動版收尾與可及性

1 participant