@akshatbuilds/sonix 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/globals.css +110 -0
- package/app/layout.tsx +54 -0
- package/app/page.tsx +40 -0
- package/app/usage/page.tsx +9 -0
- package/cli/index.mjs +151 -0
- package/components/sound-card.tsx +595 -0
- package/components/sounds-gallery.tsx +137 -0
- package/components/theme-provider.tsx +11 -0
- package/components/theme-toggle.tsx +82 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/checkbox.tsx +30 -0
- package/components/ui/slider.tsx +28 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/usage-guide.tsx +155 -0
- package/components.json +21 -0
- package/lib/sounds.ts +329 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +5 -0
- package/package.json +96 -0
- package/postcss.config.mjs +8 -0
- package/public/click1.mp3 +0 -0
- package/public/click2.mp3 +0 -0
- package/public/registry/index.json +92 -0
- package/public/registry/sounds/button-click-secondary.json +13 -0
- package/public/registry/sounds/button-click.json +13 -0
- package/public/registry/sounds/error-beep.json +10 -0
- package/public/registry/sounds/error-buzz.json +10 -0
- package/public/registry/sounds/hover-blip.json +10 -0
- package/public/registry/sounds/hover-soft.json +10 -0
- package/public/registry/sounds/key-press.json +10 -0
- package/public/registry/sounds/notification-ping.json +10 -0
- package/public/registry/sounds/notification-subtle.json +10 -0
- package/public/registry/sounds/pop.json +10 -0
- package/public/registry/sounds/slider-tick.json +10 -0
- package/public/registry/sounds/success-bell.json +10 -0
- package/public/registry/sounds/success-chime.json +10 -0
- package/public/registry/sounds/swoosh.json +10 -0
- package/scripts/build-registry.mjs +293 -0
- package/tailwind.config.ts +100 -0
- package/tsconfig.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 abhi-yo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Sonix — UI Sound Effects Library
|
|
2
|
+
|
|
3
|
+
Micro UX audio for interactive web applications. Zero dependencies. Copy-paste ready.
|
|
4
|
+
|
|
5
|
+
[Live Demo](https://abhi-yo.github.io/sound-library) · [Browse Sounds](#available-sounds) · [CLI](#cli)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **14 sounds** — clicks, hovers, success, error, notifications, transitions, and more
|
|
12
|
+
- **Zero dependencies** — every snippet is standalone Web Audio API code
|
|
13
|
+
- **Copy & paste** — hit the copy button on any card to get production-ready code
|
|
14
|
+
- **CLI** — `npx sonix add pop` to scaffold sounds into your project
|
|
15
|
+
- **Framework agnostic** — works with React, Vue, Svelte, vanilla JS, anything
|
|
16
|
+
- **Dark mode** — with cinematic circular reveal transition
|
|
17
|
+
- **`prefers-reduced-motion`** — respects user accessibility preferences
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Web Audio (no files needed)
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const ctx = new AudioContext();
|
|
25
|
+
const osc = ctx.createOscillator();
|
|
26
|
+
const gain = ctx.createGain();
|
|
27
|
+
osc.type = 'sine';
|
|
28
|
+
osc.frequency.setValueAtTime(800, ctx.currentTime);
|
|
29
|
+
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
|
30
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
|
|
31
|
+
osc.connect(gain).connect(ctx.destination);
|
|
32
|
+
osc.start();
|
|
33
|
+
osc.stop(ctx.currentTime + 0.15);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### MP3 (for button clicks)
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const audio = new Audio('/click1.mp3');
|
|
40
|
+
audio.volume = 0.5;
|
|
41
|
+
audio.play();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### React Hook
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { playSound } from './lib/sounds';
|
|
48
|
+
|
|
49
|
+
function MyButton() {
|
|
50
|
+
return <button onClick={() => playSound('pop')}>Click</button>;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## CLI
|
|
55
|
+
|
|
56
|
+
Add sounds to any project with the CLI:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx sonix list # List all available sounds
|
|
60
|
+
npx sonix add pop # Add a single sound
|
|
61
|
+
npx sonix add pop swoosh # Add multiple sounds
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Sounds are written to `lib/sounds/` as standalone files — no runtime dependency.
|
|
65
|
+
|
|
66
|
+
## Available Sounds
|
|
67
|
+
|
|
68
|
+
| Name | Category | Source | Use Case |
|
|
69
|
+
|------|----------|--------|----------|
|
|
70
|
+
| `button-click` | Interaction | MP3 | Primary button presses |
|
|
71
|
+
| `button-click-secondary` | Interaction | MP3 | Secondary / ghost buttons |
|
|
72
|
+
| `hover-blip` | Feedback | Web Audio | Hovering over interactive elements |
|
|
73
|
+
| `hover-soft` | Feedback | Web Audio | Subtle hover feedback |
|
|
74
|
+
| `success-chime` | Notification | Web Audio | Form submissions, saves |
|
|
75
|
+
| `success-bell` | Notification | Web Audio | Achievements, milestones |
|
|
76
|
+
| `error-buzz` | Alert | Web Audio | Validation errors, failed actions |
|
|
77
|
+
| `error-beep` | Alert | Web Audio | Warnings, blocked actions |
|
|
78
|
+
| `notification-ping` | Alert | Web Audio | New messages, alerts |
|
|
79
|
+
| `notification-subtle` | Alert | Web Audio | Background updates |
|
|
80
|
+
| `swoosh` | Transition | Web Audio | Page transitions, panel slides |
|
|
81
|
+
| `pop` | Interaction | Web Audio | Tooltips, popovers, reveals |
|
|
82
|
+
| `slider-tick` | Interaction | Web Audio | Sliders, range inputs |
|
|
83
|
+
| `key-press` | Interaction | Web Audio | Text inputs, keyboards |
|
|
84
|
+
|
|
85
|
+
## Accessibility
|
|
86
|
+
|
|
87
|
+
All sounds respect `prefers-reduced-motion: reduce`. When the user has requested reduced motion, sounds are automatically silenced.
|
|
88
|
+
|
|
89
|
+
To manually control sound:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
// Check user preference
|
|
93
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
94
|
+
|
|
95
|
+
if (!prefersReducedMotion) {
|
|
96
|
+
playSound('pop');
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm install
|
|
104
|
+
pnpm dev
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Build
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm build
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Registry
|
|
114
|
+
|
|
115
|
+
The project includes a shadcn-style registry at `public/registry/`. To rebuild:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node scripts/build-registry.mjs
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT — see [LICENSE](./LICENSE).
|
package/app/globals.css
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer utilities {
|
|
6
|
+
.text-balance {
|
|
7
|
+
text-wrap: balance;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@layer base {
|
|
12
|
+
:root {
|
|
13
|
+
--background: 0 0% 100%;
|
|
14
|
+
--foreground: 0 0% 3.9%;
|
|
15
|
+
--card: 0 0% 100%;
|
|
16
|
+
--card-foreground: 0 0% 3.9%;
|
|
17
|
+
--popover: 0 0% 100%;
|
|
18
|
+
--popover-foreground: 0 0% 3.9%;
|
|
19
|
+
--primary: 0 0% 9%;
|
|
20
|
+
--primary-foreground: 0 0% 98%;
|
|
21
|
+
--secondary: 0 0% 96.1%;
|
|
22
|
+
--secondary-foreground: 0 0% 9%;
|
|
23
|
+
--muted: 0 0% 96.1%;
|
|
24
|
+
--muted-foreground: 0 0% 45.1%;
|
|
25
|
+
--accent: 0 0% 96.1%;
|
|
26
|
+
--accent-foreground: 0 0% 9%;
|
|
27
|
+
--destructive: 0 84.2% 60.2%;
|
|
28
|
+
--destructive-foreground: 0 0% 98%;
|
|
29
|
+
--border: 0 0% 89.8%;
|
|
30
|
+
--input: 0 0% 89.8%;
|
|
31
|
+
--ring: 0 0% 3.9%;
|
|
32
|
+
--chart-1: 12 76% 61%;
|
|
33
|
+
--chart-2: 173 58% 39%;
|
|
34
|
+
--chart-3: 197 37% 24%;
|
|
35
|
+
--chart-4: 43 74% 66%;
|
|
36
|
+
--chart-5: 27 87% 67%;
|
|
37
|
+
--radius: 0.5rem;
|
|
38
|
+
--sidebar-background: 0 0% 98%;
|
|
39
|
+
--sidebar-foreground: 240 5.3% 26.1%;
|
|
40
|
+
--sidebar-primary: 240 5.9% 10%;
|
|
41
|
+
--sidebar-primary-foreground: 0 0% 98%;
|
|
42
|
+
--sidebar-accent: 240 4.8% 95.9%;
|
|
43
|
+
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
44
|
+
--sidebar-border: 220 13% 91%;
|
|
45
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
46
|
+
}
|
|
47
|
+
.dark {
|
|
48
|
+
--background: 0 0% 3.9%;
|
|
49
|
+
--foreground: 0 0% 98%;
|
|
50
|
+
--card: 0 0% 3.9%;
|
|
51
|
+
--card-foreground: 0 0% 98%;
|
|
52
|
+
--popover: 0 0% 3.9%;
|
|
53
|
+
--popover-foreground: 0 0% 98%;
|
|
54
|
+
--primary: 0 0% 98%;
|
|
55
|
+
--primary-foreground: 0 0% 9%;
|
|
56
|
+
--secondary: 0 0% 14.9%;
|
|
57
|
+
--secondary-foreground: 0 0% 98%;
|
|
58
|
+
--muted: 0 0% 14.9%;
|
|
59
|
+
--muted-foreground: 0 0% 63.9%;
|
|
60
|
+
--accent: 0 0% 14.9%;
|
|
61
|
+
--accent-foreground: 0 0% 98%;
|
|
62
|
+
--destructive: 0 62.8% 30.6%;
|
|
63
|
+
--destructive-foreground: 0 0% 98%;
|
|
64
|
+
--border: 0 0% 14.9%;
|
|
65
|
+
--input: 0 0% 14.9%;
|
|
66
|
+
--ring: 0 0% 83.1%;
|
|
67
|
+
--chart-1: 220 70% 50%;
|
|
68
|
+
--chart-2: 160 60% 45%;
|
|
69
|
+
--chart-3: 30 80% 55%;
|
|
70
|
+
--chart-4: 280 65% 60%;
|
|
71
|
+
--chart-5: 340 75% 55%;
|
|
72
|
+
--sidebar-background: 240 5.9% 10%;
|
|
73
|
+
--sidebar-foreground: 240 4.8% 95.9%;
|
|
74
|
+
--sidebar-primary: 224.3 76.3% 48%;
|
|
75
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
76
|
+
--sidebar-accent: 240 3.7% 15.9%;
|
|
77
|
+
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
78
|
+
--sidebar-border: 240 3.7% 15.9%;
|
|
79
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@layer base {
|
|
84
|
+
* {
|
|
85
|
+
@apply border-border;
|
|
86
|
+
}
|
|
87
|
+
body {
|
|
88
|
+
@apply bg-background text-foreground;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Circular reveal via View Transition API for cinematic theme change */
|
|
93
|
+
::view-transition-old(root) {
|
|
94
|
+
animation: none;
|
|
95
|
+
z-index: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
::view-transition-new(root) {
|
|
99
|
+
animation: none;
|
|
100
|
+
z-index: 9999;
|
|
101
|
+
mix-blend-mode: normal;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Honor prefers-reduced-motion (Web Interface Guidelines: Animation) */
|
|
105
|
+
@media (prefers-reduced-motion: reduce) {
|
|
106
|
+
::view-transition-old(root),
|
|
107
|
+
::view-transition-new(root) {
|
|
108
|
+
animation: none !important;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Metadata } from 'next'
|
|
3
|
+
import { Geist, Geist_Mono } from 'next/font/google'
|
|
4
|
+
import { ThemeProvider } from '@/components/theme-provider'
|
|
5
|
+
import { Analytics } from '@vercel/analytics/next'
|
|
6
|
+
|
|
7
|
+
import './globals.css'
|
|
8
|
+
|
|
9
|
+
const geist = Geist({ subsets: ['latin'], variable: '--font-geist' })
|
|
10
|
+
const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' })
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = {
|
|
13
|
+
title: 'Sonix — UI Sound Effects Library',
|
|
14
|
+
description: 'Micro UX audio interactions for web apps. 14 sounds, zero dependencies, copy-paste ready.',
|
|
15
|
+
metadataBase: new URL('https://abhi-yo.github.io/sound-library'),
|
|
16
|
+
openGraph: {
|
|
17
|
+
title: 'Sonix — UI Sound Effects Library',
|
|
18
|
+
description: 'Micro UX audio interactions for web apps. 14 sounds, zero dependencies, copy-paste ready.',
|
|
19
|
+
type: 'website',
|
|
20
|
+
},
|
|
21
|
+
twitter: {
|
|
22
|
+
card: 'summary_large_image',
|
|
23
|
+
title: 'Sonix — UI Sound Effects Library',
|
|
24
|
+
description: 'Micro UX audio interactions for web apps. 14 sounds, zero dependencies, copy-paste ready.',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({
|
|
29
|
+
children,
|
|
30
|
+
}: Readonly<{
|
|
31
|
+
children: React.ReactNode
|
|
32
|
+
}>) {
|
|
33
|
+
return (
|
|
34
|
+
<html lang="en" suppressHydrationWarning className={`${geist.variable} ${geistMono.variable}`}>
|
|
35
|
+
<head>
|
|
36
|
+
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
|
37
|
+
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)" />
|
|
38
|
+
</head>
|
|
39
|
+
<body className="font-sans antialiased">
|
|
40
|
+
<a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground">
|
|
41
|
+
Skip to main content
|
|
42
|
+
</a>
|
|
43
|
+
<ThemeProvider
|
|
44
|
+
attribute="class"
|
|
45
|
+
defaultTheme="system"
|
|
46
|
+
enableSystem
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</ThemeProvider>
|
|
50
|
+
<Analytics />
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
53
|
+
)
|
|
54
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SoundsGallery } from '@/components/sounds-gallery';
|
|
2
|
+
import { UsageGuide } from '@/components/usage-guide';
|
|
3
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
title: 'UI Sound Effects Library',
|
|
7
|
+
description: 'Tiny audio interactions for web apps. Button clicks, hover blips, success sounds, and error feedback.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Home() {
|
|
11
|
+
return (
|
|
12
|
+
<main id="main-content" className="flex min-h-screen flex-col bg-background text-foreground">
|
|
13
|
+
<div className="flex flex-col gap-12 px-4 py-12 sm:px-8 lg:px-16">
|
|
14
|
+
{/* Header */}
|
|
15
|
+
<div className="flex flex-col gap-4 border-b border-border pb-8">
|
|
16
|
+
<div className="flex items-center justify-between">
|
|
17
|
+
<h1 className="font-sans text-4xl font-bold tracking-tight sm:text-5xl">
|
|
18
|
+
Sound Effects
|
|
19
|
+
</h1>
|
|
20
|
+
<ThemeToggle />
|
|
21
|
+
</div>
|
|
22
|
+
<p className="max-w-2xl text-lg text-muted-foreground">
|
|
23
|
+
Micro UX audio for interactive web applications. Click to preview, copy code to use.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Gallery */}
|
|
28
|
+
<SoundsGallery />
|
|
29
|
+
|
|
30
|
+
{/* Usage Guide */}
|
|
31
|
+
<UsageGuide />
|
|
32
|
+
|
|
33
|
+
{/* Footer */}
|
|
34
|
+
<div className="border-t border-border py-8 text-center text-sm text-muted-foreground">
|
|
35
|
+
<p>Click the play button to preview. Use the copy button to get code for your project.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</main>
|
|
39
|
+
);
|
|
40
|
+
}
|
package/cli/index.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sonix CLI — add sounds to your project
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx sonix add <sound-name> Add a single sound
|
|
8
|
+
* npx sonix add <s1> <s2> ... Add multiple sounds
|
|
9
|
+
* npx sonix list List all available sounds
|
|
10
|
+
*
|
|
11
|
+
* Sounds are fetched from the public registry and written
|
|
12
|
+
* to your project as standalone files — no runtime dependency.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
const REGISTRY_URL =
|
|
19
|
+
process.env.SONIX_REGISTRY ||
|
|
20
|
+
"https://abhi-yo.github.io/sound-library/registry";
|
|
21
|
+
|
|
22
|
+
const COLORS = {
|
|
23
|
+
reset: "\x1b[0m",
|
|
24
|
+
green: "\x1b[32m",
|
|
25
|
+
cyan: "\x1b[36m",
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
red: "\x1b[31m",
|
|
28
|
+
dim: "\x1b[2m",
|
|
29
|
+
bold: "\x1b[1m",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function log(msg) {
|
|
33
|
+
console.log(msg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function success(msg) {
|
|
37
|
+
log(`${COLORS.green}✓${COLORS.reset} ${msg}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function warn(msg) {
|
|
41
|
+
log(`${COLORS.yellow}⚠${COLORS.reset} ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function error(msg) {
|
|
45
|
+
log(`${COLORS.red}✗${COLORS.reset} ${msg}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchJSON(url) {
|
|
49
|
+
const res = await fetch(url);
|
|
50
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
|
51
|
+
return res.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function listSounds() {
|
|
55
|
+
try {
|
|
56
|
+
const registry = await fetchJSON(`${REGISTRY_URL}/index.json`);
|
|
57
|
+
log("");
|
|
58
|
+
log(`${COLORS.bold}Available sounds (${registry.sounds.length}):${COLORS.reset}`);
|
|
59
|
+
log("");
|
|
60
|
+
|
|
61
|
+
const categories = {};
|
|
62
|
+
for (const s of registry.sounds) {
|
|
63
|
+
const cat = s.category || "other";
|
|
64
|
+
if (!categories[cat]) categories[cat] = [];
|
|
65
|
+
categories[cat].push(s);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [cat, sounds] of Object.entries(categories)) {
|
|
69
|
+
log(` ${COLORS.cyan}${cat}${COLORS.reset}`);
|
|
70
|
+
for (const s of sounds) {
|
|
71
|
+
const badge = s.type === "mp3" ? `${COLORS.yellow}mp3${COLORS.reset}` : `${COLORS.dim}web audio${COLORS.reset}`;
|
|
72
|
+
log(` ${s.name.padEnd(24)} ${badge} ${COLORS.dim}${s.description}${COLORS.reset}`);
|
|
73
|
+
}
|
|
74
|
+
log("");
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
error(`Could not fetch registry: ${err.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function addSounds(names) {
|
|
83
|
+
const outDir = join(process.cwd(), "lib", "sounds");
|
|
84
|
+
|
|
85
|
+
if (!existsSync(outDir)) {
|
|
86
|
+
mkdirSync(outDir, { recursive: true });
|
|
87
|
+
success(`Created ${COLORS.cyan}lib/sounds/${COLORS.reset}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
try {
|
|
92
|
+
const sound = await fetchJSON(`${REGISTRY_URL}/sounds/${name}.json`);
|
|
93
|
+
|
|
94
|
+
for (const file of sound.files) {
|
|
95
|
+
const filePath = join(outDir, file.name);
|
|
96
|
+
writeFileSync(filePath, file.content + "\n");
|
|
97
|
+
success(`Added ${COLORS.cyan}lib/sounds/${file.name}${COLORS.reset}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (sound.assets?.length) {
|
|
101
|
+
warn(
|
|
102
|
+
`${name} requires asset files: ${sound.assets.join(", ")} — download them from the website and put in your public/ folder.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
error(`Could not add "${name}": ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log("");
|
|
111
|
+
log(`${COLORS.dim}Import and use:${COLORS.reset}`);
|
|
112
|
+
log(`${COLORS.dim} import { play${toPascal(names[0])} } from './lib/sounds/${names[0]}';${COLORS.reset}`);
|
|
113
|
+
log("");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toPascal(str) {
|
|
117
|
+
return str
|
|
118
|
+
.split("-")
|
|
119
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
120
|
+
.join("");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Main ---
|
|
124
|
+
|
|
125
|
+
const args = process.argv.slice(2);
|
|
126
|
+
const command = args[0];
|
|
127
|
+
|
|
128
|
+
if (!command || command === "help" || command === "--help") {
|
|
129
|
+
log("");
|
|
130
|
+
log(`${COLORS.bold}sonix${COLORS.reset} — UI sound effects for your app`);
|
|
131
|
+
log("");
|
|
132
|
+
log(` ${COLORS.cyan}npx sonix list${COLORS.reset} List all sounds`);
|
|
133
|
+
log(` ${COLORS.cyan}npx sonix add <name>${COLORS.reset} Add a sound to your project`);
|
|
134
|
+
log(` ${COLORS.cyan}npx sonix add pop swoosh${COLORS.reset} Add multiple sounds`);
|
|
135
|
+
log("");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === "list") {
|
|
140
|
+
await listSounds();
|
|
141
|
+
} else if (command === "add") {
|
|
142
|
+
const names = args.slice(1);
|
|
143
|
+
if (names.length === 0) {
|
|
144
|
+
error("Specify at least one sound name. Run `npx sonix list` to see options.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
await addSounds(names);
|
|
148
|
+
} else {
|
|
149
|
+
error(`Unknown command: ${command}. Run \`npx sonix --help\`.`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|