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

@@ -4,6 +4,10 @@ import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
// Custom IGNY8 Design System plugin
// See: docs/30-FRONTEND/COMPONENT-SYSTEM.md for full documentation
import igny8DesignSystem from './eslint/eslint-plugin-igny8-design-system.cjs'
export default tseslint.config(
{ ignores: ['dist'] },
{
@@ -16,6 +20,7 @@ export default tseslint.config(
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'igny8-design-system': igny8DesignSystem,
},
rules: {
...reactHooks.configs.recommended.rules,
@@ -23,6 +28,27 @@ export default tseslint.config(
'warn',
{ allowConstantExport: true },
],
// =====================================================
// IGNY8 DESIGN SYSTEM ENFORCEMENT
// These rules prevent inconsistent component usage
// =====================================================
// Prevent direct imports from icon libraries - must use src/icons
'no-restricted-imports': ['error', {
patterns: [
{
group: ['@heroicons/*', 'lucide-react', '@mui/icons-material', 'react-icons/*'],
message: 'Import icons from src/icons instead. Add new icons to src/icons/index.ts if needed.'
}
]
}],
// IGNY8 Design System Rules - Set to 'warn' initially, change to 'error' after fixing existing code
// These will show warnings for all raw HTML elements that should use components
'igny8-design-system/no-raw-button': 'warn',
'igny8-design-system/no-raw-input': 'warn',
'igny8-design-system/no-raw-select': 'warn',
'igny8-design-system/no-raw-textarea': 'warn',
},
},
)

View File

@@ -0,0 +1,230 @@
/**
* IGNY8 Design System ESLint Plugin
*
* This plugin enforces consistent component usage across the application.
* It prevents raw HTML elements and ensures all UI elements come from
* the central component library.
*
* DOCUMENTATION:
* - Full component reference: docs/30-FRONTEND/COMPONENT-SYSTEM.md
* - Design guide: DESIGN-GUIDE.md (root)
* - Live demo: /ui-elements route
*
* RULES:
* 1. no-raw-button - Use <Button> from components/ui/button/Button
* 2. no-raw-input - Use <InputField> from components/form/input/InputField
* 3. no-raw-select - Use <Select> from components/form/Select
* 4. no-raw-textarea - Use <TextArea> from components/form/input/TextArea
*
* USAGE: Import in eslint.config.js and enable rules
*/
module.exports = {
meta: {
name: 'eslint-plugin-igny8-design-system',
version: '1.0.0',
},
rules: {
/**
* Disallow raw <button> elements
* Must use <Button> component from components/ui/button/Button
*/
'no-raw-button': {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow raw <button> elements. Use <Button> from components/ui/button/Button instead.',
recommended: true,
},
messages: {
useButtonComponent: 'Use <Button> from components/ui/button/Button instead of raw <button>. For icon-only buttons, use <IconButton> from components/ui/button/IconButton.',
},
schema: [],
},
create(context) {
return {
JSXOpeningElement(node) {
if (node.name.type === 'JSXIdentifier' && node.name.name === 'button') {
// Allow in specific files that need raw buttons (like the Button component itself)
const filename = context.getFilename();
const allowedFiles = [
'Button.tsx',
'IconButton.tsx',
'ButtonGroup.tsx',
'Components.tsx', // Demo page
'UIElements.tsx', // Demo page
// Low-level UI components that need raw buttons
'Pagination.tsx',
'CompactPagination.tsx',
'AlertModal.tsx',
'Accordion.tsx',
'AccordionItem.tsx',
'Tabs.tsx',
'Tab.tsx',
'DropdownItem.tsx',
'SelectDropdown.tsx',
'MultiSelect.tsx',
'List.tsx',
'ListRadioItem.tsx',
'Dropdown.tsx',
// Form components
'PhoneInput.tsx',
'SearchModal.tsx',
];
if (allowedFiles.some(f => filename.endsWith(f))) {
return;
}
context.report({
node,
messageId: 'useButtonComponent',
});
}
},
};
},
},
/**
* Disallow raw <input> elements
* Must use <InputField>, <Checkbox>, <Radio>, or <FileInput>
*/
'no-raw-input': {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow raw <input> elements. Use InputField, Checkbox, Radio, or FileInput from components/form/input.',
recommended: true,
},
messages: {
useInputComponent: 'Use InputField, Checkbox, Radio, or FileInput from components/form/input instead of raw <input>.',
},
schema: [],
},
create(context) {
return {
JSXOpeningElement(node) {
if (node.name.type === 'JSXIdentifier' && node.name.name === 'input') {
const filename = context.getFilename();
const allowedFiles = [
'InputField.tsx',
'Checkbox.tsx',
'Radio.tsx',
'RadioSm.tsx',
'FileInput.tsx',
'Switch.tsx',
'Components.tsx',
'UIElements.tsx',
'date-picker.tsx',
// Low-level UI components
'Pagination.tsx',
'CompactPagination.tsx',
'ListRadioItem.tsx',
'SearchModal.tsx',
'PhoneInput.tsx',
'SelectDropdown.tsx',
'MultiSelect.tsx',
'ConfigModal.tsx', // Complex automation config
];
if (allowedFiles.some(f => filename.endsWith(f))) {
return;
}
context.report({
node,
messageId: 'useInputComponent',
});
}
},
};
},
},
/**
* Disallow raw <select> elements
* Must use <Select> or <SelectDropdown> from components/form/
*/
'no-raw-select': {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow raw <select> elements. Use Select or SelectDropdown from components/form.',
recommended: true,
},
messages: {
useSelectComponent: 'Use Select or SelectDropdown from components/form instead of raw <select>.',
},
schema: [],
},
create(context) {
return {
JSXOpeningElement(node) {
if (node.name.type === 'JSXIdentifier' && node.name.name === 'select') {
const filename = context.getFilename();
const allowedFiles = [
'Select.tsx',
'SelectDropdown.tsx',
'MultiSelect.tsx',
'Components.tsx',
'UIElements.tsx',
'CompactPagination.tsx', // Has custom styling needs
];
if (allowedFiles.some(f => filename.endsWith(f))) {
return;
}
context.report({
node,
messageId: 'useSelectComponent',
});
}
},
};
},
},
/**
* Disallow raw <textarea> elements
* Must use <TextArea> from components/form/input/TextArea
*/
'no-raw-textarea': {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow raw <textarea> elements. Use TextArea from components/form/input/TextArea.',
recommended: true,
},
messages: {
useTextAreaComponent: 'Use TextArea from components/form/input/TextArea instead of raw <textarea>.',
},
schema: [],
},
create(context) {
return {
JSXOpeningElement(node) {
if (node.name.type === 'JSXIdentifier' && node.name.name === 'textarea') {
const filename = context.getFilename();
const allowedFiles = [
'TextArea.tsx',
'Components.tsx',
'UIElements.tsx',
];
if (allowedFiles.some(f => filename.endsWith(f))) {
return;
}
context.report({
node,
messageId: 'useTextAreaComponent',
});
}
},
};
},
},
},
};

View File

@@ -116,6 +116,9 @@ const Help = lazy(() => import("./pages/Help/Help"));
// Components - Lazy loaded
const Components = lazy(() => import("./pages/Components"));
// UI Elements - Lazy loaded (Design System Reference)
const UIElements = lazy(() => import("./pages/UIElements"));
export default function App() {
@@ -279,6 +282,9 @@ export default function App() {
{/* Components (Showcase Page) */}
<Route path="/components" element={<Components />} />
{/* UI Elements (Design System - Non-indexable) */}
<Route path="/ui-elements" element={<UIElements />} />
</Route>
{/* Fallback Route */}

View File

@@ -5,6 +5,7 @@
import React, { useState, useEffect } from 'react';
import { automationService } from '../../services/automationService';
import ComponentCard from '../common/ComponentCard';
import Select from '../form/Select';
interface ActivityLogProps {
runId: string;
@@ -39,16 +40,16 @@ const ActivityLog: React.FC<ActivityLogProps> = ({ runId }) => {
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600 dark:text-gray-400">Lines:</label>
<select
value={lines}
onChange={(e) => setLines(parseInt(e.target.value))}
className="border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
<option value={500}>500</option>
</select>
<Select
options={[
{ value: '50', label: '50' },
{ value: '100', label: '100' },
{ value: '200', label: '200' },
{ value: '500', label: '500' },
]}
defaultValue={String(lines)}
onChange={(val) => setLines(parseInt(val))}
/>
</div>
</div>
<div className="bg-gray-900 dark:bg-gray-950 text-success-400 p-4 rounded-lg font-mono text-xs overflow-auto max-h-96 border border-gray-700">

View File

@@ -6,6 +6,9 @@ import React, { useState } from 'react';
import { AutomationConfig } from '../../services/automationService';
import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
import Checkbox from '../form/input/Checkbox';
import Select from '../form/Select';
import InputField from '../form/input/InputField';
interface ConfigModalProps {
config: AutomationConfig;
@@ -51,17 +54,13 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
<form onSubmit={handleSubmit}>
{/* Enable/Disable */}
<div className="mb-4">
<label className="flex items-center">
<input
type="checkbox"
checked={formData.is_enabled || false}
onChange={(e) =>
setFormData({ ...formData, is_enabled: e.target.checked })
}
className="mr-2"
/>
<span className="font-semibold">Enable Automation</span>
</label>
<Checkbox
label="Enable Automation"
checked={formData.is_enabled || false}
onChange={(checked) =>
setFormData({ ...formData, is_enabled: checked })
}
/>
<p className="text-sm text-gray-600 ml-6">
When enabled, automation will run on the configured schedule
</p>
@@ -70,36 +69,33 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
{/* Frequency */}
<div className="mb-4">
<label className="block font-semibold mb-1">Frequency</label>
<select
value={formData.frequency || 'daily'}
onChange={(e) =>
<Select
options={[
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly (Mondays)' },
{ value: 'monthly', label: 'Monthly (1st of month)' },
]}
defaultValue={formData.frequency || 'daily'}
onChange={(val) =>
setFormData({
...formData,
frequency: e.target.value as 'daily' | 'weekly' | 'monthly',
frequency: val as 'daily' | 'weekly' | 'monthly',
})
}
className="border rounded px-3 py-2 w-full"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly (Mondays)</option>
<option value="monthly">Monthly (1st of month)</option>
</select>
/>
</div>
{/* Scheduled Time */}
<div className="mb-4">
<label className="block font-semibold mb-1">Scheduled Time</label>
<input
<InputField
label="Scheduled Time"
type="time"
value={formData.scheduled_time || '02:00'}
onChange={(e) =>
setFormData({ ...formData, scheduled_time: e.target.value })
}
className="border rounded px-3 py-2 w-full"
hint="Time of day to run automation (24-hour format)"
/>
<p className="text-sm text-gray-600 mt-1">
Time of day to run automation (24-hour format)
</p>
</div>
{/* Batch Sizes */}
@@ -111,10 +107,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
Stage 1: Keywords Clusters
</label>
<input
<InputField
label="Stage 1: KeywordsClusters"
type="number"
value={formData.stage_1_batch_size || 20}
onChange={(e) =>
@@ -123,17 +117,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
stage_1_batch_size: parseInt(e.target.value),
})
}
min={1}
max={100}
className="border rounded px-3 py-2 w-full"
min="1"
max="100"
/>
</div>
<div>
<label className="block text-sm mb-1">
Stage 2: Clusters Ideas
</label>
<input
<InputField
label="Stage 2: ClustersIdeas"
type="number"
value={formData.stage_2_batch_size || 1}
onChange={(e) =>
@@ -142,17 +133,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
stage_2_batch_size: parseInt(e.target.value),
})
}
min={1}
max={10}
className="border rounded px-3 py-2 w-full"
min="1"
max="10"
/>
</div>
<div>
<label className="block text-sm mb-1">
Stage 3: Ideas Tasks
</label>
<input
<InputField
label="Stage 3: IdeasTasks"
type="number"
value={formData.stage_3_batch_size || 20}
onChange={(e) =>
@@ -161,17 +149,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
stage_3_batch_size: parseInt(e.target.value),
})
}
min={1}
max={100}
className="border rounded px-3 py-2 w-full"
min="1"
max="100"
/>
</div>
<div>
<label className="block text-sm mb-1">
Stage 4: Tasks Content
</label>
<input
<InputField
label="Stage 4: TasksContent"
type="number"
value={formData.stage_4_batch_size || 1}
onChange={(e) =>
@@ -180,17 +165,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
stage_4_batch_size: parseInt(e.target.value),
})
}
min={1}
max={10}
className="border rounded px-3 py-2 w-full"
min="1"
max="10"
/>
</div>
<div>
<label className="block text-sm mb-1">
Stage 5: Content Image Prompts
</label>
<input
<InputField
label="Stage 5: ContentImage Prompts"
type="number"
value={formData.stage_5_batch_size || 1}
onChange={(e) =>
@@ -199,17 +181,14 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
stage_5_batch_size: parseInt(e.target.value),
})
}
min={1}
max={10}
className="border rounded px-3 py-2 w-full"
min="1"
max="10"
/>
</div>
<div>
<label className="block text-sm mb-1">
Stage 6: Image Prompts Images
</label>
<input
<InputField
label="Stage 6: Image PromptsImages"
type="number"
value={formData.stage_6_batch_size || 1}
onChange={(e) =>
@@ -218,9 +197,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
stage_6_batch_size: parseInt(e.target.value),
})
}
min={1}
max={10}
className="border rounded px-3 py-2 w-full"
min="1"
max="10"
/>
</div>
</div>
@@ -235,10 +213,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
Within-Stage Delay (seconds)
</label>
<input
<InputField
label="Within-Stage Delay (seconds)"
type="number"
value={formData.within_stage_delay || 3}
onChange={(e) =>
@@ -247,20 +223,15 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
within_stage_delay: parseInt(e.target.value),
})
}
min={0}
max={30}
className="border rounded px-3 py-2 w-full"
min="0"
max="30"
hint="Delay between batches within a stage"
/>
<p className="text-xs text-gray-500 mt-1">
Delay between batches within a stage
</p>
</div>
<div>
<label className="block text-sm mb-1">
Between-Stage Delay (seconds)
</label>
<input
<InputField
label="Between-Stage Delay (seconds)"
type="number"
value={formData.between_stage_delay || 5}
onChange={(e) =>
@@ -269,13 +240,10 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
between_stage_delay: parseInt(e.target.value),
})
}
min={0}
max={60}
className="border rounded px-3 py-2 w-full"
min="0"
max="60"
hint="Delay between stage transitions"
/>
<p className="text-xs text-gray-500 mt-1">
Delay between stage transitions
</p>
</div>
</div>
</div>

View File

