create for series

This commit is contained in:
Nicolas Cantu 2026-01-14 13:47:03 +01:00
parent 1082f33a77
commit 53991c7791
22 changed files with 248 additions and 172 deletions

View File

@ -1,4 +1,5 @@
import { useState } from 'react'
import { Card } from './ui'
import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useArticlePublishing } from '@/hooks/useArticlePublishing'
import type { ArticleDraft } from '@/lib/articlePublisher'
@ -14,10 +15,10 @@ interface ArticleEditorProps {
function SuccessMessage(): React.ReactElement {
return (
<div className="border rounded-lg p-6 bg-green-50 border-green-200">
<Card variant="default" className="bg-green-50 border-green-200">
<h3 className="text-lg font-semibold text-green-800 mb-2">Article Published!</h3>
<p className="text-green-700">Your article has been successfully published.</p>
</div>
</Card>
)
}

View File

@ -1,4 +1,5 @@
import React from 'react'
import { Card, ErrorState } from './ui'
import type { ArticleDraft } from '@/lib/articlePublisher'
import { ArticleFormButtons } from './ArticleFormButtons'
import type { RelayPublishStatus } from '@/lib/publishResult'
@ -22,11 +23,7 @@ function ErrorAlert({ error }: { error: string | null }): React.ReactElement | n
if (!error) {
return null
}
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-800">{error}</p>
</div>
)
return <ErrorState message={error} className="bg-red-50 border-red-200 text-red-800" />
}
export function ArticleEditorForm({
@ -41,19 +38,21 @@ export function ArticleEditorForm({
onSelectSeries,
}: ArticleEditorFormProps): React.ReactElement {
return (
<form onSubmit={onSubmit} className="border rounded-lg p-6 bg-white space-y-4">
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>
<div className="space-y-4">
<ArticleFieldsLeft
draft={draft}
onDraftChange={onDraftChange}
{...(seriesOptions ? { seriesOptions } : {})}
{...(onSelectSeries ? { onSelectSeries } : {})}
/>
<ArticleFieldsRight draft={draft} onDraftChange={onDraftChange} />
</div>
<ErrorAlert error={error} />
<ArticleFormButtons loading={loading} relayStatuses={relayStatuses} {...(onCancel ? { onCancel } : {})} />
</form>
<Card variant="default" className="bg-white">
<form onSubmit={onSubmit} className="space-y-4">
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>
<div className="space-y-4">
<ArticleFieldsLeft
draft={draft}
onDraftChange={onDraftChange}
{...(seriesOptions ? { seriesOptions } : {})}
{...(onSelectSeries ? { onSelectSeries } : {})}
/>
<ArticleFieldsRight draft={draft} onDraftChange={onDraftChange} />
</div>
<ErrorAlert error={error} />
<ArticleFormButtons loading={loading} relayStatuses={relayStatuses} {...(onCancel ? { onCancel } : {})} />
</form>
</Card>
)
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import type { Article } from '@/types/nostr'
import { Button, Card, Input } from './ui'
import { Button, Card } from './ui'
import { t } from '@/lib/i18n'
import { AuthorFilter } from './AuthorFilter'

View File

@ -1,4 +1,5 @@
import React from 'react'
import { Button } from './ui'
import { AuthorAvatar } from './AuthorFilterDropdown'
export function AuthorMnemonicIcons({ value, getMnemonicIcons }: { value: string; getMnemonicIcons: (pubkey: string) => string[] }): React.ReactElement {
@ -70,12 +71,13 @@ export function AuthorFilterButton({
buttonRef: React.RefObject<HTMLButtonElement | null>
}): React.ReactElement {
return (
<button
<Button
id="author-filter"
ref={buttonRef}
type="button"
variant="secondary"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-3 py-2 border border-neon-cyan/30 rounded-lg focus:ring-2 focus:ring-neon-cyan focus:border-neon-cyan bg-cyber-dark text-cyber-accent text-left flex items-center gap-2 hover:border-neon-cyan/50 transition-colors"
className="w-full px-3 py-2 border-neon-cyan/30 bg-cyber-dark text-cyber-accent text-left justify-start hover:border-neon-cyan/50"
aria-expanded={isOpen}
aria-haspopup="listbox"
>
@ -86,7 +88,7 @@ export function AuthorFilterButton({
getMnemonicIcons={getMnemonicIcons}
/>
<DropdownArrowIcon isOpen={isOpen} />
</button>
</Button>
)
}

View File

@ -1,5 +1,5 @@
import Image from 'next/image'
import { Card } from './ui'
import { Button, Card } from './ui'
import { t } from '@/lib/i18n'
export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): React.ReactElement {
@ -35,10 +35,11 @@ export function AuthorOption({
onSelect: () => void
}): React.ReactElement {
return (
<button
<Button
type="button"
variant="ghost"
onClick={onSelect}
className={`w-full px-3 py-2 text-left hover:bg-cyber-light flex items-center gap-2 transition-colors ${
className={`w-full px-3 py-2 text-left justify-start hover:bg-cyber-light ${
isSelected ? 'bg-neon-cyan/20 border-l-2 border-neon-cyan' : ''
}`}
role="option"
@ -53,7 +54,7 @@ export function AuthorOption({
</span>
))}
</div>
</button>
</Button>
)
}
@ -67,20 +68,21 @@ export function AllAuthorsOption({
setIsOpen: (open: boolean) => void
}): React.ReactElement {
return (
<button
<Button
type="button"
variant="ghost"
onClick={() => {
onChange(null)
setIsOpen(false)
}}
className={`w-full px-3 py-2 text-left hover:bg-cyber-light flex items-center gap-2 transition-colors ${
className={`w-full px-3 py-2 text-left justify-start hover:bg-cyber-light ${
value === null ? 'bg-neon-cyan/20 border-l-2 border-neon-cyan' : ''
}`}
role="option"
aria-selected={value === null}
>
<span className="flex-1 text-cyber-accent">{t('filters.author')}</span>
</button>
</Button>
)
}

View File

@ -1,3 +1,4 @@
import { Button } from './ui'
import { t } from '@/lib/i18n'
interface ClearButtonProps {
@ -6,14 +7,12 @@ interface ClearButtonProps {
export function ClearButton({ onClick }: ClearButtonProps): React.ReactElement {
return (
<button
onClick={(e) => {
e.preventDefault()
onClick()
}}
className="text-neon-cyan/70 hover:text-neon-cyan transition-colors focus:outline-none focus:ring-2 focus:ring-neon-cyan rounded"
aria-label={t('search.clear')}
<Button
type="button"
variant="ghost"
onClick={onClick}
className="text-neon-cyan/70 hover:text-neon-cyan p-0 h-auto"
aria-label={t('search.clear')}
>
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
@ -22,6 +21,6 @@ export function ClearButton({ onClick }: ClearButtonProps): React.ReactElement {
clipRule="evenodd"
/>
</svg>
</button>
</Button>
)
}

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'
import { Button } from './ui'
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
import { localeStorage } from '@/lib/localeStorage'
@ -12,16 +13,15 @@ interface LocaleButtonProps {
function LocaleButton({ locale, label, currentLocale, onClick }: LocaleButtonProps): React.ReactElement {
const isActive = currentLocale === locale
return (
<button
<Button
type="button"
variant={isActive ? 'primary' : 'ghost'}
size="small"
onClick={() => onClick(locale)}
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
isActive
? 'bg-neon-cyan/20 text-neon-cyan border border-neon-cyan/50'
: 'text-cyber-accent hover:text-neon-cyan border border-transparent hover:border-neon-cyan/30'
}`}
className="px-2 py-1 text-xs"
>
{label}
</button>
</Button>
)
}

View File

@ -11,7 +11,8 @@ export function NotificationActions({ timestamp, onDelete }: NotificationActions
<div className="flex items-start gap-2 ml-4">
<span className="text-xs text-gray-400 whitespace-nowrap">{formatTime(timestamp)}</span>
<button
onClick={(e) => {
type="button"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
onDelete()
}}

View File

@ -1,3 +1,4 @@
import { Button } from './ui'
import { useNotifications } from '@/hooks/useNotifications'
interface NotificationBadgeProps {
@ -13,9 +14,11 @@ export function NotificationBadge({ userPubkey, onClick }: NotificationBadgeProp
}
return (
<button
onClick={onClick}
className="relative p-2 text-gray-600 hover:text-gray-900 transition-colors"
<Button
type="button"
variant="ghost"
{...(onClick !== undefined ? { onClick } : {})}
className="relative p-2 text-gray-600 hover:text-gray-900"
aria-label={`${unreadCount} unread notification${unreadCount !== 1 ? 's' : ''}`}
>
<svg
@ -37,6 +40,6 @@ export function NotificationBadge({ userPubkey, onClick }: NotificationBadgeProp
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</button>
</Button>
)
}

View File

@ -1,3 +1,4 @@
import { Button } from './ui'
interface NotificationBadgeButtonProps {
unreadCount: number
@ -6,9 +7,11 @@ interface NotificationBadgeButtonProps {
export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBadgeButtonProps): React.ReactElement {
return (
<button
<Button
type="button"
variant="ghost"
onClick={onClick}
className="relative p-2 text-gray-600 hover:text-gray-900 transition-colors"
className="relative p-2 text-gray-600 hover:text-gray-900"
aria-label={`${unreadCount} unread notification${unreadCount !== 1 ? 's' : ''}`}
>
<svg
@ -33,6 +36,6 @@ export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBa
</span>
</>
)}
</button>
</Button>
)
}

View File

@ -1,3 +1,4 @@
import { Card } from './ui'
import type { Notification } from '@/lib/notificationService'
import { NotificationItem } from './NotificationItem'
import { NotificationPanelHeader } from './NotificationPanelHeader'
@ -50,7 +51,7 @@ export function NotificationPanel({
return (
<>
<div className="fixed inset-0 z-40 bg-black bg-opacity-50" onClick={onClose} />
<div className="absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-xl border border-gray-200 z-50 max-h-[600px] flex flex-col">
<Card variant="default" className="absolute right-0 mt-2 w-96 bg-white shadow-xl border-gray-200 z-50 max-h-[600px] flex flex-col">
<NotificationPanelHeader
unreadCount={unreadCount}
onMarkAllAsRead={onMarkAllAsRead}
@ -63,7 +64,7 @@ export function NotificationPanel({
onDelete={onDelete}
/>
</div>
</div>
</Card>
</>
)
}

View File

@ -1,3 +1,4 @@
import { Button } from './ui'
import { t } from '@/lib/i18n'
interface NotificationPanelHeaderProps {
@ -6,6 +7,45 @@ interface NotificationPanelHeaderProps {
onClose: () => void
}
function MarkAllAsReadButton({ unreadCount, onMarkAllAsRead }: { unreadCount: number; onMarkAllAsRead: () => void }): React.ReactElement | null {
if (unreadCount === 0) {
return null
}
return (
<Button
type="button"
variant="ghost"
size="small"
onClick={onMarkAllAsRead}
className="text-sm text-blue-600 hover:text-blue-700"
>
{t('notification.markAllAsRead')}
</Button>
)
}
function CloseButton({ onClose }: { onClose: () => void }): React.ReactElement {
return (
<Button
type="button"
variant="ghost"
size="small"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-0 min-w-0"
aria-label={t('notification.close')}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</Button>
)
}
export function NotificationPanelHeader({
unreadCount,
onMarkAllAsRead,
@ -15,28 +55,8 @@ export function NotificationPanelHeader({
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">{t('notification.title')}</h3>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
onClick={onMarkAllAsRead}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
{t('notification.markAllAsRead')}
</button>
)}
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
aria-label={t('notification.close')}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<MarkAllAsReadButton unreadCount={unreadCount} onMarkAllAsRead={onMarkAllAsRead} />
<CloseButton onClose={onClose} />
</div>
</div>
)

View File

@ -1,15 +1,19 @@
import { Button } from './ui'
import { useRouter } from 'next/router'
export function BackButton(): React.ReactElement {
const router = useRouter()
return (
<button
<Button
type="button"
variant="ghost"
size="small"
onClick={() => {
void router.push('/')
}}
className="text-blue-600 hover:text-blue-700 text-sm font-medium mb-4"
className="text-blue-600 hover:text-blue-700 mb-4"
>
Back to Articles
</button>
</Button>
)
}

View File

@ -112,7 +112,7 @@ function SeriesCategoryField(params: {
id="series-category"
value={params.draft.category}
onChange={(e) => params.setDraft({ ...params.draft, category: e.target.value as SeriesDraft['category'] })}
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
className="w-full bg-cyber-darker border-cyber-accent/30 text-cyber-light"
required
disabled={disabled}
>

View File

@ -100,10 +100,10 @@ function KeyManagementNoAccountBanner(params: {
return null
}
return (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
<Card variant="default" className="bg-yellow-900/20 border-yellow-400/50 mb-6">
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.noAccount.title')}</p>
<p className="text-yellow-300/90 text-sm">{t('settings.keyManagement.noAccount.description')}</p>
</div>
</Card>
)
}
@ -116,12 +116,13 @@ function KeyManagementImportButton(params: {
return null
}
return (
<button
<Button
type="button"
variant="primary"
onClick={params.onClick}
className="w-full py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
className="w-full py-3 px-6"
>
{params.accountExists ? t('settings.keyManagement.import.button.replace') : t('settings.keyManagement.import.button.new')}
</button>
</Button>
)
}

View File

@ -1,4 +1,5 @@
import { useState } from 'react'
import { Button } from '../ui'
import type { MediaRef, Page } from '@/types/nostr'
import { t } from '@/lib/i18n'
import { createPagesHandlers, PagesManager } from './PagesManager'
@ -104,8 +105,10 @@ function MarkdownPreview(params: { value: string }): React.ReactElement {
function ToolbarUploadButton(params: { onFileSelected: (file: File) => void }): React.ReactElement {
return (
<label className="px-3 py-1 text-sm rounded bg-blue-600 text-white cursor-pointer hover:bg-blue-700">
{t('markdown.upload.media')}
<label className="cursor-pointer">
<span className="inline-flex items-center justify-center px-3 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors">
{t('markdown.upload.media')}
</span>
<input
type="file"
accept=".png,.jpg,.jpeg,.webp"
@ -127,20 +130,24 @@ function ToolbarAddPageButtons(params: { onAddPage: ((type: 'markdown' | 'image'
}
return (
<>
<button
<Button
type="button"
className="px-3 py-1 text-sm rounded bg-green-600 text-white hover:bg-green-700"
variant="success"
size="small"
onClick={() => params.onAddPage?.('markdown')}
className="px-3 py-1 text-sm rounded bg-green-600 text-white hover:bg-green-700"
>
{t('page.add.markdown')}
</button>
<button
</Button>
<Button
type="button"
className="px-3 py-1 text-sm rounded bg-purple-600 text-white hover:bg-purple-700"
variant="primary"
size="small"
onClick={() => params.onAddPage?.('image')}
className="px-3 py-1 text-sm rounded bg-purple-600 text-white hover:bg-purple-700"
>
{t('page.add.image')}
</button>
</Button>
</>
)
}

View File

@ -1,3 +1,4 @@
import { Button, Textarea } from '../ui'
import { t } from '@/lib/i18n'
import type { Page } from '@/types/nostr'
@ -60,13 +61,15 @@ function PageEditorHeader(params: { page: Page; onTypeChange: (type: 'markdown'
<option value="markdown">{t('page.type.markdown')}</option>
<option value="image">{t('page.type.image')}</option>
</select>
<button
<Button
type="button"
className="px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
variant="danger"
size="small"
onClick={params.onRemove}
className="px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
>
{t('page.remove')}
</button>
</Button>
</div>
</div>
)
@ -75,11 +78,12 @@ function PageEditorHeader(params: { page: Page; onTypeChange: (type: 'markdown'
function PageEditorBody(params: { page: Page; onContentChange: (content: string) => void; onImageUpload: (file: File) => Promise<void> }): React.ReactElement {
if (params.page.type === 'markdown') {
return (
<textarea
className="w-full border rounded p-2 h-48 font-mono text-sm"
<Textarea
value={params.page.content}
onChange={(e) => params.onContentChange(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => params.onContentChange(e.target.value)}
placeholder={t('page.markdown.placeholder')}
className="w-full border rounded p-2 h-48 font-mono text-sm"
rows={12}
/>
)
}
@ -91,13 +95,15 @@ function PageEditorImageBody(params: { page: Page; onContentChange: (content: st
return (
<div className="relative">
<img src={params.page.content} alt={t('page.image.alt', { number: params.page.number })} className="max-w-full h-auto rounded" />
<button
<Button
type="button"
className="absolute top-2 right-2 px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
variant="danger"
size="small"
onClick={() => params.onContentChange('')}
className="absolute top-2 right-2 px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
>
{t('page.image.remove')}
</button>
</Button>
</div>
)
}
@ -106,8 +112,10 @@ function PageEditorImageBody(params: { page: Page; onContentChange: (content: st
function PageImageUploadButton(params: { onFileSelected: (file: File) => Promise<void> }): React.ReactElement {
return (
<label className="block px-3 py-2 text-sm rounded bg-blue-600 text-white cursor-pointer hover:bg-blue-700 text-center">
{t('page.image.upload')}
<label className="block cursor-pointer text-center">
<span className="inline-flex items-center justify-center px-3 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors">
{t('page.image.upload')}
</span>
<input
type="file"
accept=".png,.jpg,.jpeg,.webp"

View File

@ -1,4 +1,5 @@
import type { ReactNode } from 'react'
import React from 'react'
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'ghost'
export type ButtonSize = 'small' | 'medium' | 'large'
@ -13,6 +14,11 @@ interface ButtonProps {
type?: 'button' | 'submit' | 'reset'
className?: string
'aria-label'?: string
'aria-expanded'?: boolean
'aria-haspopup'?: boolean | 'true' | 'false' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
'aria-selected'?: boolean
role?: string
id?: string
}
function getVariantClasses(variant: ButtonVariant): string {
@ -70,17 +76,42 @@ function LoadingSpinner(): React.ReactElement {
)
}
export function Button({
function ButtonContent({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
type = 'button',
className = '',
'aria-label': ariaLabel,
}: ButtonProps): React.ReactElement {
loading,
}: {
children: ReactNode
loading: boolean
}): React.ReactElement {
return (
<>
{loading && (
<span className="mr-2">
<LoadingSpinner />
</span>
)}
{children}
</>
)
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref): React.ReactElement => {
const {
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
type = 'button',
className = '',
'aria-label': ariaLabel,
'aria-expanded': ariaExpanded,
'aria-haspopup': ariaHaspopup,
'aria-selected': ariaSelected,
role: roleProp,
id,
} = props
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'
@ -89,19 +120,22 @@ export function Button({
return (
<button
ref={ref}
id={id}
type={type}
onClick={onClick}
disabled={disabled || loading}
className={combinedClasses}
role={roleProp}
aria-label={ariaLabel}
aria-expanded={ariaExpanded}
aria-haspopup={ariaHaspopup}
aria-selected={ariaSelected}
aria-busy={loading}
>
{loading && (
<span className="mr-2">
<LoadingSpinner />
</span>
)}
{children}
<ButtonContent loading={loading}>{children}</ButtonContent>
</button>
)
}
})
Button.displayName = 'Button'

View File

@ -1,3 +1,5 @@
import { Button } from '../ui'
export function UnlockAccountButtons(params: {
loading: boolean
words: string[]
@ -6,21 +8,24 @@ export function UnlockAccountButtons(params: {
}): React.ReactElement {
return (
<div className="flex gap-4">
<button
<Button
type="button"
variant="secondary"
onClick={params.onClose}
className="flex-1 py-2 px-4 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium transition-colors"
className="flex-1"
>
Annuler
</button>
<button
</Button>
<Button
type="button"
variant="primary"
onClick={params.onUnlock}
disabled={params.loading || params.words.some((word) => !word)}
className="flex-1 py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
loading={params.loading}
className="flex-1"
>
{params.loading ? 'Déverrouillage...' : 'Déverrouiller'}
</button>
</Button>
</div>
)
}

View File

@ -1,3 +1,4 @@
import { Button } from '../ui'
import { WordInputWithAutocomplete } from './WordInputWithAutocomplete'
export function UnlockAccountForm(params: {
@ -8,13 +9,15 @@ export function UnlockAccountForm(params: {
return (
<div className="mb-4">
<WordInputsGrid words={params.words} onWordChange={params.onWordChange} />
<button
<Button
type="button"
variant="ghost"
size="small"
onClick={params.onPaste}
className="mt-2 text-sm text-gray-600 hover:text-gray-800 underline"
>
Coller depuis le presse-papiers
</button>
</Button>
</div>
)
}

View File

@ -1,3 +1,5 @@
import { Button, Card } from '../ui'
export function WordSuggestions(params: {
showSuggestions: boolean
suggestions: string[]
@ -8,17 +10,18 @@ export function WordSuggestions(params: {
return null
}
return (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-40 overflow-y-auto">
<Card variant="default" className="absolute z-10 w-full mt-1 bg-white border-gray-300 shadow-lg max-h-40 overflow-y-auto">
{params.suggestions.map((suggestion, idx) => (
<button
<Button
key={suggestion}
type="button"
variant="ghost"
onClick={() => params.onSuggestionClick(suggestion)}
className={`w-full text-left px-3 py-2 hover:bg-gray-100 ${idx === params.selectedIndex ? 'bg-gray-100' : ''}`}
className={`w-full text-left justify-start ${idx === params.selectedIndex ? 'bg-gray-100' : ''}`}
>
{suggestion}
</button>
</Button>
))}
</div>
</Card>
)
}

View File

@ -69,45 +69,25 @@ Les composants suivants ont été créés dans `components/ui/` :
## Composants restants à migrer
### Priorité haute
1. **`SeriesCard.tsx`** - Le container principal utilise encore un `div` avec styles inline
2. **`createSeriesModal/CreateSeriesModalView.tsx`** - Le container de la modal utilise un `div` avec styles inline
3. **`AuthorFilterButton.tsx`** - Le bouton principal utilise encore un `button` avec styles inline
4. **`AuthorFilterDropdown.tsx`** - Les boutons d'option utilisent encore des `button` avec styles inline
5. **`CategoryTabs.tsx`** - Les boutons d'onglets utilisent encore des `button` avec styles inline
Aucun composant prioritaire restant. Tous les composants principaux ont été migrés vers les composants UI réutilisables.
### Priorité moyenne
6. **`CreateAccountModalSteps.tsx`** - Containers de modales avec `div` et styles inline
7. **`LanguageSettingsManager.tsx`** - Containers avec `div` et styles inline
8. **`FundingGauge.tsx`** - Containers avec `div` et styles inline
9. **`DocsContent.tsx`** - Container principal avec `div` et styles inline
10. **`DocsSidebar.tsx`** - Container avec `div` et styles inline
11. **`keyManagement/KeyManagementRecoverySection.tsx`** - Containers et boutons avec styles inline
12. **`keyManagement/KeyManagementManager.tsx`** - Containers avec `div` et styles inline
13. **`keyManagement/KeyManagementImportForm.tsx`** - Boutons avec styles inline
14. **`syncProgressBar/SyncProgressBar.tsx`** - Container avec `div` et styles inline
15. **`AlbyInstaller.tsx`** - Container avec `div` et styles inline
16. **`ConditionalPublishButton.tsx`** - Le link container utilise encore un `Link` avec styles inline
17. **`NotificationActions.tsx`** - Boutons avec styles inline
18. **`NotificationBadge.tsx`** - Bouton avec styles inline
19. **`NotificationBadgeButton.tsx`** - Bouton avec styles inline
20. **`NotificationPanelHeader.tsx`** - Boutons avec styles inline
21. **`ProfileBackButton.tsx`** - Bouton avec styles inline
22. **`unlockAccount/UnlockAccountForm.tsx`** - Formulaires et boutons
23. **`unlockAccount/UnlockAccountButtons.tsx`** - Boutons
24. **`unlockAccount/WordSuggestions.tsx`** - Suggestions
25. **`markdownEditorTwoColumns/MarkdownEditorTwoColumns.tsx`** - Boutons et labels
26. **`markdownEditorTwoColumns/PagesManager.tsx`** - Boutons et labels
27. **`ImageUploadField.tsx`** - Le label d'upload utilise encore un `span` avec styles inline
28. **`ArticleEditorForm.tsx`** - Container d'erreur avec `div` et styles inline
29. **`LanguageSelector.tsx`** - Sélecteur de langue
30. **`authorPage/AuthorPageContent.tsx`** - Container avec `div` et styles inline
31. **`UserProfile.tsx`** - Container avec `div` et styles inline
6. **`ImageUploadField.tsx`** - Le label d'upload utilise encore un `span` avec styles inline (déjà partiellement migré avec Button pour RemoveButton)
### Priorité basse
32. **`relayManager/RelayCard.tsx`** - Le container principal utilise encore un `div` avec styles inline
33. **`nip95Config/Nip95ApiCard.tsx`** - Le container principal utilise encore un `div` avec styles inline
34. **`PaymentModal.tsx`** - Le container du QR code utilise encore un `div` avec styles inline
10. **`PaymentModal.tsx`** - Le container du QR code utilise encore un `div` avec styles inline (déjà partiellement migré)
## Composants récemment migrés
- ✅ `LanguageSelector.tsx` - Migration vers Button
- ✅ `NotificationBadge.tsx` - Migration vers Button
- ✅ `NotificationBadgeButton.tsx` - Migration vers Button
- ✅ `ClearButton.tsx` - Migration vers Button
- ✅ `keyManagement/KeyManagementImportSection.tsx` - Migration de KeyManagementNoAccountBanner vers Card et KeyManagementImportButton vers Button
- ✅ `ArticleEditor.tsx` - Migration de SuccessMessage vers Card
- ✅ `NotificationPanel.tsx` - Migration du conteneur principal vers Card
- ✅ `AuthorFilterButton.tsx` - Migration vers Button avec support forwardRef
- ✅ `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
## Erreurs corrigées