feat: pomodoro duration settings sheet, honest announcements, done-state reset
This commit is contained in:
@@ -30,4 +30,15 @@ describe('pomodoro integration', () => {
|
||||
expect(panel).toContain('Date.now(), WRITER_ID')
|
||||
expect(panel).toContain('savePomodoroState(window.localStorage, STORAGE_KEY, state.value)')
|
||||
})
|
||||
|
||||
it('offers duration settings and keeps announcements honest about the running state', () => {
|
||||
expect(panel).toContain("import AppSheet from './components/AppSheet.vue'")
|
||||
expect(panel).toContain('@click="settingsOpen = true"')
|
||||
expect(panel).toContain('已跳过休息,下一轮专注已开始')
|
||||
expect(panel).toContain('专注完成,休息已开始')
|
||||
expect(panel).toContain('休息完成,下一轮专注已开始')
|
||||
expect(panel).toContain("v-if=\"state.status !== 'idle'\"")
|
||||
expect(panel).toContain("setPomodoroDurations(state.value, minutes, state.value.breakMinutes")
|
||||
expect(css).toContain('.pomodoro-settings__options button{min-height:44px')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Coffee, Pause, Play, RotateCcw, SkipForward, TimerReset } from 'lucide-vue-next'
|
||||
import { Coffee, Pause, Play, RotateCcw, Settings2, SkipForward, TimerReset, X } from 'lucide-vue-next'
|
||||
import AppSheet from './components/AppSheet.vue'
|
||||
import {
|
||||
formatPomodoroTime,
|
||||
loadPomodoroState,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
recalibratePomodoro,
|
||||
resetPomodoro,
|
||||
savePomodoroState,
|
||||
setPomodoroDurations,
|
||||
shouldAcceptPomodoroState,
|
||||
skipBreak,
|
||||
startBreak,
|
||||
@@ -21,6 +23,9 @@ const WRITER_ID = crypto.randomUUID()
|
||||
const state = ref(loadPomodoroState(window.localStorage, STORAGE_KEY))
|
||||
const nowMs = ref(Date.now())
|
||||
const announcement = ref('')
|
||||
const settingsOpen = ref(false)
|
||||
const focusOptions = [15, 20, 25, 30, 45, 50]
|
||||
const breakOptions = [3, 5, 10, 15]
|
||||
let intervalId: number | undefined
|
||||
|
||||
const remainingMs = computed(() => state.value.status === 'running' && state.value.endsAt !== null
|
||||
@@ -32,6 +37,10 @@ const clockMin = computed(() => String(Math.floor(totalSeconds.value / 60)).padS
|
||||
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 timerLocked = computed(() => state.value.status === 'running')
|
||||
const eyebrowText = computed(() => state.value.todayFocusCount > 0
|
||||
? `今天完成 ${state.value.todayFocusCount} 次 · ${state.value.todayFocusCount * state.value.focusMinutes} 分钟`
|
||||
: '今天完成 0 次')
|
||||
const progress = computed(() => {
|
||||
const total = (state.value.phase === 'focus' ? state.value.focusMinutes : state.value.breakMinutes) * 60_000
|
||||
return total ? Math.min(100, Math.max(0, ((total - remainingMs.value) / total) * 100)) : 0
|
||||
@@ -53,9 +62,12 @@ function start() {
|
||||
}
|
||||
function pause() { commit(pausePomodoro(state.value, Date.now(), WRITER_ID), `${phaseLabel.value}已暂停`) }
|
||||
function reset() { commit(resetPomodoro(state.value, Date.now(), WRITER_ID), `${phaseLabel.value}已重置`) }
|
||||
function beginBreak() { commit(startBreak(state.value, Date.now(), WRITER_ID), '专注完成,休息已准备好') }
|
||||
function beginBreak() { commit(startBreak(state.value, Date.now(), WRITER_ID), '专注完成,休息已开始') }
|
||||
function skip() { commit(skipBreak(state.value, Date.now(), WRITER_ID), '已跳过休息,下一轮专注已准备好') }
|
||||
function nextFocus() { commit(startNextFocus(state.value, Date.now(), WRITER_ID), '休息完成,下一轮专注已准备好') }
|
||||
function nextFocus() { commit(startNextFocus(state.value, Date.now(), WRITER_ID), '休息完成,下一轮专注已开始') }
|
||||
function skipRest() { commit(startNextFocus(state.value, Date.now(), WRITER_ID), '已跳过休息,下一轮专注已开始') }
|
||||
function setFocusMinutes(minutes: number) { commit(setPomodoroDurations(state.value, minutes, state.value.breakMinutes, Date.now(), WRITER_ID), `专注时长已设为 ${minutes} 分钟`) }
|
||||
function setBreakMinutes(minutes: number) { commit(setPomodoroDurations(state.value, state.value.focusMinutes, minutes, Date.now(), WRITER_ID), `休息时长已设为 ${minutes} 分钟`) }
|
||||
|
||||
function refresh() {
|
||||
if (document.visibilityState === 'hidden') return
|
||||
@@ -99,12 +111,12 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<section class="pomodoro-view" aria-labelledby="pomodoro-title">
|
||||
<header class="pomodoro-heading">
|
||||
<div><span class="pomodoro-eyebrow">今天完成 {{ state.todayFocusCount }} 次</span><h1 id="pomodoro-title">专注</h1></div>
|
||||
<div><span class="pomodoro-eyebrow">{{ eyebrowText }}</span><h1 id="pomodoro-title">专注</h1></div>
|
||||
<TimerReset aria-hidden="true" />
|
||||
</header>
|
||||
|
||||
<div class="pomodoro-stage" :class="`pomodoro-stage--${state.phase}`">
|
||||
<div class="pomodoro-stage-meta" :key="`${state.phase}-${state.status}`"><span>{{ phaseLabel }}</span><small>{{ statusLabel }}</small></div>
|
||||
<div class="pomodoro-stage-meta" :key="`${state.phase}-${state.status}`" :aria-label="`${phaseLabel} · ${statusLabel}`"><span>{{ phaseLabel }}</span><span class="pomodoro-stage-sep" aria-hidden="true">·</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" :key="state.phase">{{ state.phase === 'focus' ? '留一点完整的时间,只做眼前这一件事。' : '离开屏幕,喝口水,再回来。' }}</p>
|
||||
@@ -113,10 +125,10 @@ onUnmounted(() => {
|
||||
<button v-if="state.status === 'idle'" class="primary" type="button" @click="start"><Play />开始{{ phaseLabel }}</button>
|
||||
<button v-else-if="state.status === 'running'" class="primary" type="button" @click="pause"><Pause />暂停</button>
|
||||
<button v-else-if="state.status === 'paused'" class="primary" type="button" @click="start"><Play />继续</button>
|
||||
<button v-if="state.status === 'running' || state.status === 'paused'" class="soft-button" type="button" @click="reset"><RotateCcw />重置</button>
|
||||
<button v-if="state.status !== 'idle'" class="soft-button" type="button" @click="reset"><RotateCcw />重置</button>
|
||||
<template v-if="state.phase === 'focus' && state.status === 'done'">
|
||||
<button class="primary" type="button" @click="beginBreak"><Coffee />开始休息</button>
|
||||
<button class="soft-button" type="button" @click="nextFocus"><SkipForward />跳过休息</button>
|
||||
<button class="soft-button" type="button" @click="skipRest"><SkipForward />跳过休息</button>
|
||||
</template>
|
||||
<template v-if="state.phase === 'break'">
|
||||
<button v-if="state.status !== 'done'" class="soft-button" type="button" @click="skip"><SkipForward />跳过休息</button>
|
||||
@@ -124,7 +136,26 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pomodoro-summary">专注 {{ state.focusMinutes }} 分钟 · 休息 {{ state.breakMinutes }} 分钟</p>
|
||||
<button class="pomodoro-summary" type="button" :aria-label="`计时设置:专注 ${state.focusMinutes} 分钟,休息 ${state.breakMinutes} 分钟`" @click="settingsOpen = true">专注 {{ state.focusMinutes }} 分钟 · 休息 {{ state.breakMinutes }} 分钟<Settings2 aria-hidden="true" /></button>
|
||||
|
||||
<AppSheet :open="settingsOpen" variant="actions" panel-class="pomodoro-settings-sheet" title-id="pomodoro-settings-title" initial-focus=".app-sheet__header button" @close="settingsOpen = false">
|
||||
<header class="app-sheet__header"><div><h2 id="pomodoro-settings-title">计时设置</h2></div><button type="button" aria-label="关闭计时设置" @click="settingsOpen = false"><X aria-hidden="true" /></button></header>
|
||||
<div class="app-sheet__body pomodoro-settings__body">
|
||||
<div class="pomodoro-settings__group" role="group" aria-label="专注时长">
|
||||
<span class="pomodoro-settings__label">专注时长</span>
|
||||
<div class="pomodoro-settings__options">
|
||||
<button v-for="m in focusOptions" :key="`focus-${m}`" type="button" :aria-pressed="state.focusMinutes === m" :disabled="timerLocked" @click="setFocusMinutes(m)">{{ m }} 分钟</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pomodoro-settings__group" role="group" aria-label="休息时长">
|
||||
<span class="pomodoro-settings__label">休息时长</span>
|
||||
<div class="pomodoro-settings__options">
|
||||
<button v-for="m in breakOptions" :key="`break-${m}`" type="button" :aria-pressed="state.breakMinutes === m" :disabled="timerLocked" @click="setBreakMinutes(m)">{{ m }} 分钟</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="timerLocked" class="pomodoro-settings__hint">计时进行中,暂停后可调整</p>
|
||||
</div>
|
||||
</AppSheet>
|
||||
<p class="sr-only" aria-live="polite">{{ announcement }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPomodoroState, finishPomodoro, formatPomodoroTime, loadPomodoroState, pausePomodoro, recalibratePomodoro, resetPomodoro, savePomodoroState, shouldAcceptPomodoroState, skipBreak, startBreak, startNextFocus, startPomodoro } from './pomodoro-timer'
|
||||
import { createPomodoroState, finishPomodoro, formatPomodoroTime, loadPomodoroState, pausePomodoro, recalibratePomodoro, resetPomodoro, savePomodoroState, setPomodoroDurations, shouldAcceptPomodoroState, skipBreak, startBreak, startNextFocus, startPomodoro } from './pomodoro-timer'
|
||||
|
||||
describe('pomodoro timer state', () => {
|
||||
it('starts a 25 minute focus from idle using an absolute deadline', () => {
|
||||
@@ -136,6 +136,27 @@ describe('pomodoro timer state', () => {
|
||||
expect(shouldAcceptPomodoroState(done, done)).toBe(false)
|
||||
})
|
||||
|
||||
it('applies duration changes outside an active run and resets the visible clock', () => {
|
||||
const idle = createPomodoroState(new Date('2026-09-22T08:00:00'))
|
||||
|
||||
const focusChanged = setPomodoroDurations(idle, 45, 10, Date.parse('2026-09-22T08:00:00'), 'tab-a')
|
||||
expect(focusChanged).toMatchObject({ focusMinutes: 45, breakMinutes: 10, status: 'idle', remainingMs: 45 * 60_000, revision: 1 })
|
||||
|
||||
const done = finishPomodoro(startPomodoro(idle, Date.parse('2026-09-22T08:00:00')), Date.parse('2026-09-22T08:25:00'))
|
||||
const fromDone = setPomodoroDurations(done, 30, 5, Date.parse('2026-09-22T08:26:00'), 'tab-a')
|
||||
expect(fromDone).toMatchObject({ status: 'idle', focusMinutes: 30, remainingMs: 30 * 60_000 })
|
||||
})
|
||||
|
||||
it('ignores duration changes mid-run, on identical values, and on invalid values', () => {
|
||||
const idle = createPomodoroState(new Date('2026-09-22T08:00:00'))
|
||||
const running = startPomodoro(idle, Date.parse('2026-09-22T08:00:00'))
|
||||
|
||||
expect(setPomodoroDurations(running, 45, 10, Date.parse('2026-09-22T08:01:00'), 'tab-a')).toBe(running)
|
||||
expect(setPomodoroDurations(idle, 25, 5, Date.parse('2026-09-22T08:01:00'), 'tab-a')).toBe(idle)
|
||||
expect(setPomodoroDurations(idle, 0, 10, Date.parse('2026-09-22T08:01:00'), 'tab-a')).toBe(idle)
|
||||
expect(setPomodoroDurations(idle, 45, -5, Date.parse('2026-09-22T08:01:00'), 'tab-a')).toBe(idle)
|
||||
})
|
||||
|
||||
it('formats the visible clock with a ceiling so it never shows early completion', () => {
|
||||
expect(formatPomodoroTime(25 * 60_000)).toBe('25:00')
|
||||
expect(formatPomodoroTime(60_001)).toBe('01:01')
|
||||
|
||||
@@ -101,6 +101,17 @@ export function resetPomodoro(state: PomodoroState, now = Date.now(), writerId =
|
||||
return moveToPhase(state, state.phase, now, writerId)
|
||||
}
|
||||
|
||||
export function setPomodoroDurations(state: PomodoroState, focusMinutes: number, breakMinutes: number, now = Date.now(), writerId = state.writerId): PomodoroState {
|
||||
if (state.status === 'running') return state
|
||||
if (!Number.isFinite(focusMinutes) || focusMinutes <= 0 || !Number.isFinite(breakMinutes) || breakMinutes <= 0) return state
|
||||
if (state.focusMinutes === focusMinutes && state.breakMinutes === breakMinutes) return state
|
||||
return changed({ ...state, focusMinutes, breakMinutes }, now, {
|
||||
status: 'idle',
|
||||
remainingMs: (state.phase === 'focus' ? focusMinutes : breakMinutes) * 60_000,
|
||||
endsAt: null,
|
||||
}, writerId)
|
||||
}
|
||||
|
||||
type PomodoroStorage = Pick<Storage, 'getItem' | 'setItem'>
|
||||
|
||||
function isPomodoroState(value: unknown): value is PomodoroState {
|
||||
|
||||
@@ -251,7 +251,7 @@ main:has(>.mvp-view .settings-sections) .settings-data>.inline-error{margin:var(
|
||||
@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:var(--space-18) 0 64px}.pomodoro-heading{min-height:74px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border-hairline)}.pomodoro-heading h1{margin:var(--space-4) 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(--text-secondary);font-size:12px}.pomodoro-stage{padding:var(--space-48) 0 var(--space-32);text-align:center;border-bottom:1px solid var(--border-hairline)}.pomodoro-stage-meta{display:flex;justify-content:center;align-items:baseline;gap:9px}.pomodoro-stage-meta span{font-size:14px;font-weight:700}.pomodoro-stage-meta small{color:var(--text-secondary);font-size:11px}.pomodoro-clock{display:block;margin:var(--space-18) 0 var(--space-16);font-size:clamp(68px,14vw,104px);font-weight:700;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(--text-secondary)}.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:var(--progress-track);overflow:hidden}.pomodoro-progress span{display:block;height:100%;background:var(--accent);transition:width .25s linear}.pomodoro-note{margin:var(--space-24) auto 0;color:var(--text-secondary);font-size:13px}.pomodoro-controls{min-height:52px;margin-top:var(--space-28);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:var(--space-14) 0 0;text-align:center;color:var(--text-secondary);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:var(--space-36)}.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}}
|
||||
.pomodoro-view{width:min(100%,630px);margin:0 auto;padding:var(--space-18) 0 64px}.pomodoro-heading{min-height:74px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border-hairline)}.pomodoro-heading h1{margin:var(--space-4) 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(--text-secondary);font-size:12px}.pomodoro-stage{padding:var(--space-48) 0 var(--space-32);text-align:center;border-bottom:1px solid var(--border-hairline)}.pomodoro-stage-meta{display:flex;justify-content:center;align-items:baseline;gap:9px}.pomodoro-stage-meta span{font-size:14px;font-weight:700}.pomodoro-stage-meta small{color:var(--text-secondary);font-size:11px}.pomodoro-clock{display:block;margin:var(--space-18) 0 var(--space-16);font-size:clamp(68px,14vw,104px);font-weight:700;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(--text-secondary)}.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:var(--progress-track);overflow:hidden}.pomodoro-progress span{display:block;height:100%;background:var(--accent);transition:width .25s linear}.pomodoro-note{margin:var(--space-24) auto 0;color:var(--text-secondary);font-size:13px}.pomodoro-controls{min-height:52px;margin-top:var(--space-28);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:var(--space-14) 0 0;text-align:center;color:var(--text-secondary);font-size:11px}.pomodoro-stage-meta .pomodoro-stage-sep{margin:0 2px;font-size:11px;font-weight:400;color:var(--text-secondary)}button.pomodoro-summary{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:6px;appearance:none;border:0;background:none;font:inherit;padding:0;cursor:pointer}button.pomodoro-summary::after{content:"";position:absolute;inset:-14px -12px}button.pomodoro-summary:hover{color:var(--text-primary)}button.pomodoro-summary>svg{width:13px;height:13px;opacity:.75}.pomodoro-settings__body{padding:var(--space-4) var(--space-20) var(--space-20);display:grid;gap:var(--space-18)}.pomodoro-settings__group{display:grid;gap:var(--space-8)}.pomodoro-settings__label{font-size:12px;color:var(--text-secondary)}.pomodoro-settings__options{display:flex;flex-wrap:wrap;gap:var(--space-8)}.pomodoro-settings__options button{min-height:44px;min-width:64px;padding:0 var(--space-14);border:1px solid var(--border-cream);background:var(--surface-base);border-radius:var(--radius-control);font-size:14px;font-weight:600;color:var(--text-primary);transition:background-color .14s ease,color .14s ease,border-color .14s ease}.pomodoro-settings__options button[aria-pressed="true"]{background:var(--white);color:var(--accent);border-color:var(--accent);box-shadow:0 1px 3px rgba(78,58,34,.08)}.pomodoro-settings__options button:disabled{opacity:.55;cursor:not-allowed}.pomodoro-settings__hint{margin:0;color:var(--text-secondary);font-size:12px}.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:var(--space-36)}.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(--border-cream);background:var(--surface-raised);overflow:hidden;display:flex;flex-direction:column}.shell.detail-open{transition:none}
|
||||
|
||||
Reference in New Issue
Block a user