@@ -14,6 +14,7 @@ import {
} from '../../services/api';
import { useToast } from '../ui/toast/ToastContainer';
import Button from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
import {
PlayIcon,
PauseIcon,
@@ -243,12 +244,14 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
<div className="bg-error-50 dark:bg-error-900/20 border-2 border-error-500 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between">
<p className="text-error-700 dark:text-error-300 text-sm">{error}</p>
<button
<IconButton
icon={<XMarkIcon className="w-5 h-5" />}
variant="ghost"
tone="danger"
size="sm"
title="Close"
onClick={onClose}
className="text-error-500 hover:text-error-700 dark:hover:text-error-300"
>
<XMarkIcon className="w-5 h-5" />
</button>
/>
</div>
</div>
);
@@ -564,13 +567,15 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
</div>
{/* Debug table toggle + table for stage data */}
<div className="mt-4">
<button
<Button
type="button"
variant="ghost"
tone="neutral"
size="xs"
onClick={() => setShowDebugTable(!showDebugTable)}
className="text-xs text-gray-600 hover:underline"
>
{showDebugTable ? 'Hide' : 'Show'} debug table
</button>
</Button>
{showDebugTable && (
<div className="mt-3 bg-white dark:bg-gray-800 p-3 rounded border">
<div className="text-sm font-semibold mb-2">Stage Data</div>

View File

@@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react';
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
import { useToast } from '../ui/toast/ToastContainer';
import Button from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
import {
PlayIcon,
PauseIcon,
@@ -228,13 +229,14 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
</div>
{/* Close Button - Top Right */}
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-1"
<IconButton
icon={<XMarkIcon className="w-5 h-5" />}
variant="ghost"
tone="neutral"
size="sm"
title="Close"
>
<XMarkIcon className="w-5 h-5" />
</button>
onClick={onClose}
/>
</div>
{/* Progress Section */}

View File

@@ -59,27 +59,31 @@ export default function UserAddressCard() {
</div>
</div>
<button
<Button
variant="outline"
tone="neutral"
size="sm"
onClick={openModal}
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
startIcon={
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
}
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
Edit
</button>
</Button>
</div>
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">

View File

@@ -67,27 +67,31 @@ export default function UserInfoCard() {
</div>
</div>
<button
<Button
variant="outline"
tone="neutral"
size="sm"
onClick={openModal}
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
startIcon={
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
}
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
Edit
</button>
</Button>
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">

View File

@@ -119,27 +119,31 @@ export default function UserMetaCard() {
</a>
</div>
</div>
<button
<Button
variant="outline"
tone="neutral"
size="sm"
onClick={openModal}
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
startIcon={
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
}
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
Edit
</button>
</Button>
</div>
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">

View File

@@ -7,6 +7,7 @@
*/
import React, { useState, useRef } from 'react';
import { Button } from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
import { Dropdown } from '../ui/dropdown/Dropdown';
import {
MoreDotIcon,
@@ -66,14 +67,15 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
return (
<>
<button
<IconButton
ref={buttonRef}
aria-label="more actions"
icon={<MoreDotIcon className="h-5 w-5" />}
variant="ghost"
tone="neutral"
size="sm"
title="more actions"
onClick={() => setIsOpen(!isOpen)}
className="dropdown-toggle flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
>
<MoreDotIcon className="h-5 w-5" />
</button>
/>
<Dropdown
isOpen={isOpen}
@@ -86,61 +88,76 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
{/* WordPress Publishing - Only show if images are ready */}
{canPublishToWordPress && (
<>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={handlePublishClick}
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
startIcon={<PaperPlaneIcon className="h-4 w-4" />}
className="w-full justify-start px-4 py-2"
>
<PaperPlaneIcon className="h-4 w-4" />
<span>Publish to Site</span>
</button>
Publish to Site
</Button>
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
</>
)}
{/* Edit Action */}
{onEdit && (
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleMenuAction(onEdit)}
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
startIcon={<PencilIcon className="h-4 w-4" />}
className="w-full justify-start px-4 py-2"
>
<PencilIcon className="h-4 w-4" />
<span>Edit</span>
</button>
Edit
</Button>
)}
{/* Generate Image Action */}
{onGenerateImage && (
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleMenuAction(onGenerateImage)}
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
startIcon={<FileIcon className="h-4 w-4" />}
className="w-full justify-start px-4 py-2"
>
<FileIcon className="h-4 w-4" />
<span>Generate Image Prompts</span>
</button>
Generate Image Prompts
</Button>
)}
{/* Export Action */}
{onExport && (
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleMenuAction(onExport)}
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
startIcon={<DownloadIcon className="h-4 w-4" />}
className="w-full justify-start px-4 py-2"
>
<DownloadIcon className="h-4 w-4" />
<span>Export</span>
</button>
Export
</Button>
)}
{/* Delete Action */}
{onDelete && (
<>
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
<button
<Button
variant="ghost"
tone="danger"
size="sm"
onClick={() => handleMenuAction(onDelete)}
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-error-600 hover:bg-error-50 dark:text-error-400 dark:hover:bg-error-500/10"
startIcon={<TrashBinIcon className="h-4 w-4" />}
className="w-full justify-start px-4 py-2"
>
<TrashBinIcon className="h-4 w-4" />
<span>Delete</span>
</button>
Delete
</Button>
</>
)}
</div>

View File

@@ -114,7 +114,7 @@ export default function SignInForm() {
</div>
<div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
<Button variant="outline" tone="neutral" size="md" className="gap-3">
<svg
width="20"
height="20"
@@ -140,8 +140,8 @@ export default function SignInForm() {
/>
</svg>
Sign in with Google
</button>
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
</Button>
<Button variant="outline" tone="neutral" size="md" className="gap-3">
<svg
width="21"
className="fill-current"
@@ -153,7 +153,7 @@ export default function SignInForm() {
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
</svg>
Sign in with X
</button>
</Button>
</div>
<div className="relative py-3 sm:py-5">
<div className="absolute inset-0 flex items-center">
@@ -246,21 +246,27 @@ export default function SignInForm() {
You're trying to login as: <strong>{sessionConflict.requestedUser.email}</strong>
</p>
<div className="flex gap-3">
<button
<Button
type="button"
variant="primary"
tone="warning"
size="sm"
onClick={handleForceLogout}
disabled={loading}
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-warning-600 rounded-lg hover:bg-warning-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="flex-1"
>
{loading ? 'Logging out...' : 'Logout Previous & Continue'}
</button>
<button
</Button>
<Button
type="button"
variant="outline"
tone="warning"
size="sm"
onClick={() => setSessionConflict(null)}
className="flex-1 px-4 py-2 text-sm font-medium text-warning-700 bg-warning-100 rounded-lg hover:bg-warning-200 dark:bg-warning-900/40 dark:text-warning-300 dark:hover:bg-warning-900/60 transition-colors"
className="flex-1"
>
Cancel
</button>
</Button>
</div>
</div>
)}

View File

@@ -4,6 +4,7 @@ import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
import Label from "../form/Label";
import Input from "../form/input/InputField";
import Checkbox from "../form/input/Checkbox";
import Button from "../ui/button/Button";
import { useAuthStore } from "../../store/authStore";
export default function SignUpForm({ planDetails: planDetailsProp, planLoading: planLoadingProp }: { planDetails?: any; planLoading?: boolean }) {
@@ -134,7 +135,7 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
</div>
<div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
<Button variant="outline" tone="neutral" size="md" className="gap-3">
<svg
width="20"
height="20"
@@ -160,8 +161,8 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
/>
</svg>
Sign up with Google
</button>
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
</Button>
<Button variant="outline" tone="neutral" size="md" className="gap-3">
<svg
width="21"
className="fill-current"
@@ -173,7 +174,7 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
</svg>
Sign up with X
</button>
</Button>
</div>
<div className="relative py-3 sm:py-5">
<div className="absolute inset-0 flex items-center">
@@ -294,13 +295,16 @@ export default function SignUpForm({ planDetails: planDetailsProp, planLoading:
</div>
{/* Submit Button */}
<div>
<button
<Button
type="submit"
variant="primary"
tone="brand"
size="md"
fullWidth
disabled={loading}
className="flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg bg-brand-500 shadow-theme-xs hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Creating your account..." : "Start Free Trial"}
</button>
</Button>
</div>
</div>
</form>

View File

@@ -11,6 +11,7 @@ import Label from '../form/Label';
import Input from '../form/input/InputField';
import Checkbox from '../form/input/Checkbox';
import Button from '../ui/button/Button';
import SelectDropdown from '../form/SelectDropdown';
import { useAuthStore } from '../../store/authStore';
interface PaymentMethodConfig {
@@ -321,21 +322,21 @@ export default function SignUpFormSimplified({ planDetails: planDetailsProp, pla
<Label>
Country<span className="text-error-500">*</span>
</Label>
<select
name="billingCountry"
<SelectDropdown
options={[
{ value: 'US', label: 'United States' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'IN', label: 'India' },
{ value: 'PK', label: 'Pakistan' },
{ value: 'CA', label: 'Canada' },
{ value: 'AU', label: 'Australia' },
{ value: 'DE', label: 'Germany' },
{ value: 'FR', label: 'France' },
]}
placeholder="Select country"
value={formData.billingCountry}
onChange={handleChange}
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
>
<option value="US">United States</option>
<option value="GB">United Kingdom</option>
<option value="IN">India</option>
<option value="PK">Pakistan</option>
<option value="CA">Canada</option>
<option value="AU">Australia</option>
<option value="DE">Germany</option>
<option value="FR">France</option>
</select>
onChange={(val) => setFormData({ ...formData, billingCountry: val })}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Payment methods will be filtered by your country
</p>

View File

@@ -284,24 +284,28 @@ export default function SignUpFormUnified({
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-28'
}`}
></span>
<button
type="button"
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => setBillingPeriod('monthly')}
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
}`}
>
Monthly
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => setBillingPeriod('annually')}
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
}`}
>
Annually
</button>
</Button>
</div>
<div className="h-6 flex items-center justify-center">
<span className={`inline-flex items-center gap-1.5 text-xs text-success-600 dark:text-success-400 font-semibold bg-success-50 dark:bg-success-900/20 px-2 py-1 rounded-full transition-opacity duration-200 ${
@@ -345,8 +349,11 @@ export default function SignUpFormUnified({
const isFree = parseFloat(String(plan.price || 0)) === 0;
return (
<button
<Button
key={plan.id}
variant="ghost"
tone="neutral"
size="md"
onClick={() => onPlanSelect(plan)}
className={`p-3 rounded-lg border-2 text-left transition-all ${
isSelected
@@ -366,7 +373,7 @@ export default function SignUpFormUnified({
<div className="text-xs text-gray-500 dark:text-gray-400">
{billingPeriod === 'annually' && !isFree ? '/year' : '/month'}
</div>
</button>
</Button>
);
})}
</div>
@@ -550,24 +557,28 @@ export default function SignUpFormUnified({
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-32'
}`}
></span>
<button
type="button"
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => setBillingPeriod('monthly')}
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
}`}
>
Monthly
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => setBillingPeriod('annually')}
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
}`}
>
Annually
</button>
</Button>
</div>
<div className="h-7 flex items-center justify-center">
<p className={`inline-flex items-center gap-1.5 text-success-600 dark:text-success-400 text-sm font-semibold bg-success-50 dark:bg-success-900/20 px-3 py-1.5 rounded-full transition-opacity duration-200 ${

View File

@@ -9,6 +9,7 @@ import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
import Label from '../form/Label';
import Input from '../form/input/InputField';
import TextArea from '../form/input/TextArea';
import { Loader2Icon, UploadIcon, XIcon, CheckCircleIcon } from '../../icons';
import { API_BASE_URL } from '../../services/api';
import { useAuthStore } from '../../store/authStore';
@@ -245,15 +246,12 @@ export default function PaymentConfirmationModal({
{/* Additional Notes */}
<div>
<Label>Additional Notes (Optional)</Label>
<textarea
id="manual_notes"
name="manual_notes"
value={formData.manual_notes}
onChange={(e) => setFormData({ ...formData, manual_notes: e.target.value })}
<TextArea
placeholder="Any additional information about the payment..."
rows={3}
value={formData.manual_notes}
onChange={(val) => setFormData({ ...formData, manual_notes: val })}
disabled={loading}
className="w-full px-4 py-3 text-sm border border-gray-300 rounded-lg focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:focus:border-brand-400"
/>
</div>

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>
);

View File

@@ -1,5 +1,7 @@
import React, { useState, useEffect } from 'react';
import { SourceBadge, ContentSource } from './SourceBadge';
import InputField from '../form/input/InputField';
import Button from '../ui/button/Button';
interface ContentFilterProps {
onFilterChange: (filters: FilterState) => void;
@@ -34,12 +36,11 @@ export const ContentFilter: React.FC<ContentFilterProps> = ({ onFilterChange, cl
<div className={`space-y-4 ${className}`}>
{/* Search */}
<div>
<input
<InputField
type="text"
placeholder="Search content..."
value={filters.search}
onChange={handleSearchChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-transparent dark:bg-gray-800 dark:border-gray-600 dark:text-white"
/>
</div>
@@ -47,28 +48,24 @@ export const ContentFilter: React.FC<ContentFilterProps> = ({ onFilterChange, cl
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Source</label>
<div className="flex flex-wrap gap-2">
<button
<Button
variant={filters.source === 'all' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => handleSourceChange('all')}
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
filters.source === 'all'
? 'bg-brand-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
>
All
</button>
</Button>
{(['igny8', 'wordpress', 'shopify', 'custom'] as ContentSource[]).map((source) => (
<button
<Button
key={source}
variant={filters.source === source ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => handleSourceChange(source)}
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
filters.source === source
? 'bg-brand-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
>
<SourceBadge source={source} />
</button>
</Button>
))}
</div>
</div>

View File

@@ -11,6 +11,7 @@ import {
FileIcon,
ChevronDownIcon,
} from '../../icons';
import Button from '../ui/button/Button';
export interface AIOperation {
type: 'clustering' | 'ideas' | 'content' | 'images';
@@ -73,31 +74,33 @@ export default function AIOperationsWidget({ data, onPeriodChange, loading }: AI
{/* Period Dropdown */}
<div className="relative">
<button
<Button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
variant="ghost"
tone="neutral"
size="xs"
endIcon={<ChevronDownIcon className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />}
>
{currentPeriod.label}
<ChevronDownIcon className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />
</button>
</Button>
{isDropdownOpen && (
<div className="absolute right-0 mt-1 w-24 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-10">
{periods.map((period) => (
<button
<Button
key={period.value}
onClick={() => {
onPeriodChange?.(period.value);
setIsDropdownOpen(false);
}}
className={`w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 ${
data.period === period.value
? 'text-brand-600 dark:text-brand-400 font-medium'
: 'text-gray-600 dark:text-gray-400'
}`}
variant="ghost"
tone={data.period === period.value ? 'brand' : 'neutral'}
size="xs"
fullWidth
className="justify-start"
>
{period.label}
</button>
</Button>
))}
</div>
)}

View File

@@ -12,6 +12,8 @@ import {
CheckCircleIcon,
CloseIcon,
} from '../../icons';
import Button from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
export interface AttentionItem {
id: string;
@@ -80,9 +82,17 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
return (
<div className="mb-4">
{/* Header */}
<button
<Button
onClick={() => setIsCollapsed(!isCollapsed)}
className="w-full flex items-center justify-between px-5 py-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-800 rounded-t-xl hover:bg-warning-100 dark:hover:bg-warning-900/30 transition-colors"
variant="ghost"
tone="warning"
fullWidth
className="flex items-center justify-between px-5 py-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-800 rounded-t-xl hover:bg-warning-100 dark:hover:bg-warning-900/30"
endIcon={isCollapsed ? (
<ChevronDownIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
) : (
<ChevronUpIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
)}
>
<div className="flex items-center gap-2.5">
<AlertIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
@@ -90,12 +100,7 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
Needs Attention ({items.length})
</span>
</div>
{isCollapsed ? (
<ChevronDownIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
) : (
<ChevronUpIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
)}
</button>
</Button>
{/* Content */}
{!isCollapsed && (
@@ -127,12 +132,14 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
{item.actionLabel}
</Link>
) : item.onAction ? (
<button
<Button
onClick={item.onAction}
className="text-sm font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
variant="ghost"
tone="brand"
size="xs"
>
{item.actionLabel}
</button>
</Button>
) : null}
{item.secondaryActionHref && (
<Link
@@ -145,12 +152,14 @@ export default function NeedsAttentionBar({ items, onDismiss }: NeedsAttentionBa
</div>
</div>
{onDismiss && (
<button
<IconButton
icon={<CloseIcon className="w-4 h-4" />}
onClick={() => onDismiss(item.id)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<CloseIcon className="w-4 h-4" />
</button>
variant="ghost"
tone="neutral"
size="sm"
aria-label="Dismiss"
/>
)}
</div>
);

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useBillingStore } from '../../store/billingStore';
import ComponentCard from '../common/ComponentCard';
import Select from '../form/Select';
export default function UsageChartWidget() {
const { usageSummary, loading, loadUsageSummary } = useBillingStore();
@@ -40,15 +41,15 @@ export default function UsageChartWidget() {
>
<div className="space-y-4">
<div className="flex justify-end items-center mb-4">
<select
className="h-9 rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 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:focus:border-brand-800"
value={dateRange}
onChange={(e) => setDateRange(e.target.value as 'week' | 'month' | 'year')}
>
<option value="week">Last 7 Days</option>
<option value="month">This Month</option>
<option value="year">This Year</option>
</select>
<Select
options={[
{ value: 'week', label: 'Last 7 Days' },
{ value: 'month', label: 'This Month' },
{ value: 'year', label: 'This Year' },
]}
defaultValue={dateRange}
onChange={(val) => setDateRange(val as 'week' | 'month' | 'year')}
/>
</div>
<div className="grid grid-cols-2 gap-4">

View File

@@ -16,6 +16,7 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Card } from '../ui/card/Card';
import Button from '../ui/button/Button';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { useWorkflowStats, TimeFilter } from '../../hooks/useWorkflowStats';
import { WORKFLOW_COLORS } from '../../config/colors.config';
@@ -55,17 +56,15 @@ function TimeFilterButtons({ value, onChange }: TimeFilterProps) {
return (
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
{options.map((option) => (
<button
<Button
key={option.value}
onClick={() => onChange(option.value)}
className={`px-2 py-1 text-xs font-medium rounded-md transition-all ${
value === option.value
? 'bg-brand-500 text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
variant={value === option.value ? 'primary' : 'ghost'}
tone={value === option.value ? 'brand' : 'neutral'}
size="xs"
>
{option.label}
</button>
</Button>
))}
</div>
);

View File

@@ -3,6 +3,7 @@ import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { MoreDotIcon } from "../../icons";
import CountryMap from "./CountryMap";
import IconButton from "../ui/button/IconButton";
export default function DemographicCard() {
const [isOpen, setIsOpen] = useState(false);
@@ -26,9 +27,9 @@ export default function DemographicCard() {
</p>
</div>
<div className="relative inline-block">
<button className="dropdown-toggle" onClick={toggleDropdown}>
<IconButton variant="ghost" size="sm" onClick={toggleDropdown} aria-label="More options">
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
</button>
</IconButton>
<Dropdown
isOpen={isOpen}
onClose={closeDropdown}

View File

@@ -4,6 +4,7 @@ import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { MoreDotIcon } from "../../icons";
import { useState } from "react";
import IconButton from "../ui/button/IconButton";
export default function MonthlySalesChart() {
const options: ApexOptions = {
@@ -107,9 +108,9 @@ export default function MonthlySalesChart() {
Monthly Sales
</h3>
<div className="relative inline-block">
<button className="dropdown-toggle" onClick={toggleDropdown}>
<IconButton variant="ghost" size="sm" onClick={toggleDropdown} aria-label="More options">
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
</button>
</IconButton>
<Dropdown
isOpen={isOpen}
onClose={closeDropdown}

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { MoreDotIcon } from "../../icons";
import IconButton from "../ui/button/IconButton";
export default function MonthlyTarget() {
const series = [75.55];
@@ -76,9 +77,9 @@ export default function MonthlyTarget() {
</p>
</div>
<div className="relative inline-block">
<button className="dropdown-toggle" onClick={toggleDropdown}>
<IconButton variant="ghost" size="sm" onClick={toggleDropdown} aria-label="More options">
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 size-6" />
</button>
</IconButton>
<Dropdown
isOpen={isOpen}
onClose={closeDropdown}

View File

@@ -6,6 +6,7 @@ import {
TableRow,
} from "../ui/table";
import Badge from "../ui/badge/Badge";
import Button from "../ui/button/Button";
// Define the TypeScript interface for the table rows
interface Product {
@@ -79,7 +80,7 @@ export default function RecentOrders() {
</div>
<div className="flex items-center gap-3">
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
<Button variant="outline" size="sm">
<svg
className="stroke-current fill-white dark:fill-gray-800"
width="20"
@@ -116,10 +117,10 @@ export default function RecentOrders() {
/>
</svg>
Filter
</button>
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
</Button>
<Button variant="outline" size="sm">
See all
</button>
</Button>
</div>
</div>
<div className="max-w-full overflow-x-auto">

View File

@@ -14,6 +14,7 @@
import React from 'react';
import Input from './input/InputField';
import TextArea from './input/TextArea';
import SelectDropdown from './SelectDropdown';
import Label from './Label';
@@ -97,19 +98,13 @@ export default function FormFieldRenderer({
className="w-full"
/>
) : field.type === 'textarea' ? (
<textarea
id={fieldId}
className={`w-full rounded-lg border ${
error
? 'border-error-500'
: 'border-gray-300 dark:border-gray-700'
} 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:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
value={value}
onChange={(e) => onChange(field.key, e.target.value)}
<TextArea
placeholder={field.placeholder}
required={field.required}
rows={field.rows || 4}
value={value}
onChange={(val) => onChange(field.key, val)}
disabled={disabled}
error={!!error}
/>
) : (
<Input

View File

@@ -3,6 +3,8 @@ import { ThemeToggleButton } from "../common/ThemeToggleButton";
import NotificationDropdown from "./NotificationDropdown";
import UserDropdown from "./UserDropdown";
import { Link } from "react-router-dom";
import IconButton from "../ui/button/IconButton";
import InputField from "../form/input/InputField";
// Define the interface for the props
interface HeaderProps {
@@ -20,63 +22,54 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
<header className="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 lg:border-b">
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
<button
className="block w-10 h-10 text-gray-500 lg:hidden dark:text-gray-400"
<IconButton
icon={
<svg
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
fill="currentColor"
/>
</svg>
}
variant="ghost"
tone="neutral"
size="sm"
title="Toggle menu"
onClick={onToggle}
>
{/* Hamburger Icon */}
<svg
className={`block`}
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
fill="currentColor"
/>
</svg>
<svg
className="hidden"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
fill="currentColor"
/>
</svg>
{/* Cross Icon */}
</button>
<button
className="lg:hidden"
/>
<IconButton
icon={
<svg
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
fill="currentColor"
/>
</svg>
}
variant="outline"
tone="neutral"
size="sm"
title="Toggle sidebar"
onClick={onClick}
className="items-center justify-center hidden w-10 h-10 text-gray-500 border-gray-200 rounded-lg z-99999 dark:border-gray-800 lg:flex dark:text-gray-400 lg:h-11 lg:w-11 lg:border"
>
<svg
className="hidden fill-current lg:block"
width="16"
height="12"
viewBox="0 0 16 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
fill=""
/>
</svg>
</button>
className="hidden lg:flex"
/>
<Link to="/" className="lg:hidden">
<img
@@ -91,30 +84,35 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
/>
</Link>
<button
<IconButton
icon={
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
fill="currentColor"
/>
</svg>
}
variant="ghost"
tone="neutral"
size="sm"
title="More options"
onClick={toggleApplicationMenu}
className="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
fill="currentColor"
/>
</svg>
</button>
className="lg:hidden"
/>
<div className="hidden lg:block">
<form action="https://formbold.com/s/unique_form_id" method="POST">
<div className="relative">
<button className="absolute -translate-y-1/2 left-4 top-1/2">
<span className="absolute -translate-y-1/2 left-4 top-1/2 pointer-events-none">
<svg
className="fill-gray-500 dark:fill-gray-400"
width="20"
@@ -130,17 +128,17 @@ const Header: React.FC<HeaderProps> = ({ onClick, onToggle }) => {
fill=""
/>
</svg>
</button>
<input
</span>
<InputField
type="text"
placeholder="Search or type command..."
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-gray-900 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]"
className="pl-12 pr-14 xl:w-[430px]"
/>
<button className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
<span className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400 pointer-events-none">
<span> </span>
<span> K </span>
</button>
</span>
</div>
</form>
</div>

View File

@@ -8,6 +8,8 @@ import { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import IconButton from "../ui/button/IconButton";
import Button from "../ui/button/Button";
import {
useNotificationStore,
formatNotificationTime,
@@ -126,9 +128,10 @@ export default function NotificationDropdown() {
return (
<div className="relative">
<button
ref={buttonRef}
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full dropdown-toggle hover:text-gray-700 h-11 w-11 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
<IconButton
ref={buttonRef as React.RefObject<HTMLButtonElement>}
variant="outline"
className="relative dropdown-toggle h-11 w-11"
onClick={handleClick}
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
>
@@ -153,7 +156,7 @@ export default function NotificationDropdown() {
fill="currentColor"
/>
</svg>
</button>
</IconButton>
<Dropdown
isOpen={isOpen}
@@ -174,16 +177,21 @@ export default function NotificationDropdown() {
</h5>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
<Button
type="button"
onClick={markAllAsRead}
className="text-xs text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
variant="ghost"
tone="brand"
size="xs"
className="text-xs"
>
Mark all read
</button>
</Button>
)}
<button
<IconButton
variant="ghost"
size="sm"
onClick={toggleDropdown}
className="text-gray-500 transition dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
aria-label="Close notifications"
>
<svg
@@ -200,7 +208,7 @@ export default function NotificationDropdown() {
fill="currentColor"
/>
</svg>
</button>
</IconButton>
</div>
</div>

View File

@@ -7,6 +7,8 @@ import { useState, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { DropdownItem } from "../ui/dropdown/DropdownItem";
import IconButton from "../ui/button/IconButton";
import Button from "../ui/button/Button";
import {
useNotificationStore,
formatNotificationTime,
@@ -97,34 +99,38 @@ export default function NotificationDropdown() {
return (
<div className="relative">
<button
<IconButton
ref={buttonRef}
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full dropdown-toggle hover:text-gray-700 h-11 w-11 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
icon={
<svg
className="fill-current"
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
fill="currentColor"
/>
</svg>
}
variant="outline"
tone="neutral"
shape="circle"
className="relative dropdown-toggle h-11 w-11"
onClick={handleClick}
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
>
{/* Notification badge */}
{unreadCount > 0 && (
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-warning-500 text-[10px] font-semibold text-white">
{unreadCount > 9 ? '9+' : unreadCount}
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></span>
</span>
)}
<svg
className="fill-current"
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
fill="currentColor"
/>
</svg>
</button>
/>
{/* Notification badge */}
{unreadCount > 0 && (
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-warning-500 text-[10px] font-semibold text-white">
{unreadCount > 9 ? '9+' : unreadCount}
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></span>
</span>
)}
<Dropdown
isOpen={isOpen}
@@ -145,18 +151,23 @@ export default function NotificationDropdown() {
</h5>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
<Button
onClick={markAllAsRead}
className="text-xs text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
variant="ghost"
tone="brand"
size="xs"
className="text-xs"
>
Mark all read
</button>
</Button>
)}
<button
<IconButton
onClick={toggleDropdown}
className="text-gray-500 transition dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
variant="ghost"
tone="neutral"
size="sm"
aria-label="Close notifications"
>
icon={
<svg
className="fill-current"
width="20"
@@ -171,7 +182,8 @@ export default function NotificationDropdown() {
fill="currentColor"
/>
</svg>
</button>
}
/>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { Dropdown } from "../ui/dropdown/Dropdown";
import { Link } from "react-router-dom";
import { useAuthStore } from "../../store/authStore";
import Button from "../ui/button/Button";
export default function UserDropdown() {
const [isOpen, setIsOpen] = useState(false);
@@ -26,9 +27,11 @@ export default function UserDropdown() {
};
return (
<div className="relative">
<button
<Button
ref={buttonRef}
onClick={toggleDropdown}
variant="ghost"
tone="neutral"
className="flex items-center text-gray-700 dropdown-toggle dark:text-gray-400"
>
<span className="mr-3 overflow-hidden rounded-full h-11 w-11 bg-brand-500 flex items-center justify-center">
@@ -62,7 +65,7 @@ export default function UserDropdown() {
strokeLinejoin="round"
/>
</svg>
</button>
</Button>
<Dropdown
isOpen={isOpen}
@@ -162,8 +165,10 @@ export default function UserDropdown() {
</DropdownItem>
</li>
</ul>
<button
<Button
onClick={handleLogout}
variant="ghost"
tone="neutral"
className="flex items-center gap-3 px-3 py-2 mt-3 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
>
<svg
@@ -182,7 +187,7 @@ export default function UserDropdown() {
/>
</svg>
Sign out
</button>
</Button>
</Dropdown>
</div>
);

View File

@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
import { Card } from '../ui/card';
import Button from '../ui/button/Button';
import Badge from '../ui/badge/Badge';
import InputField from '../form/input/InputField';
import {
CloseIcon,
ArrowRightIcon,
@@ -297,12 +298,10 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
Site Name
</label>
<input
type="text"
<InputField
value={siteName}
onChange={(e) => setSiteName(e.target.value)}
placeholder="Enter site name"
className="w-full px-4 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-base focus:border-brand-500 focus:ring-2 focus:ring-brand-200 dark:focus:ring-brand-800"
/>
</div>
@@ -311,12 +310,10 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
Website Address
</label>
<input
type="text"
<InputField
value={websiteAddress}
onChange={(e) => setWebsiteAddress(e.target.value)}
placeholder="https://example.com"
className="w-full px-4 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-base focus:border-brand-500 focus:ring-2 focus:ring-brand-200 dark:focus:ring-brand-800"
/>
</div>

View File

@@ -93,18 +93,17 @@ export default function Step1Welcome({ onNext, onSkip }: Step1WelcomeProps) {
<div className="flex items-center justify-between">
<Button
variant="ghost"
tone="neutral"
onClick={onSkip}
className="text-gray-500"
>
Skip for now
</Button>
<Button
variant="primary"
onClick={onNext}
className="gap-2"
endIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Let's Get Started
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</div>

View File

@@ -278,20 +278,19 @@ export default function Step2AddSite({
<div className="flex items-center justify-between">
<Button
variant="ghost"
tone="neutral"
onClick={onBack}
className="gap-2"
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<ArrowLeftIcon className="w-4 h-4" />
Back
</Button>
<Button
variant="primary"
onClick={handleCreateSite}
disabled={isCreating || !data.siteName.trim() || !selectedIndustry || data.selectedSectors.length === 0}
className="gap-2"
endIcon={!isCreating ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
>
{isCreating ? 'Creating...' : 'Create Site'}
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</div>

View File

@@ -245,24 +245,13 @@ export default function Step3ConnectIntegration({
size="sm"
onClick={handleTestConnection}
disabled={isTesting}
className="gap-1"
startIcon={
isTesting ? <TimeIcon className="w-4 h-4 animate-spin" /> :
testResult === 'success' ? <CheckCircleIcon className="w-4 h-4" /> :
<TimeIcon className="w-4 h-4" />
}
>
{isTesting ? (
<>
<TimeIcon className="w-4 h-4 animate-spin" />
Testing...
</>
) : testResult === 'success' ? (
<>
<CheckCircleIcon className="w-4 h-4" />
Connected
</>
) : (
<>
<TimeIcon className="w-4 h-4" />
Test Connection
</>
)}
{isTesting ? 'Testing...' : testResult === 'success' ? 'Connected' : 'Test Connection'}
</Button>
</div>
</Card>
@@ -278,15 +267,16 @@ export default function Step3ConnectIntegration({
<div className="flex items-center justify-between">
<Button
variant="ghost"
tone="neutral"
onClick={onBack}
className="gap-2"
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<ArrowLeftIcon className="w-4 h-4" />
Back
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
tone="neutral"
onClick={onSkip}
>
Skip for now
@@ -294,10 +284,9 @@ export default function Step3ConnectIntegration({
<Button
variant="primary"
onClick={onNext}
className="gap-2"
endIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</div>

View File

@@ -4,6 +4,8 @@
*/
import React, { useState } from 'react';
import Button from '../../ui/button/Button';
import IconButton from '../../ui/button/IconButton';
import InputField from '../../form/input/InputField';
import { Card } from '../../ui/card';
import Badge from '../../ui/badge/Badge';
import Alert from '../../ui/alert/Alert';
@@ -158,14 +160,11 @@ export default function Step4AddKeywords({
<div className="flex gap-2">
<div className="relative flex-1">
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
<InputField
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder="Enter a keyword..."
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
className="pl-10"
/>
</div>
<Button
@@ -199,12 +198,14 @@ export default function Step4AddKeywords({
className="gap-1 pr-1"
>
{keyword}
<button
<IconButton
icon={<CloseIcon className="w-3 h-3" />}
onClick={() => handleRemoveKeyword(keyword)}
className="ml-1 p-0.5 hover:bg-gray-300 dark:hover:bg-gray-600 rounded"
>
<CloseIcon className="w-3 h-3" />
</button>
variant="ghost"
size="xs"
className="ml-1"
aria-label="Remove keyword"
/>
</Badge>
))}
</div>
@@ -214,12 +215,14 @@ export default function Step4AddKeywords({
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
{keywords.length > 0 && (
<button
<Button
variant="ghost"
size="sm"
onClick={() => setKeywords([])}
className="text-error-500 hover:text-error-600"
>
Clear all
</button>
</Button>
)}
</div>
@@ -251,15 +254,16 @@ export default function Step4AddKeywords({
<div className="flex items-center justify-between">
<Button
variant="ghost"
tone="neutral"
onClick={onBack}
className="gap-2"
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
<ArrowLeftIcon className="w-4 h-4" />
Back
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
tone="neutral"
onClick={onSkip}
>
Skip for now
@@ -268,10 +272,9 @@ export default function Step4AddKeywords({
variant="primary"
onClick={handleSubmitKeywords}
disabled={isAdding || keywords.length === 0}
className="gap-2"
endIcon={!isAdding ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
>
{isAdding ? 'Adding...' : `Add ${keywords.length} Keyword${keywords.length !== 1 ? 's' : ''}`}
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</div>

View File

@@ -157,10 +157,10 @@ export default function Step5Complete({
size="lg"
onClick={onComplete}
disabled={isLoading}
className="gap-2 w-full"
fullWidth
endIcon={!isLoading ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
>
{isLoading ? 'Loading...' : 'Go to Dashboard'}
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
);

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import './blocks.css';
import Button from '../../ui/button/Button';
export interface CTABlockProps {
title: string;
@@ -44,13 +45,12 @@ export function CTABlock({
{primaryCtaLabel}
</a>
) : (
<button
type="button"
className="shared-button shared-button--primary"
<Button
variant="primary"
onClick={onPrimaryCtaClick}
>
{primaryCtaLabel}
</button>
</Button>
)
)}
{secondaryCtaLabel && (
@@ -59,13 +59,12 @@ export function CTABlock({
{secondaryCtaLabel}
</a>
) : (
<button
type="button"
className="shared-button shared-button--secondary"
<Button
variant="secondary"
onClick={onSecondaryCtaClick}
>
{secondaryCtaLabel}
</button>
</Button>
)
)}
</div>

View File

@@ -1,5 +1,8 @@
import { useState } from 'react';
import './blocks.css';
import InputField from '../../form/input/InputField';
import TextArea from '../../form/input/TextArea';
import Button from '../../ui/button/Button';
export interface ContactFormBlockProps {
title?: string;
@@ -49,33 +52,25 @@ export function ContactFormBlock({
{field.required && <span className="shared-contact-form__required">*</span>}
</label>
{field.type === 'textarea' ? (
<textarea
id={field.name}
name={field.name}
required={field.required}
<TextArea
placeholder={field.placeholder}
value={formData[field.name] || ''}
onChange={(e) => handleChange(field.name, e.target.value)}
className="shared-contact-form__input"
onChange={(val) => handleChange(field.name, val)}
rows={4}
/>
) : (
<input
id={field.name}
name={field.name}
<InputField
type={field.type}
required={field.required}
placeholder={field.placeholder}
value={formData[field.name] || ''}
onChange={(e) => handleChange(field.name, e.target.value)}
className="shared-contact-form__input"
/>
)}
</div>
))}
<button type="submit" className="shared-button shared-button--primary">
<Button type="submit" variant="primary">
{submitLabel}
</button>
</Button>
</form>
</section>
);

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import './blocks.css';
import Button from '../../ui/button/Button';
export interface HeroBlockProps {
eyebrow?: string;
@@ -18,9 +19,9 @@ export function HeroBlock({ eyebrow, title, subtitle, ctaLabel, onCtaClick, supp
{subtitle && <p className="shared-hero__subtitle">{subtitle}</p>}
{supportingContent && <div className="shared-hero__support">{supportingContent}</div>}
{ctaLabel && (
<button type="button" className="shared-button" onClick={onCtaClick}>
<Button variant="primary" onClick={onCtaClick}>
{ctaLabel}
</button>
</Button>
)}
</section>
);

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import './blocks.css';
import Button from '../../ui/button/Button';
export interface ProductItem {
name: string;
@@ -41,13 +42,12 @@ export function ProductsBlock({
)}
{product.price && <p className="shared-products__price">{product.price}</p>}
{product.ctaLabel && (
<button
type="button"
className="shared-button"
<Button
variant="primary"
onClick={product.onCtaClick}
>
{product.ctaLabel}
</button>
</Button>
)}
</div>
))}

View File

@@ -6,6 +6,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../ui/card';
import Badge from '../ui/badge/Badge';
import Button from '../ui/button/Button';
// import { fetchSiteProgress, SiteProgress } from '../../services/api';
import { CheckCircleIcon, XCircleIcon, AlertCircleIcon, ArrowRightIcon } from '../../icons';
@@ -326,7 +327,7 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
<div className="flex items-start gap-2">
<AlertCircleIcon className="w-4 h-4 text-warning-600 dark:text-warning-400 mt-0.5 flex-shrink-0" />
<div className="text-xs text-warning-800 dark:text-warning-300">
Some data may be outdated. <button onClick={handleRetry} className="underline font-medium">Refresh</button>
Some data may be outdated. <Button variant="ghost" size="xs" onClick={handleRetry} className="underline font-medium p-0 h-auto">Refresh</Button>
</div>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { Card } from '../../ui/card';
import Button from '../../ui/button/Button';
import Label from '../../form/Label';
import TextArea from '../../form/input/TextArea';
import InputField from '../../form/input/InputField';
import { useToast } from '../../ui/toast/ToastContainer';
export interface StyleSettings {
@@ -106,53 +107,41 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant={activeTab === 'css' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('css')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'css'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<CodeIcon className="w-4 h-4 inline mr-2" />
<CodeIcon className="w-4 h-4 mr-2" />
Custom CSS
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'colors' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('colors')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'colors'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<PaletteIcon className="w-4 h-4 inline mr-2" />
<PaletteIcon className="w-4 h-4 mr-2" />
Colors
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'typography' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('typography')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'typography'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<TypeIcon className="w-4 h-4 inline mr-2" />
<TypeIcon className="w-4 h-4 mr-2" />
Typography
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'spacing' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('spacing')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'spacing'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Spacing
</button>
</Button>
</div>
</div>
@@ -185,18 +174,18 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
<div key={colorKey}>
<Label>{colorKey.charAt(0).toUpperCase() + colorKey.slice(1)} Color</Label>
<div className="flex gap-2 mt-1">
<input
<InputField
type="color"
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || '#000000'}
onChange={(e) => handleColorChange(colorKey, e.target.value)}
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
className="w-16 h-10"
/>
<input
<InputField
type="text"
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || ''}
onChange={(e) => handleColorChange(colorKey, e.target.value)}
placeholder="#000000"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="flex-1"
/>
</div>
</div>
@@ -211,45 +200,45 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
<div className="space-y-4">
<div>
<Label>Font Family</Label>
<input
<InputField
type="text"
value={styleSettings.typography?.fontFamily || ''}
onChange={(e) => handleTypographyChange('fontFamily', e.target.value)}
placeholder="Arial, sans-serif"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
</div>
<div>
<Label>Heading Font</Label>
<input
<InputField
type="text"
value={styleSettings.typography?.headingFont || ''}
onChange={(e) => handleTypographyChange('headingFont', e.target.value)}
placeholder="Georgia, serif"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
</div>
<div>
<Label>Base Font Size</Label>
<input
<InputField
type="text"
value={styleSettings.typography?.fontSize || ''}
onChange={(e) => handleTypographyChange('fontSize', e.target.value)}
placeholder="16px"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
</div>
<div>
<Label>Line Height</Label>
<input
<InputField
type="text"
value={styleSettings.typography?.lineHeight || ''}
onChange={(e) => handleTypographyChange('lineHeight', e.target.value)}
placeholder="1.5"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
</div>
</div>
@@ -262,12 +251,12 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
<div className="space-y-4">
<div>
<Label>Base Spacing Unit</Label>
<input
<InputField
type="text"
value={styleSettings.spacing?.base || ''}
onChange={(e) => handleSpacingChange('base', e.target.value)}
placeholder="8px"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Base unit for spacing calculations (e.g., 8px, 1rem)
@@ -276,12 +265,12 @@ export default function StyleEditor({ styleSettings, onChange, onSave, onReset }
<div>
<Label>Spacing Scale</Label>
<input
<InputField
type="text"
value={styleSettings.spacing?.scale || ''}
onChange={(e) => handleSpacingChange('scale', e.target.value)}
placeholder="1.5"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Multiplier for spacing scale (e.g., 1.5 for 1.5x spacing)

View File

@@ -9,6 +9,7 @@ import { Card } from '../../ui/card';
import Label from '../../form/Label';
import SelectDropdown from '../../form/SelectDropdown';
import Button from '../../ui/button/Button';
import InputField from '../../form/input/InputField';
export interface TemplateCustomization {
layout: string;
@@ -68,54 +69,42 @@ export default function TemplateCustomizer({
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant={activeTab === 'layout' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('layout')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'layout'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<LayoutIcon className="w-4 h-4 inline mr-2" />
<LayoutIcon className="w-4 h-4 mr-2" />
Layout
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'colors' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('colors')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'colors'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<PaletteIcon className="w-4 h-4 inline mr-2" />
<PaletteIcon className="w-4 h-4 mr-2" />
Colors
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'typography' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('typography')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'typography'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<TypeIcon className="w-4 h-4 inline mr-2" />
<TypeIcon className="w-4 h-4 mr-2" />
Typography
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'spacing' ? 'primary' : 'ghost'}
tone="brand"
size="sm"
onClick={() => setActiveTab('spacing')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'spacing'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<SettingsIcon className="w-4 h-4 inline mr-2" />
<SettingsIcon className="w-4 h-4 mr-2" />
Spacing
</button>
</Button>
</div>
</div>
@@ -203,7 +192,7 @@ export default function TemplateCustomizer({
<div>
<Label>Primary Color</Label>
<div className="flex gap-2 mt-1">
<input
<InputField
type="color"
value={customization.customStyles?.primaryColor || '#3b82f6'}
onChange={(e) =>
@@ -211,9 +200,9 @@ export default function TemplateCustomizer({
customStyles: { ...customization.customStyles, primaryColor: e.target.value },
})
}
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
className="w-16 h-10"
/>
<input
<InputField
type="text"
value={customization.customStyles?.primaryColor || '#3b82f6'}
onChange={(e) =>
@@ -222,7 +211,7 @@ export default function TemplateCustomizer({
})
}
placeholder="#3b82f6"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="flex-1"
/>
</div>
</div>
@@ -230,7 +219,7 @@ export default function TemplateCustomizer({
<div>
<Label>Background Color</Label>
<div className="flex gap-2 mt-1">
<input
<InputField
type="color"
value={customization.customStyles?.backgroundColor || '#ffffff'}
onChange={(e) =>
@@ -238,9 +227,9 @@ export default function TemplateCustomizer({
customStyles: { ...customization.customStyles, backgroundColor: e.target.value },
})
}
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
className="w-16 h-10"
/>
<input
<InputField
type="text"
value={customization.customStyles?.backgroundColor || '#ffffff'}
onChange={(e) =>
@@ -249,7 +238,7 @@ export default function TemplateCustomizer({
})
}
placeholder="#ffffff"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="flex-1"
/>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import React, { useState } from 'react';
import { SearchIcon, FilterIcon, CheckIcon } from '../../icons';
import { Card } from '../../ui/card';
import Button from '../../ui/button/Button';
import InputField from '../../form/input/InputField';
export interface TemplateOption {
id: string;
@@ -106,47 +107,37 @@ export default function TemplateLibrary({
<div className="space-y-4">
{/* Search and Filters */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
<div className="flex-1">
<InputField
type="text"
placeholder="Search templates..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
startIcon={<SearchIcon className="w-4 h-4" />}
/>
</div>
<div className="flex gap-2">
<button
type="button"
<Button
variant={showFeaturedOnly ? 'primary' : 'outline'}
onClick={() => setShowFeaturedOnly(!showFeaturedOnly)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
showFeaturedOnly
? 'bg-brand-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
startIcon={<FilterIcon className="w-4 h-4" />}
>
<FilterIcon className="w-4 h-4 inline mr-2" />
Featured Only
</button>
</Button>
</div>
</div>
{/* Category Filter */}
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
<Button
key={category}
type="button"
variant={selectedCategory === category ? 'primary' : 'outline'}
size="sm"
onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
selectedCategory === category
? 'bg-brand-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{category.charAt(0).toUpperCase() + category.slice(1)}
</button>
</Button>
))}
</div>

View File

@@ -5,9 +5,11 @@
import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import Button from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
import Label from '../form/Label';
import Input from '../form/input/InputField';
import Checkbox from '../form/input/Checkbox';
import Switch from '../form/switch/Switch';
import { useToast } from '../ui/toast/ToastContainer';
import { integrationApi, SiteIntegration } from '../../services/integration.api';
import { fetchAPI } from '../../services/api';
@@ -218,26 +220,11 @@ export default function WordPressIntegrationForm({
{/* Toggle Switch */}
{apiKey && (
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
</span>
<button
type="button"
onClick={() => handleToggleIntegration(!integrationEnabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 ${
integrationEnabled ? 'bg-brand-600' : 'bg-gray-300 dark:bg-gray-600'
}`}
role="switch"
aria-checked={integrationEnabled}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
integrationEnabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
<Switch
label={integrationEnabled ? 'Sync Enabled' : 'Sync Disabled'}
checked={integrationEnabled}
onChange={(checked) => handleToggleIntegration(checked)}
/>
)}
</div>
@@ -293,30 +280,32 @@ export default function WordPressIntegrationForm({
</label>
<div className="flex items-center gap-3">
<div className="relative">
<input
id="api-key"
className="dark:bg-dark-900 shadow-theme-xs focus:border-brand-300 focus:ring-brand-500/10 dark:focus:border-brand-800 h-11 w-full min-w-[360px] rounded-lg border border-gray-300 bg-transparent py-3 pr-[90px] pl-4 text-sm text-gray-800 placeholder:text-gray-400 focus:ring-3 focus:outline-hidden dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 font-mono"
<Input
className="w-full min-w-[360px] pr-[90px] font-mono"
readOnly
type={apiKeyVisible ? 'text' : 'password'}
value={apiKeyVisible ? apiKey : maskApiKey(apiKey)}
/>
<button
<Button
onClick={handleCopyApiKey}
className="absolute top-1/2 right-0 inline-flex h-11 -translate-y-1/2 cursor-pointer items-center gap-1 rounded-r-lg border border-gray-300 py-3 pr-3 pl-3.5 text-sm font-medium text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
variant="outline"
tone="neutral"
size="sm"
className="absolute top-1/2 right-0 -translate-y-1/2 rounded-l-none"
>
<CopyIcon className="w-4 h-4 fill-current" />
<span>Copy</span>
</button>
<CopyIcon className="w-4 h-4" />
Copy
</Button>
</div>
<div className="group relative inline-block">
<button
<IconButton
onClick={handleRegenerateApiKey}
disabled={generatingKey}
className="inline-flex h-11 w-11 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50"
variant="outline"
title="Regenerate"
>
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
</button>
</IconButton>
<div className="invisible absolute bottom-full left-1/2 z-50 mb-2.5 -translate-x-1/2 opacity-0 transition-opacity duration-300 group-hover:visible group-hover:opacity-100">
<div className="relative">
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium whitespace-nowrap text-gray-700 shadow-xs dark:bg-gray-800 dark:text-white">
@@ -326,9 +315,9 @@ export default function WordPressIntegrationForm({
</div>
</div>
</div>
<button
<IconButton
onClick={() => setApiKeyVisible(!apiKeyVisible)}
className="inline-flex h-11 w-11 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
variant="outline"
>
{apiKeyVisible ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -340,7 +329,7 @@ export default function WordPressIntegrationForm({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</IconButton>
</div>
</div>
</td>
@@ -354,22 +343,23 @@ export default function WordPressIntegrationForm({
</td>
<td className="px-5 py-3 whitespace-nowrap">
<div className="flex items-center gap-3">
<button
<IconButton
onClick={handleRegenerateApiKey}
disabled={generatingKey}
className="text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-400 disabled:opacity-50 transition-colors"
variant="ghost"
title="Regenerate API key"
>
<RefreshCwIcon className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
</button>
<button
</IconButton>
<IconButton
onClick={handleRevokeApiKey}
disabled={generatingKey}
className="text-gray-500 hover:text-error-500 dark:text-gray-400 dark:hover:text-error-400 disabled:opacity-50 transition-colors"
variant="ghost"
tone="danger"
title="Revoke API key"
>
<TrashBinIcon className="w-5 h-5" />
</button>
</IconButton>
</div>
</td>
</tr>

View File

@@ -1,6 +1,8 @@
import React, { useState } from "react";
import { PlusIcon, HorizontaLDots } from "../../icons";
import { PlusIcon, HorizontaLDots, FilterIcon } from "../../icons";
import RelationshipMap from "./RelationshipMap";
import Button from "../ui/button/Button";
import IconButton from "../ui/button/IconButton";
export interface Task {
id: string;
@@ -103,13 +105,12 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("All")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "All"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "All" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
All Tasks
<span
@@ -121,15 +122,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
>
{tasks.length}
</span>
</button>
</Button>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("Todo")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Todo"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "Todo" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
To do
<span
@@ -141,15 +141,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
>
{todoTasks.length}
</span>
</button>
</Button>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("InProgress")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "InProgress"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "InProgress" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
In Progress
<span
@@ -161,15 +160,14 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
>
{inProgressTasks.length}
</span>
</button>
</Button>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("Completed")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Completed"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "Completed" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
Completed
<span
@@ -181,24 +179,28 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
>
{completedTasks.length}
</span>
</button>
</Button>
</div>
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
</svg>
<Button
variant="outline"
tone="neutral"
size="sm"
startIcon={<FilterIcon className="fill-current" />}
>
Filter & Sort
</button>
</Button>
<button
<Button
variant="primary"
tone="brand"
size="sm"
onClick={onAddTask}
className="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
endIcon={<PlusIcon className="fill-current" />}
>
Add New Task
<PlusIcon className="fill-current" width={20} height={20} />
</button>
</Button>
</div>
</div>
</div>
@@ -324,12 +326,14 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
</h3>
<div className="relative">
<button
<IconButton
icon={<HorizontaLDots className="fill-current" />}
variant="ghost"
tone="neutral"
size="sm"
title="More options"
onClick={() => onDropdownToggle(status)}
className="text-gray-700 dark:text-gray-400"
>
<HorizontaLDots className="fill-current" width={24} height={24} />
</button>
/>
{openDropdown && (
<>
<div
@@ -337,15 +341,15 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
onClick={() => onDropdownClose(status)}
/>
<div className="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800">
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
Edit
</button>
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
</Button>
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
Delete
</button>
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
</Button>
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
Clear All
</button>
</Button>
</div>
</>
)}

View File

@@ -1,6 +1,7 @@
import React from "react";
import { Link } from "react-router-dom";
import { ArrowRightIcon } from "../../icons";
import Button from "../ui/button/Button";
export interface RelationshipData {
taskId: number;
@@ -43,13 +44,15 @@ const RelationshipMap: React.FC<RelationshipMapProps> = ({
<>
<div className="flex flex-wrap items-center gap-1">
{task.keywordNames?.slice(0, 3).map((keyword, idx) => (
<button
<Button
key={idx}
onClick={() => onNavigate?.("keyword", task.keywordIds![idx])}
className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
variant="ghost"
tone="neutral"
size="xs"
>
{keyword}
</button>
</Button>
))}
{task.keywordIds.length > 3 && (
<span className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">

View File

@@ -1,6 +1,9 @@
import React, { useState } from "react";
import { PlusIcon, HorizontaLDots } from "../../icons";
import { PlusIcon, HorizontaLDots, FilterIcon } from "../../icons";
import { Task } from "./KanbanBoard";
import Button from "../ui/button/Button";
import IconButton from "../ui/button/IconButton";
import Checkbox from "../form/input/Checkbox";
interface TaskListProps {
tasks: Task[];
@@ -66,13 +69,12 @@ const TaskList: React.FC<TaskListProps> = ({
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("All")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "All"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "All" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
All Tasks
<span
@@ -84,15 +86,14 @@ const TaskList: React.FC<TaskListProps> = ({
>
{tasks.length}
</span>
</button>
</Button>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("Todo")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Todo"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "Todo" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
To do
<span
@@ -104,15 +105,14 @@ const TaskList: React.FC<TaskListProps> = ({
>
{todoTasks.length}
</span>
</button>
</Button>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("InProgress")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "InProgress"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "InProgress" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
In Progress
<span
@@ -124,15 +124,14 @@ const TaskList: React.FC<TaskListProps> = ({
>
{inProgressTasks.length}
</span>
</button>
</Button>
<button
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleFilterChange("Completed")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Completed"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
className={`group ${selectedFilter === "Completed" ? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" : ""}`}
>
Completed
<span
@@ -144,24 +143,28 @@ const TaskList: React.FC<TaskListProps> = ({
>
{completedTasks.length}
</span>
</button>
</Button>
</div>
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
</svg>
<Button
variant="outline"
tone="neutral"
size="sm"
startIcon={<FilterIcon className="fill-current" />}
>
Filter & Sort
</button>
</Button>
<button
<Button
variant="primary"
tone="brand"
size="sm"
onClick={onAddTask}
className="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
endIcon={<PlusIcon className="fill-current" />}
>
Add New Task
<PlusIcon className="fill-current" width={20} height={20} />
</button>
</Button>
</div>
</div>
</div>
@@ -257,12 +260,14 @@ const TaskListSection: React.FC<TaskListSectionProps> = ({
</h3>
<div className="relative">
<button
<IconButton
icon={<HorizontaLDots className="fill-current" />}
variant="ghost"
tone="neutral"
size="sm"
title="More options"
onClick={() => onDropdownToggle(title.toLowerCase().replace(" ", "_"))}
className="text-gray-700 dark:text-gray-400"
>
<HorizontaLDots className="fill-current" width={24} height={24} />
</button>
/>
{openDropdown && (
<>
<div
@@ -270,15 +275,15 @@ const TaskListSection: React.FC<TaskListSectionProps> = ({
onClick={() => onDropdownClose(title.toLowerCase().replace(" ", "_"))}
/>
<div className="absolute right-0 top-full z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 shadow-theme-md dark:border-gray-800 dark:bg-gray-dark">
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
Edit
</button>
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
</Button>
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
Delete
</button>
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
</Button>
<Button variant="ghost" tone="neutral" size="xs" fullWidth className="justify-start">
Clear All
</button>
</Button>
</div>
</>
)}
@@ -334,27 +339,18 @@ const TaskListItem: React.FC<TaskListItemProps> = ({ task, checked, onClick, onC
</svg>
</span>
<label htmlFor={`taskCheckbox-${task.id}`} className="w-full cursor-pointer">
<div className="relative flex items-start">
<input
type="checkbox"
<div className="w-full flex items-start">
<div className="mr-3">
<Checkbox
id={`taskCheckbox-${task.id}`}
className="sr-only taskCheckbox"
checked={checked}
onChange={(e) => onCheckboxChange(e.target.checked)}
onChange={onCheckboxChange}
/>
<div className={`flex items-center justify-center w-full h-5 mr-3 border border-gray-300 rounded-md box max-w-5 dark:border-gray-700 ${checked ? "bg-brand-500 border-brand-500" : ""}`}>
<span className={checked ? "opacity-100" : "opacity-0"}>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6668 3.5L5.25016 9.91667L2.3335 7" stroke="white" strokeWidth="1.94437" strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
</span>
</div>
<p className="-mt-0.5 text-base text-gray-800 dark:text-white/90" onClick={onClick}>
{task.title}
</p>
</div>
</label>
<p className="-mt-0.5 text-base text-gray-800 dark:text-white/90 cursor-pointer" onClick={onClick}>
{task.title}
</p>
</div>
</div>
<div className="flex flex-col-reverse items-start justify-end w-full gap-3 xl:flex-row xl:items-center xl:gap-5">

View File

@@ -0,0 +1,145 @@
/**
* IconButton Component
*
* 🔒 STYLE LOCKED - See DESIGN_SYSTEM.md for available variants and sizes.
* Icon-only button component for actions that only need an icon.
*/
import { ReactNode, forwardRef } from "react";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
type IconButtonSize = "xs" | "sm" | "md" | "lg";
type IconButtonVariant = "solid" | "outline" | "ghost";
type IconButtonTone = "brand" | "success" | "warning" | "danger" | "neutral";
type IconButtonShape = "rounded" | "circle";
interface IconButtonProps {
icon: ReactNode;
size?: IconButtonSize;
variant?: IconButtonVariant;
tone?: IconButtonTone;
shape?: IconButtonShape;
onClick?: () => void;
disabled?: boolean;
className?: string;
type?: "button" | "submit" | "reset";
title?: string;
"aria-label"?: string;
}
const toneMap: Record<
IconButtonTone,
{
solid: string;
outline: string;
ghost: string;
ring: string;
}
> = {
brand: {
solid: "bg-brand-500 text-white hover:bg-brand-600",
outline: "text-brand-600 ring-1 ring-brand-200 hover:bg-brand-50 dark:ring-brand-500/40 dark:text-brand-300 dark:hover:bg-brand-500/10",
ghost: "text-brand-600 hover:bg-brand-50 dark:text-brand-300 dark:hover:bg-brand-500/10",
ring: "focus-visible:ring-brand-500",
},
success: {
solid: "bg-success-500 text-white hover:bg-success-600",
outline: "text-success-600 ring-1 ring-success-200 hover:bg-success-50 dark:ring-success-500/40 dark:text-success-300 dark:hover:bg-success-500/10",
ghost: "text-success-600 hover:bg-success-50 dark:text-success-300 dark:hover:bg-success-500/10",
ring: "focus-visible:ring-success-500",
},
warning: {
solid: "bg-warning-500 text-white hover:bg-warning-600",
outline: "text-warning-600 ring-1 ring-warning-200 hover:bg-warning-50 dark:ring-warning-500/40 dark:text-warning-300 dark:hover:bg-warning-500/10",
ghost: "text-warning-600 hover:bg-warning-50 dark:text-warning-300 dark:hover:bg-warning-500/10",
ring: "focus-visible:ring-warning-500",
},
danger: {
solid: "bg-error-500 text-white hover:bg-error-600",
outline: "text-error-600 ring-1 ring-error-200 hover:bg-error-50 dark:ring-error-500/40 dark:text-error-300 dark:hover:bg-error-500/10",
ghost: "text-error-600 hover:bg-error-50 dark:text-error-300 dark:hover:bg-error-500/10",
ring: "focus-visible:ring-error-500",
},
neutral: {
solid: "bg-gray-700 text-white hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500",
outline: "text-gray-700 ring-1 ring-gray-300 hover:bg-gray-100 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-800",
ghost: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800",
ring: "focus-visible:ring-gray-400",
},
};
const sizeClasses: Record<IconButtonSize, string> = {
xs: "h-6 w-6 text-xs",
sm: "h-8 w-8 text-sm",
md: "h-10 w-10 text-base",
lg: "h-12 w-12 text-lg",
};
const iconSizeClasses: Record<IconButtonSize, string> = {
xs: "[&>svg]:w-3 [&>svg]:h-3",
sm: "[&>svg]:w-4 [&>svg]:h-4",
md: "[&>svg]:w-5 [&>svg]:h-5",
lg: "[&>svg]:w-6 [&>svg]:h-6",
};
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
icon,
size = "md",
variant = "ghost",
tone = "neutral",
shape = "rounded",
onClick,
className = "",
disabled = false,
type = "button",
title,
"aria-label": ariaLabel,
},
ref,
) => {
const toneStyles = toneMap[tone];
const variantClasses: Record<IconButtonVariant, string> = {
solid: toneStyles.solid,
outline: clsx("bg-transparent transition-colors", toneStyles.outline),
ghost: toneStyles.ghost,
};
const baseClasses =
"inline-flex items-center justify-center transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
const shapeClasses = shape === "circle" ? "rounded-full" : "rounded-lg";
const computedClass = twMerge(
clsx(
baseClasses,
sizeClasses[size],
iconSizeClasses[size],
shapeClasses,
variantClasses[variant],
toneStyles.ring,
className,
),
);
return (
<button
ref={ref}
className={computedClass}
onClick={onClick}
type={type}
disabled={disabled}
title={title}
aria-label={ariaLabel || title}
>
{icon}
</button>
);
},
);
IconButton.displayName = "IconButton";
export default IconButton;

View File

@@ -1,4 +1,5 @@
import { ReactNode } from "react";
import Checkbox from "../../form/input/Checkbox";
interface ListProps {
children: ReactNode;
@@ -134,23 +135,13 @@ export const ListCheckboxItem: React.FC<ListCheckboxItemProps> = ({
}) => {
return (
<li className={`border-b border-gray-200 px-3 py-2.5 last:border-b-0 dark:border-gray-800 ${className}`}>
<div className="flex items-center gap-2">
<label className="flex items-center space-x-3 group cursor-pointer">
<div className="relative w-5 h-5">
<input
id={id}
className="w-5 h-5 appearance-none cursor-pointer dark:border-gray-700 border border-gray-300 checked:border-transparent rounded-md checked:bg-brand-500 disabled:opacity-60"
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(e) => onChange?.(e.target.checked)}
/>
</div>
</label>
<label htmlFor={id} className="flex items-center text-sm text-gray-500 cursor-pointer select-none dark:text-gray-400">
{label}
</label>
</div>
<Checkbox
id={id}
label={label}
checked={checked}
disabled={disabled}
onChange={(val) => onChange?.(val)}
/>
</li>
);
};

View File

@@ -1,5 +1,6 @@
import React from "react";
import { AngleLeftIcon, AngleRightIcon } from "../../../icons";
import Select from "../../form/Select";
interface CompactPaginationProps {
currentPage: number;
@@ -61,16 +62,15 @@ export const CompactPagination: React.FC<CompactPaginationProps> = ({
<label htmlFor="page-size" className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
Show:
</label>
<select
id="page-size"
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
className="h-8 px-2 text-sm rounded-lg border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
<Select
options={[
{ value: '10', label: '10' },
{ value: '20', label: '20' },
{ value: '50', label: '50' },
]}
defaultValue={String(pageSize)}
onChange={(val) => onPageSizeChange(Number(val))}
/>
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
per page
</span>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import Button from '../button/Button';
export interface PricingPlan {
id?: number;
@@ -109,8 +110,10 @@ export default function PricingTable({
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
}}
></span>
<button
type="button"
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => setBillingPeriod('monthly')}
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
billingPeriod === 'monthly'
@@ -119,9 +122,11 @@ export default function PricingTable({
}`}
>
Monthly
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => setBillingPeriod('annually')}
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
billingPeriod === 'annually'
@@ -130,7 +135,7 @@ export default function PricingTable({
}`}
>
Annually
</button>
</Button>
</div>
{billingPeriod === 'annually' && (
<span className="absolute left-[calc(100%+1rem)] whitespace-nowrap inline-flex items-center gap-1.5 text-success-600 dark:text-success-400 font-semibold bg-success-50 dark:bg-success-900/20 px-3 py-1.5 rounded-full text-sm">
@@ -230,7 +235,10 @@ export default function PricingTable({
);
})}
</ul>
<button
<Button
variant="primary"
tone="brand"
size="md"
onClick={() => handlePlanClick(plan)}
disabled={plan.disabled}
className={`flex w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium text-white shadow-theme-xs transition-colors mt-auto ${
@@ -240,7 +248,7 @@ export default function PricingTable({
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
</button>
</Button>
</div>
);
})}
@@ -304,7 +312,10 @@ export default function PricingTable({
);
})}
</ul>
<button
<Button
variant="primary"
tone="brand"
size="md"
onClick={() => handlePlanClick(plan)}
disabled={plan.disabled}
className={`flex w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium text-white shadow-theme-xs transition-colors ${
@@ -314,7 +325,7 @@ export default function PricingTable({
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{plan.buttonText || (isHighlighted ? 'Choose This Plan' : 'Choose Starter')}
</button>
</Button>
</div>
);
})}
@@ -376,7 +387,10 @@ export default function PricingTable({
>
{plan.period || 'For a Lifetime'}
</span>
<button
<Button
variant="primary"
tone="brand"
size="md"
onClick={() => handlePlanClick(plan)}
disabled={plan.disabled}
className={`flex h-11 w-full items-center justify-center rounded-lg p-3.5 text-sm font-medium shadow-theme-xs transition-colors ${
@@ -388,7 +402,7 @@ export default function PricingTable({
}`}
>
{plan.buttonText || (plan.disabled ? 'Current Plan' : 'Try for Free')}
</button>
</Button>
<ul className="mt-6 space-y-3">
{plan.features.map((feature, idx) => {
const isExcluded = feature.startsWith('!');

View File

@@ -5,6 +5,7 @@
import { useState } from 'react';
import { CheckIcon } from '../../../icons';
import Button from '../button/Button';
export interface PricingPlan {
id: number;
@@ -66,26 +67,20 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
<div className="flex justify-center mb-8">
<div className="inline-flex items-center gap-3">
<div className="inline-flex items-center gap-3 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
<button
<Button
variant={billingPeriod === 'monthly' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setBillingPeriod('monthly')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
billingPeriod === 'monthly'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
Monthly
</button>
<button
</Button>
<Button
variant={billingPeriod === 'annual' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setBillingPeriod('annual')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
billingPeriod === 'annual'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
Annually
</button>
</Button>
</div>
{billingPeriod === 'annual' && (
<span className="badge-success">
@@ -202,13 +197,14 @@ export function PricingTable({ variant = '1', title, plans, showToggle = false,
)}
</ul>
<button
className={plan.highlighted ? 'btn-primary' : 'btn-outline'}
<Button
variant={plan.highlighted ? 'primary' : 'outline'}
fullWidth
onClick={() => onPlanSelect?.(plan)}
disabled={plan.disabled}
>
{plan.buttonText}
</button>
</Button>
</div>
))}
</div>

View File

@@ -10,6 +10,7 @@ import SearchModal from "../components/common/SearchModal";
import SiteAndSectorSelector from "../components/common/SiteAndSectorSelector";
import SingleSiteSelector from "../components/common/SingleSiteSelector";
import SiteWithAllSitesSelector from "../components/common/SiteWithAllSitesSelector";
import IconButton from "../components/ui/button/IconButton";
import React from "react";
// Route patterns for selector visibility
@@ -104,22 +105,26 @@ const AppHeader: React.FC = () => {
</Link>
{/* Mobile Menu Toggle */}
<button
<IconButton
variant="ghost"
onClick={toggleApplicationMenu}
className="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden"
className="w-10 h-10 z-99999 lg:hidden"
aria-label="Toggle menu"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z" fill="currentColor" />
</svg>
</button>
</IconButton>
{/* Page Title with Badge - Desktop */}
{pageInfo && (
<div className="hidden lg:flex items-center gap-3">
{/* Sidebar Toggle Button - Always visible on desktop */}
<button
<IconButton
variant="outline"
size="xs"
onClick={toggleSidebar}
className="flex items-center justify-center w-6 h-6 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-full shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
className="w-6 h-6 rounded-full"
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
>
<svg
@@ -130,7 +135,7 @@ const AppHeader: React.FC = () => {
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</IconButton>
{pageInfo.badge && (
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${badgeColors[pageInfo.badge.color]?.bg || 'bg-gray-600'} flex-shrink-0`}>
@@ -181,15 +186,17 @@ const AppHeader: React.FC = () => {
)}
{/* Search Icon */}
<button
<IconButton
variant="ghost"
onClick={() => setIsSearchOpen(true)}
className="flex items-center justify-center w-10 h-10 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 transition-colors"
className="w-10 h-10"
title="Search (⌘K)"
aria-label="Search"
>
<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>
</button>
</IconButton>
{/* Dark Mode Toggler */}
<ThemeToggleButton />

View File

@@ -8,6 +8,7 @@ import {
} from "@heroicons/react/24/outline";
import SEO from "../components/SEO";
import { getMetaTags } from "../config/metaTags";
import Button from "../../components/ui/button/Button";
const CaseStudies: React.FC = () => {
const renderCta = (cta: { label: string; href: string }, className: string) => {
@@ -190,10 +191,12 @@ const CaseStudies: React.FC = () => {
<p className="text-base text-gray-700 leading-relaxed">
Igny8's roadmap is shaped by an active community of customer strategists, agency partners, and product marketers. Join and get early access to features, template libraries, and industry benchmarks.
</p>
<button className="inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white px-6 py-3 text-sm font-semibold hover:shadow-lg transition">
<Button
variant="gradient"
endIcon={<ArrowRightIcon className="h-4 w-4" />}
>
Join the CAB waitlist
<ArrowRightIcon className="h-4 w-4" />
</button>
</Button>
</div>
</div>
</section>

View File

@@ -3,6 +3,9 @@ import SectionHeading from "../components/SectionHeading";
import CTASection from "../components/CTASection";
import SEO from "../components/SEO";
import { getMetaTags } from "../config/metaTags";
import InputField from "../../components/form/input/InputField";
import TextArea from "../../components/form/input/TextArea";
import Button from "../../components/ui/button/Button";
const Contact: React.FC = () => {
return (
@@ -22,7 +25,7 @@ const Contact: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label className="flex flex-col gap-2 text-sm text-gray-600">
First name
<input
<InputField
type="text"
placeholder="Alex"
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
@@ -30,7 +33,7 @@ const Contact: React.FC = () => {
</label>
<label className="flex flex-col gap-2 text-sm text-gray-600">
Last name
<input
<InputField
type="text"
placeholder="Rivera"
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
@@ -40,7 +43,7 @@ const Contact: React.FC = () => {
<label className="flex flex-col gap-2 text-sm text-gray-600">
Work email
<input
<InputField
type="email"
placeholder="you@company.com"
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
@@ -49,7 +52,7 @@ const Contact: React.FC = () => {
<label className="flex flex-col gap-2 text-sm text-gray-600">
Company
<input
<InputField
type="text"
placeholder="Company name"
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
@@ -58,19 +61,22 @@ const Contact: React.FC = () => {
<label className="flex flex-col gap-2 text-sm text-gray-600">
How can we help?
<textarea
<TextArea
rows={4}
placeholder="Tell us about your current workflow, challenges, and goals."
className="rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 resize-none"
/>
</label>
<button
<Button
type="submit"
variant="primary"
tone="brand"
size="md"
className="inline-flex items-center justify-center rounded-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-brand-700)] hover:from-[var(--color-brand-700)] hover:to-[var(--color-primary)] text-white px-6 py-3 text-sm font-semibold shadow-lg shadow-[var(--color-primary)]/30 transition-all w-full"
>
Book strategy call
</button>
</Button>
</form>
<div className="space-y-8">

View File

@@ -14,6 +14,7 @@ import { PricingTable, PricingPlan } from "../../components/ui/pricing-table";
import PricingTable1 from "../../components/ui/pricing-table/pricing-table-1";
import { Link } from "react-router-dom";
import { Plan, convertToPricingPlan } from "../../utils/pricingHelpers";
import Button from "../../components/ui/button/Button";
const Pricing: React.FC = () => {
const [plans, setPlans] = useState<Plan[]>([]);
@@ -326,12 +327,15 @@ const Pricing: React.FC = () => {
<section className="max-w-7xl mx-auto px-6 pb-24">
<div className="text-center py-12">
<p className="text-error-600">{error}</p>
<button
<Button
variant="primary"
tone="brand"
size="md"
onClick={() => window.location.reload()}
className="mt-4 px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)]"
>
Retry
</button>
</Button>
</div>
</section>
)}

View File

@@ -3,6 +3,9 @@ import SectionHeading from "../components/SectionHeading";
import CTASection from "../components/CTASection";
import SEO from "../components/SEO";
import { getMetaTags } from "../config/metaTags";
import InputField from "../../components/form/input/InputField";
import TextArea from "../../components/form/input/TextArea";
import Button from "../../components/ui/button/Button";
const roadmapItems = [
{
@@ -42,27 +45,25 @@ const Waitlist: React.FC = () => {
Share your details and we'll invite you to beta cohorts with onboarding resources and direct feedback loops to our product team.
</p>
<form className="space-y-4">
<input
<InputField
type="text"
placeholder="Name"
className="w-full rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
<input
<InputField
type="email"
placeholder="Work email"
className="w-full rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>
<textarea
<TextArea
rows={4}
placeholder="Tell us about your current workflow and why you're excited."
className="w-full rounded-xl border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-500 focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 resize-none"
/>
<button
<Button
type="submit"
className="inline-flex items-center justify-center rounded-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-brand-700)] hover:from-[var(--color-brand-700)] hover:to-[var(--color-primary)] text-white px-6 py-3 text-sm font-semibold shadow-lg shadow-[var(--color-primary)]/30 transition-all w-full"
variant="primary"
className="w-full"
>
Join waitlist
</button>
</Button>
</form>
</div>

View File

@@ -7,6 +7,8 @@ import { EventInput, DateSelectArg, EventClickArg } from "@fullcalendar/core";
import { Modal } from "../components/ui/modal";
import { useModal } from "../hooks/useModal";
import PageMeta from "../components/common/PageMeta";
import Button from "../components/ui/button/Button";
import InputField from "../components/form/input/InputField";
interface CalendarEvent extends EventInput {
extendedProps: {
@@ -166,12 +168,11 @@ const Calendar: React.FC = () => {
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Event Title
</label>
<input
<InputField
id="event-title"
type="text"
value={eventTitle}
onChange={(e) => setEventTitle(e.target.value)}
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs 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"
/>
</div>
</div>
@@ -220,12 +221,11 @@ const Calendar: React.FC = () => {
Enter Start Date
</label>
<div className="relative">
<input
<InputField
id="event-start-date"
type="date"
value={eventStartDate}
onChange={(e) => setEventStartDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs 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"
/>
</div>
</div>
@@ -235,31 +235,32 @@ const Calendar: React.FC = () => {
Enter End Date
</label>
<div className="relative">
<input
<InputField
id="event-end-date"
type="date"
value={eventEndDate}
onChange={(e) => setEventEndDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs 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"
/>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
<Button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
variant="outline"
tone="neutral"
size="md"
>
Close
</button>
<button
</Button>
<Button
onClick={handleAddOrUpdateEvent}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
variant="primary"
tone="brand"
size="md"
>
{selectedEvent ? "Update Changes" : "Add Event"}
</button>
</Button>
</div>
</div>
</Modal>

View File

@@ -8,6 +8,8 @@ import { Pagination } from '../components/ui/pagination/Pagination';
import { Card, CardImage, CardTitle, CardDescription, CardAction, CardIcon } from '../components/ui/card/Card';
import ChartTab from '../components/common/ChartTab';
import PageMeta from '../components/common/PageMeta';
import InputField from '../components/form/input/InputField';
import TextArea from '../components/form/input/TextArea';
export default function Components() {
// Alert modals state
@@ -257,9 +259,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
<InputField
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="Emirhan"
/>
</div>
@@ -267,9 +268,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
<InputField
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="Boruch"
/>
</div>
@@ -279,9 +279,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
<InputField
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="emirhanboruch55@gmail.com"
/>
</div>
@@ -289,9 +288,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone
</label>
<input
<InputField
type="tel"
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="+09 363 398 46"
/>
</div>
@@ -300,9 +298,8 @@ export default function Components() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Bio
</label>
<textarea
<TextArea
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
defaultValue="Team Manager"
/>
</div>
@@ -449,34 +446,34 @@ function ButtonGroupsShowcase() {
<div className="space-y-6">
{/* Default Button Group */}
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
<button className="px-4 py-2 text-sm font-medium text-gray-700 rounded-l-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
<Button variant="ghost" className="rounded-r-none border-r-0">
Left
</button>
<button className="px-4 py-2 text-sm font-medium text-gray-700 border-l border-r border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:border-gray-700 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="rounded-none border-x border-gray-300 dark:border-gray-700">
Center
</button>
<button className="px-4 py-2 text-sm font-medium text-gray-700 rounded-r-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="rounded-l-none border-l-0">
Right
</button>
</Button>
</div>
{/* Icon Button Group */}
<div className="inline-flex rounded-lg border border-gray-300 bg-white shadow-theme-xs dark:border-gray-700 dark:bg-gray-800">
<button className="p-2 text-gray-700 rounded-l-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
<Button variant="ghost" className="p-2 rounded-r-none border-r-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
</svg>
</button>
<button className="p-2 text-gray-700 border-l border-r border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:border-gray-700 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="p-2 rounded-none border-x border-gray-300 dark:border-gray-700">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1z" clipRule="evenodd" />
</svg>
</button>
<button className="p-2 text-gray-700 rounded-r-lg hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white">
</Button>
<Button variant="ghost" className="p-2 rounded-l-none border-l-0">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M3 10a1 1 0 011 1h12a1 1 0 110-2H4a1 1 0 01-1-1z" clipRule="evenodd" />
</svg>
</button>
</Button>
</div>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import Button from '../../components/ui/button/Button';
import { linkerApi } from '../../api/linker.api';
import { fetchContent, Content as ContentType } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
@@ -170,10 +171,11 @@ export default function LinkerContentList() {
{item.linker_version || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
<Button
onClick={() => handleLink(item.id)}
disabled={isProcessing || processing === -1}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
variant="primary"
size="sm"
>
{isProcessing ? (
<>
@@ -186,7 +188,7 @@ export default function LinkerContentList() {
Add Links
</>
)}
</button>
</Button>
</td>
</tr>
);
@@ -202,20 +204,22 @@ export default function LinkerContentList() {
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
</div>
<div className="flex gap-2">
<button
<Button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
variant="outline"
size="sm"
>
Previous
</button>
<button
</Button>
<Button
onClick={() => setCurrentPage(prev => prev + 1)}
disabled={currentPage * pageSize >= totalCount}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
variant="outline"
size="sm"
>
Next
</button>
</Button>
</div>
</div>
)}

View File

@@ -12,6 +12,9 @@ import { OptimizationScores } from '../../components/optimizer/OptimizationScore
import { BoltIcon, CheckCircleIcon, FileIcon } from '../../icons';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
import Button from '../../components/ui/button/Button';
export default function OptimizerContentSelector() {
const navigate = useNavigate();
@@ -153,21 +156,21 @@ export default function OptimizerContentSelector() {
/>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<select
value={entryPoint}
onChange={(e) => setEntryPoint(e.target.value as EntryPoint)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="auto">Auto-detect</option>
<option value="writer">From Writer</option>
<option value="wordpress">From WordPress</option>
<option value="external">From External</option>
<option value="manual">Manual</option>
</select>
<button
<Select
options={[
{ value: 'auto', label: 'Auto-detect' },
{ value: 'writer', label: 'From Writer' },
{ value: 'wordpress', label: 'From WordPress' },
{ value: 'external', label: 'From External' },
{ value: 'manual', label: 'Manual' },
]}
defaultValue={entryPoint}
onChange={(val) => setEntryPoint(val as EntryPoint)}
/>
<Button
variant="primary"
onClick={handleBatchOptimize}
disabled={selectedIds.length === 0 || processing.length > 0}
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{processing.length > 0 ? (
<>
@@ -180,7 +183,7 @@ export default function OptimizerContentSelector() {
Optimize Selected ({selectedIds.length})
</>
)}
</button>
</Button>
</div>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-6">
@@ -202,11 +205,9 @@ export default function OptimizerContentSelector() {
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th className="px-6 py-3 text-left">
<input
type="checkbox"
<Checkbox
checked={selectedIds.length === filteredContent.length && filteredContent.length > 0}
onChange={toggleSelectAll}
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
@@ -238,11 +239,9 @@ export default function OptimizerContentSelector() {
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-brand-50 dark:bg-brand-900/20' : ''}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
<Checkbox
checked={isSelected}
onChange={() => toggleSelection(item.id)}
className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
@@ -266,10 +265,11 @@ export default function OptimizerContentSelector() {
{item.optimizer_version || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
<Button
variant="primary"
size="sm"
onClick={() => handleOptimize(item.id)}
disabled={isProcessing || processing.length > 0}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isProcessing ? (
<>
@@ -282,7 +282,7 @@ export default function OptimizerContentSelector() {
Optimize
</>
)}
</button>
</Button>
</td>
</tr>
);
@@ -298,20 +298,22 @@ export default function OptimizerContentSelector() {
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
</div>
<div className="flex gap-2">
<button
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
>
Previous
</button>
<button
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => prev + 1)}
disabled={currentPage * pageSize >= totalCount}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
>
Next
</button>
</Button>
</div>
</div>
)}

View File

@@ -1,6 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate, Link } from "react-router-dom";
import { useAuthStore } from "../store/authStore";
import InputField from "../components/form/input/InputField";
import TextArea from "../components/form/input/TextArea";
import Label from "../components/form/Label";
import Button from "../components/ui/button/Button";
const PLAN_COPY: Record<string, { name: string; price: string; content: string }> = {
starter: { name: "Starter", price: "$49/mo", content: "50 content pieces/month" },
@@ -76,42 +80,39 @@ export default function Payment() {
)}
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-800">
Contact email
<input
type="email"
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder="you@example.com"
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-brand-500 focus:outline-none"
/>
</label>
<label className="block text-sm font-medium text-gray-800">
Notes (optional)
<textarea
<InputField
label="Contact email"
type="email"
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder="you@example.com"
/>
<div>
<Label className="mb-2">Notes (optional)</Label>
<TextArea
value={note}
onChange={(e) => setNote(e.target.value)}
onChange={(value) => setNote(value)}
placeholder="Company name, billing contact, or questions"
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-900 focus:border-brand-500 focus:outline-none"
rows={3}
/>
</label>
</div>
</div>
<div className="flex items-center justify-between">
<Link to="/signup" className="text-sm text-gray-600 hover:text-gray-800">
Prefer the free plan? Start your trial
</Link>
<a
href={mailtoHref || "#"}
onClick={handleRequest}
className={`inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold text-white ${
contactEmail.trim() ? "bg-brand-600 hover:bg-brand-700" : "bg-brand-400 cursor-not-allowed"
}`}
aria-disabled={!contactEmail.trim()}
<Button
onClick={() => {
if (mailtoHref && contactEmail.trim()) {
window.location.href = mailtoHref;
}
}}
disabled={!contactEmail.trim()}
variant="primary"
>
Request payment instructions
</a>
</Button>
</div>
{error && <p className="text-sm text-error-600">{error}</p>}
</div>

View File

@@ -233,10 +233,10 @@ export default function ClusterDetail() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant="ghost"
onClick={() => handleTabChange('articles')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'articles'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -244,11 +244,11 @@ export default function ClusterDetail() {
>
<FileIcon className="w-4 h-4 inline mr-2" />
Articles
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => handleTabChange('pages')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'pages'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -256,11 +256,11 @@ export default function ClusterDetail() {
>
<PageIcon className="w-4 h-4 inline mr-2" />
Pages
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => handleTabChange('products')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'products'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -268,11 +268,11 @@ export default function ClusterDetail() {
>
<GridIcon className="w-4 h-4 inline mr-2" />
Products
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => handleTabChange('taxonomy')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'taxonomy'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -280,7 +280,7 @@ export default function ClusterDetail() {
>
<TagIcon className="w-4 h-4 inline mr-2" />
Taxonomy
</button>
</Button>
</div>
</div>

View File

@@ -127,26 +127,28 @@ const CreditsAndBilling: React.FC = () => {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
<button
<Button
variant="ghost"
onClick={() => setActiveTab('overview')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
className={`py-4 px-1 border-b-2 font-medium text-sm rounded-none ${
activeTab === 'overview'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Overview
</button>
<button
</Button>
<Button
variant="ghost"
onClick={() => setActiveTab('transactions')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
className={`py-4 px-1 border-b-2 font-medium text-sm rounded-none ${
activeTab === 'transactions'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
Transactions ({transactions.length})
</button>
</Button>
</nav>
</div>

View File

@@ -5,6 +5,8 @@ import { useSettingsStore } from '../../store/settingsStore';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
export default function GeneralSettings() {
const toast = useToast();
@@ -49,13 +51,10 @@ export default function GeneralSettings() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="records_per_page">Records Per Page</Label>
<input
<InputField
id="records_per_page"
type="number"
min="5"
max="100"
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={tableSettings.records_per_page}
value={tableSettings.records_per_page.toString()}
onChange={(e) => setTableSettings({
...tableSettings,
records_per_page: parseInt(e.target.value) || 20
@@ -65,10 +64,9 @@ export default function GeneralSettings() {
<div>
<Label htmlFor="default_sort">Default Sort Field</Label>
<input
<InputField
id="default_sort"
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={tableSettings.default_sort}
onChange={(e) => setTableSettings({
...tableSettings,
@@ -79,18 +77,17 @@ export default function GeneralSettings() {
<div>
<Label htmlFor="default_sort_direction">Default Sort Direction</Label>
<select
id="default_sort_direction"
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 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:focus:border-brand-800"
<Select
options={[
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
]}
value={tableSettings.default_sort_direction}
onChange={(e) => setTableSettings({
onChange={(value) => setTableSettings({
...tableSettings,
default_sort_direction: e.target.value as 'asc' | 'desc'
default_sort_direction: value as 'asc' | 'desc'
})}
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
/>
</div>
</div>
</div>

View File

@@ -5,6 +5,8 @@ import FormModal, { FormField } from '../../components/common/FormModal';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Alert from '../../components/ui/alert/Alert';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
import {
fetchSites,
createSite,
@@ -475,21 +477,20 @@ export default function Sites() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Industry
</label>
<select
<Select
options={[
{ value: '', label: 'Select an industry...' },
...industries.map((industry) => ({
value: industry.slug,
label: industry.name,
})),
]}
value={selectedIndustry}
onChange={(e) => {
setSelectedIndustry(e.target.value);
onChange={(value) => {
setSelectedIndustry(value);
setSelectedSectors([]); // Reset sectors when industry changes
}}
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"
>
<option value="">Select an industry...</option>
{industries.map((industry) => (
<option key={industry.slug} value={industry.slug}>
{industry.name}
</option>
))}
</select>
/>
{selectedIndustry && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{industries.find(i => i.slug === selectedIndustry)?.description}
@@ -504,15 +505,14 @@ export default function Sites() {
</label>
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
{getIndustrySectors().map((sector) => (
<label
<div
key={sector.slug}
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
>
<input
type="checkbox"
<Checkbox
checked={selectedSectors.includes(sector.slug)}
onChange={(e) => {
if (e.target.checked) {
onChange={(checked) => {
if (checked) {
if (selectedSectors.length >= 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
@@ -522,17 +522,18 @@ export default function Sites() {
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
}
}}
className="mt-1 h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
label={
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
{sector.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{sector.description}
</div>
</div>
}
/>
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
{sector.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{sector.description}
</div>
</div>
</label>
</div>
))}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">

View File

@@ -9,6 +9,8 @@ import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { SearchIcon } from '../../icons';
@@ -140,63 +142,50 @@ export default function SiteContentManager() {
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
<InputField
placeholder="Search content..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white"
className="pl-10"
/>
</div>
<select
value={statusFilter}
onChange={(e) => {
setStatusFilter(e.target.value);
<Select
options={STATUS_OPTIONS}
defaultValue={statusFilter}
onChange={(value) => {
setStatusFilter(value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
/>
<select
value={sourceFilter}
onChange={(e) => {
setSourceFilter(e.target.value);
<Select
options={SOURCE_OPTIONS}
defaultValue={sourceFilter}
onChange={(value) => {
setSourceFilter(value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{SOURCE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
/>
<select
value={`${sortBy}-${sortDirection}`}
onChange={(e) => {
const [field, direction] = e.target.value.split('-');
<Select
options={[
{ value: 'created_at-desc', label: 'Newest First' },
{ value: 'created_at-asc', label: 'Oldest First' },
{ value: 'updated_at-desc', label: 'Recently Updated' },
{ value: 'title-asc', label: 'Title A-Z' },
{ value: 'title-desc', label: 'Title Z-A' },
]}
defaultValue={`${sortBy}-${sortDirection}`}
onChange={(value) => {
const [field, direction] = value.split('-');
setSortBy(field as typeof sortBy);
setSortDirection(direction as 'asc' | 'desc');
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
<option value="created_at-desc">Newest First</option>
<option value="created_at-asc">Oldest First</option>
<option value="updated_at-desc">Recently Updated</option>
<option value="title-asc">Title A-Z</option>
<option value="title-desc">Title Z-A</option>
</select>
/>
</div>
</Card>

View File

@@ -263,9 +263,11 @@ export default function SiteDashboard() {
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Common site management tasks">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<button
<Button
onClick={() => navigate(`/sites/${siteId}/pages`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<PageIcon className="h-6 w-6" />
@@ -275,11 +277,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View and edit pages</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/content`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
<FileIcon className="h-6 w-6" />
@@ -289,11 +293,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View and edit content</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-success)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
<PlugInIcon className="h-6 w-6" />
@@ -303,11 +309,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">Manage connections</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-purple)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/sync`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
<BoltIcon className="h-6 w-6" />
@@ -317,11 +325,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View sync status</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-warning)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/deploy`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<ArrowUpIcon className="h-6 w-6" />
@@ -331,11 +341,13 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">Deploy to production</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</button>
</Button>
<button
<Button
onClick={() => navigate(`/sites/${siteId}/publishing-queue`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group"
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white shadow-lg">
<ClockIcon className="h-6 w-6" />
@@ -345,7 +357,7 @@ export default function SiteDashboard() {
<p className="text-sm text-gray-600">View scheduled content</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
</button>
</Button>
</div>
</ComponentCard>

View File

@@ -13,6 +13,8 @@ import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
import Alert from '../../components/ui/alert/Alert';
import Switch from '../../components/form/switch/Switch';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import ViewToggle from '../../components/common/ViewToggle';
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
import {
@@ -605,40 +607,38 @@ export default function SiteList() {
>
<div className="flex flex-nowrap gap-3 items-center justify-between w-full">
<div className="flex flex-nowrap gap-3 items-center flex-1 min-w-0 w-full">
<input
type="text"
placeholder="Search sites..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 min-w-[200px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
<select
value={siteTypeFilter}
onChange={(e) => setSiteTypeFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
>
{SITE_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<select
value={hostingTypeFilter}
onChange={(e) => setHostingTypeFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
>
{HOSTING_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<div className="flex-1 min-w-[200px]">
<InputField
type="text"
placeholder="Search sites..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex-1 min-w-[140px]">
<Select
options={SITE_TYPES}
placeholder="Show All Types"
defaultValue={siteTypeFilter}
onChange={(val) => setSiteTypeFilter(val)}
/>
</div>
<div className="flex-1 min-w-[140px]">
<Select
options={HOSTING_TYPES}
placeholder="Show All Hosting"
defaultValue={hostingTypeFilter}
onChange={(val) => setHostingTypeFilter(val)}
/>
</div>
<div className="flex-1 min-w-[140px]">
<Select
options={STATUS_OPTIONS}
placeholder="Show All Status"
defaultValue={statusFilter}
onChange={(val) => setStatusFilter(val)}
/>
</div>
</div>
{hasActiveFilters && (
<Button

View File

@@ -76,7 +76,8 @@ const DraggablePageItem: React.FC<{
e.stopPropagation();
onSelect(page.id);
}}
className="cursor-pointer"
className="cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
aria-label={isSelected ? 'Deselect page' : 'Select page'}
>
{isSelected ? (
<CheckLineIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
@@ -365,8 +366,9 @@ export default function PageManager() {
<Card className="p-6">
<div className="mb-4 flex items-center justify-between">
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={handleSelectAll}
className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
@@ -376,7 +378,7 @@ export default function PageManager() {
<div className="w-5 h-5 border-2 border-gray-400 rounded" />
)}
<span>Select All</span>
</button>
</Button>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag and drop to reorder pages
</p>

View File

@@ -11,6 +11,7 @@ import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import TextArea from '../../components/form/input/TextArea';
import InputField from '../../components/form/input/InputField';
import SelectDropdown from '../../components/form/SelectDropdown';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api';
@@ -266,51 +267,39 @@ export default function PostEditor() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant={activeTab === 'content' ? 'primary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('content')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'content'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<FileTextIcon className="w-4 h-4 inline mr-2" />
<FileTextIcon className="w-4 h-4" />
Content
</button>
<button
type="button"
</Button>
<Button
variant={activeTab === 'taxonomy' ? 'primary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('taxonomy')}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'taxonomy'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<TagIcon className="w-4 h-4 inline mr-2" />
<TagIcon className="w-4 h-4" />
Taxonomy & Cluster
</button>
</Button>
{content.id && (
<button
type="button"
<Button
variant={activeTab === 'validation' ? 'primary' : 'ghost'}
size="sm"
onClick={() => {
setActiveTab('validation');
loadValidation();
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'validation'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<CheckCircleIcon className="w-4 h-4 inline mr-2" />
<CheckCircleIcon className="w-4 h-4" />
Validation
{validationResult && !validationResult.is_valid && (
<span className="ml-2 px-2 py-0.5 text-xs bg-error-100 dark:bg-error-900 text-error-600 dark:text-error-400 rounded-full">
{validationResult.validation_errors.length}
</span>
)}
</button>
</Button>
)}
</div>
</div>
@@ -322,12 +311,12 @@ export default function PostEditor() {
<div className="space-y-4">
<div>
<Label>Title *</Label>
<input
<InputField
type="text"
value={content.title}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="Enter post title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
className="mt-1"
/>
</div>

View File

@@ -10,6 +10,8 @@ import PageHeader from '../../components/common/PageHeader';
import ComponentCard from '../../components/common/ComponentCard';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import IconButton from '../../components/ui/button/IconButton';
import { ButtonGroup, ButtonGroupItem } from '../../components/ui/button-group/ButtonGroup';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchContent, Content } from '../../services/api';
import {
@@ -277,30 +279,22 @@ export default function PublishingQueue() {
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{queueItems.length} items in queue
</h2>
<div className="flex items-center gap-2 bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
<button
<ButtonGroup>
<ButtonGroupItem
isActive={viewMode === 'list'}
onClick={() => setViewMode('list')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'list'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<ListIcon className="w-4 h-4" />
<ListIcon className="w-4 h-4 mr-1.5" />
List
</button>
<button
</ButtonGroupItem>
<ButtonGroupItem
isActive={viewMode === 'calendar'}
onClick={() => setViewMode('calendar')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'calendar'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<CalendarIcon className="w-4 h-4" />
<CalendarIcon className="w-4 h-4 mr-1.5" />
Calendar
</button>
</div>
</ButtonGroupItem>
</ButtonGroup>
</div>
{/* Queue Content */}
@@ -361,27 +355,30 @@ export default function PublishingQueue() {
{/* Actions */}
<div className="flex items-center gap-1">
<button
<IconButton
icon={<EyeIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleViewContent(item)}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
title="View content"
>
<EyeIcon className="w-4 h-4" />
</button>
<button
/>
<IconButton
icon={item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handlePauseItem(item)}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
title={item.isPaused ? 'Resume' : 'Pause'}
>
{item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
</button>
<button
/>
<IconButton
icon={<TrashBinIcon className="w-4 h-4" />}
variant="ghost"
tone="danger"
size="sm"
onClick={() => handleRemoveFromQueue(item)}
className="p-2 text-error-500 hover:text-error-700 dark:text-error-400 dark:hover:text-error-300 rounded-lg hover:bg-error-50 dark:hover:bg-error-900/20"
title="Remove from queue"
>
<TrashBinIcon className="w-4 h-4" />
</button>
/>
</div>
</div>
))}

View File

@@ -9,10 +9,14 @@ import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import IconButton from '../../components/ui/button/IconButton';
import Label from '../../components/form/Label';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import SelectDropdown from '../../components/form/SelectDropdown';
import Checkbox from '../../components/form/input/Checkbox';
import TextArea from '../../components/form/input/TextArea';
import Switch from '../../components/form/switch/Switch';
import { useToast } from '../../components/ui/toast/ToastContainer';
import {
fetchAPI,
@@ -23,7 +27,7 @@ import {
} from '../../services/api';
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
import { integrationApi, SiteIntegration } from '../../services/integration.api';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons';
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon } from '../../icons';
import Badge from '../../components/ui/badge/Badge';
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
@@ -554,10 +558,11 @@ export default function SiteSettings() {
{/* Site Selector - Only show if more than 1 site */}
{!sitesLoading && sites.length > 1 && (
<div className="relative inline-block">
<button
<Button
ref={siteSelectorRef}
onClick={() => setIsSiteSelectorOpen(!isSiteSelectorOpen)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-brand-200 rounded-lg hover:bg-brand-50 hover:border-brand-300 dark:bg-gray-800 dark:text-gray-300 dark:border-brand-700/50 dark:hover:bg-brand-500/10 dark:hover:border-brand-600/50 transition-colors"
variant="outline"
className="flex items-center gap-2"
aria-label="Switch site"
>
<span className="flex items-center gap-2">
@@ -565,7 +570,7 @@ export default function SiteSettings() {
<span className="max-w-[150px] truncate">{site?.name || 'Select Site'}</span>
</span>
<ChevronDownIcon className={`w-4 h-4 text-brand-500 dark:text-brand-400 transition-transform ${isSiteSelectorOpen ? 'rotate-180' : ''}`} />
</button>
</Button>
<Dropdown
isOpen={isSiteSelectorOpen}
onClose={() => setIsSiteSelectorOpen(false)}
@@ -605,13 +610,13 @@ export default function SiteSettings() {
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
type="button"
<Button
variant="ghost"
onClick={() => {
setActiveTab('general');
navigate(`/sites/${siteId}/settings`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'general'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -619,14 +624,14 @@ export default function SiteSettings() {
>
<GridIcon className="w-4 h-4 inline mr-2" />
General
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('integrations');
navigate(`/sites/${siteId}/settings?tab=integrations`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'integrations'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -634,14 +639,14 @@ export default function SiteSettings() {
>
<PlugInIcon className="w-4 h-4 inline mr-2" />
Integrations
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('publishing');
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'publishing'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -649,15 +654,15 @@ export default function SiteSettings() {
>
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
Publishing
</button>
</Button>
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && (
<button
type="button"
<Button
variant="ghost"
onClick={() => {
setActiveTab('content-types');
navigate(`/sites/${siteId}/settings?tab=content-types`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
activeTab === 'content-types'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
@@ -665,7 +670,7 @@ export default function SiteSettings() {
>
<FileIcon className="w-4 h-4 inline mr-2" />
Content Types
</button>
</Button>
)}
</div>
</div>
@@ -696,17 +701,15 @@ export default function SiteSettings() {
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
<Switch
label=""
checked={publishingSettings.auto_approval_enabled}
onChange={(e) => {
const newSettings = { ...publishingSettings, auto_approval_enabled: e.target.checked };
onChange={(checked) => {
const newSettings = { ...publishingSettings, auto_approval_enabled: checked };
setPublishingSettings(newSettings);
savePublishingSettings({ auto_approval_enabled: e.target.checked });
savePublishingSettings({ auto_approval_enabled: checked });
}}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
</label>
</div>
</div>
@@ -721,17 +724,15 @@ export default function SiteSettings() {
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
<Switch
label=""
checked={publishingSettings.auto_publish_enabled}
onChange={(e) => {
const newSettings = { ...publishingSettings, auto_publish_enabled: e.target.checked };
onChange={(checked) => {
const newSettings = { ...publishingSettings, auto_publish_enabled: checked };
setPublishingSettings(newSettings);
savePublishingSettings({ auto_publish_enabled: e.target.checked });
savePublishingSettings({ auto_publish_enabled: checked });
}}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-300 dark:peer-focus:ring-brand-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-600"></div>
</label>
</div>
</div>
@@ -745,7 +746,7 @@ export default function SiteSettings() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label>Daily Limit</Label>
<input
<InputField
type="number"
min="1"
max="50"
@@ -754,14 +755,12 @@ export default function SiteSettings() {
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
}}
onBlur={() => savePublishingSettings({ daily_publish_limit: publishingSettings.daily_publish_limit })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
</div>
<div>
<Label>Weekly Limit</Label>
<input
<InputField
type="number"
min="1"
max="200"
@@ -770,14 +769,12 @@ export default function SiteSettings() {
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
}}
onBlur={() => savePublishingSettings({ weekly_publish_limit: publishingSettings.weekly_publish_limit })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
</div>
<div>
<Label>Monthly Limit</Label>
<input
<InputField
type="number"
min="1"
max="500"
@@ -786,8 +783,6 @@ export default function SiteSettings() {
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
}}
onBlur={() => savePublishingSettings({ monthly_publish_limit: publishingSettings.monthly_publish_limit })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
</div>
@@ -810,9 +805,11 @@ export default function SiteSettings() {
{ value: 'sat', label: 'Sat' },
{ value: 'sun', label: 'Sun' },
].map((day) => (
<button
<Button
key={day.value}
type="button"
variant={(publishingSettings.publish_days || []).includes(day.value) ? 'primary' : 'outline'}
tone="brand"
size="sm"
onClick={() => {
const currentDays = publishingSettings.publish_days || [];
const newDays = currentDays.includes(day.value)
@@ -821,14 +818,9 @@ export default function SiteSettings() {
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
savePublishingSettings({ publish_days: newDays });
}}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
(publishingSettings.publish_days || []).includes(day.value)
? 'bg-brand-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{day.label}
</button>
</Button>
))}
</div>
</div>
@@ -842,7 +834,7 @@ export default function SiteSettings() {
<div className="space-y-3">
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
<div key={index} className="flex items-center gap-2">
<input
<InputField
type="time"
value={time}
onChange={(e) => {
@@ -850,40 +842,36 @@ export default function SiteSettings() {
newSlots[index] = e.target.value;
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
}}
onBlur={() => savePublishingSettings({ publish_time_slots: publishingSettings.publish_time_slots })}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-brand-500 focus:border-brand-500"
/>
{(publishingSettings.publish_time_slots || []).length > 1 && (
<button
type="button"
<IconButton
icon={<CloseIcon className="w-5 h-5" />}
variant="ghost"
tone="danger"
size="sm"
title="Remove time slot"
onClick={() => {
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
savePublishingSettings({ publish_time_slots: newSlots });
}}
className="p-2 text-gray-400 hover:text-error-500 transition-colors"
>
<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>
))}
<button
type="button"
<Button
variant="ghost"
tone="brand"
size="sm"
startIcon={<PlusIcon className="w-4 h-4" />}
onClick={() => {
const newSlots = [...(publishingSettings.publish_time_slots || []), '12:00'];
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
savePublishingSettings({ publish_time_slots: newSlots });
}}
className="text-sm text-brand-600 hover:text-brand-700 font-medium flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Time Slot
</button>
</Button>
</div>
</div>
@@ -1084,32 +1072,29 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>Site Name</Label>
<input
<InputField
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Slug</Label>
<input
<InputField
type="text"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Site URL</Label>
<input
<InputField
type="text"
value={formData.site_url}
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
placeholder="https://example.com"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1134,7 +1119,7 @@ export default function SiteSettings() {
<div>
<Checkbox
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
onChange={(checked) => setFormData({ ...formData, is_active: checked })}
label="Active"
/>
</div>
@@ -1150,13 +1135,12 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>Meta Title</Label>
<input
<InputField
type="text"
value={formData.meta_title}
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
placeholder="SEO title (recommended: 50-60 characters)"
maxLength={60}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
max="60"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formData.meta_title.length}/60 characters
@@ -1180,12 +1164,11 @@ export default function SiteSettings() {
<div>
<Label>Meta Keywords (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.meta_keywords}
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
placeholder="keyword1, keyword2, keyword3"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Separate keywords with commas
@@ -1203,12 +1186,11 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>OG Title</Label>
<input
<InputField
type="text"
value={formData.og_title}
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
placeholder="Open Graph title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1225,12 +1207,11 @@ export default function SiteSettings() {
<div>
<Label>OG Image URL</Label>
<input
<InputField
type="url"
value={formData.og_image}
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
placeholder="https://example.com/image.jpg"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Recommended: 1200x630px image
@@ -1253,12 +1234,11 @@ export default function SiteSettings() {
<div>
<Label>OG Site Name</Label>
<input
<InputField
type="text"
value={formData.og_site_name}
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
placeholder="Site name for social sharing"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
@@ -1288,12 +1268,11 @@ export default function SiteSettings() {
<div>
<Label>Schema Name</Label>
<input
<InputField
type="text"
value={formData.schema_name}
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
placeholder="Organization name"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1310,34 +1289,31 @@ export default function SiteSettings() {
<div>
<Label>Schema URL</Label>
<input
<InputField
type="url"
value={formData.schema_url}
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
placeholder="https://example.com"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Schema Logo URL</Label>
<input
<InputField
type="url"
value={formData.schema_logo}
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
placeholder="https://example.com/logo.png"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Same As URLs (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.schema_same_as}
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
placeholder="https://facebook.com/page, https://twitter.com/page"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Social media profiles and other related URLs
@@ -1359,21 +1335,18 @@ export default function SiteSettings() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Industry
</label>
<select
value={selectedIndustry}
onChange={(e) => {
setSelectedIndustry(e.target.value);
<Select
options={industries.map((industry) => ({
value: industry.slug,
label: industry.name,
}))}
placeholder="Select an industry..."
defaultValue={selectedIndustry}
onChange={(value) => {
setSelectedIndustry(value);
setSelectedSectors([]);
}}
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"
>
<option value="">Select an industry...</option>
{industries.map((industry) => (
<option key={industry.slug} value={industry.slug}>
{industry.name}
</option>
))}
</select>
/>
{selectedIndustry && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{industries.find(i => i.slug === selectedIndustry)?.description}
@@ -1388,15 +1361,14 @@ export default function SiteSettings() {
</label>
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
{getIndustrySectors().map((sector) => (
<label
<div
key={sector.slug}
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
>
<input
type="checkbox"
<Checkbox
checked={selectedSectors.includes(sector.slug)}
onChange={(e) => {
if (e.target.checked) {
onChange={(checked) => {
if (checked) {
if (selectedSectors.length >= 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
@@ -1406,7 +1378,6 @@ export default function SiteSettings() {
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
}
}}
className="mt-1 h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
/>
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
@@ -1416,7 +1387,7 @@ export default function SiteSettings() {
{sector.description}
</div>
</div>
</label>
</div>
))}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
@@ -1447,13 +1418,12 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>Meta Title</Label>
<input
<InputField
type="text"
value={formData.meta_title}
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
placeholder="SEO title (recommended: 50-60 characters)"
maxLength={60}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
max="60"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formData.meta_title.length}/60 characters
@@ -1477,12 +1447,11 @@ export default function SiteSettings() {
<div>
<Label>Meta Keywords (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.meta_keywords}
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
placeholder="keyword1, keyword2, keyword3"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Separate keywords with commas
@@ -1498,12 +1467,11 @@ export default function SiteSettings() {
<div className="space-y-4">
<div>
<Label>OG Title</Label>
<input
<InputField
type="text"
value={formData.og_title}
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
placeholder="Open Graph title"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1520,12 +1488,11 @@ export default function SiteSettings() {
<div>
<Label>OG Image URL</Label>
<input
<InputField
type="url"
value={formData.og_image}
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
placeholder="https://example.com/image.jpg"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Recommended: 1200x630px image
@@ -1542,18 +1509,17 @@ export default function SiteSettings() {
{ value: 'product', label: 'Product' },
]}
value={formData.og_type}
onChange={(e) => setFormData({ ...formData, og_type: e.target.value })}
onChange={(value) => setFormData({ ...formData, og_type: value })}
/>
</div>
<div>
<Label>OG Site Name</Label>
<input
<InputField
type="text"
value={formData.og_site_name}
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
placeholder="Site name for social sharing"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
@@ -1575,18 +1541,17 @@ export default function SiteSettings() {
{ value: 'NGO', label: 'NGO' },
]}
value={formData.schema_type}
onChange={(e) => setFormData({ ...formData, schema_type: e.target.value })}
onChange={(value) => setFormData({ ...formData, schema_type: value })}
/>
</div>
<div>
<Label>Schema Name</Label>
<input
<InputField
type="text"
value={formData.schema_name}
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
placeholder="Organization name"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
@@ -1603,34 +1568,31 @@ export default function SiteSettings() {
<div>
<Label>Schema URL</Label>
<input
<InputField
type="url"
value={formData.schema_url}
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
placeholder="https://example.com"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Schema Logo URL</Label>
<input
<InputField
type="url"
value={formData.schema_logo}
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
placeholder="https://example.com/logo.png"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<Label>Same As URLs (comma-separated)</Label>
<input
<InputField
type="text"
value={formData.schema_same_as}
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
placeholder="https://facebook.com/page, https://twitter.com/page"
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Social media profiles and other related URLs

View File

@@ -287,9 +287,10 @@ export default function SyncDashboard() {
{/* Mismatches Section */}
{totalMismatches > 0 && (
<Card className="p-6 mb-6">
<button
<Button
onClick={() => setShowMismatches(!showMismatches)}
className="w-full flex items-center justify-between mb-4"
variant="ghost"
className="w-full flex items-center justify-between mb-4 px-0 hover:bg-transparent"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Mismatches ({totalMismatches})
@@ -299,7 +300,7 @@ export default function SyncDashboard() {
) : (
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
)}
</button>
</Button>
{showMismatches && mismatches && (
<div className="space-y-4">
@@ -412,9 +413,10 @@ export default function SyncDashboard() {
{/* Sync Logs */}
<Card className="p-6">
<button
<Button
onClick={() => setShowLogs(!showLogs)}
className="w-full flex items-center justify-between mb-4"
variant="ghost"
className="w-full flex items-center justify-between mb-4 px-0 hover:bg-transparent"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Sync History ({logs.length})
@@ -424,7 +426,7 @@ export default function SyncDashboard() {
) : (
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
)}
</button>
</Button>
{showLogs && (
<div className="space-y-2">

View File

@@ -0,0 +1,737 @@
/**
* UI Elements Showcase Page
*
* Single source of truth for all UI components from components/ui/
* This page is non-indexable and serves as documentation/reference
*
* Route: /ui-elements
*/
import React, { useState } from 'react';
import PageMeta from '../components/common/PageMeta';
// ============================================================================
// UI COMPONENTS - All imports from components/ui/ (SINGLE SOURCE OF TRUTH)
// ============================================================================
// Accordion
import { Accordion, AccordionItem } from '../components/ui/accordion/Accordion';
// Alert
import Alert from '../components/ui/alert/Alert';
import AlertModal from '../components/ui/alert/AlertModal';
// Avatar
import Avatar from '../components/ui/avatar/Avatar';
// Badge
import Badge from '../components/ui/badge/Badge';
// Breadcrumb
import { Breadcrumb } from '../components/ui/breadcrumb/Breadcrumb';
// Button
import Button from '../components/ui/button/Button';
import ButtonWithTooltip from '../components/ui/button/ButtonWithTooltip';
import IconButton from '../components/ui/button/IconButton';
// Button Group
import { ButtonGroup, ButtonGroupItem } from '../components/ui/button-group/ButtonGroup';
// Card
import { Card, CardImage, CardTitle, CardContent, CardDescription, CardAction, CardIcon, HorizontalCard } from '../components/ui/card/Card';
// DataView
import { DataView, DataViewHeader, DataViewToolbar, DataViewEmptyState } from '../components/ui/dataview/DataView';
// Dropdown
import { Dropdown } from '../components/ui/dropdown/Dropdown';
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
// List
import { List, ListItem, ListDot, ListIcon, ListCheckboxItem, ListRadioItem } from '../components/ui/list/List';
// Modal
import { Modal } from '../components/ui/modal';
// Pagination
import { Pagination } from '../components/ui/pagination/Pagination';
import { CompactPagination } from '../components/ui/pagination/CompactPagination';
// Progress
import { ProgressBar } from '../components/ui/progress/ProgressBar';
// Ribbon
import { Ribbon } from '../components/ui/ribbon/Ribbon';
// Spinner
import { Spinner } from '../components/ui/spinner/Spinner';
// Table
import { Table, TableHeader, TableBody, TableRow, TableCell } from '../components/ui/table';
// Tabs
import { Tabs, TabList, Tab, TabPanel } from '../components/ui/tabs/Tabs';
// Toast
import { useToast } from '../components/ui/toast/ToastContainer';
// Tooltip
import { Tooltip } from '../components/ui/tooltip/Tooltip';
import { EnhancedTooltip } from '../components/ui/tooltip/EnhancedTooltip';
// Icons for demos
import {
CheckCircleIcon,
CloseIcon,
PlusIcon,
ArrowRightIcon,
GridIcon,
FileIcon,
BoltIcon,
} from '../icons';
// Component categories for navigation
const CATEGORIES = [
{ id: 'buttons', label: 'Buttons' },
{ id: 'badges', label: 'Badges' },
{ id: 'cards', label: 'Cards' },
{ id: 'alerts', label: 'Alerts' },
{ id: 'modals', label: 'Modals' },
{ id: 'tables', label: 'Tables' },
{ id: 'tabs', label: 'Tabs' },
{ id: 'accordion', label: 'Accordion' },
{ id: 'dropdown', label: 'Dropdown' },
{ id: 'pagination', label: 'Pagination' },
{ id: 'progress', label: 'Progress' },
{ id: 'spinner', label: 'Spinner' },
{ id: 'avatar', label: 'Avatar' },
{ id: 'breadcrumb', label: 'Breadcrumb' },
{ id: 'list', label: 'List' },
{ id: 'tooltip', label: 'Tooltip' },
{ id: 'ribbon', label: 'Ribbon' },
{ id: 'toast', label: 'Toast' },
{ id: 'dataview', label: 'DataView' },
];
// Section wrapper component
function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
return (
<section id={id} className="mb-12 scroll-mt-24">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
{title}
</h2>
<div className="space-y-6">
{children}
</div>
</section>
);
}
// Demo box component
function DemoBox({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<p className="text-xs font-mono text-gray-500 dark:text-gray-400 mb-3">{label}</p>
<div className="flex flex-wrap items-center gap-3">
{children}
</div>
</div>
);
}
export default function UIElements() {
const toast = useToast();
// Modal states
const [showModal, setShowModal] = useState(false);
const [showAlertModal, setShowAlertModal] = useState(false);
// Dropdown state
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = React.useRef<HTMLButtonElement>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// Tabs state
const [activeTab, setActiveTab] = useState('tab1');
// List state
const [checkboxChecked, setCheckboxChecked] = useState(false);
const [radioValue, setRadioValue] = useState('option1');
// Button group state
const [activeButton, setActiveButton] = useState(0);
return (
<>
<PageMeta
title="UI Elements | IGNY8 Design System"
description="Component library reference - Single source of truth for all UI components"
noIndex={true}
/>
<div className="flex gap-8">
{/* Sticky Navigation */}
<nav className="hidden lg:block w-48 shrink-0">
<div className="sticky top-24 space-y-1">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">Components</h3>
{CATEGORIES.map(cat => (
<a
key={cat.id}
href={`#${cat.id}`}
className="block py-1.5 px-3 text-sm text-gray-600 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
>
{cat.label}
</a>
))}
</div>
</nav>
{/* Main Content */}
<div className="flex-1 min-w-0">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
UI Elements Library
</h1>
<p className="text-gray-600 dark:text-gray-400">
Single source of truth for all UI components. All components are imported from <code className="text-sm bg-gray-100 dark:bg-gray-800 px-1 rounded">components/ui/</code>
</p>
</div>
{/* ================================================================ */}
{/* BUTTONS */}
{/* ================================================================ */}
<Section id="buttons" title="Button">
<DemoBox label="variants: primary | secondary | outline | ghost | gradient">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="gradient">Gradient</Button>
</DemoBox>
<DemoBox label="tones: brand | success | warning | danger | neutral">
<Button tone="brand">Brand</Button>
<Button tone="success">Success</Button>
<Button tone="warning">Warning</Button>
<Button tone="danger">Danger</Button>
<Button tone="neutral">Neutral</Button>
</DemoBox>
<DemoBox label="sizes: xs | sm | md | lg | xl | 2xl">
<Button size="xs">XS</Button>
<Button size="sm">SM</Button>
<Button size="md">MD</Button>
<Button size="lg">LG</Button>
<Button size="xl">XL</Button>
</DemoBox>
<DemoBox label="shapes: rounded | pill">
<Button shape="rounded">Rounded</Button>
<Button shape="pill">Pill</Button>
</DemoBox>
<DemoBox label="with icons: startIcon | endIcon">
<Button startIcon={<PlusIcon className="w-4 h-4" />}>Add Item</Button>
<Button endIcon={<ArrowRightIcon className="w-4 h-4" />}>Continue</Button>
</DemoBox>
<DemoBox label="states: disabled | fullWidth">
<Button disabled>Disabled</Button>
</DemoBox>
<DemoBox label="ButtonWithTooltip (shows tooltip when disabled)">
<ButtonWithTooltip disabled tooltip="This action is not available">
Disabled with Tooltip
</ButtonWithTooltip>
</DemoBox>
<DemoBox label="ButtonGroup">
<ButtonGroup>
<ButtonGroupItem isActive={activeButton === 0} onClick={() => setActiveButton(0)}>Day</ButtonGroupItem>
<ButtonGroupItem isActive={activeButton === 1} onClick={() => setActiveButton(1)}>Week</ButtonGroupItem>
<ButtonGroupItem isActive={activeButton === 2} onClick={() => setActiveButton(2)}>Month</ButtonGroupItem>
</ButtonGroup>
</DemoBox>
<DemoBox label="IconButton - variants: solid | outline | ghost">
<IconButton icon={<PlusIcon />} variant="solid" tone="brand" title="Add" />
<IconButton icon={<PlusIcon />} variant="outline" tone="brand" title="Add" />
<IconButton icon={<PlusIcon />} variant="ghost" tone="brand" title="Add" />
</DemoBox>
<DemoBox label="IconButton - sizes: xs | sm | md | lg">
<IconButton icon={<CloseIcon />} size="xs" title="Close" />
<IconButton icon={<CloseIcon />} size="sm" title="Close" />
<IconButton icon={<CloseIcon />} size="md" title="Close" />
<IconButton icon={<CloseIcon />} size="lg" title="Close" />
</DemoBox>
<DemoBox label="IconButton - shapes: rounded | circle">
<IconButton icon={<CheckCircleIcon />} shape="rounded" variant="solid" tone="success" title="Approve" />
<IconButton icon={<CheckCircleIcon />} shape="circle" variant="solid" tone="success" title="Approve" />
</DemoBox>
<DemoBox label="IconButton - tones">
<IconButton icon={<BoltIcon />} variant="ghost" tone="brand" title="Brand" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="success" title="Success" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="warning" title="Warning" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="danger" title="Danger" />
<IconButton icon={<BoltIcon />} variant="ghost" tone="neutral" title="Neutral" />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* BADGES */}
{/* ================================================================ */}
<Section id="badges" title="Badge">
<DemoBox label="variants: solid | soft | outline | light">
<Badge variant="solid" tone="brand">Solid</Badge>
<Badge variant="soft" tone="brand">Soft</Badge>
<Badge variant="outline" tone="brand">Outline</Badge>
<Badge variant="light" tone="brand">Light</Badge>
</DemoBox>
<DemoBox label="tones: brand | success | warning | danger | info | neutral">
<Badge tone="brand">Brand</Badge>
<Badge tone="success">Success</Badge>
<Badge tone="warning">Warning</Badge>
<Badge tone="danger">Danger</Badge>
<Badge tone="info">Info</Badge>
<Badge tone="neutral">Neutral</Badge>
</DemoBox>
<DemoBox label="sizes: xs | sm | md">
<Badge size="xs">XS</Badge>
<Badge size="sm">SM</Badge>
<Badge size="md">MD</Badge>
</DemoBox>
<DemoBox label="with icons: startIcon | endIcon">
<Badge startIcon={<CheckCircleIcon className="w-3 h-3" />} tone="success">Completed</Badge>
<Badge endIcon={<CloseIcon className="w-3 h-3" />} tone="danger">Remove</Badge>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* CARDS */}
{/* ================================================================ */}
<Section id="cards" title="Card">
<DemoBox label="variants: surface | panel | frosted | borderless | gradient">
<Card variant="surface" className="w-48">
<CardContent>Surface Card</CardContent>
</Card>
<Card variant="panel" className="w-48">
<CardContent>Panel Card</CardContent>
</Card>
</DemoBox>
<DemoBox label="padding: none | sm | md | lg">
<Card padding="sm" className="w-48">
<CardContent>Small Padding</CardContent>
</Card>
<Card padding="lg" className="w-48">
<CardContent>Large Padding</CardContent>
</Card>
</DemoBox>
<DemoBox label="Card with components">
<Card className="w-64">
<CardIcon><FileIcon className="w-8 h-8 text-brand-500" /></CardIcon>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description text goes here</CardDescription>
<CardAction>View More</CardAction>
</Card>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* ALERTS */}
{/* ================================================================ */}
<Section id="alerts" title="Alert">
<DemoBox label="variants: success | error | warning | info">
<div className="space-y-3 w-full">
<Alert variant="success" title="Success" message="Operation completed successfully!" />
<Alert variant="error" title="Error" message="Something went wrong. Please try again." />
<Alert variant="warning" title="Warning" message="Please review before continuing." />
<Alert variant="info" title="Info" message="Here's some useful information." />
</div>
</DemoBox>
<DemoBox label="AlertModal">
<Button onClick={() => setShowAlertModal(true)}>Open Alert Modal</Button>
<AlertModal
isOpen={showAlertModal}
onClose={() => setShowAlertModal(false)}
variant="warning"
title="Confirm Action"
message="Are you sure you want to proceed?"
isConfirmation={true}
onConfirm={() => setShowAlertModal(false)}
/>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* MODALS */}
{/* ================================================================ */}
<Section id="modals" title="Modal">
<DemoBox label="Basic Modal">
<Button onClick={() => setShowModal(true)}>Open Modal</Button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<div className="p-6">
<h3 className="text-lg font-semibold mb-4">Modal Title</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
This is modal content. You can put any React components here.
</p>
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => setShowModal(false)}>Cancel</Button>
<Button onClick={() => setShowModal(false)}>Confirm</Button>
</div>
</div>
</Modal>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TABLES */}
{/* ================================================================ */}
<Section id="tables" title="Table">
<DemoBox label="Table components">
<Table className="w-full">
<TableHeader>
<TableRow>
<TableCell isHeader>Name</TableCell>
<TableCell isHeader>Status</TableCell>
<TableCell isHeader>Date</TableCell>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Item One</TableCell>
<TableCell><Badge tone="success">Active</Badge></TableCell>
<TableCell>Jan 1, 2024</TableCell>
</TableRow>
<TableRow>
<TableCell>Item Two</TableCell>
<TableCell><Badge tone="warning">Pending</Badge></TableCell>
<TableCell>Jan 2, 2024</TableCell>
</TableRow>
</TableBody>
</Table>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TABS */}
{/* ================================================================ */}
<Section id="tabs" title="Tabs">
<DemoBox label="Tabs component">
<div className="w-full">
<Tabs defaultTab="tab1" onChange={setActiveTab}>
<TabList>
<Tab tabId="tab1">Tab One</Tab>
<Tab tabId="tab2">Tab Two</Tab>
<Tab tabId="tab3">Tab Three</Tab>
</TabList>
<TabPanel tabId="tab1">
<div className="py-4">Content for Tab One</div>
</TabPanel>
<TabPanel tabId="tab2">
<div className="py-4">Content for Tab Two</div>
</TabPanel>
<TabPanel tabId="tab3">
<div className="py-4">Content for Tab Three</div>
</TabPanel>
</Tabs>
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* ACCORDION */}
{/* ================================================================ */}
<Section id="accordion" title="Accordion">
<DemoBox label="Accordion component">
<div className="w-full">
<Accordion>
<AccordionItem title="Section One" defaultOpen>
Content for section one goes here.
</AccordionItem>
<AccordionItem title="Section Two">
Content for section two goes here.
</AccordionItem>
<AccordionItem title="Section Three">
Content for section three goes here.
</AccordionItem>
</Accordion>
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* DROPDOWN */}
{/* ================================================================ */}
<Section id="dropdown" title="Dropdown">
<DemoBox label="Dropdown component">
<div className="relative">
<Button ref={dropdownRef} onClick={() => setDropdownOpen(!dropdownOpen)}>
Open Dropdown
</Button>
<Dropdown
isOpen={dropdownOpen}
onClose={() => setDropdownOpen(false)}
anchorRef={dropdownRef}
>
<DropdownItem onClick={() => setDropdownOpen(false)}>Option One</DropdownItem>
<DropdownItem onClick={() => setDropdownOpen(false)}>Option Two</DropdownItem>
<DropdownItem onClick={() => setDropdownOpen(false)}>Option Three</DropdownItem>
</Dropdown>
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* PAGINATION */}
{/* ================================================================ */}
<Section id="pagination" title="Pagination">
<DemoBox label="Pagination variants">
<Pagination
currentPage={currentPage}
totalPages={10}
onPageChange={setCurrentPage}
variant="text"
/>
</DemoBox>
<DemoBox label="CompactPagination (with page size)">
<CompactPagination
currentPage={currentPage}
totalPages={10}
pageSize={pageSize}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
/>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* PROGRESS */}
{/* ================================================================ */}
<Section id="progress" title="Progress">
<DemoBox label="ProgressBar colors">
<div className="space-y-3 w-full">
<ProgressBar value={75} color="primary" showLabel />
<ProgressBar value={60} color="success" showLabel />
<ProgressBar value={45} color="warning" showLabel />
<ProgressBar value={30} color="error" showLabel />
</div>
</DemoBox>
<DemoBox label="ProgressBar sizes: sm | md | lg">
<div className="space-y-3 w-full">
<ProgressBar value={50} size="sm" />
<ProgressBar value={50} size="md" />
<ProgressBar value={50} size="lg" />
</div>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* SPINNER */}
{/* ================================================================ */}
<Section id="spinner" title="Spinner">
<DemoBox label="sizes: sm | md | lg">
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
</DemoBox>
<DemoBox label="colors: primary | success | error | warning | info">
<Spinner color="primary" />
<Spinner color="success" />
<Spinner color="error" />
<Spinner color="warning" />
<Spinner color="info" />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* AVATAR */}
{/* ================================================================ */}
<Section id="avatar" title="Avatar">
<DemoBox label="sizes: xsmall | small | medium | large | xlarge | xxlarge">
<Avatar src="/images/user/user-01.jpg" size="xsmall" />
<Avatar src="/images/user/user-01.jpg" size="small" />
<Avatar src="/images/user/user-01.jpg" size="medium" />
<Avatar src="/images/user/user-01.jpg" size="large" />
<Avatar src="/images/user/user-01.jpg" size="xlarge" />
</DemoBox>
<DemoBox label="status: online | offline | busy | none">
<Avatar src="/images/user/user-01.jpg" status="online" />
<Avatar src="/images/user/user-01.jpg" status="offline" />
<Avatar src="/images/user/user-01.jpg" status="busy" />
<Avatar src="/images/user/user-01.jpg" status="none" />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* BREADCRUMB */}
{/* ================================================================ */}
<Section id="breadcrumb" title="Breadcrumb">
<DemoBox label="Breadcrumb component">
<Breadcrumb items={[
{ label: 'Home', path: '/', icon: <GridIcon className="w-4 h-4" /> },
{ label: 'Category', path: '/category' },
{ label: 'Current Page' },
]} />
</DemoBox>
</Section>
{/* ================================================================ */}
{/* LIST */}
{/* ================================================================ */}
<Section id="list" title="List">
<DemoBox label="List variants">
<List variant="unordered">
<ListItem><ListDot />Item One</ListItem>
<ListItem><ListDot />Item Two</ListItem>
<ListItem><ListDot />Item Three</ListItem>
</List>
</DemoBox>
<DemoBox label="ListCheckboxItem">
<List variant="checkbox">
<ListCheckboxItem
id="check1"
label="Checkbox Option"
checked={checkboxChecked}
onChange={setCheckboxChecked}
/>
</List>
</DemoBox>
<DemoBox label="ListRadioItem">
<List variant="radio">
<ListRadioItem
id="radio1"
name="radioGroup"
value="option1"
label="Option One"
checked={radioValue === 'option1'}
onChange={setRadioValue}
/>
<ListRadioItem
id="radio2"
name="radioGroup"
value="option2"
label="Option Two"
checked={radioValue === 'option2'}
onChange={setRadioValue}
/>
</List>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TOOLTIP */}
{/* ================================================================ */}
<Section id="tooltip" title="Tooltip">
<DemoBox label="placements: top | bottom | left | right">
<Tooltip text="Top tooltip" placement="top">
<Button variant="outline" size="sm">Top</Button>
</Tooltip>
<Tooltip text="Bottom tooltip" placement="bottom">
<Button variant="outline" size="sm">Bottom</Button>
</Tooltip>
<Tooltip text="Left tooltip" placement="left">
<Button variant="outline" size="sm">Left</Button>
</Tooltip>
<Tooltip text="Right tooltip" placement="right">
<Button variant="outline" size="sm">Right</Button>
</Tooltip>
</DemoBox>
<DemoBox label="EnhancedTooltip (supports ReactNode content)">
<EnhancedTooltip
content={
<div>
<strong>Rich Content</strong>
<p className="text-xs">With multiple lines</p>
</div>
}
>
<Button variant="outline" size="sm">Hover Me</Button>
</EnhancedTooltip>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* RIBBON */}
{/* ================================================================ */}
<Section id="ribbon" title="Ribbon">
<DemoBox label="Ribbon variants">
<Ribbon text="New" variant="rounded" color="primary">
<Card className="w-48 h-24">
<CardContent>Card with ribbon</CardContent>
</Card>
</Ribbon>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* TOAST */}
{/* ================================================================ */}
<Section id="toast" title="Toast">
<DemoBox label="Toast notifications (via useToast hook)">
<Button onClick={() => toast.success('Success', 'Operation completed!')}>
Show Success
</Button>
<Button onClick={() => toast.error('Error', 'Something went wrong!')} tone="danger">
Show Error
</Button>
<Button onClick={() => toast.warning('Warning', 'Please review!')} tone="warning">
Show Warning
</Button>
<Button onClick={() => toast.info('Info', 'Here is some information')} variant="outline">
Show Info
</Button>
</DemoBox>
</Section>
{/* ================================================================ */}
{/* DATAVIEW */}
{/* ================================================================ */}
<Section id="dataview" title="DataView">
<DemoBox label="DataView container">
<DataView className="w-full">
<DataViewHeader
title="Data View Title"
description="Description of the data view"
actions={<Button size="sm">Action</Button>}
/>
<DataViewToolbar>
<Button size="sm" variant="outline">Filter</Button>
<Button size="sm" variant="outline">Sort</Button>
</DataViewToolbar>
<div className="p-4 text-gray-500">Data content goes here...</div>
</DataView>
</DemoBox>
<DemoBox label="DataViewEmptyState">
<DataViewEmptyState
title="No Data Found"
description="Try adjusting your filters or add new items"
action={<Button size="sm">Add Item</Button>}
/>
</DemoBox>
</Section>
</div>
</div>
</>
);
}

View File

@@ -12,6 +12,9 @@ import {
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useToast } from '../../components/ui/toast/ToastContainer';
@@ -323,40 +326,30 @@ export default function AccountSettingsPage() {
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Name
</label>
<input
<InputField
type="text"
name="name"
label="Account Name"
value={accountForm.name}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Slug
</label>
<input
<InputField
type="text"
label="Account Slug"
value={settings?.slug || ''}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700"
disabled
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Billing Email
</label>
<input
<InputField
type="email"
name="billing_email"
label="Billing Email"
value={accountForm.billing_email}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</Card>
@@ -366,77 +359,59 @@ export default function AccountSettingsPage() {
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 1
</label>
<input
<InputField
type="text"
name="billing_address_line1"
label="Address Line 1"
value={accountForm.billing_address_line1}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 2
</label>
<input
<InputField
type="text"
name="billing_address_line2"
label="Address Line 2"
value={accountForm.billing_address_line2}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
City
</label>
<input
<InputField
type="text"
name="billing_city"
label="City"
value={accountForm.billing_city}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
State/Province
</label>
<input
<InputField
type="text"
name="billing_state"
label="State/Province"
value={accountForm.billing_state}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Postal Code
</label>
<input
<InputField
type="text"
name="billing_postal_code"
label="Postal Code"
value={accountForm.billing_postal_code}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Country
</label>
<input
<InputField
type="text"
name="billing_country"
label="Country"
value={accountForm.billing_country}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
placeholder="US, GB, IN, etc."
/>
</div>
@@ -447,15 +422,12 @@ export default function AccountSettingsPage() {
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h2>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID / VAT Number
</label>
<input
<InputField
type="text"
name="tax_id"
label="Tax ID / VAT Number"
value={accountForm.tax_id}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
placeholder="Optional"
/>
</div>
@@ -485,47 +457,35 @@ export default function AccountSettingsPage() {
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
<InputField
type="text"
label="First Name"
value={profileForm.firstName}
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
<InputField
type="text"
label="Last Name"
value={profileForm.lastName}
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
<InputField
type="email"
label="Email"
value={profileForm.email}
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone Number (optional)
</label>
<input
<InputField
type="tel"
label="Phone Number (optional)"
value={profileForm.phone}
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</div>
@@ -538,33 +498,33 @@ export default function AccountSettingsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Timezone
</label>
<select
value={profileForm.timezone}
onChange={(e) => setProfileForm({ ...profileForm, timezone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="UTC">UTC</option>
<option value="Europe/London">London</option>
<option value="Asia/Kolkata">India</option>
</select>
<Select
options={[
{ value: 'America/New_York', label: 'Eastern Time' },
{ value: 'America/Chicago', label: 'Central Time' },
{ value: 'America/Denver', label: 'Mountain Time' },
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
{ value: 'UTC', label: 'UTC' },
{ value: 'Europe/London', label: 'London' },
{ value: 'Asia/Kolkata', label: 'India' },
]}
defaultValue={profileForm.timezone}
onChange={(value) => setProfileForm({ ...profileForm, timezone: value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Language
</label>
<select
value={profileForm.language}
onChange={(e) => setProfileForm({ ...profileForm, language: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
<Select
options={[
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
]}
defaultValue={profileForm.language}
onChange={(value) => setProfileForm({ ...profileForm, language: value })}
/>
</div>
</div>
</Card>
@@ -582,11 +542,9 @@ export default function AccountSettingsPage() {
Get notified about important changes to your account
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profileForm.emailNotifications}
onChange={(e) => setProfileForm({ ...profileForm, emailNotifications: e.target.checked })}
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
onChange={(checked) => setProfileForm({ ...profileForm, emailNotifications: checked })}
/>
</div>
<div className="flex items-center justify-between">
@@ -596,11 +554,9 @@ export default function AccountSettingsPage() {
Hear about new features and content tips
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profileForm.marketingEmails}
onChange={(e) => setProfileForm({ ...profileForm, marketingEmails: e.target.checked })}
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
onChange={(checked) => setProfileForm({ ...profileForm, marketingEmails: checked })}
/>
</div>
</div>
@@ -769,39 +725,30 @@ export default function AccountSettingsPage() {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email *
</label>
<input
<InputField
type="email"
label="Email *"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="user@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name
</label>
<input
<InputField
type="text"
label="First Name"
value={inviteForm.first_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name
</label>
<input
<InputField
type="text"
label="Last Name"
value={inviteForm.last_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
@@ -854,38 +801,29 @@ export default function AccountSettingsPage() {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Current Password
</label>
<input
<InputField
type="password"
label="Current Password"
value={passwordForm.currentPassword}
onChange={(e) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Enter current password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password
</label>
<input
<InputField
type="password"
label="New Password"
value={passwordForm.newPassword}
onChange={(e) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Enter new password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password
</label>
<input
<InputField
type="password"
label="Confirm New Password"
value={passwordForm.confirmPassword}
onChange={(e) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Confirm new password"
/>
</div>

View File

@@ -16,6 +16,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import SelectDropdown from '../../components/form/SelectDropdown';
import Label from '../../components/form/Label';
import Checkbox from '../../components/form/input/Checkbox';
import TextArea from '../../components/form/input/TextArea';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { BoxCubeIcon } from '../../icons';
@@ -357,11 +358,11 @@ export default function ContentSettingsPage() {
<div className="space-y-6">
<div>
<Label className="mb-2">Append to Every Prompt</Label>
<textarea
<TextArea
value={contentSettings.appendToPrompt}
onChange={(e) => setContentSettings({ ...contentSettings, appendToPrompt: e.target.value })}
onChange={(value) => setContentSettings({ ...contentSettings, appendToPrompt: value })}
placeholder="Add custom instructions that will be included with every content generation request..."
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800 min-h-[120px] resize-y"
rows={5}
/>
<p className="text-xs text-gray-500 mt-1">
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.

View File

@@ -15,6 +15,7 @@ import {
} from '../../icons';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Select from '../../components/form/Select';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useNotificationStore } from '../../store/notificationStore';
@@ -237,19 +238,19 @@ export default function NotificationsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Severity
</label>
<select
value={filters.severity}
onChange={(e) =>
setFilters({ ...filters, severity: e.target.value })
<Select
options={[
{ value: '', label: 'All' },
{ value: 'info', label: 'Info' },
{ value: 'success', label: 'Success' },
{ value: 'warning', label: 'Warning' },
{ value: 'error', label: 'Error' },
]}
defaultValue={filters.severity}
onChange={(value) =>
setFilters({ ...filters, severity: value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
/>
</div>
{/* Type Filter */}
@@ -257,20 +258,19 @@ export default function NotificationsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Type
</label>
<select
value={filters.notification_type}
onChange={(e) =>
setFilters({ ...filters, notification_type: e.target.value })
<Select
options={[
{ value: '', label: 'All' },
...notificationTypes.map((type) => ({
value: type,
label: getTypeLabel(type),
})),
]}
defaultValue={filters.notification_type}
onChange={(value) =>
setFilters({ ...filters, notification_type: value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
{notificationTypes.map((type) => (
<option key={type} value={type}>
{getTypeLabel(type)}
</option>
))}
</select>
/>
</div>
{/* Read Status Filter */}
@@ -278,17 +278,17 @@ export default function NotificationsPage() {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
value={filters.is_read}
onChange={(e) =>
setFilters({ ...filters, is_read: e.target.value })
<Select
options={[
{ value: '', label: 'All' },
{ value: 'false', label: 'Unread' },
{ value: 'true', label: 'Read' },
]}
defaultValue={filters.is_read}
onChange={(value) =>
setFilters({ ...filters, is_read: value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="false">Unread</option>
<option value="true">Read</option>
</select>
/>
</div>
</div>

View File

@@ -6,6 +6,9 @@
import { useState, useEffect } from 'react';
import { AlertCircleIcon, CheckIcon, CreditCardIcon, Building2Icon, WalletIcon, Loader2Icon, ZapIcon } from '../../icons';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import TextArea from '../../components/form/input/TextArea';
import Label from '../../components/form/Label';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import {
@@ -245,33 +248,26 @@ export default function PurchaseCreditsPage() {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Transaction Reference / ID *
</label>
<input
<InputField
label="Transaction Reference / ID *"
type="text"
required
value={manualPaymentData.transaction_reference}
onChange={(e) =>
setManualPaymentData({ ...manualPaymentData, transaction_reference: e.target.value })
}
placeholder="Enter transaction ID or reference number"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Additional Notes (Optional)
</label>
<textarea
<Label className="mb-2">Additional Notes (Optional)</Label>
<TextArea
value={manualPaymentData.notes}
onChange={(e) =>
setManualPaymentData({ ...manualPaymentData, notes: e.target.value })
onChange={(value) =>
setManualPaymentData({ ...manualPaymentData, notes: value })
}
placeholder="Any additional information..."
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]"
/>
</div>

View File

@@ -6,6 +6,10 @@
import { useState } from 'react';
import { SaveIcon, UserIcon, MailIcon, LockIcon, Loader2Icon } from '../../icons';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
export default function ProfileSettingsPage() {
const [saving, setSaving] = useState(false);
@@ -38,14 +42,15 @@ export default function ProfileSettingsPage() {
Update your personal settings - Your name, preferences, and notification choices
</p>
</div>
<button
<Button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50"
variant="primary"
tone="brand"
>
{saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
{saving ? 'Saving...' : '✓ Save My Settings'}
</button>
</Button>
</div>
<div className="space-y-6">
@@ -53,47 +58,35 @@ export default function ProfileSettingsPage() {
<h2 className="text-lg font-semibold mb-4">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
<InputField
type="text"
label="First Name"
value={profile.firstName}
onChange={(e) => setProfile({ ...profile, firstName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
<InputField
type="text"
label="Last Name"
value={profile.lastName}
onChange={(e) => setProfile({ ...profile, lastName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
<InputField
type="email"
label="Email"
value={profile.email}
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone Number (optional)
</label>
<input
<InputField
type="tel"
label="Phone Number (optional)"
value={profile.phone}
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
/>
</div>
</div>
@@ -103,34 +96,30 @@ export default function ProfileSettingsPage() {
<h2 className="text-lg font-semibold mb-4">How You Like It</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Timezone
</label>
<select
value={profile.timezone}
onChange={(e) => setProfile({ ...profile, timezone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="UTC">UTC</option>
</select>
<Select
label="Your Timezone"
options={[
{ value: 'America/New_York', label: 'Eastern Time' },
{ value: 'America/Chicago', label: 'Central Time' },
{ value: 'America/Denver', label: 'Mountain Time' },
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
{ value: 'UTC', label: 'UTC' },
]}
defaultValue={profile.timezone}
onChange={(val) => setProfile({ ...profile, timezone: val })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Language
</label>
<select
value={profile.language}
onChange={(e) => setProfile({ ...profile, language: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-brand-500 dark:bg-gray-800"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
<Select
label="Language"
options={[
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
]}
defaultValue={profile.language}
onChange={(val) => setProfile({ ...profile, language: val })}
/>
</div>
</div>
</Card>
@@ -148,11 +137,9 @@ export default function ProfileSettingsPage() {
Get notified about important changes to your account
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profile.emailNotifications}
onChange={(e) => setProfile({ ...profile, emailNotifications: e.target.checked })}
className="w-5 h-5 text-brand-600 rounded focus:ring-brand-500"
onChange={(checked) => setProfile({ ...profile, emailNotifications: checked })}
/>
</div>
<div className="flex items-center justify-between">
@@ -162,11 +149,9 @@ export default function ProfileSettingsPage() {
Hear about new features and content tips
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={profile.marketingEmails}
onChange={(e) => setProfile({ ...profile, marketingEmails: e.target.checked })}
className="w-5 h-5 text-brand-600 rounded focus:ring-brand-500"
onChange={(checked) => setProfile({ ...profile, marketingEmails: checked })}
/>
</div>
</div>
@@ -177,9 +162,9 @@ export default function ProfileSettingsPage() {
<LockIcon className="w-5 h-5" />
Security
</h2>
<button className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800">
<Button variant="outline" tone="neutral">
Change Password
</button>
</Button>
</Card>
</div>
</div>

View File

@@ -19,6 +19,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { Content, fetchImages, ImageRecord } from '../services/api';
import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon, PencilIcon, ImageIcon, BoltIcon } from '../icons';
import { useNavigate } from 'react-router-dom';
import Button from '../components/ui/button/Button';
interface ContentViewTemplateProps {
content: Content | null;
@@ -759,13 +760,13 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">The content you're looking for doesn't exist or has been deleted.</p>
{onBack && (
<button
<Button
variant="primary"
onClick={onBack}
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
<ArrowLeftIcon className="w-4 h-4" />
Back to Content List
</button>
</Button>
)}
</div>
</div>
@@ -824,13 +825,14 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
{/* Back Button */}
{onBack && (
<button
<Button
variant="ghost"
onClick={onBack}
className="mb-6 inline-flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
className="mb-6"
>
<ArrowLeftIcon className="w-5 h-5" />
<span className="font-medium">Back to Content</span>
</button>
<ArrowLeftIcon className="w-4 h-4" />
Back to Content List
</Button>
)}
{/* Main Content Card */}
@@ -1025,40 +1027,42 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
{/* Draft status: Show Edit Content + Generate Images */}
{content.status.toLowerCase() === 'draft' && (
<>
<button
<Button
variant="primary"
onClick={() => navigate(`/sites/${content.site_id}/posts/${content.id}/edit`)}
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 hover:bg-brand-600 text-white rounded-lg font-medium transition-colors"
>
<PencilIcon className="w-4 h-4" />
Edit Content
</button>
<button
</Button>
<Button
variant="primary"
tone="brand"
onClick={() => navigate(`/writer/images?contentId=${content.id}`)}
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg font-medium transition-colors"
>
<ImageIcon className="w-4 h-4" />
Generate Images
</button>
</Button>
</>
)}
{/* Review status: Show Edit Content + Publish */}
{content.status.toLowerCase() === 'review' && (
<>
<button
<Button
variant="primary"
onClick={() => navigate(`/sites/${content.site_id}/posts/${content.id}/edit`)}
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 hover:bg-brand-600 text-white rounded-lg font-medium transition-colors"
>
<PencilIcon className="w-4 h-4" />
Edit Content
</button>
<button
</Button>
<Button
variant="primary"
tone="brand"
onClick={() => navigate(`/writer/published?contentId=${content.id}&action=publish`)}
className="inline-flex items-center gap-2 px-4 py-2 bg-success-500 hover:bg-success-600 text-white rounded-lg font-medium transition-colors"
>
<BoltIcon className="w-4 h-4" />
Publish
</button>
</Button>
</>
)}
</div>

View File

@@ -12,6 +12,7 @@
*/
import React, { ReactNode, useState } from 'react';
import Button from '../components/ui/button/Button';
interface FormSection {
title: string;
@@ -89,21 +90,21 @@ export default function FormPageTemplate({
{(onSave || onCancel) && (
<div className="mt-6 flex items-center justify-end gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-white/[0.03]">
{onCancel && (
<button
<Button
onClick={onCancel}
className="rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-white/[0.03]"
variant="outline"
>
{cancelLabel}
</button>
</Button>
)}
{onSave && (
<button
<Button
onClick={handleSave}
disabled={loading || !isDirty}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed"
variant="primary"
>
{loading ? 'Saving...' : saveLabel}
</button>
</Button>
)}
</div>
)}