254 Commits

Author SHA1 Message Date
MAZE
df210a1246 chore(release): 2.0.1 2025-03-25 17:54:27 +03:30
MAZE
4895a7266d fix: add delay to cipher text 2025-03-25 17:54:12 +03:30
MAZE
87f64e6574 chore(release): 2.0.0 2025-03-25 17:27:40 +03:30
MAZE
496c831552 fix: correct link 2025-03-25 17:20:09 +03:30
MAZE
c5adffb4d7 chore: comment out the banner 2025-03-25 17:19:48 +03:30
MAZE
536db4cd15 style: minor changes 2025-03-25 17:18:40 +03:30
MAZE
761c730129 feat: add PWA 2025-03-25 17:09:19 +03:30
MAZE
11e0ba2f93 style: change other assets 2025-03-25 16:42:27 +03:30
MAZE
4a92d2f1c1 style: change logo 2025-03-25 16:32:54 +03:30
MAZE
99e694161f fix: remove dropdown menu item from slider 2025-03-17 14:18:24 +03:30
MAZE
3d1d45cd49 feat: change logos 2025-03-17 13:32:24 +03:30
MAZE
309dd89a8c chore: add library sound 2025-02-18 20:14:57 +03:30
MAZE
699f49bfa3 feat: add binary animation 2025-02-14 15:20:17 +03:30
MAZE
29bebb3ec7 feat: add cipher animation 2025-02-14 15:14:53 +03:30
MAZE
7a47282165 chore: update logos 2025-02-13 20:35:40 +03:30
MAZE
2b85b276eb chore: update logos 2025-02-13 20:32:28 +03:30
MAZE ✧
0a1bf16d18 Merge pull request #54 from underoot/feature/media-controls
feat: media session support
2025-02-13 20:09:11 +03:30
MAZE
10259d013f feat: better heading 2025-02-13 20:04:10 +03:30
MAZE
e61307a302 feat: use custom slider in binaural and isochronic 2025-02-13 19:53:39 +03:30
MAZE
cb340c53a3 feat: add custom checkbox 2025-02-13 19:47:54 +03:30
MAZE
3b77c12114 feat: add custom slider 2025-02-13 19:43:21 +03:30
MAZE
b8ed79f48a feat: remove pre-made binaurals 2025-02-13 19:38:44 +03:30
Aleksandr Shoronov
d3a9f1ddba Removed Media Controls menu item 2025-01-26 10:57:51 +02:00
Aleksandr Shoronov
18ed2e6f05 feat: media session support 2025-01-18 14:06:37 +02:00
MAZE
3b829fce07 feat: add global volume 2024-10-13 21:54:52 +03:30
MAZE
e77c67bc24 fix: better implement shortcuts 2024-10-13 21:32:58 +03:30
MAZE
14c331ab6e fix: add default value 2024-09-13 19:59:29 +03:30
MAZE
5c536786ea style: add style to generators 2024-09-13 15:58:03 +03:30
MAZE
2e1fce4669 style: change icons 2024-09-13 15:28:04 +03:30
MAZE
d759064373 feat: add isochronic tone generator without styles 2024-09-13 15:17:20 +03:30
MAZE
f40e8206f8 feat: add binaural beat generator without styles 2024-09-13 14:55:04 +03:30
MAZE
d2e289e5d5 feat: add more sounds 2024-09-03 18:49:47 +03:30
MAZE
a59db41dc5 feat: change and add shortcuts 2024-09-03 18:30:24 +03:30
MAZE
554309ebd8 feat: add more sounds 2024-09-03 18:27:30 +03:30
MAZE
be38b92647 feat: add more sounds 2024-09-03 18:12:33 +03:30
MAZE
b497d16fd8 feat: add more sounds 2024-09-03 17:51:19 +03:30
MAZE
ace0d6eecc feat: add confetti 2024-09-01 13:07:19 +04:30
MAZE
aa8161aac5 feat: add done counter 2024-09-01 13:00:10 +04:30
MAZE
c6cc61a17f feat: add header to todos 2024-09-01 12:57:59 +04:30
MAZE
7f3ac26b98 style: minor changes 2024-09-01 12:48:15 +04:30
MAZE
6a4dc1ed95 feat: bring back all tools 2024-09-01 12:44:27 +04:30
MAZE
4cc85975e5 style: minor changes 2024-09-01 12:29:56 +04:30
MAZE
d42eb25f7b fix: disable the sleep timer when no sound is selected 2024-08-31 20:43:22 +03:30
MAZE
4f45279938 feat: change shortcuts 2024-08-31 20:41:22 +03:30
MAZE
105f53ea02 feat: add reverse timer 2024-08-31 19:39:31 +03:30
MAZE
f3cea66847 refactor: relocate folders 2024-08-31 19:25:51 +03:30
MAZE
a4a31dd43e refactor: remove extra hook 2024-08-31 19:22:07 +03:30
MAZE
973e0df6fb feat: remove all extra tools 2024-08-31 19:19:42 +03:30
MAZE
13d26b3337 feat: remove lofi modal 2024-08-31 19:11:58 +03:30
MAZE
e1de5c48b2 feat: bring back all tools 2024-08-31 19:05:12 +03:30
MAZE
07f37ef17f feat: add desktop notice 2024-08-31 18:22:46 +03:30
MAZE
bb39b4ba98 feat: add lofi radios 2024-08-31 18:15:24 +03:30
MAZE
76fdc74710 feat: remove the breathing exercises 2024-08-31 17:49:00 +03:30
MAZE
41845ffe5e style: remove animation on change 2024-08-31 14:34:10 +04:30
MAZE
48a85b2601 feat: add animation for labels 2024-08-31 14:02:11 +04:30
MAZE
5865fc867d feat: add timer for breathing exercises 2024-08-31 13:58:44 +04:30
MAZE
b27f24d374 style: minor changes 2024-08-31 13:26:10 +04:30
MAZE
5c9a2aa23a style: change border radius 2024-08-31 13:09:55 +04:30
MAZE
12d3255d57 style: fix pointer event 2024-08-31 13:00:04 +04:30
MAZE
c12ef12b79 style: remove pointer event 2024-08-31 12:55:55 +04:30
MAZE
ba3cd5ca5b style: add binary pattern 2024-08-31 12:49:40 +04:30
MAZE
a3b794d974 feat: add breathing exercise shortcut 2024-08-31 00:23:21 +03:30
MAZE
3ef4a076a2 fix: remove extra hook 2024-08-31 00:21:08 +03:30
MAZE
1f2b6b952c feat: add breathing exercise 2024-08-31 00:19:55 +03:30
MAZE
2bbdc7e09e feat: remove all tools 2024-08-31 00:10:58 +03:30
MAZE
47a63a774e feat: add store to the notepad 2024-08-30 23:38:37 +03:30
MAZE
edd53d8102 feat: add countdown timer 2024-08-30 23:35:58 +03:30
MAZE
302a71cdc6 style: minor change 2024-08-30 23:18:15 +03:30
MAZE
b73fd0b16e style: minor change 2024-08-30 23:17:13 +03:30
MAZE
5b3972b347 feat: add Moodist description to tools 2024-08-30 22:26:57 +03:30
MAZE
bee391acfe feat: add pomodoro timer tool 2024-08-30 22:21:23 +03:30
MAZE
1fd02f927c feat: add notepad tool page 2024-08-30 22:14:15 +03:30
MAZE
d56f8be448 refactor: refactor the breathing tool 2024-08-30 22:10:21 +03:30
MAZE
eee755378a feat: add breathing exercises and other tools 2024-08-30 22:09:07 +03:30
MAZE
4b015016e7 style: change logo color 2024-08-30 17:51:51 +03:30
MAZE
251f30930c feat: change shortcuts 2024-08-30 17:17:46 +03:30
MAZE
a29e2c20e4 style: minor changes 2024-08-30 17:14:29 +03:30
MAZE
1cf9a85e13 style: center icons 2024-08-30 17:09:07 +03:30
MAZE
69eb8832da style: add pattern 2024-08-30 17:01:30 +03:30
MAZE
c1ece582f4 feat: add new logo 2024-08-30 16:42:41 +03:30
MAZE
b32d8b2803 feat: remove all tools 2024-08-30 16:33:20 +03:30
MAZE
1768ba1548 feat: update the menu items 2024-08-30 15:30:22 +03:30
MAZE
a80289db57 feat: add notepad tool 2024-08-30 15:24:39 +03:30
MAZE
9208663050 style: change description 2024-08-30 15:20:28 +03:30
MAZE
d2edeb48be feat: add pomodoro timer 2024-08-30 15:19:35 +03:30
MAZE
27f25785e1 feat: add breathing exercises tool 2024-08-30 15:12:11 +03:30
MAZE ✧
f526f97908 Merge pull request #39 from arapl3y/feature/fix-modal-layout-shift
feat: fix modal and scrollbar layout shift
2024-08-03 21:35:17 +03:30
Alex Rapley
e399673462 feat: fix modal and scrollbar layout shift 2024-08-02 16:40:08 +10:00
MAZE
3d83a1427f perf: improve the breathing cricle 2024-07-06 19:37:30 +03:30
MAZE ✧
ddf929f4c0 Merge pull request #38 from SuperMeepBoy/accessbility-ignore-icons-screen-reader
accessibility: ignore icons for screen readers
2024-07-06 19:34:29 +03:30
MAZE
5ffb06be03 refactor: remove the timer store 2024-07-06 19:31:32 +03:30
MAZE
d6ed3fd251 feat: remove the countdown timer 2024-07-06 19:29:52 +03:30
MAZE
0052b917a8 style: reorder menu items 2024-07-04 20:02:46 +03:30
MAZE
9e38a8fd7d style: change gradient 2024-07-01 20:28:23 +03:30
MAZE
60cb453847 feat: add shortcut for breathing exercise 2024-07-01 18:54:09 +03:30
MAZE
fc4f52146e feat: add simple breathing exercise tool 2024-07-01 18:50:12 +03:30
MAZE
1a1359c989 fix: icons path 2024-06-25 20:02:19 +04:30
MAZE
a6c7ac41ad feat: replace reverse timer 2024-06-25 20:01:10 +04:30
MAZE
3e11fb6123 feat: add move up and down functionality 2024-06-25 19:56:04 +04:30
Jef Roelandt
ee0a28b296 accessibility: ignore icons for screen readers 2024-06-19 23:08:39 +02:00
MAZE
d356d77aa9 test: write tests for motion lib 2024-06-19 14:26:23 +04:30
MAZE
9cc0ccd325 test: write more tests 2024-06-19 14:23:27 +04:30
MAZE
cad85c7667 test: write tests for random helper 2024-06-19 14:18:47 +04:30
MAZE
def9a57e0c test: add Vitest and some tests 2024-06-19 14:12:06 +04:30
MAZE
74f6b5851d feat: scroll into view after marking favorite 2024-06-17 21:01:53 +04:30
MAZE
f4c66e3092 feat: scroll the new timer into view 2024-06-16 22:22:32 +03:30
MAZE
28abc16b9c style: remove animations 2024-06-16 22:14:47 +03:30
MAZE
787a9b60b5 style: add animation to presets 2024-06-16 22:14:44 +04:30
MAZE
73a5c21be9 chore: add animation to countdown timer 2024-06-16 22:12:12 +04:30
MAZE
cfd2744e92 chore: add toolbox copy 2024-06-16 21:06:26 +04:30
MAZE
4c0f417469 feat: add persist mode to the modal 2024-06-16 19:32:40 +03:30
MAZE
9d1d8f8035 style: change notice 2024-06-16 18:51:23 +03:30
MAZE
8a79ccf018 style: change button style 2024-06-16 18:50:06 +03:30
MAZE
a3c384d105 style: add title to timer 2024-06-16 18:49:15 +03:30
MAZE
96ca376885 style: increase menu width 2024-06-16 18:44:35 +03:30
MAZE
18987cc339 style: add min width 2024-06-16 19:41:09 +04:30
MAZE
919831538f style: change item order 2024-06-16 19:28:29 +04:30
MAZE
edd15f4b9a fix: change shortcuts 2024-06-16 19:27:33 +04:30
MAZE
09c0a6ce93 fix: change icon path 2024-06-16 19:23:54 +04:30
MAZE
2bfb9b181c feat: implement countdown timer functionality 2024-06-16 19:19:22 +04:30
MAZE
c272914416 feat: add basic form 2024-06-16 19:00:38 +04:30
MAZE
d73b2bc1ff refactor: rename components 2024-06-16 18:47:57 +04:30
MAZE
c5657d0642 feat: add countdown timer structure 2024-06-16 18:40:13 +04:30
MAZE
c35409ce0a refactor: separate the migration 2024-06-16 18:09:51 +04:30
MAZE
7658842324 refactor: use the ID instead of index 2024-06-16 17:42:44 +04:30
MAZE
78222be011 feat: add ID to presets 2024-06-16 17:39:44 +04:30
MAZE
2c8135db43 refactor: add description for events 2024-06-15 13:36:10 +04:30
MAZE
fddf75cdca refactor: write JSDoc for libs 2024-06-15 13:32:00 +04:30
MAZE
0f50e6ae8b refactor: add JSDoc for custom hooks 2024-06-15 13:19:00 +04:30
MAZE
4ae0504937 refactor: add JSDoc for helper functions 2024-06-15 13:06:48 +04:30
MAZE
af075b32e6 style: add focus state 2024-06-15 12:58:24 +04:30
MAZE
82d8240b97 feat: add active indicator for sleep timer 2024-06-15 12:55:45 +04:30
MAZE
096251ec0a refactor: change stores structure 2024-06-15 12:44:46 +04:30
MAZE
2a86a88ed6 refactor: rename stores folder 2024-06-15 12:36:47 +04:30
MAZE
c60dcc74ed chore(release): 1.5.1 2024-06-14 20:53:41 +03:30
MAZE
aca746148e Merge branch 'develop' 2024-06-14 20:53:14 +03:30
MAZE
095e3c795e chore: add more sounds 2024-06-14 20:51:35 +03:30
MAZE
7e65bb75f9 chore: add washing machine sound 2024-06-14 20:36:41 +03:30
MAZE
0533460667 refactor: rename some functions 2024-06-14 19:08:00 +04:30
MAZE
9d633a9637 refactor: use nullish operator 2024-06-14 18:57:13 +04:30
MAZE
a9fe7f7b4f chore: update README file 2024-06-12 23:42:28 +03:30
MAZE
ffe260f4a0 refactor: migrate to Astro components 2024-05-20 13:14:42 +04:30
MAZE
78656bb61f chore(release): 1.5.0 2024-05-19 17:25:10 +03:30
MAZE
629f0a514e chore: update README file 2024-05-18 23:31:49 +03:30
MAZE
9338b1d30a fix: remove media session 2024-05-11 16:48:44 +03:30
MAZE
34d3f07581 feat: add media session (wip) 2024-05-11 16:44:22 +03:30
MAZE
cf4870b0d6 feat: add media session (wip) 2024-05-11 16:38:26 +03:30
MAZE
9f0a28d930 feat: add media session (wip) 2024-05-11 16:35:07 +03:30
MAZE
56b0e9bf1a feat: add media session (wip) 2024-05-11 16:31:32 +03:30
MAZE
4f752bb6d0 feat: add media session (wip) 2024-05-11 16:25:14 +03:30
MAZE
1547b0a436 feat: add media session (wip) 2024-05-11 16:20:48 +03:30
MAZE
9ad16306cf chore: remove extra sound 2024-05-11 16:49:55 +04:30
MAZE
4b73e45dd4 Merge branch 'main' of https://github.com/remvze/moodist into main 2024-05-11 16:46:43 +04:30
MAZE
05b298f51e Merge branch 'main' into develop 2024-05-11 16:45:58 +04:30
MAZE
8d01d74bd3 fix: remove media session 2024-05-11 16:45:13 +04:30
MAZE
f311ec114e feat: add media session (wip) 2024-05-11 16:26:25 +04:30
MAZE
df1b05f7ce feat: add media session (wip) 2024-05-11 16:20:26 +04:30
MAZE
ea0dfff9c1 feat: add media session (wip) 2024-05-11 15:57:20 +04:30
MAZE
fc1bd07b7d feat: add media session (wip) 2024-05-11 15:53:30 +04:30
MAZE
f79e941527 feat: add media session (wip) 2024-05-11 15:30:49 +04:30
MAZE
11a4514a0f feat: add media session (wip) 2024-05-11 15:20:41 +04:30
MAZE
e41f901041 Merge branch 'main' of https://github.com/remvze/moodist 2024-05-09 20:53:01 +03:30
MAZE
de49d37f08 chore: update README file 2024-05-09 20:52:32 +03:30
MAZE
5f066a4eff feat: add countdown timer button 2024-05-09 20:51:57 +03:30
MAZE
b925a2e04f Merge branch 'develop' into main 2024-05-03 16:00:36 +04:30
MAZE
c66cddc4c9 fix: turn off spell check 2024-05-03 15:59:19 +04:30
MAZE
7cb0f1c752 chore: update README file 2024-05-03 00:02:17 +03:30
MAZE
d09e598297 chore: add emojis 2024-05-01 16:28:32 +04:30
MAZE
5899d1bbbb chore: add contributing guide 2024-05-01 16:25:22 +04:30
MAZE
81678ea384 refactor: add constants 2024-05-01 15:50:07 +04:30
MAZE
d9246b692b feat: add lock while fading 2024-04-30 18:13:00 +03:30
MAZE
c893e2a6ad refactor: reduce dependency 2024-04-30 18:00:19 +03:30
MAZE
f025213ef2 fix: close all modals 2024-04-30 17:50:53 +03:30
MAZE
6ce766af47 feat: add basic fading effect 2024-04-30 17:47:49 +03:30
MAZE
7c57fb686b refactor: rewrite timer logic 2024-04-30 16:53:49 +03:30
MAZE
dc139e41e6 chore: update README file 2024-04-30 16:36:42 +03:30
MAZE
b990778142 chore: add contribution section to README file 2024-04-30 15:58:16 +03:30
MAZE
672988c36e chore: add support section 2024-04-30 15:51:10 +03:30
MAZE
06d0dfbe7e fix: typo in README file 2024-04-30 15:48:29 +03:30
MAZE
954a1b1ce2 chore: update README file 2024-04-30 15:47:54 +03:30
MAZE
383f898125 chore: update README file 2024-04-30 15:46:53 +03:30
MAZE
8d90344b26 chore: add npm commands to README file 2024-04-29 18:10:57 +03:30
MAZE
c614e3d4f5 chore: add features to README file 2024-04-29 18:01:54 +03:30
MAZE
781adcf17e chore: add emojis 2024-04-29 17:52:29 +03:30
MAZE
3e44516509 chore: add divider 2024-04-29 17:48:48 +03:30
MAZE
aeccf2dabd chore: complete tech stack 2024-04-29 17:47:30 +03:30
MAZE
8e6e690006 chore: add tech stack to README file 2024-04-29 17:41:10 +03:30
MAZE
75e7c48b21 Merge branch 'main' into develop 2024-04-29 15:50:26 +03:30
MAZE
dcef777295 chore: add licenses to README file 2024-04-29 15:48:53 +03:30
MAZE
cc77f9e9c0 fix: make inputs full width 2024-04-29 15:57:07 +04:30
MAZE
8fe90daf1e style: change input styles 2024-04-29 15:46:06 +04:30
MAZE
34d3c72f35 fix: change default values 2024-04-29 15:34:00 +04:30
MAZE
9d458fb60e feat: add form to sleep timer 2024-04-29 15:21:50 +04:30
MAZE
e674738ce7 style: change button styles 2024-04-29 15:06:09 +04:30
MAZE
77e2ec5e79 feat: add description for sleep timer 2024-04-29 00:43:35 +03:30
MAZE
4adfb3ddc9 refactor: relocate generic components 2024-04-29 00:38:15 +03:30
MAZE
ae0cbf1aa3 refactor: reorder menu items 2024-04-29 00:35:53 +03:30
MAZE ✧
dbbd68b73d Merge pull request #32 from SuperMeepBoy/add-sleep-timer
feat: add sleep timer
2024-04-29 00:32:09 +03:30
Jef Roelandt
2e375ad40a fix: play sounds when starting timer if not already playing 2024-04-28 20:40:08 +02:00
Jef Roelandt
58bf28bb24 fix: fix button disabled and reset to 0 2024-04-28 20:40:08 +02:00
Jef Roelandt
0517c31fc1 fix: take remvze comments into account 2024-04-28 18:47:24 +02:00
Jef Roelandt
71b62ed3dd feat: add sleep timer 2024-04-28 16:11:38 +02:00
MAZE
0300df3852 chore: add accessibility addon 2024-04-27 22:49:48 +03:30
MAZE
3f3bcdda21 chore: add autodocs for button 2024-04-27 22:47:23 +03:30
MAZE
f19d151f4a chore: add story for snackbar provider 2024-04-27 22:45:32 +03:30
MAZE
43f6245227 feat: add story for snackbar 2024-04-27 22:41:59 +03:30
MAZE
9b7d3c645b feat: add story for modal 2024-04-27 22:38:45 +03:30
MAZE
f8fb1ed61e chore: add link to story 2024-04-27 22:22:16 +03:30
MAZE
6fe9ce8915 chore: add link to issue 2024-04-27 22:21:07 +03:30
MAZE
603d318e68 chore: write story for button 2024-04-27 21:03:54 +03:30
MAZE
65ca7e1c94 chore: install Storybook 2024-04-27 20:07:39 +03:30
MAZE
583578b315 feat: close notepad on escape 2024-04-27 19:53:59 +03:30
MAZE
60f167c4d7 feat: add shortcuts list 2024-04-26 15:30:27 +03:30
MAZE
99f3a41598 feat: add keyboard shortcut for unselect button 2024-04-26 15:03:02 +03:30
MAZE
d3a2a12e1f feat: add keyboard shortcut for play button 2024-04-26 14:41:57 +03:30
MAZE
ebb35deaf9 style: add hover state to button 2024-04-26 14:24:35 +03:30
MAZE
9ad49d021a fix: make share hotkey conditional 2024-04-26 14:22:39 +03:30
MAZE
8307657628 fix: replace the animation on button 2024-04-26 12:40:04 +03:30
MAZE
3b0c22968e fix: remove auto focus on load 2024-04-25 23:48:27 +03:30
MAZE
e490a1da84 refactor: remove extra types 2024-04-25 21:21:21 +03:30
MAZE
7c6f068d15 fix: remove console log 2024-04-25 20:23:39 +03:30
MAZE
a46a4cdc96 Merge branch 'develop' 2024-04-25 20:00:22 +03:30
MAZE
b955fc93f4 fix: refocus on show more button 2024-04-25 19:59:41 +03:30
MAZE
54c777276d fix: focus on the first new sound 2024-04-25 19:48:09 +03:30
MAZE
136a009379 Merge branch 'develop' 2024-04-25 15:38:41 +03:30
MAZE
601ba6def7 fix: allow empty inputs 2024-04-25 15:35:07 +03:30
MAZE
89a83089c5 fix: reset values on cancel 2024-04-25 14:39:13 +03:30
MAZE
2192335238 refactor: better name 2024-04-24 19:50:44 +03:30
MAZE
af92b1ed90 feat: add close event for modals 2024-04-24 19:13:29 +03:30
MAZE
f81ea9e7bd refactor: better shortcut handling 2024-04-24 16:59:27 +03:30
MAZE
98e5021f56 feat: add better aria labels 2024-04-24 16:04:18 +03:30
MAZE
9774532308 feat: add better aria labels 2024-04-24 15:55:42 +03:30
MAZE
837826fbc1 feat: change shortcuts to shift 2024-04-24 13:29:28 +03:30
MAZE
2f84268017 refactor: rename hook file 2024-04-23 23:22:39 +03:30
MAZE
24a53c81df feat: add autofocus for note 2024-04-23 23:20:58 +03:30
MAZE ✧
ab9d47befb Merge pull request #26 from SuperMeepBoy/improve-accessibility-2
Allow using spacebar or enter to trigger buttons
2024-04-23 23:08:53 +03:30
Jef Roelandt
60cc2e9369 feat: allow using spacebar or enter to trigger buttons
It is a good practice for accessibility.
Cf https://webaim.org/techniques/keyboard/ or
any other resource on the Internet talking about buttons accessibiltiy.
2024-04-23 21:15:32 +02:00
MAZE
42f82ab95d feat: add shortcuts to items 2024-04-23 17:51:51 +03:30
MAZE
669df1f082 feat: add keyboard shortcuts 2024-04-23 17:24:32 +03:30
MAZE
a3cfbb98db style: add outlines to toolbar buttons 2024-04-23 17:08:15 +03:30
MAZE
3c8d75b018 style: better outlines for accessibility 2024-04-23 17:06:35 +03:30
MAZE
e7d7a37a12 style: add outline for better accessibility 2024-04-23 00:02:27 +03:30
MAZE
6f9c941a87 style: change outline color 2024-04-22 23:52:18 +03:30
MAZE
8596a0014c fix: relocate focus trap 2024-04-22 23:47:57 +03:30
MAZE
908fe01c5e Merge branch 'main' into develop 2024-04-22 23:44:09 +03:30
MAZE ✧
8009e1519f Merge pull request #24 from SuperMeepBoy/add-accessibility
Improve accessibility
2024-04-22 20:51:17 +03:30
MAZE
0252fa96ab feat: make the modal more accessible 2024-04-22 18:46:51 +03:30
MAZE
48291a6457 refactor: remove hide delay for tooltips 2024-04-22 18:40:25 +03:30
MAZE
ddae0b660f refactor: add Radix 2024-04-22 18:39:30 +03:30
Jef Roelandt
8669489747 Fix keyboard focus on Sound cards 2024-04-21 16:28:42 +02:00
Jef Roelandt
4f4ffe3e3a Add visibility on buttons focus 2024-04-21 16:28:36 +02:00
Jef Roelandt
42d3bd9e8c Make Dockerfile compatible with podman 2024-04-19 20:41:29 +02:00
MAZE
4b5456a51d chore(release): 1.4.3 2024-04-11 16:19:31 +03:30
MAZE
fa9711a1e0 chore: remove arm/v7 2024-04-11 16:19:04 +03:30
256 changed files with 18426 additions and 2757 deletions

