refactor(ui): remove unused design-system package and create STORYBOOK_CONTRACT
- Remove packages/design-system/ directory (superseded by SUMI tokens in apps/web/src/index.css, confirmed no imports exist) - Update package.json keywords from kodo-design-system to sumi-design-system - Create docs/STORYBOOK_CONTRACT.md defining mandatory story structure: Default, Loading, Error, Empty states for feature components - Typography audit: SUMI utility classes defined in index.css, codebase correctly uses Tailwind classes with SUMI tokens via @theme — no migration needed Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
34e9d69af3
commit
1695606e24
26 changed files with 173 additions and 2427 deletions
172
apps/web/docs/STORYBOOK_CONTRACT.md
Normal file
172
apps/web/docs/STORYBOOK_CONTRACT.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Storybook Contract — Veza Design System
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the mandatory structure and conventions for all Storybook stories in the Veza frontend. It is referenced by `.cursorrules` and must be followed by all contributors and AI assistants.
|
||||
|
||||
## Story Requirements
|
||||
|
||||
### 1. Feature Components
|
||||
|
||||
Every **Feature component** (components in `src/features/`) must have the following story variants:
|
||||
|
||||
| Story Name | Description | Required |
|
||||
|---|---|---|
|
||||
| `Default` | Component with typical data | Yes |
|
||||
| `Loading` | Component in loading state (using Skeleton components) | Yes |
|
||||
| `Error` | Component displaying an error fallback | Yes |
|
||||
| `Empty` | Component with no data (empty list/state) | Yes, if applicable |
|
||||
|
||||
### 2. UI Components
|
||||
|
||||
Every **UI component** (components in `src/components/ui/`) must have:
|
||||
|
||||
| Story Name | Description | Required |
|
||||
|---|---|---|
|
||||
| `Default` | Default variant/state | Yes |
|
||||
| `Variants` | All visual variants (size, color, etc.) | If applicable |
|
||||
| `Disabled` | Disabled state | If applicable |
|
||||
| `WithIcon` | With icon prop | If applicable |
|
||||
|
||||
### 3. Layout Components
|
||||
|
||||
Layout components (`src/components/layout/`) should demonstrate:
|
||||
|
||||
- Responsive behavior (mobile, tablet, desktop)
|
||||
- Light and dark modes
|
||||
|
||||
## Decorators
|
||||
|
||||
### Global Decorator
|
||||
|
||||
All stories automatically receive the global `StorybookDecorator` configured in `.storybook/preview.tsx`. This provides:
|
||||
|
||||
- Theme provider (dark/light mode)
|
||||
- i18n provider
|
||||
- Router context (MemoryRouter)
|
||||
- Toast provider
|
||||
- MSW request interception
|
||||
|
||||
### Do NOT Import Application Providers
|
||||
|
||||
Never import application-level providers directly in stories:
|
||||
|
||||
```tsx
|
||||
// BAD — do not do this
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
export const MyStory = () => (
|
||||
<AuthProvider>
|
||||
<MyComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
// GOOD — the global decorator handles providers
|
||||
export const MyStory = () => <MyComponent />;
|
||||
```
|
||||
|
||||
## Data Mocking
|
||||
|
||||
### Use MSW Handlers
|
||||
|
||||
All API data should be mocked via MSW handlers in `src/mocks/handlers.ts`:
|
||||
|
||||
```tsx
|
||||
// In the story file
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
const meta: Meta<typeof MyComponent> = {
|
||||
component: MyComponent,
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get('*/api/v1/my-endpoint', () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
data: { items: [] },
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Loading States
|
||||
|
||||
Use `delay: 'infinite'` in MSW handlers to simulate loading:
|
||||
|
||||
```tsx
|
||||
export const Loading: StoryObj<typeof MyComponent> = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get('*/api/v1/my-endpoint', async () => {
|
||||
await new Promise(() => {}); // Never resolves
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Error States
|
||||
|
||||
Return error responses from MSW handlers:
|
||||
|
||||
```tsx
|
||||
export const Error: StoryObj<typeof MyComponent> = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get('*/api/v1/my-endpoint', () => {
|
||||
return HttpResponse.json(
|
||||
{ success: false, error: { message: 'Server error' } },
|
||||
{ status: 500 },
|
||||
);
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
- Story files: `ComponentName.stories.tsx`
|
||||
- Co-located with their component file
|
||||
- Example: `src/components/ui/button.tsx` → `src/components/ui/button.stories.tsx`
|
||||
|
||||
## Story File Template
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MyComponent } from './MyComponent';
|
||||
|
||||
const meta: Meta<typeof MyComponent> = {
|
||||
title: 'Features/MyFeature/MyComponent',
|
||||
component: MyComponent,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof MyComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Default props
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
// Loading state configuration
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
// Error state configuration
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
// Empty state configuration
|
||||
};
|
||||
```
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
"typescript",
|
||||
"vite",
|
||||
"tailwindcss",
|
||||
"kodo-design-system",
|
||||
"sumi-design-system",
|
||||
"zustand",
|
||||
"websocket",
|
||||
"music",
|
||||
|
|
|
|||
1565
packages/design-system/package-lock.json
generated
1565
packages/design-system/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"name": "@veza/design-system",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./styles": "./dist/styles/index.css"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts",
|
||||
"dev": "tsup src/index.ts --format esm --dts --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.562.0",
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"tailwindcss": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface AvatarProps {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
fallback?: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar - User avatar component
|
||||
* Part of Kōdō Design System
|
||||
*/
|
||||
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||
({ src, alt = 'Avatar', fallback, size = 'md', className }, ref) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-8 h-8 text-xs',
|
||||
md: 'w-10 h-10 text-sm',
|
||||
lg: 'w-12 h-12 text-base',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-full overflow-hidden bg-kodo-slate flex items-center justify-center',
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{src ? (
|
||||
<img src={src} alt={alt} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="font-medium text-kodo-primary">
|
||||
{fallback || alt.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Avatar.displayName = 'Avatar';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Avatar, type AvatarProps } from './Avatar';
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import React from 'react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'gaming' | 'terminal' | 'nature' | 'icon';
|
||||
size?: 'sm' | 'md' | 'lg' | 'icon';
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'primary', size = 'md', icon, children, className, style, ...props }, ref) => {
|
||||
// Base styles
|
||||
const baseStyles =
|
||||
'relative inline-flex items-center justify-center font-body font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-kodo-void rounded-lg';
|
||||
|
||||
// Size styles
|
||||
const sizeStyles = {
|
||||
sm: 'text-xs px-3 py-1.5 gap-2',
|
||||
md: 'text-sm px-5 py-2.5 gap-2',
|
||||
lg: 'text-base px-8 py-3.5 gap-3',
|
||||
icon: 'p-2.5',
|
||||
};
|
||||
|
||||
// Variant styles
|
||||
const variantStyles = {
|
||||
primary:
|
||||
'bg-gradient-to-r from-kodo-cyan-dim to-kodo-cyan text-kodo-void hover:shadow-lg hover:shadow-kodo-cyan/20 border border-transparent font-bold tracking-wide',
|
||||
secondary:
|
||||
'bg-transparent border border-kodo-magenta/50 text-kodo-magenta hover:bg-kodo-magenta/5 hover:border-kodo-magenta hover:text-white',
|
||||
ghost: 'bg-transparent text-gray-400 hover:text-white hover:bg-white/5',
|
||||
gaming:
|
||||
'bg-kodo-slate border border-kodo-gold/40 text-kodo-gold hover:bg-kodo-gold/10 hover:border-kodo-gold font-bold tracking-wider uppercase',
|
||||
terminal:
|
||||
'bg-kodo-ink border border-kodo-steel text-gray-300 font-mono text-xs hover:border-kodo-cyan hover:text-kodo-cyan',
|
||||
nature: 'bg-kodo-slate border border-kodo-lime/30 text-kodo-lime hover:bg-kodo-lime/10',
|
||||
icon: 'bg-transparent hover:bg-white/10 text-gray-400 hover:text-white rounded-full p-2',
|
||||
};
|
||||
|
||||
// Combine classes
|
||||
const finalClass =
|
||||
variant === 'icon'
|
||||
? cn(baseStyles, variantStyles.icon, className)
|
||||
: cn(baseStyles, sizeStyles[size], variantStyles[variant], className);
|
||||
|
||||
// Force width: 100% if w-full class is present
|
||||
const inlineStyles: React.CSSProperties = {
|
||||
...(className?.includes('w-full') ? {
|
||||
width: '100%',
|
||||
minWidth: '200px', /* Largeur minimale pour éviter les boutons trop étroits */
|
||||
maxWidth: '100%',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
whiteSpace: 'nowrap' /* Empêche le texte de se couper */
|
||||
} : {}),
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={finalClass}
|
||||
style={inlineStyles}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className={children ? '' : 'flex items-center justify-center'}>{icon}</span>}
|
||||
{children && <span>{children}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Button, type ButtonProps } from './Button';
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'manga' | 'gaming' | 'glass';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ variant = 'default', children, className, onClick, style, ...props }, ref) => {
|
||||
const base = 'relative transition-all duration-300 rounded-xl';
|
||||
|
||||
const variants = {
|
||||
// Standard Card
|
||||
default: 'bg-kodo-graphite border border-kodo-steel/60 p-6 shadow-sm hover:border-kodo-steel',
|
||||
|
||||
// Creative/Talas style
|
||||
manga:
|
||||
'bg-gradient-to-br from-kodo-graphite to-kodo-slate border border-kodo-magenta/20 p-6 hover:border-kodo-magenta/40 hover:shadow-neon-magenta/10',
|
||||
|
||||
// Tech/Veza style
|
||||
gaming:
|
||||
'bg-kodo-ink border border-kodo-cyan/20 p-6 hover:border-kodo-cyan/40 hover:shadow-neon-cyan/10',
|
||||
|
||||
// Glassmorphism
|
||||
glass: 'bg-kodo-slate/40 backdrop-blur-xl border border-white/5 p-6 hover:bg-kodo-slate/50',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(base, variants[variant], onClick && 'cursor-pointer', className)}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
...(className?.includes('w-full') ? {
|
||||
width: '100%',
|
||||
minWidth: '0',
|
||||
maxWidth: '100%',
|
||||
boxSizing: 'border-box'
|
||||
} : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Card, type CardProps } from './Card';
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Search, Upload as UploadIcon } from 'lucide-react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Text Input */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, icon, className, type, autoComplete, required, id, ...props }, ref) => {
|
||||
// CRITIQUE FIX #4: Générer un ID stable si non fourni pour l'association avec le label
|
||||
const generatedId = React.useId();
|
||||
const inputId = id || generatedId;
|
||||
|
||||
// CRITIQUE FIX #4: Déterminer autoComplete par défaut basé sur le type
|
||||
const defaultAutoComplete = autoComplete !== undefined
|
||||
? autoComplete
|
||||
: (type === 'email' ? 'email'
|
||||
: type === 'password' ? 'current-password'
|
||||
: undefined);
|
||||
|
||||
// CRITIQUE FIX #51: S'assurer que aria-describedby et aria-invalid sont correctement passés
|
||||
const ariaDescribedBy = props['aria-describedby'];
|
||||
const ariaInvalid = props['aria-invalid'];
|
||||
|
||||
return (
|
||||
<div className="w-full" style={{ width: '100%', minWidth: '0', maxWidth: '100%', boxSizing: 'border-box' }}>
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-gray-400 mb-2 font-body">{label}</label>
|
||||
)}
|
||||
<div className="relative w-full" style={{ width: '100%', minWidth: '0', maxWidth: '100%', boxSizing: 'border-box' }}>
|
||||
{icon && (
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
type={type}
|
||||
autoComplete={defaultAutoComplete}
|
||||
required={required}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
aria-invalid={ariaInvalid}
|
||||
className={cn(
|
||||
'w-full py-3 bg-kodo-graphite border border-kodo-steel text-white placeholder-gray-500 font-body text-base rounded-lg focus-visible:outline-none focus-visible:border-kodo-cyan/60 transition-colors duration-200',
|
||||
icon ? 'pl-11 pr-4' : 'px-4',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minWidth: '200px', /* Largeur minimale pour éviter les inputs trop étroits */
|
||||
maxWidth: '100%',
|
||||
boxSizing: 'border-box',
|
||||
display: 'block',
|
||||
...props.style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Search Input */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const SearchInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.InputHTMLAttributes<HTMLInputElement>
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<div className="relative w-full group">
|
||||
<input
|
||||
ref={ref}
|
||||
type="search"
|
||||
className="w-full pl-12 pr-4 py-3 bg-kodo-graphite border border-kodo-steel text-white placeholder-gray-500 rounded-full focus-visible:outline-none focus-visible:border-kodo-cyan/60 transition-colors duration-300"
|
||||
placeholder="Search platform..."
|
||||
{...props}
|
||||
/>
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500 group-focus-within:text-kodo-cyan transition-colors" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SearchInput.displayName = 'SearchInput';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* File Upload */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export interface FileUploadProps {
|
||||
onUpload?: (files: FileList) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FileUpload: React.FC<FileUploadProps> = ({ onUpload, className }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-2 border-dashed border-kodo-steel rounded-xl p-8 bg-kodo-graphite/50 hover:bg-kodo-slate/30 hover:border-kodo-cyan/50 transition-all duration-300 cursor-pointer text-center group',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-kodo-slate flex items-center justify-center mx-auto mb-4 group-hover:scale-110 group-hover:bg-kodo-steel transition-all">
|
||||
<UploadIcon className="w-8 h-8 text-kodo-cyan" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-2 font-display">Drop your stems here</h3>
|
||||
<p className="text-gray-500 text-sm max-w-md mx-auto">
|
||||
Support for WAV, FLAC, AIFF. Up to 500MB per file.{' '}
|
||||
<span className="text-kodo-cyan">Premium users</span> get unlimited storage.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { Input, SearchInput, FileUpload, type InputProps, type FileUploadProps } from './Input';
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface NotificationBadgeProps {
|
||||
count?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationBadge - Simple notification indicator
|
||||
* Part of Kōdō Design System
|
||||
*/
|
||||
export const NotificationBadge = React.forwardRef<HTMLDivElement, NotificationBadgeProps>(
|
||||
({ count = 0, className }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={cn('relative inline-block', className)}>
|
||||
<Bell className="w-5 h-5 text-kodo-secondary hover:text-kodo-primary transition-colors" />
|
||||
{count > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-kodo-red rounded-full flex items-center justify-center text-[10px] font-bold text-white border border-kodo-void">
|
||||
{count > 9 ? '9+' : count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NotificationBadge.displayName = 'NotificationBadge';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { NotificationBadge, type NotificationBadgeProps } from './NotificationBadge';
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import React from 'react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface ProgressBarProps {
|
||||
value: number; // 0 to 100
|
||||
max?: number;
|
||||
variant?: 'default' | 'gaming' | 'segmented';
|
||||
color?: 'cyan' | 'magenta' | 'lime' | 'gold';
|
||||
labelLeft?: string;
|
||||
labelRight?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||
value,
|
||||
max = 100,
|
||||
variant = 'default',
|
||||
color = 'cyan',
|
||||
labelLeft,
|
||||
labelRight,
|
||||
className,
|
||||
}) => {
|
||||
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
const colorStyles = {
|
||||
cyan: 'bg-kodo-cyan',
|
||||
magenta: 'bg-kodo-magenta',
|
||||
lime: 'bg-kodo-lime',
|
||||
gold: 'bg-kodo-gold',
|
||||
};
|
||||
|
||||
const gradientStyles = {
|
||||
cyan: 'from-kodo-cyan to-blue-500',
|
||||
magenta: 'from-kodo-magenta to-purple-600',
|
||||
lime: 'from-kodo-lime to-green-600',
|
||||
gold: 'from-kodo-gold to-orange-500',
|
||||
};
|
||||
|
||||
if (variant === 'gaming') {
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<div className="h-4 bg-kodo-void rounded-full overflow-hidden border border-kodo-gold/30">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full bg-gradient-to-r shadow-[0_0_10px_rgba(255,215,0,0.5)] transition-all duration-500',
|
||||
gradientStyles.gold
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
{(labelLeft || labelRight) && (
|
||||
<div className="flex justify-between text-[10px] font-mono font-bold text-kodo-gold mt-1 uppercase tracking-wider">
|
||||
<span>{labelLeft}</span>
|
||||
<span>{labelRight}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
<div className="h-2 bg-kodo-steel rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full transition-all duration-300 shadow-[0_0_10px_currentColor]',
|
||||
colorStyles[color]
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
{(labelLeft || labelRight) && (
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1 font-mono">
|
||||
<span>{labelLeft}</span>
|
||||
<span>{labelRight}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { ProgressBar, type ProgressBarProps } from './Progress';
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
change?: string;
|
||||
icon?: React.ReactNode;
|
||||
color?: 'cyan' | 'lime' | 'magenta' | 'gold' | 'orange';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* StatCard - Dashboard statistics card component
|
||||
* Part of Kōdō Design System
|
||||
*/
|
||||
export const StatCard = React.forwardRef<HTMLDivElement, StatCardProps>(
|
||||
({ title, value, change, icon, color = 'cyan', className }, ref) => {
|
||||
const colorClasses = {
|
||||
cyan: 'text-kodo-cyan',
|
||||
lime: 'text-kodo-lime',
|
||||
magenta: 'text-kodo-magenta',
|
||||
gold: 'text-kodo-gold',
|
||||
orange: 'text-kodo-orange',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-kodo-graphite border border-kodo-steel/60 p-6 rounded-xl hover:border-kodo-cyan/40 transition-all duration-300',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<h3 className="text-sm font-medium text-kodo-secondary">{title}</h3>
|
||||
{icon && <div className={cn('h-4 w-4', colorClasses[color])}>{icon}</div>}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-2xl font-bold text-white">{value}</div>
|
||||
{change && (
|
||||
<p className="text-xs text-kodo-secondary mt-1">
|
||||
<span className="text-kodo-lime">{change}</span>
|
||||
{' par rapport au mois dernier'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StatCard.displayName = 'StatCard';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { StatCard, type StatCardProps } from './StatCard';
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Music, Play, MoreVertical } from 'lucide-react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface Track {
|
||||
id: string;
|
||||
title: string;
|
||||
artist?: string;
|
||||
duration?: string;
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
export interface TrackListProps {
|
||||
tracks: Track[];
|
||||
onTrackClick?: (track: Track) => void;
|
||||
onPlayClick?: (track: Track) => void;
|
||||
onMoreClick?: (track: Track) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TrackList - Display a list of tracks
|
||||
* Part of Kōdō Design System
|
||||
*/
|
||||
export const TrackList = React.forwardRef<HTMLDivElement, TrackListProps>(
|
||||
({ tracks, onTrackClick, onPlayClick, onMoreClick, className }, ref) => {
|
||||
if (tracks.length === 0) {
|
||||
return (
|
||||
<div ref={ref} className={cn('text-center py-8', className)}>
|
||||
<p className="text-kodo-secondary text-sm">Aucune piste disponible</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn('space-y-2', className)}>
|
||||
{tracks.map((track) => (
|
||||
<div
|
||||
key={track.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-kodo-graphite/50 hover:bg-kodo-slate/50 transition-all group cursor-pointer"
|
||||
onClick={() => onTrackClick?.(track)}
|
||||
>
|
||||
{/* Cover / Icon */}
|
||||
<div className="relative w-12 h-12 rounded bg-kodo-slate flex items-center justify-center flex-shrink-0">
|
||||
{track.coverUrl ? (
|
||||
<img
|
||||
src={track.coverUrl}
|
||||
alt={track.title}
|
||||
className="w-full h-full object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<Music className="w-5 h-5 text-kodo-cyan" />
|
||||
)}
|
||||
{/* Play button overlay */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPlayClick?.(track);
|
||||
}}
|
||||
className="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded"
|
||||
>
|
||||
<Play className="w-5 h-5 text-white fill-current" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Track Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{track.title}</p>
|
||||
{track.artist && (
|
||||
<p className="text-xs text-kodo-secondary truncate">{track.artist}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
{track.duration && (
|
||||
<span className="text-xs text-kodo-secondary font-mono">{track.duration}</span>
|
||||
)}
|
||||
|
||||
{/* More button */}
|
||||
{onMoreClick && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoreClick(track);
|
||||
}}
|
||||
className="p-2 rounded-lg hover:bg-kodo-steel/30 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<MoreVertical className="w-4 h-4 text-kodo-secondary" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TrackList.displayName = 'TrackList';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { TrackList, type TrackListProps, type Track } from './TrackList';
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Components
|
||||
export { Button, type ButtonProps } from './components/Button';
|
||||
export { Card, type CardProps } from './components/Card';
|
||||
export { Input, SearchInput, FileUpload, type InputProps, type FileUploadProps } from './components/Input';
|
||||
export { ProgressBar, type ProgressBarProps } from './components/Progress';
|
||||
export { StatCard, type StatCardProps } from './components/StatCard';
|
||||
export { TrackList, type TrackListProps, type Track } from './components/TrackList';
|
||||
export { NotificationBadge, type NotificationBadgeProps } from './components/NotificationBadge';
|
||||
export { Avatar, type AvatarProps } from './components/Avatar';
|
||||
|
||||
// Tokens
|
||||
export { colors, cssVariables, type KodoColor } from './tokens/colors';
|
||||
|
||||
// Utils
|
||||
export { cn } from './utils/cn';
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Kōdō Design System Global Styles */
|
||||
|
||||
:root {
|
||||
/* SPECTRE ASTRAL PALETTE */
|
||||
--kodo-void: 11 12 16;
|
||||
--kodo-ink: 23 25 35;
|
||||
--kodo-graphite: 31 40 51;
|
||||
--kodo-slate: 44 54 67;
|
||||
--kodo-steel: 59 69 84;
|
||||
|
||||
--kodo-cyan: 102 252 241;
|
||||
--kodo-cyan-dim: 69 162 158;
|
||||
--kodo-magenta: 138 126 164;
|
||||
--kodo-orange: 230 184 156;
|
||||
|
||||
--kodo-lime: 54 229 209;
|
||||
--kodo-gold: 234 179 8;
|
||||
--kodo-red: 230 57 70;
|
||||
|
||||
--kodo-text-main: 243 243 224;
|
||||
--kodo-content-highlight: 255 255 255;
|
||||
--kodo-content-dim: 156 163 175;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-kodo-void text-kodo-primary font-body;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-image:
|
||||
radial-gradient(circle at 15% 0%, rgba(var(--kodo-cyan), 0.05) 0%, transparent 40%),
|
||||
radial-gradient(circle at 85% 100%, rgba(var(--kodo-magenta), 0.05) 0%, transparent 40%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(var(--kodo-steel));
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(var(--kodo-cyan-dim));
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 10px rgba(var(--kodo-cyan), 0.2);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(var(--kodo-cyan), 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Kōdō Design System - Spectre Astral Color Palette
|
||||
* Extracted from veza_frontend_web_v2
|
||||
*/
|
||||
|
||||
export const colors = {
|
||||
// Core Backgrounds
|
||||
void: 'rgb(11 12 16)', // #0B0C10 Nadir Black
|
||||
ink: 'rgb(23 25 35)', // Darker Blue-Grey for panels
|
||||
graphite: 'rgb(31 40 51)', // #1F2833 Graphite Blue
|
||||
slate: 'rgb(44 54 67)', // Surface Elevated
|
||||
steel: 'rgb(59 69 84)', // Borders
|
||||
|
||||
// Accents
|
||||
cyan: 'rgb(102 252 241)', // #66FCF1 Spectral Cyan
|
||||
'cyan-dim': 'rgb(69 162 158)', // #45A29E Anodized Turquoise
|
||||
magenta: 'rgb(138 126 164)', // #8A7EA4 Astral Lavender
|
||||
orange: 'rgb(230 184 156)', // #E6B89C Soft Ember
|
||||
|
||||
// Utility Colors
|
||||
lime: 'rgb(54 229 209)', // Muted Teal/Green for Success
|
||||
gold: 'rgb(234 179 8)', // Refined Gold
|
||||
red: 'rgb(230 57 70)', // #E63946 Error
|
||||
|
||||
// Text
|
||||
primary: 'rgb(243 243 224)', // #F3F3E0 Quiet Paper
|
||||
secondary: 'rgb(156 163 175)', // Gray-400
|
||||
} as const;
|
||||
|
||||
export type KodoColor = keyof typeof colors;
|
||||
|
||||
export const cssVariables = `
|
||||
:root {
|
||||
/* SPECTRE ASTRAL PALETTE */
|
||||
--kodo-void: 11 12 16;
|
||||
--kodo-ink: 23 25 35;
|
||||
--kodo-graphite: 31 40 51;
|
||||
--kodo-slate: 44 54 67;
|
||||
--kodo-steel: 59 69 84;
|
||||
|
||||
--kodo-cyan: 102 252 241;
|
||||
--kodo-cyan-dim: 69 162 158;
|
||||
--kodo-magenta: 138 126 164;
|
||||
--kodo-orange: 230 184 156;
|
||||
|
||||
--kodo-lime: 54 229 209;
|
||||
--kodo-gold: 234 179 8;
|
||||
--kodo-red: 230 57 70;
|
||||
|
||||
--kodo-text-main: 243 243 224;
|
||||
--kodo-content-highlight: 255 255 255;
|
||||
--kodo-content-dim: 156 163 175;
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Utility function to merge Tailwind CSS classes
|
||||
* Combines clsx for conditional classes and tailwind-merge for deduplication
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
kodo: {
|
||||
void: 'rgb(var(--kodo-void) / <alpha-value>)',
|
||||
ink: 'rgb(var(--kodo-ink) / <alpha-value>)',
|
||||
graphite: 'rgb(var(--kodo-graphite) / <alpha-value>)',
|
||||
slate: 'rgb(var(--kodo-slate) / <alpha-value>)',
|
||||
steel: 'rgb(var(--kodo-steel) / <alpha-value>)',
|
||||
cyan: 'rgb(var(--kodo-cyan) / <alpha-value>)',
|
||||
'cyan-dim': 'rgb(var(--kodo-cyan-dim) / <alpha-value>)',
|
||||
magenta: 'rgb(var(--kodo-magenta) / <alpha-value>)',
|
||||
lime: 'rgb(var(--kodo-lime) / <alpha-value>)',
|
||||
orange: 'rgb(var(--kodo-orange) / <alpha-value>)',
|
||||
gold: 'rgb(var(--kodo-gold) / <alpha-value>)',
|
||||
red: 'rgb(var(--kodo-red) / <alpha-value>)',
|
||||
primary: 'rgb(var(--kodo-content-highlight) / <alpha-value>)',
|
||||
secondary: 'rgb(var(--kodo-content-dim) / <alpha-value>)',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['Space Grotesk', 'sans-serif'],
|
||||
heading: ['Space Grotesk', 'sans-serif'],
|
||||
body: ['Inter', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-neon': 'linear-gradient(135deg, rgb(var(--kodo-cyan-dim)) 0%, rgb(var(--kodo-cyan)) 100%)',
|
||||
'gradient-gaming': 'linear-gradient(135deg, rgb(var(--kodo-graphite)) 0%, rgb(var(--kodo-ink)) 100%)',
|
||||
'gradient-cyber': 'linear-gradient(135deg, rgba(var(--kodo-cyan), 0.1) 0%, rgba(var(--kodo-magenta), 0.1) 100%)',
|
||||
},
|
||||
boxShadow: {
|
||||
'neon-cyan': '0 0 20px rgba(var(--kodo-cyan), 0.15)',
|
||||
'neon-magenta': '0 0 20px rgba(var(--kodo-magenta), 0.15)',
|
||||
gaming: '0 10px 30px -10px rgba(0,0,0,0.5)',
|
||||
glass: '0 8px 32px 0 rgba(0, 0, 0, 0.36)',
|
||||
},
|
||||
borderRadius: {
|
||||
xl: '12px',
|
||||
'2xl': '16px',
|
||||
'3xl': '24px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue