Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ export default function App() {
return {
ringProgress: progress,
waterLevel: progress,
waterVariant: "countdown",
title: "倒數",
subtitle: `總長 ${formatHMS(cd.totalMs)}`,
flashKey: null,
};
}

Expand All @@ -38,14 +36,9 @@ export default function App() {
const phase = (sw.elapsedMs % loop) / loop;
return {
ringProgress: phase,
waterLevel: 0.5,
waterVariant: "stopwatch",
title: "碼錶",
subtitle: sw.isRunning ? "流動中…" : "跑跑跑~向前跑~",
flashKey:
sw.isRunning && sw.elapsedMs >= 10000
? Math.floor(sw.elapsedMs / 10000)
: null,
waterLevel: 0.15 + phase * 0.7, // keep some padding (never fully empty/full)
title: "碼表",
subtitle: sw.isRunning ? "流動中…" : "休息一下也很好",
};
}, [mode, sw.elapsedMs, sw.isRunning, cd.remainingMs, cd.totalMs]);

Expand Down Expand Up @@ -77,7 +70,7 @@ export default function App() {
<header className="header">
<div className="brand">
<div className="brandTitle">Tide Timer</div>
<div className="brandTag">療癒型碼錶 / 倒數</div>
<div className="brandTag">療癒型碼表 / 倒數</div>
</div>
<ModeSwitch mode={mode} setMode={(m) => {
// stop running when switching modes (keeps mental model simple)
Expand All @@ -91,10 +84,7 @@ export default function App() {
<section className="card breathe" aria-label="Timer">
<div className="vizWrap">
<Ring progress={viz.ringProgress} />
{mode === "stopwatch" && viz.flashKey !== null ? (
<div className="ringFlash" key={viz.flashKey} />
) : null}
<WaterGauge level={viz.waterLevel} variant={viz.waterVariant} />
<WaterGauge level={viz.waterLevel} />
<div className="vizOverlay">
<div className="modeTitle">{viz.title}</div>
<TimeDisplay primary={primaryTime} secondary={viz.subtitle} />
Expand All @@ -118,7 +108,7 @@ export default function App() {
extraLeft={
mode === "stopwatch" ? (
<button className="btn" onClick={onLap} disabled={!sw.isRunning}>
記錄
Lap
</button>
) : null
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/LapList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function LapList({ laps }) {
if (!laps.length) return null;
return (
<div className="laps">
<div className="lapsTitle">記錄</div>
<div className="lapsTitle">Laps</div>
<ol className="lapsList">
{laps.map((ms, idx) => (
<li key={idx} className="lapItem">
Expand Down
2 changes: 1 addition & 1 deletion src/components/ModeSwitch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function ModeSwitch({ mode, setMode }) {
role="tab"
aria-selected={mode === "stopwatch"}
>
碼錶
碼表
</button>
<button
className={`pill ${mode === "countdown" ? "active" : ""}`}
Expand Down
15 changes: 3 additions & 12 deletions src/components/WaterGauge.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,10 @@ import React from "react";
import { clamp } from "../lib/time.js";

/**
* WaterGauge: soothing water level + slow wave drift (countdown),
* or static dark plate for stopwatch.
* level: 0..1 (1 = full) for animated mode
* WaterGauge: soothing water level + slow wave drift.
* level: 0..1 (1 = full)
*/
export default function WaterGauge({ level = 0.5, variant = "countdown" }) {
if (variant === "stopwatch") {
return (
<div className="waterGauge waterGaugeStatic" aria-hidden="true">
<div className="waterPlate" />
</div>
);
}

export default function WaterGauge({ level = 0.5 }) {
const l = clamp(level, 0, 1);
const y = `${(1 - l) * 100}%`;

Expand Down
4 changes: 2 additions & 2 deletions src/lib/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 msPart = Math.floor((total % 1000) / 10);
const msPart = total % 1000;

const mm = String(m).padStart(2, "0");
const ss = String(s).padStart(2, "0");
Expand All @@ -16,5 +16,5 @@ export function formatHMS(ms, { showMs = false } = {}) {
const base = `${hh}:${mm}:${ss}`;
if (!showMs) return base;

return `${base}.${String(msPart).padStart(2, "0")}`;
return `${base}.${String(msPart).padStart(3, "0")}`;
}
51 changes: 4 additions & 47 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,13 @@ a { color: inherit; }
.vizOverlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
display: grid;
place-items: center;
text-align: center;
pointer-events: none;
padding-top: 10px;
}
.modeTitle {
position: absolute;
top: 16px;
left: 0;
right: 0;
font-size: 14px;
color: var(--muted);
letter-spacing: 2px;
text-transform: uppercase;
}
.modeTitle { font-size: 14px; color: var(--muted); letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px; }

.timeDisplay { display: grid; gap: 10px; justify-items: center; }
.timePrimary {
Expand Down Expand Up @@ -260,24 +251,6 @@ a { color: inherit; }
transform: rotate(-90deg);
transition: stroke-dashoffset 140ms linear;
}
.ringFlash {
position: absolute;
width: 280px;
height: 280px;
border-radius: 999px;
border: 2px solid rgba(255,255,255,0.75);
z-index: 2;
box-shadow:
0 0 30px rgba(255,255,255,0.45),
0 0 60px rgba(255,255,255,0.25);
pointer-events: none;
animation: ringFlash 0.9s ease-out forwards;
}
@keyframes ringFlash {
0% { opacity: 0; transform: scale(0.95); }
25% { opacity: 0.95; transform: scale(1); }
100% { opacity: 0; transform: scale(1.14); }
}

/* Water */
.waterGauge {
Expand All @@ -290,29 +263,13 @@ a { color: inherit; }
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);
}
.waterGaugeStatic {
background: radial-gradient(210px 160px at 50% 34%, rgba(255,255,255,0.06), rgba(0,0,0,0.58));
border: 1px solid rgba(255,255,255,0.14);
box-shadow:
inset 0 -30px 60px rgba(0,0,0,0.45),
inset 0 18px 40px rgba(0,0,0,0.35);
}
.waterFill {
position: absolute;
inset: 0;
transform: translateY(50%);
transition: transform 180ms linear;
background: linear-gradient(180deg, var(--water-top), var(--water-bottom));
}
.waterPlate {
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(160px 140px at 50% 38%, rgba(255,255,255,0.08), rgba(0,0,0,0.7));
box-shadow:
inset 0 14px 40px rgba(0,0,0,0.55),
inset 0 -12px 30px rgba(0,0,0,0.4);
}
.waterSurface {
position: absolute;
left: 0; right: 0;
Expand Down