diff --git a/src/components/tools/about.astro b/src/components/tools/about.astro
new file mode 100644
index 0000000..1490962
--- /dev/null
+++ b/src/components/tools/about.astro
@@ -0,0 +1,34 @@
+---
+import { Container } from '../container';
+
+interface Props {
+ text: string;
+}
+
+const { text } = Astro.props;
+---
+
+
+
+ About This Tool
+ {text}
+
+
+
+
diff --git a/src/components/tools/breathing/breathing.module.css b/src/components/tools/breathing/breathing.module.css
new file mode 100644
index 0000000..fdbd99d
--- /dev/null
+++ b/src/components/tools/breathing/breathing.module.css
@@ -0,0 +1 @@
+/* WIP */
diff --git a/src/components/tools/breathing/breathing.tsx b/src/components/tools/breathing/breathing.tsx
new file mode 100644
index 0000000..cc54de4
--- /dev/null
+++ b/src/components/tools/breathing/breathing.tsx
@@ -0,0 +1,10 @@
+import { Container } from '@/components/container';
+import { Exercise } from './exercise';
+
+export function BreathingExercises() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/tools/breathing/exercise/exercise.module.css b/src/components/tools/breathing/exercise/exercise.module.css
new file mode 100644
index 0000000..05b9baa
--- /dev/null
+++ b/src/components/tools/breathing/exercise/exercise.module.css
@@ -0,0 +1,47 @@
+.exercise {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 75px 0;
+ margin-top: 12px;
+ background-color: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 8px;
+
+ & .phase {
+ font-family: var(--font-display);
+ font-size: var(--font-lg);
+ font-weight: 600;
+ }
+
+ & .circle {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: -1;
+ height: 55%;
+ aspect-ratio: 1 / 1;
+ background-image: radial-gradient(
+ var(--color-neutral-50),
+ var(--color-neutral-100)
+ );
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
+
+.selectBox {
+ width: 100%;
+ min-width: 0;
+ height: 45px;
+ padding: 0 12px;
+ margin-top: 8px;
+ font-size: var(--font-sm);
+ color: var(--color-foreground);
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 8px;
+}
diff --git a/src/components/tools/breathing/exercise/exercise.tsx b/src/components/tools/breathing/exercise/exercise.tsx
new file mode 100644
index 0000000..b3087ea
--- /dev/null
+++ b/src/components/tools/breathing/exercise/exercise.tsx
@@ -0,0 +1,122 @@
+import { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+
+import styles from './exercise.module.css';
+
+type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
+type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
+
+export function Exercise() {
+ const [selectedExercise, setSelectedExercise] =
+ useState('4-7-8 Breathing');
+
+ const getAnimationPhases = (
+ exercise: Exercise,
+ ): Array<'inhale' | 'holdInhale' | 'exhale' | 'holdExhale'> => {
+ switch (exercise) {
+ case 'Box Breathing':
+ return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
+ case 'Resonant Breathing':
+ return ['inhale', 'exhale'];
+ case '4-7-8 Breathing':
+ return ['inhale', 'holdInhale', 'exhale'];
+ default:
+ return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
+ }
+ };
+
+ const getAnimationDurations = (exercise: Exercise) => {
+ switch (exercise) {
+ case 'Box Breathing':
+ return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
+ case 'Resonant Breathing':
+ return { exhale: 5, inhale: 5 };
+ case '4-7-8 Breathing':
+ return { exhale: 8, holdInhale: 7, inhale: 4 };
+ default:
+ return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
+ }
+ };
+
+ const getLabel = (phase: Phase) => {
+ switch (phase) {
+ case 'inhale':
+ return 'Inhale';
+ case 'exhale':
+ return 'Exhale';
+ default:
+ return 'Hold';
+ }
+ };
+
+ const [phase, setPhase] = useState('inhale');
+ const [durations, setDurations] = useState(
+ getAnimationDurations(selectedExercise),
+ );
+
+ const animationVariants = {
+ exhale: {
+ transform: 'translate(-50%, -50%) scale(1)',
+ transition: { duration: durations.exhale },
+ },
+ holdExhale: {
+ transform: 'translate(-50%, -50%) scale(1)',
+ transition: { duration: durations.holdExhale || 4 },
+ },
+ holdInhale: {
+ transform: 'translate(-50%, -50%) scale(1.5)',
+ transition: { duration: durations.holdInhale || 4 },
+ },
+ inhale: {
+ transform: 'translate(-50%, -50%) scale(1.5)',
+ transition: { duration: durations.inhale },
+ },
+ };
+
+ useEffect(() => {
+ setDurations(getAnimationDurations(selectedExercise));
+ }, [selectedExercise]);
+
+ useEffect(() => {
+ const phases = getAnimationPhases(selectedExercise);
+
+ let phaseIndex = 0;
+
+ setPhase(phases[phaseIndex]);
+
+ const interval = setInterval(
+ () => {
+ phaseIndex = (phaseIndex + 1) % phases.length;
+
+ setPhase(phases[phaseIndex]);
+ },
+ (durations[phases[phaseIndex]] || 4) * 1000,
+ );
+
+ return () => clearInterval(interval);
+ }, [selectedExercise, durations]);
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/components/tools/breathing/exercise/index.ts b/src/components/tools/breathing/exercise/index.ts
new file mode 100644
index 0000000..881062d
--- /dev/null
+++ b/src/components/tools/breathing/exercise/index.ts
@@ -0,0 +1 @@
+export { Exercise } from './exercise';
diff --git a/src/components/tools/breathing/index.ts b/src/components/tools/breathing/index.ts
new file mode 100644
index 0000000..dbd185a
--- /dev/null
+++ b/src/components/tools/breathing/index.ts
@@ -0,0 +1 @@
+export { BreathingExercises } from './breathing';
diff --git a/src/components/tools/generics/button/button.module.css b/src/components/tools/generics/button/button.module.css
new file mode 100644
index 0000000..a96e112
--- /dev/null
+++ b/src/components/tools/generics/button/button.module.css
@@ -0,0 +1,34 @@
+.button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ font-size: var(--font-sm);
+ color: var(--color-foreground);
+ cursor: pointer;
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 4px;
+ outline: none;
+ transition: 0.2s;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-neutral-400);
+ outline-offset: 2px;
+ }
+
+ &:hover,
+ &:focus-visible {
+ background-color: var(--color-neutral-200);
+ }
+
+ &.smallIcon {
+ font-size: var(--font-xsm);
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.4;
+ }
+}
diff --git a/src/components/tools/generics/button/button.tsx b/src/components/tools/generics/button/button.tsx
new file mode 100644
index 0000000..5828ad6
--- /dev/null
+++ b/src/components/tools/generics/button/button.tsx
@@ -0,0 +1,33 @@
+import { Tooltip } from '@/components/tooltip';
+
+import { cn } from '@/helpers/styles';
+
+import styles from './button.module.css';
+
+interface ButtonProps {
+ disabled?: boolean;
+ icon: React.ReactElement;
+ onClick: () => void;
+ smallIcon?: boolean;
+ tooltip: string;
+}
+
+export function Button({
+ disabled = false,
+ icon,
+ onClick,
+ smallIcon,
+ tooltip,
+}: ButtonProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/tools/generics/button/index.ts b/src/components/tools/generics/button/index.ts
new file mode 100644
index 0000000..a039b75
--- /dev/null
+++ b/src/components/tools/generics/button/index.ts
@@ -0,0 +1 @@
+export { Button } from './button';
diff --git a/src/components/tools/hero.astro b/src/components/tools/hero.astro
new file mode 100644
index 0000000..a502ae4
--- /dev/null
+++ b/src/components/tools/hero.astro
@@ -0,0 +1,114 @@
+---
+import { Container } from '../container';
+
+interface Props {
+ desc: string;
+ title: string;
+}
+
+const { desc, title } = Astro.props;
+---
+
+
+
+
+
+

+
+
{title}
+
{desc}
+
+
Part of Moodist
+
+
+
+
+
diff --git a/src/components/tools/notepad/button/button.module.css b/src/components/tools/notepad/button/button.module.css
new file mode 100644
index 0000000..4a4e756
--- /dev/null
+++ b/src/components/tools/notepad/button/button.module.css
@@ -0,0 +1,45 @@
+.button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ font-size: var(--font-sm);
+ color: var(--color-foreground);
+ cursor: pointer;
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 4px;
+ outline: none;
+ transition: 0.2s;
+ transition-property: border-color, color, background-color;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-neutral-400);
+ outline-offset: 2px;
+ }
+
+ &.critical {
+ color: #f43f5e;
+ border-color: #f43f5e;
+
+ &:hover {
+ background-color: rgb(244 63 94 / 10%);
+ }
+ }
+
+ &.recommended {
+ font-size: var(--font-xsm);
+ color: #22c55e;
+ border-color: #22c55e;
+
+ &:hover {
+ background-color: rgb(34 197 94 / 10%);
+ }
+ }
+
+ &:hover,
+ &:focus-visible {
+ background-color: var(--color-neutral-200);
+ }
+}
diff --git a/src/components/tools/notepad/button/button.tsx b/src/components/tools/notepad/button/button.tsx
new file mode 100644
index 0000000..d3da910
--- /dev/null
+++ b/src/components/tools/notepad/button/button.tsx
@@ -0,0 +1,36 @@
+import { Tooltip } from '@/components/tooltip';
+
+import { cn } from '@/helpers/styles';
+
+import styles from './button.module.css';
+
+interface ButtonProps {
+ critical?: boolean;
+ icon: React.ReactElement;
+ onClick: () => void;
+ recommended?: boolean;
+ tooltip: string;
+}
+
+export function Button({
+ critical,
+ icon,
+ onClick,
+ recommended,
+ tooltip,
+}: ButtonProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/tools/notepad/button/index.ts b/src/components/tools/notepad/button/index.ts
new file mode 100644
index 0000000..a039b75
--- /dev/null
+++ b/src/components/tools/notepad/button/index.ts
@@ -0,0 +1 @@
+export { Button } from './button';
diff --git a/src/components/tools/notepad/index.ts b/src/components/tools/notepad/index.ts
new file mode 100644
index 0000000..8a3fad5
--- /dev/null
+++ b/src/components/tools/notepad/index.ts
@@ -0,0 +1 @@
+export { Notepad } from './notepad';
diff --git a/src/components/tools/notepad/notepad.module.css b/src/components/tools/notepad/notepad.module.css
new file mode 100644
index 0000000..2dadc40
--- /dev/null
+++ b/src/components/tools/notepad/notepad.module.css
@@ -0,0 +1,44 @@
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+
+ & .label {
+ font-size: var(--font-sm);
+ font-weight: 500;
+ color: var(--color-foreground-subtle);
+ }
+
+ & .buttons {
+ display: flex;
+ column-gap: 4px;
+ align-items: center;
+ }
+}
+
+.textarea {
+ width: 100%;
+ height: 350px;
+ padding: 12px;
+ line-height: 1.6;
+ color: var(--color-foreground-subtle);
+ resize: none;
+ background-color: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 4px;
+ outline: none;
+ scroll-padding-bottom: 12px;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-neutral-400);
+ outline-offset: 2px;
+ }
+}
+
+.counter {
+ margin-top: 8px;
+ font-size: var(--font-xsm);
+ color: var(--color-foreground-subtle);
+ text-align: center;
+}
diff --git a/src/components/tools/notepad/notepad.tsx b/src/components/tools/notepad/notepad.tsx
new file mode 100644
index 0000000..921556f
--- /dev/null
+++ b/src/components/tools/notepad/notepad.tsx
@@ -0,0 +1,90 @@
+import { useRef, useEffect } from 'react';
+import { BiTrash } from 'react-icons/bi/index';
+import { LuCopy, LuDownload } from 'react-icons/lu/index';
+import { FaCheck } from 'react-icons/fa6/index';
+import { FaUndo } from 'react-icons/fa/index';
+
+import { Modal } from '@/components/modal';
+import { Button } from './button';
+
+import { useNoteStore } from '@/stores/note';
+import { useCopy } from '@/hooks/use-copy';
+import { download } from '@/helpers/download';
+
+import styles from './notepad.module.css';
+
+interface NotepadProps {
+ onClose: () => void;
+ show: boolean;
+}
+
+export function Notepad({ onClose, show }: NotepadProps) {
+ const textareaRef = useRef(null);
+
+ const note = useNoteStore(state => state.note);
+ const history = useNoteStore(state => state.history);
+ const write = useNoteStore(state => state.write);
+ const words = useNoteStore(state => state.words());
+ const characters = useNoteStore(state => state.characters());
+ const clear = useNoteStore(state => state.clear);
+ const restore = useNoteStore(state => state.restore);
+
+ const { copy, copying } = useCopy();
+
+ useEffect(() => {
+ if (show && textareaRef.current) {
+ setTimeout(() => {
+ textareaRef.current?.focus();
+ }, 10);
+ }
+ }, [show]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ e.stopPropagation();
+
+ if (e.key === 'Escape') onClose();
+ };
+
+ return (
+
+
+ Your Note
+
+ : }
+ tooltip="Copy Note"
+ onClick={() => copy(note)}
+ />
+ }
+ tooltip="Download Note"
+ onClick={() => download('Moodit Note.txt', note)}
+ />
+ : }
+ recommended={!!history}
+ tooltip={history ? 'Restore Note' : 'Clear Note'}
+ onClick={() => (history ? restore() : clear())}
+ />
+
+
+
+
+ );
+}
diff --git a/src/components/tools/pomodoro/index.ts b/src/components/tools/pomodoro/index.ts
new file mode 100644
index 0000000..9b721ae
--- /dev/null
+++ b/src/components/tools/pomodoro/index.ts
@@ -0,0 +1 @@
+export { Pomodoro } from './pomodoro';
diff --git a/src/components/tools/pomodoro/pomodoro.module.css b/src/components/tools/pomodoro/pomodoro.module.css
new file mode 100644
index 0000000..33109cf
--- /dev/null
+++ b/src/components/tools/pomodoro/pomodoro.module.css
@@ -0,0 +1,36 @@
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+
+ & .title {
+ font-size: var(--font-sm);
+ font-weight: 500;
+ color: var(--color-foreground-subtle);
+ }
+
+ & .buttons {
+ display: flex;
+ column-gap: 4px;
+ align-items: center;
+ }
+}
+
+.control {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 8px;
+
+ & .completed {
+ font-size: var(--font-xsm);
+ color: var(--color-foreground-subtle);
+ }
+
+ & .buttons {
+ display: flex;
+ column-gap: 4px;
+ align-items: center;
+ }
+}
diff --git a/src/components/tools/pomodoro/pomodoro.tsx b/src/components/tools/pomodoro/pomodoro.tsx
new file mode 100644
index 0000000..3f612d6
--- /dev/null
+++ b/src/components/tools/pomodoro/pomodoro.tsx
@@ -0,0 +1,179 @@
+import { useState, useEffect, useRef, useMemo } from 'react';
+import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
+import { IoMdSettings } from 'react-icons/io/index';
+
+import { Modal } from '@/components/modal';
+import { Button } from '../generics/button';
+import { Timer } from '@/components/timer';
+import { Tabs } from './tabs';
+import { Setting } from './setting';
+
+import { useLocalStorage } from '@/hooks/use-local-storage';
+import { useSoundEffect } from '@/hooks/use-sound-effect';
+import { usePomodoroStore } from '@/stores/pomodoro';
+import { useCloseListener } from '@/hooks/use-close-listener';
+
+import styles from './pomodoro.module.css';
+
+interface PomodoroProps {
+ onClose: () => void;
+ open: () => void;
+ show: boolean;
+}
+
+export function Pomodoro({ onClose, open, show }: PomodoroProps) {
+ const [showSetting, setShowSetting] = useState(false);
+
+ const [selectedTab, setSelectedTab] = useState('pomodoro');
+
+ const running = usePomodoroStore(state => state.running);
+ const setRunning = usePomodoroStore(state => state.setRunning);
+
+ const [timer, setTimer] = useState(0);
+ const interval = useRef | null>(null);
+
+ const alarm = useSoundEffect('/sounds/alarm.mp3');
+
+ const defaultTimes = useMemo(
+ () => ({
+ long: 15 * 60,
+ pomodoro: 25 * 60,
+ short: 5 * 60,
+ }),
+ [],
+ );
+
+ const [times, setTimes] = useLocalStorage>(
+ 'moodist-pomodoro-setting',
+ defaultTimes,
+ );
+
+ const [completions, setCompletions] = useState>({
+ long: 0,
+ pomodoro: 0,
+ short: 0,
+ });
+
+ const tabs = useMemo(
+ () => [
+ { id: 'pomodoro', label: 'Pomodoro' },
+ { id: 'short', label: 'Break' },
+ { id: 'long', label: 'Long Break' },
+ ],
+ [],
+ );
+
+ useCloseListener(() => setShowSetting(false));
+
+ useEffect(() => {
+ if (running) {
+ if (interval.current) clearInterval(interval.current);
+
+ interval.current = setInterval(() => {
+ setTimer(prev => prev - 1);
+ }, 1000);
+ } else {
+ if (interval.current) clearInterval(interval.current);
+ }
+ }, [running]);
+
+ useEffect(() => {
+ if (timer <= 0 && running) {
+ if (interval.current) clearInterval(interval.current);
+
+ alarm.play();
+
+ setRunning(false);
+ setCompletions(prev => ({
+ ...prev,
+ [selectedTab]: prev[selectedTab] + 1,
+ }));
+ }
+ }, [timer, selectedTab, running, setRunning, alarm]);
+
+ useEffect(() => {
+ const time = times[selectedTab] || 10;
+
+ if (interval.current) clearInterval(interval.current);
+
+ setRunning(false);
+ setTimer(time);
+ }, [selectedTab, times, setRunning]);
+
+ const toggleRunning = () => {
+ if (running) setRunning(false);
+ else if (timer <= 0) {
+ const time = times[selectedTab] || 10;
+
+ setTimer(time);
+ setRunning(true);
+ } else setRunning(true);
+ };
+
+ const restart = () => {
+ if (interval.current) clearInterval(interval.current);
+
+ const time = times[selectedTab] || 10;
+
+ setRunning(false);
+ setTimer(time);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {completions[selectedTab] || 0} completed
+
+
+ }
+ smallIcon
+ tooltip="Restart"
+ onClick={restart}
+ />
+ : }
+ smallIcon
+ tooltip={running ? 'Pause' : 'Start'}
+ onClick={toggleRunning}
+ />
+
+
+
+
+ {
+ setShowSetting(false);
+ setTimes(times);
+ open();
+ }}
+ onClose={() => {
+ setShowSetting(false);
+ open();
+ }}
+ />
+ >
+ );
+}
diff --git a/src/components/tools/pomodoro/setting/index.ts b/src/components/tools/pomodoro/setting/index.ts
new file mode 100644
index 0000000..0394f8b
--- /dev/null
+++ b/src/components/tools/pomodoro/setting/index.ts
@@ -0,0 +1 @@
+export { Setting } from './setting';
diff --git a/src/components/tools/pomodoro/setting/setting.module.css b/src/components/tools/pomodoro/setting/setting.module.css
new file mode 100644
index 0000000..89583ef
--- /dev/null
+++ b/src/components/tools/pomodoro/setting/setting.module.css
@@ -0,0 +1,76 @@
+.title {
+ margin-bottom: 16px;
+ font-family: var(--font-heading);
+ font-size: var(--font-md);
+ font-weight: 600;
+}
+
+& .form {
+ display: flex;
+ flex-direction: column;
+
+ & .field {
+ display: flex;
+ flex-direction: column;
+ row-gap: 8px;
+ margin-bottom: 16px;
+
+ & .label {
+ font-size: var(--font-sm);
+ color: var(--color-foreground);
+
+ & span {
+ color: var(--color-foreground-subtle);
+ }
+ }
+
+ & .input {
+ display: block;
+ height: 40px;
+ padding: 0 8px;
+ color: var(--color-foreground);
+ background-color: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 4px;
+ outline: none;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-neutral-400);
+ outline-offset: 2px;
+ }
+ }
+ }
+
+ & .buttons {
+ display: flex;
+ column-gap: 8px;
+ align-items: center;
+ justify-content: flex-end;
+
+ & button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px;
+ padding: 0 16px;
+ font-size: var(--font-sm);
+ font-weight: 500;
+ color: var(--color-foreground);
+ cursor: pointer;
+ background-color: var(--color-neutral-200);
+ border: none;
+ border-radius: 4px;
+ outline: none;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-neutral-400);
+ outline-offset: 2px;
+ }
+
+ &.primary {
+ color: var(--color-neutral-100);
+ background-color: var(--color-neutral-950);
+ }
+ }
+ }
+}
diff --git a/src/components/tools/pomodoro/setting/setting.tsx b/src/components/tools/pomodoro/setting/setting.tsx
new file mode 100644
index 0000000..8db1a93
--- /dev/null
+++ b/src/components/tools/pomodoro/setting/setting.tsx
@@ -0,0 +1,110 @@
+import { useEffect, useState } from 'react';
+
+import { Modal } from '@/components/modal';
+
+import styles from './setting.module.css';
+
+interface SettingProps {
+ onChange: (newTimes: Record) => void;
+ onClose: () => void;
+ show: boolean;
+ times: Record;
+}
+
+export function Setting({ onChange, onClose, show, times }: SettingProps) {
+ const [values, setValues] = useState>(times);
+
+ useEffect(() => {
+ if (show) setValues(times);
+ }, [times, show]);
+
+ const handleChange = (id: string) => (value: number | string) => {
+ setValues(prev => ({
+ ...prev,
+ [id]: typeof value === 'number' ? value * 60 : '',
+ }));
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const newValues: Record = {};
+
+ Object.keys(values).forEach(name => {
+ newValues[name] =
+ typeof values[name] === 'number' ? values[name] : times[name];
+ });
+
+ onChange(newValues);
+ };
+
+ const handleCancel = (e: React.MouseEvent) => {
+ e.preventDefault();
+
+ onClose();
+ };
+
+ return (
+
+ Change Times
+
+
+
+ );
+}
+
+interface FieldProps {
+ id: string;
+ label: string;
+ onChange: (value: number | string) => void;
+ value: number | string;
+}
+
+function Field({ id, label, onChange, value }: FieldProps) {
+ return (
+
+
+ {
+ onChange(e.target.value === '' ? '' : Number(e.target.value));
+ }}
+ />
+
+ );
+}
diff --git a/src/components/tools/pomodoro/tabs/index.ts b/src/components/tools/pomodoro/tabs/index.ts
new file mode 100644
index 0000000..81aabb7
--- /dev/null
+++ b/src/components/tools/pomodoro/tabs/index.ts
@@ -0,0 +1 @@
+export { Tabs } from './tabs';
diff --git a/src/components/tools/pomodoro/tabs/tabs.module.css b/src/components/tools/pomodoro/tabs/tabs.module.css
new file mode 100644
index 0000000..222164c
--- /dev/null
+++ b/src/components/tools/pomodoro/tabs/tabs.module.css
@@ -0,0 +1,43 @@
+.tabs {
+ display: flex;
+ column-gap: 4px;
+ align-items: center;
+ padding: 4px;
+ margin: 8px 0;
+ background-color: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 8px;
+
+ & .tab {
+ display: flex;
+ flex-grow: 1;
+ align-items: center;
+ justify-content: center;
+ height: 45px;
+ font-size: var(--font-sm);
+ color: var(--color-foreground-subtle);
+ cursor: pointer;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ outline: none;
+ transition: 0.2s;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-neutral-400);
+ outline-offset: 2px;
+ }
+
+ &.selected {
+ color: var(--color-foreground);
+ background-color: var(--color-neutral-200);
+ border-color: var(--color-neutral-300);
+ }
+
+ &:not(.selected):hover,
+ &:not(.selected):focus-visible {
+ color: var(--color-foreground);
+ background-color: var(--color-neutral-100);
+ }
+ }
+}
diff --git a/src/components/tools/pomodoro/tabs/tabs.tsx b/src/components/tools/pomodoro/tabs/tabs.tsx
new file mode 100644
index 0000000..728bc5e
--- /dev/null
+++ b/src/components/tools/pomodoro/tabs/tabs.tsx
@@ -0,0 +1,25 @@
+import { cn } from '@/helpers/styles';
+
+import styles from './tabs.module.css';
+
+interface TabsProps {
+ onSelect: (id: string) => void;
+ selectedTab: string;
+ tabs: Array<{ id: string; label: string }>;
+}
+
+export function Tabs({ onSelect, selectedTab, tabs }: TabsProps) {
+ return (
+
+ {tabs.map(tab => (
+
+ ))}
+
+ );
+}
diff --git a/src/pages/tools/breathing-exercises.astro b/src/pages/tools/breathing-exercises.astro
new file mode 100644
index 0000000..9331e92
--- /dev/null
+++ b/src/pages/tools/breathing-exercises.astro
@@ -0,0 +1,24 @@
+---
+import Layout from '@/layouts/layout.astro';
+
+import Donate from '@/components/donate.astro';
+import Hero from '@/components/tools/hero.astro';
+import { BreathingExercises as Breathing } from '@/components/tools/breathing';
+import Footer from '@/components/footer.astro';
+import About from '@/components/tools/about.astro';
+import Source from '@/components/source.astro';
+---
+
+
+
+
+
+
+
+
+