create for series

This commit is contained in:
Nicolas Cantu 2026-01-14 16:30:39 +01:00
parent 53991c7791
commit 95a2019956
11 changed files with 64 additions and 29 deletions

View File

@ -1,3 +1,5 @@
import { Card } from './ui'
interface SeriesStatsProps { interface SeriesStatsProps {
sponsoring: number sponsoring: number
purchases: number purchases: number
@ -17,10 +19,10 @@ export function SeriesStats({ sponsoring, purchases, reviewTips }: SeriesStatsPr
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{items.map((item) => ( {items.map((item) => (
<div key={item.label} className="border rounded-lg p-3 bg-white text-sm"> <Card key={item.label} variant="default" className="bg-white text-sm">
<div className="text-gray-600">{item.label}</div> <div className="text-gray-600">{item.label}</div>
<div className="font-semibold text-gray-900">{item.value}</div> <div className="font-semibold text-gray-900">{item.value}</div>
</div> </Card>
))} ))}
</div> </div>
) )

View File

@ -1,5 +1,6 @@
import { ArticleEditorForm } from './ArticleEditorForm' import { ArticleEditorForm } from './ArticleEditorForm'
import type { ArticleDraft } from '@/lib/articlePublisher' import type { ArticleDraft } from '@/lib/articlePublisher'
import { Card } from './ui'
interface EditPanelProps { interface EditPanelProps {
draft: ArticleDraft | null draft: ArticleDraft | null
@ -24,7 +25,7 @@ export function EditPanel({
return null return null
} }
return ( return (
<div className="border rounded-lg p-4 bg-white space-y-3"> <Card variant="default" className="bg-white space-y-3">
<h3 className="text-lg font-semibold">Edit article</h3> <h3 className="text-lg font-semibold">Edit article</h3>
<ArticleEditorForm <ArticleEditorForm
draft={draft} draft={draft}
@ -37,6 +38,6 @@ export function EditPanel({
error={error} error={error}
onCancel={onCancel} onCancel={onCancel}
/> />
</div> </Card>
) )
} }

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useNostrAuth } from '@/hooks/useNostrAuth' import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation' import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
import { Button } from '../ui' import { Button, Card } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { NoAccountView } from './NoAccountView' import { NoAccountView } from './NoAccountView'
import { PresentationForm } from './PresentationForm' import { PresentationForm } from './PresentationForm'
@ -10,7 +10,7 @@ import { useAuthorPresentationState } from './useAuthorPresentationState'
function SuccessNotice(params: { pubkey: string | null }): React.ReactElement { function SuccessNotice(params: { pubkey: string | null }): React.ReactElement {
return ( return (
<div className="border border-neon-green/50 rounded-lg p-6 bg-neon-green/10"> <Card variant="default" className="border-neon-green/50 bg-neon-green/10">
<h3 className="text-lg font-semibold text-neon-green mb-2">{t('presentation.success')}</h3> <h3 className="text-lg font-semibold text-neon-green mb-2">{t('presentation.success')}</h3>
<p className="text-cyber-accent mb-4">{t('presentation.successMessage')}</p> <p className="text-cyber-accent mb-4">{t('presentation.successMessage')}</p>
{params.pubkey ? ( {params.pubkey ? (
@ -22,7 +22,7 @@ function SuccessNotice(params: { pubkey: string | null }): React.ReactElement {
</a> </a>
</div> </div>
) : null} ) : null}
</div> </Card>
) )
} }

View File

@ -1,4 +1,4 @@
import { Button, Textarea } from '../ui' import { Button, Card, Textarea } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import type { Page } from '@/types/nostr' import type { Page } from '@/types/nostr'
@ -39,10 +39,10 @@ function PageEditor(params: {
onImageUpload: (file: File) => Promise<void> onImageUpload: (file: File) => Promise<void>
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div className="border rounded-lg p-4 space-y-3"> <Card variant="default" className="space-y-3">
<PageEditorHeader page={params.page} onTypeChange={params.onTypeChange} onRemove={params.onRemove} /> <PageEditorHeader page={params.page} onTypeChange={params.onTypeChange} onRemove={params.onRemove} />
<PageEditorBody page={params.page} onContentChange={params.onContentChange} onImageUpload={params.onImageUpload} /> <PageEditorBody page={params.page} onContentChange={params.onContentChange} onImageUpload={params.onImageUpload} />
</div> </Card>
) )
} }

View File

@ -17,6 +17,7 @@ interface ButtonProps {
'aria-expanded'?: boolean 'aria-expanded'?: boolean
'aria-haspopup'?: boolean | 'true' | 'false' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' 'aria-haspopup'?: boolean | 'true' | 'false' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
'aria-selected'?: boolean 'aria-selected'?: boolean
'aria-controls'?: string
role?: string role?: string
id?: string id?: string
} }
@ -95,6 +96,17 @@ function ButtonContent({
) )
} }
function getCombinedClasses(
variant: ButtonVariant,
size: ButtonSize,
className: string
): string {
const variantClasses = getVariantClasses(variant)
const sizeClasses = getSizeClasses(size)
const baseClasses = 'inline-flex items-center justify-center rounded-lg font-medium transition-all border focus:outline-none focus:ring-2 focus:ring-neon-cyan disabled:opacity-50 disabled:cursor-not-allowed'
return `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`.trim()
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref): React.ReactElement => { export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref): React.ReactElement => {
const { const {
children, children,
@ -109,14 +121,11 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, r
'aria-expanded': ariaExpanded, 'aria-expanded': ariaExpanded,
'aria-haspopup': ariaHaspopup, 'aria-haspopup': ariaHaspopup,
'aria-selected': ariaSelected, 'aria-selected': ariaSelected,
'aria-controls': ariaControls,
role: roleProp, role: roleProp,
id, id,
} = props } = props
const variantClasses = getVariantClasses(variant) const combinedClasses = getCombinedClasses(variant, size, className)
const sizeClasses = getSizeClasses(size)
const baseClasses = 'inline-flex items-center justify-center rounded-lg font-medium transition-all border focus:outline-none focus:ring-2 focus:ring-neon-cyan disabled:opacity-50 disabled:cursor-not-allowed'
const combinedClasses = `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`.trim()
return ( return (
<button <button
@ -131,6 +140,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, r
aria-expanded={ariaExpanded} aria-expanded={ariaExpanded}
aria-haspopup={ariaHaspopup} aria-haspopup={ariaHaspopup}
aria-selected={ariaSelected} aria-selected={ariaSelected}
aria-controls={ariaControls}
aria-busy={loading} aria-busy={loading}
> >
<ButtonContent loading={loading}>{children}</ButtonContent> <ButtonContent loading={loading}>{children}</ButtonContent>

View File

@ -8,6 +8,7 @@ interface CardProps {
className?: string className?: string
onClick?: () => void onClick?: () => void
'aria-label'?: string 'aria-label'?: string
role?: string
} }
function getVariantClasses(variant: CardVariant, hasOnClick: boolean): string { function getVariantClasses(variant: CardVariant, hasOnClick: boolean): string {
@ -40,17 +41,19 @@ export function Card({
className = '', className = '',
onClick, onClick,
'aria-label': ariaLabel, 'aria-label': ariaLabel,
role: roleProp,
}: CardProps): React.ReactElement { }: CardProps): React.ReactElement {
const variantClasses = getVariantClasses(variant, onClick !== undefined) const variantClasses = getVariantClasses(variant, onClick !== undefined)
const paddingClasses = getPaddingClasses(variant) const paddingClasses = getPaddingClasses(variant)
const combinedClasses = `${variantClasses} ${paddingClasses} ${className}`.trim() const combinedClasses = `${variantClasses} ${paddingClasses} ${className}`.trim()
const role = roleProp ?? (onClick !== undefined ? 'button' : undefined)
if (onClick) { if (onClick) {
return ( return (
<div <div
onClick={onClick} onClick={onClick}
className={combinedClasses} className={combinedClasses}
role="button" role={role}
tabIndex={0} tabIndex={0}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
@ -66,7 +69,7 @@ export function Card({
} }
return ( return (
<div className={combinedClasses} aria-label={ariaLabel}> <div className={combinedClasses} role={role} aria-label={ariaLabel}>
{children} {children}
</div> </div>
) )

View File

@ -1,4 +1,5 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Card } from './Card'
interface ErrorStateProps { interface ErrorStateProps {
message: string message: string
@ -27,7 +28,7 @@ function ErrorIcon(): React.ReactElement {
export function ErrorState({ message, action, className = '' }: ErrorStateProps): React.ReactElement { export function ErrorState({ message, action, className = '' }: ErrorStateProps): React.ReactElement {
return ( return (
<div className={`bg-red-900/20 border border-red-500/50 rounded-lg p-4 ${className}`} role="alert"> <Card variant="default" className={`bg-red-900/20 border-red-500/50 ${className}`} role="alert">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-shrink-0 text-red-400"> <div className="flex-shrink-0 text-red-400">
<ErrorIcon /> <ErrorIcon />
@ -37,6 +38,6 @@ export function ErrorState({ message, action, className = '' }: ErrorStateProps)
{action && <div className="mt-3">{action}</div>} {action && <div className="mt-3">{action}</div>}
</div> </div>
</div> </div>
</div> </Card>
) )
} }

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Button } from './Button'
interface MobileMenuProps { interface MobileMenuProps {
children: ReactNode children: ReactNode
@ -130,13 +131,15 @@ function MobileMenuContent({
> >
<div className="p-4"> <div className="p-4">
<div className="flex justify-end mb-4"> <div className="flex justify-end mb-4">
<button <Button
type="button"
variant="ghost"
onClick={onClose} onClick={onClose}
className="text-cyber-accent hover:text-neon-cyan text-2xl transition-colors focus:outline-none focus:ring-2 focus:ring-neon-cyan rounded" className="text-cyber-accent hover:text-neon-cyan text-2xl p-0"
aria-label="Close menu" aria-label="Close menu"
> >
× ×
</button> </Button>
</div> </div>
<div className="space-y-4">{children}</div> <div className="space-y-4">{children}</div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Button } from './Button'
interface ModalProps { interface ModalProps {
children: ReactNode children: ReactNode
@ -28,13 +29,15 @@ function getSizeClasses(size: ModalProps['size']): string {
function CloseButton({ onClose }: { onClose: () => void }): React.ReactElement { function CloseButton({ onClose }: { onClose: () => void }): React.ReactElement {
return ( return (
<button <Button
type="button"
variant="ghost"
onClick={onClose} onClick={onClose}
className="text-cyber-accent hover:text-neon-cyan text-2xl transition-colors focus:outline-none focus:ring-2 focus:ring-neon-cyan rounded" className="text-cyber-accent hover:text-neon-cyan text-2xl p-0 w-auto h-auto min-w-0"
aria-label="Close modal" aria-label="Close modal"
> >
× ×
</button> </Button>
) )
} }

View File

@ -1,5 +1,6 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Button } from './Button'
export type ToastVariant = 'info' | 'success' | 'warning' | 'error' export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
@ -55,13 +56,15 @@ export function Toast({
aria-label={ariaLabel} aria-label={ariaLabel}
> >
<div className="flex-1">{children}</div> <div className="flex-1">{children}</div>
<button <Button
type="button"
variant="ghost"
onClick={onClose} onClick={onClose}
className="ml-4 text-current hover:opacity-70 transition-opacity focus:outline-none focus:ring-2 focus:ring-current rounded" className="ml-4 text-current hover:opacity-70 p-0"
aria-label="Close notification" aria-label="Close notification"
> >
× ×
</button> </Button>
</div> </div>
) )
} }

View File

@ -87,7 +87,16 @@ Aucun composant prioritaire restant. Tous les composants principaux ont été mi
- ✅ `NotificationPanel.tsx` - Migration du conteneur principal vers Card - ✅ `NotificationPanel.tsx` - Migration du conteneur principal vers Card
- ✅ `AuthorFilterButton.tsx` - Migration vers Button avec support forwardRef - ✅ `AuthorFilterButton.tsx` - Migration vers Button avec support forwardRef
- ✅ `AuthorFilterDropdown.tsx` - Migration de AuthorOption et AllAuthorsOption vers Button - ✅ `AuthorFilterDropdown.tsx` - Migration de AuthorOption et AllAuthorsOption vers Button
- ✅ `components/ui/Button.tsx` - Ajout du support forwardRef, aria-expanded, aria-haspopup, aria-selected, role, id - ✅ `components/ui/Button.tsx` - Ajout du support forwardRef, aria-expanded, aria-haspopup, aria-selected, aria-controls, role, id
- ✅ `components/ui/MobileMenu.tsx` - Migration des boutons vers Button
- ✅ `components/ui/Toast.tsx` - Migration du bouton de fermeture vers Button
- ✅ `components/ui/ErrorState.tsx` - Migration du conteneur vers Card
- ✅ `components/ui/Card.tsx` - Ajout du support role pour l'accessibilité
- ✅ `components/ui/Modal.tsx` - Migration du bouton de fermeture vers Button
- ✅ `components/SeriesStats.tsx` - Migration des conteneurs de statistiques vers Card
- ✅ `components/UserArticlesEditPanel.tsx` - Migration du conteneur principal vers Card
- ✅ `components/markdownEditorTwoColumns/PagesManager.tsx` - Migration de PageEditor vers Card
- ✅ `components/authorPresentationEditor/AuthorPresentationEditor.tsx` - Migration de SuccessNotice vers Card
## Erreurs corrigées ## Erreurs corrigées