final section 10 -- and lgoabl styles adn compoeents plan

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 10:41:16 +00:00
parent 41e124d8e8
commit d389576634
18 changed files with 1918 additions and 1374 deletions

View File

@@ -211,5 +211,182 @@ import { MODULE_COLORS } from '@/config/colors.config';
---
**Last Updated**: December 30, 2025
**Last Updated**: January 1, 2026
**Status**: Active Design System Rules
---
## 🚨 MANDATORY COMPONENT & STYLING RULES
> **FOR ALL DEVELOPERS AND AI AGENTS**: This section defines the ONLY allowed sources for components, styling, and colors. Violating these rules will result in inconsistent UI and technical debt.
### ALLOWED COMPONENT SOURCES
**ONLY import components from these locations:**
| Category | Allowed Path | Components |
|----------|--------------|------------|
| **UI Components** | `@/components/ui/` or relative `../components/ui/` | Button, Card, Modal, Alert, Badge, Dropdown, Tooltip, Spinner, Tabs, Toast, Pagination, Progress, Avatar, Breadcrumb |
| **Common Components** | `@/components/common/` | PageHeader, ComponentCard, ConfirmDialog, FormModal, TablePageTemplate |
| **Icons** | `@/icons` or `@heroicons/react` | All SVG icons |
| **Templates** | `@/templates/` | TablePageTemplate, ContentViewTemplate |
### BANNED IMPORTS (DO NOT USE)
```tsx
// ❌ BANNED - Material UI
import { Button, Dialog, Alert, Box } from '@mui/material';
import { SomeIcon } from '@mui/icons-material';
// ❌ BANNED - Chakra UI
import { Button } from '@chakra-ui/react';
// ❌ BANNED - Ant Design
import { Button } from 'antd';
// ❌ BANNED - Custom inline components
const MyButton = () => <button className="...">; // If used more than once, create in ui/
```
### ALLOWED CSS/STYLING SOURCES
**ONLY these CSS files should be used:**
| File | Purpose | When to Use |
|------|---------|-------------|
| `src/index.css` | Main Tailwind config, utilities | Auto-imported, don't import manually |
| `src/styles/tokens.css` | CSS variables (colors, shadows) | Use via `var(--color-name)` |
**DO NOT CREATE OR USE:**
-`styles/global.css` - Deprecated
-`components/shared/blocks/blocks.css` - For marketing only
-`components/shared/layouts/layouts.css` - For marketing only
- ❌ Any new `.css` files in component folders
### COLOR USAGE RULES
**Allowed Color Methods:**
```tsx
// ✅ Tailwind brand colors (defined in index.css @theme)
className="bg-brand-500 text-brand-600 border-brand-200"
className="bg-success-500 text-warning-600 bg-error-50"
className="bg-gray-100 text-gray-700 border-gray-300"
// ✅ CSS variables from tokens.css
className="bg-[var(--color-primary)]"
style={{ '--accent': 'var(--color-success)' }}
// ✅ Dark mode variants
className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white"
```
**BANNED Color Methods:**
```tsx
// ❌ Hardcoded hex colors
className="bg-[#0693e3]"
className="text-[#ff7a00]"
style={{ color: '#4c1d95' }}
// ❌ Arbitrary Tailwind colors not in tokens
className="bg-[#4c1d95]"
// ❌ Inline SVG fills with hardcoded colors (use currentColor)
<svg fill="#F04438"> // ❌
<svg fill="currentColor" className="text-error-500"> // ✅
```
### COMPONENT-SPECIFIC RULES
#### Button Component
```tsx
// ✅ CORRECT - Use Button from ui/
import Button from '@/components/ui/button/Button';
<Button variant="primary" tone="brand" size="md">
Click Me
</Button>
// ❌ WRONG
import { Button } from '@mui/material';
<button className="bg-blue-500 px-4 py-2">Click</button> // Use Button component
```
#### Modal/Dialog Component
```tsx
// ✅ CORRECT - Use Modal from ui/
import { Modal } from '@/components/ui/modal';
<Modal isOpen={open} onClose={() => setOpen(false)}>
<div className="p-6">Modal content</div>
</Modal>
// ❌ WRONG
import { Dialog } from '@mui/material';
```
#### Alert Component
```tsx
// ✅ CORRECT
import Alert from '@/components/ui/alert/Alert';
<Alert variant="success" title="Success" message="Action completed" />
// ❌ WRONG
import { Alert } from '@mui/material';
```
#### Badge Component
```tsx
// ✅ CORRECT
import Badge from '@/components/ui/badge/Badge';
<Badge tone="success" variant="soft">Active</Badge>
// ❌ WRONG
import { Chip } from '@mui/material';
```
### FOLDER STRUCTURE FOR NEW COMPONENTS
If a new component is absolutely necessary:
```
src/components/ui/
├── new-component/
│ ├── NewComponent.tsx # Main component
│ └── index.ts # Export barrel
```
**Requirements for new components:**
1. Must be approved in a feature request
2. Must follow existing component patterns (see Button.tsx, Badge.tsx)
3. Must use design tokens for all colors
4. Must support dark mode
5. Must be documented in this file
6. Must NOT duplicate existing functionality
### REFERENCE: PROPERLY STYLED PAGES
**Use these pages as reference for correct styling:**
| Module | Pages | Path |
|--------|-------|------|
| Planner | Keywords, Clusters, Ideas | `src/pages/Planner/` |
| Writer | Review, Approved, Content | `src/pages/Writer/` |
| Sites | List, Settings | `src/pages/Sites/` |
| Dashboard | Home | `src/pages/Dashboard/` |
### AUDIT CHECKLIST FOR CODE REVIEW
Before merging any PR, verify:
- [ ] No imports from `@mui/material` or `@mui/icons-material`
- [ ] All buttons use `Button` from `ui/button/Button`
- [ ] All modals use `Modal` from `ui/modal`
- [ ] All alerts use `Alert` from `ui/alert/Alert`
- [ ] No hardcoded hex colors (search for `#[0-9a-fA-F]{3,6}`)
- [ ] No new CSS files created
- [ ] Dark mode variants included where needed
- [ ] Component props match design system (tone, variant, size)

View File

@@ -0,0 +1,305 @@
# Frontend Standardization Audit Plan
## Objective
Systematically audit every page and component to ensure 100% consistency with the design system defined in `DESIGN_SYSTEM.md` and eliminate all non-standard code patterns.
---
## Phase 1: Automated Detection Scripts
### 1.1 Run These Detection Commands
```bash
# Save all results to audit-results/ folder
mkdir -p /data/app/igny8/frontend/audit-results
# 1. Find ALL inline styles (style={{ }})
grep -rn "style={{" src/pages/ src/components/ --include="*.tsx" > audit-results/inline-styles.txt
# 2. Find ALL sx={{ }} (MUI-style inline)
grep -rn "sx={{" src/pages/ src/components/ --include="*.tsx" > audit-results/sx-styles.txt
# 3. Find ALL hardcoded hex colors
grep -rn "#[0-9a-fA-F]\{3,6\}" src/pages/ src/components/ --include="*.tsx" > audit-results/hardcoded-hex.txt
# 4. Find ALL rgb/rgba colors
grep -rn "rgb\|rgba" src/pages/ src/components/ --include="*.tsx" > audit-results/rgb-colors.txt
# 5. Find ALL non-standard gray classes (should use design tokens)
grep -rn "gray-[0-9]\{2,3\}" src/pages/ src/components/ --include="*.tsx" > audit-results/gray-classes.txt
# 6. Find ALL blue/red/green direct Tailwind (should use brand/success/error)
grep -rn "bg-blue-\|text-blue-\|border-blue-\|bg-red-\|text-red-\|bg-green-\|text-green-" src/pages/ src/components/ --include="*.tsx" > audit-results/direct-colors.txt
# 7. Find ALL external UI library imports
grep -rn "from '@mui\|from 'lucide-react\|from '@chakra\|from 'antd" src/pages/ src/components/ --include="*.tsx" > audit-results/external-imports.txt
# 8. Find ALL inline component definitions (const Component = () => inside pages)
grep -rn "const [A-Z][a-zA-Z]* = \(\)" src/pages/ --include="*.tsx" > audit-results/inline-components.txt
# 9. Find files NOT importing from ui/ folder
grep -L "from.*components/ui" src/pages/**/*.tsx > audit-results/missing-ui-imports.txt
# 10. Find duplicate button patterns (custom buttons instead of Button component)
grep -rn "<button\|<Button " src/pages/ --include="*.tsx" > audit-results/button-usage.txt
```
---
## Phase 2: Manual Page-by-Page Audit Checklist
### 2.1 Page Groups to Audit (in order)
| Priority | Group | Folder | Est. Files |
|----------|-------|--------|------------|
| 1 | Account | `pages/account/` | ~6 |
| 2 | Settings | `pages/Settings/` | ~10 |
| 3 | Sites | `pages/Sites/` | ~8 |
| 4 | Writer | `pages/Writer/` | ~5 |
| 5 | Planner | `pages/Planner/` | ~3 |
| 6 | Automation | `pages/Automation/` | ~2 |
| 7 | Optimizer | `pages/Optimizer/` | ~3 |
| 8 | Billing | `pages/Billing/` | ~4 |
| 9 | Other | Remaining pages | ~10+ |
### 2.2 Per-Page Audit Checklist
For EACH `.tsx` file, check:
#### A. IMPORTS (Lines 1-30)
- [ ] No imports from `@mui/material` or `@mui/icons-material`
- [ ] No imports from `lucide-react` (use `../../icons` instead)
- [ ] No imports from `@chakra-ui`, `antd`, or other UI libraries
- [ ] Uses `Button` from `components/ui/button/Button`
- [ ] Uses `Modal` from `components/ui/modal`
- [ ] Uses `Alert` from `components/ui/alert/Alert`
- [ ] Uses `Badge` from `components/ui/badge/Badge`
- [ ] Uses `Dropdown` from `components/ui/dropdown/Dropdown`
- [ ] Uses `Spinner` from `components/ui/spinner/Spinner`
- [ ] Uses `Tooltip` from `components/ui/tooltip/Tooltip`
- [ ] Uses icons from `../../icons` or `@/icons`
#### B. INLINE STYLES (Search entire file)
- [ ] No `style={{` patterns
- [ ] No `sx={{` patterns
- [ ] No inline `className` with hardcoded hex (e.g., `text-[#xxx]`)
#### C. COLOR USAGE (Search entire file)
- [ ] No hardcoded hex colors (`#XXXXXX`)
- [ ] No `rgb()` or `rgba()` values
- [ ] No direct Tailwind colors: `blue-500`, `red-500`, `green-500`
- [ ] Uses design tokens: `brand-500`, `success-500`, `error-500`, `warning-500`
- [ ] Gray scale uses: `gray-50` through `gray-900` (these ARE allowed)
#### D. COMPONENT PATTERNS
- [ ] No inline component definitions (components defined inside the page file)
- [ ] No duplicate code that exists in `components/ui/`
- [ ] No custom button implementations (use `Button` component)
- [ ] No custom modal implementations (use `Modal` component)
- [ ] No custom alert/toast implementations (use `Alert` component)
- [ ] No custom dropdown implementations (use `Dropdown` component)
#### E. DARK MODE SUPPORT
- [ ] All backgrounds have dark mode variant: `bg-white dark:bg-gray-900`
- [ ] All text colors have dark mode: `text-gray-900 dark:text-white`
- [ ] All borders have dark mode: `border-gray-200 dark:border-gray-700`
#### F. FORM ELEMENTS
- [ ] Input fields use consistent styling pattern
- [ ] Select dropdowns use consistent styling pattern
- [ ] Checkboxes/radios use consistent styling pattern
- [ ] Form labels use consistent styling pattern
---
## Phase 3: Component Audit
### 3.1 Standard Components Inventory
These are the ONLY allowed UI component sources:
| Component | Import Path | Usage |
|-----------|------------|-------|
| Button | `components/ui/button/Button` | All buttons |
| Modal | `components/ui/modal` | All modals/dialogs |
| Alert | `components/ui/alert/Alert` | All alerts/notifications |
| Badge | `components/ui/badge/Badge` | All badges/chips/tags |
| Dropdown | `components/ui/dropdown/Dropdown` | All dropdown menus |
| Spinner | `components/ui/spinner/Spinner` | All loading states |
| Tooltip | `components/ui/tooltip/Tooltip` | All tooltips |
| Card | `components/ui/card/*` | All card containers |
| Table | `components/ui/table/*` | All data tables |
| Tabs | `components/ui/tabs/*` | All tab interfaces |
| Progress | `components/ui/progress/*` | All progress bars |
### 3.2 Non-Standard Components to Find & Replace
| Pattern to Find | Replace With |
|----------------|--------------|
| `<button className=...>` | `<Button>` from ui/ |
| `<Dialog>` or `<dialog>` | `<Modal>` from ui/ |
| Custom alert divs | `<Alert>` from ui/ |
| `<Chip>` or chip patterns | `<Badge>` from ui/ |
| Custom loading spinners | `<Spinner>` from ui/ |
| `<Menu>` patterns | `<Dropdown>` from ui/ |
---
## Phase 4: Style Token Mapping
### 4.1 Color Replacements
| Wrong Pattern | Correct Pattern |
|---------------|-----------------|
| `bg-blue-500` | `bg-brand-500` |
| `text-blue-600` | `text-brand-600` |
| `border-blue-200` | `border-brand-200` |
| `bg-green-500` | `bg-success-500` |
| `text-green-600` | `text-success-600` |
| `bg-red-500` | `bg-error-500` |
| `text-red-600` | `text-error-600` |
| `bg-yellow-500` | `bg-warning-500` |
| `text-yellow-600` | `text-warning-600` |
| `#3B82F6` | Use `text-brand-500` or CSS var |
| `#10B981` | Use `text-success-500` or CSS var |
| `#EF4444` | Use `text-error-500` or CSS var |
### 4.2 Allowed Gray Scale (DO NOT REPLACE)
```
gray-25, gray-50, gray-100, gray-200, gray-300, gray-400,
gray-500, gray-600, gray-700, gray-800, gray-900, gray-950
```
---
## Phase 5: Execution Tracking
### 5.1 Audit Progress Tracker
| Page/Component | Audited | Issues Found | Fixed | Verified |
|----------------|---------|--------------|-------|----------|
| `pages/account/Profile.tsx` | ☐ | - | ☐ | ☐ |
| `pages/account/Team.tsx` | ☐ | - | ☐ | ☐ |
| `pages/account/Security.tsx` | ☐ | - | ☐ | ☐ |
| `pages/account/Preferences.tsx` | ☐ | - | ☐ | ☐ |
| `pages/account/Notifications.tsx` | ☐ | - | ☐ | ☐ |
| `pages/account/ApiKeys.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/General.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/Integrations.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/Notifications.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/Publishing.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/ContentDefaults.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/AIModels.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/ImageGeneration.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/SEO.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/Categories.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Settings/Tags.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/SitesList.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/SiteDetail.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/SiteSettings.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/SiteDashboard.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/CreateSite.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/ContentCategories.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/ContentTags.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Sites/Authors.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Writer/*.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Planner/*.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Automation/*.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Optimizer/*.tsx` | ☐ | - | ☐ | ☐ |
| `pages/Billing/*.tsx` | ☐ | - | ☐ | ☐ |
| _Add remaining pages..._ | | | | |
### 5.2 Component Folder Audit
| Component Folder | Audited | Issues | Fixed |
|------------------|---------|--------|-------|
| `components/ui/button/` | ☐ | - | ☐ |
| `components/ui/modal/` | ☐ | - | ☐ |
| `components/ui/alert/` | ☐ | - | ☐ |
| `components/ui/badge/` | ☐ | - | ☐ |
| `components/ui/dropdown/` | ☐ | - | ☐ |
| `components/ui/card/` | ☐ | - | ☐ |
| `components/ui/table/` | ☐ | - | ☐ |
| `components/ui/tabs/` | ☐ | - | ☐ |
| `components/common/` | ☐ | - | ☐ |
| `components/layout/` | ☐ | - | ☐ |
---
## Phase 6: Reference Pages
### 6.1 Gold Standard Pages (Use as Reference)
These pages are correctly styled and should be used as reference:
- `pages/Planner/Dashboard.tsx` - Correct card layouts
- `pages/Writer/Dashboard.tsx` - Correct table patterns
- `components/ui/*` - All standard components
### 6.2 Reference Patterns
**Correct Button Usage:**
```tsx
import { Button } from '../../components/ui/button/Button';
<Button variant="primary" tone="brand" size="md">
Save Changes
</Button>
```
**Correct Modal Usage:**
```tsx
import { Modal } from '../../components/ui/modal';
<Modal isOpen={isOpen} onClose={onClose} className="max-w-md p-6">
{/* content */}
</Modal>
```
**Correct Alert Usage:**
```tsx
import Alert from '../../components/ui/alert/Alert';
<Alert variant="success" title="Saved" message="Changes saved successfully" />
```
**Correct Icon Usage:**
```tsx
import { CheckCircleIcon, ErrorIcon } from '../../icons';
<CheckCircleIcon className="h-5 w-5 text-success-500" />
```
---
## Phase 7: Verification
### 7.1 Final Verification Checklist
After all fixes are applied, run:
```bash
# Should return 0 results for each:
grep -rn "from '@mui" src/pages/ src/components/ --include="*.tsx" | wc -l
grep -rn "from 'lucide-react" src/pages/ --include="*.tsx" | wc -l
grep -rn "style={{" src/pages/ --include="*.tsx" | wc -l
grep -rn "#[0-9a-fA-F]\{6\}" src/pages/ --include="*.tsx" | wc -l
grep -rn "bg-blue-\|bg-red-\|bg-green-" src/pages/ --include="*.tsx" | wc -l
```
### 7.2 Visual Verification
- [ ] Check each page in browser (light mode)
- [ ] Check each page in browser (dark mode)
- [ ] Verify all buttons look consistent
- [ ] Verify all modals look consistent
- [ ] Verify all forms look consistent
- [ ] Verify all colors match brand palette
---
## Notes
- **DO NOT** replace gray-* classes - these are part of the design system
- **DO** replace blue/red/green with brand/error/success
- **DO** replace all inline styles with Tailwind classes
- **DO** replace all hardcoded hex with CSS variables or Tailwind classes
- **DO** extract inline components to separate files
- **DO** use standard ui/ components instead of custom implementations

