componenets standardization 1

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 21:42:04 +00:00
parent c880e24fc0
commit a4691ad2da
95 changed files with 3597 additions and 1745 deletions

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import Button from "../ui/button/Button";
const ChartTab: React.FC = () => {
const [selected, setSelected] = useState<
@@ -12,32 +13,35 @@ const ChartTab: React.FC = () => {
return (
<div className="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button
<Button
variant="ghost"
onClick={() => setSelected("optionOne")}
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
"optionOne"
)}`}
>
Monthly
</button>
</Button>
<button
<Button
variant="ghost"
onClick={() => setSelected("optionTwo")}
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
"optionTwo"
)}`}
>
Quarterly
</button>
</Button>
<button
<Button
variant="ghost"
onClick={() => setSelected("optionThree")}
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
"optionThree"
)}`}
>
Annually
</button>
</Button>
</div>
);
};

View File

@@ -4,6 +4,7 @@
*/
import React, { useState } from 'react';
import Badge from '../ui/badge/Badge';
import Button from '../ui/button/Button';
export interface ContentImageData {
id?: number;
@@ -73,12 +74,15 @@ export default function ContentImageCell({ image, maxPromptLength = 100, showPro
<p className="text-gray-700 dark:text-gray-300">
{displayPrompt}
{shouldTruncate && (
<button
<Button
variant="ghost"
tone="brand"
size="xs"
onClick={() => setShowFullPrompt(!showFullPrompt)}
className="ml-1 text-brand-500 hover:text-brand-600 text-xs"
className="ml-1 p-0 h-auto text-xs"
>
{showFullPrompt ? 'Show less' : 'Show more'}
</button>
</Button>
)}
</p>
</div>

View File

@@ -4,6 +4,8 @@
import { Modal } from '../ui/modal';
import { CloseIcon } from '../../icons';
import Button from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
interface ContentViewerModalProps {
isOpen: boolean;
@@ -30,12 +32,14 @@ export default function ContentViewerModal({
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h2>
<button
<IconButton
icon={<CloseIcon className="w-6 h-6" />}
variant="ghost"
tone="neutral"
size="sm"
title="Close"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
>
<CloseIcon className="w-6 h-6" />
</button>
/>
</div>
{/* Content */}
@@ -48,12 +52,14 @@ export default function ContentViewerModal({
{/* Footer */}
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<button
<Button
variant="outline"
tone="neutral"
size="sm"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
</Button>
</div>
</div>
</Modal>

View File

@@ -3,6 +3,8 @@ import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
import SelectDropdown from '../form/SelectDropdown';
import Label from '../form/Label';
import InputField from '../form/input/InputField';
import TextArea from '../form/input/TextArea';
export interface FormField {
key: string;
@@ -69,17 +71,15 @@ export default function FormModal({
<>
{fields.find(f => f.key === 'keyword') && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Label className="mb-2">
{fields.find(f => f.key === 'keyword')!.label}
{fields.find(f => f.key === 'keyword')!.required && <span className="text-error-500 ml-1">*</span>}
</label>
<input
</Label>
<InputField
type="text"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={fields.find(f => f.key === 'keyword')!.value || ''}
onChange={(e) => fields.find(f => f.key === 'keyword')!.onChange(e.target.value)}
placeholder={fields.find(f => f.key === 'keyword')!.placeholder}
required={fields.find(f => f.key === 'keyword')!.required}
/>
</div>
)}
@@ -87,20 +87,18 @@ export default function FormModal({
<div className="grid grid-cols-2 gap-4">
{fields.find(f => f.key === 'volume') && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Label className="mb-2">
{fields.find(f => f.key === 'volume')!.label}
{fields.find(f => f.key === 'volume')!.required && <span className="text-error-500 ml-1">*</span>}
</label>
<input
</Label>
<InputField
type="number"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={fields.find(f => f.key === 'volume')!.value || ''}
onChange={(e) => {
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
fields.find(f => f.key === 'volume')!.onChange(value);
}}
placeholder={fields.find(f => f.key === 'volume')!.placeholder}
required={fields.find(f => f.key === 'volume')!.required}
/>
</div>
)}
@@ -121,18 +119,14 @@ export default function FormModal({
className="w-full"
/>
) : (
<input
<InputField
type="number"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={difficultyField.value || ''}
onChange={(e) => {
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
difficultyField.onChange(value);
}}
placeholder={difficultyField.placeholder}
required={difficultyField.required}
min={difficultyField.min}
max={difficultyField.max}
/>
)}
</div>
@@ -161,36 +155,30 @@ export default function FormModal({
if (field.type === 'textarea') {
return (
<div key={`${field.key}-${idx}`}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Label className="mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</label>
<textarea
</Label>
<TextArea
rows={field.rows || 4}
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={field.value || ''}
onChange={(e) => field.onChange(e.target.value)}
onChange={(val) => field.onChange(val)}
placeholder={field.placeholder}
required={field.required}
/>
</div>
);
}
return (
<div key={`${field.key}-${idx}`}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Label className="mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</label>
<input
</Label>
<InputField
type={field.type}
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={field.value || ''}
onChange={(e) => field.onChange(e.target.value)}
placeholder={field.placeholder}
required={field.required}
min={field.min}
max={field.max}
/>
</div>
);

View File

@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react';
import { useErrorHandler } from '../../hooks/useErrorHandler';
import IconButton from '../ui/button/IconButton';
import Button from '../ui/button/Button';
export default function GlobalErrorDisplay() {
const { errors, clearError, clearAllErrors } = useErrorHandler('GlobalErrorDisplay');
@@ -42,23 +44,27 @@ export default function GlobalErrorDisplay() {
</details>
)}
</div>
<button
<IconButton
icon={<span className="text-xl leading-none">×</span>}
onClick={() => clearError(index)}
className="text-error-600 dark:text-error-400 hover:text-error-800 dark:hover:text-error-200 text-xl leading-none"
variant="ghost"
tone="danger"
size="sm"
aria-label="Dismiss error"
>
×
</button>
/>
</div>
</div>
))}
{errors.length > 1 && (
<button
<Button
onClick={clearAllErrors}
className="w-full px-3 py-2 text-xs bg-error-600 text-white rounded hover:bg-error-700"
variant="primary"
tone="danger"
size="sm"
fullWidth
>
Clear All Errors
</button>
</Button>
)}
</div>
);

View File

@@ -1,5 +1,8 @@
import { ReactNode, useState, useEffect } from 'react';
import Button from '../ui/button/Button';
import TextArea from '../form/input/TextArea';
import Select from '../form/Select';
import Label from '../form/Label';
import { useToast } from '../ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
@@ -292,14 +295,13 @@ export default function ImageGenerationCard({
{/* Prompt Description - Full Width */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
<Label className="mb-2">
Prompt Description *
</label>
<textarea
</Label>
<TextArea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onChange={(val) => setPrompt(val)}
rows={6}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
placeholder="Describe the visual elements, style, mood, and composition you want in the image..."
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
@@ -309,14 +311,13 @@ export default function ImageGenerationCard({
{/* Negative Prompt - Small */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
<Label className="mb-2">
Negative Prompt
</label>
<textarea
</Label>
<TextArea
value={negativePrompt}
onChange={(e) => setNegativePrompt(e.target.value)}
onChange={(val) => setNegativePrompt(val)}
rows={2}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
placeholder="Describe what you DON'T want in the image..."
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
@@ -328,56 +329,38 @@ export default function ImageGenerationCard({
<div className="grid grid-cols-3 gap-4">
{/* Image Type */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
<Label className="mb-2">
Image Type
</label>
<select
value={imageType}
onChange={(e) => setImageType(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
>
{typeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</Label>
<Select
options={typeOptions}
defaultValue={imageType}
onChange={(val) => setImageType(val)}
/>
</div>
{/* Image Size */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
<Label className="mb-2">
Image Size
</label>
<select
value={imageSize}
onChange={(e) => setImageSize(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
>
{sizeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</Label>
<Select
options={sizeOptions}
defaultValue={imageSize}
onChange={(val) => setImageSize(val)}
/>
</div>
{/* Image Format */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
<Label className="mb-2">
Image Format
</label>
<select
value={imageFormat}
onChange={(e) => setImageFormat(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
>
{formatOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</Label>
<Select
options={formatOptions}
defaultValue={imageFormat}
onChange={(val) => setImageFormat(val)}
/>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import React, { useEffect, useState, useRef } from 'react';
import { Modal } from '../ui/modal';
import { FileIcon, TimeIcon, CheckCircleIcon, ErrorIcon } from '../../icons';
import { fetchAPI } from '../../services/api';
import Button from '../ui/button/Button';
export interface ImageQueueItem {
imageId: number | null;
@@ -593,12 +594,14 @@ export default function ImageQueueModal({
{completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''} of {totalImages} total
</div>
{allDone && (
<button
<Button
variant="primary"
tone="brand"
size="sm"
onClick={onClose}
className="px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
Close
</button>
</Button>
)}
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { ReactNode, useEffect, useState } from 'react';
import Button from '../ui/button/Button';
interface ImageResultCardProps {
title: string;
@@ -195,11 +196,13 @@ export default function ImageResultCard({
</svg>
View Original
</a>
<button
<Button
variant="outline"
tone="neutral"
size="sm"
onClick={() => {
navigator.clipboard.writeText(imageData.url);
}}
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -211,12 +214,13 @@ export default function ImageResultCard({
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
Copy URL
</button>
</Button>
</div>
</div>
) : (

View File

@@ -4,6 +4,7 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
interface SearchModalProps {
isOpen: boolean;
@@ -87,11 +88,12 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
<Modal isOpen={isOpen} onClose={onClose} className="sm:max-w-lg">
<div className="p-0">
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 z-10">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</span>
{/* Using native input for ref and onKeyDown support - styled to match design system */}
<input
ref={inputRef}
type="text"
@@ -99,9 +101,9 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search pages..."
className="w-full pl-12 pr-4 py-4 text-lg border-b border-gray-200 dark:border-gray-700 bg-transparent focus:outline-none dark:text-white"
className="h-9 w-full rounded-lg border appearance-none px-3 py-2 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:focus:border-brand-800 pl-12 pr-4 py-4 text-lg border-b border-gray-200 dark:border-gray-700 rounded-none border-x-0 border-t-0"
/>
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-gray-400 hidden sm:block">
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-gray-400 hidden sm:block z-10">
ESC to close
</span>
</div>
@@ -113,10 +115,12 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
</div>
) : (
filteredResults.map((result, index) => (
<button
<Button
key={result.path}
variant="ghost"
tone="neutral"
onClick={() => handleSelect(result)}
className={`w-full px-4 py-3 flex items-center gap-3 text-left transition-colors ${
className={`w-full px-4 py-3 flex items-center gap-3 text-left justify-start rounded-none ${
index === selectedIndex
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400'
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'
@@ -126,7 +130,7 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<span className="font-medium">{result.title}</span>
</button>
</Button>
))
)}
</div>

View File

@@ -1,5 +1,6 @@
import React from "react";
import { GridIcon, ListIcon, TableIcon } from "../../icons";
import Button from "../ui/button/Button";
export type ViewType = "table" | "kanban" | "list";
@@ -23,19 +24,18 @@ const ViewToggle: React.FC<ViewToggleProps> = ({
return (
<div className={`inline-flex items-center gap-1 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900 ${className}`}>
{views.map((view) => (
<button
<Button
key={view.type}
onClick={() => onViewChange(view.type)}
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
currentView === view.type
? "bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm"
: "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
}`}
variant={currentView === view.type ? "secondary" : "ghost"}
tone="neutral"
size="sm"
className={currentView === view.type ? "shadow-sm" : ""}
title={view.label}
startIcon={view.icon}
>
{view.icon}
<span className="hidden sm:inline">{view.label}</span>
</button>
</Button>
))}
</div>
);