06 - Components
Overview
Wavic WebApp uses a combination of RizzUI v2 (pre-built component library) and custom components. The component architecture follows atomic design principles.
Component Organization
src/components/
├── ui/ # Base UI components (atoms/molecules)
├── cards/ # Card components
├── headers/ # Page headers
├── modal/ # Modal dialogs
├── skeletons/ # Loading states
├── tables/ # Data tables
├── icons/ # Icon components
├── loader/ # Loading indicators
├── charts/ # Data visualization
├── banners/ # Promotional banners
├── button/ # Button variants
├── search/ # Search components
├── dnd-sortable/ # Drag and drop
├── drop-downs/ # Dropdown menus
└── settings/ # Settings UIDesign System
RizzUI Components
RizzUI provides the base component library:
typescript
import {
Button,
Text,
Loader,
Tooltip,
Input,
Select,
Modal,
Drawer,
Avatar,
Badge,
Checkbox,
Radio,
Switch,
Textarea,
// ... more
} from 'rizzui';Tailwind CSS
All styling uses Tailwind with custom theme:
typescript
// Utility for merging classes
import cn from '@/utils/class-names';
<div className={cn(
'flex items-center gap-4',
'bg-gray-100 dark:bg-gray-800',
isActive && 'ring-2 ring-primary'
)} />Color System
css
/* src/app/globals.css - CSS-first Tailwind v4 config */
@theme inline {
--color-gray-0: rgb(var(--gray-0) / <alpha-value>);
/* ... 50-1000 */
--color-primary-lighter: rgb(var(--primary-lighter) / <alpha-value>);
--color-primary: rgb(var(--primary-default) / <alpha-value>);
--color-primary-dark: rgb(var(--primary-dark) / <alpha-value>);
/* secondary, red, orange, blue, green... */
}Core Components
1. Page Header
Standard page header with breadcrumbs:
typescript
// src/app/shared/page-header.tsx
import PageHeader from '@/app/shared/page-header';
<PageHeader
title="Projects"
breadcrumb={[
{ href: '/', name: 'Artist spaces' },
{ name: 'Artist space' },
]}
/>2. Artist Header
Artist-specific header with actions:
typescript
// src/components/headers/artist-header.tsx
<ArtistHeader
artist={artist}
isEditable={isEditable}
role={role}
onShare={handleShareModal}
onSettings={handleSettings}
/>3. Project Header
typescript
// src/components/headers/new-project-header.tsx
<ProjectHeader
project={project}
tracks={tracks}
onAddTrack={handleAddTrack}
isEditable={isEditable}
/>4. Track Player
Audio player with waveform:
typescript
// src/components/ui/Track-player.tsx
// Used by src/app/shared/track-player/
<TrackPlayer
track={track}
audioFile={selectedAudioTrack}
onPlayPause={togglePlay}
onSeek={handleSeek}
/>Components:
desktop-player.tsx- Full desktop playermobile-player.tsx- Compact mobile playeraudio-controllers.tsx- Play/pause, volume, etc.player-dashboard.tsx- Main player wrapper
5. Project Cards
typescript
// src/app/shared/artist/get-projects.tsx
<ProjectCards
projects={projects}
artistId={artistId}
onProjectClick={handleProjectClick}
isEditable={isEditable}
/>6. Share Modal
typescript
// src/components/modal/share-modal.tsx
<ShareModal
artistId={artistId}
teamMembers={collaborators}
type="artist" // or "project", "track"
/>Modal System
Modal Views Container
typescript
// src/app/shared/modal-views/container.tsx
// Global modal container - included in root layout
<GlobalModal />Using Modals
typescript
import { useModal } from '@/app/shared/modal-views/use-modal';
function MyComponent() {
const { openModal, closeModal } = useModal();
const handleOpen = () => {
openModal({
view: <ShareModal artistId={artistId} />,
customSize: '700px',
});
};
return <Button onClick={handleOpen}>Share</Button>;
}Drawer System
Drawer Views Container
typescript
// src/app/shared/drawer-views/container.tsx
<GlobalDrawer />Using Drawers
typescript
import { useDrawer } from '@/app/shared/drawer-views/use-drawer';
function MyComponent() {
const { openDrawer, closeDrawer } = useDrawer();
const handleOpen = () => {
openDrawer({
view: <SettingsPanel />,
placement: 'right',
});
};
return <Button onClick={handleOpen}>Settings</Button>;
}Loading States
Skeleton Components
typescript
// src/components/skeletons/
import HeaderSkeleton from '@/components/skeletons/header-skeleton';
import { ProjectSkeleton } from '@/components/skeletons/project-skeleton';
if (loading) {
return (
<>
<HeaderSkeleton />
<ProjectSkeleton count={6} />
</>
);
}Loader Component
typescript
import { Loader } from 'rizzui';
<Loader size="lg" variant="spinner" />Progress Bar
typescript
// src/components/progress-bar/
<ProgressBar value={uploadProgress} max={100} />Form Components
Upload Component
typescript
// src/components/ui/upload.tsx
import Upload from '@/components/ui/upload';
<Upload
accept="audio/*"
multiple
onDrop={handleFiles}
maxSize={50 * 1024 * 1024} // 50MB
/>Dropzone
typescript
// src/components/globalDropZone.tsx
// Global file drop handling for the entire appSelect with Status
typescript
// src/components/ui/status-select.tsx
import StatusSelect from '@/components/ui/status-select';
<StatusSelect
value={status}
onChange={setStatus}
options={TRACK_STATUS_OPTIONS}
/>Date Picker
typescript
// src/components/ui/datepicker.tsx
import DatePicker from '@/components/ui/datepicker';
<DatePicker
selected={releaseDate}
onChange={setReleaseDate}
placeholderText="Release date"
/>Table Components
Controlled Table
typescript
// src/components/controlled-table/
import ControlledTable from '@/components/controlled-table';
<ControlledTable
data={tracks}
columns={columns}
isLoading={loading}
showLoadingText
scroll={{ x: 800 }}
/>Table with Sorting/Filtering
Using @tanstack/react-table:
typescript
import { useTable } from '@/hooks/use-table';
const {
table,
columnFilters,
setColumnFilters,
sortingState,
setSortingState,
} = useTable({
data: tracks,
columns,
});Icon System
React Icons
typescript
import { HiOutlineUpload } from 'react-icons/hi';
import { IoMdAdd, IoIosLink } from 'react-icons/io';
import { FiPlay, FiPause } from 'react-icons/fi';
<HiOutlineUpload className="h-5 w-5" />Custom Icons
typescript
// src/components/icons/
import { PlayIcon, PauseIcon, TrashIcon } from '@/components/icons';Animation
Framer Motion
typescript
import { motion, AnimatePresence } from 'framer-motion';
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
Content
</motion.div>
)}
</AnimatePresence>Auto-Animate
typescript
import { useAutoAnimate } from '@formkit/auto-animate/react';
function List() {
const [parent] = useAutoAnimate();
return (
<ul ref={parent}>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Drag and Drop
DnD Kit
typescript
// src/components/dnd-sortable/
import {
DndContext,
closestCenter,
useSensor,
useSensors,
PointerSensor,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map((item) => (
<SortableItem key={item.id} id={item.id} />
))}
</SortableContext>
</DndContext>Toast Notifications
typescript
import { toast } from 'react-hot-toast';
// Success
toast.success('Track uploaded successfully');
// Error
toast.error('Failed to upload track');
// Custom
toast('Custom message', {
icon: '🎵',
duration: 3000,
});Tooltips
typescript
import { Tooltip } from 'rizzui';
<Tooltip content="Share with team" placement="top">
<Button>Share</Button>
</Tooltip>Component Best Practices
- Use RizzUI components as base building blocks
- Compose custom components from RizzUI atoms
- Use
cn()utility for conditional classes - Extract loading states into skeleton components
- Use TypeScript for all component props
- Keep components focused - single responsibility
- Use modals/drawers for forms and detailed views