diff --git a/.github/workflows/bot-reviewer.yml b/.github/workflows/bot-reviewer.yml index 3c8cecf..60fc877 100644 --- a/.github/workflows/bot-reviewer.yml +++ b/.github/workflows/bot-reviewer.yml @@ -1,4 +1,4 @@ -name: PR Bot Reviewer +name: AIPCSS Bot Reviewer on: pull_request: @@ -10,336 +10,157 @@ permissions: checks: read jobs: - review: - name: Automated PR Review + # -- Backend Job -- + lint-backend: + name: Backend Quality runs-on: ubuntu-latest + outputs: + failed: ${{ steps.check.outputs.failed }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - # ── Backend Checks ────────────────────────────────────────── - - - name: Install backend lint tools + cache: 'pip' + - name: Install Tools run: pip install flake8 bandit - - - name: Run flake8 on changed Python files - id: flake8 + - name: Run Checks + id: check run: | - echo "## Flake8 Lint Report" > flake8_report.md - echo "" >> flake8_report.md - - if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q '^backend/.*\.py$'; then - CHANGED_PY=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^backend/.*\.py$') - RESULT=$(flake8 $CHANGED_PY --max-line-length=120 2>&1) - if [ $? -ne 0 ]; then - echo "### Issues Found" >> flake8_report.md - echo "\`\`\`" >> flake8_report.md - echo "$RESULT" >> flake8_report.md - echo "\`\`\`" >> flake8_report.md - echo "flake8_failed=true" >> $GITHUB_OUTPUT + echo "## Backend Report" > backend_report.md + PY_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^backend/.*\.py$' || true) + if [ -n "$PY_FILES" ]; then + set +e + flake8 $PY_FILES --max-line-length=120 > f8.txt 2>&1 + F8_EXIT=$? + bandit $PY_FILES -f txt > b.txt 2>&1 + B_EXIT=$? + set -e + if [ $F8_EXIT -ne 0 ] || [ $B_EXIT -ne 0 ]; then + echo "failed=true" >> $GITHUB_OUTPUT + echo "### Issues Found" >> backend_report.md + [ $F8_EXIT -ne 0 ] && echo "#### Flake8" >> backend_report.md && echo "\`\`\`" >> backend_report.md && cat f8.txt >> backend_report.md && echo "\`\`\`" >> backend_report.md + [ $B_EXIT -ne 0 ] && echo "#### Bandit Security" >> backend_report.md && echo "\`\`\`" >> backend_report.md && cat b.txt >> backend_report.md && echo "\`\`\`" >> backend_report.md else - echo "All Python files pass flake8 linting." >> flake8_report.md - echo "flake8_failed=false" >> $GITHUB_OUTPUT + echo "Passed: Backend quality checks passed." >> backend_report.md fi else - echo "No Python files changed." >> flake8_report.md - echo "flake8_failed=false" >> $GITHUB_OUTPUT + echo "No Python files changed." >> backend_report.md fi + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: backend-report + path: backend_report.md - - name: Run Bandit security scan on changed files - id: bandit - run: | - echo "## Security Scan Report" >> bandit_report.md - echo "" >> bandit_report.md - - if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q '^backend/.*\.py$'; then - CHANGED_PY=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^backend/.*\.py$') - RESULT=$(bandit $CHANGED_PY -f txt 2>&1) - if [ $? -ne 0 ]; then - echo "### Security Issues Found" >> bandit_report.md - echo "\`\`\`" >> bandit_report.md - echo "$RESULT" >> bandit_report.md - echo "\`\`\`" >> bandit_report.md - echo "bandit_failed=true" >> $GITHUB_OUTPUT - else - echo "No security issues found." >> bandit_report.md - echo "bandit_failed=false" >> $GITHUB_OUTPUT - fi - else - echo "No Python files changed." >> bandit_report.md - echo "bandit_failed=false" >> $GITHUB_OUTPUT - fi - - # ── Frontend Checks ───────────────────────────────────────── - - - name: Install frontend lint tools + # -- Frontend Job -- + lint-frontend: + name: Frontend Quality + runs-on: ubuntu-latest + outputs: + failed: ${{ steps.check.outputs.failed }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - name: Install working-directory: frontend run: npm ci - - - name: Run ESLint on changed frontend files - id: eslint - working-directory: frontend + - name: Run Checks + id: check run: | - cd $GITHUB_WORKSPACE - echo "## ESLint Report" > eslint_report.md - echo "" >> eslint_report.md - - CHANGED_TS=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^frontend/src/.*\.\(ts\|tsx\)$' || true) - - if [ -n "$CHANGED_TS" ]; then - RESULT=$(cd frontend && npx eslint $CHANGED_TS 2>&1) - if [ $? -ne 0 ]; then - echo "### Issues Found" >> eslint_report.md - echo "\`\`\`" >> eslint_report.md - echo "$RESULT" >> eslint_report.md - echo "\`\`\`" >> eslint_report.md - echo "eslint_failed=true" >> $GITHUB_OUTPUT + echo "## Frontend Report" > frontend_report.md + TS_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^frontend/src/.*\.\(ts\|tsx\)$' || true) + if [ -n "$TS_FILES" ]; then + set +e + npx eslint $TS_FILES > es.txt 2>&1 + ES_EXIT=$? + cd frontend && npx tsc --noEmit > ../tsc.txt 2>&1 + TSC_EXIT=$? + cd .. + set -e + if [ $ES_EXIT -ne 0 ] || [ $TSC_EXIT -ne 0 ]; then + echo "failed=true" >> $GITHUB_OUTPUT + echo "### Issues Found" >> frontend_report.md + [ $ES_EXIT -ne 0 ] && echo "#### ESLint" >> frontend_report.md && echo "\`\`\`" >> frontend_report.md && cat es.txt >> frontend_report.md && echo "\`\`\`" >> frontend_report.md + [ $TSC_EXIT -ne 0 ] && echo "#### TypeScript" >> frontend_report.md && echo "\`\`\`" >> frontend_report.md && cat tsc.txt >> frontend_report.md && echo "\`\`\`" >> frontend_report.md else - echo "All TypeScript files pass ESLint." >> eslint_report.md - echo "eslint_failed=false" >> $GITHUB_OUTPUT + echo "Passed: Frontend quality checks passed." >> frontend_report.md fi else - echo "No TypeScript files changed." >> eslint_report.md - echo "eslint_failed=false" >> $GITHUB_OUTPUT + echo "No TypeScript files changed." >> frontend_report.md fi + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: frontend-report + path: frontend_report.md - - name: Run TypeScript check - id: tsc - working-directory: frontend - run: | - cd $GITHUB_WORKSPACE - echo "## TypeScript Check Report" > tsc_report.md - echo "" >> tsc_report.md - - CHANGED_TS=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^frontend/src/.*\.\(ts\|tsx\)$' || true) - - if [ -n "$CHANGED_TS" ]; then - RESULT=$(cd frontend && ./node_modules/.bin/tsc -p tsconfig.json --noEmit 2>&1) - if [ $? -ne 0 ]; then - echo "### Type Errors Found" >> tsc_report.md - echo "\`\`\`" >> tsc_report.md - echo "$RESULT" >> tsc_report.md - echo "\`\`\`" >> tsc_report.md - echo "tsc_failed=true" >> $GITHUB_OUTPUT - else - echo "All TypeScript files compile successfully." >> tsc_report.md - echo "tsc_failed=false" >> $GITHUB_OUTPUT - fi - else - echo "No TypeScript files changed." >> tsc_report.md - echo "tsc_failed=false" >> $GITHUB_OUTPUT - fi - - # ── Code Quality Checks ───────────────────────────────────── - - - name: Check PR description - id: pr_desc - run: | - DESC="${{ github.event.pull_request.body }}" - if [ -z "$DESC" ] || [ "$DESC" == "null" ]; then - echo "pr_desc_ok=false" >> $GITHUB_OUTPUT - else - echo "pr_desc_ok=true" >> $GITHUB_OUTPUT - fi - - - name: Check file size (prevent large file commits) - id: file_size - run: | - echo "## File Size Check" > size_report.md - echo "" >> size_report.md - LARGE_FILES="" - - for file in $(git diff --name-only origin/${{ github.base_ref }}...HEAD); do - if [ -f "$file" ]; then - SIZE=$(wc -c < "$file") - if [ "$SIZE" -gt 500000 ]; then - LARGE_FILES="$LARGE_FILES\n- \`$file\` ($(( SIZE / 1024 )) KB)" - fi - fi - done - - if [ -n "$LARGE_FILES" ]; then - echo "### Large Files Detected (>500KB)" >> size_report.md - echo "" >> size_report.md - echo -e "$LARGE_FILES" >> size_report.md - echo "" >> size_report.md - echo "> Consider using Git LFS for large files." >> size_report.md - echo "size_ok=false" >> $GITHUB_OUTPUT - else - echo "All files are within size limits." >> size_report.md - echo "size_ok=true" >> $GITHUB_OUTPUT - fi - - - name: Check for TODO/FIXME/HACK comments - id: todo_check - run: | - echo "## TODO/FIXME Check" > todo_report.md - echo "" >> todo_report.md - - TODOS=$(git diff origin/${{ github.base_ref }}...HEAD | grep -E '^\+.*#?\s*(TODO|FIXME|HACK|XXX)' || true) - - if [ -n "$TODOS" ]; then - echo "### New TODO/FIXME/HACK Comments Found" >> todo_report.md - echo "" >> todo_report.md - echo "\`\`\`" >> todo_report.md - echo "$TODOS" >> todo_report.md - echo "\`\`\`" >> todo_report.md - echo "" >> todo_report.md - echo "> Please track these as GitHub Issues and reference them with issue numbers." >> todo_report.md - echo "todos_found=true" >> $GITHUB_OUTPUT - else - echo "No new TODO/FIXME/HACK comments detected." >> todo_report.md - echo "todos_found=false" >> $GITHUB_OUTPUT - fi - - # ── Generate and Post Review ──────────────────────────────── - - - name: Generate review body - id: review_body + # -- AI Review & Decision Job -- + ai-review: + name: AI Review and Approval + needs: [lint-backend, lint-frontend] + runs-on: ubuntu-latest + if: always() + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download Reports + uses: actions/download-artifact@v4 + with: + pattern: '*-report' + merge-multiple: true + - name: Gemini AI Review + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} run: | - cat > review.md << 'REVIEW_EOF' - ## 🤖 Automated PR Review - - _This review was automatically generated by the **AIPCSS Bot Reviewer**._ - - REVIEW_EOF - - # Approval status - if [ "${{ steps.flake8.outputs.flake8_failed }}" == "true" ] || \ - [ "${{ steps.eslint.outputs.eslint_failed }}" == "true" ] || \ - [ "${{ steps.tsc.outputs.tsc_failed }}" == "true" ] || \ - [ "${{ steps.bandit.outputs.bandit_failed }}" == "true" ]; then - echo "" >> review.md - echo "### ❌ Issues Found - Action Required" >> review.md - echo "" >> review.md - echo "This PR has **linting, type, or security issues** that need to be addressed before merging." >> review.md + echo "## AI Logic Review" > ai_report.md + if [ -z "$GEMINI_API_KEY" ]; then + echo "Warning: AI Review skipped (Secret missing)." >> ai_report.md else - echo "" >> review.md - echo "### ✅ All Checks Passed" >> review.md - echo "" >> review.md - echo "This PR passes all automated quality checks." >> review.md + DIFF=$(git diff origin/${{ github.base_ref }}...HEAD | head -c 15000) + CLEAN_DIFF=$(echo "$DIFF" | jq -aRs .) + RESPONSE=$(curl -s -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"contents\": [{\"parts\":[{\"text\": \"Review this git diff for logic errors and performance. Respond in Markdown.\n\n$CLEAN_DIFF\"}]}]}") + echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].text' >> ai_report.md fi - - # Append all reports - echo "" >> review.md - echo "---" >> review.md - cat flake8_report.md >> review.md - echo "" >> review.md - echo "---" >> review.md - cat bandit_report.md >> review.md - echo "" >> review.md - echo "---" >> review.md - cat eslint_report.md >> review.md - echo "" >> review.md - echo "---" >> review.md - cat tsc_report.md >> review.md - echo "" >> review.md - echo "---" >> review.md - cat size_report.md >> review.md - echo "" >> review.md - echo "---" >> review.md - cat todo_report.md >> review.md - - # PR description check - if [ "${{ steps.pr_desc.outputs.pr_desc_ok }}" == "false" ]; then - echo "" >> review.md - echo "---" >> review.md - echo "" >> review.md - echo "### ⚠️ Missing PR Description" >> review.md - echo "" >> review.md - echo "Please add a description explaining:" >> review.md - echo "- What changes were made" >> review.md - echo "- Why these changes are needed" >> review.md - echo "- How to test the changes" >> review.md - fi - - # Summary footer - cat >> review.md << 'FOOTER_EOF' - - --- - - ### 📋 Review Summary - - | Check | Status | - |-------|--------| - | Flake8 (Backend) | ${{ steps.flake8.outputs.flake8_failed == 'true' && '❌ Failed' || '✅ Passed' }} | - | Bandit (Security) | ${{ steps.bandit.outputs.bandit_failed == 'true' && '❌ Failed' || '✅ Passed' }} | - | ESLint (Frontend) | ${{ steps.eslint.outputs.eslint_failed == 'true' && '❌ Failed' || '✅ Passed' }} | - | TypeScript | ${{ steps.tsc.outputs.tsc_failed == 'true' && '❌ Failed' || '✅ Passed' }} | - | File Sizes | ${{ steps.file_size.outputs.size_ok == 'true' && '✅ Passed' || '⚠️ Warning' }} | - | PR Description | ${{ steps.pr_desc.outputs.pr_desc_ok == 'true' && '✅ Provided' || '⚠️ Missing' }} | - - FOOTER_EOF - - - name: Post PR review comment + - name: Post Decision uses: actions/github-script@v7 with: script: | const fs = require('fs'); - const reviewBody = fs.readFileSync('review.md', 'utf8'); - - // Find existing bot comment to update - const { data: comments } = await github.rest.issues.listComments({ - ...context.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(c => - c.user.type === 'Bot' && - c.body.includes('🤖 Automated PR Review') - ); - + const backend = fs.existsSync('backend_report.md') ? fs.readFileSync('backend_report.md', 'utf8') : 'No backend changes.'; + const frontend = fs.existsSync('frontend_report.md') ? fs.readFileSync('frontend_report.md', 'utf8') : 'No frontend changes.'; + const ai = fs.existsSync('ai_report.md') ? fs.readFileSync('ai_report.md', 'utf8') : 'AI review skipped.'; + + const hasFailed = '${{ needs.lint-backend.outputs.failed }}' === 'true' || '${{ needs.lint-frontend.outputs.failed }}' === 'true'; + + const reportBody = `## AIPCSS Bot Reviewer Report\n\n### Status: ${hasFailed ? 'Issues Found - Action Required' : 'All Quality Checks Passed'}\n\n---\n${ai}\n---\n${backend}\n---\n${frontend}`; + + // 1. Post Comment + const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number: context.issue.number }); + const botComment = comments.find(c => c.body.includes('AIPCSS Bot Reviewer Report')); if (botComment) { - await github.rest.issues.updateComment({ - ...context.repo, - comment_id: botComment.id, - body: reviewBody, - }); - console.log('Updated existing bot review comment'); + await github.rest.issues.updateComment({ ...context.repo, comment_id: botComment.id, body: reportBody }); } else { - await github.rest.issues.createComment({ - ...context.repo, - issue_number: context.issue.number, - body: reviewBody, - }); - console.log('Created new bot review comment'); - } - - - name: Set review status (approve or request changes) - if: success() - uses: actions/github-script@v7 - with: - script: | - const needsChanges = '${{ steps.flake8.outputs.flake8_failed }}' === 'true' || - '${{ steps.eslint.outputs.eslint_failed }}' === 'true' || - '${{ steps.tsc.outputs.tsc_failed }}' === 'true' || - '${{ steps.bandit.outputs.bandit_failed }}' === 'true'; - - if (needsChanges) { - await github.rest.pulls.createReview({ - ...context.repo, - pull_number: context.issue.number, - event: 'REQUEST_CHANGES', - body: '🤖 **Bot Reviewer**: This PR has linting, type, or security issues. Please fix the issues listed in the review comment above before merging.', - }); - console.log('Created REQUEST_CHANGES review'); - } else { - await github.rest.pulls.createReview({ - ...context.repo, - pull_number: context.issue.number, - event: 'APPROVE', - body: '🤖 **Bot Reviewer**: All automated checks passed. This PR looks good to merge!', - }); - console.log('Created APPROVE review'); + await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body: reportBody }); } + + // 2. Set PR Status (Approve or Request Changes) + await github.rest.pulls.createReview({ + ...context.repo, + pull_number: context.issue.number, + event: hasFailed ? 'REQUEST_CHANGES' : 'APPROVE', + body: hasFailed ? 'Bot Reviewer: Please fix the issues mentioned in the report.' : 'Bot Reviewer: All checks passed. Approved!' + }); diff --git a/frontend/src/components/ui/Loading.tsx b/frontend/src/components/ui/Loading.tsx index 481808d..520b59a 100644 --- a/frontend/src/components/ui/Loading.tsx +++ b/frontend/src/components/ui/Loading.tsx @@ -18,11 +18,11 @@ import React from 'react' // ── Spinner ─────────────────────────────────────────────────────────────────── -export function Spinner({ size = 20 }: { size?: number }) { +export function Spinner({ size = 20, color = 'currentColor' }: { size?: number; color?: string }) { return (
diff --git a/frontend/src/index.css b/frontend/src/index.css index 524e914..3c328f2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -6,56 +6,7 @@ Primary: Indigo (#4f46e5) · Accent: Amber (#d97706) · Surface: Slate ═══════════════════════════════════════════════════════════════════════════════ */ -@theme { - /* ── Primary: Indigo ─────────────────────────────────────────────────────── */ - --color-primary-50: #eef2ff; - --color-primary-100: #e0e7ff; - --color-primary-200: #c7d2fe; - --color-primary-300: #a5b4fc; - --color-primary-400: #818cf8; - --color-primary-500: #6366f1; - --color-primary-600: #4f46e5; - --color-primary-700: #4338ca; - --color-primary-800: #3730a3; - --color-primary-900: #312e81; - --color-primary-950: #1e1b4b; - - /* ── Surface: Slate ──────────────────────────────────────────────────────── */ - --color-surface-50: #f8fafc; - --color-surface-100: #f1f5f9; - --color-surface-200: #e2e8f0; - --color-surface-300: #cbd5e1; - --color-surface-400: #94a3b8; - --color-surface-500: #64748b; - --color-surface-600: #475569; - --color-surface-700: #334155; - --color-surface-800: #1e293b; - --color-surface-900: #0f172a; - --color-surface-950: #020617; - - /* ── Accent Colors ───────────────────────────────────────────────────────── */ - --color-accent-amber: #d97706; - --color-accent-teal: #14b8a6; - --color-accent-violet: #7c3aed; - --color-accent-rose: #f43f5e; - --color-accent-emerald: #10b981; - - /* ── Typography ──────────────────────────────────────────────────────────── */ - --font-sans: 'Inter', system-ui, sans-serif; - --font-display: 'Plus Jakarta Sans', system-ui, sans-serif; - - /* ── Border Radius ───────────────────────────────────────────────────────── */ - --radius-sm: 0.375rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; - --radius-2xl: 1.5rem; - - /* ── Shadows ─────────────────────────────────────────────────────────────── */ - --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-elevated: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-floating: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} +/* Theme variables moved to :root to resolve IDE warnings */ /* ═══════════════════════════════════════════════════════════════════════════════ Base Layer — CSS Custom Properties & Element Resets @@ -65,42 +16,96 @@ /* ── Light Mode Tokens ───────────────────────────────────────────────────── */ :root { + /* ── Primary: Indigo ─────────────────────────────────────────────────────── */ + --color-primary-50: #eef2ff; + --color-primary-100: #e0e7ff; + --color-primary-200: #c7d2fe; + --color-primary-300: #a5b4fc; + --color-primary-400: #818cf8; + --color-primary-500: #6366f1; + --color-primary-600: #4f46e5; + --color-primary-700: #4338ca; + --color-primary-800: #3730a3; + --color-primary-900: #312e81; + --color-primary-950: #1e1b4b; + + /* ── Surface: Slate ──────────────────────────────────────────────────────── */ + --color-surface-50: #f8fafc; + --color-surface-100: #f1f5f9; + --color-surface-200: #e2e8f0; + --color-surface-300: #cbd5e1; + --color-surface-400: #94a3b8; + --color-surface-500: #64748b; + --color-surface-600: #475569; + --color-surface-700: #334155; + --color-surface-800: #1e293b; + --color-surface-900: #0f172a; + --color-surface-950: #020617; + + /* ── Accent Colors ───────────────────────────────────────────────────────── */ + --color-accent-amber: #d97706; + --color-accent-teal: #14b8a6; + --color-accent-violet: #7c3aed; + --color-accent-rose: #f43f5e; + --color-accent-emerald: #10b981; + + /* ── Typography ──────────────────────────────────────────────────────────── */ + --font-sans: 'Inter', system-ui, sans-serif; + --font-display: 'Plus Jakarta Sans', system-ui, sans-serif; + + /* ── Border Radius ───────────────────────────────────────────────────────── */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.5rem; + + /* ── Shadows ─────────────────────────────────────────────────────────────── */ + --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-elevated: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-floating: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + /* ── UI Tokens ──────────────────────────────────────────────────────────── */ --bg-main: #f8fafc; --bg-card: #ffffff; --bg-sidebar: #0f172a; --bg-sidebar-hover: #1e293b; - --bg-sidebar-active: #4f46e5; + --bg-sidebar-active: #6366f1; --text-primary: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8; --text-sidebar: #94a3b8; --text-sidebar-active: #ffffff; --border: #e2e8f0; - --border-focus: #4f46e5; + --border-focus: #6366f1; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08); --scrollbar-thumb: rgba(0, 0, 0, 0.12); + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(255, 255, 255, 0.3); } /* ── Dark Mode Tokens ────────────────────────────────────────────────────── */ .dark { - --bg-main: #0f172a; - --bg-card: #1e293b; + --bg-main: #020617; + --bg-card: #0f172a; --bg-sidebar: #020617; --bg-sidebar-hover: #0f172a; - --bg-sidebar-active: #4f46e5; + --bg-sidebar-active: #6366f1; --text-primary: #f8fafc; - --text-secondary: #cbd5e1; - --text-muted: #94a3b8; + --text-secondary: #94a3b8; + --text-muted: #64748b; --text-sidebar: #64748b; --text-sidebar-active: #ffffff; - --border: #334155; + --border: #1e293b; --border-focus: #818cf8; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.25); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3); - --scrollbar-thumb: rgba(255, 255, 255, 0.1); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); + --shadow-md: 0 4px 12px -1px rgba(0, 0, 0, 0.6); + --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.7); + --scrollbar-thumb: rgba(255, 255, 255, 0.05); + --glass-bg: rgba(15, 23, 42, 0.7); + --glass-border: rgba(255, 255, 255, 0.1); } /* ── Box Model Reset ─────────────────────────────────────────────────────── */ @@ -180,13 +185,15 @@ border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-md); - transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.2s ease; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; } .card:hover { - transform: translateY(-2px); + transform: translateY(-4px); box-shadow: var(--shadow-lg); + border-color: var(--border-focus); } .card-elevated { @@ -196,6 +203,39 @@ box-shadow: var(--shadow-lg); } + .card-glass { + background: var(--glass-bg); + backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + } + + .glass-effect { + background: var(--glass-bg); + backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + } + + .glow-border { + position: relative; + } + + .glow-border::after { + content: ''; + position: absolute; + inset: -1px; + background: linear-gradient(135deg, var(--border-focus), transparent, var(--border-focus)); + border-radius: inherit; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; + } + + .glow-border:hover::after { + opacity: 0.5; + } + /* ── Buttons ─────────────────────────────────────────────────────────────── */ .btn { display: inline-flex; @@ -220,15 +260,16 @@ } .btn-primary { - background: #4f46e5; + background: linear-gradient(135deg, #6366f1, #818cf8); color: #ffffff; - box-shadow: var(--shadow-sm), 0 2px 8px rgba(79, 70, 229, 0.2); + box-shadow: 0 4px 15px -1px rgba(99, 102, 241, 0.4); + border: 1px solid rgba(255, 255, 255, 0.1); } .btn-primary:hover:not(:disabled) { - background: #4338ca; - box-shadow: var(--shadow-md), 0 4px 12px rgba(79, 70, 229, 0.3); - transform: translateY(-1px); + background: linear-gradient(135deg, #4f46e5, #6366f1); + box-shadow: 0 8px 20px -1px rgba(99, 102, 241, 0.5); + transform: translateY(-2px); } .btn-primary:active:not(:disabled) { @@ -587,11 +628,11 @@ width: 2.5rem; height: 2.5rem; border-radius: 0.875rem; - background: linear-gradient(135deg, #4f46e5, #7c3aed); + background: linear-gradient(135deg, var(--color-primary-600), var(--color-accent-violet)); display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } .brand-title { @@ -663,13 +704,13 @@ } .navbar-link.active { - color: #4f46e5; - background: rgba(79, 70, 229, 0.08); + color: var(--color-primary-500); + background: rgba(99, 102, 241, 0.1); } .dark .navbar-link.active { - color: #a5b4fc; - background: rgba(79, 70, 229, 0.12); + color: var(--color-primary-400); + background: rgba(99, 102, 241, 0.15); } .active-indicator { @@ -678,9 +719,9 @@ left: 20%; right: 20%; height: 3px; - background: #4f46e5; + background: var(--color-primary-500); border-radius: 99px 99px 0 0; - box-shadow: 0 -2px 10px rgba(79, 70, 229, 0.3); + box-shadow: 0 -2px 10px rgba(99, 102, 241, 0.3); } .navbar-actions { diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 87e41af..ad4c896 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -65,12 +65,12 @@ function StatCard({ icon, label, value, color, bgColor, path }: StatCardProps) { } const quickActions = [ - { label: 'Departments', icon: , path: '/departments', color: '#4f46e5' }, - { label: 'Teachers', icon: , path: '/teachers', color: '#059669' }, - { label: 'Timetable', icon: , path: '/timetable', color: '#d97706' }, - { label: 'Programs', icon: , path: '/programs', color: '#14b8a6' }, - { label: 'Batches', icon: , path: '/batches', color: '#f43f5e' }, - { label: 'Sections', icon: , path: '/sections', color: '#6366f1' }, + { label: 'Departments', icon: , path: '/departments', color: 'var(--color-primary-500)' }, + { label: 'Teachers', icon: , path: '/teachers', color: 'var(--color-accent-emerald)' }, + { label: 'Timetable', icon: , path: '/timetable', color: 'var(--color-accent-amber)' }, + { label: 'Programs', icon: , path: '/programs', color: 'var(--color-accent-teal)' }, + { label: 'Batches', icon: , path: '/batches', color: 'var(--color-accent-rose)' }, + { label: 'Sections', icon: , path: '/sections', color: 'var(--color-primary-400)' }, ] export function DashboardPage() { @@ -120,10 +120,10 @@ export function DashboardPage() { }, []) const chartData = [ - { name: 'Depts', value: stats.departments, color: '#4f46e5' }, - { name: 'Teachers', value: stats.teachers, color: '#059669' }, - { name: 'Courses', value: stats.courses, color: '#7c3aed' }, - { name: 'Rooms', value: stats.rooms, color: '#d97706' }, + { name: 'Depts', value: stats.departments, color: 'var(--color-primary-500)' }, + { name: 'Teachers', value: stats.teachers, color: 'var(--color-accent-emerald)' }, + { name: 'Courses', value: stats.courses, color: 'var(--color-accent-violet)' }, + { name: 'Rooms', value: stats.rooms, color: 'var(--color-accent-amber)' }, ] const roomCapacityData = stats.room_dist && stats.room_dist.length > 0 ? stats.room_dist.map((r, i) => ({ @@ -142,10 +142,10 @@ export function DashboardPage() { {/* 1. Welcome Banner */}
} label="Departments" value={stats.departments} - color="#4f46e5" bgColor="rgba(79,70,229,0.12)" path="/departments" /> + color="var(--color-primary-500)" bgColor="rgba(99, 102, 241, 0.15)" path="/departments" /> } label="Teachers" value={stats.teachers} - color="#059669" bgColor="rgba(5,150,105,0.12)" path="/teachers" /> + color="var(--color-accent-emerald)" bgColor="rgba(16, 185, 129, 0.15)" path="/teachers" /> } label="Courses" value={stats.courses} - color="#7c3aed" bgColor="rgba(124,58,237,0.12)" path="/courses" /> + color="var(--color-accent-violet)" bgColor="rgba(124, 58, 237, 0.15)" path="/courses" /> } label="Rooms" value={stats.rooms} - color="#d97706" bgColor="rgba(217,119,6,0.12)" path="/rooms" /> + color="var(--color-accent-amber)" bgColor="rgba(217, 119, 6, 0.15)" path="/rooms" /> )}
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index a6a975f..7bd9801 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -327,28 +327,52 @@ export function SettingsPage() {
{/* Modern Tab Navigation */} -
+
{[ { id: 'general', label: 'General', icon: Settings, desc: 'Days & basic config' }, { id: 'breaks', label: 'Breaks', icon: Coffee, desc: 'Rest periods' }, { id: 'timeslots', label: 'Time Slots', icon: Clock, desc: 'Class periods' }, { id: 'preview', label: 'Preview', icon: LayoutGrid, desc: 'Visual overview' }, - ].map(tab => ( - - ))} + style={{ + background: isActive ? 'var(--bg-card)' : 'transparent', + border: isActive ? '1px solid var(--color-primary-500)' : '1px solid transparent', + color: isActive ? 'var(--color-primary-600)' : 'var(--text-secondary)', + }} + > + +
+

+ {tab.label} +

+

+ {tab.desc} +

+
+ + ) + })}
{/* Content */} diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index ebb3b89..6905ab5 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -59,7 +59,7 @@ export function LoginPage() {
{/* Left panel - 60% */}
{/* Mobile logo - hidden on desktop */}
- {isLoading ? : null} + {isLoading ? : null} {isLoading ? 'Signing in...' : 'Sign in'} diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index 1234d6c..4d55d3b 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -64,7 +64,7 @@ export function RegisterPage() {
{/* Left panel - 60% */}
{/* Mobile logo - hidden on desktop */}