View File

@@ -1,15 +1,12 @@
{
"root": true,
"env": {
"browser": true,
"amd": true,
"node": true,
"es2022": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
@@ -17,7 +14,6 @@
"jsx": true
}
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
@@ -28,9 +24,9 @@
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended",
"plugin:astro/recommended",
"prettier"
"prettier",
"plugin:storybook/recommended"
],
"plugins": [
"@typescript-eslint",
"typescript-sort-keys",
@@ -38,7 +34,6 @@
"sort-destructure-keys",
"prettier"
],
"rules": {
"@typescript-eslint/ban-ts-comment": "off",
"prettier/prettier": "error",
@@ -46,6 +41,8 @@
"sort-destructure-keys/sort-destructure-keys": "warn",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"jsx-a11y/label-has-associated-control": "off",
"react/jsx-sort-props": [
"warn",
{
@@ -54,48 +51,40 @@
}
]
},
"settings": {
"react": {
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx"]
},
"import/resolver": {
"typescript": true,
"node": true,
"alias": {
"extensions": [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
"map": [["@", "./src"]]
}
}
},
"overrides": [
{
"files": ["**/*.astro"],
"parser": "astro-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"extraFileExtensions": [".astro"]
},
"rules": {
"prettier/prettier": "error",
"react/no-unknown-property": "off",
"react/jsx-key": "off"
"react/jsx-key": "off",
"react/jsx-no-undef": "off"
},
"globals": {
"Astro": "readonly"
}
},
{
"files": ["**/*.astro/*.js"],
"rules": {

View File

@@ -34,7 +34,7 @@ jobs:
GIT_TAG=${GIT_TAG#refs/tags/}
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--platform linux/amd64,linux/arm64 \
-t $IMAGE_NAME:latest \
-t $IMAGE_NAME:$GIT_TAG \
--push .

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ pnpm-debug.log*
# macOS-specific files
.DS_Store
*storybook.log

46
.storybook/main.ts Normal file
View File

@@ -0,0 +1,46 @@
import path from 'node:path';
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal(config) {
return {
...config,
define: {
'process.env.NODE_DEBUG': false, // https://github.com/storybookjs/storybook/issues/18920
},
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(__dirname, '../src'),
},
],
},
}
}
};
export default config;

