diff --git a/CHANGELOG.md b/CHANGELOG.md index cbad7bb..19a0f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to Mellow Visual Timer are documented in this file. +## [0.4.0] - 2026-08-19 + +### Added + +- Compact, expandable routine-step editor for creating and editing sequential timers. +- Context-aware timer creation: the Basic and Routine filters open their matching timer form. + +### Changed + +- Unified timer type, duration-unit, and list-filter controls with sliding segmented controls. +- Simplified timer cards to prioritize the timer information and management actions. +- Updated the About version and README to reflect the v0.4.0 release. + +### Fixed + +- Dragging a clock face no longer scrolls the surrounding view; scrolling remains available outside the dial. + ## [0.3.0] - 2026-08-19 ### Added @@ -17,4 +34,3 @@ All notable changes to Mellow Visual Timer are documented in this file. - Combined timer direction, alarm sound, and volume settings into one tab. - Simplified the focus-statistics sharing UI and added distinct KPI emoji markers. - Migrated the app build to Vite with Vite PWA support. - diff --git a/README.md b/README.md index 1d66773..711bd8c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - **Drag** on the clock face or **click** to set your desired time quickly and easily. 2. **Create Your Own Timers**: - Save your frequently used timers as **Basic Timers** in the **Timer List**. - - Combine multiple Basic Timers into a **Routine Timer** for sequential execution, perfect for workflows or workout routines. + - Combine multiple Basic Timers into a **Routine Timer** for sequential execution, then edit, reorder, or remove its individual steps. 3. **Start the Timer**: - Press the **Play** button and watch the timer progress in real-time. - The timer will continue running even if you switch tabs or minimize the app. @@ -42,7 +42,8 @@ - Create single timers with predefined durations and custom colors. - **Routine Timer** - Group multiple Basic Timers to run sequentially. - - Customize **time intervals**, **start points**, and **repeat modes** (single or infinite loop). + - Add, edit, reorder, or remove individual timer steps, including each step's duration, color, and alarm interval. + - Start a routine once or enable repeat mode while it runs. - **Use cases**: - Pomodoro sessions (e.g., 50-minute focus + 10-minute break). - Workout routines with different exercises and rest periods. @@ -170,7 +171,9 @@ Contributions are welcome! To contribute: Feel free to open [issues](https://github.com/do0ori/visual-timer/issues) for bug reports or feature suggestions. -## Release Roadmap +## Release History - **v0.3.0:** Vite migration, focus stats, custom alarm audio, unified settings, updated themes, and version information. -- **v0.4.0 (planned):** A dedicated Routine Timer editing UX update, focused on clearer step creation, editing, reordering, and deletion. +- **v0.4.0:** A streamlined Routine Timer editing flow with clearer step creation, inline editing, reordering, deletion, context-aware timer creation, and compact timer cards. + +See [CHANGELOG.md](CHANGELOG.md) for detailed release notes. diff --git a/package.json b/package.json index 87dec11..4002f6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "visual-timer", - "version": "0.3.0", + "version": "0.4.0", "private": false, "type": "module", "homepage": "https://do0ori.github.io/visual-timer", diff --git a/src/components/common/TimerFace.tsx b/src/components/common/TimerFace.tsx index b50dc07..b21baf9 100644 --- a/src/components/common/TimerFace.tsx +++ b/src/components/common/TimerFace.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useSettingsStore } from '../../store/settingsStore'; import { Theme } from '../../store/types/theme'; import { getAdjustedColor } from '../../utils/colorUtils'; @@ -24,6 +24,17 @@ const TimerFace: React.FC = ({ }) => { const { isClockwise } = useSettingsStore(); const [isDragging, setIsDragging] = useState(false); + const dialCircleRef = useRef(null); + + useEffect(() => { + const dialCircle = dialCircleRef.current; + if (!dialCircle || !handleDragEvent) return; + + const preventTouchScroll = (event: TouchEvent) => event.preventDefault(); + dialCircle.addEventListener('touchmove', preventTouchScroll, { passive: false }); + + return () => dialCircle.removeEventListener('touchmove', preventTouchScroll); + }, [handleDragEvent]); const fullProgress = 2 * Math.PI * progressRadius; @@ -48,6 +59,7 @@ const TimerFace: React.FC = ({ {/* Timer Background */} = ({ onMouseDown={() => setIsDragging(true)} onMouseUp={() => setIsDragging(false)} onMouseMove={handleMouseMove} - onTouchStart={() => setIsDragging(true)} + onTouchStart={(e) => { + if (!handleDragEvent) return; + e.preventDefault(); + setIsDragging(true); + }} onTouchEnd={() => setIsDragging(false)} - onTouchMove={handleMouseMove} + onTouchCancel={() => setIsDragging(false)} + onTouchMove={(e) => { + if (!handleDragEvent) return; + e.preventDefault(); + handleMouseMove(e); + }} onClick={handleDragEvent} - style={{ pointerEvents: 'visiblePainted' }} + style={{ pointerEvents: 'visiblePainted', touchAction: handleDragEvent ? 'none' : 'auto' }} className="relative cursor-pointer" /> {/* Clock Ticks */} diff --git a/src/components/timers/timer-management/TimerItemOverlay.tsx b/src/components/timers/timer-management/TimerItemOverlay.tsx index 2dfcef9..9f052be 100644 --- a/src/components/timers/timer-management/TimerItemOverlay.tsx +++ b/src/components/timers/timer-management/TimerItemOverlay.tsx @@ -6,24 +6,28 @@ import { useThemeStore } from '../../../store/themeStore'; import { BaseTimerData, RoutineTimerData, TimerData } from '../../../store/types/timer'; import BaseTimerForm from './forms/BaseTimerForm'; import RoutineTimerForm from './forms/RoutineTimerForm'; +import TimerTypeSelector from './fields/TimerTypeSelector'; type TimerItemOverlayProps = { initialTimerData: TimerData | null; + initialTimerType?: TimerType; mode: 'add' | 'edit'; onClose: () => void; }; -const TimerItemOverlay: React.FC = ({ initialTimerData, mode, onClose }) => { +const TimerItemOverlay: React.FC = ({ initialTimerData, initialTimerType, mode, onClose }) => { const { selectedTheme, compColor } = useThemeStore(); - const [timerType, setTimerType] = useState(initialTimerData?.type || TIMER_TYPE.BASE); + const [timerType, setTimerType] = useState( + initialTimerData?.type || initialTimerType || TIMER_TYPE.BASE + ); const { isOpen, close } = useOverlay('timer-item', onClose); useEffect(() => { if (isOpen) { - setTimerType(initialTimerData?.type || TIMER_TYPE.BASE); + setTimerType(initialTimerData?.type || initialTimerType || TIMER_TYPE.BASE); } - }, [isOpen, initialTimerData]); + }, [isOpen, initialTimerData, initialTimerType]); if (!isOpen) return null; @@ -55,12 +59,16 @@ const TimerItemOverlay: React.FC = ({ initialTimerData, m {/* Content Body */}
+ {mode === 'add' && ( +
+ +
+ )} + {timerType === TIMER_TYPE.BASE && ( )} @@ -69,8 +77,6 @@ const TimerItemOverlay: React.FC = ({ initialTimerData, m )} diff --git a/src/components/timers/timer-management/TimerListOverlay.tsx b/src/components/timers/timer-management/TimerListOverlay.tsx index 11e81bc..d4b5848 100644 --- a/src/components/timers/timer-management/TimerListOverlay.tsx +++ b/src/components/timers/timer-management/TimerListOverlay.tsx @@ -1,14 +1,14 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { IoMdAdd, IoMdClose, IoMdFlash, IoMdSearch } from 'react-icons/io'; -import { MdDeleteOutline, MdEdit, MdHourglassTop, MdPlayArrow } from 'react-icons/md'; +import { MdDeleteOutline, MdEdit, MdExpandMore, MdHourglassTop } from 'react-icons/md'; import { getCardSurface, getTextColor } from '../../../utils/colorUtils'; -import { TIMER_TYPE, TIMER_TYPE_CONFIG } from '../../../config/timer/type'; +import { TIMER_TYPE, TIMER_TYPE_CONFIG, TimerType } from '../../../config/timer/type'; import { useOverlay } from '../../../hooks/useOverlay'; import { useBaseTimerStore } from '../../../store/baseTimerStore'; import { useRoutineTimerStore } from '../../../store/routineTimerStore'; import { useSelectedTimerStore } from '../../../store/selectedTimerStore'; import { useThemeStore } from '../../../store/themeStore'; -import { BaseTimerData, RoutineTimerData, TimerData } from '../../../store/types/timer'; +import { TimerData } from '../../../store/types/timer'; import { getTimerPointColor } from '../../../utils/themeUtils'; import Button from '../../common/Button'; import TimerItemOverlay from './TimerItemOverlay'; @@ -75,7 +75,7 @@ const QUICK_TEMPLATES: { { title: '☕ Power Nap 15m', desc: '15 minutes quick recharge', - badge: 'Single', + badge: 'Basic', badgeColor: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300', create: (idx) => ({ id: `base_nap_${Date.now()}`, @@ -91,13 +91,15 @@ const QUICK_TEMPLATES: { const TimerListOverlay: React.FC = () => { const { selectedTheme, compColor } = useThemeStore(); const selectTimer = useSelectedTimerStore((state) => state.selectTimer); - const { timers: baseTimers, addTimer: addBaseTimer, removeTimer: removeBaseTimer } = useBaseTimerStore(); - const { timers: routineTimers, addTimer: addRoutineTimer, removeTimer: removeRoutineTimer } = useRoutineTimerStore(); + const { timers: baseTimers, removeTimer: removeBaseTimer } = useBaseTimerStore(); + const { timers: routineTimers, removeTimer: removeRoutineTimer } = useRoutineTimerStore(); const [targetTimer, setTargetTimer] = useState(null); const [mode, setMode] = useState<'add' | 'edit'>('add'); + const [initialTimerType, setInitialTimerType] = useState(TIMER_TYPE.BASE); const [activeTab, setActiveTab] = useState<'all' | 'base' | 'routine'>('all'); const [searchQuery, setSearchQuery] = useState(''); + const [isTemplatesOpen, setIsTemplatesOpen] = useState(true); const { isOpen, close } = useOverlay('timer-list'); @@ -118,9 +120,14 @@ const TimerListOverlay: React.FC = () => { close(); }; - const openOverlay = (timer?: TimerData) => { + useEffect(() => { + if (isOpen) setIsTemplatesOpen(timers.length === 0); + }, [isOpen]); + + const openOverlay = (timer?: TimerData, nextMode: 'add' | 'edit' = timer ? 'edit' : 'add') => { setTargetTimer(timer || null); - setMode(timer ? 'edit' : 'add'); + setMode(nextMode); + setInitialTimerType(activeTab === 'routine' ? TIMER_TYPE.ROUTINE : TIMER_TYPE.BASE); window.location.hash = 'timer-list&timer-item'; }; @@ -159,15 +166,7 @@ const TimerListOverlay: React.FC = () => { } }; - const handleAddTemplate = (tpl: (typeof QUICK_TEMPLATES)[0]) => { - const newTimer = tpl.create(4); - if (newTimer.type === TIMER_TYPE.BASE) { - addBaseTimer(newTimer as BaseTimerData); - } else { - addRoutineTimer(newTimer as RoutineTimerData); - } - handleSelectTimer(newTimer.id); - }; + const handleCustomizeTemplate = (tpl: (typeof QUICK_TEMPLATES)[0]) => openOverlay(tpl.create(4), 'add'); if (!isOpen) return null; @@ -214,23 +213,31 @@ const TimerListOverlay: React.FC = () => {
{/* Category Segmented Control */}
+
@@ -271,15 +278,23 @@ const TimerListOverlay: React.FC = () => { {/* Quick 1-Click Templates */} {!searchQuery && (
-
- - Popular Preset Templates -
-
+ + {isTemplatesOpen &&
{QUICK_TEMPLATES.map((tpl, i) => (
))} -
+
}
)} @@ -333,10 +348,10 @@ const TimerListOverlay: React.FC = () => {
handleSelectTimer(timer.id)} - className="btn-tactile group cursor-pointer p-4 rounded-2xl border shadow-soft hover:shadow-dial transition-all flex flex-col justify-between gap-3" + className="btn-tactile group flex cursor-pointer items-center gap-3 rounded-2xl border p-4 shadow-soft transition-all hover:shadow-dial" style={{ backgroundColor: '#FFFFFF', borderColor: 'rgba(0,0,0,0.09)', color: '#1A1A1A' }} > -
+
{getTimerIcon(timer)}

@@ -353,40 +368,25 @@ const TimerListOverlay: React.FC = () => { )}m total)`}

- - {timer.type} -
- {/* Card Action Footer */} -
- e.stopPropagation()}> + - -
+ + +
))} @@ -396,7 +396,7 @@ const TimerListOverlay: React.FC = () => { {/* Mobile Bottom Bar */} -
+
- + ); }; diff --git a/src/components/timers/timer-management/fields/TimerTypeSelector.tsx b/src/components/timers/timer-management/fields/TimerTypeSelector.tsx index 9b531c6..6aae4e8 100644 --- a/src/components/timers/timer-management/fields/TimerTypeSelector.tsx +++ b/src/components/timers/timer-management/fields/TimerTypeSelector.tsx @@ -1,4 +1,4 @@ -import { NUM_TIMER_TYPES, TIMER_TYPE_CONFIG, TimerType } from '../../../../config/timer/type'; +import { TIMER_TYPE, TIMER_TYPE_CONFIG, TimerType } from '../../../../config/timer/type'; import { useThemeStore } from '../../../../store/themeStore'; import { getTextColor } from '../../../../utils/colorUtils'; import Tooltip from '../../../common/Tooltip'; @@ -14,23 +14,31 @@ const TimerTypeSelector: React.FC = ({ selectedType, onT return (
+