feat: Added CSS button utilities (.btn, .btn-icon, .btn-ghost, .btn-dan…
- "src/frontend/src/styles/global.css" - "src/frontend/src/components/Skeleton.tsx" - "src/frontend/src/components/Modal.tsx" - "src/frontend/src/components/Sidebar.tsx" - "src/frontend/src/pages/Queue.tsx" - "src/frontend/src/pages/Library.tsx" - "src/frontend/src/pages/Activity.tsx" - "src/frontend/src/pages/System.tsx" GSD-Task: S02/T02
This commit is contained in:
parent
a0906f3cdb
commit
3355326526
8 changed files with 205 additions and 204 deletions
|
|
@ -112,6 +112,7 @@ export function Modal({ title, open, onClose, children, width = 480 }: ModalProp
|
||||||
border: '1px solid var(--border-light)',
|
border: '1px solid var(--border-light)',
|
||||||
borderRadius: 'var(--radius-lg)',
|
borderRadius: 'var(--radius-lg)',
|
||||||
boxShadow: 'var(--shadow-lg)',
|
boxShadow: 'var(--shadow-lg)',
|
||||||
|
animation: 'modal-enter 200ms ease-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -137,24 +138,7 @@ export function Modal({ title, open, onClose, children, width = 480 }: ModalProp
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
style={{
|
className="btn-icon"
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 'var(--radius-sm)',
|
|
||||||
color: 'var(--text-muted)',
|
|
||||||
transition: 'color var(--transition-fast), background-color var(--transition-fast)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-primary)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-muted)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'transparent';
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -78,26 +78,11 @@ export function Sidebar() {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
className="btn-icon"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: 'var(--space-1)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
color: 'var(--text-muted)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'color var(--transition-fast), background-color var(--transition-fast)',
|
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-secondary)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-muted)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'transparent';
|
|
||||||
}}
|
|
||||||
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
>
|
>
|
||||||
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,95 @@ export function SkeletonChannelHeader() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Skeleton for the queue list page. */
|
||||||
|
export function SkeletonQueueList({ rows = 6 }: { rows?: number }) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 'var(--space-4)' }}>
|
||||||
|
{/* Tab row placeholder */}
|
||||||
|
<div style={{ display: 'flex', gap: '2px', marginBottom: 'var(--space-4)' }}>
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} width={80} height={32} borderRadius="var(--radius-md) var(--radius-md) 0 0" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<SkeletonTable rows={rows} columns={7} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Skeleton for the library page. */
|
||||||
|
export function SkeletonLibrary({ rows = 8 }: { rows?: number }) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 'var(--space-4)' }}>
|
||||||
|
{/* Search + filters placeholder */}
|
||||||
|
<div style={{ display: 'flex', gap: 'var(--space-4)', marginBottom: 'var(--space-4)' }}>
|
||||||
|
<Skeleton width={240} height={36} borderRadius="var(--radius-md)" />
|
||||||
|
<Skeleton width={120} height={36} borderRadius="var(--radius-md)" />
|
||||||
|
<Skeleton width={100} height={36} borderRadius="var(--radius-md)" />
|
||||||
|
<Skeleton width={130} height={36} borderRadius="var(--radius-md)" />
|
||||||
|
</div>
|
||||||
|
<SkeletonTable rows={rows} columns={9} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Skeleton for the activity page. */
|
||||||
|
export function SkeletonActivityList({ rows = 6 }: { rows?: number }) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 'var(--space-4)' }}>
|
||||||
|
{/* Tab row placeholder */}
|
||||||
|
<div style={{ display: 'flex', gap: '2px', marginBottom: 'var(--space-4)' }}>
|
||||||
|
<Skeleton width={80} height={32} borderRadius="var(--radius-md) var(--radius-md) 0 0" />
|
||||||
|
<Skeleton width={80} height={32} borderRadius="var(--radius-md) var(--radius-md) 0 0" />
|
||||||
|
</div>
|
||||||
|
<SkeletonTable rows={rows} columns={5} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Skeleton for the system page. */
|
||||||
|
export function SkeletonSystem() {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 'var(--space-4)' }}>
|
||||||
|
<Skeleton width={120} height={24} style={{ marginBottom: 'var(--space-6)' }} />
|
||||||
|
{/* Health card placeholder */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-card)',
|
||||||
|
borderRadius: 'var(--radius-lg)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
padding: 'var(--space-4)',
|
||||||
|
marginBottom: 'var(--space-6)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)', marginBottom: i < 2 ? 'var(--space-3)' : 0 }}>
|
||||||
|
<Skeleton width={14} height={14} borderRadius="50%" />
|
||||||
|
<Skeleton width={100} height={14} />
|
||||||
|
<Skeleton width={60} height={20} borderRadius="var(--radius-md)" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Status table placeholder */}
|
||||||
|
<Skeleton width={100} height={24} style={{ marginBottom: 'var(--space-4)' }} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-card)',
|
||||||
|
borderRadius: 'var(--radius-lg)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
padding: 'var(--space-4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} style={{ display: 'flex', gap: 'var(--space-4)', marginBottom: i < 4 ? 'var(--space-3)' : 0 }}>
|
||||||
|
<Skeleton width={140} height={14} />
|
||||||
|
<Skeleton width={200} height={14} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Skeleton for the channels list page. */
|
/** Skeleton for the channels list page. */
|
||||||
export function SkeletonChannelsList({ rows = 4 }: { rows?: number }) {
|
export function SkeletonChannelsList({ rows = 4 }: { rows?: number }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { ActivityIcon, Clock, Loader, RefreshCw } from 'lucide-react';
|
import { ActivityIcon, Clock, RefreshCw } from 'lucide-react';
|
||||||
import { Table, type Column } from '../components/Table';
|
import { Table, type Column } from '../components/Table';
|
||||||
import { StatusBadge } from '../components/StatusBadge';
|
import { StatusBadge } from '../components/StatusBadge';
|
||||||
import { Pagination } from '../components/Pagination';
|
import { Pagination } from '../components/Pagination';
|
||||||
import { FilterBar, type FilterDefinition } from '../components/FilterBar';
|
import { FilterBar, type FilterDefinition } from '../components/FilterBar';
|
||||||
|
import { SkeletonActivityList } from '../components/Skeleton';
|
||||||
import { useHistory, useActivity, type HistoryFilters } from '../api/hooks/useActivity';
|
import { useHistory, useActivity, type HistoryFilters } from '../api/hooks/useActivity';
|
||||||
import type { DownloadHistoryRecord } from '@shared/types/index';
|
import type { DownloadHistoryRecord } from '@shared/types/index';
|
||||||
|
|
||||||
|
|
@ -278,18 +279,7 @@ export function ActivityPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => refetchHistory()}
|
onClick={() => refetchHistory()}
|
||||||
aria-label="Retry"
|
aria-label="Retry"
|
||||||
style={{
|
className="btn btn-danger"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--danger)',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Retry
|
Retry
|
||||||
|
|
@ -298,12 +288,7 @@ export function ActivityPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading */}
|
{/* Loading */}
|
||||||
{historyLoading && (
|
{historyLoading && <SkeletonActivityList />}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-8)', color: 'var(--text-muted)' }}>
|
|
||||||
<Loader size={20} style={{ animation: 'spin 1s linear infinite', marginRight: 'var(--space-3)' }} />
|
|
||||||
Loading history…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
{!historyLoading && (
|
{!historyLoading && (
|
||||||
|
|
@ -351,18 +336,7 @@ export function ActivityPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => refetchRecent()}
|
onClick={() => refetchRecent()}
|
||||||
aria-label="Retry"
|
aria-label="Retry"
|
||||||
style={{
|
className="btn btn-danger"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--danger)',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Retry
|
Retry
|
||||||
|
|
@ -371,12 +345,7 @@ export function ActivityPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading */}
|
{/* Loading */}
|
||||||
{recentLoading && (
|
{recentLoading && <SkeletonActivityList />}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-8)', color: 'var(--text-muted)' }}>
|
|
||||||
<Loader size={20} style={{ animation: 'spin 1s linear infinite', marginRight: 'var(--space-3)' }} />
|
|
||||||
Loading recent activity…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Activity feed */}
|
{/* Activity feed */}
|
||||||
{!recentLoading && (
|
{!recentLoading && (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Library as LibraryIcon, Loader, RefreshCw, Film, Music } from 'lucide-react';
|
import { Library as LibraryIcon, RefreshCw, Film, Music } from 'lucide-react';
|
||||||
import { Table, type Column } from '../components/Table';
|
import { Table, type Column } from '../components/Table';
|
||||||
import { StatusBadge } from '../components/StatusBadge';
|
import { StatusBadge } from '../components/StatusBadge';
|
||||||
import { QualityLabel } from '../components/QualityLabel';
|
import { QualityLabel } from '../components/QualityLabel';
|
||||||
|
|
@ -8,6 +8,7 @@ import { PlatformBadge } from '../components/PlatformBadge';
|
||||||
import { Pagination } from '../components/Pagination';
|
import { Pagination } from '../components/Pagination';
|
||||||
import { SearchBar } from '../components/SearchBar';
|
import { SearchBar } from '../components/SearchBar';
|
||||||
import { FilterBar, type FilterDefinition } from '../components/FilterBar';
|
import { FilterBar, type FilterDefinition } from '../components/FilterBar';
|
||||||
|
import { SkeletonLibrary } from '../components/Skeleton';
|
||||||
import { useLibraryContent, type LibraryFilters } from '../api/hooks/useLibrary';
|
import { useLibraryContent, type LibraryFilters } from '../api/hooks/useLibrary';
|
||||||
import { useChannels } from '../api/hooks/useChannels';
|
import { useChannels } from '../api/hooks/useChannels';
|
||||||
import type { ContentItem, ContentStatus, ContentType } from '@shared/types/index';
|
import type { ContentItem, ContentStatus, ContentType } from '@shared/types/index';
|
||||||
|
|
@ -335,18 +336,7 @@ export function Library() {
|
||||||
<button
|
<button
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
aria-label="Retry"
|
aria-label="Retry"
|
||||||
style={{
|
className="btn btn-danger"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--danger)',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Retry
|
Retry
|
||||||
|
|
@ -355,12 +345,7 @@ export function Library() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{isLoading && (
|
{isLoading && <SkeletonLibrary />}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-8)', color: 'var(--text-muted)' }}>
|
|
||||||
<Loader size={20} style={{ animation: 'spin 1s linear infinite', marginRight: 'var(--space-3)' }} />
|
|
||||||
Loading library…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Content table */}
|
{/* Content table */}
|
||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ListOrdered, RotateCcw, X, Loader, RefreshCw } from 'lucide-react';
|
import { ListOrdered, RotateCcw, X, RefreshCw } from 'lucide-react';
|
||||||
import { Table, type Column } from '../components/Table';
|
import { Table, type Column } from '../components/Table';
|
||||||
import { StatusBadge } from '../components/StatusBadge';
|
import { StatusBadge } from '../components/StatusBadge';
|
||||||
|
import { SkeletonQueueList } from '../components/Skeleton';
|
||||||
import { useQueue, useRetryQueueItem, useCancelQueueItem } from '../api/hooks/useQueue';
|
import { useQueue, useRetryQueueItem, useCancelQueueItem } from '../api/hooks/useQueue';
|
||||||
import type { QueueItem, QueueStatus } from '@shared/types/index';
|
import type { QueueItem, QueueStatus } from '@shared/types/index';
|
||||||
|
|
||||||
|
|
@ -163,20 +164,8 @@ export function Queue() {
|
||||||
disabled={retryMutation.isPending}
|
disabled={retryMutation.isPending}
|
||||||
title="Retry"
|
title="Retry"
|
||||||
aria-label="Retry failed item"
|
aria-label="Retry failed item"
|
||||||
style={{
|
className="btn-icon"
|
||||||
display: 'inline-flex',
|
style={{ color: 'var(--warning)' }}
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
padding: 0,
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--bg-input)',
|
|
||||||
color: 'var(--warning)',
|
|
||||||
cursor: retryMutation.isPending ? 'wait' : 'pointer',
|
|
||||||
transition: 'background-color var(--transition-fast)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RotateCcw size={14} />
|
<RotateCcw size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -190,20 +179,8 @@ export function Queue() {
|
||||||
disabled={cancelMutation.isPending}
|
disabled={cancelMutation.isPending}
|
||||||
title="Cancel"
|
title="Cancel"
|
||||||
aria-label="Cancel pending item"
|
aria-label="Cancel pending item"
|
||||||
style={{
|
className="btn-icon"
|
||||||
display: 'inline-flex',
|
style={{ color: 'var(--danger)' }}
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
padding: 0,
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--bg-input)',
|
|
||||||
color: 'var(--danger)',
|
|
||||||
cursor: cancelMutation.isPending ? 'wait' : 'pointer',
|
|
||||||
transition: 'background-color var(--transition-fast)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -294,18 +271,7 @@ export function Queue() {
|
||||||
<button
|
<button
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
aria-label="Retry"
|
aria-label="Retry"
|
||||||
style={{
|
className="btn btn-danger"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--danger)',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Retry
|
Retry
|
||||||
|
|
@ -336,12 +302,7 @@ export function Queue() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{isLoading && (
|
{isLoading && <SkeletonQueueList />}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-8)', color: 'var(--text-muted)' }}>
|
|
||||||
<Loader size={20} style={{ animation: 'spin 1s linear infinite', marginRight: 'var(--space-3)' }} />
|
|
||||||
Loading queue…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Queue table */}
|
{/* Queue table */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Loader, RefreshCw, Server, Activity, Cpu, HardDrive } from 'lucide-react';
|
import { RefreshCw, Server, Activity, Cpu, HardDrive } from 'lucide-react';
|
||||||
import { useSystemStatus, useHealth } from '../api/hooks/useSystem';
|
import { useSystemStatus, useHealth } from '../api/hooks/useSystem';
|
||||||
import { HealthStatus } from '../components/HealthStatus';
|
import { HealthStatus } from '../components/HealthStatus';
|
||||||
|
import { SkeletonSystem } from '../components/Skeleton';
|
||||||
import { formatBytes } from '../utils/format';
|
import { formatBytes } from '../utils/format';
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
@ -26,12 +27,7 @@ export function SystemPage() {
|
||||||
const isLoading = healthLoading || statusLoading;
|
const isLoading = healthLoading || statusLoading;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <SkeletonSystem />;
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-12)', color: 'var(--text-muted)' }}>
|
|
||||||
<Loader size={20} style={{ animation: 'spin 1s linear infinite', marginRight: 'var(--space-3)' }} />
|
|
||||||
Loading system info...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -52,25 +48,7 @@ export function SystemPage() {
|
||||||
onClick={() => refetchHealth()}
|
onClick={() => refetchHealth()}
|
||||||
title="Refresh health status"
|
title="Refresh health status"
|
||||||
aria-label="Refresh health status"
|
aria-label="Refresh health status"
|
||||||
style={{
|
className="btn btn-ghost"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--bg-hover)',
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
transition: 'color var(--transition-fast), background-color var(--transition-fast)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-primary)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--bg-selected)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-secondary)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Refresh
|
Refresh
|
||||||
|
|
@ -95,17 +73,7 @@ export function SystemPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => refetchHealth()}
|
onClick={() => refetchHealth()}
|
||||||
aria-label="Retry"
|
aria-label="Retry"
|
||||||
style={{
|
className="btn btn-danger"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--danger)',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Retry
|
Retry
|
||||||
|
|
@ -127,25 +95,7 @@ export function SystemPage() {
|
||||||
onClick={() => refetchStatus()}
|
onClick={() => refetchStatus()}
|
||||||
title="Refresh system status"
|
title="Refresh system status"
|
||||||
aria-label="Refresh system status"
|
aria-label="Refresh system status"
|
||||||
style={{
|
className="btn btn-ghost"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--bg-hover)',
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
transition: 'color var(--transition-fast), background-color var(--transition-fast)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-primary)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--bg-selected)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--text-secondary)';
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Refresh
|
Refresh
|
||||||
|
|
@ -170,17 +120,7 @@ export function SystemPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => refetchStatus()}
|
onClick={() => refetchStatus()}
|
||||||
aria-label="Retry"
|
aria-label="Retry"
|
||||||
style={{
|
className="btn btn-danger"
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-2)',
|
|
||||||
padding: 'var(--space-2) var(--space-3)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
backgroundColor: 'var(--danger)',
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 'var(--font-size-sm)',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Retry
|
Retry
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,94 @@ tr:hover {
|
||||||
50% { box-shadow: 0 0 8px var(--accent-glow); }
|
50% { box-shadow: 0 0 8px var(--accent-glow); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Button utility classes ── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
border-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background-color: var(--bg-hover);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
background-color: var(--bg-selected);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--transition-fast), background-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modal animation ── */
|
||||||
|
@keyframes modal-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Animations ── */
|
/* ── Animations ── */
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue