diff --git a/apps/web/src/components/commerce/WishlistView.tsx b/apps/web/src/components/commerce/WishlistView.tsx index fb3038570..35c7f3697 100644 --- a/apps/web/src/components/commerce/WishlistView.tsx +++ b/apps/web/src/components/commerce/WishlistView.tsx @@ -97,11 +97,16 @@ export const WishlistView: React.FC = () => { if (wishlist.length === 0) { return ( } title="Your wishlist is empty" - description="Save items you want to listen to later or purchase in the future." + description="Explore the marketplace and save items you love." + action={{ + label: 'Browse Marketplace', + onClick: () => (window.location.href = '/marketplace'), + }} size="lg" - className="min-h-96 animate-fadeIn" + className="min-h-96" /> ); } diff --git a/apps/web/src/components/data/List.tsx b/apps/web/src/components/data/List.tsx index 82f93c85c..ec7761419 100644 --- a/apps/web/src/components/data/List.tsx +++ b/apps/web/src/components/data/List.tsx @@ -70,7 +70,7 @@ export function List({ variant === 'bordered' && 'px-4 py-4', variant === 'spaced' && 'px-2 py-2', variant === 'default' && 'px-2 py-2', - item.onClick && !item.disabled && 'cursor-pointer hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', + item.onClick && !item.disabled && 'cursor-pointer hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background', item.disabled && 'cursor-not-allowed opacity-50', itemClassName, )} diff --git a/apps/web/src/components/library/playlists/QueueView.tsx b/apps/web/src/components/library/playlists/QueueView.tsx index fa051d5a6..ae248a0cb 100644 --- a/apps/web/src/components/library/playlists/QueueView.tsx +++ b/apps/web/src/components/library/playlists/QueueView.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useAudio } from '../../../context/AudioContext'; import { Card } from '../../ui/card'; import { Button } from '../../ui/button'; +import { EmptyState } from '../../ui/empty-state'; import { SaveQueueAsPlaylistModal } from './SaveQueueAsPlaylistModal'; import { Play, @@ -157,17 +158,17 @@ export const QueueView: React.FC = () => {
{queue.length === 0 ? ( -
- -

- Queue is empty. Add tracks to keep the vibe going. -

- {autoplay && ( -

- Autoplay is on. We'll pick a song for you. -

- )} -
+ } + title="Nothing in your queue" + description={ + autoplay + ? 'Autoplay is on — we\u2019ll pick something for you.' + : 'Start playing music and add tracks to build your queue.' + } + size="md" + /> ) : ( queue.map((track, i) => (
( className=" w-5 h-5 rounded border border-border bg-kodo-graphite peer-checked:bg-primary peer-checked:border-border - peer-focus:ring-2 peer-focus:ring-kodo-steel/30 + peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background transition-all duration-[var(--duration-fast)] " >
diff --git a/apps/web/src/components/ui/empty-state.tsx b/apps/web/src/components/ui/empty-state.tsx index a9d66612b..7c5b02e76 100644 --- a/apps/web/src/components/ui/empty-state.tsx +++ b/apps/web/src/components/ui/empty-state.tsx @@ -83,6 +83,17 @@ export interface EmptyStateProps { * @default 'md' */ size?: 'sm' | 'md' | 'lg'; + + /** + * Variante d'affichage de l'état vide + * + * - `default`: Affiché dans un Card (comportement actuel) + * - `centered`: Centré verticalement et horizontalement dans le parent + * - `card`: Affiché dans un Card avec bordure en pointillé + * + * @default 'default' + */ + variant?: 'default' | 'centered' | 'card'; } /** @@ -90,6 +101,7 @@ export interface EmptyStateProps { * * Composant pour afficher un état vide lorsqu'une liste ou une section est vide. * Design Kodo intégré avec support pour icône, titre, description et action. + * Inclut une animation d'entrée subtile (fade-in + scale) pour un rendu poli. * * FE-COMP-003: Add empty states to all list views * @@ -104,8 +116,9 @@ export interface EmptyStateProps { * * @example * ```tsx - * // État vide avec icône et action + * // État vide centré avec icône et action * } * title="Aucun message" * description="Vous n'avez pas encore de messages" @@ -128,6 +141,7 @@ export function EmptyState({ action, className, size = 'md', + variant = 'default', }: EmptyStateProps) { const sizeClasses = { sm: 'py-6', @@ -141,33 +155,82 @@ export function EmptyState({ lg: 'h-16 w-16', }; - return ( - - - {icon && ( -
-
+ const iconBgSizeClasses = { + sm: 'p-3', + md: 'p-4', + lg: 'p-5', + }; + + const content = ( +
+ {icon && ( +
+
+
{icon}
+
+ )} +

+ {title} +

+ {description && ( +

+ {description} +

+ )} + {action && ( + + )} +
+ ); + + if (variant === 'centered') { + return ( +
- {title} - - {description && ( -

- {description} -

- )} - {action && ( - + > + {content} +
+ ); + } + + if (variant === 'card') { + return ( +
+ {content} +
+ ); + } + + return ( + + + {content} ); diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index ae27604f5..fae1e8293 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -33,7 +33,7 @@ const Input = React.forwardRef( className={cn( "flex h-11 w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-sm text-white placeholder:text-muted-foreground/50 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50", "backdrop-blur-sm transition-all duration-[var(--duration-fast)]", - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary/50", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", "hover:bg-white/5 hover:border-white/20", icon && "pl-10", error && "border-destructive focus-visible:ring-destructive/30", diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx index b4fb17bfc..7f3c31ed5 100644 --- a/apps/web/src/components/ui/textarea.tsx +++ b/apps/web/src/components/ui/textarea.tsx @@ -87,7 +87,7 @@ const Textarea = React.forwardRef( 'text-white placeholder-gray-500', 'font-body text-base', 'rounded-lg', - 'focus:outline-none focus:border-border focus:ring-1 focus:ring-kodo-steel', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background', 'transition-all duration-[var(--duration-fast)]', 'min-h-[100px] resize-y', className, diff --git a/apps/web/src/features/chat/components/ChatRoom.tsx b/apps/web/src/features/chat/components/ChatRoom.tsx index c65b66e22..641679209 100644 --- a/apps/web/src/features/chat/components/ChatRoom.tsx +++ b/apps/web/src/features/chat/components/ChatRoom.tsx @@ -7,7 +7,7 @@ import { TypingIndicator } from './TypingIndicator'; import { Search, X, Disc, Clock, - MessageSquare, Wifi + MessageSquare } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; @@ -61,13 +61,18 @@ export const ChatRoom: React.FC = ({ conversationId }) => { if (!conversationId) { return ( -
-
- +
+
+ +
+
+

+ No conversation selected +

+

+ Pick a channel from the sidebar to start chatting. +

-

- Awaiting Frequency Selection -

); } @@ -118,14 +123,14 @@ export const ChatRoom: React.FC = ({ conversationId }) => {
{/* Welcome Message for Empty Room */} {currentMessages.length === 0 && ( -
-
- +
+
+
-

Channel Established

+

No messages yet

- Begin transmission on this frequency. + Send the first message to start the conversation.

diff --git a/apps/web/src/features/chat/components/chat-sidebar/ChatSidebarEmpty.tsx b/apps/web/src/features/chat/components/chat-sidebar/ChatSidebarEmpty.tsx index f29eab240..14203ef6a 100644 --- a/apps/web/src/features/chat/components/chat-sidebar/ChatSidebarEmpty.tsx +++ b/apps/web/src/features/chat/components/chat-sidebar/ChatSidebarEmpty.tsx @@ -1,4 +1,5 @@ import { cn } from '@/lib/utils'; +import { MessageSquarePlus } from 'lucide-react'; export interface ChatSidebarEmptyProps { className?: string; @@ -8,13 +9,19 @@ export function ChatSidebarEmpty({ className }: ChatSidebarEmptyProps) { return (
- No active frequencies detected. -
- Initialize a new channel. +
+ +
+
+

No conversations yet

+

+ Start a new conversation to get going. +

+
); } diff --git a/apps/web/src/features/library/pages/library-page/LibraryPageEmpty.tsx b/apps/web/src/features/library/pages/library-page/LibraryPageEmpty.tsx index 10f24b4c5..47c234c69 100644 --- a/apps/web/src/features/library/pages/library-page/LibraryPageEmpty.tsx +++ b/apps/web/src/features/library/pages/library-page/LibraryPageEmpty.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/ui/button'; +import { EmptyState } from '@/components/ui/empty-state'; import { FileAudio } from 'lucide-react'; interface LibraryPageEmptyProps { @@ -7,21 +7,17 @@ interface LibraryPageEmptyProps { export function LibraryPageEmpty({ onUploadClick }: LibraryPageEmptyProps) { return ( -
-
- -
-

It's empty here

-

- Start by uploading your first audio files to populate your library. -

- -
+ } + title="Your library is empty" + description="Upload your first track or create a playlist to get started." + action={{ + label: 'Upload Track', + onClick: onUploadClick, + }} + size="lg" + className="min-h-layout-page-sm" + /> ); } diff --git a/apps/web/src/features/playlists/components/PlaylistCard.tsx b/apps/web/src/features/playlists/components/PlaylistCard.tsx index 85a2ff412..02f07f4bb 100644 --- a/apps/web/src/features/playlists/components/PlaylistCard.tsx +++ b/apps/web/src/features/playlists/components/PlaylistCard.tsx @@ -97,6 +97,7 @@ function PlaylistCardComponent({ className={cn( 'w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all', 'touch-manipulation min-h-6 min-w-6', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background', selected ? 'bg-primary border-primary text-white' : 'bg-white/90 dark:bg-kodo-graphite/90 border-border dark:border-border text-transparent hover:border-border/50', diff --git a/apps/web/src/features/search/components/search-page/SearchPageEmpty.tsx b/apps/web/src/features/search/components/search-page/SearchPageEmpty.tsx index a0aaa606c..a6f13ebd4 100644 --- a/apps/web/src/features/search/components/search-page/SearchPageEmpty.tsx +++ b/apps/web/src/features/search/components/search-page/SearchPageEmpty.tsx @@ -1,11 +1,15 @@ +import { EmptyState } from '@/components/ui/empty-state'; +import { SearchX } from 'lucide-react'; + export function SearchPageEmpty() { return ( -
-
- 🔭 -
-

No signals found

-

Try adjusting your search frequency.

-
+ } + title="No results found" + description="Try adjusting your search or use different keywords." + size="lg" + className="py-20" + /> ); } diff --git a/apps/web/src/features/tracks/components/TrackListRow.tsx b/apps/web/src/features/tracks/components/TrackListRow.tsx index 0fb748f02..8e98ad10b 100644 --- a/apps/web/src/features/tracks/components/TrackListRow.tsx +++ b/apps/web/src/features/tracks/components/TrackListRow.tsx @@ -111,9 +111,11 @@ export function TrackListRow({ role="row" className={cn( 'hover:bg-muted/50 transition-colors duration-[var(--duration-normal)]', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background', isSelected && 'bg-primary/10', className, )} + tabIndex={onTrackClick ? 0 : undefined} onClick={() => onTrackClick?.(track)} > {showSelection && ( @@ -159,7 +161,7 @@ export function TrackListRow({ @@ -197,9 +199,11 @@ export function TrackListRow({ className={cn( 'flex items-center gap-4 p-2 rounded-[var(--radius-md)] hover:bg-muted/50 group h-14', 'transition-colors duration-[var(--duration-normal)]', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background', isSelected && 'bg-primary/10', className, )} + tabIndex={onTrackClick ? 0 : undefined} onClick={() => onTrackClick?.(track)} > {showSelection && ( @@ -247,7 +251,7 @@ export function TrackListRow({ @@ -277,14 +281,14 @@ export function TrackListRow({ diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 158c58eaf..c8446af05 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -907,6 +907,10 @@ animation: fade-in var(--duration-normal) var(--ease-out); } + .animate-empty-state-in { + animation: empty-state-in var(--duration-normal) var(--ease-out) both; + } + .animate-spin-slow { animation: spin-slow 10s linear infinite; } @@ -1067,6 +1071,18 @@ } } +@keyframes empty-state-in { + from { + opacity: 0; + transform: scale(0.96); + } + + to { + opacity: 1; + transform: scale(1); + } +} + @keyframes shimmer { 0% { background-position: 200% 0;