diff --git a/docs/plans/FRONTEND-AUDIT-PLAN.md b/docs/plans/FRONTEND-AUDIT-PLAN.md index a0841b95..1578384d 100644 --- a/docs/plans/FRONTEND-AUDIT-PLAN.md +++ b/docs/plans/FRONTEND-AUDIT-PLAN.md @@ -1,5 +1,26 @@ # Frontend Complete Refactoring & Standardization Plan +## ✅ STATUS: TAILWIND DEFAULT COLORS DISABLED + +**As of 2026-01-01**: All Tailwind default color palettes (blue-500, red-600, green-400, etc.) have been **disabled** via `@theme` in `design-system.css`. Only our custom design system colors work now. + +### What's Disabled (via `@theme { --color-*-*: initial; }`) +- `red-*`, `orange-*`, `amber-*`, `yellow-*`, `lime-*` +- `green-*`, `emerald-*`, `teal-*`, `cyan-*`, `sky-*` +- `blue-*`, `indigo-*`, `violet-*`, `fuchsia-*`, `pink-*`, `rose-*` +- `slate-*`, `zinc-*`, `neutral-*`, `stone-*` + +### What WORKS (our design system) +- `brand-*` (25-950) - derived from `--color-primary` +- `success-*` (25-950) - derived from `--color-success` +- `warning-*` (25-950) - derived from `--color-warning` +- `error-*` (25-950) - derived from `--color-danger` +- `purple-*` (25-950) - derived from `--color-purple` +- `gray-*` (25-950) - derived from `--color-gray-base` +- `blue-light-*` - info/link colors (derived from primary) + +--- + ## 🎯 Objective **Completely remove Tailwind default styles** and refactor the entire frontend to use **ONLY** the custom design system defined in `/src/styles/design-system.css`. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1232278..8a94d402 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -43,6 +43,7 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "@vitest/ui": "^1.0.4", + "autoprefixer": "^10.4.23", "eslint": "^9.19.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.18", @@ -3058,6 +3059,43 @@ "node": ">=4" } }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3081,6 +3119,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -3116,9 +3164,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -3136,10 +3184,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3232,9 +3281,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001699", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", - "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "dev": true, "funding": [ { @@ -3676,9 +3725,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.98", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.98.tgz", - "integrity": "sha512-bI/LbtRBxU2GzK7KK5xxFd2y9Lf9XguHooPYbcXWy6wUoT8NMnffsvRhPmSeUHLSDKAEtKuTaEtK4Ms15zkIEA==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -4243,6 +4292,20 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5623,9 +5686,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -5977,6 +6040,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/preact": { "version": "10.12.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", @@ -7007,9 +7077,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { diff --git a/frontend/package.json b/frontend/package.json index 08241774..3b55281f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "@vitest/ui": "^1.0.4", + "autoprefixer": "^10.4.23", "eslint": "^9.19.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.18", diff --git a/frontend/src/components/dashboard/StandardThreeWidgetFooter.tsx b/frontend/src/components/dashboard/StandardThreeWidgetFooter.tsx index 85ad9ec4..40121139 100644 --- a/frontend/src/components/dashboard/StandardThreeWidgetFooter.tsx +++ b/frontend/src/components/dashboard/StandardThreeWidgetFooter.tsx @@ -272,7 +272,7 @@ export default function StandardThreeWidgetFooter({ const moduleType: ModuleType = module || (moduleStats?.title?.toLowerCase().includes('planner') ? 'planner' : 'writer'); return ( -
+
{/* * Widget widths adjusted per requirements: * - Widget 1 (Page Progress): 28.3% (reduced by 5%) diff --git a/frontend/src/components/dashboard/ThreeWidgetFooter.tsx b/frontend/src/components/dashboard/ThreeWidgetFooter.tsx index 733ad039..446e9d2a 100644 --- a/frontend/src/components/dashboard/ThreeWidgetFooter.tsx +++ b/frontend/src/components/dashboard/ThreeWidgetFooter.tsx @@ -367,7 +367,7 @@ export default function ThreeWidgetFooter({ className = '', }: ThreeWidgetFooterProps) { return ( -
+
{/* * Widget widths adjusted per requirements: * - Widget 1 (Page Progress): 28.3% (reduced by 5%) diff --git a/frontend/src/components/onboarding/OnboardingWizard.tsx b/frontend/src/components/onboarding/OnboardingWizard.tsx index 1a2bc3ec..e9fc9e5e 100644 --- a/frontend/src/components/onboarding/OnboardingWizard.tsx +++ b/frontend/src/components/onboarding/OnboardingWizard.tsx @@ -191,8 +191,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar }; return ( -
- +
+ {/* Header */}
diff --git a/frontend/src/components/ui/badge/Badge.tsx b/frontend/src/components/ui/badge/Badge.tsx index f07d821a..417dd37c 100644 --- a/frontend/src/components/ui/badge/Badge.tsx +++ b/frontend/src/components/ui/badge/Badge.tsx @@ -105,9 +105,9 @@ const toneStyles: Record< }; const sizeClasses: Record = { - xs: "min-h-[20px] px-2.5 py-1 text-[11px] leading-[1.4]", - sm: "min-h-[24px] px-3 py-1 text-xs leading-[1.4]", - md: "min-h-[28px] px-3.5 py-1.5 text-sm leading-[1.4]", + xs: "min-h-[20px] px-2.5 py-1 leading-[1.4]", + sm: "min-h-[24px] px-3 py-1 leading-[1.4]", + md: "min-h-[28px] px-3.5 py-1.5 leading-[1.4]", }; const Badge: React.FC = ({ diff --git a/frontend/src/components/ui/table/index.tsx b/frontend/src/components/ui/table/index.tsx index 18dfd599..fa860e31 100644 --- a/frontend/src/components/ui/table/index.tsx +++ b/frontend/src/components/ui/table/index.tsx @@ -35,11 +35,12 @@ interface TableCellProps { children: ReactNode; // Cell content isHeader?: boolean; // If true, renders as , otherwise className?: string; // Optional className for styling + style?: React.CSSProperties; // Optional inline styles for width, fontSize, etc. } // Table Component const Table: React.FC = ({ children, className }) => { - return {children}
; + return {children}
; }; // TableHeader Component @@ -62,9 +63,10 @@ const TableCell: React.FC = ({ children, isHeader = false, className, + style, }) => { const CellTag = isHeader ? "th" : "td"; - return {children}; + return {children}; }; export { Table, TableHeader, TableBody, TableRow, TableCell }; diff --git a/frontend/src/config/pages/approved.config.tsx b/frontend/src/config/pages/approved.config.tsx index 304a3534..eae40241 100644 --- a/frontend/src/config/pages/approved.config.tsx +++ b/frontend/src/config/pages/approved.config.tsx @@ -62,17 +62,18 @@ export function createApprovedPageConfig(params: { label: 'Content Idea Title', sortable: true, sortField: 'title', + width: '400px', render: (value: string, row: Content) => (
{params.onRowClick ? ( ) : ( - + {value || `Content #${row.id}`} )} @@ -95,7 +96,6 @@ export function createApprovedPageConfig(params: { label: 'Status', sortable: true, sortField: 'status', - width: '130px', render: (value: string, row: Content) => { // Map internal status to user-friendly labels const statusConfig: Record = { @@ -251,8 +251,8 @@ export function createApprovedPageConfig(params: { sortable: false, // Backend doesn't support sorting by word_count sortField: 'word_count', numeric: true, - width: '100px', - align: 'right' as const, + align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => ( {value ? value.toLocaleString() : '-'} @@ -265,7 +265,8 @@ export function createApprovedPageConfig(params: { sortable: true, sortField: 'created_at', date: true, - width: '140px', + width: '130px', + hasActions: true, render: (value: string) => ( {formatRelativeDate(value)} diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 45b9fe22..4ac0e028 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -10,7 +10,7 @@ import { sectorColumn, difficultyColumn, statusColumn, - createdColumn, + createdWithActionsColumn, } from '../snippets/columns.snippets'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; @@ -107,6 +107,7 @@ export const createClustersPageConfig = ( label: 'Cluster Name', sortable: true, sortField: 'name', + width: '400px', render: (value: string, row: Cluster) => ( value.toLocaleString(), - }, - { - key: 'ideas_count', - label: 'Ideas', - sortable: false, // Backend doesn't support sorting by ideas_count - sortField: 'ideas_count', - width: '120px', align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { @@ -148,8 +140,8 @@ export const createClustersPageConfig = ( label: 'Volume', sortable: true, sortField: 'volume', - width: '120px', align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { @@ -159,6 +151,7 @@ export const createClustersPageConfig = ( sortable: true, sortField: 'difficulty', align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => { const difficultyNum = getDifficultyNumber(value); const difficultyBadgeColor = @@ -187,8 +180,8 @@ export const createClustersPageConfig = ( label: 'Content', sortable: false, // Backend doesn't support sorting by content_count sortField: 'content_count', - width: '120px', align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { @@ -205,9 +198,44 @@ export const createClustersPageConfig = ( }, }, { - ...createdColumn, + key: 'ideas_count', + label: 'Ideas', + sortable: false, // Backend doesn't support sorting by ideas_count + sortField: 'ideas_count', + align: 'center' as const, + headingAlign: 'center' as const, + render: (value: number) => value.toLocaleString(), + }, + // Generate Ideas action column - only shows button for status = 'new' + { + key: 'generate_action', + label: 'Generate Ideas', + sortable: false, + render: (_value: any, row: Cluster) => { + // Only show generate button for clusters with status 'new' + if (row.status === 'new' && handlers.onGenerateIdeas) { + return ( + + ); + } + return -; + }, + }, + { + ...createdWithActionsColumn, sortable: true, sortField: 'created_at', + width: '130px', render: (value: string) => formatRelativeDate(value), }, // Optional columns - hidden by default @@ -240,32 +268,6 @@ export const createClustersPageConfig = ( defaultVisible: false, render: (value: string) => formatRelativeDate(value), }, - // Generate Ideas action column - only shows button for status = 'new' - { - key: 'generate_action', - label: 'Generate Ideas', - sortable: false, - width: '120px', - render: (_value: any, row: Cluster) => { - // Only show generate button for clusters with status 'new' - if (row.status === 'new' && handlers.onGenerateIdeas) { - return ( - - ); - } - return -; - }, - }, ], filters: [ { diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index a0d7be97..5c64f3a8 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { titleColumn, statusColumn, - createdColumn, + createdWithActionsColumn, wordCountColumn, sectorColumn, } from '../snippets/columns.snippets'; @@ -100,17 +100,18 @@ export const createContentPageConfig = ( label: 'Content Idea Title', sortable: true, sortField: 'title', + width: '400px', render: (value: string, row: Content) => (
{handlers.onRowClick ? ( ) : ( -
+
{row.title || `Content #${row.id}`}
)} @@ -130,7 +131,6 @@ export const createContentPageConfig = ( label: 'Type', sortable: true, sortField: 'content_type', - width: '110px', render: (value: string) => { const label = TYPE_LABELS[value] || value || '-'; // Proper case: capitalize first letter only @@ -147,7 +147,6 @@ export const createContentPageConfig = ( label: 'Structure', sortable: true, sortField: 'content_structure', - width: '130px', render: (value: string) => { const label = STRUCTURE_LABELS[value] || value || '-'; // Proper case: capitalize first letter of each word @@ -165,16 +164,15 @@ export const createContentPageConfig = ( key: 'cluster_name', label: 'Cluster', sortable: false, - width: '130px', render: (_value: any, row: Content) => { const clusterName = row.cluster_name; if (!clusterName) { - return -; + return -; } return ( - - {clusterName} - + + {clusterName} + ); }, }, @@ -305,11 +303,11 @@ export const createContentPageConfig = ( ), }, { - ...createdColumn, + ...createdWithActionsColumn, sortable: true, sortField: 'created_at', label: 'Created', - align: 'right', + width: '130px', render: (value: string, row: Content) => { // Prompt icon logic (unchanged) const hasPrompts = row.has_image_prompts || false; @@ -335,7 +333,7 @@ export const createContentPageConfig = ( }; return ( -
+
{formatRelativeDate(value)} diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index e5f1ea10..9b9f5fa3 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -9,7 +9,7 @@ import { titleColumn, sectorColumn, statusColumn, - createdColumn, + createdWithActionsColumn, } from '../snippets/columns.snippets'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; @@ -98,6 +98,7 @@ export const createIdeasPageConfig = ( label: 'Content Idea Title', sortable: true, sortField: 'idea_title', + width: '400px', toggleable: true, // Enable toggle for this column toggleContentKey: 'description', // Use description field for toggle content toggleContentLabel: 'Content Outline', // Label for expanded content @@ -119,7 +120,6 @@ export const createIdeasPageConfig = ( label: 'Structure', sortable: false, // Backend doesn't support sorting by content_structure sortField: 'content_structure', - width: '130px', render: (value: string) => { const label = value?.replace('_', ' ') || '-'; const properCase = label.split(/[_\s]+/).map(word => @@ -137,7 +137,6 @@ export const createIdeasPageConfig = ( label: 'Type', sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', - width: '110px', render: (value: string) => { const label = value?.replace('_', ' ') || '-'; const properCase = label.charAt(0).toUpperCase() + label.slice(1); @@ -152,9 +151,8 @@ export const createIdeasPageConfig = ( key: 'target_keywords', label: 'Keywords', sortable: false, - width: '250px', render: (value: string) => ( - + {value || '-'} ), @@ -164,7 +162,6 @@ export const createIdeasPageConfig = ( label: 'Cluster', sortable: false, // Backend doesn't support sorting by keyword_cluster_id sortField: 'keyword_cluster_id', - width: '200px', render: (_value: string, row: ContentIdea) => { if (row.keyword_cluster_id && row.keyword_cluster_name) { return ( @@ -202,14 +199,15 @@ export const createIdeasPageConfig = ( label: 'Words', sortable: true, sortField: 'estimated_word_count', - width: '100px', align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { - ...createdColumn, + ...createdWithActionsColumn, sortable: true, sortField: 'created_at', + width: '130px', render: (value: string) => formatRelativeDate(value), }, // Optional columns - hidden by default diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index b3658eda..d8b25dcc 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -64,7 +64,7 @@ export const createImagesPageConfig = ( label: 'Content Title', sortable: true, sortField: 'content_title', - width: '250px', + width: '400px', render: (_value: string, row: ContentImagesGroup) => { const statusColors: Record = { draft: 'warning', @@ -130,13 +130,14 @@ export const createImagesPageConfig = ( }); } - // Add status column (separate from generate button) + // Add status column with actions (3-dot dropdown rendered by TablePageTemplate) columns.push({ key: 'overall_status', label: 'Status', sortable: true, sortField: 'overall_status', - width: '120px', + width: '130px', + hasActions: true, render: (value: string, row: ContentImagesGroup) => { const statusColors: Record = { 'complete': 'success', @@ -159,40 +160,6 @@ export const createImagesPageConfig = ( }, }); - // Add generate button column (separate from status) - columns.push({ - key: 'generate_action', - label: 'Actions', - sortable: false, - width: '120px', - render: (_value: any, row: ContentImagesGroup) => { - // Check if there are any pending images with prompts - const hasPendingImages = - (row.featured_image?.status === 'pending' && row.featured_image?.prompt) || - row.in_article_images.some(img => img.status === 'pending' && img.prompt); - - return ( - <> - {hasPendingImages && handlers.onGenerateImages ? ( - - ) : ( - - - )} - - ); - }, - }); - return { columns, filters: [ diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index 0aa8f250..777e1e0c 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -15,7 +15,7 @@ import { clusterColumn, sectorColumn, statusColumn, - createdColumn, + createdWithActionsColumn, } from '../snippets/columns.snippets'; // Icons removed - bulkActions and rowActions are now in table-actions.config.tsx import Badge from '../../components/ui/badge/Badge'; @@ -146,13 +146,19 @@ export const createKeywordsPageConfig = ( ...keywordColumn, sortable: false, // Backend doesn't support sorting by keyword field sortField: 'seed_keyword__keyword', + width: '300px', + render: (value: string) => ( + + {value || '-'} + + ), }, // Sector column - only show when viewing all sectors ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Keyword) => ( - {row.sector_name || '-'} + {row.sector_name || '-'} ), }] : []), @@ -161,16 +167,18 @@ export const createKeywordsPageConfig = ( sortable: true, sortField: 'seed_keyword__volume', // Backend expects seed_keyword__volume align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => value.toLocaleString(), }, { ...clusterColumn, sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', + width: '300px', render: (_value: string, row: Keyword) => row.cluster_name ? ( - - {row.cluster_name} - + + {row.cluster_name} + ) : -, }, { @@ -178,6 +186,7 @@ export const createKeywordsPageConfig = ( sortable: true, sortField: 'seed_keyword__difficulty', // Backend expects seed_keyword__difficulty align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => { const difficultyNum = getDifficultyNumber(value); const difficultyBadgeColor = @@ -194,7 +203,7 @@ export const createKeywordsPageConfig = ( : 'gray'; return typeof difficultyNum === 'number' ? ( - {difficultyNum} + {difficultyNum} ) : ( difficultyNum @@ -206,23 +215,12 @@ export const createKeywordsPageConfig = ( sortable: false, // Backend doesn't support sorting by country sortField: 'seed_keyword__country', align: 'center' as const, - render: (value: string) => { - const countryNames: Record = { - 'US': 'United States', - 'CA': 'Canada', - 'GB': 'United Kingdom', - 'AE': 'United Arab Emirates', - 'AU': 'Australia', - 'IN': 'India', - 'PK': 'Pakistan', - }; - const displayName = countryNames[value] || value || '-'; - return ( - - {value || '-'} - - ); - }, + headingAlign: 'center' as const, + render: (value: string) => ( + + {value || '-'} + + ), }, { ...statusColumn, @@ -242,16 +240,17 @@ export const createKeywordsPageConfig = ( size="xs" variant="soft" > - {properCase} + {properCase} ); }, }, { - ...createdColumn, + ...createdWithActionsColumn, label: 'Added', sortable: true, sortField: 'created_at', + width: '130px', render: (value: string) => formatRelativeDate(value), }, ], diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx index fb47dd62..53c80c5e 100644 --- a/frontend/src/config/pages/review.config.tsx +++ b/frontend/src/config/pages/review.config.tsx @@ -62,17 +62,18 @@ export function createReviewPageConfig(params: { label: 'Content Idea Title', sortable: true, sortField: 'title', + width: '400px', render: (value: string, row: Content) => (
{params.onRowClick ? ( ) : ( - + {value || `Content #${row.id}`} )} @@ -83,7 +84,6 @@ export function createReviewPageConfig(params: { key: 'categories', label: 'Categories', sortable: false, - width: '180px', render: (_value: any, row: Content) => { const categories = row.categories || []; if (!categories || categories.length === 0) { @@ -107,7 +107,6 @@ export function createReviewPageConfig(params: { key: 'tags', label: 'Tags', sortable: false, - width: '180px', render: (_value: any, row: Content) => { const tags = row.tags || []; if (!tags || tags.length === 0) { @@ -132,7 +131,6 @@ export function createReviewPageConfig(params: { label: 'Type', sortable: true, sortField: 'content_type', - width: '110px', render: (value: string) => { const label = TYPE_LABELS[value] || value || '-'; const properCase = label.charAt(0).toUpperCase() + label.slice(1); @@ -148,7 +146,6 @@ export function createReviewPageConfig(params: { label: 'Structure', sortable: true, sortField: 'content_structure', - width: '130px', render: (value: string) => { const label = STRUCTURE_LABELS[value] || value || '-'; const properCase = label.split(/[_\s]+/).map(word => @@ -165,16 +162,15 @@ export function createReviewPageConfig(params: { key: 'cluster_name', label: 'Cluster', sortable: false, - width: '150px', render: (_value: any, row: Content) => { const clusterName = row.cluster_name; if (!clusterName) { return -; } return ( - - {clusterName} - + + {clusterName} + ); }, }, @@ -183,7 +179,6 @@ export function createReviewPageConfig(params: { label: 'Status', sortable: true, sortField: 'status', - width: '120px', render: (value: string, row: Content) => { const status = value || 'draft'; const statusColors: Record = { @@ -214,7 +209,8 @@ export function createReviewPageConfig(params: { sortable: true, sortField: 'word_count', numeric: true, - width: '100px', + align: 'center' as const, + headingAlign: 'center' as const, render: (value: number) => ( {value?.toLocaleString() || 0} @@ -227,7 +223,8 @@ export function createReviewPageConfig(params: { sortable: true, sortField: 'created_at', date: true, - align: 'right', + width: '130px', + hasActions: true, render: (value: string) => ( {formatRelativeDate(value)} diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 42e5a2b1..dba0722c 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { titleColumn, statusColumn, - createdColumn, + createdWithActionsColumn, wordCountColumn, sectorColumn, } from '../snippets/columns.snippets'; @@ -104,6 +104,7 @@ export const createTasksPageConfig = ( label: 'Content Idea Title', sortable: true, sortField: 'title', + width: '400px', toggleable: true, toggleContentKey: 'description', toggleContentLabel: 'Idea & Content Outline', @@ -133,8 +134,11 @@ export const createTasksPageConfig = ( label: 'Cluster', sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', - width: '200px', - render: (_value: string, row: Task) => row.cluster_name || '-', + render: (_value: string, row: Task) => ( + + {row.cluster_name || '-'} + + ), }, { key: 'taxonomy_name', @@ -159,7 +163,6 @@ export const createTasksPageConfig = ( label: 'Type', sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', - width: '110px', render: (value: string) => { const label = TYPE_LABELS[value] || value || '-'; const properCase = label.charAt(0).toUpperCase() + label.slice(1); @@ -175,7 +178,6 @@ export const createTasksPageConfig = ( label: 'Structure', sortable: true, sortField: 'content_structure', - width: '130px', render: (value: string) => { const label = STRUCTURE_LABELS[value] || value || '-'; const properCase = label.split(/[_\s]+/).map(word => @@ -211,12 +213,14 @@ export const createTasksPageConfig = ( sortable: true, sortField: 'word_count', align: 'center' as const, + headingAlign: 'center' as const, render: (value: number | null | undefined) => (value != null ? value.toLocaleString() : '-'), }, { - ...createdColumn, + ...createdWithActionsColumn, sortable: true, sortField: 'created_at', + width: '130px', render: (value: string) => formatRelativeDate(value), }, // Optional columns - hidden by default diff --git a/frontend/src/config/snippets/columns.snippets.ts b/frontend/src/config/snippets/columns.snippets.ts index c66cd065..e5a59131 100644 --- a/frontend/src/config/snippets/columns.snippets.ts +++ b/frontend/src/config/snippets/columns.snippets.ts @@ -7,14 +7,12 @@ export const titleColumn = { key: 'title', label: 'Title', sortable: true, - width: 'auto', }; export const keywordColumn = { key: 'keyword', label: 'Keyword', sortable: true, - width: 'auto', }; export const statusColumn = { @@ -22,7 +20,6 @@ export const statusColumn = { label: 'Status', sortable: true, badge: true, - width: '120px', }; export const volumeColumn = { @@ -30,7 +27,7 @@ export const volumeColumn = { label: 'Volume', sortable: true, numeric: true, - width: '100px', + align: 'center' as const, }; export const difficultyColumn = { @@ -38,7 +35,7 @@ export const difficultyColumn = { label: 'Difficulty', sortable: true, badge: true, - width: '120px', + align: 'center' as const, }; export const countryColumn = { @@ -46,14 +43,12 @@ export const countryColumn = { label: 'Country', sortable: true, badge: true, - width: '120px', }; export const clusterColumn = { key: 'cluster', label: 'Cluster', sortable: true, - width: '200px', }; export const createdColumn = { @@ -61,7 +56,16 @@ export const createdColumn = { label: 'Created', sortable: true, date: true, - width: '100px', +}; + +// Combined column with date and actions in one cell +export const createdWithActionsColumn = { + key: 'created_at', + label: 'Created', + sortable: true, + date: true, + align: 'left' as const, + hasActions: true, // Special flag to indicate this column has row actions }; export const updatedColumn = { @@ -69,14 +73,12 @@ export const updatedColumn = { label: 'Modified', sortable: true, date: true, - width: '150px', }; export const actionsColumn = { key: 'actions', label: 'Actions', sortable: false, - width: '100px', fixed: true, }; @@ -85,13 +87,12 @@ export const wordCountColumn = { label: 'Words', sortable: true, numeric: true, - width: '120px', + align: 'center' as const, }; export const sectorColumn = { key: 'sector_name', label: 'Sector', sortable: false, - width: '150px', }; diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx index 23ae813f..084b9ca0 100644 --- a/frontend/src/pages/Planner/Keywords.tsx +++ b/frontend/src/pages/Planner/Keywords.tsx @@ -663,6 +663,7 @@ export default function Keywords() { data={keywords} loading={loading} showContent={showContent} + checkboxColumnWidth="40px" filters={pageConfig.filters} filterValues={{ search: searchTerm, diff --git a/frontend/src/pages/Setup/SetupWizard.tsx b/frontend/src/pages/Setup/SetupWizard.tsx index c847b251..aa63f239 100644 --- a/frontend/src/pages/Setup/SetupWizard.tsx +++ b/frontend/src/pages/Setup/SetupWizard.tsx @@ -20,14 +20,14 @@ export default function SetupWizard() { }; return ( -
+
, color: 'blue' }} description="Complete guided setup for your site" /> -
+
ReactNode; toggleable?: boolean; // If true, this column will have a toggle button for expanding content toggleContentKey?: string; // Key of the field containing content to display when toggled toggleContentLabel?: string; // Label for the expanded content (e.g., "Content Outline") defaultVisible?: boolean; // Whether column is visible by default (default: true) + hasActions?: boolean; // If true, this column will include row actions (3-dot menu) } interface FilterConfig { @@ -179,6 +181,8 @@ interface TablePageTemplateProps { }; // Custom row highlight function (returns bg class based on row data) getRowClassName?: (row: any) => string; + // Custom checkbox column width (default: w-12 = 48px) + checkboxColumnWidth?: string; } export default function TablePageTemplate({ @@ -217,6 +221,7 @@ export default function TablePageTemplate({ bulkActions: customBulkActions, primaryAction, getRowClassName, + checkboxColumnWidth = '48px', }: TablePageTemplateProps) { const location = useLocation(); const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false); @@ -799,7 +804,8 @@ export default function TablePageTemplate({ {selection && ( {showContent && ( { const isLastColumn = colIndex === visibleColumnsList.length - 1; + const hasActionsInColumn = column.hasActions && rowActions.length > 0; const displayName = column.label || formatColumnKey(column.key); + const headerAlign = column.headingAlign || column.align || 'start'; + const justifyClass = headerAlign === 'center' ? 'justify-center' : headerAlign === 'right' ? 'justify-end' : 'justify-start'; return ( 0 ? 'pr-16' : ''}`} + className={`font-medium text-gray-500 text-${hasActionsInColumn ? 'start' : headerAlign} dark:text-gray-400 ${column.sortable ? 'cursor-pointer hover:text-gray-700 dark:hover:text-gray-300' : ''} ${hasActionsInColumn ? 'pr-16' : ''}`} + style={column.width ? { width: column.width } : undefined} > -
-
+
+
{column.sortable ? (
handleSort(column)} className="flex items-center"> {displayName} @@ -915,7 +925,10 @@ export default function TablePageTemplate({ className={`igny8-data-row ${isRowAdded ? 'bg-brand-50 dark:bg-brand-500/10' : ''} ${customRowClass}`} > {selection && ( - + handleSelectRow(row.id?.toString() || '', checked)} @@ -925,11 +938,11 @@ export default function TablePageTemplate({ )} {visibleColumnsList.map((column, colIndex) => { - const isLastColumn = colIndex === visibleColumnsList.length - 1; const rowId = row.id || index; + const hasActionsInColumn = column.hasActions && rowActions.length > 0; // Get or create ref for this row's actions button - if (isLastColumn && rowActions.length > 0) { + if (hasActionsInColumn) { if (!rowActionButtonRefs.current.has(rowId)) { const ref = React.createRef(); rowActionButtonRefs.current.set(rowId, ref); @@ -939,7 +952,7 @@ export default function TablePageTemplate({ return ( 0 ? 'relative pr-16' : ''}`} + className={`text-${column.align || 'start'} text-gray-800 dark:text-white/90 ${hasActionsInColumn ? 'relative pr-16' : ''}`} >
@@ -962,8 +975,8 @@ export default function TablePageTemplate({ )}
- {/* Actions button - absolutely positioned in last column */} - {isLastColumn && rowActions.length > 0 && (() => { + {/* Actions button - absolutely positioned in column with hasActions flag */} + {hasActionsInColumn && (() => { // Check if row is already added - use same logic as handleBulkAddSelected const isRowAdded = !!(row as any).isAdded; @@ -1172,7 +1185,7 @@ export default function TablePageTemplate({ {/* Pagination - Compact icon-based pagination */} {pagination && ( -
+
Showing {data.length} of {pagination.totalCount} {selectionLabel || 'items'}