final section 10 -- and lgoabl styles adn compoeents plan
This commit is contained in:
@@ -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)
|
||||
|
||||
305
frontend/FRONTEND-AUDIT-PLAN.md
Normal file
305
frontend/FRONTEND-AUDIT-PLAN.md
Normal 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
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 *));
|
||||
|
||||
|
||||
38
frontend/src/pages/Setup/SetupWizard.tsx
Normal file
38
frontend/src/pages/Setup/SetupWizard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
972
frontend/src/styles/design-system.css
Normal file
972
frontend/src/styles/design-system.css
Normal 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
|
||||
=================================================================== */
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user