create for series
This commit is contained in:
parent
1082f33a77
commit
53991c7791
@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { Card } from './ui'
|
||||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||||
import { useArticlePublishing } from '@/hooks/useArticlePublishing'
|
import { useArticlePublishing } from '@/hooks/useArticlePublishing'
|
||||||
import type { ArticleDraft } from '@/lib/articlePublisher'
|
import type { ArticleDraft } from '@/lib/articlePublisher'
|
||||||
@ -14,10 +15,10 @@ interface ArticleEditorProps {
|
|||||||
|
|
||||||
function SuccessMessage(): React.ReactElement {
|
function SuccessMessage(): React.ReactElement {
|
||||||
return (
|
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>
|
<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>
|
<p className="text-green-700">Your article has been successfully published.</p>
|
||||||
</div>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Card, ErrorState } from './ui'
|
||||||
import type { ArticleDraft } from '@/lib/articlePublisher'
|
import type { ArticleDraft } from '@/lib/articlePublisher'
|
||||||
import { ArticleFormButtons } from './ArticleFormButtons'
|
import { ArticleFormButtons } from './ArticleFormButtons'
|
||||||
import type { RelayPublishStatus } from '@/lib/publishResult'
|
import type { RelayPublishStatus } from '@/lib/publishResult'
|
||||||
@ -22,11 +23,7 @@ function ErrorAlert({ error }: { error: string | null }): React.ReactElement | n
|
|||||||
if (!error) {
|
if (!error) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return <ErrorState message={error} className="bg-red-50 border-red-200 text-red-800" />
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
|
||||||
<p className="text-sm text-red-800">{error}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ArticleEditorForm({
|
export function ArticleEditorForm({
|
||||||
@ -41,19 +38,21 @@ export function ArticleEditorForm({
|
|||||||
onSelectSeries,
|
onSelectSeries,
|
||||||
}: ArticleEditorFormProps): React.ReactElement {
|
}: ArticleEditorFormProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="border rounded-lg p-6 bg-white space-y-4">
|
<Card variant="default" className="bg-white">
|
||||||
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>
|
<form onSubmit={onSubmit} className="space-y-4">
|
||||||
<div className="space-y-4">
|
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>
|
||||||
<ArticleFieldsLeft
|
<div className="space-y-4">
|
||||||
draft={draft}
|
<ArticleFieldsLeft
|
||||||
onDraftChange={onDraftChange}
|
draft={draft}
|
||||||
{...(seriesOptions ? { seriesOptions } : {})}
|
onDraftChange={onDraftChange}
|
||||||
{...(onSelectSeries ? { onSelectSeries } : {})}
|
{...(seriesOptions ? { seriesOptions } : {})}
|
||||||
/>
|
{...(onSelectSeries ? { onSelectSeries } : {})}
|
||||||
<ArticleFieldsRight draft={draft} onDraftChange={onDraftChange} />
|
/>
|
||||||
</div>
|
<ArticleFieldsRight draft={draft} onDraftChange={onDraftChange} />
|
||||||
<ErrorAlert error={error} />
|
</div>
|
||||||
<ArticleFormButtons loading={loading} relayStatuses={relayStatuses} {...(onCancel ? { onCancel } : {})} />
|
<ErrorAlert error={error} />
|
||||||
</form>
|
<ArticleFormButtons loading={loading} relayStatuses={relayStatuses} {...(onCancel ? { onCancel } : {})} />
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { Article } from '@/types/nostr'
|
import type { Article } from '@/types/nostr'
|
||||||
import { Button, Card, Input } from './ui'
|
import { Button, Card } from './ui'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import { AuthorFilter } from './AuthorFilter'
|
import { AuthorFilter } from './AuthorFilter'
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Button } from './ui'
|
||||||
import { AuthorAvatar } from './AuthorFilterDropdown'
|
import { AuthorAvatar } from './AuthorFilterDropdown'
|
||||||
|
|
||||||
export function AuthorMnemonicIcons({ value, getMnemonicIcons }: { value: string; getMnemonicIcons: (pubkey: string) => string[] }): React.ReactElement {
|
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>
|
buttonRef: React.RefObject<HTMLButtonElement | null>
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
id="author-filter"
|
id="author-filter"
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
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-expanded={isOpen}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
>
|
>
|
||||||
@ -86,7 +88,7 @@ export function AuthorFilterButton({
|
|||||||
getMnemonicIcons={getMnemonicIcons}
|
getMnemonicIcons={getMnemonicIcons}
|
||||||
/>
|
/>
|
||||||
<DropdownArrowIcon isOpen={isOpen} />
|
<DropdownArrowIcon isOpen={isOpen} />
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Card } from './ui'
|
import { Button, Card } from './ui'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): React.ReactElement {
|
export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): React.ReactElement {
|
||||||
@ -35,10 +35,11 @@ export function AuthorOption({
|
|||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={onSelect}
|
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' : ''
|
isSelected ? 'bg-neon-cyan/20 border-l-2 border-neon-cyan' : ''
|
||||||
}`}
|
}`}
|
||||||
role="option"
|
role="option"
|
||||||
@ -53,7 +54,7 @@ export function AuthorOption({
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,20 +68,21 @@ export function AllAuthorsOption({
|
|||||||
setIsOpen: (open: boolean) => void
|
setIsOpen: (open: boolean) => void
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(null)
|
onChange(null)
|
||||||
setIsOpen(false)
|
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' : ''
|
value === null ? 'bg-neon-cyan/20 border-l-2 border-neon-cyan' : ''
|
||||||
}`}
|
}`}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={value === null}
|
aria-selected={value === null}
|
||||||
>
|
>
|
||||||
<span className="flex-1 text-cyber-accent">{t('filters.author')}</span>
|
<span className="flex-1 text-cyber-accent">{t('filters.author')}</span>
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from './ui'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface ClearButtonProps {
|
interface ClearButtonProps {
|
||||||
@ -6,14 +7,12 @@ interface ClearButtonProps {
|
|||||||
|
|
||||||
export function ClearButton({ onClick }: ClearButtonProps): React.ReactElement {
|
export function ClearButton({ onClick }: ClearButtonProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<button
|
<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')}
|
|
||||||
type="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">
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
@ -22,6 +21,6 @@ export function ClearButton({ onClick }: ClearButtonProps): React.ReactElement {
|
|||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Button } from './ui'
|
||||||
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
|
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
|
||||||
import { localeStorage } from '@/lib/localeStorage'
|
import { localeStorage } from '@/lib/localeStorage'
|
||||||
|
|
||||||
@ -12,16 +13,15 @@ interface LocaleButtonProps {
|
|||||||
function LocaleButton({ locale, label, currentLocale, onClick }: LocaleButtonProps): React.ReactElement {
|
function LocaleButton({ locale, label, currentLocale, onClick }: LocaleButtonProps): React.ReactElement {
|
||||||
const isActive = currentLocale === locale
|
const isActive = currentLocale === locale
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={isActive ? 'primary' : 'ghost'}
|
||||||
|
size="small"
|
||||||
onClick={() => onClick(locale)}
|
onClick={() => onClick(locale)}
|
||||||
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
|
className="px-2 py-1 text-xs"
|
||||||
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'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,8 @@ export function NotificationActions({ timestamp, onDelete }: NotificationActions
|
|||||||
<div className="flex items-start gap-2 ml-4">
|
<div className="flex items-start gap-2 ml-4">
|
||||||
<span className="text-xs text-gray-400 whitespace-nowrap">{formatTime(timestamp)}</span>
|
<span className="text-xs text-gray-400 whitespace-nowrap">{formatTime(timestamp)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
type="button"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onDelete()
|
onDelete()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from './ui'
|
||||||
import { useNotifications } from '@/hooks/useNotifications'
|
import { useNotifications } from '@/hooks/useNotifications'
|
||||||
|
|
||||||
interface NotificationBadgeProps {
|
interface NotificationBadgeProps {
|
||||||
@ -13,9 +14,11 @@ export function NotificationBadge({ userPubkey, onClick }: NotificationBadgeProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
onClick={onClick}
|
type="button"
|
||||||
className="relative p-2 text-gray-600 hover:text-gray-900 transition-colors"
|
variant="ghost"
|
||||||
|
{...(onClick !== undefined ? { onClick } : {})}
|
||||||
|
className="relative p-2 text-gray-600 hover:text-gray-900"
|
||||||
aria-label={`${unreadCount} unread notification${unreadCount !== 1 ? 's' : ''}`}
|
aria-label={`${unreadCount} unread notification${unreadCount !== 1 ? 's' : ''}`}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -37,6 +40,6 @@ export function NotificationBadge({ userPubkey, onClick }: NotificationBadgeProp
|
|||||||
{unreadCount > 99 ? '99+' : unreadCount}
|
{unreadCount > 99 ? '99+' : unreadCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from './ui'
|
||||||
|
|
||||||
interface NotificationBadgeButtonProps {
|
interface NotificationBadgeButtonProps {
|
||||||
unreadCount: number
|
unreadCount: number
|
||||||
@ -6,9 +7,11 @@ interface NotificationBadgeButtonProps {
|
|||||||
|
|
||||||
export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBadgeButtonProps): React.ReactElement {
|
export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBadgeButtonProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={onClick}
|
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' : ''}`}
|
aria-label={`${unreadCount} unread notification${unreadCount !== 1 ? 's' : ''}`}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -33,6 +36,6 @@ export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBa
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Card } from './ui'
|
||||||
import type { Notification } from '@/lib/notificationService'
|
import type { Notification } from '@/lib/notificationService'
|
||||||
import { NotificationItem } from './NotificationItem'
|
import { NotificationItem } from './NotificationItem'
|
||||||
import { NotificationPanelHeader } from './NotificationPanelHeader'
|
import { NotificationPanelHeader } from './NotificationPanelHeader'
|
||||||
@ -50,7 +51,7 @@ export function NotificationPanel({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40 bg-black bg-opacity-50" onClick={onClose} />
|
<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
|
<NotificationPanelHeader
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
onMarkAllAsRead={onMarkAllAsRead}
|
onMarkAllAsRead={onMarkAllAsRead}
|
||||||
@ -63,7 +64,7 @@ export function NotificationPanel({
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from './ui'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface NotificationPanelHeaderProps {
|
interface NotificationPanelHeaderProps {
|
||||||
@ -6,6 +7,45 @@ interface NotificationPanelHeaderProps {
|
|||||||
onClose: () => void
|
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({
|
export function NotificationPanelHeader({
|
||||||
unreadCount,
|
unreadCount,
|
||||||
onMarkAllAsRead,
|
onMarkAllAsRead,
|
||||||
@ -15,28 +55,8 @@ export function NotificationPanelHeader({
|
|||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
<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>
|
<h3 className="text-lg font-semibold text-gray-900">{t('notification.title')}</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{unreadCount > 0 && (
|
<MarkAllAsReadButton unreadCount={unreadCount} onMarkAllAsRead={onMarkAllAsRead} />
|
||||||
<button
|
<CloseButton onClose={onClose} />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
|
import { Button } from './ui'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
export function BackButton(): React.ReactElement {
|
export function BackButton(): React.ReactElement {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void router.push('/')
|
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
|
← Back to Articles
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,7 @@ function SeriesCategoryField(params: {
|
|||||||
id="series-category"
|
id="series-category"
|
||||||
value={params.draft.category}
|
value={params.draft.category}
|
||||||
onChange={(e) => params.setDraft({ ...params.draft, category: e.target.value as SeriesDraft['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
|
required
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -100,10 +100,10 @@ function KeyManagementNoAccountBanner(params: {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
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-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>
|
<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 null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
onClick={params.onClick}
|
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')}
|
{params.accountExists ? t('settings.keyManagement.import.button.replace') : t('settings.keyManagement.import.button.new')}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { Button } from '../ui'
|
||||||
import type { MediaRef, Page } from '@/types/nostr'
|
import type { MediaRef, Page } from '@/types/nostr'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import { createPagesHandlers, PagesManager } from './PagesManager'
|
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 {
|
function ToolbarUploadButton(params: { onFileSelected: (file: File) => void }): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<label className="px-3 py-1 text-sm rounded bg-blue-600 text-white cursor-pointer hover:bg-blue-700">
|
<label className="cursor-pointer">
|
||||||
{t('markdown.upload.media')}
|
<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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".png,.jpg,.jpeg,.webp"
|
accept=".png,.jpg,.jpeg,.webp"
|
||||||
@ -127,20 +130,24 @@ function ToolbarAddPageButtons(params: { onAddPage: ((type: 'markdown' | 'image'
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="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')}
|
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')}
|
{t('page.add.markdown')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="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')}
|
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')}
|
{t('page.add.image')}
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button, 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'
|
||||||
|
|
||||||
@ -60,13 +61,15 @@ function PageEditorHeader(params: { page: Page; onTypeChange: (type: 'markdown'
|
|||||||
<option value="markdown">{t('page.type.markdown')}</option>
|
<option value="markdown">{t('page.type.markdown')}</option>
|
||||||
<option value="image">{t('page.type.image')}</option>
|
<option value="image">{t('page.type.image')}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<Button
|
||||||
type="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}
|
onClick={params.onRemove}
|
||||||
|
className="px-2 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
|
||||||
>
|
>
|
||||||
{t('page.remove')}
|
{t('page.remove')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
function PageEditorBody(params: { page: Page; onContentChange: (content: string) => void; onImageUpload: (file: File) => Promise<void> }): React.ReactElement {
|
||||||
if (params.page.type === 'markdown') {
|
if (params.page.type === 'markdown') {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<Textarea
|
||||||
className="w-full border rounded p-2 h-48 font-mono text-sm"
|
|
||||||
value={params.page.content}
|
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')}
|
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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img src={params.page.content} alt={t('page.image.alt', { number: params.page.number })} className="max-w-full h-auto rounded" />
|
<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"
|
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('')}
|
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')}
|
{t('page.image.remove')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -106,8 +112,10 @@ function PageEditorImageBody(params: { page: Page; onContentChange: (content: st
|
|||||||
|
|
||||||
function PageImageUploadButton(params: { onFileSelected: (file: File) => Promise<void> }): React.ReactElement {
|
function PageImageUploadButton(params: { onFileSelected: (file: File) => Promise<void> }): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<label className="block px-3 py-2 text-sm rounded bg-blue-600 text-white cursor-pointer hover:bg-blue-700 text-center">
|
<label className="block cursor-pointer text-center">
|
||||||
{t('page.image.upload')}
|
<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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".png,.jpg,.jpeg,.webp"
|
accept=".png,.jpg,.jpeg,.webp"
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'ghost'
|
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'ghost'
|
||||||
export type ButtonSize = 'small' | 'medium' | 'large'
|
export type ButtonSize = 'small' | 'medium' | 'large'
|
||||||
@ -13,6 +14,11 @@ interface ButtonProps {
|
|||||||
type?: 'button' | 'submit' | 'reset'
|
type?: 'button' | 'submit' | 'reset'
|
||||||
className?: string
|
className?: string
|
||||||
'aria-label'?: 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 {
|
function getVariantClasses(variant: ButtonVariant): string {
|
||||||
@ -70,17 +76,42 @@ function LoadingSpinner(): React.ReactElement {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button({
|
function ButtonContent({
|
||||||
children,
|
children,
|
||||||
variant = 'primary',
|
loading,
|
||||||
size = 'medium',
|
}: {
|
||||||
disabled = false,
|
children: ReactNode
|
||||||
loading = false,
|
loading: boolean
|
||||||
onClick,
|
}): React.ReactElement {
|
||||||
type = 'button',
|
return (
|
||||||
className = '',
|
<>
|
||||||
'aria-label': ariaLabel,
|
{loading && (
|
||||||
}: ButtonProps): React.ReactElement {
|
<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 variantClasses = getVariantClasses(variant)
|
||||||
const sizeClasses = getSizeClasses(size)
|
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 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 (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
className={combinedClasses}
|
className={combinedClasses}
|
||||||
|
role={roleProp}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
aria-expanded={ariaExpanded}
|
||||||
|
aria-haspopup={ariaHaspopup}
|
||||||
|
aria-selected={ariaSelected}
|
||||||
aria-busy={loading}
|
aria-busy={loading}
|
||||||
>
|
>
|
||||||
{loading && (
|
<ButtonContent loading={loading}>{children}</ButtonContent>
|
||||||
<span className="mr-2">
|
|
||||||
<LoadingSpinner />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Button } from '../ui'
|
||||||
|
|
||||||
export function UnlockAccountButtons(params: {
|
export function UnlockAccountButtons(params: {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
words: string[]
|
words: string[]
|
||||||
@ -6,21 +8,24 @@ export function UnlockAccountButtons(params: {
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
onClick={params.onClose}
|
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
|
Annuler
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
onClick={params.onUnlock}
|
onClick={params.onUnlock}
|
||||||
disabled={params.loading || params.words.some((word) => !word)}
|
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'}
|
{params.loading ? 'Déverrouillage...' : 'Déverrouiller'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Button } from '../ui'
|
||||||
import { WordInputWithAutocomplete } from './WordInputWithAutocomplete'
|
import { WordInputWithAutocomplete } from './WordInputWithAutocomplete'
|
||||||
|
|
||||||
export function UnlockAccountForm(params: {
|
export function UnlockAccountForm(params: {
|
||||||
@ -8,13 +9,15 @@ export function UnlockAccountForm(params: {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<WordInputsGrid words={params.words} onWordChange={params.onWordChange} />
|
<WordInputsGrid words={params.words} onWordChange={params.onWordChange} />
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
onClick={params.onPaste}
|
onClick={params.onPaste}
|
||||||
className="mt-2 text-sm text-gray-600 hover:text-gray-800 underline"
|
className="mt-2 text-sm text-gray-600 hover:text-gray-800 underline"
|
||||||
>
|
>
|
||||||
Coller depuis le presse-papiers
|
Coller depuis le presse-papiers
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Card } from '../ui'
|
||||||
|
|
||||||
export function WordSuggestions(params: {
|
export function WordSuggestions(params: {
|
||||||
showSuggestions: boolean
|
showSuggestions: boolean
|
||||||
suggestions: string[]
|
suggestions: string[]
|
||||||
@ -8,17 +10,18 @@ export function WordSuggestions(params: {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
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) => (
|
{params.suggestions.map((suggestion, idx) => (
|
||||||
<button
|
<Button
|
||||||
key={suggestion}
|
key={suggestion}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => params.onSuggestionClick(suggestion)}
|
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}
|
{suggestion}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,45 +69,25 @@ Les composants suivants ont été créés dans `components/ui/` :
|
|||||||
|
|
||||||
## Composants restants à migrer
|
## Composants restants à migrer
|
||||||
|
|
||||||
### Priorité haute
|
Aucun composant prioritaire restant. Tous les composants principaux ont été migrés vers les composants UI réutilisables.
|
||||||
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
|
|
||||||
|
|
||||||
### Priorité moyenne
|
### Priorité moyenne
|
||||||
6. **`CreateAccountModalSteps.tsx`** - Containers de modales 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)
|
||||||
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
|
|
||||||
|
|
||||||
### Priorité basse
|
### Priorité basse
|
||||||
32. **`relayManager/RelayCard.tsx`** - Le container principal 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é)
|
||||||
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
|
## 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
|
## Erreurs corrigées
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user