feat: add restrained motion to pomodoro focus timer
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 3m40s

This commit is contained in:
2026-09-22 10:11:37 +08:00
parent a535f69cae
commit b3b0e07c42
2 changed files with 7 additions and 4 deletions
+6 -3
View File
@@ -27,6 +27,9 @@ const remainingMs = computed(() => state.value.status === 'running' && state.val
? Math.max(0, state.value.endsAt - nowMs.value)
: state.value.remainingMs)
const clock = computed(() => formatPomodoroTime(remainingMs.value))
const totalSeconds = computed(() => Math.max(0, Math.ceil(remainingMs.value / 1000)))
const clockMin = computed(() => String(Math.floor(totalSeconds.value / 60)).padStart(2, '0'))
const clockSec = computed(() => String(totalSeconds.value % 60).padStart(2, '0'))
const phaseLabel = computed(() => state.value.phase === 'focus' ? '专注' : '休息')
const statusLabel = computed(() => ({ idle: '准备开始', running: '进行中', paused: '已暂停', done: '已完成' })[state.value.status])
const progress = computed(() => {
@@ -101,10 +104,10 @@ onUnmounted(() => {
</header>
<div class="pomodoro-stage" :class="`pomodoro-stage--${state.phase}`">
<div class="pomodoro-stage-meta"><span>{{ phaseLabel }}</span><small>{{ statusLabel }}</small></div>
<output class="pomodoro-clock" :aria-label="`${phaseLabel}剩余 ${clock}`">{{ clock }}</output>
<div class="pomodoro-stage-meta" :key="`${state.phase}-${state.status}`"><span>{{ phaseLabel }}</span><small>{{ statusLabel }}</small></div>
<output class="pomodoro-clock" :aria-label="`${phaseLabel}剩余 ${clock}`"><span class="pomodoro-clock-min">{{ clockMin }}</span><span class="pomodoro-clock-sep">:</span><span class="pomodoro-clock-sec" :key="clockSec">{{ clockSec }}</span></output>
<div class="pomodoro-progress" role="progressbar" :aria-label="`${phaseLabel}进度`" aria-valuemin="0" aria-valuemax="100" :aria-valuenow="Math.round(progress)"><span :style="{ width: `${progress}%` }" /></div>
<p class="pomodoro-note">{{ state.phase === 'focus' ? '留一点完整的时间,只做眼前这一件事。' : '离开屏幕,喝口水,再回来。' }}</p>
<p class="pomodoro-note" :key="state.phase">{{ state.phase === 'focus' ? '留一点完整的时间,只做眼前这一件事。' : '离开屏幕,喝口水,再回来。' }}</p>
<div class="pomodoro-controls">
<button v-if="state.status === 'idle'" class="primary" type="button" @click="start"><Play />开始{{ phaseLabel }}</button>
+1 -1
View File
@@ -234,7 +234,7 @@ main:has(>.mvp-view .settings-sections) .settings-data>.inline-error{margin:14px
@media(prefers-reduced-motion:reduce){:is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button){transition:none}:is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):active:not(:disabled):not(.disabled){transform:none}}
/* Local-first focus timer: continuous paper, one orange accent. */
.pomodoro-view{width:min(100%,630px);margin:0 auto;padding:18px 0 64px}.pomodoro-heading{min-height:74px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #e8e0d5}.pomodoro-heading h1{margin:4px 0 0;font-size:34px;line-height:1.1;letter-spacing:-.035em}.pomodoro-heading>svg{width:28px;height:28px;color:var(--accent)}.pomodoro-eyebrow{color:var(--muted);font-size:12px}.pomodoro-stage{padding:48px 0 32px;text-align:center;border-bottom:1px solid #e8e0d5}.pomodoro-stage-meta{display:flex;justify-content:center;align-items:baseline;gap:9px}.pomodoro-stage-meta span{font-size:14px;font-weight:750}.pomodoro-stage-meta small{color:var(--muted);font-size:11px}.pomodoro-clock{display:block;margin:18px 0 16px;font-size:clamp(68px,14vw,104px);font-weight:680;line-height:1;letter-spacing:-.065em;font-variant-numeric:tabular-nums}.pomodoro-progress{width:min(360px,80%);height:4px;margin:0 auto;background:#eadfce;overflow:hidden}.pomodoro-progress span{display:block;height:100%;background:var(--accent);transition:width .25s linear}.pomodoro-note{margin:23px auto 0;color:var(--muted);font-size:13px}.pomodoro-controls{min-height:52px;margin-top:28px;display:flex;justify-content:center;flex-wrap:wrap;gap:9px}.pomodoro-controls button{min-height:44px;min-width:112px}.pomodoro-summary{margin:14px 0 0;text-align:center;color:var(--muted);font-size:11px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:720px){.pomodoro-view{width:100%;padding:8px 12px calc(84px + var(--safe-area-bottom))}.pomodoro-heading h1{font-size:24px}.pomodoro-stage{padding-top:38px}.pomodoro-clock{font-size:clamp(64px,22vw,88px)}.pomodoro-controls button{min-height:44px;flex:1 1 128px}}@media(prefers-reduced-motion:reduce){.pomodoro-progress span{transition:none}}
.pomodoro-view{width:min(100%,630px);margin:0 auto;padding:18px 0 64px}.pomodoro-heading{min-height:74px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #e8e0d5}.pomodoro-heading h1{margin:4px 0 0;font-size:34px;line-height:1.1;letter-spacing:-.035em}.pomodoro-heading>svg{width:28px;height:28px;color:var(--accent)}.pomodoro-eyebrow{color:var(--muted);font-size:12px}.pomodoro-stage{padding:48px 0 32px;text-align:center;border-bottom:1px solid #e8e0d5}.pomodoro-stage-meta{display:flex;justify-content:center;align-items:baseline;gap:9px}.pomodoro-stage-meta span{font-size:14px;font-weight:750}.pomodoro-stage-meta small{color:var(--muted);font-size:11px}.pomodoro-clock{display:block;margin:18px 0 16px;font-size:clamp(68px,14vw,104px);font-weight:680;line-height:1;letter-spacing:-.065em;font-variant-numeric:tabular-nums}.pomodoro-clock-min,.pomodoro-clock-sep,.pomodoro-clock-sec{display:inline-block}.pomodoro-clock-sec{min-width:1.28em;text-align:left;animation:pomodoro-tick .5s cubic-bezier(.2,.8,.3,1)}.pomodoro-clock-sep{transform:translateY(-.02em);color:var(--muted)}.pomodoro-stage-meta{-webkit-animation:pomodoro-phase-in .4s ease;animation:pomodoro-phase-in .4s ease}.pomodoro-note{-webkit-animation:pomodoro-phase-in .45s ease .03s both;animation:pomodoro-phase-in .45s ease .03s both}@keyframes pomodoro-tick{from{transform:translateY(26%);opacity:.55}to{transform:none;opacity:1}}@keyframes pomodoro-phase-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.pomodoro-progress{width:min(360px,80%);height:4px;margin:0 auto;background:#eadfce;overflow:hidden}.pomodoro-progress span{display:block;height:100%;background:var(--accent);transition:width .25s linear}.pomodoro-note{margin:23px auto 0;color:var(--muted);font-size:13px}.pomodoro-controls{min-height:52px;margin-top:28px;display:flex;justify-content:center;flex-wrap:wrap;gap:9px}.pomodoro-controls button{min-height:44px;min-width:112px;transition:transform .14s ease,background-color .14s ease,border-color .14s ease,box-shadow .14s ease,color .14s ease}.pomodoro-controls button:hover:not(:disabled){transform:translateY(-2px)}.pomodoro-controls button:active:not(:disabled){transform:scale(.96)}.pomodoro-summary{margin:14px 0 0;text-align:center;color:var(--muted);font-size:11px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:720px){.pomodoro-view{width:100%;padding:8px 12px calc(84px + var(--safe-area-bottom))}.pomodoro-heading h1{font-size:24px}.pomodoro-stage{padding-top:38px}.pomodoro-clock{font-size:clamp(64px,22vw,88px)}.pomodoro-controls button{min-height:44px;flex:1 1 128px}}@media(prefers-reduced-motion:reduce){.pomodoro-progress span{transition:none}.pomodoro-clock-sec{animation:none}.pomodoro-stage-meta,.pomodoro-note{animation:none}.pomodoro-controls button{transition:none}.pomodoro-controls button:active:not(:disabled),.pomodoro-controls button:hover:not(:disabled){transform:none}}
/* Approved task detail 01: paper flow. */
.detail{min-width:0;border-left:1px solid var(--line);background:#fffdf8;overflow:hidden;display:flex;flex-direction:column}.shell.detail-open{transition:none}