View File

@@ -107,6 +107,9 @@ const SyncDashboard = lazy(() => import("./pages/Sites/SyncDashboard"));
const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel"));
const PublishingQueue = lazy(() => import("./pages/Sites/PublishingQueue"));
// Setup - Lazy loaded
const SetupWizard = lazy(() => import("./pages/Setup/SetupWizard"));
// Help - Lazy loaded
const Help = lazy(() => import("./pages/Help/Help"));
@@ -256,6 +259,7 @@ export default function App() {
<Route path="/settings/import-export" element={<Navigate to="/" replace />} />
{/* Sites Management */}
<Route path="/setup/wizard" element={<SetupWizard />} />
<Route path="/sites" element={<SiteList />} />
<Route path="/sites/:id" element={<SiteDashboard />} />
<Route path="/sites/:id/pages" element={<PageManager />} />

View File

@@ -1,284 +0,0 @@
import React, { useState } from 'react';
import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Box,
LinearProgress,
Alert,
List,
ListItem,
ListItemText,
ListItemIcon,
CircularProgress,
Chip
} from '@mui/material';
import {
Publish as PublishIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon,
Schedule as PendingIcon
} from '@mui/icons-material';
import { api } from '../../services/api';
interface BulkWordPressPublishProps {
selectedContentIds: string[];
contentItems: Array<{
id: string;
title: string;
status: string;
}>;
onPublishComplete: () => void;
onClose: () => void;
}
interface BulkPublishResult {
total: number;
queued: number;
skipped: number;
errors: string[];
}
export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
selectedContentIds,
contentItems,
onPublishComplete,
onClose
}) => {
const [open, setOpen] = useState(false);
const [publishing, setPublishing] = useState(false);
const [result, setResult] = useState<BulkPublishResult | null>(null);
const [error, setError] = useState<string | null>(null);
const selectedItems = contentItems.filter(item =>
selectedContentIds.includes(item.id)
);
const handleBulkPublish = async () => {
setPublishing(true);
setError(null);
setResult(null);
try {
const response = await api.post('/api/v1/content/bulk-publish-to-wordpress/', {
content_ids: selectedContentIds.map(id => parseInt(id))
});
if (response.data.success) {
setResult({
total: selectedContentIds.length,
queued: response.data.data.content_count,
skipped: 0,
errors: []
});
// Start polling for individual status updates
startStatusPolling();
} else {
setError(response.data.message || 'Failed to start bulk publishing');
}
} catch (error: any) {
setError(error.response?.data?.message || 'Error starting bulk publish');
} finally {
setPublishing(false);
}
};
const startStatusPolling = () => {
// Poll for 2 minutes to check status
const pollInterval = setInterval(async () => {
try {
// Check status of all items (this could be optimized with a dedicated endpoint)
const statusPromises = selectedContentIds.map(id =>
api.get(`/api/v1/content/${id}/wordpress-status/`)
);
const responses = await Promise.allSettled(statusPromises);
let completedCount = 0;
let successCount = 0;
let failedCount = 0;
responses.forEach((response) => {
if (response.status === 'fulfilled' && response.value.data.success) {
const status = response.value.data.data.wordpress_sync_status;
if (status === 'success' || status === 'failed') {
completedCount++;
if (status === 'success') successCount++;
if (status === 'failed') failedCount++;
}
}
});
// If all items are complete, stop polling
if (completedCount === selectedContentIds.length) {
clearInterval(pollInterval);
setResult(prev => prev ? {
...prev,
queued: successCount,
errors: Array(failedCount).fill('Publishing failed')
} : null);
onPublishComplete();
}
} catch (error) {
console.error('Error polling status:', error);
}
}, 5000);
// Stop polling after 2 minutes
setTimeout(() => {
clearInterval(pollInterval);
}, 120000);
};
const handleOpen = () => {
setOpen(true);
setResult(null);
setError(null);
};
const handleClose = () => {
setOpen(false);
onClose();
};
const getResultSummary = () => {
if (!result) return null;
const { total, queued, skipped, errors } = result;
const failed = errors.length;
return (
<Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>
Bulk Publish Results
</Typography>
<Box display="flex" gap={1} flexWrap="wrap" mb={2}>
<Chip
icon={<SuccessIcon />}
label={`${queued} Queued`}
color="success"
size="small"
/>
{skipped > 0 && (
<Chip
icon={<PendingIcon />}
label={`${skipped} Skipped`}
color="warning"
size="small"
/>
)}
{failed > 0 && (
<Chip
icon={<ErrorIcon />}
label={`${failed} Failed`}
color="error"
size="small"
/>
)}
</Box>
{failed > 0 && (
<Alert severity="warning" sx={{ mt: 1 }}>
Some items failed to publish. Check individual item status for details.
</Alert>
)}
</Box>
);
};
return (
<>
<Button
variant="contained"
color="primary"
startIcon={<PublishIcon />}
onClick={handleOpen}
disabled={selectedContentIds.length === 0}
>
Bulk Publish to Site ({selectedContentIds.length})
</Button>
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
>
<DialogTitle>
Bulk Publish to Site
</DialogTitle>
<DialogContent>
{!publishing && !result && (
<>
<Typography variant="body1" gutterBottom>
You are about to publish {selectedContentIds.length} content items to your site:
</Typography>
<List dense sx={{ maxHeight: 300, overflow: 'auto', mt: 2 }}>
{selectedItems.map((item) => (
<ListItem key={item.id}>
<ListItemIcon>
<PublishIcon />
</ListItemIcon>
<ListItemText
primary={item.title}
secondary={`Status: ${item.status}`}
/>
</ListItem>
))}
</List>
<Alert severity="info" sx={{ mt: 2 }}>
This will create new posts on your WordPress site with all content,
images, categories, and SEO metadata. Items already published will be skipped.
</Alert>
</>
)}
{publishing && (
<Box sx={{ py: 3 }}>
<Box display="flex" alignItems="center" gap={2} mb={2}>
<CircularProgress size={24} />
<Typography>Queuing content for WordPress publishing...</Typography>
</Box>
<LinearProgress />
</Box>
)}
{result && getResultSummary()}
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
{result ? 'Close' : 'Cancel'}
</Button>
{!publishing && !result && (
<Button
onClick={handleBulkPublish}
color="primary"
variant="contained"
disabled={selectedContentIds.length === 0}
>
Publish All to WordPress
</Button>
)}
</DialogActions>
</Dialog>
</>
);
};
export default BulkWordPressPublish;

View File

