From bc9bb53c3290e95fd2dfb44b47ee7836fe0a46d1 Mon Sep 17 00:00:00 2001 From: ethan-CCY Date: Thu, 18 Dec 2025 16:24:52 +0800 Subject: [PATCH 01/10] Refresh timer themes and controls --- .gitignore | 2 + src/App.jsx | 26 +------- src/components/Controls.jsx | 10 +-- src/components/TimeDisplay.jsx | 7 ++- src/lib/time.js | 10 +-- src/styles.css | 109 +++++++++++++++++++++++---------- 6 files changed, 97 insertions(+), 67 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/src/App.jsx b/src/App.jsx index 69b2e95..550ba2e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -45,7 +45,7 @@ export default function App() { const primaryTime = mode === "stopwatch" ? formatHMS(sw.elapsedMs, { showMs: true }) - : formatHMS(cd.remainingMs); + : formatHMS(cd.remainingMs, { showMs: true }); const onReset = () => { if (mode === "stopwatch") { @@ -66,7 +66,7 @@ export default function App() { }; return ( -
+
Tide Timer
@@ -122,28 +122,6 @@ export default function App() {
) : null} - -
-
-
你可以接著練的功能
-
    -
  • 倒數自訂:分鐘/秒輸入、滑桿、+10s/-10s
  • -
  • 音效開關(完成提示音)
  • -
  • localStorage:記住上次模式與倒數秒數
  • -
  • 更療癒:水波更慢、呼吸週期可調
  • -
  • 統計:今日累積專注時間 / 完成次數
  • -
-
- -
-
部署到 GitHub Pages
-
    -
  1. 把整包檔案推到你的 GitHub repo
  2. -
  3. Repo Settings → Pages → Source 選 GitHub Actions
  4. -
  5. 等待 Actions 跑完,就會自動發佈
  6. -
-
-
diff --git a/src/components/Controls.jsx b/src/components/Controls.jsx index 7797f4e..f8f9c2a 100644 --- a/src/components/Controls.jsx +++ b/src/components/Controls.jsx @@ -13,14 +13,14 @@ export default function Controls({
{extraLeft} - {extraRight}
diff --git a/src/components/TimeDisplay.jsx b/src/components/TimeDisplay.jsx index b1719cd..2c84fa2 100644 --- a/src/components/TimeDisplay.jsx +++ b/src/components/TimeDisplay.jsx @@ -1,9 +1,14 @@ import React from "react"; export default function TimeDisplay({ primary, secondary }) { + const [main, ms] = String(primary || "").split("."); + return (
-
{primary}
+
+ {main} + {ms ? .{ms} : null} +
{secondary ?
{secondary}
: null}
); diff --git a/src/lib/time.js b/src/lib/time.js index 804a5c6..1a27cf0 100644 --- a/src/lib/time.js +++ b/src/lib/time.js @@ -7,14 +7,14 @@ export function formatHMS(ms, { showMs = false } = {}) { const h = Math.floor(total / 3600000); const m = Math.floor((total % 3600000) / 60000); const s = Math.floor((total % 60000) / 1000); - const cs = Math.floor((total % 1000) / 10); // centiseconds + const msPart = total % 1000; const mm = String(m).padStart(2, "0"); const ss = String(s).padStart(2, "0"); const hh = String(h).padStart(2, "0"); - if (h > 0) { - return showMs ? `${hh}:${mm}:${ss}.${String(cs).padStart(2, "0")}` : `${hh}:${mm}:${ss}`; - } - return showMs ? `${mm}:${ss}.${String(cs).padStart(2, "0")}` : `${mm}:${ss}`; + const base = `${hh}:${mm}:${ss}`; + if (!showMs) return base; + + return `${base}.${String(msPart).padStart(3, "0")}`; } diff --git a/src/styles.css b/src/styles.css index e0aebfc..0b8e3a0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,26 +1,50 @@ :root { - --bg1: #0b1326; - --bg2: #0a1b2e; - --card: rgba(255,255,255,0.06); - --stroke: rgba(255,255,255,0.12); + --bg1: #cdb997; + --bg2: #9f7d57; + --bg-highlight1: rgba(255, 255, 255, 0.12); + --bg-highlight2: rgba(255, 255, 255, 0.1); + --card: rgba(255,255,255,0.08); + --stroke: rgba(255,255,255,0.14); --stroke2: rgba(255,255,255,0.22); - --text: rgba(255,255,255,0.92); - --muted: rgba(255,255,255,0.68); - --muted2: rgba(255,255,255,0.55); - --shadow: 0 18px 60px rgba(0,0,0,0.55); + --text: rgba(255,255,255,0.94); + --muted: rgba(255,255,255,0.78); + --muted2: rgba(255,255,255,0.62); + --shadow: 0 18px 60px rgba(0,0,0,0.35); + --water-top: rgba(136, 210, 255, 0.45); + --water-bottom: rgba(72, 158, 230, 0.24); + --water-surface: rgba(255,255,255,0.24); + --cta-start: #7fcf7c; + --cta-pause: #e25656; + --cta-reset: #a5724b; +} + +.app.mode-countdown { + --bg1: #8dbe78; + --bg2: #4d7844; + --bg-highlight1: rgba(255, 255, 255, 0.10); + --bg-highlight2: rgba(255, 255, 255, 0.08); + --water-top: rgba(193, 232, 173, 0.45); + --water-bottom: rgba(114, 176, 104, 0.26); } * { box-sizing: border-box; } html, body { height: 100%; margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Noto Sans TC", Arial; } body { color: var(--text); - background: radial-gradient(900px 450px at 20% 10%, rgba(130, 200, 255, 0.18), transparent 60%), - radial-gradient(900px 450px at 80% 15%, rgba(120, 160, 255, 0.14), transparent 55%), - linear-gradient(160deg, var(--bg1), var(--bg2)); + background: #0f0f0f; } a { color: inherit; } -.app { min-height: 100%; display: flex; flex-direction: column; padding: 24px; gap: 18px; } +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + padding: 24px; + gap: 18px; + background: radial-gradient(900px 480px at 20% 12%, var(--bg-highlight1), transparent 60%), + radial-gradient(900px 480px at 80% 14%, var(--bg-highlight2), transparent 55%), + linear-gradient(160deg, var(--bg1), var(--bg2)); +} .header { display: flex; align-items: center; justify-content: space-between; @@ -51,12 +75,12 @@ a { color: inherit; } .main { display: grid; - grid-template-columns: 1.2fr 0.8fr; + grid-template-columns: 1fr; gap: 18px; align-items: start; -} -@media (max-width: 980px) { - .main { grid-template-columns: 1fr; } + max-width: 1100px; + width: 100%; + margin: 0 auto; } .card { @@ -90,12 +114,22 @@ a { color: inherit; } } .modeTitle { font-size: 14px; color: var(--muted); letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px; } -.timeDisplay { display: grid; gap: 10px; } +.timeDisplay { display: grid; gap: 10px; justify-items: center; } .timePrimary { - font-size: 44px; + font-size: 48px; font-weight: 700; letter-spacing: 0.8px; text-shadow: 0 10px 20px rgba(0,0,0,0.35); + display: inline-flex; + align-items: baseline; + gap: 6px; + font-variant-numeric: tabular-nums; +} +.timeMain { font-variant-numeric: inherit; } +.timeMs { + font-size: 26px; + opacity: 0.88; + font-variant-numeric: tabular-nums; } .timeSecondary { font-size: 13px; color: var(--muted); } @@ -111,14 +145,35 @@ a { color: inherit; } padding: 10px 14px; border-radius: 14px; transition: transform 120ms ease, background 200ms ease, border 200ms ease; + font-weight: 700; } .btn:hover { transform: translateY(-1px); background: rgba(255,255,255,0.10); } .btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; } .btnPrimary { border: 1px solid rgba(255,255,255,0.18); - background: rgba(255,255,255,0.14); padding: 10px 18px; } +.btnStart { + background: var(--cta-start); + border-color: #6bbf68; + color: #0f2b12; + box-shadow: 0 14px 30px rgba(127, 207, 124, 0.30); +} +.btnStart:hover { background: #74c66f; } +.btnPause { + background: var(--cta-pause); + border-color: #d44c4c; + color: #fff; + box-shadow: 0 14px 30px rgba(226, 86, 86, 0.32); +} +.btnPause:hover { background: #d84c4c; } +.btnReset { + background: var(--cta-reset); + border-color: #8b5f3f; + color: #fff; + box-shadow: 0 12px 26px rgba(165, 114, 75, 0.32); +} +.btnReset:hover { background: #9b6946; } .presets { display: flex; @@ -174,16 +229,6 @@ a { color: inherit; } background: radial-gradient(260px 140px at 50% 50%, rgba(255,255,255,0.12), rgba(255,255,255,0.05)); } -.side { display: grid; gap: 14px; } -.panel { - background: rgba(255,255,255,0.05); - border: 1px solid rgba(255,255,255,0.10); - border-radius: 18px; - padding: 16px; - backdrop-filter: blur(14px); -} -.panelTitle { font-size: 14px; font-weight: 700; margin-bottom: 10px; } -.panelList { margin: 0; padding-left: 18px; color: var(--muted); font-size: 13px; display: grid; gap: 8px; } .footer { text-align: center; color: var(--muted2); font-size: 12px; padding: 10px 0; } /* Breathing animation */ @@ -215,7 +260,7 @@ a { color: inherit; } border-radius: 999px; overflow: hidden; border: 1px solid rgba(255,255,255,0.10); - background: radial-gradient(200px 140px at 50% 30%, rgba(255,255,255,0.08), rgba(0,0,0,0.06)); + background: radial-gradient(200px 140px at 50% 30%, rgba(255,255,255,0.12), rgba(0,0,0,0.04)); box-shadow: inset 0 -30px 60px rgba(0,0,0,0.25); } .waterFill { @@ -223,13 +268,13 @@ a { color: inherit; } inset: 0; transform: translateY(50%); transition: transform 180ms linear; - background: linear-gradient(180deg, rgba(160, 210, 255, 0.22), rgba(80, 150, 255, 0.10)); + background: linear-gradient(180deg, var(--water-top), var(--water-bottom)); } .waterSurface { position: absolute; left: 0; right: 0; height: 2px; - background: rgba(255,255,255,0.14); + background: var(--water-surface); filter: blur(0.2px); transition: top 180ms linear; opacity: 0.8; From 2630bf57931fd19c3e4fe4350a198adcb902f7ff Mon Sep 17 00:00:00 2001 From: ethan-CCY Date: Mon, 22 Dec 2025 10:10:27 +0800 Subject: [PATCH 02/10] Fix formatting in README for statistics section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2fa34f..08a341e 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ npm run dev - 倒數結束:加提示音(並可開關) - localStorage:記住上次模式與倒數秒數 - 視覺:波紋更慢、呼吸週期可調 -- 統計:今日累積專注時間 / 完成次數 +- 統計:今日累積專注時間 / 完成次數 From b806e0ff4b66aea6a1d0ac45f7c34fb680053074 Mon Sep 17 00:00:00 2001 From: ethan-CCY Date: Mon, 22 Dec 2025 12:01:13 +0800 Subject: [PATCH 03/10] Refine stopwatch layout and visuals --- src/App.jsx | 116 +++++++++++++++++++++------------- src/components/LapList.jsx | 29 +++++---- src/components/ModeSwitch.jsx | 2 +- src/components/WaterGauge.jsx | 21 ++++-- src/lib/time.js | 4 +- src/styles.css | 77 ++++++++++++++++++---- 6 files changed, 175 insertions(+), 74 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 550ba2e..5981f53 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,19 +26,22 @@ export default function App() { return { ringProgress: progress, waterLevel: progress, + waterVariant: "countdown", title: "倒數", subtitle: `總長 ${formatHMS(cd.totalMs)}`, + flashKey: null, }; } // Stopwatch: make it feel calm by looping visuals each minute - const loop = 60 * 1000; + const loop = 10 * 1000; const phase = (sw.elapsedMs % loop) / loop; return { ringProgress: phase, - waterLevel: 0.15 + phase * 0.7, // keep some padding (never fully empty/full) - title: "碼表", - subtitle: sw.isRunning ? "流動中…" : "休息一下也很好", + waterLevel: 0.5, + waterVariant: "stopwatch", + title: "碼錶", + subtitle: sw.isRunning ? "流動中…" : "跑跑跑~向前跑~", }; }, [mode, sw.elapsedMs, sw.isRunning, cd.remainingMs, cd.totalMs]); @@ -70,7 +73,7 @@ export default function App() {
Tide Timer
-
療癒型碼表 / 倒數
+
療癒型碼錶 / 倒數
{ // stop running when switching modes (keeps mental model simple) @@ -80,48 +83,73 @@ export default function App() { }} />
-
-
-
- - -
-
{viz.title}
- -
-
+
+ {mode === "stopwatch" ? ( +
+
+
+ + +
+
{viz.title}
+ +
+
- {mode === "countdown" ? ( -
- -
- 小提示:使用快速鍵,依點擊次數,初始時間可累加,或按 "清除" 重新設定。 + + 記錄 + + } + /> +
+ + +
+ ) : ( +
+
+ + +
+
{viz.title}
+
- ) : null} - - - Lap - - ) : null - } - /> - - {mode === "stopwatch" ? : null} - - {mode === "countdown" && cd.remainingMs === 0 ? ( -
- 完成 ✨ -
- ) : null} -
+ + {mode === "countdown" ? ( +
+ +
+ 小提示:使用快速鍵,依點擊次數,初始時間可累加,或按 "清除" 重新設定。 +
+
+ ) : null} + + + + {mode === "countdown" && cd.remainingMs === 0 ? ( +
+ 完成 ✨ +
+ ) : null} +
+ )}
diff --git a/src/components/LapList.jsx b/src/components/LapList.jsx index b824470..6df55a3 100644 --- a/src/components/LapList.jsx +++ b/src/components/LapList.jsx @@ -2,18 +2,23 @@ import React from "react"; import { formatHMS } from "../lib/time.js"; export default function LapList({ laps }) { - if (!laps.length) return null; return ( -
-
Laps
-
    - {laps.map((ms, idx) => ( -
  1. - {String(idx + 1).padStart(2, "0")} - {formatHMS(ms, { showMs: true })} -
  2. - ))} -
-
+
+
+
記錄
+ {laps.length ? ( +
    + {laps.map((ms, idx) => ( +
  1. + {String(idx + 1).padStart(2, "0")} + {formatHMS(ms, { showMs: true })} +
  2. + ))} +
+ ) : ( +
尚無紀錄
+ )} +
+
); } diff --git a/src/components/ModeSwitch.jsx b/src/components/ModeSwitch.jsx index 4e42c75..261ec8e 100644 --- a/src/components/ModeSwitch.jsx +++ b/src/components/ModeSwitch.jsx @@ -9,7 +9,7 @@ export default function ModeSwitch({ mode, setMode }) { role="tab" aria-selected={mode === "stopwatch"} > - 碼表 + 碼錶 ))} +
); } diff --git a/src/components/WaterGauge.jsx b/src/components/WaterGauge.jsx index 2f660e4..80d8118 100644 --- a/src/components/WaterGauge.jsx +++ b/src/components/WaterGauge.jsx @@ -23,9 +23,16 @@ export default function WaterGauge({ level = 0.5, variant = "countdown", size = const y = `${(1 - l) * 100}%`; return ( -