16
.storybook/preview.ts Normal file
View File

@@ -0,0 +1,16 @@
import '../src/styles/global.css';
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -8,7 +8,8 @@
"rules": {
"import-notation": "string",
"selector-class-pattern": null
"selector-class-pattern": null,
"no-descending-specificity": null
},
"overrides": [

File diff suppressed because it is too large Load Diff

25
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,25 @@
# Contributing Guidelines
Thank you for considering contributing to our project! We welcome your contributions.
## How to Contribute
1. Fork the repository.
2. Create a new branch: `git checkout -b feature/your-feature-name`.
3. Make your changes and commit them: `git commit -m 'feat: add some feature'`.
4. Push to the branch: `git push origin feature/your-feature-name`.
5. Submit a pull request. ⚡
⚠️ **Notice**: Commit messages should follow [Conventional Commits Specification](https://www.conventionalcommits.org/en/v1.0.0/).
## Report Bugs
To report a bug, please open an issue on GitHub and provide detailed information about the bug, including steps to reproduce it.
## Request Features
To request a new feature, open an issue on GitHub and describe the feature you would like to see added.
## License
By contributing, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE).

View File

@@ -1,11 +1,11 @@
FROM node:20-alpine3.18 AS build
FROM docker.io/node:20-alpine3.18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
FROM docker.io/nginx:alpine AS runtime
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html

View File

@@ -1,6 +1,89 @@
<div align="center">
<img src="/assets/banner.svg" alt="Moodist Logo Banner" />
<!-- <img src="/assets/banner.svg" alt="Moodist Logo Banner" /> -->
<h2>Moodist 🌲</h2>
<p>Ambient sounds for focus and calm.</p>
<a href="https://moodist.app">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
<a href="https://moodist.mvze.net">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
</div>
## Table of Contents
- ⚡ [Features](#features)
- 🧰 [Tools](#tools)
- 🔮 [Commands](#commands)
- 🚧 [Contributing](#contributing)
- ⭐ [Support](#support-moodist)
- 📜 [License](#license)
## Features
1. 🎵 Over 75 ambient sounds.
1. 📝 Persistent sound selection.
1. ✈️ Sharing sound selections with others.
1. 🧰 Custom sound presets.
1. 🌙 Sleep timer for sounds.
1. 📓 Notepad for quick notes.
1. 🍅 Pomodoro timer.
1. ✅ Simple to-do list (soon).
1. ⏯️ Media controls.
1. ⌨️ Keyboard shortcuts for everything.
1. 🥷 Privacy focused: no data collection.
1. 💰 Completely free, open-source, and self-hostable.
## Tools
-**TypeScript**: Programming Language
- 🔨 **React**: UI Library
- 🧑‍🚀 **Astro**: Meta Framework
- 🎨 **CSS Modules**: Styling
- 🐻 **Zustand**: State Management
- 🎭 **Framer Motion**: Animation Library
- ⚙️ **Radix**: Accessible Components
- 📕 **Storybook**: Component Documentation
- 🧪 **Vitest**: Unit Testing (soon)
- 🔭 **Playwright**: End-To-End Testing (soon)
- 🔍 **ESLint**: Code Linting
- 🧹 **Prettier**: Code Formatting
- 🧼 **Stylelint**: CSS Linting
- 🐶 **Husky**: Git Hooks
- 📝 **Lint Staged**: Running Linters on Staged Files
- 🧽 **Commitlint**: Git Commit Linting
- 🧭 **Commitizen**: Git Commit Message Helper
- 📓 **Standard Version**: Versioning and CHANGLOG Generation
- 🧰 **PostCSS**: CSS Transformations
## Commands
- `npm run dev`: run development server
- `npm run build`: build for production
- `npm run preview`: preview the built app
- `npm run lint`: lint files using ESLint
- `npm run lint:fix`: lint and fix using ESLint
- `npm run lint:style`: lint styles using Stylelint
- `npm run lint:style:fix`: lint and fix styles using Stylelint
- `npm run format`: format files using Prettier
- `npm run commit`: commit message using Commitizen
- `npm run release:major`: release major version
- `npm run release:minor`: release minor version
- `npm run release:patch`: release patch version
- `npm run storybook`: run Storybook
## Contributing
🚧 Please check [CONTRIBUTING.md](CONTRIBUTING.md) file.
## Support Moodist
⭐ Give a star if you liked this project.
☕ [Buy Me a Coffee](https://buymeacoffee.com/remvze) to help me maintain Moodist.
## License
This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
### ⚠️ Third-Party Assets
Some sounds used in this project are sourced from third-party providers and **are subject to different licenses**:
- Sounds licensed under the **Pixabay Content License**: [Pixabay Content License](https://pixabay.com/service/license-summary/)
- Sounds licensed under **CC0**: [Creative Commons Zero License](https://creativecommons.org/publicdomain/zero/1.0/)

View File

@@ -1,8 +1,36 @@
import { defineConfig } from 'astro/config';
import react from "@astrojs/react";
import react from '@astrojs/react';
import AstroPWA from '@vite-pwa/astro';
// https://astro.build/config
export default defineConfig({
integrations: [react()]
});
integrations: [
react(),
AstroPWA({
manifest: {
background_color: '#09090b',
description: 'Ambient sounds for focus and calm.',
display: 'standalone',
icons: [
...[72, 128, 144, 152, 192, 256, 512].map(size => ({
sizes: `${size}x${size}`,
src: `/assets/pwa/${size}.png`,
type: 'image/png',
})),
],
name: 'Moodist',
orientation: 'any',
scope: '/',
short_name: 'Moodist',
start_url: '/',
theme_color: '#09090b',
},
registerType: 'prompt',
workbox: {
globPatterns: ['**/*'],
maximumFileSizeToCacheInBytes: Number.MAX_SAFE_INTEGER,
navigateFallback: '/',
},
}),
],
});

13403
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
{
"name": "moodist",
"type": "module",
"version": "1.4.2",
"version": "2.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"test": "vitest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro",
"lint:fix": "npm run lint -- --fix",
"lint:style": "stylelint ./**/*.{css,astro,html}",
@@ -18,27 +19,49 @@
"release": "standard-version --no-verify",
"release:major": "npm run release -- --release-as major",
"release:minor": "npm run release -- --release-as minor",
"release:patch": "npm run release -- --release-as patch"
"release:patch": "npm run release -- --release-as patch",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@astrojs/react": "^3.0.3",
"@astrojs/react": "3.6.0",
"@floating-ui/react": "0.26.0",
"@formkit/auto-animate": "0.8.2",
"@radix-ui/react-checkbox": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.0.6",
"@radix-ui/react-slider": "1.2.3",
"@radix-ui/react-tooltip": "1.0.7",
"@types/howler": "2.2.10",
"@types/react": "^18.2.25",
"@types/react-dom": "^18.2.10",
"astro": "4.0.3",
"@vite-pwa/astro": "0.5.0",
"astro": "4.10.3",
"deepmerge": "4.3.1",
"focus-trap-react": "10.2.3",
"framer-motion": "10.16.4",
"howler": "2.2.4",
"js-confetti": "0.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "3.2.1",
"react-icons": "4.11.0",
"react-wrap-balancer": "1.1.0",
"uuid": "10.0.0",
"zustand": "4.4.3"
},
"devDependencies": {
"@chromatic-com/storybook": "1.3.3",
"@commitlint/cli": "17.7.2",
"@commitlint/config-conventional": "17.7.0",
"@storybook/addon-a11y": "8.0.9",
"@storybook/addon-essentials": "8.0.9",
"@storybook/addon-interactions": "8.0.9",
"@storybook/addon-links": "8.0.9",
"@storybook/addon-onboarding": "8.0.9",
"@storybook/blocks": "8.0.9",
"@storybook/react": "8.0.9",
"@storybook/react-vite": "8.0.9",
"@storybook/test": "8.0.9",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"astro-eslint-parser": "0.16.0",
@@ -57,6 +80,7 @@
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-sort-keys-fix": "1.1.2",
"eslint-plugin-storybook": "0.8.0",
"eslint-plugin-typescript-sort-keys": "3.1.0",
"husky": "8.0.3",
"lint-staged": "14.0.1",
@@ -64,11 +88,14 @@
"postcss-nesting": "12.0.1",
"prettier": "3.0.3",
"prettier-plugin-astro": "0.12.0",
"prop-types": "15.8.1",
"standard-version": "9.5.0",
"storybook": "8.0.9",
"stylelint": "15.10.3",
"stylelint-config-html": "1.1.0",
"stylelint-config-recess-order": "4.4.0",
"stylelint-config-standard": "34.0.0",
"stylelint-prettier": "4.0.2"
"stylelint-prettier": "4.0.2",
"vitest": "1.6.0"
}
}

BIN
public/assets/pwa/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/assets/pwa/144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/assets/pwa/152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
public/assets/pwa/192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/assets/pwa/256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/assets/pwa/512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/assets/pwa/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,12 +1,4 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="25" fill="#09090B"/>
<path d="M64 19C76.4264 19 86.5 29.0736 86.5 41.5H75.25C75.25 35.2868 70.2132 30.25 64 30.25C57.7868 30.25 52.75 35.2868 52.75 41.5L41.5 41.5C41.5 29.0736 51.5736 19 64 19Z" fill="white"/>
<path d="M41.5 86.5C29.0736 86.5 19 76.4264 19 64C19 51.5736 29.0736 41.5 41.5 41.5V52.75C35.2868 52.75 30.25 57.7868 30.25 64C30.25 70.2132 35.2868 75.25 41.5 75.25V86.5Z" fill="white"/>
<path d="M86.5 86.5C86.5 98.9264 76.4264 109 64 109C51.5736 109 41.5 98.9264 41.5 86.5H52.75C52.75 92.7132 57.7868 97.75 64 97.75C70.2132 97.75 75.25 92.7132 75.25 86.5H86.5Z" fill="white"/>
<path d="M86.5 86.5C98.9264 86.5 109 76.4264 109 64C109 51.5736 98.9264 41.5 86.5 41.5V52.75C92.7132 52.75 97.75 57.7868 97.75 64C97.75 70.2132 92.7132 75.25 86.5 75.25V86.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64 86.5C76.4264 86.5 86.5 76.4264 86.5 64C86.5 51.5736 76.4264 41.5 64 41.5C51.5736 41.5 41.5 51.5736 41.5 64C41.5 76.4264 51.5736 86.5 64 86.5ZM64 75.25C70.2132 75.25 75.25 70.2132 75.25 64C75.25 57.7868 70.2132 52.75 64 52.75C57.7868 52.75 52.75 57.7868 52.75 64C52.75 70.2132 57.7868 75.25 64 75.25Z" fill="white"/>
<path d="M30.25 41.5C30.25 35.2868 35.2868 30.25 41.5 30.25V19C29.0736 19 19 29.0736 19 41.5H30.25Z" fill="white"/>
<path d="M97.75 41.5C97.75 35.2868 92.7132 30.25 86.5 30.25V19C98.9264 19 109 29.0736 109 41.5H97.75Z" fill="white"/>
<path d="M97.75 86.5C97.75 92.7132 92.7132 97.75 86.5 97.75V109C98.9264 109 109 98.9264 109 86.5H97.75Z" fill="white"/>
<path d="M30.25 86.5C30.25 92.7132 35.2868 97.75 41.5 97.75V109C29.0736 109 19 98.9264 19 86.5H30.25Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.2493 29.2516C58.2929 22.2495 69.7071 22.2495 76.7507 29.2516C77.1507 29.6492 77.5279 30.0608 77.8825 30.4848C78.433 30.4357 78.9908 30.4114 79.5547 30.4131C89.4866 30.4424 97.5576 38.5135 97.587 48.4453C97.5886 49.0092 97.5643 49.567 97.5152 50.1176C97.9392 50.4721 98.3508 50.8493 98.7484 51.2493C105.751 58.2929 105.751 69.7071 98.7484 76.7507C98.3508 77.1507 97.9392 77.5279 97.5152 77.8825C97.5643 78.433 97.5886 78.9908 97.587 79.5547C97.5576 89.4866 89.4865 97.5577 79.5547 97.587C78.9908 97.5886 78.433 97.5643 77.8824 97.5152C77.5279 97.9392 77.1506 98.3508 76.7507 98.7484C69.7071 105.751 58.2929 105.751 51.2493 98.7484C50.8493 98.3508 50.4721 97.9392 50.1175 97.5152C49.567 97.5643 49.0092 97.5886 48.4453 97.5869C38.5134 97.5576 30.4424 89.4865 30.413 79.5547C30.4114 78.9908 30.4357 78.433 30.4848 77.8824C30.0608 77.5279 29.6492 77.1506 29.2516 76.7507C22.2495 69.7071 22.2495 58.2929 29.2516 51.2493C29.6492 50.8493 30.0608 50.4721 30.4848 50.1175C30.4357 49.567 30.4114 49.0092 30.4131 48.4453C30.4424 38.5134 38.5135 30.4423 48.4453 30.413C49.0092 30.4114 49.567 30.4356 50.1176 30.4848C50.4721 30.0608 50.8494 29.6492 51.2493 29.2516ZM47.29 35.173C40.877 35.7508 35.7508 40.8769 35.1731 47.29C38.8003 45.8063 42.8126 45.5647 46.5652 46.5652C45.5647 42.8126 45.8063 38.8002 47.29 35.173ZM53.7297 50.3922C50.2143 46.1861 49.7248 40.3267 52.2613 35.6603C57.3546 37.1664 61.1517 41.6557 61.64 47.1156C61.6129 47.529 61.5997 47.9462 61.6009 48.3669L61.6302 58.2787L54.6421 51.2493C54.3456 50.951 54.0412 50.6653 53.7297 50.3922ZM50.3922 53.7297C46.1861 50.2143 40.3268 49.7248 35.6603 52.2613C37.1665 57.3546 41.6558 61.1517 47.1157 61.64C47.5291 61.6128 47.9462 61.5996 48.3668 61.6009L58.2787 61.6302L51.2493 54.6421C50.951 54.3456 50.6653 54.0412 50.3922 53.7297ZM39.3435 64C35.9825 62.0539 33.3162 59.046 31.8005 55.432C27.6743 60.3752 27.6743 67.6247 31.8005 72.5679C33.3162 68.954 35.9825 65.946 39.3435 64ZM35.6603 75.7387C37.1664 70.6454 41.6558 66.8483 47.1157 66.36C47.5291 66.3871 47.9463 66.4003 48.3669 66.3991L58.2787 66.3698L51.2493 73.3579C50.951 73.6544 50.6653 73.9588 50.3922 74.2703C46.1861 77.7857 40.3268 78.2752 35.6603 75.7387ZM35.1731 80.71C35.7508 87.123 40.8769 92.2492 47.29 92.8269C45.8063 89.1997 45.5647 85.1874 46.5652 81.4348C42.8126 82.4353 38.8003 82.1937 35.1731 80.71ZM53.7297 77.6078C50.2143 81.8138 49.7248 87.6732 52.2613 92.3397C57.3546 90.8336 61.1516 86.3443 61.64 80.8844C61.6128 80.471 61.5996 80.0538 61.6009 79.6332L61.6302 69.7213L54.6421 76.7507C54.3456 77.049 54.0412 77.3347 53.7297 77.6078ZM64 88.6565C62.0539 92.0175 59.046 94.6838 55.4321 96.1995C60.3753 100.326 67.6247 100.326 72.5679 96.1995C68.954 94.6838 65.946 92.0175 64 88.6565ZM75.7387 92.3397C70.6454 90.8336 66.8483 86.3443 66.36 80.8844C66.3871 80.471 66.4004 80.0538 66.3991 79.6331L66.3699 69.7213L73.3579 76.7507C73.6544 77.049 73.9588 77.3347 74.2703 77.6078C77.7857 81.8139 78.2752 87.6733 75.7387 92.3397ZM80.71 92.827C87.1231 92.2492 92.2492 87.1231 92.8269 80.71C89.1997 82.1937 85.1874 82.4353 81.4348 81.4348C82.4353 85.1874 82.1937 89.1997 80.71 92.827ZM77.6078 74.2703C81.8138 77.7857 87.6732 78.2752 92.3397 75.7387C90.8336 70.6454 86.3442 66.8483 80.8843 66.36C80.471 66.3872 80.0538 66.4004 79.6332 66.3991L69.7213 66.3698L76.7507 73.3579C77.049 73.6544 77.3347 73.9588 77.6078 74.2703ZM88.6565 64C92.0175 65.9461 94.6838 68.954 96.1995 72.568C100.326 67.6248 100.326 60.3753 96.1995 55.4321C94.6838 59.046 92.0175 62.054 88.6565 64ZM92.3397 52.2613C90.8336 57.3546 86.3442 61.1517 80.8843 61.64C80.471 61.6129 80.0538 61.5997 79.6331 61.6009L69.7213 61.6302L76.7507 54.6421C77.049 54.3456 77.3347 54.0412 77.6078 53.7297C81.8139 50.2143 87.6732 49.7248 92.3397 52.2613ZM92.8269 47.29C92.2492 40.877 87.1231 35.7508 80.71 35.1731C82.1937 38.8003 82.4353 42.8126 81.4348 46.5652C85.1874 45.5647 89.1997 45.8063 92.8269 47.29ZM74.2703 50.3922C77.7857 46.1861 78.2752 40.3268 75.7387 35.6603C70.6455 37.1664 66.8484 41.6557 66.36 47.1156C66.3872 47.529 66.4004 47.9462 66.3991 48.3668L66.3699 58.2787L73.3579 51.2493C73.6544 50.951 73.9588 50.6653 74.2703 50.3922ZM64 39.3435C62.054 35.9825 59.046 33.3162 55.4321 31.8005C60.3753 27.6743 67.6248 27.6743 72.568 31.8005C68.954 33.3162 65.9461 35.9825 64 39.3435Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/logo-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
public/logo-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -1,11 +1,3 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M100 150C127.614 150 150 127.614 150 100C150 72.3858 127.614 50 100 50C72.3858 50 50 72.3858 50 100C50 127.614 72.3858 150 100 150ZM100 125C113.807 125 125 113.807 125 100C125 86.1929 113.807 75 100 75C86.1929 75 75 86.1929 75 100C75 113.807 86.1929 125 100 125Z" fill="#FAFAFA"/>
<path d="M50 50C22.3858 50 2.00172e-06 72.3857 7.94663e-07 100C-4.12393e-07 127.614 22.3858 150 50 150V125C36.1929 125 25 113.807 25 100C25 86.1929 36.1929 75 50 75V50Z" fill="#D4D4D8"/>
<path d="M150 50C150 22.3858 127.614 2.41411e-06 100 0C72.3858 -2.41411e-06 50 22.3858 50 50L75 50C75 36.1929 86.1929 25 100 25C113.807 25 125 36.1929 125 50H150Z" fill="#D4D4D8"/>
<path d="M150 150C177.614 150 200 127.614 200 100C200 72.3858 177.614 50 150 50V75C163.807 75 175 86.1929 175 100C175 113.807 163.807 125 150 125V150Z" fill="#D4D4D8"/>
<path d="M50 150C50 177.614 72.3857 200 100 200C127.614 200 150 177.614 150 150H125C125 163.807 113.807 175 100 175C86.1929 175 75 163.807 75 150H50Z" fill="#D4D4D8"/>
<path d="M25 50C25 36.1929 36.1929 25 50 25V1.39091e-06C22.3858 1.83851e-07 2.00172e-06 22.3857 7.94663e-07 50H25Z" fill="#A1A1AA"/>
<path d="M150 25C163.807 25 175 36.1929 175 50H200C200 22.3858 177.614 3.24858e-06 150 8.34465e-07L150 25Z" fill="#A1A1AA"/>
<path d="M175 150C175 163.807 163.807 175 150 175V200C177.614 200 200 177.614 200 150H175Z" fill="#A1A1AA"/>
<path d="M50 175C36.1929 175 25 163.807 25 150H0C0 177.614 22.3857 200 50 200V175Z" fill="#A1A1AA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.1232 13.129C85.7322 -4.37634 114.268 -4.37634 131.877 13.129C132.877 14.1229 133.82 15.1519 134.706 16.212C136.083 16.0892 137.477 16.0285 138.887 16.0326C163.716 16.1059 183.894 36.2836 183.967 61.1133C183.972 62.5231 183.911 63.9175 183.788 65.2939C184.848 66.1803 185.877 67.1234 186.871 68.1232C204.376 85.7322 204.376 114.268 186.871 131.877C185.877 132.877 184.848 133.82 183.788 134.706C183.911 136.083 183.972 137.477 183.967 138.887C183.894 163.716 163.716 183.894 138.887 183.967C137.477 183.972 136.082 183.911 134.706 183.788C133.82 184.848 132.877 185.877 131.877 186.871C114.268 204.376 85.7322 204.376 68.1232 186.871C67.1234 185.877 66.1803 184.848 65.2939 183.788C63.9175 183.911 62.523 183.972 61.1133 183.967C36.2836 183.894 16.1059 163.716 16.0326 138.887C16.0284 137.477 16.0892 136.082 16.212 134.706C15.1519 133.82 14.1229 132.877 13.129 131.877C-4.37634 114.268 -4.37634 85.7322 13.129 68.1232C14.1229 67.1234 15.1519 66.1803 16.212 65.2939C16.0892 63.9175 16.0285 62.523 16.0326 61.1132C16.1059 36.2836 36.2837 16.1059 61.1133 16.0325C62.5231 16.0284 63.9176 16.0891 65.294 16.2119C66.1803 15.1519 67.1234 14.1229 68.1232 13.129ZM58.225 27.9326C42.1924 29.3769 29.377 42.1923 27.9327 58.2249C37.0007 54.5157 47.0315 53.9118 56.413 56.413C53.9118 47.0315 54.5158 37.0006 58.225 27.9326ZM74.3243 65.9805C65.5357 55.4653 64.3121 40.8169 70.6533 29.1507C83.3865 32.916 92.8792 44.1393 94.1001 57.789C94.0322 58.8224 93.9991 59.8655 94.0023 60.9171L94.0754 85.6967L76.6053 68.1232C75.8639 67.3774 75.103 66.6632 74.3243 65.9805ZM65.9805 74.3243C55.4654 65.5357 40.8169 64.3121 29.1508 70.6533C32.9161 83.3864 44.1395 92.8792 57.7893 94.1C58.8226 94.0321 59.8656 93.9991 60.9171 94.0022L85.6967 94.0754L68.1232 76.6053C67.3774 75.8639 66.6632 75.103 65.9805 74.3243ZM38.3587 99.9999C29.9563 95.1348 23.2906 87.6149 19.5013 78.5801C9.18585 90.9381 9.18584 109.062 19.5013 121.42C23.2905 112.385 29.9563 104.865 38.3587 99.9999ZM29.1508 129.347C32.9161 116.613 44.1394 107.121 57.7893 105.9C58.8227 105.968 59.8656 106.001 60.9171 105.998L85.6968 105.925L68.1232 123.395C67.3774 124.136 66.6631 124.897 65.9805 125.676C55.4653 134.464 40.8169 135.688 29.1508 129.347ZM27.9327 141.775C29.377 157.808 42.1924 170.623 58.2249 172.067C54.5157 162.999 53.9118 152.969 56.413 143.587C47.0315 146.088 37.0007 145.484 27.9327 141.775ZM74.3243 134.019C65.5357 144.535 64.3121 159.183 70.6533 170.849C83.3864 167.084 92.8791 155.861 94.1001 142.211C94.0321 141.178 93.9991 140.135 94.0022 139.083L94.0754 114.303L76.6053 131.877C75.8639 132.623 75.103 133.337 74.3243 134.019ZM100 161.641C95.1349 170.044 87.6149 176.709 78.5801 180.499C90.9381 190.814 109.062 190.814 121.42 180.499C112.385 176.71 104.865 170.044 100 161.641ZM129.347 170.849C116.614 167.084 107.121 155.861 105.9 142.211C105.968 141.178 106.001 140.134 105.998 139.083L105.925 114.303L123.395 131.877C124.136 132.623 124.897 133.337 125.676 134.019C134.464 144.535 135.688 159.183 129.347 170.849ZM141.775 172.067C157.808 170.623 170.623 157.808 172.067 141.775C162.999 145.484 152.969 146.088 143.587 143.587C146.088 152.969 145.484 162.999 141.775 172.067ZM134.019 125.676C144.535 134.464 159.183 135.688 170.849 129.347C167.084 116.614 155.861 107.121 142.211 105.9C141.177 105.968 140.134 106.001 139.083 105.998L114.303 105.925L131.877 123.395C132.623 124.136 133.337 124.897 134.019 125.676ZM161.641 100C170.044 104.865 176.709 112.385 180.499 121.42C190.814 109.062 190.814 90.9382 180.499 78.5802C176.709 87.615 170.044 95.135 161.641 100ZM170.849 70.6533C167.084 83.3865 155.861 92.8793 142.211 94.1001C141.177 94.0322 140.134 93.9992 139.083 94.0023L114.303 94.0754L131.877 76.6053C132.623 75.8639 133.337 75.103 134.019 74.3243C144.535 65.5357 159.183 64.3121 170.849 70.6533ZM172.067 58.2249C170.623 42.1924 157.808 29.377 141.775 27.9327C145.484 37.0007 146.088 47.0315 143.587 56.413C152.969 53.9118 162.999 54.5157 172.067 58.2249ZM125.676 65.9805C134.464 55.4653 135.688 40.8169 129.347 29.1508C116.614 32.9161 107.121 44.1393 105.9 57.789C105.968 58.8224 106.001 59.8655 105.998 60.9171L105.925 85.6967L123.395 68.1232C124.136 67.3774 124.897 66.6631 125.676 65.9805ZM100 38.3587C95.135 29.9563 87.615 23.2905 78.5802 19.5012C90.9382 9.18585 109.062 9.18587 121.42 19.5013C112.385 23.2905 104.865 29.9563 100 38.3587Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

156
src/components/about.astro Normal file
View File

@@ -0,0 +1,156 @@
---
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
const paragraphs = [
{
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.',
title: 'Free Ambient Sounds',
},
{
body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`,
title: 'Carefully Curated Sounds',
},
{
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
title: 'Create Your Soundscape',
},
{
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
title: 'Sounds for Every Moment',
},
];
---
<section class="about">
<div class="effect"></div>
<Container tight>
{
paragraphs.map((paragraph, index) => (
<div class="paragraph">
<div class="counter">
<span>0{index + 1}</span> / 0{paragraphs.length}
</div>
<h2 class="title">{paragraph.title}</h2>
<p class="body">{paragraph.body}</p>
</div>
))
}
<button class="button" id="use-moodist"> Use Moodist</button>
</Container>
</section>
<script lang="ts">
const button = document.getElementById('use-moodist');
button.addEventListener('click', () => {
const app = document.getElementById('app');
app?.scrollIntoView();
});
</script>
<style>
.about {
padding-top: 10px;
& .effect {
position: sticky;
top: 0;
height: 80px;
background: linear-gradient(var(--color-neutral-50), transparent);
}
& .paragraph {
padding: 30px 0;
background: linear-gradient(
transparent,
var(--color-neutral-50) 10%,
var(--color-neutral-50) 90%,
transparent
);
&:last-of-type {
padding-bottom: 0;
}
& .counter {
width: max-content;
padding: 6px 16px;
margin-bottom: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-300);
border-radius: 20px 20px 20px 8px;
& span {
font-weight: 500;
color: var(--color-foreground);
}
}
& .title {
margin-bottom: 8px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .body {
line-height: 1.6;
color: var(--color-foreground-subtle);
}
}
.button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
margin-top: 20px;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border-radius: 50px;
outline: none;
transition: 0.2s;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
&:hover,
&:focus-visible {
background-color: var(--color-neutral-100);
}
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
}
</style>

View File

@@ -1,90 +0,0 @@
.about {
padding-top: 10px;
& .effect {
position: sticky;
top: 0;
height: 80px;
background: linear-gradient(var(--color-neutral-50), transparent);
}
& .paragraph {
padding: 30px 0;
background: linear-gradient(
transparent,
var(--color-neutral-50) 10%,
var(--color-neutral-50) 90%,
transparent
);
&:last-of-type {
padding-bottom: 0;
}
& .counter {
width: max-content;
padding: 6px 16px;
margin-bottom: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-300);
border-radius: 20px 20px 20px 8px;
& span {
font-weight: 500;
color: var(--color-foreground);
}
}
& .title {
margin-bottom: 8px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .body {
line-height: 1.6;
color: var(--color-foreground-subtle);
}
}
.button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
margin-top: 20px;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border-radius: 50px;
outline: none;
transition: 0.2s;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
&:hover {
background-color: var(--color-neutral-100);
}
}
}

View File

@@ -1,60 +0,0 @@
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
import styles from './about.module.css';
export function About() {
const count = soundCount();
const paragraphs = [
{
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.',
title: 'Free Ambient Sounds',
},
{
body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`,
title: 'Carefully Curated Sounds',
},
{
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
title: 'Create Your Soundscape',
},
// {
// body: 'Moodist goes beyond just ambient sounds by offering a suite of productivity tools to help you stay organized and focused. Utilize the built-in pomodoro timer to structure your workday in focused intervals, jot down thoughts and ideas in the simple notepad, and keep track of your tasks with the handy to-do list. These tools seamlessly integrate with the ambient soundscapes, allowing you to create a personalized environment that fosters both focus and relaxation.',
// title: 'A Productivity Toolbox',
// },
{
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
title: 'Sounds for Every Moment',
},
];
const handleClick = () => {
const app = document.getElementById('app');
app?.scrollIntoView();
};
return (
<section className={styles.about}>
<div className={styles.effect} />
<Container tight>
{paragraphs.map((paragraph, index) => (
<div className={styles.paragraph} key={index}>
<div className={styles.counter}>
<span>0{index + 1}</span> / 0{paragraphs.length}
</div>
<h2 className={styles.title}>{paragraph.title}</h2>
<p className={styles.body}>{paragraph.body}</p>
</div>
))}
<button className={styles.button} onClick={handleClick}>
Use Moodist
</button>
</Container>
</section>
);
}

View File

@@ -1 +0,0 @@
export { About } from './about';

View File

@@ -3,7 +3,7 @@ import { useShallow } from 'zustand/react/shallow';
import { BiSolidHeart } from 'react-icons/bi/index';
import { Howler } from 'howler';
import { useSoundStore } from '@/store';
import { useSoundStore } from '@/stores/sound';
import { Container } from '@/components/container';
import { StoreConsumer } from '@/components/store-consumer';
@@ -12,15 +12,21 @@ import { Categories } from '@/components/categories';
import { SharedModal } from '@/components/modals/shared';
import { Toolbar } from '@/components/toolbar';
import { SnackbarProvider } from '@/contexts/snackbar';
import { MediaControls } from '@/components/media-controls';
import { sounds } from '@/data/sounds';
import { FADE_OUT } from '@/constants/events';
import type { Sound } from '@/data/types';
import { subscribe } from '@/lib/event';
export function App() {
const categories = useMemo(() => sounds.categories, []);
const favorites = useSoundStore(useShallow(state => state.getFavorites()));
const pause = useSoundStore(state => state.pause);
const lock = useSoundStore(state => state.lock);
const unlock = useSoundStore(state => state.unlock);
const favoriteSounds = useMemo(() => {
const favoriteSounds = categories
@@ -52,6 +58,19 @@ export function App() {
return () => document.removeEventListener('visibilitychange', onChange);
}, []);
useEffect(() => {
const unsubscribe = subscribe(FADE_OUT, (e: { duration: number }) => {
lock();
setTimeout(() => {
pause();
unlock();
}, e.duration);
});
return unsubscribe;
}, [pause, lock, unlock]);
const allCategories = useMemo(() => {
const favorites = [];
@@ -70,6 +89,7 @@ export function App() {
return (
<SnackbarProvider>
<StoreConsumer>
<MediaControls />
<Container>
<div id="app" />
<Buttons />

17
src/components/binary.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react';
import { generateRandomBinaryString } from '@/helpers/binary';
export function Binary() {
const [binary, setBinary] = useState('');
useEffect(() => {
setBinary(generateRandomBinaryString(1000));
setInterval(() => {
setBinary(generateRandomBinaryString(1000));
}, 200);
}, []);
return <span>{binary}</span>;
}

View File

@@ -12,7 +12,6 @@
background-color: var(--color-neutral-950);
border: 1px solid var(--color-neutral-50);
border-radius: 100px;
outline: none;
transition: 0.2s;
&:hover {
@@ -27,4 +26,9 @@
& span {
font-size: var(--font-lg);
}
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}

View File

@@ -1,7 +1,8 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { BiPause, BiPlay } from 'react-icons/bi/index';
import { useHotkeys } from 'react-hotkeys-hook';
import { useSoundStore } from '@/store';
import { useSoundStore } from '@/stores/sound';
import { useSnackbar } from '@/contexts/snackbar';
import { cn } from '@/helpers/styles';
@@ -12,35 +13,40 @@ export function PlayButton() {
const pause = useSoundStore(state => state.pause);
const toggle = useSoundStore(state => state.togglePlay);
const noSelected = useSoundStore(state => state.noSelected());
const locked = useSoundStore(state => state.locked);
const showSnackbar = useSnackbar();
const handleClick = () => {
const handleToggle = useCallback(() => {
if (locked) return;
if (noSelected) return showSnackbar('Please first select a sound to play.');
toggle();
};
}, [showSnackbar, toggle, noSelected, locked]);
useEffect(() => {
if (isPlaying && noSelected) pause();
}, [isPlaying, pause, noSelected]);
useHotkeys('shift+space', handleToggle, {}, [handleToggle]);
return (
<button
aria-disabled={noSelected}
className={cn(styles.playButton, noSelected && styles.disabled)}
onClick={handleClick}
onClick={handleToggle}
>
{isPlaying ? (
<>
<span>
<span aria-hidden="true">
<BiPause />
</span>{' '}
Pause
</>
) : (
<>
<span>
<span aria-hidden="true">
<BiPlay />
</span>{' '}
Play

View File

@@ -12,7 +12,6 @@
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 100px;
outline: none;
transition: 0.2s;
&:disabled,
@@ -20,9 +19,15 @@
cursor: not-allowed;
}
&:hover {
&:hover,
&:focus-visible {
background-color: var(--color-neutral-200);
}
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
.tooltip {

View File

@@ -1,9 +1,11 @@
import { useCallback } from 'react';
import { BiUndo, BiTrash } from 'react-icons/bi/index';
import { AnimatePresence, motion } from 'framer-motion';
import { useHotkeys } from 'react-hotkeys-hook';
import { Tooltip } from '@/components/tooltip';
import { useSoundStore } from '@/store';
import { useSoundStore } from '@/stores/sound';
import { cn } from '@/helpers/styles';
import { fade, mix, slideX } from '@/lib/motion';
@@ -14,12 +16,21 @@ export function UnselectButton() {
const restoreHistory = useSoundStore(state => state.restoreHistory);
const hasHistory = useSoundStore(state => !!state.history);
const unselectAll = useSoundStore(state => state.unselectAll);
const locked = useSoundStore(state => state.locked);
const variants = {
...mix(fade(), slideX(15)),
exit: { opacity: 0 },
};
const handleToggle = useCallback(() => {
if (locked) return;
if (hasHistory) restoreHistory();
else if (!noSelected) unselectAll(true);
}, [hasHistory, noSelected, unselectAll, restoreHistory, locked]);
useHotkeys('shift+r', handleToggle, {}, [handleToggle]);
return (
<>
<AnimatePresence mode="wait">
@@ -31,7 +42,6 @@ export function UnselectButton() {
variants={variants}
>
<Tooltip
hideDelay={0}
showDelay={0}
content={
hasHistory
@@ -50,10 +60,7 @@ export function UnselectButton() {
styles.unselectButton,
noSelected && !hasHistory && styles.disabled,
)}
onClick={() => {
if (hasHistory) restoreHistory();
else if (!noSelected) unselectAll(true);
}}
onClick={handleToggle}
>
{hasHistory ? <BiUndo /> : <BiTrash />}
</button>

View File

@@ -1,6 +1,6 @@
import { AnimatePresence } from 'framer-motion';
import { Category } from '@/components/category';
import { Category } from './category';
import { Donate } from './donate';
import type { Categories } from '@/data/types';

View File

@@ -22,7 +22,10 @@
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}

View File

@@ -16,10 +16,12 @@ export function Category({
title,
}: CategoryProps) {
return (
<div className={styles.category}>
<div className={styles.category} id={`category-${id}`}>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>{icon}</div>
<div aria-hidden="true" className={styles.icon}>
{icon}
</div>
</div>
<div className={styles.title}>{title}</div>

View File

@@ -20,7 +20,10 @@
width: 45px;
height: 45px;
font-size: var(--font-md);
background-color: var(--color-neutral-100);
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
}
@@ -31,6 +34,16 @@
font-size: var(--font-lg);
font-weight: 600;
text-align: center;
& span {
background: linear-gradient(
135deg,
var(--color-foreground),
var(--color-foreground-subtle)
);
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
& .desc {

View File

@@ -9,12 +9,14 @@ export function Donate() {
<div className={styles.donate}>
<div className={styles.iconContainer}>
<div className={styles.tail} />
<div className={styles.icon}>
<div aria-hidden="true" className={styles.icon}>
<FaCoffee />
</div>
</div>
<div className={styles.title}>Support Me</div>
<div className={styles.title}>
<span>Support Me</span>
</div>
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
<SpecialButton
className={styles.button}

View File

@@ -0,0 +1,23 @@
.checkboxRoot {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
background: var(--color-neutral-100);
border: 2px solid var(--color-neutral-300);
border-radius: 4px;
transition: 0.2s;
}
.checkboxRoot[data-state='checked'] {
background: var(--color-neutral-950);
border: 2px solid var(--color-neutral-950);
}
.checkboxIndicator {
font-size: var(--font-2xsm);
color: var(--color-neutral-50);
transform: translateY(2px);
}

View File

@@ -0,0 +1,38 @@
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import { FaCheck } from 'react-icons/fa6/index';
import styles from './checkbox.module.css';
type CheckboxInputProps = {
checked?: boolean;
className?: string;
defaultChecked?: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
};
export function Checkbox({
checked,
className,
defaultChecked = false,
disabled = false,
onChange,
}: CheckboxInputProps) {
const handleCheckedChange = (checked: boolean) => {
if (onChange) onChange(checked);
};
return (
<RadixCheckbox.Root
checked={checked}
className={`${styles.checkboxRoot} ${className}`}
defaultChecked={defaultChecked}
disabled={disabled}
onCheckedChange={handleCheckedChange}
>
<RadixCheckbox.Indicator className={styles.checkboxIndicator}>
<FaCheck />
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
);
}

View File

@@ -0,0 +1 @@
export { Checkbox } from './checkbox';

61
src/components/cipher.tsx Normal file
View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from 'react';
interface CipherTextProps {
interval?: number;
text: string;
}
const chars = '-_~`!@#$%^&*()+=[]{}|;:,.<>?';
export function CipherText({ interval = 50, text }: CipherTextProps) {
const [outputText, setOutputText] = useState('');
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setTimeout(() => setIsMounted(true), 2000);
}, []);
useEffect(() => {
if (!isMounted) return;
let timer: NodeJS.Timeout;
if (outputText !== text) {
timer = setInterval(() => {
if (outputText.length < text.length) {
setOutputText(prev => prev + text[prev.length]);
} else {
clearInterval(timer);
}
}, interval);
}
return () => clearInterval(timer);
}, [text, interval, outputText, isMounted]);
useEffect(() => {
if (outputText === text) {
setTimeout(() => setOutputText(''), 6000);
}
}, [outputText, text]);
const remainder =
outputText.length < text.length
? text
.slice(outputText.length)
.split('')
.map(() => chars[Math.floor(Math.random() * chars.length)])
.join('')
: '';
if (!isMounted) {
return <span>{text}</span>;
}
return (
<span className="text-white">
{outputText}
{remainder}
</span>
);
}

View File

@@ -0,0 +1,57 @@
---
import { Container } from './container';
---
<Container>
<section class="wrapper">
<p class="text">
Enjoy Moodist?{' '}
<a
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Support with a donation!
</a>
</p>
</section>
</Container>
<style>
.wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
&::after {
position: absolute;
bottom: 0;
left: 50%;
width: 80%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-200),
transparent
);
transform: translateX(-50%);
}
& .text {
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}
</style>

View File

@@ -1,22 +0,0 @@
import { Container } from '@/components/container';
import styles from './donate.module.css';
export function Donate() {
return (
<Container>
<section className={styles.wrapper}>
<p className={styles.text}>
Enjoy Moodist?{' '}
<a
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Support with a donation!
</a>
</p>
</section>
</Container>
);
}

View File

@@ -1 +0,0 @@
export { Donate } from './donate';

View File

@@ -0,0 +1,31 @@
---
import { Container } from './container';
---
<footer class="footer">
<Container>
<p>
Created by <a href="https://twitter.com/remvze">Maze ✦</a>
</p>
</Container>
</footer>
<style>
.footer {
display: flex;
align-items: center;
height: 100px;
& p {
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}
</style>

View File

@@ -1,17 +0,0 @@
.footer {
display: flex;
align-items: center;
height: 100px;
& p {
font-size: var(--font-sm);
color: var(--color-foreground-subtle);
text-align: center;
& a {
font-weight: 500;
color: var(--color-foreground);
text-decoration: none;
}
}
}

View File

@@ -1,15 +0,0 @@
import { Container } from '@/components/container';
import styles from './footer.module.css';
export function Footer() {
return (
<footer className={styles.footer}>
<Container>
<p>
Created by <a href="https://twitter.com/remvze">Maze </a>
</p>
</Container>
</footer>
);
}

View File

@@ -1 +0,0 @@
export { Footer } from './footer';

173
src/components/hero.astro Normal file
View File

@@ -0,0 +1,173 @@
---
import { BsSoundwave } from 'react-icons/bs/index';
import { Container } from './container';
import { CipherText } from './cipher';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
---
<div class="hero">
<Container>
<div class="wrapper">
<div class="pattern"></div>
<div class="logo-wrapper">
<img
alt="Faded Moodist Logo"
aria-hidden="true"
class="logo"
height={48}
src="/logo.svg"
width={48}
/>
</div>
<h1 class="title">
Ambient Sounds<span class="line">For Focus and Calm</span>
</h1>
<h2 class="desc">
Free and <CipherText client:load text="Open-Source" />.
</h2>
<p class="sounds">
<span aria-hidden="true" class="icon">
<BsSoundwave />
</span>
<span>{count} Sounds</span>
</p>
</div>
</Container>
</div>
<style>
.hero {
text-align: center;
.wrapper {
position: relative;
padding: 120px 0 80px;
& .pattern {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-image: radial-gradient(
var(--color-neutral-500) 5%,
transparent 5%
);
background-position: top center;
background-size: 21px 21px;
opacity: 0.8;
mask-image: linear-gradient(#fff, transparent, transparent);
}
}
& .logo-wrapper {
mask-image: linear-gradient(#000, rgb(0 0 0 / 40%), rgb(0 0 0 / 5%));
& .logo {
display: block;
width: 48px;
margin: 0 auto 20px;
opacity: 1;
animation-name: logo;
animation-duration: 60s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
}
& .title {
font-family: var(--font-display);
font-size: var(--font-xlg);
font-weight: 600;
line-height: 1;
/* & .gradient {
background: linear-gradient(
135deg,
var(--color-foreground),
var(--color-foreground-subtle)
);
background-clip: text;
-webkit-text-fill-color: transparent;
} */
& .line {
display: block;
margin-top: 2px;
background: linear-gradient(
var(--color-foreground-subtler),
var(--color-foreground-subtle)
);
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
& .desc {
margin-top: 12px;
line-height: 1.6;
color: var(--color-foreground-subtle);
}
& .sounds {
position: relative;
display: flex;
column-gap: 8px;
align-items: center;
justify-content: center;
width: max-content;
height: 28px;
padding-right: 12px;
margin: 20px auto 0;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-200);
border-radius: 100px;
& .icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 10px;
color: var(--color-foreground);
border-right: 1px solid var(--color-neutral-200);
border-radius: 0 100px 100px 0;
}
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);
}
}
}
@keyframes logo {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,120 +0,0 @@
.hero {
text-align: center;
.container {
position: relative;
padding: 100px 0 80px;
/* padding: 120px 0 60px; */
& .pattern {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-image: radial-gradient(
var(--color-neutral-300) 5%,
transparent 5%
);
background-position: top center;
background-size: 31px 31px;
opacity: 0.9;
mask-image: linear-gradient(#fff, transparent, transparent);
}
}
& .logo {
display: block;
width: 45px;
margin: 0 auto 12px;
}
& .title {
display: flex;
column-gap: 15px;
align-items: center;
& div {
flex-grow: 1;
height: 1px;
&.left {
background: linear-gradient(
90deg,
transparent,
transparent,
var(--color-neutral-200),
var(--color-neutral-300)
);
}
&.right {
background: linear-gradient(
90deg,
var(--color-neutral-300),
var(--color-neutral-200),
transparent,
transparent
);
}
}
& h2 {
font-family: var(--font-display);
font-size: var(--font-2xlg);
font-weight: 600;
}
}
& .desc {
margin-top: 5px;
line-height: 1.6;
color: var(--color-foreground-subtle);
}
& .sounds {
position: relative;
display: flex;
column-gap: 8px;
align-items: center;
justify-content: center;
width: max-content;
height: 28px;
padding-right: 12px;
margin: 20px auto 0;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-200);
border-radius: 100px;
& .icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 10px;
color: var(--color-foreground);
border-right: 1px solid var(--color-neutral-200);
border-radius: 0 100px 100px 0;
}
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-400),
transparent
);
transform: translateX(-50%);
}
}
}

View File

@@ -1,42 +0,0 @@
import { useMemo } from 'react';
import { BsSoundwave } from 'react-icons/bs/index';
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
import styles from './hero.module.css';
export function Hero() {
const count = useMemo(soundCount, []);
return (
<div className={styles.hero}>
<Container className={styles.container}>
{/* <div className={styles.pattern} /> */}
<img
alt="Faded Moodist Logo"
className={styles.logo}
height={45}
src="/logo.svg"
width={45}
/>
<div className={styles.title}>
<div className={styles.left}></div>
<h2>Moodist</h2>
<div className={styles.right}></div>
</div>
<h1 className={styles.desc}>Ambient sounds for focus and calm.</h1>
<p className={styles.sounds}>
<span className={styles.icon}>
<BsSoundwave />
</span>
<span>{count} Sounds</span>
</p>
</Container>
</div>
);
}

View File

@@ -1 +0,0 @@
export { Hero } from './hero';

View File

@@ -0,0 +1 @@
export { MediaControls } from './media-controls';

View File

@@ -0,0 +1,20 @@
import { MediaSessionTrack } from './media-session-track';
import { useEffect, useState } from 'react';
import { useSSR } from '@/hooks/use-ssr';
export function MediaControls() {
const [mediaControlsEnabled, setMediaControlsEnabled] = useState(false);
const { isBrowser } = useSSR();
useEffect(() => {
if (!isBrowser) return;
setMediaControlsEnabled('mediaSession' in navigator);
}, [isBrowser]);
if (!mediaControlsEnabled) {
return null;
}
return <MediaSessionTrack />;
}

View File

@@ -0,0 +1,104 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { getSilenceDataURL } from '@/helpers/sound';
import { BrowserDetect } from '@/helpers/browser-detect';
import { useSoundStore } from '@/stores/sound';
import { useSSR } from '@/hooks/use-ssr';
import { useDarkTheme } from '@/hooks/use-dark-theme';
const metadata: MediaMetadataInit = {
artist: 'Moodist',
title: 'Ambient Sounds for Focus and Calm',
};
export function MediaSessionTrack() {
const { isBrowser } = useSSR();
const isDarkTheme = useDarkTheme();
const [isGenerated, setIsGenerated] = useState(false);
const isPlaying = useSoundStore(state => state.isPlaying);
const play = useSoundStore(state => state.play);
const pause = useSoundStore(state => state.pause);
const masterAudioSoundRef = useRef<HTMLAudioElement>(null);
const artworkURL = isDarkTheme ? '/logo-dark.png' : '/logo-light.png';
const generateSilence = useCallback(async () => {
if (!masterAudioSoundRef.current) return;
masterAudioSoundRef.current.src = await getSilenceDataURL();
setIsGenerated(true);
}, []);
useEffect(() => {
if (!isBrowser || !isPlaying || !isGenerated) return;
navigator.mediaSession.metadata = new MediaMetadata({
...metadata,
artwork: [
{
sizes: '200x200',
src: artworkURL,
type: 'image/png',
},
],
});
}, [artworkURL, isBrowser, isDarkTheme, isGenerated, isPlaying]);
useEffect(() => {
generateSilence();
}, [generateSilence]);
const startMasterAudio = useCallback(async () => {
if (!masterAudioSoundRef.current) return;
if (!masterAudioSoundRef.current.paused) return;
try {
await masterAudioSoundRef.current.play();
navigator.mediaSession.playbackState = 'playing';
navigator.mediaSession.setActionHandler('play', play);
navigator.mediaSession.setActionHandler('pause', pause);
} catch {
// Do nothing
}
}, [pause, play]);
const stopMasterAudio = useCallback(() => {
if (!masterAudioSoundRef.current) return;
/**
* Otherwise in Safari we cannot play the audio again
* through the media session controls
*/
if (BrowserDetect.isSafari()) {
masterAudioSoundRef.current.load();
} else {
masterAudioSoundRef.current.pause();
}
navigator.mediaSession.playbackState = 'paused';
}, []);
useEffect(() => {
if (!isGenerated) return;
if (!masterAudioSoundRef.current) return;
if (isPlaying) {
startMasterAudio();
} else {
stopMasterAudio();
}
}, [isGenerated, isPlaying, startMasterAudio, stopMasterAudio]);
useEffect(() => {
const masterAudioSound = masterAudioSoundRef.current;
return () => {
masterAudioSound?.pause();
navigator.mediaSession.setActionHandler('play', null);
navigator.mediaSession.setActionHandler('pause', null);
navigator.mediaSession.playbackState = 'none';
};
}, []);
return <audio id="media-session-track" loop ref={masterAudioSoundRef} />;
}

View File

@@ -1,33 +0,0 @@
import styles from './item.module.css';
interface ItemProps {
active?: boolean;
disabled?: boolean;
href?: string;
icon: React.ReactElement;
label: string;
onClick?: () => void;
}
export function Item({
active,
disabled = false,
href,
icon,
label,
onClick = () => {},
}: ItemProps) {
const Comp = href ? 'a' : 'button';
return (
<Comp
className={styles.item}
disabled={disabled}
onClick={onClick}
{...(href ? { href, target: '_blank' } : {})}
>
<span className={styles.icon}>{icon}</span> {label}
{active && <div className={styles.active} />}
</Comp>
);
}

View File

@@ -1,7 +0,0 @@
export { Shuffle as ShuffleItem } from './shuffle';
export { Share as ShareItem } from './share';
export { Donate as DonateItem } from './donate';
export { Notepad as NotepadItem } from './notepad';
export { Source as SourceItem } from './source';
export { Pomodoro as PomodoroItem } from './pomodoro';
export { Presets as PresetsItem } from './presets';

View File

@@ -1,11 +0,0 @@
import { BiShuffle } from 'react-icons/bi/index';
import { useSoundStore } from '@/store';
import { Item } from '../item';
export function Shuffle() {
const shuffle = useSoundStore(state => state.shuffle);
return <Item icon={<BiShuffle />} label="Shuffle Sounds" onClick={shuffle} />;
}

View File

@@ -1,33 +0,0 @@
.wrapper {
& .menuButton {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
font-size: var(--font-md);
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 50%;
transition: 0.2s;
&:hover {
background-color: var(--color-neutral-200);
}
}
& .menu {
display: flex;
flex-direction: column;
row-gap: 4px;
width: 240px;
height: max-content;
padding: 4px;
overflow: auto;
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 4px;
}
}

View File

@@ -1,117 +0,0 @@
import { useState } from 'react';
import { IoMenu, IoClose } from 'react-icons/io5/index';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
size,
useClick,
useDismiss,
useRole,
useInteractions,
FloatingFocusManager,
} from '@floating-ui/react';
import {
ShuffleItem,
ShareItem,
DonateItem,
NotepadItem,
SourceItem,
PomodoroItem,
PresetsItem,
} from './items';
import { Divider } from './divider';
import { ShareLinkModal } from '@/components/modals/share-link';
import { PresetsModal } from '@/components/modals/presets';
import { Notepad, Pomodoro } from '@/components/toolbox';
import styles from './menu.module.css';
export function Menu() {
const [isOpen, setIsOpen] = useState(false);
const [showPresets, setShowPresets] = useState(false);
const [showShareLink, setShowShareLink] = useState(false);
const [showNotepad, setShowNotepad] = useState(false);
const [showPomodoro, setShowPomodoro] = useState(false);
const { context, floatingStyles, refs } = useFloating({
middleware: [
offset(12),
flip(),
shift(),
size({
apply({ availableHeight, elements }) {
Object.assign(elements.floating.style, {
maxHeight: `${availableHeight}px`,
});
},
padding: 10,
}),
],
onOpenChange: setIsOpen,
open: isOpen,
placement: 'top-end',
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);
const { getFloatingProps, getReferenceProps } = useInteractions([
click,
dismiss,
role,
]);
return (
<>
<div className={styles.wrapper}>
<button
aria-label="Menu"
className={styles.menuButton}
ref={refs.setReference}
onClick={() => setIsOpen(prev => !prev)}
{...getReferenceProps()}
>
{isOpen ? <IoClose /> : <IoMenu />}
</button>
{isOpen && (
<FloatingFocusManager context={context} modal={false}>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className={styles.menu}
>
<PresetsItem open={() => setShowPresets(true)} />
<ShareItem open={() => setShowShareLink(true)} />
<ShuffleItem />
<Divider />
<NotepadItem open={() => setShowNotepad(true)} />
<PomodoroItem open={() => setShowPomodoro(true)} />
<Divider />
<DonateItem />
<SourceItem />
</div>
</FloatingFocusManager>
)}
</div>
<ShareLinkModal
show={showShareLink}
onClose={() => setShowShareLink(false)}
/>
<PresetsModal show={showPresets} onClose={() => setShowPresets(false)} />
<Notepad show={showNotepad} onClose={() => setShowNotepad(false)} />
<Pomodoro show={showPomodoro} onClose={() => setShowPomodoro(false)} />
</>
);
}

View File

@@ -50,7 +50,13 @@
cursor: pointer;
background-color: transparent;
border: none;
border-radius: 4px;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
}
}

View File

@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Modal } from './modal';
const meta: Meta<typeof Modal> = {
component: Modal,
title: 'Modal',
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Hello World',
show: true,
},
};
export const Wide: Story = {
args: {
...Default.args,
wide: true,
},
};

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { IoClose } from 'react-icons/io5/index';
import FocusTrap from 'focus-trap-react';
import { Portal } from '@/components/portal';
@@ -13,14 +14,18 @@ interface ModalProps {
children: React.ReactNode;
lockBody?: boolean;
onClose: () => void;
persist?: boolean;
show: boolean;
wide?: boolean;
}
const TRANSITION_DURATION = 300;
export function Modal({
children,
lockBody = true,
onClose,
persist = false,
show,
wide,
}: ModalProps) {
@@ -31,44 +36,72 @@ export function Modal({
useEffect(() => {
if (show && lockBody) {
document.body.style.overflow = 'hidden';
document.body.style.overflowY = 'hidden';
} else if (lockBody) {
document.body.style.overflow = 'auto';
// Wait for transition to finish before allowing scrollbar to return
setTimeout(() => {
document.body.style.overflowY = 'auto';
}, TRANSITION_DURATION);
}
}, [show, lockBody]);
useEffect(() => {
function keyListener(e: KeyboardEvent) {
if (show && e.key === 'Escape') {
onClose();
}
}
document.addEventListener('keydown', keyListener);
return () => document.removeEventListener('keydown', keyListener);
}, [onClose, show]);
const animationProps = persist
? {
animate: show ? 'show' : 'hidden',
}
: {
animate: 'show',
exit: 'hidden',
initial: 'hidden',
};
const content = (
<FocusTrap active={show}>
<div>
<motion.div
{...animationProps}
className={styles.overlay}
transition={{ duration: TRANSITION_DURATION / 1000 }}
variants={variants.overlay}
onClick={onClose}
onKeyDown={onClose}
/>
<div className={styles.modal}>
<motion.div
{...animationProps}
className={cn(styles.content, wide && styles.wide)}
transition={{ duration: TRANSITION_DURATION / 1000 }}
variants={variants.modal}
>
<button className={styles.close} onClick={onClose}>
<IoClose />
</button>
{children}
</motion.div>
</div>
</div>
</FocusTrap>
);
return (
<Portal>
<AnimatePresence>
{show && (
<>
<motion.div
animate="show"
className={styles.overlay}
exit="hidden"
initial="hidden"
variants={variants.overlay}
onClick={onClose}
onKeyDown={onClose}
/>
<div className={styles.modal}>
<motion.div
animate="show"
className={cn(styles.content, wide && styles.wide)}
exit="hidden"
initial="hidden"
variants={variants.modal}
>
<button className={styles.close} onClick={onClose}>
<IoClose />
</button>
{children}
</motion.div>
</div>
</>
)}
</AnimatePresence>
{persist ? (
<div style={{ display: show ? 'block' : 'none' }}>{content}</div>
) : (
<AnimatePresence>{show && content}</AnimatePresence>
)}
</Portal>
);
}

View File

@@ -0,0 +1,76 @@
.header {
margin-bottom: 16px;
& .title {
margin-bottom: 4px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .desc {
color: var(--color-foreground-subtle);
}
}
.fieldWrapper {
margin-bottom: 12px;
& label {
display: block;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
& input,
select {
display: block;
width: 100%;
min-width: 0;
height: 45px;
padding: 0 8px;
margin-top: 4px;
color: var(--color-foreground);
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
outline: none;
}
& .volume {
margin-top: 4px;
}
}
}
.buttons {
display: flex;
column-gap: 8px;
align-items: center;
margin-top: 12px;
& button {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
height: 45px;
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-200);
border: none;
border-radius: 8px;
outline: none;
&:disabled {
cursor: not-allowed;
opacity: 0.3;
}
&.primary {
color: var(--color-neutral-50);
background-color: var(--color-neutral-950);
}
}
}

View File

@@ -0,0 +1,223 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider';
import styles from './binaural.module.css';
interface BinauralProps {
onClose: () => void;
show: boolean;
}
interface Preset {
baseFrequency: number;
beatFrequency: number;
name: string;
}
const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
];
export function BinauralModal({ onClose, show }: BinauralProps) {
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
const audioContextRef = useRef<AudioContext | null>(null);
const leftOscillatorRef = useRef<OscillatorNode | null>(null);
const rightOscillatorRef = useRef<OscillatorNode | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
const startSound = () => {
if (isPlaying) return;
// Initialize the AudioContext
audioContextRef.current = new window.AudioContext();
const audioContext = audioContextRef.current;
if (!audioContext) return;
// Create a gain node for volume control
gainNodeRef.current = audioContext.createGain();
gainNodeRef.current.gain.value = volume; // Set volume based on state
// Create oscillators for left and right channels
leftOscillatorRef.current = audioContext.createOscillator();
rightOscillatorRef.current = audioContext.createOscillator();
if (
!leftOscillatorRef.current ||
!rightOscillatorRef.current ||
!gainNodeRef.current
)
return;
leftOscillatorRef.current.frequency.value =
baseFrequency - beatFrequency / 2;
rightOscillatorRef.current.frequency.value =
baseFrequency + beatFrequency / 2;
// Pan oscillators to left and right
const leftPanner = audioContext.createStereoPanner();
leftPanner.pan.value = -1;
const rightPanner = audioContext.createStereoPanner();
rightPanner.pan.value = 1;
// Connect nodes
leftOscillatorRef.current.connect(leftPanner).connect(gainNodeRef.current);
rightOscillatorRef.current
.connect(rightPanner)
.connect(gainNodeRef.current);
gainNodeRef.current.connect(audioContext.destination);
// Start oscillators
leftOscillatorRef.current.start();
rightOscillatorRef.current.start();
setIsPlaying(true);
};
const stopSound = useCallback(() => {
if (!isPlaying) return;
leftOscillatorRef.current?.stop();
rightOscillatorRef.current?.stop();
audioContextRef.current?.close();
setIsPlaying(false);
}, [isPlaying]);
useEffect(() => {
// Update gain node when volume changes
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = volume;
}
}, [volume]);
useEffect(() => {
// Cleanup when component unmounts
return () => {
if (isPlaying) {
stopSound();
}
};
}, [isPlaying, stopSound]);
useEffect(() => {
// Update frequencies when a preset is selected
if (selectedPreset !== 'Custom') {
const preset = presets.find(p => p.name === selectedPreset);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
}
}, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
setSelectedPreset(selected);
if (selected === 'Custom') {
// Allow user to input custom frequencies
return;
}
const preset = presets.find(p => p.name === selected);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
};
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Binaural Beat</h2>
<p className={styles.desc}>Binaural beat generator.</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
</option>
))}
</select>
</label>
</div>
{selectedPreset === 'Custom' && (
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
<input
max="1500"
min="20"
step="0.1"
type="number"
value={baseFrequency}
onChange={e =>
setBaseFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
<div className={styles.fieldWrapper}>
<label>
Beat Frequency (Hz):
<input
max="40"
min="0.1"
step="0.1"
type="number"
value={beatFrequency}
onChange={e =>
setBeatFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
</>
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
<Slider
className={styles.volume}
max={1}
min={0}
step={0.01}
value={volume}
onChange={value => setVolume(value)}
/>
</label>
</div>
<div className={styles.buttons}>
<button
className={styles.primary}
disabled={isPlaying}
onClick={startSound}
>
Start
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
</button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1 @@
export { BinauralModal } from './binaural';

View File

@@ -0,0 +1 @@
/* WIP */

View File

@@ -0,0 +1,18 @@
import { Modal } from '@/components/modal';
import { Exercise } from './exercise';
import styles from './breathing.module.css';
interface TimerProps {
onClose: () => void;
show: boolean;
}
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Breathing Exercise</h2>
<Exercise />
</Modal>
);
}

View File

@@ -0,0 +1,91 @@
.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;
& .timer {
position: absolute;
top: 4px;
left: 4px;
padding: 4px 12px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(
var(--color-neutral-100),
var(--color-neutral-50)
);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
}
& .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%);
}
}
.selectWrapper {
position: relative;
width: 100%;
height: 45px;
padding: 0 12px;
margin-top: 8px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 80%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
& .selectBox {
width: 100%;
min-width: 0;
height: 100%;
font-size: var(--font-sm);
color: var(--color-foreground);
background-color: transparent;
border: none;
outline: none;
& option {
color: var(--color-neutral-50);
}
}
}

View File

@@ -0,0 +1,126 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { motion } from 'framer-motion';
import { padNumber } from '@/helpers/number';
import styles from './exercise.module.css';
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
'4-7-8 Breathing': ['inhale', 'holdInhale', 'exhale'],
'Box Breathing': ['inhale', 'holdInhale', 'exhale', 'holdExhale'],
'Resonant Breathing': ['inhale', 'exhale'],
};
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 },
'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale
};
const PHASE_LABELS: Record<Phase, string> = {
exhale: 'Exhale',
holdExhale: 'Hold',
holdInhale: 'Hold',
inhale: 'Inhale',
};
export function Exercise() {
const [selectedExercise, setSelectedExercise] =
useState<Exercise>('4-7-8 Breathing');
const [phaseIndex, setPhaseIndex] = useState(0);
const phases = useMemo(
() => EXERCISE_PHASES[selectedExercise],
[selectedExercise],
);
const durations = useMemo(
() => EXERCISE_DURATIONS[selectedExercise],
[selectedExercise],
);
const currentPhase = phases[phaseIndex];
const animationVariants = useMemo(
() => ({
exhale: {
transform: 'translate(-50%, -50%) scale(1)',
transition: { duration: durations.exhale },
},
holdExhale: {
transform: 'translate(-50%, -50%) scale(1)',
transition: { duration: durations.holdExhale },
},
holdInhale: {
transform: 'translate(-50%, -50%) scale(1.5)',
transition: { duration: durations.holdInhale },
},
inhale: {
transform: 'translate(-50%, -50%) scale(1.5)',
transition: { duration: durations.inhale },
},
}),
[durations],
);
const resetExercise = useCallback(() => {
setPhaseIndex(0);
}, []);
const updatePhase = useCallback(() => {
setPhaseIndex(prevIndex => (prevIndex + 1) % phases.length);
}, [phases.length]);
useEffect(() => {
resetExercise();
}, [selectedExercise, resetExercise]);
useEffect(() => {
const intervalDuration = (durations[currentPhase] || 4) * 1000;
const interval = setInterval(updatePhase, intervalDuration);
return () => clearInterval(interval);
}, [currentPhase, durations, updatePhase]);
const [timer, setTimer] = useState(0);
useEffect(() => {
const interval = setInterval(() => setTimer(prev => prev + 1), 1000);
return () => clearInterval(interval);
}, []);
return (
<>
<div className={styles.exercise}>
<div className={styles.timer}>
{padNumber(Math.floor(timer / 60))}:{padNumber(timer % 60)}
</div>
<motion.div
animate={currentPhase}
className={styles.circle}
key={selectedExercise}
variants={animationVariants}
/>
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
</div>
<div className={styles.selectWrapper}>
<select
className={styles.selectBox}
value={selectedExercise}
onChange={e => setSelectedExercise(e.target.value as Exercise)}
>
{Object.keys(EXERCISE_PHASES).map(exercise => (
<option key={exercise} value={exercise}>
{exercise}
</option>
))}
</select>
</div>
</>
);
}

View File

@@ -0,0 +1 @@
export { Exercise } from './exercise';

View File

@@ -0,0 +1 @@
export { BreathingExerciseModal } from './breathing';

View File

@@ -0,0 +1 @@
export { IsochronicModal } from './isochronic';

View File

@@ -0,0 +1,76 @@
.header {
margin-bottom: 16px;
& .title {
margin-bottom: 4px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .desc {
color: var(--color-foreground-subtle);
}
}
.fieldWrapper {
margin-bottom: 12px;
& label {
display: block;
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground);
& input,
select {
display: block;
width: 100%;
min-width: 0;
height: 45px;
padding: 0 8px;
margin-top: 4px;
color: var(--color-foreground);
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
outline: none;
}
& .volume {
margin-top: 4px;
}
}
}
.buttons {
display: flex;
column-gap: 8px;
align-items: center;
margin-top: 12px;
& button {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
height: 45px;
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-200);
border: none;
border-radius: 8px;
outline: none;
&:disabled {
cursor: not-allowed;
opacity: 0.3;
}
&.primary {
color: var(--color-neutral-50);
background-color: var(--color-neutral-950);
}
}
}

View File

@@ -0,0 +1,258 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider';
import styles from './isochornic.module.css';
interface IsochronicProps {
onClose: () => void;
show: boolean;
}
interface Preset {
baseFrequency: number;
beatFrequency: number;
name: string;
}
const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
];
export function IsochronicModal({ onClose, show }: IsochronicProps) {
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
const [waveform] = useState<OscillatorType>('sine'); // Default waveform
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
const audioContextRef = useRef<AudioContext | null>(null);
const oscillatorRef = useRef<OscillatorNode | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
const beatGainRef = useRef<GainNode | null>(null);
const modulatorRef = useRef<OscillatorNode | null>(null);
const startSound = () => {
if (isPlaying) return;
audioContextRef.current = new window.AudioContext();
const audioContext = audioContextRef.current;
if (!audioContext) return;
// Main gain node for volume control
gainNodeRef.current = audioContext.createGain();
gainNodeRef.current.gain.value = volume;
// Oscillator for the base tone
oscillatorRef.current = audioContext.createOscillator();
oscillatorRef.current.frequency.value = baseFrequency;
oscillatorRef.current.type = waveform;
// Gain node to create isochronic beats
beatGainRef.current = audioContext.createGain();
beatGainRef.current.gain.value = 0; // Start with silence
// Oscillator for modulation
modulatorRef.current = audioContext.createOscillator();
modulatorRef.current.frequency.value = beatFrequency;
modulatorRef.current.type = 'square'; // Square wave for on/off effect
// Modulator gain to adjust modulation depth
const modulatorGain = audioContext.createGain();
modulatorGain.gain.value = 0.5; // Modulation depth
// Connect modulator to the beat gain node
modulatorRef.current
.connect(modulatorGain)
.connect(beatGainRef.current.gain);
// Connect oscillator through beat gain and main gain to destination
oscillatorRef.current
.connect(beatGainRef.current)
.connect(gainNodeRef.current)
.connect(audioContext.destination);
// Start oscillators
oscillatorRef.current.start();
modulatorRef.current.start();
setIsPlaying(true);
};
const stopSound = useCallback(() => {
if (!isPlaying) return;
oscillatorRef.current?.stop();
modulatorRef.current?.stop();
audioContextRef.current?.close();
setIsPlaying(false);
}, [isPlaying]);
useEffect(() => {
// Update gain when volume changes
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = volume;
}
}, [volume]);
useEffect(() => {
// Update base frequency when it changes
if (oscillatorRef.current) {
oscillatorRef.current.frequency.value = baseFrequency;
}
}, [baseFrequency]);
useEffect(() => {
// Update beat frequency when it changes
if (modulatorRef.current) {
modulatorRef.current.frequency.value = beatFrequency;
}
}, [beatFrequency]);
useEffect(() => {
// Update waveform when it changes
if (oscillatorRef.current) {
oscillatorRef.current.type = waveform;
}
}, [waveform]);
useEffect(() => {
// Cleanup when component unmounts
return () => {
if (isPlaying) {
stopSound();
}
};
}, [isPlaying, stopSound]);
useEffect(() => {
// Update frequencies when a preset is selected
if (selectedPreset !== 'Custom') {
const preset = presets.find(p => p.name === selectedPreset);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
}
}, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
setSelectedPreset(selected);
if (selected === 'Custom') {
// Allow user to input custom frequencies
return;
}
const preset = presets.find(p => p.name === selected);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
};
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Isochronic Tone</h2>
<p className={styles.desc}>Isochronic tone generator.</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
</option>
))}
</select>
</label>
</div>
{selectedPreset === 'Custom' && (
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
<input
max="2000"
min="20"
step="0.1"
type="number"
value={baseFrequency}
onChange={e =>
setBaseFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
<div className={styles.fieldWrapper}>
<label>
Tone Frequency (Hz):
<input
max="40"
min="0.1"
step="0.1"
type="number"
value={beatFrequency}
onChange={e =>
setBeatFrequency(parseFloat(e.target.value || '0'))
}
/>
</label>
</div>
{/* <div className={styles.fieldWrapper}>
<label>
Waveform:
<select
value={waveform}
onChange={e => setWaveform(e.target.value as OscillatorType)}
>
<option value="sine">Sine</option>
<option value="square">Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle">Triangle</option>
</select>
</label>
</div> */}
</>
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
<Slider
className={styles.volume}
max={1}
min={0}
step={0.01}
value={volume}
onChange={value => setVolume(value)}
/>
</label>
</div>
<div className={styles.buttons}>
<button
className={styles.primary}
disabled={isPlaying}
onClick={startSound}
>
Start
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
</button>
</div>
</Modal>
);
}

View File

@@ -34,6 +34,11 @@
background: transparent;
border: none;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
& button {
@@ -51,6 +56,11 @@
border-radius: 4px;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
&.primary {
font-size: var(--font-xsm);
color: var(--color-foreground);

View File

@@ -2,7 +2,8 @@ import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
import styles from './list.module.css';
import { usePresetStore, useSoundStore } from '@/store';
import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset';
interface ListProps {
close: () => void;
@@ -25,15 +26,15 @@ export function List({ close }: ListProps) {
<p className={styles.empty}>You don&apos;t have any presets yet.</p>
)}
{presets.map((preset, index) => (
<div className={styles.preset} key={index}>
{presets.map(preset => (
<div className={styles.preset} key={preset.id}>
<input
placeholder="Untitled"
type="text"
value={preset.label}
onChange={e => changeName(index, e.target.value)}
onChange={e => changeName(preset.id, e.target.value)}
/>
<button onClick={() => deletePreset(index)}>
<button onClick={() => deletePreset(preset.id)}>
<FaRegTrashAlt />
</button>
<button

View File

@@ -31,6 +31,11 @@
background: transparent;
border: none;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
& button {
@@ -48,6 +53,11 @@
border-radius: 4px;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
&:disabled {
cursor: not-allowed;
}

View File

@@ -1,7 +1,8 @@
import { useState, type FormEvent } from 'react';
import { cn } from '@/helpers/styles';
import { useSoundStore, usePresetStore } from '@/store';
import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset';
import styles from './new.module.css';

View File

@@ -31,6 +31,11 @@
background: transparent;
border: none;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
& button {
@@ -47,7 +52,13 @@
outline: none;
transition: 0.2s;
&:hover {
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
&:hover,
&:focus-visible {
background-color: var(--color-neutral-200);
}
}

View File

@@ -4,7 +4,7 @@ import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
import { Modal } from '@/components/modal';
import { useCopy } from '@/hooks/use-copy';
import { useSoundStore } from '@/store';
import { useSoundStore } from '@/stores/sound';
import styles from './share-link.module.css';

View File

@@ -51,7 +51,13 @@
outline: none;
transition: 0.2s;
&:hover {
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
&:hover,
&:focus-visible {
color: var(--color-foreground);
background-color: var(--color-neutral-300);
}
@@ -60,7 +66,8 @@
color: var(--color-neutral-200);
background-color: var(--color-neutral-950);
&:hover {
&:hover,
&:focus-visible {
background-color: var(--color-neutral-800);
}
}

Some files were not shown because too many files have changed in this diff Show More