@@ -1,24 +1,20 @@
/**
* BulkWordPressPublish Component
*
* Handles bulk publishing multiple content items to connected WordPress sites.
*
* 🔒 STYLE LOCKED - Uses only standard ui/ components. See DESIGN_SYSTEM.md
*/
import React, { useState } from 'react';
import { Button } from '../ui/button/Button';
import { Modal } from '../ui/modal';
import Alert from '../ui/alert/Alert';
import { Spinner } from '../ui/spinner/Spinner';
import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Alert,
Box,
CircularProgress,
List,
ListItem,
ListItemText,
Divider
} from '@mui/material';
import {
Publish as PublishIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon
} from '@mui/icons-material';
CheckCircleIcon,
ErrorIcon,
PaperPlaneIcon,
} from '../../icons';
import { api } from '../../services/api';
interface BulkWordPressPublishProps {
@@ -121,140 +117,140 @@ export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
return (
<>
<Button
variant="contained"
color="primary"
startIcon={<PublishIcon />}
variant="primary"
tone="brand"
startIcon={<PaperPlaneIcon className="h-4 w-4" />}
onClick={() => setOpen(true)}
size="small"
size="sm"
>
Publish Ready ({readyToPublish.length})
</Button>
<Dialog
open={open}
<Modal
isOpen={open}
onClose={handleClose}
maxWidth="md"
fullWidth
disableEscapeKeyDown={publishing}
className="max-w-2xl p-6"
>
<DialogTitle>
Bulk Publish to Site
</DialogTitle>
<DialogContent>
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Bulk Publish to Site
</h2>
{!publishing && results.length === 0 && (
<>
<Typography variant="body1" gutterBottom>
<p className="text-gray-700 dark:text-gray-300">
Ready to publish <strong>{readyToPublish.length}</strong> content items to your site:
</Typography>
</p>
<Alert severity="info" sx={{ mt: 2, mb: 2 }}>
Only content with generated images and not yet published will be included.
</Alert>
<Alert
variant="info"
title="Info"
message="Only content with generated images and not yet published will be included."
/>
<Box sx={{ maxHeight: 300, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
<List dense>
{readyToPublish.map((item, index) => (
<div key={item.id}>
<ListItem>
<ListItemText
primary={item.title}
secondary={`ID: ${item.id}`}
/>
</ListItem>
{index < readyToPublish.length - 1 && <Divider />}
<div className="max-h-72 overflow-auto rounded-lg border border-gray-200 dark:border-gray-700">
{readyToPublish.map((item, index) => (
<div key={item.id}>
<div className="px-4 py-3">
<p className="font-medium text-gray-900 dark:text-white">{item.title}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">ID: {item.id}</p>
</div>
))}
</List>
</Box>
{index < readyToPublish.length - 1 && (
<div className="border-t border-gray-200 dark:border-gray-700" />
)}
</div>
))}
</div>
</>
)}
{publishing && (
<Box display="flex" alignItems="center" gap={2} py={4}>
<CircularProgress />
<Typography>
<div className="flex items-center gap-3 py-8">
<Spinner size="md" />
<p className="text-gray-700 dark:text-gray-300">
Publishing {readyToPublish.length} items to WordPress...
</Typography>
</Box>
</p>
</div>
)}
{!publishing && results.length > 0 && (
<>
<Box sx={{ mb: 2 }}>
<div className="space-y-2">
{successCount > 0 && (
<Alert severity="success" sx={{ mb: 1 }}>
Successfully published {successCount} items
</Alert>
<Alert
variant="success"
title="Published"
message={`Successfully published ${successCount} items`}
/>
)}
{failedCount > 0 && (
<Alert severity="error">
Failed to publish {failedCount} items
</Alert>
<Alert
variant="error"
title="Failed"
message={`Failed to publish ${failedCount} items`}
/>
)}
</Box>
</div>
<Typography variant="h6" gutterBottom>
Results:
</Typography>
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Results:</h3>
<Box sx={{ maxHeight: 400, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
<List dense>
{results.map((result, index) => (
<div key={result.id}>
<ListItem>
<Box display="flex" alignItems="center" width="100%">
{result.status === 'success' ? (
<SuccessIcon color="success" sx={{ mr: 1 }} />
) : (
<ErrorIcon color="error" sx={{ mr: 1 }} />
)}
<ListItemText
primary={result.title}
secondary={result.message || (result.status === 'success' ? 'Published successfully' : 'Publishing failed')}
/>
</Box>
</ListItem>
{index < results.length - 1 && <Divider />}
<div className="max-h-96 overflow-auto rounded-lg border border-gray-200 dark:border-gray-700">
{results.map((result, index) => (
<div key={result.id}>
<div className="flex items-center gap-3 px-4 py-3">
{result.status === 'success' ? (
<CheckCircleIcon className="h-5 w-5 text-success-500" />
) : (
<ErrorIcon className="h-5 w-5 text-error-500" />
)}
<div className="flex-1">
<p className="font-medium text-gray-900 dark:text-white">{result.title}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{result.message || (result.status === 'success' ? 'Published successfully' : 'Publishing failed')}
</p>
</div>
</div>
))}
</List>
</Box>
</>
)}
</DialogContent>
<DialogActions>
{!publishing && results.length === 0 && (
<>
<Button onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleBulkPublish}
variant="contained"
color="primary"
startIcon={<PublishIcon />}
>
Publish All ({readyToPublish.length})
</Button>
{index < results.length - 1 && (
<div className="border-t border-gray-200 dark:border-gray-700" />
)}
</div>
))}
</div>
</>
)}
{publishing && (
<Button disabled>
Publishing...
</Button>
)}
{!publishing && results.length > 0 && (
<Button onClick={handleClose} variant="contained">
Close
</Button>
)}
</DialogActions>
</Dialog>
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
{!publishing && results.length === 0 && (
<>
<Button variant="outline" tone="neutral" onClick={handleClose}>
Cancel
</Button>
<Button
variant="primary"
tone="brand"
onClick={handleBulkPublish}
startIcon={<PaperPlaneIcon className="h-4 w-4" />}
>
Publish All ({readyToPublish.length})
</Button>
</>
)}
{publishing && (
<Button variant="outline" tone="neutral" disabled>
Publishing...
</Button>
)}
{!publishing && results.length > 0 && (
<Button variant="primary" tone="brand" onClick={handleClose}>
Close
</Button>
)}
</div>
</div>
</Modal>
</>
);
};

View File

@@ -1,20 +1,21 @@
import React, { useState } from 'react';
/**
* ContentActionsMenu Component
*
* Dropdown menu for content actions (edit, generate images, export, delete, publish).
*
* 🔒 STYLE LOCKED - Uses only standard ui/ components. See DESIGN_SYSTEM.md
*/
import React, { useState, useRef } from 'react';
import { Button } from '../ui/button/Button';
import { Dropdown } from '../ui/dropdown/Dropdown';
import {
IconButton,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Divider
} from '@mui/material';
import {
MoreVert as MoreVertIcon,
Publish as PublishIcon,
Edit as EditIcon,
Image as ImageIcon,
GetApp as ExportIcon,
Delete as DeleteIcon
} from '@mui/icons-material';
MoreDotIcon,
PaperPlaneIcon,
PencilIcon,
FileIcon,
DownloadIcon,
TrashBinIcon,
} from '../../icons';
import { WordPressPublish } from './WordPressPublish';
interface ContentActionsMenuProps {
@@ -40,16 +41,12 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
onDelete,
onWordPressStatusChange
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [showWordPressDialog, setShowWordPressDialog] = useState(false);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const buttonRef = useRef<HTMLButtonElement>(null);
const handleClose = () => {
setAnchorEl(null);
setIsOpen(false);
};
const handlePublishClick = () => {
@@ -69,85 +66,85 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
return (
<>
<IconButton
<button
ref={buttonRef}
aria-label="more actions"
id="content-actions-button"
aria-controls={open ? 'content-actions-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
onClick={handleClick}
size="small"
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"
>
<MoreVertIcon />
</IconButton>
<MoreDotIcon className="h-5 w-5" />
</button>
<Menu
id="content-actions-menu"
anchorEl={anchorEl}
open={open}
<Dropdown
isOpen={isOpen}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'content-actions-button',
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
anchorRef={buttonRef as React.RefObject<HTMLElement>}
placement="bottom-right"
className="w-48"
>
{/* WordPress Publishing - Only show if images are ready */}
{canPublishToWordPress && (
<>
<MenuItem onClick={handlePublishClick}>
<ListItemIcon>
<PublishIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Publish to Site</ListItemText>
</MenuItem>
<Divider />
</>
)}
{/* Edit Action */}
{onEdit && (
<MenuItem onClick={() => handleMenuAction(onEdit)}>
<ListItemIcon>
<EditIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Edit</ListItemText>
</MenuItem>
)}
{/* Generate Image Action */}
{onGenerateImage && (
<MenuItem onClick={() => handleMenuAction(onGenerateImage)}>
<ListItemIcon>
<ImageIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Generate Image Prompts</ListItemText>
</MenuItem>
)}
{/* Export Action */}
{onExport && (
<MenuItem onClick={() => handleMenuAction(onExport)}>
<ListItemIcon>
<ExportIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Export</ListItemText>
</MenuItem>
)}
{/* Delete Action */}
{onDelete && (
<>
<Divider />
<MenuItem onClick={() => handleMenuAction(onDelete)} sx={{ color: 'error.main' }}>
<ListItemIcon>
<DeleteIcon fontSize="small" color="error" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</>
)}
</Menu>
<div className="py-1">
{/* WordPress Publishing - Only show if images are ready */}
{canPublishToWordPress && (
<>
<button
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"
>
<PaperPlaneIcon className="h-4 w-4" />
<span>Publish to Site</span>
</button>
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
</>
)}
{/* Edit Action */}
{onEdit && (
<button
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"
>
<PencilIcon className="h-4 w-4" />
<span>Edit</span>
</button>
)}
{/* Generate Image Action */}
{onGenerateImage && (
<button
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"
>
<FileIcon className="h-4 w-4" />
<span>Generate Image Prompts</span>
</button>
)}
{/* Export Action */}
{onExport && (
<button
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"
>
<DownloadIcon className="h-4 w-4" />
<span>Export</span>
</button>
)}
{/* Delete Action */}
{onDelete && (
<>
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
<button
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"
>
<TrashBinIcon className="h-4 w-4" />
<span>Delete</span>
</button>
</>
)}
</div>
</Dropdown>
{/* WordPress Publish Dialog */}
{showWordPressDialog && (

View File

@@ -1,26 +1,23 @@
/**
* WordPressPublish Component
*
* Handles publishing content to connected WordPress sites.
*
* 🔒 STYLE LOCKED - Uses only standard ui/ components. See DESIGN_SYSTEM.md
*/
import React, { useState, useEffect } from 'react';
import {
Button,
Alert,
Chip,
IconButton,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
CircularProgress,
Box,
Typography
} from '@mui/material';
import {
Publish as PublishIcon,
Refresh as RefreshIcon,
CheckCircle as SuccessIcon,
Error as ErrorIcon,
Schedule as PendingIcon,
Sync as SyncingIcon
} from '@mui/icons-material';
import { Button } from '../ui/button/Button';
import { Modal } from '../ui/modal';
import Alert from '../ui/alert/Alert';
import { Badge } from '../ui/badge/Badge';
import { Tooltip } from '../ui/tooltip/Tooltip';
import { Spinner } from '../ui/spinner/Spinner';
import {
CheckCircleIcon,
ErrorIcon,
TimeIcon,
PaperPlaneIcon,
} from '../../icons';
import { api } from '../../services/api';
export interface WordPressPublishProps {
@@ -149,8 +146,9 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
const getStatusInfo = () => {
if (!wpStatus) {
return {
color: 'default' as const,
icon: <PublishIcon />,
tone: 'neutral' as const,
badgeTone: 'neutral' as const,
icon: <PaperPlaneIcon className="h-4 w-4" />,
label: 'Not Published',
action: 'publish'
};
@@ -159,36 +157,41 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
switch (wpStatus.wordpress_sync_status) {
case 'pending':
return {
color: 'warning' as const,
icon: <PendingIcon />,
tone: 'warning' as const,
badgeTone: 'warning' as const,
icon: <TimeIcon className="h-4 w-4" />,
label: 'Queued',
action: 'wait'
};
case 'syncing':
return {
color: 'info' as const,
icon: <SyncingIcon className="animate-spin" />,
tone: 'brand' as const,
badgeTone: 'info' as const,
icon: <Spinner size="sm" />,
label: 'Publishing...',
action: 'wait'
};
case 'success':
return {
color: 'success' as const,
icon: <SuccessIcon />,
tone: 'success' as const,
badgeTone: 'success' as const,
icon: <CheckCircleIcon className="h-4 w-4" />,
label: 'Published',
action: 'view'
};
case 'failed':
return {
color: 'error' as const,
icon: <ErrorIcon />,
tone: 'danger' as const,
badgeTone: 'danger' as const,
icon: <ErrorIcon className="h-4 w-4" />,
label: 'Failed',
action: 'retry'
};
default:
return {
color: 'default' as const,
icon: <PublishIcon />,
tone: 'neutral' as const,
badgeTone: 'neutral' as const,
icon: <PaperPlaneIcon className="h-4 w-4" />,
label: 'Not Published',
action: 'publish'
};
@@ -202,181 +205,178 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
if (!shouldShowPublishButton) {
return (
<Tooltip title={`Images must be generated before publishing to site`}>
<Box display="flex" alignItems="center" gap={1}>
<Tooltip text="Images must be generated before publishing to site">
<div className="flex items-center gap-2">
<Button
variant="outlined"
variant="outline"
tone="neutral"
disabled
size={size}
startIcon={<PendingIcon />}
size={size === 'small' ? 'sm' : size === 'large' ? 'lg' : 'md'}
startIcon={<TimeIcon className="h-4 w-4" />}
>
Awaiting Images
</Button>
{size !== 'small' && (
<Chip
icon={<PendingIcon />}
label="Images Pending"
color="warning"
size="small"
variant="outlined"
/>
<Badge
tone="warning"
variant="outline"
startIcon={<TimeIcon className="h-3 w-3" />}
>
Images Pending
</Badge>
)}
</Box>
</div>
</Tooltip>
);
}
const renderButton = () => {
if (size === 'small') {
return (
<Tooltip title={`Site: ${statusInfo.label}`}>
<IconButton
size="small"
onClick={() => {
if (statusInfo.action === 'publish') {
setPublishDialogOpen(true);
} else if (statusInfo.action === 'retry') {
handleRetry();
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
disabled={loading || statusInfo.action === 'wait'}
color={statusInfo.color}
>
{loading ? <CircularProgress size={16} /> : statusInfo.icon}
</IconButton>
</Tooltip>
);
const handleButtonClick = () => {
if (statusInfo.action === 'publish') {
setPublishDialogOpen(true);
} else if (statusInfo.action === 'retry') {
handleRetry();
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
};
const renderButton = () => {
const buttonSize = size === 'small' ? 'sm' : size === 'large' ? 'lg' : 'md';
return (
<Button
variant={statusInfo.action === 'publish' ? 'contained' : 'outlined'}
color={statusInfo.color}
startIcon={loading ? <CircularProgress size={20} /> : statusInfo.icon}
onClick={() => {
if (statusInfo.action === 'publish') {
setPublishDialogOpen(true);
} else if (statusInfo.action === 'retry') {
handleRetry();
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
disabled={loading || statusInfo.action === 'wait'}
size={size}
>
{statusInfo.action === 'publish' && 'Publish to Site'}
{statusInfo.action === 'retry' && 'Retry'}
{statusInfo.action === 'view' && 'View on Site'}
{statusInfo.action === 'wait' && statusInfo.label}
</Button>
<Tooltip text={`Site: ${statusInfo.label}`}>
<Button
size={buttonSize}
variant={statusInfo.action === 'publish' ? 'primary' : 'outline'}
tone={statusInfo.tone}
onClick={handleButtonClick}
disabled={loading || statusInfo.action === 'wait'}
startIcon={loading ? <Spinner size="sm" /> : statusInfo.icon}
>
{statusInfo.action === 'publish' && 'Publish to Site'}
{statusInfo.action === 'retry' && 'Retry'}
{statusInfo.action === 'view' && 'View on Site'}
{statusInfo.action === 'wait' && statusInfo.label}
</Button>
</Tooltip>
);
};
const renderStatusChip = () => {
const renderStatusBadge = () => {
if (size === 'small') return null;
return (
<Chip
icon={statusInfo.icon}
label={statusInfo.label}
color={statusInfo.color}
size="small"
variant="outlined"
onClick={() => {
if (wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
style={{
marginLeft: 8,
cursor: wpStatus?.wordpress_post_url ? 'pointer' : 'default'
}}
/>
<Badge
tone={statusInfo.badgeTone}
variant="outline"
startIcon={statusInfo.icon}
className={wpStatus?.wordpress_post_url ? 'cursor-pointer' : ''}
>
<span
onClick={() => {
if (wpStatus?.wordpress_post_url) {
window.open(wpStatus.wordpress_post_url, '_blank');
}
}}
>
{statusInfo.label}
</span>
</Badge>
);
};
return (
<Box display="flex" alignItems="center" gap={1}>
<div className="flex items-center gap-2">
{renderButton()}
{renderStatusChip()}
{renderStatusBadge()}
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
<IconButton
size="small"
onClick={() => setError(null)}
sx={{ ml: 1 }}
>
×
</IconButton>
</Alert>
<div className="mt-2">
<Alert
variant="error"
title="Publishing Error"
message={error}
/>
</div>
)}
{/* Publish Confirmation Dialog */}
<Dialog
open={publishDialogOpen}
{/* Publish Confirmation Modal */}
<Modal
isOpen={publishDialogOpen}
onClose={() => setPublishDialogOpen(false)}
maxWidth="sm"
fullWidth
className="max-w-md p-6"
>
<DialogTitle>Publish to Site</DialogTitle>
<DialogContent>
<Typography variant="body1" gutterBottom>
Are you sure you want to publish "<strong>{contentTitle}</strong>" to your site?
</Typography>
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Publish to Site
</h2>
<Typography variant="body2" color="textSecondary" sx={{ mt: 2 }}>
<p className="text-gray-700 dark:text-gray-300">
Are you sure you want to publish "<strong>{contentTitle}</strong>" to your site?
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
This will create a new post on your connected site with all content,
images, categories, and SEO metadata.
</Typography>
</p>
{imageGenerationStatus === 'complete' && (
<Alert severity="success" sx={{ mt: 2 }}>
Images are generated and ready for publishing
</Alert>
<Alert
variant="success"
title="Ready to Publish"
message="Images are generated and ready for publishing"
/>
)}
{imageGenerationStatus !== 'complete' && showOnlyIfImagesReady && (
<Alert severity="warning" sx={{ mt: 2 }}>
Images are still being generated. Please wait before publishing.
</Alert>
<Alert
variant="warning"
title="Images Not Ready"
message="Images are still being generated. Please wait before publishing."
/>
)}
{wpStatus?.wordpress_sync_status === 'success' && (
<Alert severity="info" sx={{ mt: 2 }}>
This content is already published to your site.
You can force republish to update the existing post.
</Alert>
<Alert
variant="info"
title="Already Published"
message="This content is already published to your site. You can force republish to update the existing post."
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setPublishDialogOpen(false)}>
Cancel
</Button>
{wpStatus?.wordpress_sync_status === 'success' && (
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
onClick={() => handlePublishToWordPress(true)}
color="warning"
disabled={loading}
variant="outline"
tone="neutral"
onClick={() => setPublishDialogOpen(false)}
>
Force Republish
Cancel
</Button>
)}
<Button
onClick={() => handlePublishToWordPress(false)}
color="primary"
variant="contained"
disabled={loading}
>
{loading ? 'Publishing...' : 'Publish'}
</Button>
</DialogActions>
</Dialog>
</Box>
{wpStatus?.wordpress_sync_status === 'success' && (
<Button
variant="outline"
tone="warning"
onClick={() => handlePublishToWordPress(true)}
disabled={loading}
>
Force Republish
</Button>
)}
<Button
variant="primary"
tone="brand"
onClick={() => handlePublishToWordPress(false)}
disabled={loading}
startIcon={loading ? <Spinner size="sm" /> : <PaperPlaneIcon className="h-4 w-4" />}
>
{loading ? 'Publishing...' : 'Publish'}
</Button>
</div>
</div>
</Modal>
</div>
);
};

View File

@@ -71,7 +71,7 @@ const Alert: React.FC<AlertProps> = ({
fillRule="evenodd"
clipRule="evenodd"
d="M20.3499 12.0004C20.3499 16.612 16.6115 20.3504 11.9999 20.3504C7.38832 20.3504 3.6499 16.612 3.6499 12.0004C3.6499 7.38881 7.38833 3.65039 11.9999 3.65039C16.6115 3.65039 20.3499 7.38881 20.3499 12.0004ZM11.9999 22.1504C17.6056 22.1504 22.1499 17.6061 22.1499 12.0004C22.1499 6.3947 17.6056 1.85039 11.9999 1.85039C6.39421 1.85039 1.8499 6.3947 1.8499 12.0004C1.8499 17.6061 6.39421 22.1504 11.9999 22.1504ZM13.0008 16.4753C13.0008 15.923 12.5531 15.4753 12.0008 15.4753L11.9998 15.4753C11.4475 15.4753 10.9998 15.923 10.9998 16.4753C10.9998 17.0276 11.4475 17.4753 11.9998 17.4753L12.0008 17.4753C12.5531 17.4753 13.0008 17.0276 13.0008 16.4753ZM11.9998 6.62898C12.414 6.62898 12.7498 6.96476 12.7498 7.37898L12.7498 13.0555C12.7498 13.4697 12.414 13.8055 11.9998 13.8055C11.5856 13.8055 11.2498 13.4697 11.2498 13.0555L11.2498 7.37898C11.2498 6.96476 11.5856 6.62898 11.9998 6.62898Z"
fill="#F04438"
fill="currentColor"
/>
</svg>
),

View File

@@ -82,7 +82,7 @@ export function createApprovedPageConfig(params: {
target="_blank"
rel="noopener noreferrer"
className="text-brand-500 hover:text-brand-600 transition-colors"
title="View on WordPress"
title="View on Site"
>
<ArrowRightIcon className="w-4 h-4" />
</a>

View File

@@ -1,571 +0,0 @@
@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap") layer(base);
@import "./styles/tokens.css";
@keyframes slide-in-right {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-slide-in-right {
animation: slide-in-right 0.3s ease-out;
}
@custom-variant dark (&:is(.dark *));
@theme {
/* Fonts */
--font-outfit: Outfit, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
/* Breakpoints */
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
--breakpoint-3xl: 2000px;
/* Typography */
--text-title-2xl: 72px;
--text-title-2xl--line-height: 90px;
--text-title-xl: 60px;
--text-title-xl--line-height: 72px;
--text-title-lg: 48px;
--text-title-lg--line-height: 60px;
--text-title-md: 36px;
--text-title-md--line-height: 44px;
--text-title-sm: 30px;
--text-title-sm--line-height: 38px;
--text-theme-xl: 20px;
--text-theme-xl--line-height: 30px;
--text-theme-sm: 14px;
--text-theme-sm--line-height: 20px;
--text-theme-xs: 12px;
--text-theme-xs--line-height: 18px;
/* Base Colors */
--color-current: currentColor;
--color-transparent: transparent;
--color-white: #ffffff;
--color-black: #101828;
/* Brand Colors */
--color-brand-25: color-mix(in srgb, var(--color-primary) 3%, white);
--color-brand-50: color-mix(in srgb, var(--color-primary) 8%, white);
--color-brand-100: color-mix(in srgb, var(--color-primary) 15%, white);
--color-brand-200: color-mix(in srgb, var(--color-primary) 25%, white);
--color-brand-300: color-mix(in srgb, var(--color-primary) 40%, white);
--color-brand-400: color-mix(in srgb, var(--color-primary) 60%, white);
--color-brand-500: var(--color-primary);
--color-brand-600: var(--color-primary-dark);
--color-brand-700: color-mix(in srgb, var(--color-primary-dark) 85%, black);
--color-brand-800: color-mix(in srgb, var(--color-primary-dark) 70%, black);
--color-brand-900: color-mix(in srgb, var(--color-primary-dark) 55%, black);
--color-brand-950: color-mix(in srgb, var(--color-primary-dark) 35%, black);
/* Gray Scale */
--color-gray-25: #fcfcfd;
--color-gray-50: #f9fafb;
--color-gray-100: #f2f4f7;
--color-gray-200: #e4e7ec;
--color-gray-300: #d0d5dd;
--color-gray-400: #98a2b3;
--color-gray-500: #667085;
--color-gray-600: #475467;
--color-gray-700: #344054;
--color-gray-800: #1d2939;
--color-gray-900: #101828;
--color-gray-950: #0c111d;
--color-gray-dark: #1a2231;
/* Success */
--color-success-25: color-mix(in srgb, var(--color-success) 2%, white);
--color-success-50: color-mix(in srgb, var(--color-success) 6%, white);
--color-success-100: color-mix(in srgb, var(--color-success) 15%, white);
--color-success-200: color-mix(in srgb, var(--color-success) 30%, white);
--color-success-300: color-mix(in srgb, var(--color-success) 50%, white);
--color-success-400: color-mix(in srgb, var(--color-success) 75%, white);
--color-success-500: var(--color-success);
--color-success-600: var(--color-success-dark);
--color-success-700: color-mix(in srgb, var(--color-success-dark) 85%, black);
--color-success-800: color-mix(in srgb, var(--color-success-dark) 65%, black);
--color-success-900: color-mix(in srgb, var(--color-success-dark) 50%, black);
--color-success-950: color-mix(in srgb, var(--color-success-dark) 35%, black);
/* Error */
--color-error-25: color-mix(in srgb, var(--color-danger) 2%, white);
--color-error-50: color-mix(in srgb, var(--color-danger) 5%, white);
--color-error-100: color-mix(in srgb, var(--color-danger) 10%, white);
--color-error-200: color-mix(in srgb, var(--color-danger) 20%, white);
--color-error-300: color-mix(in srgb, var(--color-danger) 40%, white);
--color-error-400: color-mix(in srgb, var(--color-danger) 65%, white);
--color-error-500: var(--color-danger);
--color-error-600: var(--color-danger-dark);
--color-error-700: color-mix(in srgb, var(--color-danger-dark) 80%, black);
--color-error-800: color-mix(in srgb, var(--color-danger-dark) 65%, black);
--color-error-900: color-mix(in srgb, var(--color-danger-dark) 50%, black);
--color-error-950: color-mix(in srgb, var(--color-danger-dark) 30%, black);
/* Warning */
--color-warning-25: color-mix(in srgb, var(--color-warning) 2%, white);
--color-warning-50: color-mix(in srgb, var(--color-warning) 4%, white);
--color-warning-100: color-mix(in srgb, var(--color-warning) 10%, white);
--color-warning-200: color-mix(in srgb, var(--color-warning) 20%, white);
--color-warning-300: color-mix(in srgb, var(--color-warning) 40%, white);
--color-warning-400: color-mix(in srgb, var(--color-warning) 65%, white);
--color-warning-500: var(--color-warning);
--color-warning-600: var(--color-warning-dark);
--color-warning-700: color-mix(in srgb, var(--color-warning-dark) 80%, black);
--color-warning-800: color-mix(in srgb, var(--color-warning-dark) 65%, black);
--color-warning-900: color-mix(in srgb, var(--color-warning-dark) 50%, black);
--color-warning-950: color-mix(in srgb, var(--color-warning-dark) 30%, black);
/* Purple */
--color-purple-25: color-mix(in srgb, var(--color-purple) 2%, white);
--color-purple-50: color-mix(in srgb, var(--color-purple) 8%, white);
--color-purple-100: color-mix(in srgb, var(--color-purple) 15%, white);
--color-purple-200: color-mix(in srgb, var(--color-purple) 25%, white);
--color-purple-300: color-mix(in srgb, var(--color-purple) 40%, white);
--color-purple-400: color-mix(in srgb, var(--color-purple) 60%, white);
--color-purple-500: var(--color-purple);
--color-purple-600: var(--color-purple-dark);
--color-purple-700: color-mix(in srgb, var(--color-purple-dark) 85%, black);
--color-purple-800: color-mix(in srgb, var(--color-purple-dark) 70%, black);
--color-purple-900: color-mix(in srgb, var(--color-purple-dark) 55%, black);
--color-purple-950: color-mix(in srgb, var(--color-purple-dark) 35%, black);
/* Shadows */
--shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
--shadow-theme-sm: 0px 1px 3px 0px rgba(16, 24, 40, 0.1), 0px 1px 2px 0px rgba(16, 24, 40, 0.06);
--shadow-theme-md: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
--shadow-theme-lg: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
--shadow-theme-xl: 0px 20px 24px -4px rgba(16, 24, 40, 0.08), 0px 8px 8px -4px rgba(16, 24, 40, 0.03);
--drop-shadow-4xl: 0 35px 35px rgba(0, 0, 0, 0.25), 0 45px 65px rgba(0, 0, 0, 0.15);
/* Z-Index */
--z-index-1: 1;
--z-index-9: 9;
--z-index-99: 99;
--z-index-999: 999;
--z-index-9999: 9999;
--z-index-99999: 99999;
--z-index-999999: 999999;
}
@layer base {
*,::after,::before,::backdrop,::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled),[role="button"]:not(:disabled) {
cursor: pointer;
}
body {
@apply relative font-normal font-outfit z-1 bg-gray-50;
}
}
@utility menu-item {
@apply relative flex items-center w-full gap-3.5 px-4 py-3 font-medium rounded-lg text-theme-sm;
}
@utility menu-item-active {
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}
@utility menu-item-inactive {
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
}
/* Menu icon sizing - consistent across sidebar */
@utility menu-item-icon-size {
@apply w-6 h-6 flex-shrink-0;
/* Force SVG icons to inherit parent size */
& svg {
width: 100%;
height: 100%;
}
}
@utility menu-item-icon-active {
@apply text-brand-500;
}
@utility menu-item-icon-inactive {
@apply text-gray-500 group-hover:text-gray-700;
}
/* Dropdown menu items - increased spacing */
@utility menu-dropdown-item {
@apply block px-3.5 py-2.5 text-theme-sm font-medium rounded-md transition-colors;
}
@utility menu-dropdown-item-active {
@apply text-brand-600 bg-brand-50;
}
@utility menu-dropdown-item-inactive {
@apply text-gray-600 hover:text-gray-900 hover:bg-gray-50;
}
@utility menu-dropdown-badge {
@apply px-1.5 py-0.5 text-xs font-medium rounded;
}
@utility menu-dropdown-badge-active {
@apply bg-brand-100 text-brand-700;
}
@utility menu-dropdown-badge-inactive {
@apply bg-gray-100 text-gray-600;
}
@layer utilities {
input[type="date"]::-webkit-inner-spin-button,
input[type="time"]::-webkit-inner-spin-button,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
.tableCheckbox:checked ~ span span { @apply opacity-100; }
.tableCheckbox:checked ~ span { @apply border-brand-500 bg-brand-500; }
.apexcharts-legend-text { @apply !text-gray-700 dark:!text-gray-400; }
.apexcharts-text { @apply !fill-gray-700 dark:!fill-gray-400; }
.apexcharts-tooltip.apexcharts-theme-light { @apply gap-1 !rounded-lg !border-gray-200 p-3 !shadow-theme-sm dark:!border-gray-800 dark:!bg-gray-900; }
.apexcharts-tooltip-marker { margin-right: 6px; height: 6px; width: 6px; }
.apexcharts-legend-text { @apply !pl-5 !text-gray-700 dark:!text-gray-400; }
.apexcharts-tooltip-series-group { @apply !p-0; }
.apexcharts-tooltip-y-group { @apply !p-0; }
.apexcharts-tooltip-title { @apply !mb-0 !border-b-0 !bg-transparent !p-0 !text-[10px] !leading-4 !text-gray-800 dark:!text-white/90; }
.apexcharts-tooltip-text { @apply !text-theme-xs !text-gray-700 dark:!text-white/90; }
.apexcharts-tooltip-text-y-value { @apply !font-medium; }
.apexcharts-gridline { @apply !stroke-gray-100 dark:!stroke-gray-800; }
#chartTwo .apexcharts-datalabels-group { @apply !-translate-y-24; }
#chartTwo .apexcharts-datalabels-group .apexcharts-text { @apply !fill-gray-800 !font-semibold dark:!fill-white/90; }
#chartDarkStyle .apexcharts-datalabels-group .apexcharts-text { @apply !fill-gray-800 !font-semibold dark:!fill-white/90; }
#chartSixteen .apexcharts-legend { @apply !p-0 !pl-6; }
.jvectormap-container { @apply !bg-gray-50 dark:!bg-gray-900; }
.jvectormap-region.jvectormap-element { @apply !fill-gray-300 hover:!fill-brand-500 dark:!fill-gray-700 dark:hover:!fill-brand-500; }
.jvectormap-marker.jvectormap-element { @apply !stroke-gray-200 dark:!stroke-gray-800; }
.jvectormap-tip { @apply !bg-brand-500 !border-none !px-2 !py-1; }
.jvectormap-zoomin,.jvectormap-zoomout { @apply !bg-brand-500; }
.task-item + .task-item { @apply !border-t-gray-200 dark:!border-t-gray-700; }
.custom-input-date input::-webkit-calendar-picker-indicator { background-position: center; background-repeat: no-repeat; background-size: 20px; cursor: pointer; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.chat-height { height: calc(100vh - 8.125rem); }
.inbox-height { height: calc(100vh - 8.125rem); }
/* ===================================================================
IGNY8 ACTIVE UTILITY CLASSES
Migrated from igny8-colors.css - these are actively used in components
=================================================================== */
/* === Styled Dropdown/Select === */
.igny8-select-styled {
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%23647085' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 12px center !important;
padding-right: 36px !important;
}
.dark .igny8-select-styled {
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
}
/* === Header Metrics Bar (compact, right-aligned) === */
.igny8-header-metrics {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border-radius: 6px;
box-shadow: 0 2px 6px 3px rgba(0, 0, 0, 0.08);
}
.dark .igny8-header-metrics {
background: transparent;
box-shadow: 0 2px 6px 3px rgba(0, 0, 0, 0.08);
}
.igny8-header-metric {
display: flex;
align-items: center;
gap: 8px;
}
.igny8-header-metric-separator {
width: 1px;
height: 16px;
background: rgb(203 213 225);
opacity: 0.4;
}
.dark .igny8-header-metric-separator {
background: rgb(148 163 184);
opacity: 0.3;
}
.igny8-header-metric-label {
font-size: 13px;
font-weight: 500;
text-transform: capitalize;
letter-spacing: 0.3px;
color: rgb(100 116 139);
}
.dark .igny8-header-metric-label {
color: rgb(148 163 184);
}
.igny8-header-metric-value {
font-size: 16px;
font-weight: 700;
color: rgb(30 41 59);
margin-left: 4px;
}
.igny8-header-metric-value-credits {
font-size: 13px;
}
.dark .igny8-header-metric-value {
color: white;
}
.igny8-header-metric-accent {
width: 4px;
height: 20px;
border-radius: 5px;
}
.igny8-header-metric-accent.blue {
background: var(--color-primary);
}
.igny8-header-metric-accent.green {
background: var(--color-success);
}
.igny8-header-metric-accent.amber {
background: var(--color-warning);
}
.igny8-header-metric-accent.purple {
background: var(--color-purple);
}
/* === Table Compact Styles === */
.igny8-table-compact th {
padding: 12px 16px !important;
font-size: 14px !important;
font-weight: 600 !important;
color: var(--color-gray-600) !important;
text-align: left !important;
background-color: var(--color-gray-50) !important;
border-bottom: 2px solid var(--color-gray-200) !important;
text-transform: capitalize;
letter-spacing: 0.3px;
}
.dark .igny8-table-compact th {
color: var(--color-gray-200) !important;
background-color: rgba(15, 23, 42, 0.5) !important;
border-bottom-color: rgba(255, 255, 255, 0.1) !important;
}
.igny8-table-compact td {
padding: 8px 12px !important;
font-size: 14px !important;
border-bottom: 1px solid var(--color-gray-200) !important;
}
.dark .igny8-table-compact td {
border-bottom-color: rgba(255, 255, 255, 0.05) !important;
}
.igny8-table-compact th.text-center {
text-align: center !important;
}
.igny8-table-compact td.text-center {
text-align: center !important;
}
/* === Smooth Table Height Transitions === */
.igny8-table-container {
min-height: 500px;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
will-change: min-height;
}
.igny8-table-container.loading {
min-height: 500px;
overflow: hidden !important;
contain: layout style paint;
}
.igny8-table-container.loaded {
min-height: auto;
overflow: visible;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1);
animation: fadeInContainer 0.3s ease-out;
}
@keyframes fadeInContainer {
from { opacity: 0.95; }
to { opacity: 1; }
}
.igny8-table-wrapper {
width: 100%;
position: relative;
overflow-x: hidden;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
transition: opacity 0.4s ease-in-out;
contain: layout;
}
.igny8-table-container.loading .igny8-table-wrapper {
overflow-x: hidden !important;
overflow-y: hidden !important;
scrollbar-width: none;
-ms-overflow-style: none;
}
.igny8-table-container.loading .igny8-table-wrapper::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.igny8-table-container.loaded .igny8-table-wrapper {
overflow-x: auto;
overflow-y: hidden;
animation: showScrollbar 0.4s ease-out 0.3s both;
}
@keyframes showScrollbar {
from { scrollbar-width: none; }
to { scrollbar-width: thin; }
}
.igny8-table-smooth {
width: 100%;
table-layout: fixed;
min-width: 100%;
transition: opacity 0.5s ease-in-out;
contain: layout;
}
.igny8-table-container.loading .igny8-table-smooth {
opacity: 0.8;
visibility: visible;
}
.igny8-table-container.loaded .igny8-table-smooth {
opacity: 1;
table-layout: auto;
transition: opacity 0.5s ease-in-out, table-layout 0.1s ease-out;
}
.igny8-table-body {
position: relative;
min-height: 450px;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease-in-out;
contain: layout;
}
.igny8-table-container.loading .igny8-table-body {
min-height: 450px;
opacity: 1;
height: auto;
}
.igny8-table-container.loaded .igny8-table-body {
min-height: 0;
opacity: 1;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease-in-out;
}
.igny8-table-container.loading .igny8-table-body > tr:not(.igny8-skeleton-row) {
display: none !important;
visibility: hidden;
}
.igny8-table-container.loaded .igny8-table-body > tr.igny8-skeleton-row {
display: none !important;
visibility: hidden;
opacity: 0;
pointer-events: none;
}
.igny8-data-row {
animation: fadeInRow 0.6s ease-out forwards;
opacity: 0;
transform: translateY(8px);
}
@keyframes fadeInRow {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.igny8-skeleton-row {
animation: none !important;
opacity: 1 !important;
transform: none !important;
display: table-row !important;
}
.igny8-table-container.loading * {
backface-visibility: hidden;
perspective: 1000px;
}
/* === Filter Bar === */
.igny8-filter-bar {
display: flex;
align-items: center;
gap: 8px;
}
/* === Difficulty Badge Special Styling === */
.difficulty-badge {
border-radius: 3px !important;
min-width: 28px !important;
display: inline-flex !important;
justify-content: center !important;
align-items: center !important;
}
.difficulty-badge.difficulty-very-hard {
background-color: var(--color-error-600) !important;
color: white !important;
}
.dark .difficulty-badge.difficulty-very-hard {
background-color: var(--color-error-600) !important;
color: white !important;
}

View File

@@ -18,6 +18,7 @@ import {
FileIcon,
UserIcon,
UserCircleIcon,
ShootingStarIcon,
} from "../icons";
import { useSidebar } from "../context/SidebarContext";
import { useAuthStore } from "../store/authStore";
@@ -67,9 +68,16 @@ const AppSidebar: React.FC = () => {
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
// Module visibility is controlled by GlobalModuleSettings (Django Admin only)
const menuSections: MenuSection[] = useMemo(() => {
// SETUP section items - Ordered: Sites → Add Keywords → Content Settings → Thinker
// SETUP section items - Ordered: Setup Wizard → Sites → Add Keywords → Content Settings → Thinker
const setupItems: NavItem[] = [];
// Setup Wizard at top - guides users through site setup
setupItems.push({
icon: <ShootingStarIcon />,
name: "Setup Wizard",
path: "/setup/wizard",
});
// Sites is always visible - it's core functionality for managing sites
setupItems.push({
icon: <GridIcon />,

View File

@@ -1,6 +1,6 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import "./styles/design-system.css";
import "swiper/swiper-bundle.css";
import "flatpickr/dist/flatpickr.css";
import App from "./App";

View File

@@ -1,8 +1,7 @@
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap")
layer(base);
@import "../../styles/tokens.css";
@import "tailwindcss";
@import "../../styles/design-system.css";
@custom-variant dark (&:is(.dark *));

View File

@@ -0,0 +1,38 @@
/**
* Setup Wizard Page
* Wraps the OnboardingWizard component for direct access via sidebar
* Can be accessed anytime, not just for new users
*/
import { useNavigate } from 'react-router-dom';
import OnboardingWizard from '../../components/onboarding/OnboardingWizard';
import PageHeader from '../../components/common/PageHeader';
import { ShootingStarIcon } from '../../icons';
export default function SetupWizard() {
const navigate = useNavigate();
const handleComplete = () => {
navigate('/dashboard');
};
const handleSkip = () => {
navigate('/dashboard');
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<PageHeader
title="Setup Wizard"
badge={{ icon: <ShootingStarIcon className="w-5 h-5" />, color: 'blue' }}
description="Complete guided setup for your site"
/>
<div className="max-w-4xl mx-auto py-8 px-4">
<OnboardingWizard
onComplete={handleComplete}
onSkip={handleSkip}
/>
</div>
</div>
);
}

View File

@@ -1,24 +1,33 @@
# Design Tokens & Styles
# Design System Styles
This directory contains the centralized design token system for the IGNY8 application.
This directory contains the centralized design system for the IGNY8 application.
## Files
- `tokens.css` - **Single source of truth** for all design tokens (colors, gradients, shadows)
- `global.css` - Global base styles
- `cms/` - CMS-specific styles
- `design-system.css` - **SINGLE SOURCE OF TRUTH** for all styles
- Design tokens (colors, gradients, shadows, radii)
- Tailwind @theme configuration
- Dark mode overrides
- Component utilities
- All custom CSS classes
## Design Tokens (`tokens.css`)
## Quick Reference
All design tokens use plain naming (no "igny8" prefix):
### Entry Point
```tsx
// In main.tsx - THE ONLY CSS import needed
import "./styles/design-system.css";
```
### Color Tokens
- `--color-primary` - Primary brand blue (#2C7AA1)
- `--color-primary-dark` - Primary dark variant (#236082)
- `--color-success` - Success green (#2CA18E)
- `--color-warning` - Warning amber (#D9A12C)
- `--color-danger` - Danger red (#A12C40)
- `--color-purple` - Purple accent (#2C40A1)
```css
--color-primary /* Primary brand blue (#0077B6) */
--color-primary-dark /* Darker variant (#005A8C) */
--color-success /* Success green (#00B894) */
--color-warning /* Warning amber (#F59E0B) */
--color-danger /* Error red (#DC2626) */
--color-purple /* Premium purple (#7C3AED) */
```
### Usage
@@ -35,7 +44,7 @@ All design tokens use plain naming (no "igny8" prefix):
// Use colors.config.ts for programmatic access
import { getModuleCSSColor } from '@/config/colors.config';
const color = getModuleCSSColor('keywords'); // Returns computed CSS value
const color = getModuleCSSColor('keywords');
```
**❌ DON'T:**
@@ -50,22 +59,25 @@ const color = getModuleCSSColor('keywords'); // Returns computed CSS value
<div style={{ color: '#F59E0B' }}>Content</div>
```
## Active Utility Classes (in index.css)
## Utility Classes
The following utility classes are actively used:
- `.igny8-table-*` - Table styling utilities
- `.igny8-header-metric-*` - Header metrics bar styling
- `.igny8-select-styled` - Dropdown arrow styling
All utility classes are in `design-system.css`:
These are defined in `/src/index.css`, not here.
| Class | Purpose |
|-------|---------|
| `.menu-item` | Sidebar menu items |
| `.igny8-table-*` | Table styling utilities |
| `.igny8-header-metric-*` | Header metrics bar |
| `.igny8-select-styled` | Dropdown arrow styling |
| `.igny8-filter-bar` | Filter bar layout |
## Migration
All code should use:
1. Design tokens from `tokens.css` via CSS variables
1. Design tokens from `design-system.css` via CSS variables
2. Tailwind utilities mapped to design tokens (`bg-brand-500`, `text-success-500`)
3. `colors.config.ts` for programmatic color access
3. React components (`Button`, `Badge`, `Card`)
4. Standard React components (`Button`, `Badge`, `Card`)
See `../MIGRATION_GUIDE.md` for complete migration guide.
See `../DESIGN_SYSTEM.md` for complete guidelines.

View File

@@ -0,0 +1,972 @@
/* ===================================================================
IGNY8 DESIGN SYSTEM - SINGLE SOURCE OF TRUTH
===================================================================
This file consolidates ALL design tokens, color scales, utilities,
and component styles. Import this file in main.tsx/App.tsx.
🔒 STYLE LOCKED - All colors, spacing, and components must use
these tokens. See DESIGN_SYSTEM.md for usage guidelines.
Last Updated: 2026-01-01
=================================================================== */
@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap") layer(base);
/* ===================================================================
SECTION 1: DESIGN TOKENS (CSS VARIABLES)
=================================================================== */
:root {
/* -----------------------------------------------------------------
1.1 PRIMARY BRAND COLORS
----------------------------------------------------------------- */
/* Primary Blue - Electric & Confident */
--color-primary: #0077B6;
--color-primary-dark: #005A8C;
--color-primary-light: #00A8E8;
--color-primary-subtle: #E0F4FF;
/* Success - Fresh Mint Teal */
--color-success: #00B894;
--color-success-dark: #009676;
--color-success-light: #55EFC4;
--color-success-subtle: #E0FFF7;
/* Warning - Bold Amber */
--color-warning: #F59E0B;
--color-warning-dark: #D97706;
--color-warning-light: #FCD34D;
--color-warning-subtle: #FFF8E6;
/* Danger/Error - Vivid Red */
--color-danger: #DC2626;
--color-danger-dark: #B91C1C;
--color-danger-light: #F87171;
--color-danger-subtle: #FEE8E8;
/* Purple - Electric Violet (Premium/Special) */
--color-purple: #7C3AED;
--color-purple-dark: #5B21B6;
--color-purple-light: #A78BFA;
--color-purple-subtle: #F3EEFF;
/* -----------------------------------------------------------------
1.2 ACCENT COLORS (Charts, Tags, Highlights)
----------------------------------------------------------------- */
/* Coral */
--color-coral: #FF6B6B;
--color-coral-dark: #EE5A5A;
--color-coral-subtle: #FFF0F0;
/* Cyan */
--color-cyan: #06B6D4;
--color-cyan-dark: #0891B2;
--color-cyan-subtle: #E0FCFF;
/* Indigo */
--color-indigo: #4F46E5;
--color-indigo-dark: #4338CA;
--color-indigo-subtle: #EEF2FF;
/* Rose */
--color-rose: #F43F5E;
--color-rose-dark: #E11D48;
--color-rose-subtle: #FFF1F3;
/* Emerald */
--color-emerald: #10B981;
--color-emerald-dark: #059669;
--color-emerald-subtle: #ECFDF5;
/* Orange */
--color-orange: #F97316;
--color-orange-dark: #EA580C;
--color-orange-subtle: #FFF7ED;
/* -----------------------------------------------------------------
1.3 SURFACE & BACKGROUND COLORS
----------------------------------------------------------------- */
/* Sidebar */
--color-navy: #0F172A;
--color-navy-light: #1E293B;
--color-navy-lighter: #334155;
/* Page Backgrounds */
--color-surface: #F8FAFC;
--color-surface-alt: #F1F5F9;
/* Cards & Panels */
--color-panel: #FFFFFF;
--color-panel-alt: #F8FAFC;
--color-panel-hover: #F1F5F9;
/* Elevated Surfaces */
--color-elevated: #FFFFFF;
--color-overlay: rgba(15, 23, 42, 0.5);
/* -----------------------------------------------------------------
1.4 TEXT COLORS
----------------------------------------------------------------- */
--color-text: #0F172A;
--color-text-secondary: #475569;
--color-text-tertiary: #94A3B8;
--color-text-inverse: #FFFFFF;
--color-text-light: #E2E8F0;
--color-text-link: #0077B6;
--color-text-link-hover: #005A8C;
/* -----------------------------------------------------------------
1.5 BORDER & STROKE COLORS
----------------------------------------------------------------- */
--color-stroke: #E2E8F0;
--color-stroke-light: #F1F5F9;
--color-stroke-dark: #CBD5E1;
--color-stroke-focus: #0077B6;
/* -----------------------------------------------------------------
1.6 INTERACTIVE STATES
----------------------------------------------------------------- */
--color-hover: rgba(0, 119, 182, 0.08);
--color-active: rgba(0, 119, 182, 0.12);
--color-selected: rgba(0, 119, 182, 0.16);
--color-disabled: #94A3B8;
--color-disabled-bg: #F1F5F9;
/* -----------------------------------------------------------------
1.7 BORDER RADIUS
----------------------------------------------------------------- */
--radius-xs: 4px;
--radius-sm: 6px;
--radius-base: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 20px;
--radius-full: 9999px;
/* -----------------------------------------------------------------
1.8 SHADOWS
----------------------------------------------------------------- */
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 6px -1px rgba(15, 23, 42, 0.08), 0 2px 4px -1px rgba(15, 23, 42, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(15, 23, 42, 0.08), 0 4px 6px -2px rgba(15, 23, 42, 0.04);
--shadow-xl: 0 20px 25px -5px rgba(15, 23, 42, 0.10), 0 10px 10px -5px rgba(15, 23, 42, 0.04);
--shadow-glow-primary: 0 0 20px rgba(0, 119, 182, 0.3);
--shadow-glow-success: 0 0 20px rgba(0, 184, 148, 0.3);
--shadow-glow-purple: 0 0 20px rgba(124, 58, 237, 0.3);
/* -----------------------------------------------------------------
1.9 GRADIENTS
----------------------------------------------------------------- */
--gradient-primary: linear-gradient(135deg, #00A8E8 0%, #0077B6 50%, #005A8C 100%);
--gradient-primary-soft: linear-gradient(135deg, #E0F4FF 0%, #B8E4FF 100%);
--gradient-success: linear-gradient(135deg, #55EFC4 0%, #00B894 50%, #009676 100%);
--gradient-success-soft: linear-gradient(135deg, #E0FFF7 0%, #B8F5E8 100%);
--gradient-warning: linear-gradient(135deg, #FCD34D 0%, #F59E0B 50%, #D97706 100%);
--gradient-warning-soft: linear-gradient(135deg, #FFF8E6 0%, #FFEDB8 100%);
--gradient-danger: linear-gradient(135deg, #F87171 0%, #DC2626 50%, #B91C1C 100%);
--gradient-danger-soft: linear-gradient(135deg, #FEE8E8 0%, #FECACA 100%);
--gradient-purple: linear-gradient(135deg, #A78BFA 0%, #7C3AED 50%, #5B21B6 100%);
--gradient-purple-soft: linear-gradient(135deg, #F3EEFF 0%, #E4D8FF 100%);
--gradient-hero: linear-gradient(135deg, #0077B6 0%, #7C3AED 100%);
--gradient-premium: linear-gradient(135deg, #F59E0B 0%, #F97316 50%, #DC2626 100%);
--gradient-ocean: linear-gradient(135deg, #06B6D4 0%, #0077B6 50%, #4F46E5 100%);
--gradient-forest: linear-gradient(135deg, #10B981 0%, #00B894 50%, #06B6D4 100%);
--gradient-panel: linear-gradient(180deg, #FFFFFF 0%, #F8FAFC 100%);
--gradient-sidebar: linear-gradient(180deg, #1E293B 0%, #0F172A 100%);
/* -----------------------------------------------------------------
1.10 CHART COLORS (Data Visualization)
----------------------------------------------------------------- */
--chart-1: #0077B6;
--chart-2: #00B894;
--chart-3: #7C3AED;
--chart-4: #F59E0B;
--chart-5: #F43F5E;
--chart-6: #06B6D4;
--chart-7: #10B981;
--chart-8: #F97316;
/* -----------------------------------------------------------------
1.11 BADGE PRESETS
----------------------------------------------------------------- */
--badge-info-bg: var(--color-primary-subtle);
--badge-info-text: var(--color-primary-dark);
--badge-info-border: var(--color-primary-light);
--badge-success-bg: var(--color-success-subtle);
--badge-success-text: var(--color-success-dark);
--badge-success-border: var(--color-success-light);
--badge-warning-bg: var(--color-warning-subtle);
--badge-warning-text: var(--color-warning-dark);
--badge-warning-border: var(--color-warning-light);
--badge-danger-bg: var(--color-danger-subtle);
--badge-danger-text: var(--color-danger-dark);
--badge-danger-border: var(--color-danger-light);
--badge-purple-bg: var(--color-purple-subtle);
--badge-purple-text: var(--color-purple-dark);
--badge-purple-border: var(--color-purple-light);
--badge-neutral-bg: #F1F5F9;
--badge-neutral-text: #475569;
--badge-neutral-border: #CBD5E1;
}
/* ===================================================================
SECTION 2: DARK MODE OVERRIDES
=================================================================== */
.dark {
/* Backgrounds */
--color-surface: #0F172A;
--color-surface-alt: #1E293B;
--color-panel: #1E293B;
--color-panel-alt: #334155;
--color-panel-hover: #334155;
--color-elevated: #334155;
--color-overlay: rgba(0, 0, 0, 0.7);
/* Text */
--color-text: #F1F5F9;
--color-text-secondary: #CBD5E1;
--color-text-tertiary: #64748B;
--color-text-link: #00A8E8;
--color-text-link-hover: #38BDF8;
/* Borders */
--color-stroke: #334155;
--color-stroke-light: #1E293B;
--color-stroke-dark: #475569;
--color-stroke-focus: #00A8E8;
/* Interactive States */
--color-hover: rgba(0, 168, 232, 0.12);
--color-active: rgba(0, 168, 232, 0.18);
--color-selected: rgba(0, 168, 232, 0.24);
--color-disabled: #64748B;
--color-disabled-bg: #1E293B;
/* Primary Colors (brighter for dark mode) */
--color-primary: #00A8E8;
--color-primary-dark: #0077B6;
--color-primary-light: #38BDF8;
--color-primary-subtle: rgba(0, 168, 232, 0.15);
--color-success: #34D399;
--color-success-dark: #10B981;
--color-success-light: #6EE7B7;
--color-success-subtle: rgba(52, 211, 153, 0.15);
--color-warning: #FBBF24;
--color-warning-dark: #F59E0B;
--color-warning-light: #FDE68A;
--color-warning-subtle: rgba(251, 191, 36, 0.15);
--color-danger: #F87171;
--color-danger-dark: #EF4444;
--color-danger-light: #FCA5A5;
--color-danger-subtle: rgba(248, 113, 113, 0.15);
--color-purple: #A78BFA;
--color-purple-dark: #8B5CF6;
--color-purple-light: #C4B5FD;
--color-purple-subtle: rgba(167, 139, 250, 0.15);
/* Accents (brighter) */
--color-coral: #FCA5A5;
--color-coral-dark: #F87171;
--color-coral-subtle: rgba(252, 165, 165, 0.15);
--color-cyan: #22D3EE;
--color-cyan-dark: #06B6D4;
--color-cyan-subtle: rgba(34, 211, 238, 0.15);
--color-indigo: #818CF8;
--color-indigo-dark: #6366F1;
--color-indigo-subtle: rgba(129, 140, 248, 0.15);
/* Gradients */
--gradient-panel: linear-gradient(180deg, #1E293B 0%, #0F172A 100%);
--gradient-sidebar: linear-gradient(180deg, #0F172A 0%, #020617 100%);
--gradient-primary-soft: linear-gradient(135deg, rgba(0, 168, 232, 0.2) 0%, rgba(0, 119, 182, 0.1) 100%);
/* Shadows */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
--shadow-glow-primary: 0 0 30px rgba(0, 168, 232, 0.4);
--shadow-glow-success: 0 0 30px rgba(52, 211, 153, 0.4);
--shadow-glow-purple: 0 0 30px rgba(167, 139, 250, 0.4);
/* Chart Colors */
--chart-1: #38BDF8;
--chart-2: #34D399;
--chart-3: #A78BFA;
--chart-4: #FBBF24;
--chart-5: #FB7185;
--chart-6: #22D3EE;
--chart-7: #6EE7B7;
--chart-8: #FB923C;
/* Badges */
--badge-info-bg: rgba(0, 168, 232, 0.15);
--badge-info-text: #38BDF8;
--badge-info-border: rgba(0, 168, 232, 0.3);
--badge-success-bg: rgba(52, 211, 153, 0.15);
--badge-success-text: #34D399;
--badge-success-border: rgba(52, 211, 153, 0.3);
--badge-warning-bg: rgba(251, 191, 36, 0.15);
--badge-warning-text: #FBBF24;
--badge-warning-border: rgba(251, 191, 36, 0.3);
--badge-danger-bg: rgba(248, 113, 113, 0.15);
--badge-danger-text: #F87171;
--badge-danger-border: rgba(248, 113, 113, 0.3);
--badge-purple-bg: rgba(167, 139, 250, 0.15);
--badge-purple-text: #A78BFA;
--badge-purple-border: rgba(167, 139, 250, 0.3);
--badge-neutral-bg: #334155;
--badge-neutral-text: #CBD5E1;
--badge-neutral-border: #475569;
}
/* ===================================================================
SECTION 3: TAILWIND CONFIGURATION
=================================================================== */
/*
🚫 TAILWIND DEFAULT COLORS ARE DISABLED
Only these color palettes are available:
- brand-* (primary blue)
- gray-* (neutrals)
- success-* (green)
- error-* (red)
- warning-* (amber)
- purple-* (premium)
❌ These will NOT work: blue-*, red-*, green-*, emerald-*,
amber-*, indigo-*, pink-*, rose-*, sky-*, teal-*, etc.
*/
@custom-variant dark (&:is(.dark *));
@theme {
/* -----------------------------------------------------------------
DISABLE ALL TAILWIND DEFAULT COLOR PALETTES
This forces use of design system colors only
----------------------------------------------------------------- */
--color-red-*: initial;
--color-orange-*: initial;
--color-amber-*: initial;
--color-yellow-*: initial;
--color-lime-*: initial;
--color-green-*: initial;
--color-emerald-*: initial;
--color-teal-*: initial;
--color-cyan-*: initial;
--color-sky-*: initial;
--color-blue-*: initial;
--color-indigo-*: initial;
--color-violet-*: initial;
--color-fuchsia-*: initial;
--color-pink-*: initial;
--color-rose-*: initial;
--color-slate-*: initial;
--color-zinc-*: initial;
--color-neutral-*: initial;
--color-stone-*: initial;
/* -----------------------------------------------------------------
FONTS
----------------------------------------------------------------- */
--font-outfit: Outfit, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
/* -----------------------------------------------------------------
BREAKPOINTS
----------------------------------------------------------------- */
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
--breakpoint-3xl: 2000px;
/* Typography */
--text-title-2xl: 72px;
--text-title-2xl--line-height: 90px;
--text-title-xl: 60px;
--text-title-xl--line-height: 72px;
--text-title-lg: 48px;
--text-title-lg--line-height: 60px;
--text-title-md: 36px;
--text-title-md--line-height: 44px;
--text-title-sm: 30px;
--text-title-sm--line-height: 38px;
--text-theme-xl: 20px;
--text-theme-xl--line-height: 30px;
--text-theme-sm: 14px;
--text-theme-sm--line-height: 20px;
--text-theme-xs: 12px;
--text-theme-xs--line-height: 18px;
/* Base Colors */
--color-current: currentColor;
--color-transparent: transparent;
--color-white: #ffffff;
--color-black: #101828;
/* Brand Color Scale (Tailwind integration) */
--color-brand-25: color-mix(in srgb, var(--color-primary) 3%, white);
--color-brand-50: color-mix(in srgb, var(--color-primary) 8%, white);
--color-brand-100: color-mix(in srgb, var(--color-primary) 15%, white);
--color-brand-200: color-mix(in srgb, var(--color-primary) 25%, white);
--color-brand-300: color-mix(in srgb, var(--color-primary) 40%, white);
--color-brand-400: color-mix(in srgb, var(--color-primary) 60%, white);
--color-brand-500: var(--color-primary);
--color-brand-600: var(--color-primary-dark);
--color-brand-700: color-mix(in srgb, var(--color-primary-dark) 85%, black);
--color-brand-800: color-mix(in srgb, var(--color-primary-dark) 70%, black);
--color-brand-900: color-mix(in srgb, var(--color-primary-dark) 55%, black);
--color-brand-950: color-mix(in srgb, var(--color-primary-dark) 35%, black);
/* Gray Scale */
--color-gray-25: #fcfcfd;
--color-gray-50: #f9fafb;
--color-gray-100: #f2f4f7;
--color-gray-200: #e4e7ec;
--color-gray-300: #d0d5dd;
--color-gray-400: #98a2b3;
--color-gray-500: #667085;
--color-gray-600: #475467;
--color-gray-700: #344054;
--color-gray-800: #1d2939;
--color-gray-900: #101828;
--color-gray-950: #0c111d;
--color-gray-dark: #1a2231;
/* Success Scale */
--color-success-25: color-mix(in srgb, var(--color-success) 2%, white);
--color-success-50: color-mix(in srgb, var(--color-success) 6%, white);
--color-success-100: color-mix(in srgb, var(--color-success) 15%, white);
--color-success-200: color-mix(in srgb, var(--color-success) 30%, white);
--color-success-300: color-mix(in srgb, var(--color-success) 50%, white);
--color-success-400: color-mix(in srgb, var(--color-success) 75%, white);
--color-success-500: var(--color-success);
--color-success-600: var(--color-success-dark);
--color-success-700: color-mix(in srgb, var(--color-success-dark) 85%, black);
--color-success-800: color-mix(in srgb, var(--color-success-dark) 65%, black);
--color-success-900: color-mix(in srgb, var(--color-success-dark) 50%, black);
--color-success-950: color-mix(in srgb, var(--color-success-dark) 35%, black);
/* Error Scale */
--color-error-25: color-mix(in srgb, var(--color-danger) 2%, white);
--color-error-50: color-mix(in srgb, var(--color-danger) 5%, white);
--color-error-100: color-mix(in srgb, var(--color-danger) 10%, white);
--color-error-200: color-mix(in srgb, var(--color-danger) 20%, white);
--color-error-300: color-mix(in srgb, var(--color-danger) 40%, white);
--color-error-400: color-mix(in srgb, var(--color-danger) 65%, white);
--color-error-500: var(--color-danger);
--color-error-600: var(--color-danger-dark);
--color-error-700: color-mix(in srgb, var(--color-danger-dark) 80%, black);
--color-error-800: color-mix(in srgb, var(--color-danger-dark) 65%, black);
--color-error-900: color-mix(in srgb, var(--color-danger-dark) 50%, black);
--color-error-950: color-mix(in srgb, var(--color-danger-dark) 30%, black);
/* Warning Scale */
--color-warning-25: color-mix(in srgb, var(--color-warning) 2%, white);
--color-warning-50: color-mix(in srgb, var(--color-warning) 4%, white);
--color-warning-100: color-mix(in srgb, var(--color-warning) 10%, white);
--color-warning-200: color-mix(in srgb, var(--color-warning) 20%, white);
--color-warning-300: color-mix(in srgb, var(--color-warning) 40%, white);
--color-warning-400: color-mix(in srgb, var(--color-warning) 65%, white);
--color-warning-500: var(--color-warning);
--color-warning-600: var(--color-warning-dark);
--color-warning-700: color-mix(in srgb, var(--color-warning-dark) 80%, black);
--color-warning-800: color-mix(in srgb, var(--color-warning-dark) 65%, black);
--color-warning-900: color-mix(in srgb, var(--color-warning-dark) 50%, black);
--color-warning-950: color-mix(in srgb, var(--color-warning-dark) 30%, black);
/* Purple Scale */
--color-purple-25: color-mix(in srgb, var(--color-purple) 2%, white);
--color-purple-50: color-mix(in srgb, var(--color-purple) 8%, white);
--color-purple-100: color-mix(in srgb, var(--color-purple) 15%, white);
--color-purple-200: color-mix(in srgb, var(--color-purple) 25%, white);
--color-purple-300: color-mix(in srgb, var(--color-purple) 40%, white);
--color-purple-400: color-mix(in srgb, var(--color-purple) 60%, white);
--color-purple-500: var(--color-purple);
--color-purple-600: var(--color-purple-dark);
--color-purple-700: color-mix(in srgb, var(--color-purple-dark) 85%, black);
--color-purple-800: color-mix(in srgb, var(--color-purple-dark) 70%, black);
--color-purple-900: color-mix(in srgb, var(--color-purple-dark) 55%, black);
--color-purple-950: color-mix(in srgb, var(--color-purple-dark) 35%, black);
/* Blue-Light Scale (Info) */
--color-blue-light-25: #f0faff;
--color-blue-light-50: #e0f4ff;
--color-blue-light-100: #bae6fd;
--color-blue-light-200: #7dd3fc;
--color-blue-light-300: #38bdf8;
--color-blue-light-400: #0ea5e9;
--color-blue-light-500: #0284c7;
--color-blue-light-600: #0369a1;
--color-blue-light-700: #075985;
/* Shadows */
--shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
--shadow-theme-sm: 0px 1px 3px 0px rgba(16, 24, 40, 0.1), 0px 1px 2px 0px rgba(16, 24, 40, 0.06);
--shadow-theme-md: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
--shadow-theme-lg: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
--shadow-theme-xl: 0px 20px 24px -4px rgba(16, 24, 40, 0.08), 0px 8px 8px -4px rgba(16, 24, 40, 0.03);
--drop-shadow-4xl: 0 35px 35px rgba(0, 0, 0, 0.25), 0 45px 65px rgba(0, 0, 0, 0.15);
/* Z-Index */
--z-index-1: 1;
--z-index-9: 9;
--z-index-99: 99;
--z-index-999: 999;
--z-index-9999: 9999;
--z-index-99999: 99999;
--z-index-999999: 999999;
}
/* ===================================================================
SECTION 4: BASE STYLES
=================================================================== */
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
body {
@apply relative font-normal font-outfit z-1 bg-gray-50;
}
/* Hide date/time picker arrows */
input[type="date"]::-webkit-inner-spin-button,
input[type="time"]::-webkit-inner-spin-button,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
/* ===================================================================
SECTION 5: COMPONENT UTILITIES
=================================================================== */
/* -----------------------------------------------------------------
5.1 MENU ITEMS (Sidebar Navigation)
----------------------------------------------------------------- */
@utility menu-item {
@apply relative flex items-center w-full gap-3.5 px-4 py-3 font-medium rounded-lg text-theme-sm;
}
@utility menu-item-active {
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}
@utility menu-item-inactive {
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
}
@utility menu-item-icon-size {
@apply w-6 h-6 flex-shrink-0;
& svg { width: 100%; height: 100%; }
}
@utility menu-item-icon-active {
@apply text-brand-500;
}
@utility menu-item-icon-inactive {
@apply text-gray-500 group-hover:text-gray-700;
}
/* -----------------------------------------------------------------
5.2 DROPDOWN MENU ITEMS
----------------------------------------------------------------- */
@utility menu-dropdown-item {
@apply block px-3.5 py-2.5 text-theme-sm font-medium rounded-md transition-colors;
}
@utility menu-dropdown-item-active {
@apply text-brand-600 bg-brand-50;
}
@utility menu-dropdown-item-inactive {
@apply text-gray-600 hover:text-gray-900 hover:bg-gray-50;
}
@utility menu-dropdown-badge {
@apply px-1.5 py-0.5 text-xs font-medium rounded;
}
@utility menu-dropdown-badge-active {
@apply bg-brand-100 text-brand-700;
}
@utility menu-dropdown-badge-inactive {
@apply bg-gray-100 text-gray-600;
}
/* ===================================================================
SECTION 6: FORM ELEMENT STYLES
=================================================================== */
/* Styled Select/Dropdown with custom chevron */
.igny8-select-styled {
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%23647085' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 12px center !important;
padding-right: 36px !important;
}
.dark .igny8-select-styled {
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
}
/* Checkbox styling */
.tableCheckbox:checked ~ span span { @apply opacity-100; }
.tableCheckbox:checked ~ span { @apply border-brand-500 bg-brand-500; }
/* ===================================================================
SECTION 7: TABLE STYLES
=================================================================== */
/* Compact Table Headers */
.igny8-table-compact th {
padding: 12px 16px !important;
font-size: 14px !important;
font-weight: 600 !important;
color: var(--color-gray-600) !important;
text-align: left !important;
background-color: var(--color-gray-50) !important;
border-bottom: 2px solid var(--color-gray-200) !important;
text-transform: capitalize;
letter-spacing: 0.3px;
}
.dark .igny8-table-compact th {
color: var(--color-gray-200) !important;
background-color: rgba(15, 23, 42, 0.5) !important;
border-bottom-color: rgba(255, 255, 255, 0.1) !important;
}
.igny8-table-compact td {
padding: 8px 12px !important;
font-size: 14px !important;
border-bottom: 1px solid var(--color-gray-200) !important;
}
.dark .igny8-table-compact td {
border-bottom-color: rgba(255, 255, 255, 0.05) !important;
}
.igny8-table-compact th.text-center,
.igny8-table-compact td.text-center {
text-align: center !important;
}
/* Smooth Table Height Transitions */
.igny8-table-container {
min-height: 500px;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
will-change: min-height;
}
.igny8-table-container.loading {
min-height: 500px;
overflow: hidden !important;
contain: layout style paint;
}
.igny8-table-container.loaded {
min-height: auto;
overflow: visible;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1);
animation: fadeInContainer 0.3s ease-out;
}
.igny8-table-wrapper {
width: 100%;
position: relative;
overflow-x: hidden;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
transition: opacity 0.4s ease-in-out;
contain: layout;
}
.igny8-table-smooth {
width: 100%;
table-layout: fixed;
min-width: 100%;
transition: opacity 0.5s ease-in-out;
contain: layout;
}
.igny8-table-body {
position: relative;
min-height: 450px;
transition: min-height 0.8s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease-in-out;
contain: layout;
}
.igny8-data-row {
animation: fadeInRow 0.6s ease-out forwards;
opacity: 0;
transform: translateY(8px);
}
.igny8-skeleton-row {
animation: none !important;
opacity: 1 !important;
transform: none !important;
display: table-row !important;
}
/* ===================================================================
SECTION 8: HEADER METRICS BAR
=================================================================== */
.igny8-header-metrics {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border-radius: 6px;
box-shadow: 0 2px 6px 3px rgba(0, 0, 0, 0.08);
}
.dark .igny8-header-metrics {
background: transparent;
box-shadow: 0 2px 6px 3px rgba(0, 0, 0, 0.08);
}
.igny8-header-metric {
display: flex;
align-items: center;
gap: 8px;
}
.igny8-header-metric-separator {
width: 1px;
height: 16px;
background: rgb(203 213 225);
opacity: 0.4;
}
.dark .igny8-header-metric-separator {
background: rgb(148 163 184);
opacity: 0.3;
}
.igny8-header-metric-label {
font-size: 13px;
font-weight: 500;
text-transform: capitalize;
letter-spacing: 0.3px;
color: rgb(100 116 139);
}
.dark .igny8-header-metric-label {
color: rgb(148 163 184);
}
.igny8-header-metric-value {
font-size: 16px;
font-weight: 700;
color: rgb(30 41 59);
margin-left: 4px;
}
.igny8-header-metric-value-credits {
font-size: 13px;
}
.dark .igny8-header-metric-value {
color: white;
}
.igny8-header-metric-accent {
width: 4px;
height: 20px;
border-radius: 5px;
}
.igny8-header-metric-accent.blue { background: var(--color-primary); }
.igny8-header-metric-accent.green { background: var(--color-success); }
.igny8-header-metric-accent.amber { background: var(--color-warning); }
.igny8-header-metric-accent.purple { background: var(--color-purple); }
/* ===================================================================
SECTION 9: BADGE SPECIAL STYLES
=================================================================== */
.difficulty-badge {
border-radius: 3px !important;
min-width: 28px !important;
display: inline-flex !important;
justify-content: center !important;
align-items: center !important;
}
.difficulty-badge.difficulty-very-hard {
background-color: var(--color-error-600) !important;
color: white !important;
}
.dark .difficulty-badge.difficulty-very-hard {
background-color: var(--color-error-600) !important;
color: white !important;
}
/* ===================================================================
SECTION 10: CHART LIBRARY OVERRIDES (ApexCharts)
=================================================================== */
.apexcharts-legend-text { @apply !text-gray-700 dark:!text-gray-400; }
.apexcharts-text { @apply !fill-gray-700 dark:!fill-gray-400; }
.apexcharts-tooltip.apexcharts-theme-light { @apply gap-1 !rounded-lg !border-gray-200 p-3 !shadow-theme-sm dark:!border-gray-800 dark:!bg-gray-900; }
.apexcharts-tooltip-marker { margin-right: 6px; height: 6px; width: 6px; }
.apexcharts-legend-text { @apply !pl-5 !text-gray-700 dark:!text-gray-400; }
.apexcharts-tooltip-series-group { @apply !p-0; }
.apexcharts-tooltip-y-group { @apply !p-0; }
.apexcharts-tooltip-title { @apply !mb-0 !border-b-0 !bg-transparent !p-0 !text-[10px] !leading-4 !text-gray-800 dark:!text-white/90; }
.apexcharts-tooltip-text { @apply !text-theme-xs !text-gray-700 dark:!text-white/90; }
.apexcharts-tooltip-text-y-value { @apply !font-medium; }
.apexcharts-gridline { @apply !stroke-gray-100 dark:!stroke-gray-800; }
#chartTwo .apexcharts-datalabels-group { @apply !-translate-y-24; }
#chartTwo .apexcharts-datalabels-group .apexcharts-text { @apply !fill-gray-800 !font-semibold dark:!fill-white/90; }
#chartDarkStyle .apexcharts-datalabels-group .apexcharts-text { @apply !fill-gray-800 !font-semibold dark:!fill-white/90; }
#chartSixteen .apexcharts-legend { @apply !p-0 !pl-6; }
/* ===================================================================
SECTION 11: MAP LIBRARY OVERRIDES (jVectorMap)
=================================================================== */
.jvectormap-container { @apply !bg-gray-50 dark:!bg-gray-900; }
.jvectormap-region.jvectormap-element { @apply !fill-gray-300 hover:!fill-brand-500 dark:!fill-gray-700 dark:hover:!fill-brand-500; }
.jvectormap-marker.jvectormap-element { @apply !stroke-gray-200 dark:!stroke-gray-800; }
.jvectormap-tip { @apply !bg-brand-500 !border-none !px-2 !py-1; }
.jvectormap-zoomin, .jvectormap-zoomout { @apply !bg-brand-500; }
/* ===================================================================
SECTION 12: UTILITY CLASSES
=================================================================== */
/* Task dividers */
.task-item + .task-item { @apply !border-t-gray-200 dark:!border-t-gray-700; }
/* Date picker custom styling */
.custom-input-date input::-webkit-calendar-picker-indicator {
background-position: center;
background-repeat: no-repeat;
background-size: 20px;
cursor: pointer;
}
/* Scrollbar hiding */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Chat/Inbox heights */
.chat-height { height: calc(100vh - 8.125rem); }
.inbox-height { height: calc(100vh - 8.125rem); }
/* Filter bar */
.igny8-filter-bar {
display: flex;
align-items: center;
gap: 8px;
}
/* ===================================================================
SECTION 13: ANIMATIONS
=================================================================== */
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fadeInContainer {
from { opacity: 0.95; }
to { opacity: 1; }
}
@keyframes fadeInRow {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes showScrollbar {
from { scrollbar-width: none; }
to { scrollbar-width: thin; }
}
.animate-slide-in-right {
animation: slide-in-right 0.3s ease-out;
}
/* ===================================================================
END OF DESIGN SYSTEM
=================================================================== */

View File

@@ -1,32 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap")
layer(base);
@import "./tokens.css";
@import "tailwindcss";
@layer base {
*,
::before,
::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
font-family: Outfit, sans-serif;
background-color: #050913;
color: #ffffff;
}
body {
min-height: 100vh;
}
a {
color: inherit;
text-decoration: none;
}
}

View File

@@ -1,77 +0,0 @@
/* ===================================================================
DESIGN TOKENS
===================================================================
Single source of truth for all design tokens (colors, gradients, shadows, etc.)
Used by both Tailwind @theme and legacy CSS classes.
🔒 DESIGN SYSTEM LOCKED - See DESIGN_SYSTEM.md for complete style guide
=================================================================== */
:root {
/* ===================================================================
PRIMARY BRAND COLORS
=================================================================== */
--color-primary: #2C7AA1; /* Primary rich blue - main CTA */
--color-primary-dark: #236082; /* Darkened for hover / active / gradient depth */
/* Success Green (teal-aligned) */
--color-success: #2CA18E; /* Deep teal for success states */
--color-success-dark: #238272; /* Deeper teal for hover / active */
/* Amber / Warning (warm contrast to cool palette) */
--color-warning: #D9A12C; /* Golden amber for highlight / warning */
--color-warning-dark: #B8872A; /* Darker amber for hover / strong warning */
/* Danger / Destructive */
--color-danger: #A12C40; /* Deep red aligned with palette */
--color-danger-dark: #832334; /* Darker red for hover / active */
/* Purple (accent for special emphasis) */
--color-purple: #2C40A1; /* Navy-purple from analogous palette */
--color-purple-dark: #232F7A; /* Darker navy-purple for hover / active */
/* ===================================================================
BACKGROUND COLORS
=================================================================== */
--color-navy: #1A2B3C; /* Sidebar background (charcoal-navy) */
--color-navy-light: #243A4D; /* Slightly lighter navy, hover/active */
--color-surface: #F8FAFB; /* Page background (cool gray-white) */
--color-panel: #FFFFFF; /* Cards / panel foreground */
--color-panel-alt: #EEF4F7; /* Sub-panel / hover card background (teal-tinted) */
/* ===================================================================
TEXT COLORS
=================================================================== */
--color-text: #1A2B3C; /* Main headings/body text (matches navy) */
--color-text-dim: #5A6B7C; /* Secondary/subtext (mid-gray with blue undertone) */
--color-text-light: #E8F0F4; /* Text on dark sidebar */
--color-stroke: #D4E0E8; /* Table/grid borders and dividers (cool-tinted) */
/* ===================================================================
BORDER RADIUS
=================================================================== */
--radius-base: 6px;
/* ===================================================================
GRADIENTS
=================================================================== */
--gradient-primary: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
--gradient-success: linear-gradient(135deg, var(--color-success) 0%, var(--color-success-dark) 100%);
--gradient-warning: linear-gradient(135deg, var(--color-warning) 0%, var(--color-warning-dark) 100%);
--gradient-danger: linear-gradient(135deg, var(--color-danger) 0%, var(--color-danger-dark) 100%);
--gradient-purple: linear-gradient(135deg, var(--color-purple) 0%, var(--color-purple-dark) 100%);
--gradient-panel: linear-gradient(180deg, var(--color-panel) 0%, var(--color-panel-alt) 100%);
}
/* ===================================================================
DARK MODE OVERRIDES
=================================================================== */
.dark {
--color-surface: #1A2B3C; /* Charcoal navy (matches --color-navy) */
--color-panel: #243A4D; /* Slightly lighter navy for cards */
--color-panel-alt: #142230; /* Deeper navy for nested panels */
--color-text: #E8F0F4; /* Cool off-white (matches --color-text-light) */
--color-text-dim: #8A9BAC; /* Muted blue-gray for secondary text */
--color-stroke: #2E4A5E; /* Blue-tinted border color */
}