diff --git a/src/index.css b/src/index.css index 47f4c8a..28ba74d 100644 --- a/src/index.css +++ b/src/index.css @@ -176,6 +176,9 @@ html[data-theme-loaded='true'] * { border-color 0.25s ease, box-shadow 0.25s ease; } +input[type='search']::-webkit-search-cancel-button { + display: none; +} /* Hide scrollbar but preserve scrolling */ .scrollbar-hide { @@ -206,3 +209,9 @@ html[data-theme-loaded='true'] * { opacity: 1; } } + +@keyframes skeletonShimmer { + 100% { + transform: translateX(100%); + } +} diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index d1b5f15..29a4e21 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Outlet } from 'react-router-dom' @@ -17,10 +17,16 @@ import { QueueListIcon, UsersIcon, } from '@heroicons/react/24/outline' +import { Skeleton } from '@/shared/primitives/Skeleton' const AdminLayout = () => { const [collapsed, setCollapsed] = useState(false) const [activeItem, setActiveItem] = useState('Queue') + const [loading, setLoading] = useState(true) + + useEffect(() => { + setTimeout(() => setLoading(!loading), 5000) + }, []) return (
@@ -127,7 +133,13 @@ const AdminLayout = () => { title="Moderation Queue" showSearch searchPlaceholder="Search reports..." - actionsSlot={} + actionsSlot={ + loading ? ( + + ) : ( + + ) + } />
diff --git a/src/shared/composites/ReportCard/ReportCard.tsx b/src/shared/composites/ReportCard/ReportCard.tsx index 9048d40..d17d5ab 100644 --- a/src/shared/composites/ReportCard/ReportCard.tsx +++ b/src/shared/composites/ReportCard/ReportCard.tsx @@ -213,7 +213,7 @@ export function ReportCard({ isClickable onToggle={onToggleExpand} header={ -
+
@@ -225,30 +225,29 @@ export function ReportCard({ {resolvedReport.targetType}
+
- {' '} {resolvedReport.reportReason}
-
- Reported by {resolvedReport.reporter.name} · AI score:{' '} - {resolvedReport.aiConfidenceScore} -
-
+
+
+ Reported by {resolvedReport.reporter.name} · AI score:{' '} + {resolvedReport.aiConfidenceScore} +
-
+ +
Click to view {isExpanded ? 'report summary' : 'detailed report'} diff --git a/src/shared/composites/Topbar/Topbar.tsx b/src/shared/composites/Topbar/Topbar.tsx index 928cc59..8b0fb4e 100644 --- a/src/shared/composites/Topbar/Topbar.tsx +++ b/src/shared/composites/Topbar/Topbar.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' -import { MagnifyingGlassIcon } from '@heroicons/react/24/solid' +import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/solid' import { Input } from '@/shared/primitives/Input' export interface TopbarProps { @@ -25,8 +25,6 @@ export function Topbar({ searchSlot, showSearch, actionsSlot, - - onMenuToggle, searchPlaceholder, className = '', }: TopbarProps) { @@ -37,12 +35,12 @@ export function Topbar({ > {/* Left */}
- {/* Mobile hamburger */} - + > */} {/* Title */}

{title}

@@ -61,6 +59,20 @@ export function Topbar({ }} placeholder={searchPlaceholder || 'Search...'} prefixIcon={} + suffixIcon={ + searchQuery ? ( + + ) : null + } /> )}
diff --git a/src/shared/primitives/Avatar/Avatar.stories.tsx b/src/shared/primitives/Avatar/Avatar.stories.tsx index 6c6aa95..2e64fbb 100644 --- a/src/shared/primitives/Avatar/Avatar.stories.tsx +++ b/src/shared/primitives/Avatar/Avatar.stories.tsx @@ -75,3 +75,11 @@ export const BrokenImageFallback: Story = { src: '/broken-image.png', }, } + +export const Decorative: Story = { + args: { + name: '', + + src: 'https://i.pravatar.cc/150?img=12', + }, +} diff --git a/src/shared/primitives/Badge/Badge.stories.tsx b/src/shared/primitives/Badge/Badge.stories.tsx index 288fb63..68b202d 100644 --- a/src/shared/primitives/Badge/Badge.stories.tsx +++ b/src/shared/primitives/Badge/Badge.stories.tsx @@ -117,6 +117,14 @@ export const Medium: Story = { } export const Dot: Story = { + render: (args) => ( +
+ + + System online +
+ ), + args: { variant: 'success', dot: true, diff --git a/src/shared/primitives/BaseCard/BaseCard.stories.tsx b/src/shared/primitives/BaseCard/BaseCard.stories.tsx index 8537c29..2d46496 100644 --- a/src/shared/primitives/BaseCard/BaseCard.stories.tsx +++ b/src/shared/primitives/BaseCard/BaseCard.stories.tsx @@ -91,8 +91,7 @@ export const NestedBaseCards: Story = { render: () => (
-

Parent card

- +

Parent card

Nested card diff --git a/src/shared/primitives/Button/Button.stories.tsx b/src/shared/primitives/Button/Button.stories.tsx index eef6c7f..8595b80 100644 --- a/src/shared/primitives/Button/Button.stories.tsx +++ b/src/shared/primitives/Button/Button.stories.tsx @@ -123,3 +123,22 @@ export const FullWidth: Story = { fullWidth: true, }, } +export const Focused: Story = { + args: { + children: 'Focused Button', + }, + + play: async ({ canvasElement }) => { + const button = canvasElement.querySelector('button') + + button?.focus() + }, +} + +export const IconOnly: Story = { + args: { + 'aria-label': 'Close', + + children: '×', + }, +} diff --git a/src/shared/primitives/Input/Input.stories.tsx b/src/shared/primitives/Input/Input.stories.tsx index d1bc2b8..7330393 100644 --- a/src/shared/primitives/Input/Input.stories.tsx +++ b/src/shared/primitives/Input/Input.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import { useState } from 'react' -import { MagnifyingGlassIcon, EyeIcon } from '@heroicons/react/24/outline' +import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline' import { Input, type InputProps } from './Input' @@ -81,7 +81,7 @@ export const WithIcons: Story = { prefixIcon: , - suffixIcon: , + suffixIcon: , }, } @@ -112,3 +112,15 @@ export const Focus: Story = { input?.focus() }, } + +export const ReadOnly: Story = { + render: (args) => , + + args: { + label: 'Read only', + + value: 'Readonly value', + + isReadOnly: true, + }, +} diff --git a/src/shared/primitives/Progressbar/ProgressBar.stories.tsx b/src/shared/primitives/Progressbar/ProgressBar.stories.tsx index 4e87743..1837d23 100644 --- a/src/shared/primitives/Progressbar/ProgressBar.stories.tsx +++ b/src/shared/primitives/Progressbar/ProgressBar.stories.tsx @@ -80,3 +80,13 @@ export const ExtraSmall: Story = { size: 'xs', }, } + +export const WithAccessibleLabel: Story = { + args: { + value: 0.65, + + variant: 'warning', + + scoreLabel: 'AI confidence', + }, +} diff --git a/src/shared/primitives/Skeleton/Skeleton.stories.tsx b/src/shared/primitives/Skeleton/Skeleton.stories.tsx new file mode 100644 index 0000000..cd07e88 --- /dev/null +++ b/src/shared/primitives/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { Skeleton } from './Skeleton' + +const meta: Meta = { + title: 'Shared/Primitives/Skeleton', + + component: Skeleton, + + tags: ['autodocs'], + + parameters: { + layout: 'padded', + }, + + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +export const Text: Story = { + args: { + variant: 'text', + lines: 3, + }, +} + +export const Card: Story = { + args: { + variant: 'card', + }, +} + +export const Avatar: Story = { + args: { + variant: 'avatar', + + width: 56, + + height: 56, + }, +} + +export const Bar: Story = { + args: { + variant: 'bar', + + width: '100%', + }, +} + +export const Custom: Story = { + args: { + variant: 'custom', + + width: 240, + + height: 80, + + className: 'rounded-xl', + }, +} + +export const CardList: Story = { + render: () => ( +
+ {Array.from({ + length: 3, + }).map((_, index) => ( + + ))} +
+ ), +} +export const LoadingRegion: Story = { + render: () => ( +
+ + +
+ ), +} diff --git a/src/shared/primitives/Skeleton/Skeleton.tsx b/src/shared/primitives/Skeleton/Skeleton.tsx new file mode 100644 index 0000000..50ec516 --- /dev/null +++ b/src/shared/primitives/Skeleton/Skeleton.tsx @@ -0,0 +1,108 @@ +import React from 'react' + +import { cva } from 'class-variance-authority' + +export interface SkeletonProps { + variant?: 'text' | 'card' | 'avatar' | 'bar' | 'custom' + + width?: string | number + + height?: string | number + + lines?: number + + className?: string +} + +const skeletonStyles = cva( + ` + relative + overflow-hidden + rounded-md + bg-bg-tertiary + before:absolute + before:inset-0 + before:-translate-x-full + before:animate-[skeletonShimmer_1.8s_infinite] + before:bg-gradient-to-r + before:from-transparent + before:via-bg-secondary + before:to-transparent + `, + { + variants: { + variant: { + text: 'h-4 w-full', + + card: ` + h-[160px] + w-full + rounded-xl + `, + + avatar: 'rounded-full', + + bar: ` + h-2 + rounded-full + `, + + custom: '', + }, + }, + + defaultVariants: { + variant: 'text', + }, + } +) + +export function Skeleton({ + variant = 'text', + + width, + + height, + + lines = 1, + + className = '', +}: SkeletonProps) { + if (variant === 'text') { + return ( +
+ {Array.from({ + length: lines, + }).map((_, index) => ( +
1 ? '80%' : width, + height, + }} + /> + ))} +
+ ) + } + + return ( +
+ ) +} + +export default Skeleton diff --git a/src/shared/primitives/Skeleton/index.ts b/src/shared/primitives/Skeleton/index.ts new file mode 100644 index 0000000..5c93917 --- /dev/null +++ b/src/shared/primitives/Skeleton/index.ts @@ -0,0 +1,3 @@ +export { Skeleton } from './Skeleton' + +export type { SkeletonProps } from './Skeleton' diff --git a/src/shared/primitives/Switch/Switch.stories.tsx b/src/shared/primitives/Switch/Switch.stories.tsx index 39a7fa2..50aa8b8 100644 --- a/src/shared/primitives/Switch/Switch.stories.tsx +++ b/src/shared/primitives/Switch/Switch.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite' - +import { useState } from 'react' import { fn } from 'storybook/test' import { Switch } from './Switch' @@ -55,3 +55,14 @@ export const DisabledOn: Story = { disabled: true, }, } + +export const Interactive: Story = { + args: { + checked: false, + }, + render: (args) => { + const [checked, setChecked] = useState(false) + + return + }, +} diff --git a/src/shared/primitives/Tooltip/Tooltip.stories.tsx b/src/shared/primitives/Tooltip/Tooltip.stories.tsx index f84a861..4b46bb0 100644 --- a/src/shared/primitives/Tooltip/Tooltip.stories.tsx +++ b/src/shared/primitives/Tooltip/Tooltip.stories.tsx @@ -17,7 +17,12 @@ export default meta type Story = StoryObj const IconButton = () => ( - )