- 症狀:
className={${inputClass} t-quote-15}想讓賭注①欄用明體,結果吃到黑體。 - 原因:兩個 class 都用
font:shorthand,勝負由樣式表裡的定義順序決定,不是 class 屬性的書寫順序。.t-ui-15定義在.t-quote-15後面,所以永遠贏。 - 解法:
Field.tsx拆出不含字體的inputBase,字體 class 一個元素只接一個。 - 教訓:語意排版 class 用 shorthand 很方便,但也讓「疊加」失效。要嘛全用 shorthand 且保證只接一個,要嘛拆成
font-size/line-height分開寫。
- 症狀:複製腳本後顯示「上次用在 0 天前」,但這個 0 存進 localStorage 之後永遠不會變老。
- 原因:設計規格的資料模型把「天數」當成儲存欄位,天數是衍生值,會隨時間改變。
- 解法:改存
lastUsedDate: string | null,畫面上用daysBetween(lastUsedDate, todayISO())算。 - 教訓:規格給的資料模型不是聖旨。任何「距今多久」的欄位都該存時間點,不存差值。
- 症狀:進度點的寬度轉場沒有動畫。
- 原因:預設 duration scale 只有 75/100/150/200/300/500/700/1000,
duration-250不存在,class 被靜默忽略。 - 解法:
duration-[250ms]。 - 教訓:Tailwind 對不存在的 class 不會報錯。設計稿給的非標準數值一律先確認在不在 scale 裡。
- 症狀:
vite.config.ts加了test: {...}之後tsc -b型別錯誤。 - 原因:從
viteimport 的defineConfig不認得test欄位。 - 解法:
import { defineConfig } from 'vitest/config'。
- 症狀:安裝後 8 個 high severity(brace-expansion DoS),
npm audit fix跑完數字不變。 - 原因:漏洞在
vite-plugin-pwa → workbox-build → … → brace-expansion這條鏈的深處,沒有 non-breaking 的修法。 - 解法:
package.json加"overrides": { "brace-expansion": "^5.0.9" },重裝後歸零。 - 教訓:
audit fix沒動靜時看的是「有沒有 non-breaking 版本」,不是「修不了」。overrides 通常可解。
- 症狀:
npm run dev開著卻找不到 SW,也沒有任何 cache。 - 原因:
vite-plugin-pwa預設只在 build 產物啟用(沒開devOptions)。 - 解法:離線一定要用
npm run build && npm run preview驗。真正的驗法是把 preview server 砍掉再重整——還能載入才叫離線可用。
- 症狀:要驗 <768px 的逐題流程,呼叫 resize 顯示成功,但
window.innerWidth一直是 1536。 - 原因:視窗被 OS 最大化/管理時,resize 不會生效。
- 解法:注入一個固定寬度的 iframe 指向同一個 origin:
document.body.innerHTML = '<iframe src="/" style="width:400px;height:820px">'。iframe 的寬度會驅動裡面的 media query 與matchMedia。
- 症狀:第一個 commit 的訊息開頭多了一行
@,結尾也多一個@。 - 原因:
git commit -m @'...'@是 PowerShell 語法,在 Bash 裡@'只是普通字元。 - 解法:
git commit -F - <<'EOF' … EOF。已用--amend修正。 - 教訓:這個環境兩種 shell 並存,多行字串的寫法不共用。
- 症狀:想停掉 preview server,用
taskkill //F //FI "WINDOWTITLE eq *" //IM node.exe,結果什麼都沒殺到。 - 風險:這條指令若真的命中,會連使用者其他無關的 node 程序一起殺掉。
- 解法:
netstat -ano | grep :4178找出 PID,再taskkill //F //PID <pid>。 - 教訓:關掉背景服務要用 port → PID 定位,永遠不要對
node.exe下/IM。
- 症狀:把
bets.eyebrow改成引用nav.bets之後,畫面整個空白,console 是ReferenceError: Cannot access 'nav' before initialization。 - 原因:
copy.ts裡bets宣告在nav前面。const會 hoist 但處在 TDZ,模組由上而下求值時bets先算,那時nav還沒初始化。 - 解法:把
nav移到bets與scripts之前。 - 教訓:這種錯只在「一個 const 的初始值引用另一個 const」時才會出現,型別檢查不會擋——TypeScript 認為型別沒問題。順序要自己顧。
- 症狀:每次
git add都刷一排LF will be replaced by CRLF。 - 原因:Windows 預設
core.autocrlf=true,而檔案是以 LF 寫入的。 - 現況:不影響內容,暫時不處理。若之後覺得吵,加
.gitattributes指定* text=auto eol=lf。
完成事項
- 側欄底部收斂成一個「⋯」,裡面是完整紀錄/匯出/備份。原本三個常駐項目(匯出、備份、外觀)現在只剩一個。
- 外觀切換移到品牌那一行的右側,兩者都放大:品牌 17px → 20px(新的
.t-brand),圖示 13px → 16px。 - 完整紀錄:不在分頁列上的回顧視圖,時間軸不設上限(「7 筆 · 全部」)。工作台因此收了一個
scope參數。 - 工作台的時間軸由「最近三週」改成最近 14 天,與條狀圖同一個
BAR_DAYS,兩邊看到的範圍一致。 - 工作台變成三段式響應:≥1200px 三欄;<1200px 收掉右欄(模式本來就有自己的分頁);<900px 一次只顯示列表或詳情,加一個「← 回到列表」。
- 「匯出」的回饋不再提 Obsidian,只說「已複製 N 天記錄」。
- 頁尾:
未寄 2026 · 咖啡宇宙的一部份。 - 實測 393px:品牌+外觀一行、四個分頁+⋯ 一行,共兩行;完整紀錄在窄螢幕走完 列表 → 詳情 → 回到列表。
決策與備註
- 「完整紀錄」不是第五個分頁。 四個分頁是同一個整理動作的四個階段,塞第五個進去會破壞那個結構。它是從「⋯」進來的視圖,
Tab型別多一個'records',但不出現在分頁列上。 - 工作台重用而不是另寫一個列表。 完整紀錄與寬螢幕的「模式」是同一個元件,差別只有
scope='all'。 - ⋯ 選單吃掉了 BackupPanel。 匯出與備份現在同屬一個面板的不同階段,狀態機併在
MoreMenu裡,側欄回到只負責導覽。 - README 的分頁名同步校正:第四輪改名時只動了程式,README 的表格還停在「賭注/腳本」。
起因:沒有帳號、沒有同步,所有東西只活在一個瀏覽器的 localStorage。使用者清一次瀏覽資料、換一支手機、系統回收儲存空間,記錄就全沒了。原本的「匯出」是 Markdown,好讀但救不回來。
完成事項
lib/backup.ts:toBackup產生帶格式標記的 JSON、parseBackup讀回來、downloadBackup用 Blob +<a download>存到本機。全程不經過任何伺服器。- 側欄新增「備份」,點開後「存成檔案」與「從備份還原」並列。
- 還原是全站唯一會蓋掉既有記錄的動作,所以先顯示「這份備份有 N 天記錄、M 個賭注」與「換上去之後,現在這台裝置上的內容會被它取代」,再問一次。
- 6 個單元測試(round-trip、筆數、壞檔案、壞資料、空狀態、檔名),共 29 個。
- 實測:注入合成備份檔走完整條還原路徑,localStorage 由 2 筆變 3 筆、賭注 1 個變 2 個,畫面即時更新。
決策與備註
- 「還原」不能藏在「存檔」後面。 第一版把還原入口放在存檔完成的面板裡,做出來才發現順序是錯的:真正需要還原的人,資料通常剛好已經沒了,卻得先備份一份空的才看得到還原。改成點開就兩個並列。
- 還原後強制 reload。 各畫面的狀態是掛載時算出來的(「今天」的四種狀態、逐題流程的進度、工作台選了哪一天),整份資料被抽換之後那些值全是舊的——實測前先看到「今天」還停在還原前的「好,今天就這樣。」。換完顯示「換好了。」800ms 後重載,讓每個畫面從新資料重新算。
- 驗證沿用
persist的migrate。 備份檔跟 localStorage 一樣可能是舊版或壞的,同一套清洗規則就夠了,不另寫一份。 - 備份檔沒有加密,README 據實寫明——它跟記錄本身一樣私密,放哪裡由使用者決定。
- 只多一個頂層按鈕。 側欄底部已經有「匯出」與外觀圖示,窄螢幕橫列排得下是有上限的,所以還原收在面板裡而不是再開一個按鈕。
起因:iPhone 16(393px)上「腳本」被擠到第二行。
完成事項
- 收合時品牌獨占一行,日期改成與品牌同一行(不多佔一列高度),分頁橫列因此排得下。頂部橫列從三行變兩行。
- 分頁改名:「賭注」→ 如果、「腳本」→ 遇到。頁面眉標與 Markdown 匯出的段落標題都引用同一個常數,不會各叫各的。
- 「匯出 Markdown」→「匯出」。展開後的回饋仍寫著「可貼進 Obsidian」,格式資訊留在那裡就夠了。
- 外觀切換改成單一字元:◐ 跟隨系統/○ 淺/● 深。
- 實測 393px:六個按鈕(今天、模式、如果、遇到、匯出、◐)全在同一行,內容寬 349px 用掉 257px。
決策與備註
- 改名的理由不只是排版。 「如果」正是那個分頁自己在說的話(「如果我說了,會怎樣?」),「遇到」正是每一組腳本的組頭(「遇到 · 場合」)。名字往內容靠,不是為了短而短。
- 中間一度用過「what if」,做出來看了才發現兩個問題:它是整個介面唯一的英文,以及它比「賭注」寬(47px vs 30px),單看是讓換行更糟。換成「如果」之後四個分頁都是 30px 等寬,節奏才對。
- 圖示用文字字元,不用圖檔。 設計本來就沒有任何圖片素材,「+」「→」都是字元,◐○● 沿用同一套做法。純圖示按鈕補上
aria-label,否則螢幕閱讀器只會唸到一個圓圈。 - 內文仍然說「賭注」「押下去」(「還沒有結案的賭注」「下一個賭注」「我賭對方會…」)。改的是分頁名,那套「押注 → 對答案」的說法是這個機制的核心語彙,沒有跟著動。若要一起換再說。
完成事項
scripts/fetch-fonts.mjs:向 Google css2 端點要 CSS,把 213 個帶unicode-range的 woff2 分片下載到public/fonts/,並產生public/fonts.css(本機路徑)。共 5.6 MB。index.html拿掉 Google Fonts 的 preconnect 與 stylesheet,改指向自己的/fonts.css。- service worker:字體不進 precache,改用 CacheFirst 的 runtime cache。
- 補上
public/fonts/OFL.txt(SIL Open Font License 1.1)。 - README 的隱私段落改成「沒有任何對外請求」,並新增「字體」一節說明分片做法與授權。
- 實測:
dist/全域搜尋沒有任何 googleapis/gstatic;preview 載入 24 筆請求全是 same-origin;213 片只抓了 15 片;把 preview server 砍掉後重整仍正常顯示中文。
決策與備註
- 只抓 400 一個字重。
type.css的 37 個 class 全是 400,設計稿雖然載了 400;500,但實作沒有一處用到 500。少一半體積。 - 不自己跑 pyftsubset,直接用 Google 已經切好的分片。 切分策略(哪些字碼一組)是 Google 長期調過的,自己重切只會更差;而且不必引入 Python 字體工具鏈。腳本只做「下載 + 改寫路徑」。
- 字體 CSS 獨立成
public/fonts.css,不進 app 的 CSS bundle。 213 條@font-face有 227KB,混進 bundle 會讓 app CSS 從 17KB 變成 216KB,而且每次改樣式使用者都要重抓一次。拆開後 app CSS 回到 16.8KB。 - 字體不 precache。 5.6MB 全部 precache 等於要使用者第一次開就下載一整套中文字,完全違背分片的用意。改用 runtime cache:抓過哪片就留哪片。
下一輪待辦
- 暫無。三個階段的畫面與隱私目標都到位了。
完成事項
views/desktop/Workbench.tsx:三欄回顧工作台(左時間軸 300px/中詳情彈性/右模式 250px)。- 新增 selectors:
recentEntries(最近三週、新的在上)、sameWhoBefore(同一個對象在這天之前發生過的)。 App.tsx加上(min-width: 1200px)的 matchMedia,AppShell在寬螢幕時把「模式」換成工作台,容器 max-width 由 900 放寬到 1280、內容區不受 560px 限制。- 補 3 個單元測試(時間軸範圍與排序、同對象回溯的過濾條件),共 23 個。
- 型別檢查通過;淺色/深色、有記錄/「今天還好」的詳情、<1200px 的退回都在瀏覽器實際點過。
決策與備註
- 工作台掛在「模式」分頁下,不新增第五個分頁。 規格說 1c 是「回顧」進階視圖但沒說怎麼整合。1c 的右欄本來就是模式的完整內容,左中兩欄補的是 1a 完全沒有的能力——回頭看過去那幾天(1a 的「今天」只看得到今天)。掛在模式底下等於「模式的進階版」,四個分頁平權的結構不動。
- 左欄表頭拿掉了產品名。 設計稿的 1c 是完整 app 外框,表頭掛著「未寄」;在這裡它是分頁內容,左邊兩欄外就是側欄的品牌,再掛一次會像 bug。只留「11 筆 · 最近三週」。
.t-quote-16的行高從 1.75 改成 1.8,對齊原型(賭注引句與工作台的回溯引句都是 16px/1.8)。- 新增四個排版 class:
.t-quote-19-tight、.t-mono-105-eyebrow、.t-mono-11-plain、.t-ui-14-list。都是原型裡確實存在、與既有 class 行高或字距不同的規格。
下一輪待辦
字體子集自打包(2026-07-31 第三輪完成)
完成事項
- 建立專案骨架:Vite + React 19 + TypeScript + Tailwind v4,純前端 SPA。
- Design tokens 與三層字體制度(
styles/theme.css、styles/type.css)。 - 資料層:localStorage 持久化、單一 store、衍生值 selectors。
- 共用元件:Chip / ChipGroup / BarChart / Button / Field / CustomSignalInput。
- 1a 桌機四分頁:今天(四種狀態)、模式、賭注、腳本,含匯出 Markdown 與跨分頁串接。
- 1b 手機逐題流程(六步)與 <768px 的響應式切換。
- PWA:manifest、service worker、Google Fonts runtime cache、自產圖示。
- 20 個單元測試(selectors / persist / markdown 匯出)。
- 推上 GitHub(public),README 改為中英雙語並加上隱私說明。
決策與備註
- 技術棧選 Vite 而非 Astro/Next:整個 app 都是 client 互動,SSR 幫不上忙。
- 字級收斂成語意 class:設計稿有大量 13.5px/行高 1.95/letter-spacing .14em 這種值,散在 utility 裡會失控,全部收進
type.css的三層制度。 - 深淺色用
light-dark():兩組值寫在同一處,手動覆寫只改:root[data-theme]的color-scheme,不換整組變數。 entries是唯一真實來源:對象排行、條狀圖、趨勢句、賭注戰績全部用算的,不另存。- 外觀切換是自己加的:規格要求產品要能手動覆寫深淺色,但設計稿裡沒有這個控制項(畫布右上角那組是設計工具,不是產品 UI)。做成與「匯出 Markdown」同級的純文字按鈕。
- 手機上四個分頁共用同一個 shell:規格只說 <768px 用逐題流程,沒說模式/賭注/腳本怎麼進去。若不共用,那三個分頁在手機上完全沒有入口。
- 隱私措辭不含糊:字體目前仍從 Google Fonts CDN 抓,是唯一的對外請求,README 據實寫出並列為待辦,沒有寫成「零請求」。
- 設計 handoff 已移出版控(
.gitignore),檔案留在本機design_handoff_place/。內容仍在第一個 commit 的歷史裡,未從歷史移除。 - 英文名定為 Place;程式碼裡的
PRODUCT_NAME維持「未寄」,那是 UI 語言不是專案名。
下一輪待辦
1c 回顧工作台(2026-07-31 第二輪完成)字體子集自打包(2026-07-31 第三輪完成)