refactor(studio): CreateProjectModal module, re-export, stories

- Module create-project-modal: types, useCreateProjectModal, Header,
  Form, Footer, Skeleton, orchestrator CreateProjectModal
- Re-export from projects/CreateProjectModal.tsx
- Stories: Default, Loading (Skeleton); decorator min-h-layout-story

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
senke 2026-02-06 01:51:25 +01:00
parent 3ad473fde2
commit 946b6721b3
10 changed files with 281 additions and 135 deletions

View file

@ -1,17 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CreateProjectModal } from './CreateProjectModal';
import { CreateProjectModal, CreateProjectModalSkeleton } from './CreateProjectModal';
import { fn } from '@storybook/test';
const meta: Meta<typeof CreateProjectModal> = {
title: 'Components/Features/Studio/Projects/CreateProjectModal',
component: CreateProjectModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
title: 'Components/Features/Studio/Projects/CreateProjectModal',
component: CreateProjectModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-layout-story flex items-center justify-center p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Creating: Story = { name: 'Création' };
export const Default: Story = {};
export const Loading: Story = {
render: () => <CreateProjectModalSkeleton />,
};

View file

@ -1,127 +1,4 @@
import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { Input } from '../../ui/input';
import { X, Layers } from 'lucide-react';
interface CreateProjectModalProps {
onClose: () => void;
onCreate: (project: any) => void;
}
export const CreateProjectModal: React.FC<CreateProjectModalProps> = ({
onClose,
onCreate,
}) => {
const [formData, setFormData] = useState({
name: '',
daw: 'Ableton',
bpm: '128',
key: 'C Min',
description: '',
});
const handleSubmit = () => {
if (!formData.name) return;
onCreate({
...formData,
progress: 0,
status: 'Idea',
collaborators: [],
modified: 'Just now',
});
onClose();
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
onClick={onClose}
></div>
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl animate-scaleIn overflow-hidden">
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
<h3 className="font-bold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-kodo-steel" /> New Project
</h3>
<button onClick={onClose}>
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
</button>
</div>
<div className="p-6 space-y-6">
<Input
label="Project Name"
placeholder="e.g. Neon Genesis"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
autoFocus
/>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Primary Workstation (DAW)
</label>
<div className="grid grid-cols-3 gap-4">
{['Ableton', 'FL Studio', 'Logic Pro'].map((daw) => (
<button
key={daw}
onClick={() => setFormData({ ...formData, daw })}
className={`p-4 rounded-lg border text-sm font-bold transition-all ${formData.daw === daw ? 'bg-kodo-cyan/10 border-kodo-cyan text-white' : 'bg-kodo-void border-kodo-steel text-kodo-content-dim hover:border-kodo-steel'}`}
>
{daw}
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
label="BPM"
type="number"
placeholder="128"
value={formData.bpm}
onChange={(e) =>
setFormData({ ...formData, bpm: e.target.value })
}
/>
<Input
label="Key"
placeholder="C Minor"
value={formData.key}
onChange={(e) =>
setFormData({ ...formData, key: e.target.value })
}
/>
</div>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Description
</label>
<textarea
className="w-full bg-kodo-void border border-kodo-steel rounded-lg p-4 text-white focus:border-kodo-steel outline-none text-sm resize-none h-24"
placeholder="Project goals, vibe, or reference tracks..."
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
/>
</div>
</div>
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={!formData.name}
>
Create Project
</Button>
</div>
</div>
</div>
);
};
export {
CreateProjectModal,
CreateProjectModalSkeleton,
} from './create-project-modal';

View file

@ -0,0 +1,34 @@
import React from 'react';
import { useCreateProjectModal } from './useCreateProjectModal';
import { CreateProjectModalHeader } from './CreateProjectModalHeader';
import { CreateProjectModalForm } from './CreateProjectModalForm';
import { CreateProjectModalFooter } from './CreateProjectModalFooter';
import type { CreateProjectModalProps } from './types';
export function CreateProjectModal({ onClose, onCreate }: CreateProjectModalProps) {
const { formData, updateField, handleSubmit, canSubmit } = useCreateProjectModal(onCreate);
const handleSubmitAndClose = () => {
handleSubmit();
onClose();
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
onClick={onClose}
aria-hidden
/>
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl animate-scaleIn overflow-hidden">
<CreateProjectModalHeader onClose={onClose} />
<CreateProjectModalForm formData={formData} onFieldChange={updateField} />
<CreateProjectModalFooter
onClose={onClose}
onSubmit={handleSubmitAndClose}
canSubmit={canSubmit}
/>
</div>
</div>
);
}

View file

@ -0,0 +1,24 @@
import { Button } from '@/components/ui/button';
interface CreateProjectModalFooterProps {
onClose: () => void;
onSubmit: () => void;
canSubmit: boolean;
}
export function CreateProjectModalFooter({
onClose,
onSubmit,
canSubmit,
}: CreateProjectModalFooterProps) {
return (
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" onClick={onSubmit} disabled={!canSubmit}>
Create Project
</Button>
</div>
);
}

View file

@ -0,0 +1,74 @@
import { Input } from '@/components/ui/input';
import { DAW_OPTIONS } from './types';
import type { CreateProjectFormState } from './types';
interface CreateProjectModalFormProps {
formData: CreateProjectFormState;
onFieldChange: <K extends keyof CreateProjectFormState>(
field: K,
value: CreateProjectFormState[K],
) => void;
}
export function CreateProjectModalForm({
formData,
onFieldChange,
}: CreateProjectModalFormProps) {
return (
<div className="p-6 space-y-6">
<Input
label="Project Name"
placeholder="e.g. Neon Genesis"
value={formData.name}
onChange={(e) => onFieldChange('name', e.target.value)}
autoFocus
/>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Primary Workstation (DAW)
</label>
<div className="grid grid-cols-3 gap-4">
{DAW_OPTIONS.map((daw) => (
<button
key={daw}
type="button"
onClick={() => onFieldChange('daw', daw)}
className={`p-4 rounded-lg border text-sm font-bold transition-all ${formData.daw === daw ? 'bg-kodo-cyan/10 border-kodo-cyan text-white' : 'bg-kodo-void border-kodo-steel text-kodo-content-dim hover:border-kodo-steel'}`}
>
{daw}
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
label="BPM"
type="number"
placeholder="128"
value={formData.bpm}
onChange={(e) => onFieldChange('bpm', e.target.value)}
/>
<Input
label="Key"
placeholder="C Minor"
value={formData.key}
onChange={(e) => onFieldChange('key', e.target.value)}
/>
</div>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Description
</label>
<textarea
className="w-full bg-kodo-void border border-kodo-steel rounded-lg p-4 text-white focus:border-kodo-steel outline-none text-sm resize-none h-24"
placeholder="Project goals, vibe, or reference tracks..."
value={formData.description}
onChange={(e) => onFieldChange('description', e.target.value)}
/>
</div>
</div>
);
}

View file

@ -0,0 +1,18 @@
import { X, Layers } from 'lucide-react';
interface CreateProjectModalHeaderProps {
onClose: () => void;
}
export function CreateProjectModalHeader({ onClose }: CreateProjectModalHeaderProps) {
return (
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
<h3 className="font-bold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-kodo-steel" /> New Project
</h3>
<button type="button" onClick={onClose} aria-label="Close">
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
</button>
</div>
);
}

View file

@ -0,0 +1,39 @@
/**
* Skeleton for CreateProjectModal layout primitive, no arbitrary values.
*/
export function CreateProjectModalSkeleton() {
return (
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl overflow-hidden animate-pulse">
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
<div className="h-6 w-32 rounded bg-muted" />
<div className="h-5 w-5 rounded bg-muted" />
</div>
<div className="p-6 space-y-6">
<div className="space-y-2">
<div className="h-4 w-24 rounded bg-muted" />
<div className="h-11 w-full rounded-lg bg-muted" />
</div>
<div className="space-y-2">
<div className="h-4 w-40 rounded bg-muted" />
<div className="grid grid-cols-3 gap-4">
<div className="h-14 rounded-lg bg-muted" />
<div className="h-14 rounded-lg bg-muted" />
<div className="h-14 rounded-lg bg-muted" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="h-11 rounded-lg bg-muted" />
<div className="h-11 rounded-lg bg-muted" />
</div>
<div className="space-y-2">
<div className="h-4 w-24 rounded bg-muted" />
<div className="h-24 w-full rounded-lg bg-muted" />
</div>
</div>
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
<div className="h-10 w-20 rounded-lg bg-muted" />
<div className="h-10 w-28 rounded-lg bg-muted" />
</div>
</div>
);
}

View file

@ -0,0 +1,12 @@
export { CreateProjectModal } from './CreateProjectModal';
export { CreateProjectModalSkeleton } from './CreateProjectModalSkeleton';
export { CreateProjectModalHeader } from './CreateProjectModalHeader';
export { CreateProjectModalForm } from './CreateProjectModalForm';
export { CreateProjectModalFooter } from './CreateProjectModalFooter';
export { useCreateProjectModal } from './useCreateProjectModal';
export { DAW_OPTIONS } from './types';
export type {
CreateProjectModalProps,
CreateProjectFormState,
CreateProjectFormPayload,
} from './types';

View file

@ -0,0 +1,21 @@
export interface CreateProjectModalProps {
onClose: () => void;
onCreate: (project: CreateProjectFormPayload) => void;
}
export interface CreateProjectFormState {
name: string;
daw: string;
bpm: string;
key: string;
description: string;
}
export interface CreateProjectFormPayload extends CreateProjectFormState {
progress: number;
status: string;
collaborators: number | unknown[];
modified: string;
}
export const DAW_OPTIONS = ['Ableton', 'FL Studio', 'Logic Pro'] as const;

View file

@ -0,0 +1,37 @@
import { useState, useCallback } from 'react';
import type { CreateProjectFormState, CreateProjectFormPayload } from './types';
const INITIAL_STATE: CreateProjectFormState = {
name: '',
daw: 'Ableton',
bpm: '128',
key: 'C Min',
description: '',
};
export function useCreateProjectModal(onCreate: (project: CreateProjectFormPayload) => void) {
const [formData, setFormData] = useState<CreateProjectFormState>(INITIAL_STATE);
const updateField = useCallback(<K extends keyof CreateProjectFormState>(
field: K,
value: CreateProjectFormState[K],
) => {
setFormData((prev) => ({ ...prev, [field]: value }));
}, []);
const handleSubmit = useCallback(() => {
if (!formData.name) return;
const payload: CreateProjectFormPayload = {
...formData,
progress: 0,
status: 'Idea',
collaborators: [],
modified: 'Just now',
};
onCreate(payload);
}, [formData, onCreate]);
const canSubmit = !!formData.name.trim();
return { formData, updateField, handleSubmit, canSubmit };
}