@thewhateverapp/tile-sdk 0.20.0 → 0.20.2
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/dist/audio/index.d.ts +48 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +182 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +12 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
type HowlStatic = any;
|
|
2
|
+
export interface SoundOptions {
|
|
3
|
+
volume?: number;
|
|
4
|
+
loop?: boolean;
|
|
5
|
+
rate?: number;
|
|
6
|
+
sprite?: Record<string, [number, number]>;
|
|
7
|
+
onload?: () => void;
|
|
8
|
+
onloaderror?: (id: number, error: any) => void;
|
|
9
|
+
onplay?: () => void;
|
|
10
|
+
onend?: () => void;
|
|
11
|
+
onstop?: () => void;
|
|
12
|
+
onpause?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface SoundController {
|
|
15
|
+
play: (spriteId?: string) => void;
|
|
16
|
+
pause: () => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
volume: (vol?: number) => number | void;
|
|
19
|
+
rate: (rate?: number) => number | void;
|
|
20
|
+
loop: (loop?: boolean) => boolean | void;
|
|
21
|
+
playing: () => boolean;
|
|
22
|
+
duration: () => number;
|
|
23
|
+
state: () => 'unloaded' | 'loading' | 'loaded';
|
|
24
|
+
unload: () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Hook for playing sound effects
|
|
28
|
+
*
|
|
29
|
+
* @param src - URL to the audio file
|
|
30
|
+
* @param options - Howler options
|
|
31
|
+
* @returns Sound controller
|
|
32
|
+
*/
|
|
33
|
+
export declare function useSound(src: string, options?: SoundOptions): SoundController;
|
|
34
|
+
/**
|
|
35
|
+
* Hook for playing background music with auto-play on mount
|
|
36
|
+
*
|
|
37
|
+
* @param src - URL to the music file
|
|
38
|
+
* @param options - Howler options (loop defaults to true for music)
|
|
39
|
+
* @returns Sound controller
|
|
40
|
+
*/
|
|
41
|
+
export declare function useMusic(src: string, options?: SoundOptions): SoundController;
|
|
42
|
+
/**
|
|
43
|
+
* Re-export Howl for advanced use cases
|
|
44
|
+
* Returns null during SSR, the Howl class after client-side load
|
|
45
|
+
*/
|
|
46
|
+
export declare function getHowl(): Promise<HowlStatic | null>;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audio/index.ts"],"names":[],"mappings":"AA4BA,KAAK,UAAU,GAAG,GAAG,CAAC;AA6BtB,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACxC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACvC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,CAAC;IACzC,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/C,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,eAAe,CAoGjF;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,eAAe,CAgBjF;AAED;;;GAGG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAI1D"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* Audio SDK for tile-sdk
|
|
4
|
+
*
|
|
5
|
+
* SSR-safe wrapper around Howler.js with React hooks for easy audio management.
|
|
6
|
+
* Handles dynamic import, lifecycle management, and provides a clean API.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { useSound, useMusic } from '@thewhateverapp/tile-sdk/audio';
|
|
11
|
+
*
|
|
12
|
+
* function MyGame() {
|
|
13
|
+
* const jumpSound = useSound('/sounds/jump.mp3');
|
|
14
|
+
* const bgMusic = useMusic('/music/theme.mp3', { volume: 0.3, loop: true });
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <button onClick={() => jumpSound.play()}>
|
|
18
|
+
* Jump
|
|
19
|
+
* </button>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { useEffect, useRef, useState } from 'react';
|
|
25
|
+
let Howl = null;
|
|
26
|
+
let howlerLoaded = false;
|
|
27
|
+
let howlerPromise = null;
|
|
28
|
+
/**
|
|
29
|
+
* Load Howler dynamically (client-side only)
|
|
30
|
+
*/
|
|
31
|
+
async function loadHowler() {
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
throw new Error('Howler can only be loaded in the browser');
|
|
34
|
+
}
|
|
35
|
+
if (howlerLoaded)
|
|
36
|
+
return;
|
|
37
|
+
if (howlerPromise) {
|
|
38
|
+
return howlerPromise;
|
|
39
|
+
}
|
|
40
|
+
howlerPromise = import('howler').then((module) => {
|
|
41
|
+
Howl = module.Howl;
|
|
42
|
+
howlerLoaded = true;
|
|
43
|
+
});
|
|
44
|
+
return howlerPromise;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hook for playing sound effects
|
|
48
|
+
*
|
|
49
|
+
* @param src - URL to the audio file
|
|
50
|
+
* @param options - Howler options
|
|
51
|
+
* @returns Sound controller
|
|
52
|
+
*/
|
|
53
|
+
export function useSound(src, options = {}) {
|
|
54
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
55
|
+
const howlRef = useRef(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
loadHowler().then(() => {
|
|
59
|
+
if (cancelled || !Howl)
|
|
60
|
+
return;
|
|
61
|
+
const howl = new Howl({
|
|
62
|
+
src: [src],
|
|
63
|
+
volume: options.volume ?? 1.0,
|
|
64
|
+
loop: options.loop ?? false,
|
|
65
|
+
rate: options.rate ?? 1.0,
|
|
66
|
+
sprite: options.sprite,
|
|
67
|
+
onload: () => {
|
|
68
|
+
setIsLoaded(true);
|
|
69
|
+
options.onload?.();
|
|
70
|
+
},
|
|
71
|
+
onloaderror: options.onloaderror,
|
|
72
|
+
onplay: options.onplay,
|
|
73
|
+
onend: options.onend,
|
|
74
|
+
onstop: options.onstop,
|
|
75
|
+
onpause: options.onpause,
|
|
76
|
+
});
|
|
77
|
+
howlRef.current = howl;
|
|
78
|
+
}).catch(err => {
|
|
79
|
+
console.error('[useSound] Failed to load Howler:', err);
|
|
80
|
+
});
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
if (howlRef.current) {
|
|
84
|
+
howlRef.current.unload();
|
|
85
|
+
howlRef.current = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}, [src]);
|
|
89
|
+
return {
|
|
90
|
+
play: (spriteId) => {
|
|
91
|
+
if (howlRef.current) {
|
|
92
|
+
howlRef.current.play(spriteId);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
pause: () => {
|
|
96
|
+
if (howlRef.current) {
|
|
97
|
+
howlRef.current.pause();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
stop: () => {
|
|
101
|
+
if (howlRef.current) {
|
|
102
|
+
howlRef.current.stop();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
volume: (vol) => {
|
|
106
|
+
if (howlRef.current) {
|
|
107
|
+
if (vol !== undefined) {
|
|
108
|
+
howlRef.current.volume(vol);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return howlRef.current.volume();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
rate: (rate) => {
|
|
116
|
+
if (howlRef.current) {
|
|
117
|
+
if (rate !== undefined) {
|
|
118
|
+
howlRef.current.rate(rate);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
return howlRef.current.rate();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
loop: (loop) => {
|
|
126
|
+
if (howlRef.current) {
|
|
127
|
+
if (loop !== undefined) {
|
|
128
|
+
howlRef.current.loop(loop);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
return howlRef.current.loop();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
playing: () => {
|
|
136
|
+
return howlRef.current ? howlRef.current.playing() : false;
|
|
137
|
+
},
|
|
138
|
+
duration: () => {
|
|
139
|
+
return howlRef.current ? howlRef.current.duration() : 0;
|
|
140
|
+
},
|
|
141
|
+
state: () => {
|
|
142
|
+
return howlRef.current ? howlRef.current.state() : 'unloaded';
|
|
143
|
+
},
|
|
144
|
+
unload: () => {
|
|
145
|
+
if (howlRef.current) {
|
|
146
|
+
howlRef.current.unload();
|
|
147
|
+
howlRef.current = null;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Hook for playing background music with auto-play on mount
|
|
154
|
+
*
|
|
155
|
+
* @param src - URL to the music file
|
|
156
|
+
* @param options - Howler options (loop defaults to true for music)
|
|
157
|
+
* @returns Sound controller
|
|
158
|
+
*/
|
|
159
|
+
export function useMusic(src, options = {}) {
|
|
160
|
+
const sound = useSound(src, {
|
|
161
|
+
loop: true, // Music usually loops
|
|
162
|
+
...options,
|
|
163
|
+
});
|
|
164
|
+
const hasAutoPlayed = useRef(false);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (sound.state() === 'loaded' && !hasAutoPlayed.current) {
|
|
167
|
+
hasAutoPlayed.current = true;
|
|
168
|
+
sound.play();
|
|
169
|
+
}
|
|
170
|
+
}, [sound.state()]);
|
|
171
|
+
return sound;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Re-export Howl for advanced use cases
|
|
175
|
+
* Returns null during SSR, the Howl class after client-side load
|
|
176
|
+
*/
|
|
177
|
+
export async function getHowl() {
|
|
178
|
+
if (typeof window === 'undefined')
|
|
179
|
+
return null;
|
|
180
|
+
await loadHowler();
|
|
181
|
+
return Howl;
|
|
182
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ OverlaySlot, FullOverlay, GradientOverlay, } from './react/overlay/index.js';
|
|
|
12
12
|
export type { VideoState, VideoControls, VideoContextValue, VideoPlayerProps, CuePoint, SlideImage, SlideshowState, SlideshowControls, SlideshowContextValue, SlideshowProps, SlotPosition, OverlaySlotProps, FullOverlayProps, GradientOverlayProps, } from './react/overlay/index.js';
|
|
13
13
|
export { confetti } from './react/confetti.js';
|
|
14
14
|
export type { ConfettiOptions } from './react/confetti.js';
|
|
15
|
+
export { useSound, useMusic, getHowl } from './audio/index.js';
|
|
16
|
+
export type { SoundOptions, SoundController } from './audio/index.js';
|
|
15
17
|
export { getTileBridge, TileBridge } from './bridge/TileBridge.js';
|
|
16
18
|
export type { TileMessage, TileConfig, TileTokenData, KeyboardState, VisibilityState, SafeAreaInsets } from './bridge/TileBridge.js';
|
|
17
19
|
export { StateClient } from './state/StateClient.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACpE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAEL,WAAW,EACX,aAAa,EACb,QAAQ,EAAE,0BAA0B;AACpC,WAAW,EACX,YAAY,EACZ,gBAAgB,EAEhB,SAAS,EACT,iBAAiB,EACjB,YAAY,EAAE,8BAA8B;AAE5C,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EAER,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACnE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGrI,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtE,cAAc,kBAAkB,CAAC;AAGjC,cAAc,YAAY,CAAC;AAG3B,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACpE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAEL,WAAW,EACX,aAAa,EACb,QAAQ,EAAE,0BAA0B;AACpC,WAAW,EACX,YAAY,EACZ,gBAAgB,EAEhB,SAAS,EACT,iBAAiB,EACjB,YAAY,EAAE,8BAA8B;AAE5C,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EAER,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGtE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACnE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGrI,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtE,cAAc,kBAAkB,CAAC;AAGjC,cAAc,YAAY,CAAC;AAG3B,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,8 @@ Slideshow, useSlideshowState, useSlideshow, // Alias for useSlideshowState
|
|
|
17
17
|
OverlaySlot, FullOverlay, GradientOverlay, } from './react/overlay/index.js';
|
|
18
18
|
// Confetti - CSS-based, mobile WebView safe
|
|
19
19
|
export { confetti } from './react/confetti.js';
|
|
20
|
+
// Audio hooks - SSR-safe Howler.js wrappers
|
|
21
|
+
export { useSound, useMusic, getHowl } from './audio/index.js';
|
|
20
22
|
// Bridge for secure communication
|
|
21
23
|
export { getTileBridge, TileBridge } from './bridge/TileBridge.js';
|
|
22
24
|
// State API client
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thewhateverapp/tile-sdk",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "SDK for building interactive tiles on The Whatever App platform",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"types": "./dist/excalibur/index.d.ts",
|
|
14
14
|
"import": "./dist/excalibur/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./audio": {
|
|
17
|
+
"types": "./dist/audio/index.d.ts",
|
|
18
|
+
"import": "./dist/audio/index.js"
|
|
19
|
+
},
|
|
16
20
|
"./spec": {
|
|
17
21
|
"types": "./dist/spec/index.d.ts",
|
|
18
22
|
"import": "./dist/spec/index.js"
|
|
@@ -55,20 +59,26 @@
|
|
|
55
59
|
"peerDependencies": {
|
|
56
60
|
"react": "^18.0.0",
|
|
57
61
|
"react-dom": "^18.0.0",
|
|
58
|
-
"excalibur": "^0.29.0"
|
|
62
|
+
"excalibur": "^0.29.0",
|
|
63
|
+
"howler": "^2.2.0"
|
|
59
64
|
},
|
|
60
65
|
"peerDependenciesMeta": {
|
|
61
66
|
"excalibur": {
|
|
62
67
|
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"howler": {
|
|
70
|
+
"optional": true
|
|
63
71
|
}
|
|
64
72
|
},
|
|
65
73
|
"devDependencies": {
|
|
74
|
+
"@types/howler": "^2.2.11",
|
|
66
75
|
"@types/matter-js": "^0.19.0",
|
|
67
76
|
"@types/node": "^20.0.0",
|
|
68
77
|
"@types/react": "^18.2.48",
|
|
69
78
|
"@types/react-dom": "^18.2.18",
|
|
70
79
|
"eslint": "^9.39.1",
|
|
71
80
|
"excalibur": "^0.29.3",
|
|
81
|
+
"howler": "^2.2.4",
|
|
72
82
|
"next": "^14.2.0",
|
|
73
83
|
"tsx": "^4.7.0",
|
|
74
84
|
"typescript": "^5.3.3"
|