Spaces:
Running
Running
Improve VoiceApp responsiveness and stop audio on close
Browse files- Add audio cleanup when VoiceApp window is closed
- Add responsive breakpoints for mobile/tablet/desktop
- Adaptive toolbar with icon-only refresh button on small screens
- Compact content cards with smaller icons and spacing on mobile
- Responsive audio player controls
- Add xs breakpoint (480px) to Tailwind theme
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app/components/VoiceApp.tsx +92 -53
- app/globals.css +3 -0
app/components/VoiceApp.tsx
CHANGED
|
@@ -42,6 +42,29 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 42 |
const [currentTime, setCurrentTime] = useState(0)
|
| 43 |
const [duration, setDuration] = useState(0)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
// Load saved content from server and localStorage
|
| 46 |
useEffect(() => {
|
| 47 |
// Clear any existing problematic localStorage data on first load
|
|
@@ -204,7 +227,7 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 204 |
id="voice-app"
|
| 205 |
title="Voice Studio"
|
| 206 |
isOpen={true}
|
| 207 |
-
onClose={
|
| 208 |
onMinimize={onMinimize}
|
| 209 |
onMaximize={onMaximize}
|
| 210 |
onFocus={onFocus}
|
|
@@ -218,80 +241,88 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 218 |
>
|
| 219 |
<div className="flex flex-col h-full bg-[#F5F5F7]">
|
| 220 |
{/* macOS Toolbar */}
|
| 221 |
-
<div className="px-4 py-3 bg-white/50 backdrop-blur-md border-b border-gray-200/50 flex items-center justify-between sticky top-0 z-10">
|
| 222 |
-
<div className="flex items-center gap-3">
|
| 223 |
-
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center shadow-sm">
|
| 224 |
-
<MusicNote size={
|
|
|
|
| 225 |
</div>
|
| 226 |
-
<div>
|
| 227 |
-
<h2 className="text-sm font-semibold text-gray-900 leading-none">Voice Studio</h2>
|
| 228 |
-
<p className="text-[11px] text-gray-500 mt-0.5">AI Audio Generation</p>
|
| 229 |
</div>
|
| 230 |
</div>
|
| 231 |
|
| 232 |
<button
|
| 233 |
onClick={handleRefresh}
|
| 234 |
-
className="px-3 py-1.5 bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 rounded-md text-xs font-medium border border-gray-200 shadow-sm transition-all flex items-center gap-1.5"
|
| 235 |
>
|
| 236 |
<ArrowClockwise size={14} />
|
| 237 |
-
Refresh
|
| 238 |
</button>
|
| 239 |
</div>
|
| 240 |
|
| 241 |
{/* Content Area */}
|
| 242 |
-
<div className="flex-1 overflow-y-auto p-5">
|
| 243 |
{voiceContents.length === 0 ? (
|
| 244 |
-
<div className="flex flex-col items-center justify-center h-full text-center py-10">
|
| 245 |
-
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-purple-100 to-pink-100 flex items-center justify-center mb-6 shadow-inner">
|
| 246 |
-
<FileAudio size={
|
|
|
|
| 247 |
</div>
|
| 248 |
-
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Audio Content</h3>
|
| 249 |
-
<p className="text-sm text-gray-500 max-w-sm mb-6 leading-relaxed">
|
| 250 |
Ask Claude to generate song lyrics or write a story, and your audio will appear here automatically.
|
| 251 |
</p>
|
| 252 |
-
<div className="bg-white/60 backdrop-blur-sm rounded-xl p-4 max-w-sm text-left border border-gray-200/50 shadow-sm">
|
| 253 |
-
<p className="text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wide">Try asking Claude:</p>
|
| 254 |
-
<ul className="space-y-2 text-sm text-gray-700">
|
| 255 |
<li className="flex items-start gap-2">
|
| 256 |
<span className="text-purple-500">•</span>
|
| 257 |
-
"Generate a pop song about coding"
|
| 258 |
</li>
|
| 259 |
<li className="flex items-start gap-2">
|
| 260 |
<span className="text-purple-500">•</span>
|
| 261 |
-
"Write a bedtime story and narrate it"
|
| 262 |
</li>
|
| 263 |
</ul>
|
| 264 |
</div>
|
| 265 |
</div>
|
| 266 |
) : (
|
| 267 |
-
<div className="grid grid-cols-1 gap-3">
|
| 268 |
{voiceContents.map((content) => (
|
| 269 |
<div
|
| 270 |
key={content.id}
|
| 271 |
-
className="bg-white rounded-xl p-4 shadow-sm border border-gray-200/60 hover:shadow-md transition-all duration-200 group"
|
| 272 |
>
|
| 273 |
-
<div className="flex items-start justify-between mb-3">
|
| 274 |
-
<div className="flex items-center gap-3">
|
| 275 |
-
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${content.type === 'song'
|
| 276 |
? 'bg-purple-100 text-purple-600'
|
| 277 |
: 'bg-blue-100 text-blue-600'
|
| 278 |
}`}>
|
| 279 |
{content.type === 'song' ? (
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
| 281 |
) : (
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
| 283 |
)}
|
| 284 |
</div>
|
| 285 |
-
<div>
|
| 286 |
-
<h3 className="font-semibold text-gray-900 text-sm">{content.title}</h3>
|
| 287 |
-
<div className="flex items-center gap-2 text-xs text-gray-500">
|
| 288 |
<span className="capitalize">{content.type}</span>
|
| 289 |
-
<span>•</span>
|
| 290 |
-
<span>{new Date(content.timestamp).toLocaleDateString()}</span>
|
| 291 |
{content.style && (
|
| 292 |
<>
|
| 293 |
-
<span>•</span>
|
| 294 |
-
<span className="truncate max-w-[150px]">{content.style}</span>
|
| 295 |
</>
|
| 296 |
)}
|
| 297 |
</div>
|
|
@@ -299,47 +330,54 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 299 |
</div>
|
| 300 |
|
| 301 |
{content.audioUrl && (
|
| 302 |
-
<div className="flex items-center gap-2">
|
| 303 |
<button
|
| 304 |
onClick={() => handleDownload(content)}
|
| 305 |
-
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
| 306 |
title="Download"
|
| 307 |
>
|
| 308 |
-
<DownloadSimple size={
|
|
|
|
| 309 |
</button>
|
| 310 |
</div>
|
| 311 |
)}
|
| 312 |
</div>
|
| 313 |
|
| 314 |
{content.isProcessing ? (
|
| 315 |
-
<div className="flex items-center justify-center py-4 bg-gray-50/50 rounded-lg border border-dashed border-gray-200">
|
| 316 |
-
<SpinnerGap size={
|
| 317 |
-
<
|
|
|
|
| 318 |
</div>
|
| 319 |
) : (
|
| 320 |
-
<div className="space-y-3">
|
| 321 |
{(content.lyrics || content.storyContent) && (
|
| 322 |
-
<div className="bg-gray-50/80 rounded-lg p-3 max-h-24 overflow-y-auto text-xs text-gray-600 leading-relaxed border border-gray-100">
|
| 323 |
<p className="whitespace-pre-line">{content.lyrics || content.storyContent}</p>
|
| 324 |
</div>
|
| 325 |
)}
|
| 326 |
|
| 327 |
{content.audioUrl && (
|
| 328 |
-
<div className="mt-3">
|
| 329 |
{currentlyPlaying === content.id ? (
|
| 330 |
-
<div className="bg-white rounded-lg border border-gray-200 p-3 space-y-2">
|
| 331 |
-
<div className="flex items-center gap-3">
|
| 332 |
<button
|
| 333 |
onClick={() => handlePlay(content)}
|
| 334 |
-
className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-900 text-white hover:bg-gray-800 transition-colors"
|
| 335 |
>
|
| 336 |
{isPlaying ? (
|
| 337 |
-
<Pause size={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
) : (
|
| 339 |
-
<Play size={14} weight="fill" />
|
| 340 |
)}
|
| 341 |
</button>
|
| 342 |
-
<div className="flex-1">
|
| 343 |
<input
|
| 344 |
type="range"
|
| 345 |
min="0"
|
|
@@ -348,7 +386,7 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 348 |
onChange={handleSeek}
|
| 349 |
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-gray-900 [&::-webkit-slider-thumb]:rounded-full"
|
| 350 |
/>
|
| 351 |
-
<div className="flex justify-between text-[10px] text-gray-500 mt-1 font-medium">
|
| 352 |
<span>{formatTime(currentTime)}</span>
|
| 353 |
<span>{formatTime(duration)}</span>
|
| 354 |
</div>
|
|
@@ -358,9 +396,10 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 358 |
) : (
|
| 359 |
<button
|
| 360 |
onClick={() => handlePlay(content)}
|
| 361 |
-
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium text-sm bg-[#F5F5F7] text-gray-700 border border-gray-200 hover:bg-gray-200 hover:border-gray-300 transition-all"
|
| 362 |
>
|
| 363 |
-
<Play size={
|
|
|
|
| 364 |
Play Audio
|
| 365 |
</button>
|
| 366 |
)}
|
|
|
|
| 42 |
const [currentTime, setCurrentTime] = useState(0)
|
| 43 |
const [duration, setDuration] = useState(0)
|
| 44 |
|
| 45 |
+
// Cleanup audio on unmount
|
| 46 |
+
useEffect(() => {
|
| 47 |
+
return () => {
|
| 48 |
+
if (audioElement) {
|
| 49 |
+
audioElement.pause()
|
| 50 |
+
audioElement.currentTime = 0
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}, [audioElement])
|
| 54 |
+
|
| 55 |
+
// Handle close with audio cleanup
|
| 56 |
+
const handleClose = () => {
|
| 57 |
+
if (audioElement) {
|
| 58 |
+
audioElement.pause()
|
| 59 |
+
audioElement.currentTime = 0
|
| 60 |
+
setAudioElement(null)
|
| 61 |
+
setCurrentlyPlaying(null)
|
| 62 |
+
setIsPlaying(false)
|
| 63 |
+
setCurrentTime(0)
|
| 64 |
+
}
|
| 65 |
+
onClose()
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
// Load saved content from server and localStorage
|
| 69 |
useEffect(() => {
|
| 70 |
// Clear any existing problematic localStorage data on first load
|
|
|
|
| 227 |
id="voice-app"
|
| 228 |
title="Voice Studio"
|
| 229 |
isOpen={true}
|
| 230 |
+
onClose={handleClose}
|
| 231 |
onMinimize={onMinimize}
|
| 232 |
onMaximize={onMaximize}
|
| 233 |
onFocus={onFocus}
|
|
|
|
| 241 |
>
|
| 242 |
<div className="flex flex-col h-full bg-[#F5F5F7]">
|
| 243 |
{/* macOS Toolbar */}
|
| 244 |
+
<div className="px-2 sm:px-4 py-2 sm:py-3 bg-white/50 backdrop-blur-md border-b border-gray-200/50 flex items-center justify-between sticky top-0 z-10">
|
| 245 |
+
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
|
| 246 |
+
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center shadow-sm flex-shrink-0">
|
| 247 |
+
<MusicNote size={16} weight="fill" className="text-white sm:hidden" />
|
| 248 |
+
<MusicNote size={18} weight="fill" className="text-white hidden sm:block" />
|
| 249 |
</div>
|
| 250 |
+
<div className="min-w-0">
|
| 251 |
+
<h2 className="text-xs sm:text-sm font-semibold text-gray-900 leading-none truncate">Voice Studio</h2>
|
| 252 |
+
<p className="text-[10px] sm:text-[11px] text-gray-500 mt-0.5 hidden xs:block">AI Audio Generation</p>
|
| 253 |
</div>
|
| 254 |
</div>
|
| 255 |
|
| 256 |
<button
|
| 257 |
onClick={handleRefresh}
|
| 258 |
+
className="p-1.5 sm:px-3 sm:py-1.5 bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 rounded-md text-xs font-medium border border-gray-200 shadow-sm transition-all flex items-center gap-1.5 flex-shrink-0"
|
| 259 |
>
|
| 260 |
<ArrowClockwise size={14} />
|
| 261 |
+
<span className="hidden sm:inline">Refresh</span>
|
| 262 |
</button>
|
| 263 |
</div>
|
| 264 |
|
| 265 |
{/* Content Area */}
|
| 266 |
+
<div className="flex-1 overflow-y-auto p-3 sm:p-5">
|
| 267 |
{voiceContents.length === 0 ? (
|
| 268 |
+
<div className="flex flex-col items-center justify-center h-full text-center py-6 sm:py-10 px-2">
|
| 269 |
+
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-br from-purple-100 to-pink-100 flex items-center justify-center mb-4 sm:mb-6 shadow-inner">
|
| 270 |
+
<FileAudio size={32} weight="duotone" className="text-purple-500/80 sm:hidden" />
|
| 271 |
+
<FileAudio size={40} weight="duotone" className="text-purple-500/80 hidden sm:block" />
|
| 272 |
</div>
|
| 273 |
+
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-2">No Audio Content</h3>
|
| 274 |
+
<p className="text-xs sm:text-sm text-gray-500 max-w-sm mb-4 sm:mb-6 leading-relaxed px-2">
|
| 275 |
Ask Claude to generate song lyrics or write a story, and your audio will appear here automatically.
|
| 276 |
</p>
|
| 277 |
+
<div className="bg-white/60 backdrop-blur-sm rounded-xl p-3 sm:p-4 max-w-sm text-left border border-gray-200/50 shadow-sm w-full mx-2">
|
| 278 |
+
<p className="text-[10px] sm:text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wide">Try asking Claude:</p>
|
| 279 |
+
<ul className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm text-gray-700">
|
| 280 |
<li className="flex items-start gap-2">
|
| 281 |
<span className="text-purple-500">•</span>
|
| 282 |
+
<span>"Generate a pop song about coding"</span>
|
| 283 |
</li>
|
| 284 |
<li className="flex items-start gap-2">
|
| 285 |
<span className="text-purple-500">•</span>
|
| 286 |
+
<span>"Write a bedtime story and narrate it"</span>
|
| 287 |
</li>
|
| 288 |
</ul>
|
| 289 |
</div>
|
| 290 |
</div>
|
| 291 |
) : (
|
| 292 |
+
<div className="grid grid-cols-1 gap-2 sm:gap-3">
|
| 293 |
{voiceContents.map((content) => (
|
| 294 |
<div
|
| 295 |
key={content.id}
|
| 296 |
+
className="bg-white rounded-lg sm:rounded-xl p-3 sm:p-4 shadow-sm border border-gray-200/60 hover:shadow-md transition-all duration-200 group"
|
| 297 |
>
|
| 298 |
+
<div className="flex items-start justify-between mb-2 sm:mb-3 gap-2">
|
| 299 |
+
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
| 300 |
+
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${content.type === 'song'
|
| 301 |
? 'bg-purple-100 text-purple-600'
|
| 302 |
: 'bg-blue-100 text-blue-600'
|
| 303 |
}`}>
|
| 304 |
{content.type === 'song' ? (
|
| 305 |
+
<>
|
| 306 |
+
<MusicNote size={16} weight="fill" className="sm:hidden" />
|
| 307 |
+
<MusicNote size={20} weight="fill" className="hidden sm:block" />
|
| 308 |
+
</>
|
| 309 |
) : (
|
| 310 |
+
<>
|
| 311 |
+
<BookOpen size={16} weight="fill" className="sm:hidden" />
|
| 312 |
+
<BookOpen size={20} weight="fill" className="hidden sm:block" />
|
| 313 |
+
</>
|
| 314 |
)}
|
| 315 |
</div>
|
| 316 |
+
<div className="min-w-0 flex-1">
|
| 317 |
+
<h3 className="font-semibold text-gray-900 text-xs sm:text-sm truncate">{content.title}</h3>
|
| 318 |
+
<div className="flex items-center gap-1 sm:gap-2 text-[10px] sm:text-xs text-gray-500 flex-wrap">
|
| 319 |
<span className="capitalize">{content.type}</span>
|
| 320 |
+
<span className="hidden xs:inline">•</span>
|
| 321 |
+
<span className="hidden xs:inline">{new Date(content.timestamp).toLocaleDateString()}</span>
|
| 322 |
{content.style && (
|
| 323 |
<>
|
| 324 |
+
<span className="hidden sm:inline">•</span>
|
| 325 |
+
<span className="truncate max-w-[80px] sm:max-w-[150px] hidden sm:inline">{content.style}</span>
|
| 326 |
</>
|
| 327 |
)}
|
| 328 |
</div>
|
|
|
|
| 330 |
</div>
|
| 331 |
|
| 332 |
{content.audioUrl && (
|
| 333 |
+
<div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
| 334 |
<button
|
| 335 |
onClick={() => handleDownload(content)}
|
| 336 |
+
className="p-1 sm:p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
| 337 |
title="Download"
|
| 338 |
>
|
| 339 |
+
<DownloadSimple size={16} className="sm:hidden" />
|
| 340 |
+
<DownloadSimple size={18} className="hidden sm:block" />
|
| 341 |
</button>
|
| 342 |
</div>
|
| 343 |
)}
|
| 344 |
</div>
|
| 345 |
|
| 346 |
{content.isProcessing ? (
|
| 347 |
+
<div className="flex items-center justify-center py-3 sm:py-4 bg-gray-50/50 rounded-lg border border-dashed border-gray-200">
|
| 348 |
+
<SpinnerGap size={18} className="text-purple-500 animate-spin sm:hidden" />
|
| 349 |
+
<SpinnerGap size={20} className="text-purple-500 animate-spin hidden sm:block" />
|
| 350 |
+
<span className="ml-2 text-xs sm:text-sm text-gray-500">Generating audio...</span>
|
| 351 |
</div>
|
| 352 |
) : (
|
| 353 |
+
<div className="space-y-2 sm:space-y-3">
|
| 354 |
{(content.lyrics || content.storyContent) && (
|
| 355 |
+
<div className="bg-gray-50/80 rounded-lg p-2 sm:p-3 max-h-20 sm:max-h-24 overflow-y-auto text-[10px] sm:text-xs text-gray-600 leading-relaxed border border-gray-100">
|
| 356 |
<p className="whitespace-pre-line">{content.lyrics || content.storyContent}</p>
|
| 357 |
</div>
|
| 358 |
)}
|
| 359 |
|
| 360 |
{content.audioUrl && (
|
| 361 |
+
<div className="mt-2 sm:mt-3">
|
| 362 |
{currentlyPlaying === content.id ? (
|
| 363 |
+
<div className="bg-white rounded-lg border border-gray-200 p-2 sm:p-3 space-y-2">
|
| 364 |
+
<div className="flex items-center gap-2 sm:gap-3">
|
| 365 |
<button
|
| 366 |
onClick={() => handlePlay(content)}
|
| 367 |
+
className="w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center rounded-full bg-gray-900 text-white hover:bg-gray-800 transition-colors flex-shrink-0"
|
| 368 |
>
|
| 369 |
{isPlaying ? (
|
| 370 |
+
<Pause size={12} weight="fill" className="sm:hidden" />
|
| 371 |
+
) : (
|
| 372 |
+
<Play size={12} weight="fill" className="sm:hidden" />
|
| 373 |
+
)}
|
| 374 |
+
{isPlaying ? (
|
| 375 |
+
<Pause size={14} weight="fill" className="hidden sm:block" />
|
| 376 |
) : (
|
| 377 |
+
<Play size={14} weight="fill" className="hidden sm:block" />
|
| 378 |
)}
|
| 379 |
</button>
|
| 380 |
+
<div className="flex-1 min-w-0">
|
| 381 |
<input
|
| 382 |
type="range"
|
| 383 |
min="0"
|
|
|
|
| 386 |
onChange={handleSeek}
|
| 387 |
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-gray-900 [&::-webkit-slider-thumb]:rounded-full"
|
| 388 |
/>
|
| 389 |
+
<div className="flex justify-between text-[9px] sm:text-[10px] text-gray-500 mt-0.5 sm:mt-1 font-medium">
|
| 390 |
<span>{formatTime(currentTime)}</span>
|
| 391 |
<span>{formatTime(duration)}</span>
|
| 392 |
</div>
|
|
|
|
| 396 |
) : (
|
| 397 |
<button
|
| 398 |
onClick={() => handlePlay(content)}
|
| 399 |
+
className="w-full flex items-center justify-center gap-1.5 sm:gap-2 py-2 sm:py-2.5 rounded-lg font-medium text-xs sm:text-sm bg-[#F5F5F7] text-gray-700 border border-gray-200 hover:bg-gray-200 hover:border-gray-300 transition-all active:scale-[0.98]"
|
| 400 |
>
|
| 401 |
+
<Play size={14} weight="fill" className="sm:hidden" />
|
| 402 |
+
<Play size={16} weight="fill" className="hidden sm:block" />
|
| 403 |
Play Audio
|
| 404 |
</button>
|
| 405 |
)}
|
app/globals.css
CHANGED
|
@@ -91,6 +91,9 @@
|
|
| 91 |
}
|
| 92 |
|
| 93 |
@theme inline {
|
|
|
|
|
|
|
|
|
|
| 94 |
--radius-sm: calc(var(--radius) - 4px);
|
| 95 |
--radius-md: calc(var(--radius) - 2px);
|
| 96 |
--radius-lg: var(--radius);
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
@theme inline {
|
| 94 |
+
/* Custom breakpoints */
|
| 95 |
+
--breakpoint-xs: 480px;
|
| 96 |
+
|
| 97 |
--radius-sm: calc(var(--radius) - 4px);
|
| 98 |
--radius-md: calc(var(--radius) - 2px);
|
| 99 |
--radius-lg: var(--radius);
|