From 0e5f756bb8536719b22691c1822021331295bcb0 Mon Sep 17 00:00:00 2001 From: do0ori Date: Wed, 19 Aug 2026 15:30:07 +0900 Subject: [PATCH 1/8] feat(routine): Simplify step editing --- .../forms/RoutineTimerForm.tsx | 40 ++- .../forms/RoutineTimerItemForm.tsx | 314 ++++++++---------- 2 files changed, 171 insertions(+), 183 deletions(-) diff --git a/src/components/timers/timer-management/forms/RoutineTimerForm.tsx b/src/components/timers/timer-management/forms/RoutineTimerForm.tsx index 2691bbd..2a9f9b8 100644 --- a/src/components/timers/timer-management/forms/RoutineTimerForm.tsx +++ b/src/components/timers/timer-management/forms/RoutineTimerForm.tsx @@ -25,6 +25,7 @@ type RoutineTimerFormProps = { const RoutineTimerForm: React.FC = ({ initialData, mode, timerType, setTimerType, close }) => { const [errorMessage, setErrorMessage] = useState(''); + const [expandedStepId, setExpandedStepId] = useState(null); const { selectedThemeCopy, defaultPointColorIndex } = useTheme(); const { selectedTheme } = useThemeStore(); const { addTimer, updateTimer } = useRoutineTimerStore(); @@ -102,6 +103,25 @@ const RoutineTimerForm: React.FC = ({ initialData, mode, ]); }; + const handleAddStep = () => { + const current = watch('items') || []; + const id = `step_${Date.now()}_${Math.random().toString(36).substring(2, 5)}`; + + setValue('items', [ + ...current, + { + id, + title: `Step ${current.length + 1}`, + time: 10, + isMinutes: true, + pointColorIndex: defaultPointColorIndex, + interval: 5, + type: TIMER_TYPE.BASE, + }, + ]); + setExpandedStepId(id); + }; + return (
{mode === 'add' && ( @@ -177,15 +197,17 @@ const RoutineTimerForm: React.FC = ({ initialData, mode,
setExpandedStepId(expandedStepId === item.id ? null : item.id)} onDelete={() => { const newItems = items.filter((_, i) => i !== index); setValue('items', newItems); + if (expandedStepId === item.id) setExpandedStepId(null); }} />
@@ -203,21 +225,7 @@ const RoutineTimerForm: React.FC = ({ initialData, mode, {/* Add Step Button */} - const containerRef = useAutoScroll(isOpen); + - return ( -
-
- - {({ open }) => { - useEffect(() => { - setIsOpen(open); - }, [open]); - return ( - <> - -
-
- -
- - e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - onBlur={(e) => { - const trimmedValue = e.target.value.trim(); - setValue(`items.${index}.title`, trimmedValue); - }} - /> - {!isOpen && ( - {`${item.time} ${item.isMinutes ? 'min' : 'sec'}`} - )} -
-
- - { - e.stopPropagation(); - onDelete(); - }} - className="shrink-0 rounded" - /> -
-
- - -
- - - setValue(`items.${index}.pointColorIndex`, colorIndex) - } - /> -
-
- -
-
- -
- setValue(`items.${index}.isMinutes`, true)} - checked={item.isMinutes} - className="form-radio" - style={{ accentColor: currentTheme.color.point }} - /> - Min -
-
- setValue(`items.${index}.isMinutes`, false)} - checked={!item.isMinutes} - className="form-radio" - style={{ accentColor: currentTheme.color.point }} - /> - Sec -
-
- setValue(`items.${index}.time`, newTime)} - text={item.title} - /> -
-
-
- - Alarm for - e.stopPropagation()} - /> - seconds -
-
-
- - ); - }} -
+ +
+ + {isOpen && ( +
+
+ + setValue(`items.${index}.title`, event.target.value.trim())} + />
-
- {!isOpen && ( -
- - {item.interval > 0 ? `${item.interval} sec Alarm` : 'No Alarm'} -
- )} +
+ +
+ + + +
+ setValue(`items.${index}.time`, time)} text={item.title} />
-
- ); + +
+ + setValue(`items.${index}.pointColorIndex`, colorIndex)} + /> +
+ +
+ + Alarm for + + seconds +
+
+ )} +
+ + ); }; export default RoutineTimerItemForm; From e5ade82dac76a94fca2b56b52dcad654947a7fd6 Mon Sep 17 00:00:00 2001 From: do0ori Date: Wed, 19 Aug 2026 16:19:28 +0900 Subject: [PATCH 2/8] feat(timers): Streamline routine creation flow --- .../timer-management/TimerListOverlay.tsx | 53 +++++++++-------- .../forms/RoutineTimerForm.tsx | 57 +++++++++---------- .../forms/RoutineTimerItemForm.tsx | 2 +- 3 files changed, 58 insertions(+), 54 deletions(-) diff --git a/src/components/timers/timer-management/TimerListOverlay.tsx b/src/components/timers/timer-management/TimerListOverlay.tsx index 11e81bc..89aa915 100644 --- a/src/components/timers/timer-management/TimerListOverlay.tsx +++ b/src/components/timers/timer-management/TimerListOverlay.tsx @@ -1,6 +1,6 @@ -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, MdPlayArrow } from 'react-icons/md'; import { getCardSurface, getTextColor } from '../../../utils/colorUtils'; import { TIMER_TYPE, TIMER_TYPE_CONFIG } from '../../../config/timer/type'; import { useOverlay } from '../../../hooks/useOverlay'; @@ -8,7 +8,7 @@ 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'; @@ -91,13 +91,14 @@ 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 [activeTab, setActiveTab] = useState<'all' | 'base' | 'routine'>('all'); const [searchQuery, setSearchQuery] = useState(''); + const [isTemplatesOpen, setIsTemplatesOpen] = useState(true); const { isOpen, close } = useOverlay('timer-list'); @@ -118,9 +119,13 @@ 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); window.location.hash = 'timer-list&timer-item'; }; @@ -159,15 +164,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; @@ -271,15 +268,23 @@ const TimerListOverlay: React.FC = () => { {/* Quick 1-Click Templates */} {!searchQuery && (
-
- - Popular Preset Templates -
-
+ + {isTemplatesOpen &&
{QUICK_TEMPLATES.map((tpl, i) => (
))} -
+
} )} diff --git a/src/components/timers/timer-management/forms/RoutineTimerForm.tsx b/src/components/timers/timer-management/forms/RoutineTimerForm.tsx index 2a9f9b8..2f134fa 100644 --- a/src/components/timers/timer-management/forms/RoutineTimerForm.tsx +++ b/src/components/timers/timer-management/forms/RoutineTimerForm.tsx @@ -1,7 +1,7 @@ import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'; import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { IoMdAdd, IoMdCheckmark, IoMdFlash } from 'react-icons/io'; +import { IoMdAdd, IoMdCheckmark } from 'react-icons/io'; import { MdOutlinePalette, MdTextFields } from 'react-icons/md'; import { TIMER_TYPE, TimerType } from '../../../../config/timer/type'; import { useTheme } from '../../../../hooks/useTheme'; @@ -132,7 +132,7 @@ const RoutineTimerForm: React.FC = ({ initialData, mode, {/* Routine Metadata & Summary Header */}
-
+
{/* Title Input */}
{/* Quick Stats Pill */} -
-
-
Total Steps
-
{items?.length || 0}
+
+
+
Timers
+
{items?.length || 0}
-
-
Total Duration
-
~{totalMinutes} mins
+
+
Duration
+
~{totalMinutes} mins
-
@@ -179,10 +172,14 @@ const RoutineTimerForm: React.FC = ({ initialData, mode,
- {/* Routine Steps List with Drag and Drop */} -
-
- + {/* Routine Steps List with Drag and Drop */} +
+
+
+ Timers in this routine +

Each timer starts after the previous one ends.

+
+ Routine Steps ({items?.length || 0}) • Drag to reorder
@@ -223,15 +220,17 @@ const RoutineTimerForm: React.FC = ({ initialData, mode, {errorMessage &&
{errorMessage}
} {/* Add Step Button */} - +
+ +
{/* Submit Button */} diff --git a/src/components/timers/timer-management/forms/RoutineTimerItemForm.tsx b/src/components/timers/timer-management/forms/RoutineTimerItemForm.tsx index 75b8f15..ec764aa 100644 --- a/src/components/timers/timer-management/forms/RoutineTimerItemForm.tsx +++ b/src/components/timers/timer-management/forms/RoutineTimerItemForm.tsx @@ -75,7 +75,7 @@ const RoutineTimerItemForm = ({
- {item.title || `Step ${index + 1}`} + {item.title || `Timer ${index + 1}`} {duration} · {alarm} From 03e2795e5d91ff6e5e078537b8bed6096cbeb7c6 Mon Sep 17 00:00:00 2001 From: do0ori Date: Wed, 19 Aug 2026 17:00:15 +0900 Subject: [PATCH 3/8] feat(timers): Streamline timer creation controls --- .../timer-management/TimerItemOverlay.tsx | 22 +++++++---- .../timer-management/TimerListOverlay.tsx | 33 +++++++++++----- .../fields/TimerTypeSelector.tsx | 20 +++++++--- .../fields/TimerUnitSelector.tsx | 33 ++++++++++++++++ .../timer-management/forms/BaseTimerForm.tsx | 39 ++----------------- .../forms/RoutineTimerForm.tsx | 13 +------ .../forms/RoutineTimerItemForm.tsx | 25 +++--------- 7 files changed, 97 insertions(+), 88 deletions(-) create mode 100644 src/components/timers/timer-management/fields/TimerUnitSelector.tsx 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 89aa915..97d730b 100644 --- a/src/components/timers/timer-management/TimerListOverlay.tsx +++ b/src/components/timers/timer-management/TimerListOverlay.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { IoMdAdd, IoMdClose, IoMdFlash, IoMdSearch } from 'react-icons/io'; import { MdDeleteOutline, MdEdit, MdExpandMore, MdHourglassTop, MdPlayArrow } 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'; @@ -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()}`, @@ -96,6 +96,7 @@ const TimerListOverlay: React.FC = () => { 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); @@ -126,6 +127,7 @@ const TimerListOverlay: React.FC = () => { const openOverlay = (timer?: TimerData, nextMode: 'add' | 'edit' = timer ? 'edit' : 'add') => { setTargetTimer(timer || null); setMode(nextMode); + setInitialTimerType(activeTab === 'routine' ? TIMER_TYPE.ROUTINE : TIMER_TYPE.BASE); window.location.hash = 'timer-list&timer-item'; }; @@ -211,23 +213,31 @@ const TimerListOverlay: React.FC = () => {
{/* Category Segmented Control */}
+
@@ -362,7 +372,7 @@ const TimerListOverlay: React.FC = () => { className="inline-block mt-1.5 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider" style={{ backgroundColor: `${selectedTheme.color.point}20`, color: selectedTheme.color.point }} > - {timer.type} + {TIMER_TYPE_CONFIG[timer.type].label}
@@ -415,7 +425,12 @@ const TimerListOverlay: React.FC = () => {
- + ); }; 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 (
+