feat: monochrome design system #49
44
CLAUDE.md
44
CLAUDE.md
@@ -34,14 +34,15 @@ The project was rebranded from "Patherly" to "ResolutionFlow" in the frontend (P
|
||||
|
||||
**Brand details:**
|
||||
|
||||
- **Colors:** Purple gradient (`#818cf8` → `#a78bfa`)
|
||||
- **Fonts:** Plus Jakarta Sans (headings), Inter (body), Outfit (labels) - loaded via Google Fonts
|
||||
- **Logo:** Inline SVG in `AppLayout.tsx` and `LoginPage.tsx` (decision-tree icon with gradient)
|
||||
- **Colors:** Monochrome — pure black (`#000`) backgrounds, white text with opacity levels (PR #49)
|
||||
- **Fonts:** Inter (all text) — loaded via Google Fonts
|
||||
- **Logo:** Inline SVG in `BrandLogo.tsx` (decision-tree icon, white fill)
|
||||
- **Brand assets:** `brand-assets/` (source SVGs), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
|
||||
- **CSS utilities:** `text-gradient-brand`, `bg-gradient-brand`, `bg-gradient-brand-hover` (defined in `tailwind.config.js` and `index.css`)
|
||||
- **CSS utilities:** `glass-card`, `glass-card-hover`, `glass-card-glow`, `glass-stat` (defined in `index.css`)
|
||||
- **Design system guide:** `docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md`
|
||||
- **Rebrand guide:** [REBRAND-IMPLEMENTATION-GUIDE.md](REBRAND-IMPLEMENTATION-GUIDE.md)
|
||||
|
||||
When adding new frontend pages or components, use "ResolutionFlow" for any user-visible branding. Use `font-heading` for heading elements and the `text-gradient-brand` utility for brand-colored text.
|
||||
When adding new frontend pages or components, use "ResolutionFlow" for any user-visible branding. Follow the monochrome design system: black backgrounds, `glass-card` for containers, `text-white` with opacity variants for text hierarchy, white primary buttons, functional color only for status indicators.
|
||||
|
||||
---
|
||||
|
||||
@@ -59,7 +60,7 @@ When adding new frontend pages or components, use "ResolutionFlow" for any user-
|
||||
- Sessions tracking with decisions
|
||||
- Export API (Markdown, Text, HTML)
|
||||
- Tree Editor with form-based editing and visual preview
|
||||
- Dark/Light theme toggle
|
||||
- Dark-only monochrome design (theme toggle removed in PR #49)
|
||||
- Markdown rendering in session player and node editor
|
||||
- 7 comprehensive seed decision trees
|
||||
- **Tree Organization System:**
|
||||
@@ -74,7 +75,6 @@ When adding new frontend pages or components, use "ResolutionFlow" for any user-
|
||||
- **User Preferences (Issue #3):**
|
||||
- Settings page at `/settings`
|
||||
- Default export format preference (persisted in localStorage)
|
||||
- Theme toggle integrated in Settings
|
||||
- **Step Categories (Issue #5):**
|
||||
- Database table with 10 seeded global categories
|
||||
- Full CRUD API at `/api/v1/step-categories`
|
||||
@@ -92,9 +92,14 @@ When adding new frontend pages or components, use "ResolutionFlow" for any user-
|
||||
- Rating/review system with verified use tracking
|
||||
- **Frontend Rebrand (PR #26):**
|
||||
- Renamed from "Patherly" to "ResolutionFlow" in all user-facing UI
|
||||
- Purple gradient theme, custom fonts (Plus Jakarta Sans, Inter, Outfit)
|
||||
- Custom SVG logo in header and auth pages
|
||||
- Updated favicon and browser tab title
|
||||
- **Monochrome Design System (PR #49):**
|
||||
- Dark-only mode, theme toggle removed
|
||||
- Glass-morphism cards (`glass-card`, `glass-card-glow` CSS utilities)
|
||||
- 84 files migrated from themed/colored to monochrome
|
||||
- CSS variables remapped to monochrome, Tailwind config simplified (single font: Inter)
|
||||
- Functional color (green/red/yellow/blue) reserved for status indicators only
|
||||
- **Token Refresh Fix:**
|
||||
- Silent refresh with single-flight queue (prevents concurrent 401 race conditions)
|
||||
- Backend `get_refresh_token_payload` dependency extracts refresh token from Authorization header
|
||||
@@ -146,7 +151,7 @@ When adding new frontend pages or components, use "ResolutionFlow" for any user-
|
||||
- Main content adjusts width via padding transition when panel opens
|
||||
- **Global Thin Scrollbar Styling:**
|
||||
- 6px thin scrollbars site-wide (Firefox `scrollbar-width: thin` + WebKit pseudo-elements)
|
||||
- Theme-aware colors using CSS variables (`--border`, `--muted-foreground`)
|
||||
- Monochrome colors using CSS variables
|
||||
- **Admin Panel (Feb 2026):**
|
||||
- Full admin panel at `/admin/*` with 8 pages (dashboard, users, invite codes, audit logs, plan limits, feature flags, settings, categories)
|
||||
- Super admin access: requires `is_super_admin=true` on User model
|
||||
@@ -180,8 +185,8 @@ When adding new frontend pages or components, use "ResolutionFlow" for any user-
|
||||
### Frontend
|
||||
|
||||
- **Framework:** React 19 + Vite + TypeScript
|
||||
- **Styling:** Tailwind CSS v3 with ResolutionFlow brand theme (purple gradient)
|
||||
- **Fonts:** Plus Jakarta Sans (headings), Inter (body), Outfit (labels) via Google Fonts
|
||||
- **Styling:** Tailwind CSS v3 — monochrome glass-morphism design (dark-only)
|
||||
- **Fonts:** Inter (all text) via Google Fonts
|
||||
- **State:** Zustand (with immer + zundo for undo/redo)
|
||||
- **Routing:** React Router v7
|
||||
- **API Client:** Axios with token interceptors
|
||||
@@ -261,11 +266,11 @@ patherly/
|
||||
│ │ ├── hooks/ # Custom React hooks (useKeyboardShortcuts, usePermissions)
|
||||
│ │ ├── store/
|
||||
│ │ │ ├── authStore.ts # Zustand auth state
|
||||
│ │ │ ├── themeStore.ts # Dark/light theme
|
||||
│ │ │ ├── themeStore.ts # Unused (dark-only mode, no toggle)
|
||||
│ │ │ ├── treeEditorStore.ts # Tree editor state (immer + zundo)
|
||||
│ │ │ └── userPreferencesStore.ts # User preferences (NEW)
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── common/ # Modal, ErrorBoundary, ThemeToggle, ConfirmDialog
|
||||
│ │ │ ├── common/ # Modal, ErrorBoundary, ConfirmDialog, BrandLogo
|
||||
│ │ │ ├── layout/ # AppLayout, ProtectedRoute
|
||||
│ │ │ ├── tree-editor/ # Tree editor components
|
||||
│ │ │ ├── tree-preview/ # Visual tree preview
|
||||
@@ -676,7 +681,7 @@ Key JSONB structures stored in PostgreSQL. See `frontend/src/types/` for full Ty
|
||||
### State Management
|
||||
|
||||
- **Auth:** `useAuthStore` - Zustand with localStorage persistence (includes `setTokens` for silent refresh sync)
|
||||
- **Theme:** `useThemeStore` - Dark/light/system preference
|
||||
- **Theme:** Removed — dark-only mode (no theme toggle)
|
||||
- **Tree Editor:** `useTreeEditorStore` - Zustand + immer + zundo (undo/redo)
|
||||
- **User Preferences:** `useUserPreferencesStore` - Zustand with localStorage persistence (export format default)
|
||||
|
||||
@@ -689,6 +694,17 @@ Key JSONB structures stored in PostgreSQL. See `frontend/src/types/` for full Ty
|
||||
- Forms: Show field-level validation errors
|
||||
- Conditional rendering: Add null checks when node might not exist (`currentNode && currentNode.type`)
|
||||
|
||||
### Monochrome Design System Patterns
|
||||
|
||||
- **Backgrounds:** Pure black (`bg-black`), subtle radial gradients for depth
|
||||
- **Cards:** `glass-card rounded-2xl` (transparent gradient + backdrop-blur), NOT `bg-card border-border`
|
||||
- **Buttons:** Primary: `bg-white text-black hover:bg-white/90`. Secondary: `border border-white/10 text-white/60 hover:bg-white/10`
|
||||
- **Inputs:** `border-white/10 bg-black/50 text-white` + focus: `border-white/30 ring-white/20`
|
||||
- **Text hierarchy:** `text-white` → `text-white/70` → `text-white/40` → `text-white/30`
|
||||
- **Borders/dividers:** `border-white/[0.06]` or `border-white/10`
|
||||
- **Functional color only:** emerald-400 (success), red-400 (error), yellow-400 (warning), blue-400 (info)
|
||||
- **Reference:** `docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md` and `docs/plans/Frontend/COMPONENT_EXAMPLES.md`
|
||||
|
||||
### TypeScript Type Organization
|
||||
|
||||
- New type modules: Create in `types/` directory (e.g., `types/step.ts`)
|
||||
|
||||
290
docs/plans/Frontend/COMPONENT_EXAMPLES.md
Normal file
290
docs/plans/Frontend/COMPONENT_EXAMPLES.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Component Migration Examples
|
||||
|
||||
## Common Component Transformations
|
||||
|
||||
### 1. Card Component
|
||||
|
||||
**BEFORE (Old Design):**
|
||||
```tsx
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg p-6">
|
||||
<h3 className="text-slate-200">Title</h3>
|
||||
<p className="text-slate-400">Content</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**AFTER (New Design):**
|
||||
```tsx
|
||||
<div className="bg-gradient-to-br from-white/[0.04] to-white/[0.01] border border-white/8 backdrop-blur-xl rounded-2xl p-6 hover:from-white/[0.06] hover:to-white/[0.02] hover:border-white/12 transition-all">
|
||||
<h3 className="text-white font-bold">Title</h3>
|
||||
<p className="text-white/40">Content</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Active/Highlighted Card
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<div className="bg-purple-900/20 border border-purple-700 rounded-lg p-8">
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**AFTER (Bright Glow):**
|
||||
```tsx
|
||||
<div className="bg-gradient-to-br from-white/[0.08] to-white/[0.04] border border-white/20 rounded-2xl p-8 shadow-[0_0_40px_rgba(255,255,255,0.1)]">
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Icon in Card
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-600 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**AFTER (with subtle color):**
|
||||
```tsx
|
||||
<div className="w-12 h-12 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<Icon className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Badge/Status
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<span className="px-3 py-1 bg-purple-900 text-purple-200 rounded-md text-sm">
|
||||
Admin
|
||||
</span>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<div className="px-4 py-2 rounded-xl bg-white/10 border border-white/20">
|
||||
<span className="text-sm text-white font-semibold">Admin</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Primary Button
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<button className="px-6 py-3 bg-purple-600 hover:bg-purple-700 text-white rounded-lg">
|
||||
Click Me
|
||||
</button>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<button className="px-6 py-3 bg-white text-black font-semibold rounded-xl hover:bg-white/90 transition-all hover:scale-105">
|
||||
Click Me
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Secondary Button
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<button className="px-6 py-3 border border-slate-700 text-slate-300 hover:bg-slate-800 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<button className="px-6 py-3 bg-white/10 border border-white/20 text-white font-medium rounded-xl hover:bg-white/20 transition-all">
|
||||
Cancel
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Input/Search
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<input
|
||||
className="bg-slate-800 border border-slate-700 text-white placeholder-slate-400 rounded-lg px-4 py-3"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<div className="bg-gradient-to-br from-white/[0.04] to-white/[0.01] border border-white/8 backdrop-blur-xl rounded-2xl p-1">
|
||||
<div className="flex items-center bg-black/50 rounded-xl">
|
||||
<svg className="ml-5 w-5 h-5 text-blue-400">{/* search icon */}</svg>
|
||||
<input
|
||||
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Progress Bar
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<div className="h-2 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-purple-600 rounded-full" style={{width: '60%'}}></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-white rounded-full" style={{width: '60%'}}></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. Stat Card with Trend
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg p-6">
|
||||
<div className="text-slate-400 text-sm">Active Users</div>
|
||||
<div className="text-2xl font-bold text-white">1,234</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<div className="bg-[rgba(20,20,25,0.5)] border border-white/[0.06] backdrop-blur-xl rounded-2xl p-6 hover:scale-105 transition-transform">
|
||||
<div className="text-sm text-white/40 mb-2 font-medium">Active Users</div>
|
||||
<div className="text-4xl font-bold text-white mb-1">1,234</div>
|
||||
<div className="flex items-center gap-1 text-xs text-emerald-400 font-medium">
|
||||
<svg className="w-3 h-3">{/* up arrow */}</svg>
|
||||
12% vs last week
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. Section Header
|
||||
|
||||
**BEFORE:**
|
||||
```tsx
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-slate-100">Recent Trees</h2>
|
||||
<p className="text-slate-400 mt-1">Your recently accessed decision trees</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
```tsx
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Recent Trees</h2>
|
||||
<button className="text-sm text-white/60 hover:text-white font-medium transition-colors">
|
||||
View all →
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon Color Guidelines
|
||||
|
||||
### AI/Automation Icons
|
||||
```tsx
|
||||
<svg className="w-5 h-5 text-cyan-400">{/* sparkle/star */}</svg>
|
||||
```
|
||||
|
||||
### Search Icons
|
||||
```tsx
|
||||
<svg className="w-5 h-5 text-blue-400">{/* magnifying glass */}</svg>
|
||||
```
|
||||
|
||||
### Active/Playing State
|
||||
```tsx
|
||||
<svg className="w-6 h-6 text-violet-400">{/* play button */}</svg>
|
||||
```
|
||||
|
||||
### Network Category
|
||||
```tsx
|
||||
<svg className="w-6 h-6 text-blue-400">{/* wifi/network */}</svg>
|
||||
```
|
||||
|
||||
### Printer Category
|
||||
```tsx
|
||||
<svg className="w-6 h-6 text-indigo-400">{/* printer */}</svg>
|
||||
```
|
||||
|
||||
### Email Category
|
||||
```tsx
|
||||
<svg className="w-6 h-6 text-cyan-400">{/* envelope */}</svg>
|
||||
```
|
||||
|
||||
### Success Indicators
|
||||
```tsx
|
||||
<svg className="w-4 h-4 text-emerald-400">{/* check/arrow up */}</svg>
|
||||
```
|
||||
|
||||
### Error Indicators
|
||||
```tsx
|
||||
<svg className="w-4 h-4 text-red-400">{/* x/arrow down */}</svg>
|
||||
```
|
||||
|
||||
### Time/Clock (NO COLOR - keep gray)
|
||||
```tsx
|
||||
<svg className="w-4 h-4 text-white/30">{/* clock */}</svg>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
❌ **DON'T:**
|
||||
- Use colored backgrounds on cards
|
||||
- Use gradient text
|
||||
- Color ALL icons
|
||||
- Use slate-900, slate-800 (use white/opacity instead)
|
||||
- Use purple gradients anywhere
|
||||
|
||||
✅ **DO:**
|
||||
- Use white/black with opacity for all backgrounds
|
||||
- Keep text white (with varying opacity)
|
||||
- Only color functional icons
|
||||
- Use backdrop-blur on cards
|
||||
- Use white for primary buttons
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Opacity Levels
|
||||
|
||||
**Text:**
|
||||
- Primary: `text-white`
|
||||
- Secondary: `text-white/70`
|
||||
- Tertiary: `text-white/40`
|
||||
- Placeholder: `text-white/30`
|
||||
|
||||
**Backgrounds:**
|
||||
- Card: `bg-white/[0.04]` to `bg-white/[0.01]`
|
||||
- Card hover: `bg-white/[0.06]` to `bg-white/[0.02]`
|
||||
- Active card: `bg-white/[0.08]` to `bg-white/[0.04]`
|
||||
- Button secondary: `bg-white/10`
|
||||
- Badge: `bg-white/10`
|
||||
|
||||
**Borders:**
|
||||
- Subtle: `border-white/8`
|
||||
- Normal: `border-white/10`
|
||||
- Prominent: `border-white/20`
|
||||
- Active: `border-white/30`
|
||||
528
docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md
Normal file
528
docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# ResolutionFlow Design System Implementation Guide
|
||||
|
||||
## Overview
|
||||
This guide provides everything needed to implement the new monochrome design with subtle icon accents across the entire ResolutionFlow application.
|
||||
|
||||
## Design Philosophy
|
||||
- **95% Monochrome**: Pure black backgrounds, white text, transparent card overlays
|
||||
- **5% Color**: Subtle colors ONLY on functional icons
|
||||
- **Inspiration**: Plasma + Aspect templates (minimalist, modern, enterprise-grade)
|
||||
- **Key Principle**: Restraint and sophistication over flashy gradients
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Background Colors
|
||||
```css
|
||||
/* Main background - pure black with subtle gradient */
|
||||
background: linear-gradient(to bottom, #000000 0%, #0a0a0a 50%, #000000 100%);
|
||||
|
||||
/* Subtle radial overlays (optional, adds depth) */
|
||||
background: radial-gradient(circle at 50% 0%, rgba(100, 100, 120, 0.03), transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(80, 80, 100, 0.02), transparent 50%);
|
||||
```
|
||||
|
||||
### Text Colors
|
||||
```css
|
||||
/* Primary text */
|
||||
color: white;
|
||||
|
||||
/* Secondary text */
|
||||
color: rgba(255, 255, 255, 0.7); /* white/70 */
|
||||
|
||||
/* Tertiary text */
|
||||
color: rgba(255, 255, 255, 0.4); /* white/40 */
|
||||
|
||||
/* Subtle text */
|
||||
color: rgba(255, 255, 255, 0.3); /* white/30 */
|
||||
```
|
||||
|
||||
### Card/Surface Colors
|
||||
```css
|
||||
/* Standard cards */
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
/* Card hover state */
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
|
||||
/* Stat cards */
|
||||
background: rgba(20, 20, 25, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
/* Active/Highlighted card (Bright Glow) */
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 40px rgba(255, 255, 255, 0.1);
|
||||
```
|
||||
|
||||
### Icon Colors (THE ONLY COLORS IN THE APP)
|
||||
```css
|
||||
/* AI/Sparkle icons */
|
||||
color: #22d3ee; /* cyan-400 */
|
||||
|
||||
/* Search icons */
|
||||
color: #60a5fa; /* blue-400 */
|
||||
|
||||
/* Active/Play icons */
|
||||
color: #a78bfa; /* violet-400 */
|
||||
|
||||
/* Network category */
|
||||
color: #60a5fa; /* blue-400 */
|
||||
|
||||
/* Printer category */
|
||||
color: #818cf8; /* indigo-400 */
|
||||
|
||||
/* Email category */
|
||||
color: #22d3ee; /* cyan-400 */
|
||||
|
||||
/* Success/Up trends */
|
||||
color: #34d399; /* emerald-400 */
|
||||
|
||||
/* Failure/Down trends */
|
||||
color: #f87171; /* red-400 */
|
||||
|
||||
/* Neutral/Time icons (NO COLOR) */
|
||||
color: rgba(255, 255, 255, 0.5); /* gray */
|
||||
```
|
||||
|
||||
### Button Colors
|
||||
```css
|
||||
/* Primary button (white) */
|
||||
background: white;
|
||||
color: black;
|
||||
|
||||
/* Primary button hover */
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
|
||||
/* Secondary button */
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
|
||||
/* Secondary button hover */
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family
|
||||
```css
|
||||
font-family: 'Inter', sans-serif;
|
||||
```
|
||||
|
||||
### Font Weights
|
||||
- Regular: 400
|
||||
- Medium: 500
|
||||
- Semibold: 600
|
||||
- Bold: 700
|
||||
- Extrabold: 800
|
||||
|
||||
### Type Scale
|
||||
```css
|
||||
/* Hero heading */
|
||||
font-size: 3.5rem; /* 56px */
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
|
||||
/* Section heading */
|
||||
font-size: 2rem; /* 32px */
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
|
||||
/* Card title */
|
||||
font-size: 1.25rem; /* 20px */
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
|
||||
/* Body large */
|
||||
font-size: 1.25rem; /* 20px */
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
|
||||
/* Body */
|
||||
font-size: 1rem; /* 16px */
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
|
||||
/* Small */
|
||||
font-size: 0.875rem; /* 14px */
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
|
||||
/* Extra small */
|
||||
font-size: 0.75rem; /* 12px */
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Header/Navigation
|
||||
```jsx
|
||||
<header className="mb-16">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* White logo icon */}
|
||||
<div className="w-9 h-9 rounded-xl bg-white flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-black" /* icon */></svg>
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="px-4 py-2 rounded-xl bg-white/10 border border-white/20">
|
||||
<span className="text-sm text-white font-semibold">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
```
|
||||
|
||||
### Hero Section
|
||||
```jsx
|
||||
<div className="mb-20 text-center max-w-4xl mx-auto">
|
||||
{/* Badge with colored icon */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
|
||||
<svg className="w-4 h-4 text-cyan-400" /* sparkle icon */></svg>
|
||||
<span className="text-sm text-white/70 font-medium">AI-POWERED TROUBLESHOOTING</span>
|
||||
</div>
|
||||
|
||||
{/* Main heading */}
|
||||
<h1 className="text-5xl md:text-7xl font-bold text-white mb-6 tracking-tight leading-tight">
|
||||
All Your Tickets in One<br>
|
||||
<span className="text-white/60">Unified Dashboard</span>
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xl text-white/40 mb-12 max-w-2xl mx-auto leading-relaxed">
|
||||
Search our library of proven decision trees or continue where you left off
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Search Bar
|
||||
```jsx
|
||||
<div className="relative max-w-2xl mx-auto group">
|
||||
<div className="absolute inset-0 bg-white/5 rounded-2xl blur-2xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div className="relative bg-gradient-to-br from-white/[0.04] to-white/[0.01] border border-white/8 backdrop-blur-xl rounded-2xl p-1">
|
||||
<div className="flex items-center bg-black/50 rounded-xl">
|
||||
{/* Blue search icon */}
|
||||
<svg className="ml-5 w-5 h-5 text-blue-400" /* search icon */></svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
|
||||
/>
|
||||
{/* White button */}
|
||||
<button className="mr-2 px-5 py-2.5 bg-white text-black font-semibold rounded-lg hover:bg-white/90 transition-all">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Stat Card
|
||||
```jsx
|
||||
<div className="bg-[rgba(20,20,25,0.5)] border border-white/[0.06] backdrop-blur-xl rounded-2xl p-6 hover:scale-105 transition-transform">
|
||||
<div className="text-sm text-white/40 mb-2 font-medium">Active Sessions</div>
|
||||
<div className="text-4xl font-bold text-white mb-1">18</div>
|
||||
<div className="flex items-center gap-1 text-xs text-emerald-400 font-medium">
|
||||
{/* Green up arrow icon */}
|
||||
<svg className="w-3 h-3" /* arrow up icon */></svg>
|
||||
12% vs last week
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Active Session Card (Bright Glow)
|
||||
```jsx
|
||||
<div className="bg-gradient-to-br from-white/[0.08] to-white/[0.04] border border-white/20 backdrop-blur-xl rounded-2xl p-8 shadow-[0_0_40px_rgba(255,255,255,0.1)]">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{/* Icon with violet color */}
|
||||
<div className="w-12 h-12 rounded-xl bg-white/15 border border-white/30 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-violet-400" /* play icon */></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/70 font-semibold uppercase tracking-wider mb-1">
|
||||
Active Session
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white">Email Delivery Issues</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-white/50">Ticket #5847 • Started 2h ago • 11 of 15 steps</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-white text-black rounded-xl font-semibold hover:bg-white/90 transition-all">
|
||||
Continue →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-sm text-white/70 mb-3 font-medium">
|
||||
<span>Progress</span>
|
||||
<span>72%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-white rounded-full" style={{width: '72%'}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Tree Card
|
||||
```jsx
|
||||
<div className="bg-gradient-to-br from-white/[0.04] to-white/[0.01] border border-white/8 backdrop-blur-xl rounded-2xl p-6 hover:scale-105 hover:from-white/[0.06] hover:to-white/[0.02] hover:border-white/12 transition-all cursor-pointer">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
{/* Icon with category-specific color */}
|
||||
<div className="w-12 h-12 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-blue-400" /* network icon */></svg>
|
||||
</div>
|
||||
<div className="px-2 py-1 rounded-lg bg-white/10 border border-white/20">
|
||||
<span className="text-xs text-white/80 font-semibold">Available</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-lg font-bold text-white mb-2">Network Connectivity</h4>
|
||||
<p className="text-sm text-white/40 mb-4">Diagnose and resolve network issues</p>
|
||||
<div className="flex items-center gap-1.5 text-xs text-white/30">
|
||||
{/* Gray clock icon (NO COLOR) */}
|
||||
<svg className="w-3.5 h-3.5 text-slate-500" /* clock icon */></svg>
|
||||
Last used 5h ago
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Primary Button (White)
|
||||
```jsx
|
||||
<button className="px-8 py-4 bg-white text-black font-bold rounded-xl hover:bg-white/90 transition-all hover:scale-105">
|
||||
Get Started Free
|
||||
</button>
|
||||
```
|
||||
|
||||
### Secondary Button (Transparent)
|
||||
```jsx
|
||||
<button className="px-8 py-4 bg-white/10 border border-white/20 text-white font-semibold rounded-xl hover:bg-white/20 transition-all">
|
||||
Book a Demo
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon Color Mapping
|
||||
|
||||
### Where to use each color:
|
||||
|
||||
**Cyan (#22d3ee / cyan-400):**
|
||||
- AI/Sparkle icons
|
||||
- Email-related icons
|
||||
- Magic/automation indicators
|
||||
|
||||
**Blue (#60a5fa / blue-400):**
|
||||
- Search icons
|
||||
- Network-related icons
|
||||
- General tech icons
|
||||
|
||||
**Violet (#a78bfa / violet-400):**
|
||||
- Active/playing state icons
|
||||
- Current session indicators
|
||||
- Progress-related icons
|
||||
|
||||
**Indigo (#818cf8 / indigo-400):**
|
||||
- Printer-related icons
|
||||
- Hardware icons
|
||||
|
||||
**Emerald (#34d399 / emerald-400):**
|
||||
- Success indicators
|
||||
- Up arrows/trends
|
||||
- Positive metrics
|
||||
|
||||
**Red (#f87171 / red-400):**
|
||||
- Error indicators
|
||||
- Down arrows/trends
|
||||
- Negative metrics
|
||||
|
||||
**Gray (rgba(255,255,255,0.5) / white/50):**
|
||||
- Time/clock icons
|
||||
- Neutral informational icons
|
||||
- Non-critical icons
|
||||
|
||||
---
|
||||
|
||||
## Tailwind Classes Reference
|
||||
|
||||
### Background Patterns
|
||||
```
|
||||
bg-black
|
||||
bg-white
|
||||
bg-white/5 (5% opacity)
|
||||
bg-white/10 (10% opacity)
|
||||
bg-white/15 (15% opacity)
|
||||
```
|
||||
|
||||
### Text Opacity
|
||||
```
|
||||
text-white
|
||||
text-white/70
|
||||
text-white/60
|
||||
text-white/50
|
||||
text-white/40
|
||||
text-white/30
|
||||
```
|
||||
|
||||
### Border Opacity
|
||||
```
|
||||
border-white/8
|
||||
border-white/10
|
||||
border-white/12
|
||||
border-white/20
|
||||
border-white/30
|
||||
```
|
||||
|
||||
### Icon Colors
|
||||
```
|
||||
text-cyan-400
|
||||
text-blue-400
|
||||
text-violet-400
|
||||
text-indigo-400
|
||||
text-emerald-400
|
||||
text-red-400
|
||||
text-slate-500
|
||||
```
|
||||
|
||||
### Rounded Corners
|
||||
```
|
||||
rounded-xl (12px - cards, buttons)
|
||||
rounded-2xl (16px - large cards)
|
||||
rounded-3xl (24px - hero sections)
|
||||
rounded-full (perfect circle - badges, dots)
|
||||
rounded-lg (8px - small elements)
|
||||
```
|
||||
|
||||
### Spacing
|
||||
```
|
||||
gap-1, gap-2, gap-3, gap-4, gap-6, gap-8
|
||||
p-4, p-6, p-8, p-12
|
||||
mb-2, mb-3, mb-4, mb-6, mb-8, mb-12, mb-16, mb-20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Animation & Transitions
|
||||
|
||||
### Standard Transition
|
||||
```css
|
||||
transition: all 0.2s ease;
|
||||
```
|
||||
|
||||
### Hover Scale
|
||||
```css
|
||||
hover:scale-105
|
||||
transition-transform
|
||||
```
|
||||
|
||||
### Hover Opacity
|
||||
```css
|
||||
hover:opacity-100
|
||||
transition-opacity
|
||||
```
|
||||
|
||||
### Card Hover Glow
|
||||
```css
|
||||
hover:shadow-[0_0_40px_rgba(255,255,255,0.15)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1: Global Styles
|
||||
- [ ] Update background color to pure black gradient
|
||||
- [ ] Set font-family to Inter
|
||||
- [ ] Add subtle radial gradient overlays
|
||||
|
||||
### Phase 2: Component Updates
|
||||
- [ ] Header/Navigation
|
||||
- [ ] Hero sections
|
||||
- [ ] Search bars
|
||||
- [ ] Stat cards
|
||||
- [ ] Active session cards (Bright Glow)
|
||||
- [ ] Tree/item cards
|
||||
- [ ] Buttons (primary & secondary)
|
||||
- [ ] Forms/inputs
|
||||
- [ ] Modals
|
||||
- [ ] Empty states
|
||||
|
||||
### Phase 3: Icon Colors
|
||||
- [ ] Map all icons to appropriate colors
|
||||
- [ ] Keep time/clock icons gray
|
||||
- [ ] Add category colors to tree icons
|
||||
|
||||
### Phase 4: Polish
|
||||
- [ ] Add hover states to all interactive elements
|
||||
- [ ] Test backdrop-filter blur support
|
||||
- [ ] Verify contrast ratios for accessibility
|
||||
- [ ] Test on dark displays
|
||||
|
||||
---
|
||||
|
||||
## Notes for Claude Code
|
||||
|
||||
1. **Preserve existing functionality** - Only change visual styling, not logic
|
||||
2. **Use Tailwind classes** - Avoid custom CSS where possible
|
||||
3. **Keep shadcn/ui components** - Just reskin them with new colors
|
||||
4. **Test incrementally** - Update one component type at a time
|
||||
5. **Icon mapping is critical** - Different tree types get different icon colors
|
||||
|
||||
## Questions to Resolve
|
||||
|
||||
1. Should all buttons use the same white style, or do you want variations?
|
||||
2. Do you want the subtle radial gradient overlays, or pure black background?
|
||||
3. Should stat cards always show trend indicators (up/down arrows)?
|
||||
4. Any specific components that should NOT follow this design?
|
||||
|
||||
---
|
||||
|
||||
## Example Files to Update
|
||||
|
||||
**Priority 1 (Core UI):**
|
||||
- `AppLayout.tsx` - Main layout wrapper
|
||||
- `QuickStartPage.tsx` - Homepage/dashboard
|
||||
- `Header.tsx` / `Navbar.tsx` - Navigation
|
||||
|
||||
**Priority 2 (Features):**
|
||||
- Tree list components
|
||||
- Session components
|
||||
- Search components
|
||||
- Stats/metrics components
|
||||
|
||||
**Priority 3 (Supporting):**
|
||||
- Modals
|
||||
- Forms
|
||||
- Settings pages
|
||||
- Admin panels
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After implementation:
|
||||
- [ ] Dark mode looks correct
|
||||
- [ ] All text is readable (contrast check)
|
||||
- [ ] Icons have correct colors
|
||||
- [ ] Hover states work
|
||||
- [ ] Cards have proper depth/hierarchy
|
||||
- [ ] Buttons are prominent
|
||||
- [ ] Layout is consistent across pages
|
||||
@@ -2,11 +2,9 @@ import { useEffect } from 'react'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { router } from '@/router'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { useThemeStore } from '@/store/themeStore'
|
||||
|
||||
function App() {
|
||||
const { isAuthenticated, fetchUser, setLoading } = useAuthStore()
|
||||
const { theme, setTheme } = useThemeStore()
|
||||
|
||||
useEffect(() => {
|
||||
// On app load, check if we have a token and fetch user data
|
||||
@@ -20,19 +18,6 @@ function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleChange = () => {
|
||||
if (theme === 'system') {
|
||||
setTheme('system') // Re-apply to update resolvedTheme
|
||||
}
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [theme, setTheme])
|
||||
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ export function ActionMenu({ items }: ActionMenuProps) {
|
||||
ref={buttonRef}
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
'rounded-md p-1.5 text-muted-foreground transition-colors',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md p-1.5 text-white/50 transition-colors',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
@@ -63,8 +63,8 @@ export function ActionMenu({ items }: ActionMenuProps) {
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={cn(
|
||||
'fixed z-50 min-w-[160px] rounded-md border border-border',
|
||||
'bg-card py-1 shadow-lg animate-scale-in'
|
||||
'fixed z-50 min-w-[160px] rounded-md border border-white/10',
|
||||
'bg-black py-1 shadow-lg animate-scale-in'
|
||||
)}
|
||||
style={{
|
||||
top: `${menuPosition.top}px`,
|
||||
@@ -80,8 +80,8 @@ export function ActionMenu({ items }: ActionMenuProps) {
|
||||
'flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors',
|
||||
'disabled:opacity-50 disabled:pointer-events-none',
|
||||
item.destructive
|
||||
? 'text-destructive hover:bg-destructive/10'
|
||||
: 'text-foreground hover:bg-accent'
|
||||
? 'text-red-400 hover:bg-red-400/10'
|
||||
: 'text-white/70 hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function AdminLayout() {
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
{/* Desktop sidebar */}
|
||||
<div className="hidden w-60 flex-shrink-0 border-r border-border bg-card md:block">
|
||||
<div className="hidden w-60 flex-shrink-0 border-r border-white/[0.06] bg-black md:block">
|
||||
<AdminSidebar />
|
||||
</div>
|
||||
|
||||
@@ -44,14 +44,14 @@ export function AdminLayout() {
|
||||
{mobileOpen && (
|
||||
<div className="fixed inset-0 z-40 md:hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 w-60 border-r border-border bg-card shadow-xl">
|
||||
<div className="absolute inset-y-0 left-0 w-60 border-r border-white/[0.06] bg-black shadow-xl">
|
||||
<div className="flex h-12 items-center justify-end px-3">
|
||||
<button
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent"
|
||||
className="rounded-md p-1.5 text-white/50 hover:bg-white/[0.06]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -67,7 +67,7 @@ export function AdminLayout() {
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMobileOpen(true)}
|
||||
className="mb-4 rounded-md p-2 text-muted-foreground hover:bg-accent md:hidden"
|
||||
className="mb-4 rounded-md p-2 text-white/50 hover:bg-white/[0.06] md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
|
||||
return (
|
||||
<aside className={cn('flex h-full flex-col', className)}>
|
||||
<div className="p-4">
|
||||
<h2 className="font-heading text-lg font-bold text-foreground">Admin Panel</h2>
|
||||
<h2 className="text-lg font-bold text-white">Admin Panel</h2>
|
||||
</div>
|
||||
<nav className="flex-1 space-y-1 px-3">
|
||||
{navItems.map((item) => (
|
||||
@@ -50,8 +50,8 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
isActive(item.path, item.end)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
@@ -59,13 +59,13 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="border-t border-border p-3">
|
||||
<div className="border-t border-white/[0.06] p-3">
|
||||
<Link
|
||||
to="/trees"
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
|
||||
@@ -38,7 +38,7 @@ export function CategoryRow({
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg border border-border bg-card p-4',
|
||||
'flex items-center gap-3 glass-card rounded-2xl p-4',
|
||||
isDragging && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -47,7 +47,7 @@ export function CategoryRow({
|
||||
type="button"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
|
||||
className="cursor-grab touch-none text-white/50 hover:text-white active:cursor-grabbing"
|
||||
aria-label="Drag to reorder"
|
||||
>
|
||||
<GripVertical className="h-5 w-5" />
|
||||
@@ -56,17 +56,17 @@ export function CategoryRow({
|
||||
{/* Category Info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-foreground">{category.name}</h3>
|
||||
<h3 className="font-medium text-white">{category.name}</h3>
|
||||
{!category.is_active && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs font-medium text-white/70">
|
||||
Archived
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{category.description && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">{category.description}</p>
|
||||
<p className="mt-1 text-sm text-white/40">{category.description}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
{stepCount} step{stepCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
@@ -77,8 +77,8 @@ export function CategoryRow({
|
||||
type="button"
|
||||
onClick={() => onEdit(category)}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 bg-black/50 p-2 text-white/50',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Edit category"
|
||||
>
|
||||
|
||||
@@ -58,15 +58,15 @@ export function CreateCategoryModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md glass-card rounded-2xl p-6 shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-foreground">Create Category</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Create Category</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
disabled={isSaving}
|
||||
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||||
className="rounded-full p-1 text-white/50 hover:bg-white/10 hover:text-white disabled:opacity-50"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -76,15 +76,15 @@ export function CreateCategoryModal({
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Category Name <span className="text-destructive">*</span>
|
||||
<label htmlFor="name" className="mb-1 block text-sm font-medium text-white">
|
||||
Category Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
@@ -96,21 +96,21 @@ export function CreateCategoryModal({
|
||||
placeholder="e.g., Network Troubleshooting"
|
||||
required
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
{name.length}/100 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description Field */}
|
||||
<div>
|
||||
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Description <span className="text-muted-foreground">(optional)</span>
|
||||
<label htmlFor="description" className="mb-1 block text-sm font-medium text-white">
|
||||
Description <span className="text-white/40">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
@@ -120,9 +120,9 @@ export function CreateCategoryModal({
|
||||
rows={3}
|
||||
placeholder="Brief description of this category..."
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -135,8 +135,8 @@ export function CreateCategoryModal({
|
||||
onClick={handleClose}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -145,8 +145,8 @@ export function CreateCategoryModal({
|
||||
type="submit"
|
||||
disabled={isSaving || !name.trim()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{isSaving ? 'Creating...' : 'Create Category'}
|
||||
|
||||
@@ -50,16 +50,16 @@ export function DataTable<T>({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border border-border">
|
||||
<div className="overflow-x-auto rounded-lg border border-white/[0.06]">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/50">
|
||||
<tr className="border-b border-white/[0.06] bg-white/[0.02]">
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className={cn(
|
||||
'px-4 py-3 text-left font-medium text-muted-foreground',
|
||||
col.sortable && 'cursor-pointer select-none hover:text-foreground',
|
||||
'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/50',
|
||||
col.sortable && 'cursor-pointer select-none hover:text-white',
|
||||
col.className
|
||||
)}
|
||||
onClick={col.sortable ? () => handleSort(col.key) : undefined}
|
||||
@@ -87,10 +87,10 @@ export function DataTable<T>({
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
Array.from({ length: skeletonRows }).map((_, i) => (
|
||||
<tr key={i} className="border-b border-border last:border-0">
|
||||
<tr key={i} className="border-b border-white/[0.06] last:border-0">
|
||||
{columns.map((col) => (
|
||||
<td key={col.key} className="px-4 py-3">
|
||||
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-3/4 animate-pulse rounded bg-white/10" />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -99,7 +99,7 @@ export function DataTable<T>({
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-12 text-center">
|
||||
{emptyState || (
|
||||
<span className="text-muted-foreground">No data found</span>
|
||||
<span className="text-white/40">No data found</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -107,7 +107,7 @@ export function DataTable<T>({
|
||||
data.map((item) => (
|
||||
<tr
|
||||
key={keyExtractor(item)}
|
||||
className="border-b border-border last:border-0 hover:bg-muted/30 transition-colors"
|
||||
className="border-b border-white/[0.06] last:border-0 hover:bg-white/[0.04] transition-colors"
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<td key={col.key} className={cn('px-4 py-3', col.className)}>
|
||||
|
||||
@@ -67,15 +67,15 @@ export function EditCategoryModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md glass-card rounded-2xl p-6 shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-foreground">Edit Category</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Edit Category</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
disabled={isSaving}
|
||||
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||||
className="rounded-full p-1 text-white/50 hover:bg-white/10 hover:text-white disabled:opacity-50"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -85,15 +85,15 @@ export function EditCategoryModal({
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-md bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Category Name <span className="text-destructive">*</span>
|
||||
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-white">
|
||||
Category Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="edit-name"
|
||||
@@ -105,21 +105,21 @@ export function EditCategoryModal({
|
||||
placeholder="e.g., Network Troubleshooting"
|
||||
required
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
{name.length}/100 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description Field */}
|
||||
<div>
|
||||
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Description <span className="text-muted-foreground">(optional)</span>
|
||||
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-white">
|
||||
Description <span className="text-white/40">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="edit-description"
|
||||
@@ -129,9 +129,9 @@ export function EditCategoryModal({
|
||||
rows={3}
|
||||
placeholder="Brief description of this category..."
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -144,8 +144,8 @@ export function EditCategoryModal({
|
||||
onClick={handleClose}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -154,8 +154,8 @@ export function EditCategoryModal({
|
||||
type="submit"
|
||||
disabled={isSaving || !name.trim()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
|
||||
@@ -12,10 +12,10 @@ interface EmptyStateProps {
|
||||
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
|
||||
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
|
||||
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
||||
{icon && <div className="mb-4 text-white/50">{icon}</div>}
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
{description && (
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
|
||||
<p className="mt-1 max-w-sm text-sm text-white/40">{description}</p>
|
||||
)}
|
||||
{action && <div className="mt-4">{action}</div>}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ export function PageHeader({ title, description, action, className }: PageHeader
|
||||
return (
|
||||
<div className={cn('flex items-start justify-between gap-4', className)}>
|
||||
<div>
|
||||
<h1 className="font-heading text-2xl font-bold text-foreground">{title}</h1>
|
||||
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
||||
{description && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
|
||||
@@ -36,20 +36,20 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 pt-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
Showing {start}-{end} of {total}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page <= 1}
|
||||
className={cn(btnBase, 'px-2 hover:bg-accent')}
|
||||
className={cn(btnBase, 'px-2 text-white/50 hover:bg-white/[0.06] hover:text-white')}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
{getPageNumbers().map((p, i) =>
|
||||
p === 'ellipsis' ? (
|
||||
<span key={`e${i}`} className="px-1 text-muted-foreground">...</span>
|
||||
<span key={`e${i}`} className="px-1 text-white/40">...</span>
|
||||
) : (
|
||||
<button
|
||||
key={p}
|
||||
@@ -58,8 +58,8 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
|
||||
btnBase,
|
||||
'px-2',
|
||||
p === page
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-accent text-muted-foreground'
|
||||
? 'bg-white text-black'
|
||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
>
|
||||
{p}
|
||||
@@ -69,7 +69,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
|
||||
<button
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
className={cn(btnBase, 'px-2 hover:bg-accent')}
|
||||
className={cn(btnBase, 'px-2 text-white/50 hover:bg-white/[0.06] hover:text-white')}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -40,21 +40,21 @@ export function SearchInput({ value = '', onSearch, placeholder = 'Search...', c
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/50" />
|
||||
<input
|
||||
type="text"
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'h-9 w-full rounded-md border border-border bg-background pl-9 pr-8 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'h-9 w-full rounded-md border border-white/10 bg-black/50 pl-9 pr-8 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
{localValue && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-muted-foreground hover:text-foreground"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-white/50 hover:text-white"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -9,10 +9,10 @@ interface StatusBadgeProps {
|
||||
}
|
||||
|
||||
const variantClasses: Record<BadgeVariant, string> = {
|
||||
success: 'bg-green-500/10 text-green-600 dark:text-green-400',
|
||||
destructive: 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
warning: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400',
|
||||
default: 'bg-muted text-muted-foreground',
|
||||
success: 'bg-emerald-400/10 text-emerald-400',
|
||||
destructive: 'bg-red-400/10 text-red-400',
|
||||
warning: 'bg-yellow-400/10 text-yellow-400',
|
||||
default: 'bg-white/10 text-white/70',
|
||||
}
|
||||
|
||||
export function StatusBadge({ variant = 'default', children, className }: StatusBadgeProps) {
|
||||
|
||||
@@ -6,51 +6,41 @@ interface BrandLogoProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolutionFlow brand logo icon.
|
||||
* ResolutionFlow brand logo icon — white monochrome.
|
||||
* sm (32x32) for header/navbar, lg (80x80) for login/register pages.
|
||||
*/
|
||||
export function BrandLogo({ size = 'sm', className }: BrandLogoProps) {
|
||||
const sizeClasses = size === 'sm' ? 'h-8 w-8' : 'h-20 w-20'
|
||||
|
||||
// The SVG scales via viewBox - same paths work at any size.
|
||||
// Stroke widths are tuned per size for visual clarity.
|
||||
const strokeBase = size === 'sm' ? 1 : 2
|
||||
const strokeThick = size === 'sm' ? 1.25 : 2.5
|
||||
const dashArray = size === 'sm' ? '1 1.5' : '2 3'
|
||||
const nodeR = size === 'sm' ? { outer: 2.5, inner: 2.75 } : { outer: 5, inner: 5.5 }
|
||||
const hubR = size === 'sm' ? { glow: 5, solid: 3.5 } : { glow: 10, solid: 7 }
|
||||
|
||||
// Positions scale with viewBox
|
||||
const vb = size === 'sm' ? '0 0 40 40' : '0 0 80 80'
|
||||
const s = size === 'sm' ? 1 : 2 // scale factor
|
||||
const s = size === 'sm' ? 1 : 2
|
||||
|
||||
return (
|
||||
<svg viewBox={vb} fill="none" className={cn(sizeClasses, className)}>
|
||||
<defs>
|
||||
<linearGradient id="brand-logo-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#818cf8" />
|
||||
<stop offset="100%" stopColor="#a78bfa" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Input nodes */}
|
||||
<circle cx={5 * s} cy={7 * s} r={nodeR.outer} fill="url(#brand-logo-grad)" opacity="0.35" />
|
||||
<circle cx={5 * s} cy={15 * s} r={nodeR.inner} fill="url(#brand-logo-grad)" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={25 * s} r={nodeR.inner} fill="url(#brand-logo-grad)" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={33 * s} r={nodeR.outer} fill="url(#brand-logo-grad)" opacity="0.35" />
|
||||
<circle cx={5 * s} cy={7 * s} r={nodeR.outer} fill="white" opacity="0.35" />
|
||||
<circle cx={5 * s} cy={15 * s} r={nodeR.inner} fill="white" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={25 * s} r={nodeR.inner} fill="white" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={33 * s} r={nodeR.outer} fill="white" opacity="0.35" />
|
||||
|
||||
{/* Connecting lines */}
|
||||
<path d={`M${7.5 * s} ${7 * s}L${14 * s} ${17 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
<path d={`M${7.75 * s} ${15 * s}L${14 * s} ${19 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.75 * s} ${25 * s}L${14 * s} ${21 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.5 * s} ${33 * s}L${14 * s} ${23 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
<path d={`M${7.5 * s} ${7 * s}L${14 * s} ${17 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
<path d={`M${7.75 * s} ${15 * s}L${14 * s} ${19 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.75 * s} ${25 * s}L${14 * s} ${21 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.5 * s} ${33 * s}L${14 * s} ${23 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
|
||||
{/* Central hub */}
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.glow} fill="url(#brand-logo-grad)" opacity="0.15" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.solid} fill="url(#brand-logo-grad)" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.glow} fill="white" opacity="0.15" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.solid} fill="white" />
|
||||
|
||||
{/* Output arrow */}
|
||||
<path d={`M${21.5 * s} ${20 * s}H${35 * s}M${35 * s} ${20 * s}L${30 * s} ${15 * s}M${35 * s} ${20 * s}L${30 * s} ${25 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeThick} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d={`M${21.5 * s} ${20 * s}H${35 * s}M${35 * s} ${20 * s}L${30 * s} ${15 * s}M${35 * s} ${20 * s}L${30 * s} ${25 * s}`} stroke="white" strokeWidth={strokeThick} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,20 +6,19 @@ interface BrandWordmarkProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolutionFlow wordmark with gradient "Flow" text.
|
||||
* ResolutionFlow wordmark — clean white text.
|
||||
* sm for header/navbar, lg for login/register pages.
|
||||
*/
|
||||
export function BrandWordmark({ size = 'sm', className }: BrandWordmarkProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-heading font-bold',
|
||||
size === 'sm' ? 'text-xl' : 'text-3xl tracking-tight',
|
||||
'font-semibold tracking-tight text-white',
|
||||
size === 'sm' ? 'text-xl' : 'text-3xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground">Resolution</span>
|
||||
<span className="text-gradient-brand">Flow</span>
|
||||
ResolutionFlow
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ export function ConfirmDialog({
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'rounded-md border border-border px-4 py-2 text-sm font-medium',
|
||||
'text-card-foreground hover:bg-accent',
|
||||
'rounded-xl border border-white/10 px-4 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
@@ -45,11 +45,11 @@ export function ConfirmDialog({
|
||||
onClick={onConfirm}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-2 text-sm font-medium text-white',
|
||||
'rounded-xl px-4 py-2 text-sm font-medium',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
confirmVariant === 'destructive'
|
||||
? 'bg-destructive hover:bg-destructive/90'
|
||||
: 'bg-primary hover:bg-primary/90'
|
||||
? 'bg-red-400/10 text-red-400 hover:bg-red-400/20 border border-red-400/20'
|
||||
: 'bg-white text-black hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Processing...' : confirmLabel}
|
||||
@@ -57,7 +57,7 @@ export function ConfirmDialog({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
<p className="text-sm text-white/70">{message}</p>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,22 +34,22 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
return (
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center p-8">
|
||||
<div className="max-w-md text-center">
|
||||
<h2 className="mb-2 text-xl font-semibold text-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-red-400">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
<p className="mb-4 text-white/70">
|
||||
An unexpected error occurred. Please try refreshing the page.
|
||||
</p>
|
||||
{this.state.error && (
|
||||
<pre className="mb-4 overflow-auto rounded bg-muted p-3 text-left text-xs text-muted-foreground">
|
||||
<pre className="mb-4 overflow-auto rounded-xl bg-white/5 border border-white/[0.06] p-3 text-left text-xs text-red-400">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
)}
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Refresh Page
|
||||
|
||||
@@ -52,7 +52,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -60,23 +60,23 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
||||
{/* Modal Content */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex w-full flex-col border border-border bg-card shadow-lg',
|
||||
'max-h-[100vh] rounded-t-lg sm:max-h-[85vh] sm:rounded-lg',
|
||||
'relative flex w-full flex-col border border-white/[0.06] bg-[#0a0a0a] shadow-lg',
|
||||
'max-h-[100vh] rounded-t-2xl sm:max-h-[85vh] sm:rounded-2xl',
|
||||
'animate-scale-in',
|
||||
sizeClasses[size]
|
||||
)}
|
||||
>
|
||||
{/* Header - Fixed at top */}
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<h2 id="modal-title" className="text-lg font-semibold text-card-foreground">
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-white/[0.06] px-4 py-3 sm:px-6 sm:py-4">
|
||||
<h2 id="modal-title" className="text-lg font-semibold text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-md p-1.5 text-muted-foreground transition-colors sm:p-1',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'rounded-md p-1.5 text-white/40 transition-colors sm:p-1',
|
||||
'hover:bg-white/10 hover:text-white',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
@@ -91,7 +91,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
||||
|
||||
{/* Footer - Fixed at bottom */}
|
||||
{footer && (
|
||||
<div className="flex-shrink-0 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<div className="flex-shrink-0 border-t border-white/[0.06] px-4 py-3 sm:px-6 sm:py-4">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function PageLoader() {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-background">
|
||||
<div className="flex h-screen items-center justify-center bg-black">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
<p className="text-sm text-white/40">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -17,19 +17,19 @@ export function RouteError() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-8">
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-black p-8">
|
||||
<div className="max-w-md text-center">
|
||||
<h1 className="mb-2 text-4xl font-bold text-foreground">Oops!</h1>
|
||||
<h2 className="mb-2 text-xl font-semibold text-foreground">{errorMessage}</h2>
|
||||
<h1 className="mb-2 text-4xl font-bold text-white">Oops!</h1>
|
||||
<h2 className="mb-2 text-xl font-semibold text-red-400">{errorMessage}</h2>
|
||||
{errorDetails && (
|
||||
<p className="mb-4 text-muted-foreground">{errorDetails}</p>
|
||||
<p className="mb-4 text-white/70">{errorDetails}</p>
|
||||
)}
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className={cn(
|
||||
'rounded-md border border-input px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-xl border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Go Back
|
||||
@@ -37,8 +37,8 @@ export function RouteError() {
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Go Home
|
||||
|
||||
@@ -48,14 +48,14 @@ export function StarRating({
|
||||
sizeClasses[size],
|
||||
star <= value
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'fill-none text-muted-foreground',
|
||||
: 'fill-none text-white/30',
|
||||
!readonly && 'hover:text-yellow-300'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{showCount && (
|
||||
<span className="ml-1 text-sm text-muted-foreground">
|
||||
<span className="ml-1 text-sm text-white/40">
|
||||
({value}/5)
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -37,8 +37,8 @@ export function TagBadges({
|
||||
'rounded-full transition-colors',
|
||||
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||
variant === 'default'
|
||||
? 'bg-primary/10 text-primary hover:bg-primary/20'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80',
|
||||
? 'bg-white/10 text-white/70 hover:bg-white/15'
|
||||
: 'bg-white/5 text-white/40 hover:bg-white/10',
|
||||
!onTagClick && 'cursor-default'
|
||||
)}
|
||||
>
|
||||
@@ -50,7 +50,7 @@ export function TagBadges({
|
||||
className={cn(
|
||||
'rounded-full',
|
||||
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||
'bg-muted text-muted-foreground'
|
||||
'bg-white/5 text-white/40'
|
||||
)}
|
||||
title={tags.slice(maxVisible).join(', ')}
|
||||
>
|
||||
|
||||
@@ -118,11 +118,11 @@ export function TagInput({
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap gap-1.5 rounded-md border px-2 py-1.5',
|
||||
'bg-background text-foreground',
|
||||
'focus-within:border-primary focus-within:ring-1 focus-within:ring-primary',
|
||||
'flex flex-wrap gap-1.5 rounded-xl border px-2 py-1.5',
|
||||
'bg-black/50 text-white',
|
||||
'focus-within:border-white/30 focus-within:ring-1 focus-within:ring-white/20',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||
'border-input'
|
||||
'border-white/10'
|
||||
)}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
@@ -132,7 +132,7 @@ export function TagInput({
|
||||
key={tag}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs',
|
||||
'bg-primary/10 text-primary'
|
||||
'bg-white/10 text-white/70'
|
||||
)}
|
||||
>
|
||||
{tag}
|
||||
@@ -143,7 +143,7 @@ export function TagInput({
|
||||
e.stopPropagation()
|
||||
removeTag(tag)
|
||||
}}
|
||||
className="rounded-full p-0.5 hover:bg-primary/20"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -167,8 +167,8 @@ export function TagInput({
|
||||
placeholder={tags.length === 0 ? placeholder : ''}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex-1 min-w-[80px] border-0 bg-transparent px-1 py-0.5 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'flex-1 min-w-[80px] border-0 bg-transparent px-1 py-0.5 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:outline-none focus:ring-0'
|
||||
)}
|
||||
/>
|
||||
@@ -179,8 +179,8 @@ export function TagInput({
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute z-10 mt-1 w-full rounded-md border border-input',
|
||||
'bg-popover shadow-lg'
|
||||
'absolute z-10 mt-1 w-full rounded-xl border border-white/[0.06]',
|
||||
'bg-[#0a0a0a] shadow-lg'
|
||||
)}
|
||||
>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
@@ -189,13 +189,13 @@ export function TagInput({
|
||||
type="button"
|
||||
onClick={() => addTag(suggestion.name)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between px-3 py-2 text-sm',
|
||||
'hover:bg-accent',
|
||||
index === selectedIndex && 'bg-accent'
|
||||
'flex w-full items-center justify-between px-3 py-2 text-sm text-white/70',
|
||||
'hover:bg-white/10',
|
||||
index === selectedIndex && 'bg-white/10'
|
||||
)}
|
||||
>
|
||||
<span>{suggestion.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-white/40">
|
||||
{suggestion.usage_count} trees
|
||||
</span>
|
||||
</button>
|
||||
@@ -208,8 +208,8 @@ export function TagInput({
|
||||
type="button"
|
||||
onClick={() => addTag(inputValue)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 border-t border-input px-3 py-2 text-sm',
|
||||
'hover:bg-accent text-primary'
|
||||
'flex w-full items-center gap-2 border-t border-white/[0.06] px-3 py-2 text-sm',
|
||||
'hover:bg-white/10 text-white'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -220,7 +220,7 @@ export function TagInput({
|
||||
)}
|
||||
|
||||
{/* Helper text */}
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
{tags.length}/{maxTags} tags. Press Enter or comma to add.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -11,17 +11,17 @@ export function UpgradePrompt({ feature, className }: UpgradePromptProps) {
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-4',
|
||||
'glass-card rounded-2xl border border-white/[0.06] p-4',
|
||||
className
|
||||
)}>
|
||||
<h3 className="font-semibold text-foreground">Plan Limit Reached</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<h3 className="font-semibold text-white">Plan Limit Reached</h3>
|
||||
<p className="mt-1 text-sm text-white/70">
|
||||
Your {plan} plan doesn't allow you to {feature}. Upgrade your plan to continue.
|
||||
</p>
|
||||
<button
|
||||
className={cn(
|
||||
'mt-3 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'mt-3 rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
onClick={() => window.location.href = '/account'}
|
||||
>
|
||||
|
||||
227
frontend/src/components/layout/AppLayout-original.tsx
Normal file
227
frontend/src/components/layout/AppLayout-original.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function AppLayout() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuthStore()
|
||||
const { effectiveRole, isSuperAdmin } = usePermissions()
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
setMobileMenuOpen(false)
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
// Close mobile menu on route change - key-based reset
|
||||
const [prevPath, setPrevPath] = useState(location.pathname)
|
||||
if (prevPath !== location.pathname) {
|
||||
setPrevPath(location.pathname)
|
||||
if (mobileMenuOpen) setMobileMenuOpen(false)
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setMobileMenuOpen(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [mobileMenuOpen, handleKeyDown])
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Home' },
|
||||
{ path: '/trees', label: 'Trees' },
|
||||
{ path: '/my-trees', label: 'My Trees' },
|
||||
{ path: '/sessions', label: 'Sessions' },
|
||||
{ path: '/account', label: 'Account' },
|
||||
{ path: '/settings', label: 'Settings' },
|
||||
...(isSuperAdmin ? [{ path: '/admin', label: 'Admin Panel' }] : []),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-8">
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-1 sm:flex">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'relative rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="hidden text-sm text-muted-foreground sm:block">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-block',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'hidden rounded-md px-3 py-1.5 text-sm font-medium sm:block',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Nav Drawer */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="fixed inset-0 z-50 sm:hidden">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-card shadow-xl animate-slide-in-left">
|
||||
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col p-4">
|
||||
{/* User info */}
|
||||
<div className="mb-4 border-b border-border pb-4">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
<div className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-2">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'w-full rounded-md px-3 py-2.5 text-left text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="animate-fade-in">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
@@ -2,10 +2,8 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { Menu, X, LogOut, User, Shield } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function AppLayout() {
|
||||
@@ -21,7 +19,7 @@ export function AppLayout() {
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
// Close mobile menu on route change - key-based reset
|
||||
// Close mobile menu on route change
|
||||
const [prevPath, setPrevPath] = useState(location.pathname)
|
||||
if (prevPath !== location.pathname) {
|
||||
setPrevPath(location.pathname)
|
||||
@@ -57,68 +55,92 @@ export function AppLayout() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Subtle radial overlay for depth */}
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%),radial-gradient(circle_at_80%_80%,rgba(80,80,100,0.02),transparent_50%)]" />
|
||||
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
||||
<header className="sticky top-0 z-50 border-b border-white/[0.06] bg-black/80 backdrop-blur-xl">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-8">
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
|
||||
className="rounded-xl p-2 text-white/50 hover:bg-white/10 hover:text-white transition-all sm:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
<div className="w-9 h-9 rounded-xl bg-white flex items-center justify-center transition-transform group-hover:scale-105">
|
||||
<BrandLogo size="sm" className="h-5 w-5 invert" />
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden items-center gap-1 sm:flex">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'relative rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
{navItems.map((item) => {
|
||||
const isActive = item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'rounded-xl px-4 py-2 text-sm font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-white/10 text-white border border-white/20'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="hidden text-sm text-muted-foreground sm:block">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-block',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
{/* Right side controls */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* User info */}
|
||||
<div className="hidden items-center gap-3 sm:flex">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-white/[0.06] px-3 py-1.5 border border-white/10">
|
||||
<User className="h-4 w-4 text-white/40" />
|
||||
<span className="text-sm text-white/70">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Role badge */}
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<div className="px-3 py-1.5 rounded-xl bg-white/10 border border-white/20">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white font-semibold">
|
||||
<Shield className="h-3 w-3" />
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Logout button */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'hidden rounded-md px-3 py-1.5 text-sm font-medium sm:block',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
'hidden items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium sm:flex',
|
||||
'text-white/50 hover:text-white hover:bg-white/10 transition-all',
|
||||
'border border-white/10 hover:border-white/20'
|
||||
)}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -130,21 +152,25 @@ export function AppLayout() {
|
||||
<div className="fixed inset-0 z-50 sm:hidden">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-card shadow-xl animate-slide-in-left">
|
||||
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-white/[0.06] bg-black shadow-2xl animate-in slide-in-from-left duration-300">
|
||||
<div className="flex h-16 items-center justify-between border-b border-white/[0.06] px-4">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-white flex items-center justify-center">
|
||||
<BrandLogo size="sm" className="h-5 w-5 invert" />
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
className="rounded-xl p-2 text-white/50 hover:bg-white/10 hover:text-white transition-all"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
@@ -153,61 +179,60 @@ export function AppLayout() {
|
||||
|
||||
<div className="flex flex-col p-4">
|
||||
{/* User info */}
|
||||
<div className="mb-4 border-b border-border pb-4">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
<div className="mb-4 border-b border-white/[0.06] pb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<User className="h-4 w-4 text-white/40" />
|
||||
<p className="text-sm font-medium text-white">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
</div>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
<div className="inline-flex px-3 py-1.5 rounded-xl bg-white/10 border border-white/20">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white font-semibold">
|
||||
<Shield className="h-3 w-3" />
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
<div className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{navItems.map((item) => {
|
||||
const isActive = item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-xl px-4 py-3 text-sm font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-white/10 text-white border border-white/20'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-2">
|
||||
<div className="mt-4 border-t border-white/[0.06] pt-4">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'w-full rounded-md px-3 py-2.5 text-left text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
'w-full flex items-center gap-2 rounded-xl px-4 py-3 text-sm font-medium',
|
||||
'text-white/50 hover:text-white hover:bg-white/10 transition-all',
|
||||
'border border-white/10 hover:border-white/20'
|
||||
)}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -217,7 +242,7 @@ export function AppLayout() {
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="animate-fade-in">
|
||||
<main className="relative animate-in fade-in duration-500">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-1.5 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Add to folder"
|
||||
>
|
||||
@@ -100,14 +100,14 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
||||
{isOpen && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-full z-20 mt-1 w-48 rounded-md border border-input',
|
||||
'bg-popover py-1 shadow-lg'
|
||||
'absolute right-0 top-full z-20 mt-1 w-48 rounded-md border border-white/10',
|
||||
'bg-black/90 backdrop-blur-sm py-1 shadow-lg'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">Loading...</div>
|
||||
<div className="px-3 py-2 text-sm text-white/40">Loading...</div>
|
||||
) : folders.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">No folders yet</div>
|
||||
<div className="px-3 py-2 text-sm text-white/40">No folders yet</div>
|
||||
) : (
|
||||
folders.map((folder) => (
|
||||
<button
|
||||
@@ -116,7 +116,7 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
||||
e.stopPropagation()
|
||||
toggleFolder(folder.id)
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-white/70 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-sm"
|
||||
@@ -124,13 +124,13 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
||||
/>
|
||||
<span className="flex-1 truncate text-left">{folder.name}</span>
|
||||
{treeFolderIds.has(folder.id) && (
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
<Check className="h-4 w-4 text-white" />
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
|
||||
<div className="border-t border-input my-1" />
|
||||
<div className="border-t border-white/10 my-1" />
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -138,7 +138,7 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
||||
setIsOpen(false)
|
||||
onFolderCreated?.()
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-primary hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-white/70 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create new folder
|
||||
|
||||
@@ -174,15 +174,15 @@ export function FolderEditModal({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative z-10 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="relative z-10 w-full max-w-md glass-card rounded-2xl p-6 shadow-lg">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
{isEditMode ? 'Edit Folder' : initialParentId ? 'Create Subfolder' : 'Create Folder'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="rounded-md p-1 hover:bg-accent">
|
||||
<button onClick={onClose} className="rounded-md p-1 text-white/40 hover:bg-white/[0.06] hover:text-white">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -190,7 +190,7 @@ export function FolderEditModal({
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Name input */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="folder-name" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="folder-name" className="block text-sm font-medium text-white">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
@@ -201,9 +201,9 @@ export function FolderEditModal({
|
||||
placeholder="e.g., Citrix Issues"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'border-input'
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'border-white/10'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
@@ -211,7 +211,7 @@ export function FolderEditModal({
|
||||
|
||||
{/* Parent folder dropdown */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="folder-parent" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="folder-parent" className="block text-sm font-medium text-white">
|
||||
Parent Folder
|
||||
</label>
|
||||
<select
|
||||
@@ -220,9 +220,9 @@ export function FolderEditModal({
|
||||
onChange={(e) => setParentId(e.target.value || null)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'border-input'
|
||||
'bg-black/50 text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'border-white/10'
|
||||
)}
|
||||
>
|
||||
<option value="">None (root level)</option>
|
||||
@@ -232,14 +232,14 @@ export function FolderEditModal({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
Folders can be nested up to 3 levels deep.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Color picker */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-foreground">Color</label>
|
||||
<label className="block text-sm font-medium text-white">Color</label>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{FOLDER_COLORS.map((c) => (
|
||||
<button
|
||||
@@ -248,7 +248,7 @@ export function FolderEditModal({
|
||||
onClick={() => setColor(c)}
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-full transition-transform',
|
||||
color === c && 'ring-2 ring-offset-2 ring-offset-background ring-primary scale-110'
|
||||
color === c && 'ring-2 ring-offset-2 ring-offset-black ring-white/50 scale-110'
|
||||
)}
|
||||
style={{ backgroundColor: c }}
|
||||
title={c}
|
||||
@@ -262,7 +262,7 @@ export function FolderEditModal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn('rounded-md border border-input px-4 py-2 text-sm', 'hover:bg-accent')}
|
||||
className={cn('rounded-md border border-white/10 px-4 py-2 text-sm text-white/60', 'hover:bg-white/10 hover:text-white')}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -270,8 +270,8 @@ export function FolderEditModal({
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90',
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -113,8 +113,8 @@ function FolderItem({
|
||||
onClick={() => onFolderSelect(folder.id)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1 rounded-md py-1.5 text-sm',
|
||||
'transition-colors hover:bg-accent',
|
||||
selectedFolderId === folder.id && 'bg-accent font-medium'
|
||||
'transition-colors hover:bg-white/[0.06]',
|
||||
selectedFolderId === folder.id && 'bg-white/10 text-white font-medium'
|
||||
)}
|
||||
style={{ paddingLeft: `${8 + depth * 16}px`, paddingRight: '8px' }}
|
||||
>
|
||||
@@ -125,7 +125,7 @@ function FolderItem({
|
||||
e.stopPropagation()
|
||||
onToggleExpand(folder.id)
|
||||
}}
|
||||
className="shrink-0 p-0.5 hover:bg-accent rounded"
|
||||
className="shrink-0 p-0.5 hover:bg-white/[0.06] rounded"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
@@ -138,7 +138,7 @@ function FolderItem({
|
||||
)}
|
||||
<Folder className="h-4 w-4 shrink-0" style={{ color: folder.color }} />
|
||||
<span className="flex-1 truncate text-left">{folder.name}</span>
|
||||
<span className="text-xs text-muted-foreground group-hover:hidden">{folder.tree_count}</span>
|
||||
<span className="text-xs text-white/40 group-hover:hidden">{folder.tree_count}</span>
|
||||
</button>
|
||||
|
||||
{/* Folder menu button - replaces tree count on hover */}
|
||||
@@ -150,7 +150,7 @@ function FolderItem({
|
||||
className={cn(
|
||||
'absolute right-1 top-1/2 -translate-y-1/2 rounded p-1',
|
||||
'hidden group-hover:block',
|
||||
'hover:bg-accent'
|
||||
'hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
<MoreVertical className="h-3 w-3" />
|
||||
@@ -160,8 +160,8 @@ function FolderItem({
|
||||
{menuOpenId === folder.id && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-full z-10 mt-1 w-40 rounded-md border border-input',
|
||||
'bg-popover py-1 shadow-lg'
|
||||
'absolute right-0 top-full z-10 mt-1 w-40 rounded-md border border-white/10',
|
||||
'bg-black/90 backdrop-blur-sm py-1 shadow-lg'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -170,7 +170,7 @@ function FolderItem({
|
||||
onEditFolder(folder)
|
||||
onMenuToggle(null)
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-white/70 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit
|
||||
@@ -182,7 +182,7 @@ function FolderItem({
|
||||
onAddSubfolder(folder.id)
|
||||
onMenuToggle(null)
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-white/70 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<FolderPlus className="h-3 w-3" />
|
||||
Add Subfolder
|
||||
@@ -194,7 +194,7 @@ function FolderItem({
|
||||
onDeleteFolder(folder.id, hasSubfolders)
|
||||
onMenuToggle(null)
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-destructive hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-red-400 hover:bg-red-400/10"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete
|
||||
@@ -356,13 +356,13 @@ export function FolderSidebar({
|
||||
{/* Mobile backdrop */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm md:hidden"
|
||||
className="fixed inset-0 z-40 bg-black/80 backdrop-blur-sm md:hidden"
|
||||
onClick={onMobileClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<div className={cn(
|
||||
'w-56 shrink-0 border-r border-border bg-card',
|
||||
'w-56 shrink-0 border-r border-white/[0.06] bg-transparent',
|
||||
'hidden md:block',
|
||||
mobileOpen && 'fixed inset-y-0 left-0 z-50 block animate-slide-in-left md:relative md:animate-none'
|
||||
)}>
|
||||
@@ -370,10 +370,10 @@ export function FolderSidebar({
|
||||
{/* Mobile close button */}
|
||||
{mobileOpen && (
|
||||
<div className="mb-3 flex items-center justify-between md:hidden">
|
||||
<span className="text-sm font-medium text-card-foreground">Folders</span>
|
||||
<span className="text-sm font-medium text-white">Folders</span>
|
||||
<button
|
||||
onClick={onMobileClose}
|
||||
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent"
|
||||
className="rounded-md p-1.5 text-white/40 hover:bg-white/[0.06]"
|
||||
aria-label="Close folders"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -382,7 +382,7 @@ export function FolderSidebar({
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex w-full items-center gap-2 text-sm font-medium text-card-foreground"
|
||||
className="flex w-full items-center gap-2 text-sm font-medium text-white"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -399,8 +399,8 @@ export function FolderSidebar({
|
||||
onClick={() => onFolderSelect(null)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm',
|
||||
'transition-colors hover:bg-accent',
|
||||
selectedFolderId === null && 'bg-accent font-medium'
|
||||
'transition-colors hover:bg-white/[0.06]',
|
||||
selectedFolderId === null && 'bg-white/10 text-white font-medium'
|
||||
)}
|
||||
>
|
||||
<Folder className="h-4 w-4" />
|
||||
@@ -409,7 +409,7 @@ export function FolderSidebar({
|
||||
|
||||
{/* Loading state */}
|
||||
{isLoading ? (
|
||||
<div className="px-2 py-1.5 text-sm text-muted-foreground">Loading...</div>
|
||||
<div className="px-2 py-1.5 text-sm text-white/40">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{/* User folders (hierarchical) */}
|
||||
@@ -439,7 +439,7 @@ export function FolderSidebar({
|
||||
onClick={() => onCreateFolder(null)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm',
|
||||
'text-muted-foreground transition-colors hover:bg-accent hover:text-foreground'
|
||||
'text-white/50 transition-colors hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -454,8 +454,8 @@ export function FolderSidebar({
|
||||
{contextMenu && (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed z-50 w-44 rounded-md border border-input',
|
||||
'bg-popover py-1 shadow-lg'
|
||||
'fixed z-50 w-44 rounded-md border border-white/10',
|
||||
'bg-black/90 backdrop-blur-sm py-1 shadow-lg'
|
||||
)}
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -465,7 +465,7 @@ export function FolderSidebar({
|
||||
onEditFolder(contextMenu.folder)
|
||||
closeContextMenu()
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-white/70 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit
|
||||
@@ -476,7 +476,7 @@ export function FolderSidebar({
|
||||
handleAddSubfolder(contextMenu.folder.id)
|
||||
closeContextMenu()
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-white/70 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<FolderPlus className="h-3 w-3" />
|
||||
Add Subfolder
|
||||
@@ -487,7 +487,7 @@ export function FolderSidebar({
|
||||
handleDeleteFolder(contextMenu.folder.id, contextMenu.folder.children.length > 0)
|
||||
closeContextMenu()
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-destructive hover:bg-accent"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-red-400 hover:bg-red-400/10"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete
|
||||
|
||||
@@ -114,18 +114,18 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative w-full max-w-lg rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="relative w-full max-w-lg glass-card rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Share Tree</h2>
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-6 py-4">
|
||||
<h2 className="text-lg font-semibold text-white">Share Tree</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
className="rounded-md p-1 text-white/40 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -135,9 +135,9 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
<div className="px-6 py-4 space-y-6">
|
||||
{/* Tree Info */}
|
||||
<div>
|
||||
<h3 className="font-medium text-card-foreground">{tree.name}</h3>
|
||||
<h3 className="font-medium text-white">{tree.name}</h3>
|
||||
{tree.description && (
|
||||
<p className="mt-1 text-sm text-muted-foreground line-clamp-2">
|
||||
<p className="mt-1 text-sm text-white/70 line-clamp-2">
|
||||
{tree.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -145,7 +145,7 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
|
||||
{/* Visibility Settings */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-card-foreground">
|
||||
<label className="mb-2 block text-sm font-medium text-white">
|
||||
Visibility
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
@@ -156,19 +156,19 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-md border px-4 py-3 text-left transition-colors',
|
||||
visibility === level
|
||||
? 'border-primary bg-primary/5 text-card-foreground'
|
||||
: 'border-border bg-background text-muted-foreground hover:border-primary/50 hover:bg-accent'
|
||||
? 'border-white/20 bg-white/10 text-white'
|
||||
: 'border-white/[0.06] bg-transparent text-white/50 hover:border-white/20 hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
{getVisibilityIcon(level)}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium capitalize">{level}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="text-xs text-white/40">
|
||||
{getVisibilityDescription(level)}
|
||||
</div>
|
||||
</div>
|
||||
{visibility === level && (
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
<div className="h-2 w-2 rounded-full bg-white" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -178,7 +178,7 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
{/* Share Link Generation */}
|
||||
{visibility !== 'private' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-card-foreground">
|
||||
<label className="mb-2 block text-sm font-medium text-white">
|
||||
Share Link
|
||||
</label>
|
||||
|
||||
@@ -189,11 +189,11 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
id="allow-forking"
|
||||
checked={allowForking}
|
||||
onChange={(e) => setAllowForking(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 rounded border-white/10 bg-black/50 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-2 focus:ring-offset-black"
|
||||
/>
|
||||
<label
|
||||
htmlFor="allow-forking"
|
||||
className="text-sm text-muted-foreground cursor-pointer"
|
||||
className="text-sm text-white/70 cursor-pointer"
|
||||
>
|
||||
Allow recipients to fork this tree
|
||||
</label>
|
||||
@@ -205,8 +205,8 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
onClick={handleGenerateLink}
|
||||
disabled={isGenerating}
|
||||
className={cn(
|
||||
'w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'w-full rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isGenerating ? 'Generating...' : 'Generate Share Link'}
|
||||
@@ -216,20 +216,20 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
{/* Active Share Link */}
|
||||
{activeShare && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 rounded-md border border-border bg-background p-3">
|
||||
<div className="flex items-center gap-2 rounded-md border border-white/10 bg-black/50 p-3">
|
||||
<input
|
||||
type="text"
|
||||
value={activeShare.share_url}
|
||||
readOnly
|
||||
className="flex-1 bg-transparent text-sm text-foreground outline-none"
|
||||
className="flex-1 bg-transparent text-sm text-white outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
copied
|
||||
? 'border-green-500 bg-green-500/10 text-green-600'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
? 'border-green-500 bg-green-500/10 text-green-400'
|
||||
: 'text-white/60 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
@@ -245,13 +245,13 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-white/40">
|
||||
{activeShare.allow_forking
|
||||
? 'Recipients can fork this tree'
|
||||
: 'Forking disabled for this share'}
|
||||
</p>
|
||||
{shares.length > 1 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-white/40">
|
||||
{shares.length} active share links
|
||||
</p>
|
||||
)}
|
||||
@@ -262,12 +262,12 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 border-t border-border px-6 py-4">
|
||||
<div className="flex justify-end gap-3 border-t border-white/[0.06] px-6 py-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-md border border-input px-4 py-2 text-sm font-medium text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Close
|
||||
|
||||
@@ -21,7 +21,7 @@ const sortOptions: { value: SortBy; label: string }[] = [
|
||||
export function SortDropdown({ value, onChange, className }: SortDropdownProps) {
|
||||
return (
|
||||
<div className={cn('relative inline-flex items-center', className)}>
|
||||
<span className="mr-2 flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<span className="mr-2 flex items-center gap-1.5 text-sm text-white/40">
|
||||
<ArrowUpDown className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Sort:</span>
|
||||
</span>
|
||||
@@ -29,8 +29,8 @@ export function SortDropdown({ value, onChange, className }: SortDropdownProps)
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value as SortBy)}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-1.5 text-sm',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-1.5 text-sm',
|
||||
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
|
||||
@@ -30,13 +30,13 @@ export function TreeGridView({
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
|
||||
className="glass-card rounded-2xl p-4 transition-all hover:-translate-y-0.5 hover:border-white/20 hover:shadow-md sm:p-6"
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
|
||||
<h3 className="font-semibold text-white">{tree.name}</h3>
|
||||
{tree.status === 'draft' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400">
|
||||
<FileText className="h-3 w-3" />
|
||||
Draft
|
||||
</span>
|
||||
@@ -45,21 +45,21 @@ export function TreeGridView({
|
||||
<div className="flex items-center gap-2">
|
||||
{tree.is_public ? (
|
||||
<span title="Public tree">
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
<Globe className="h-4 w-4 text-white/40" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Private tree">
|
||||
<Lock className="h-4 w-4 text-muted-foreground" />
|
||||
<Lock className="h-4 w-4 text-white/40" />
|
||||
</span>
|
||||
)}
|
||||
{tree.category_info && (
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
|
||||
<p className="mb-3 text-sm text-white/70 line-clamp-2">
|
||||
{tree.description || 'No description available'}
|
||||
</p>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function TreeGridView({
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-white/40">
|
||||
v{tree.version} · {tree.usage_count} uses
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -81,8 +81,8 @@ export function TreeGridView({
|
||||
type="button"
|
||||
onClick={() => onForkTree(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-2 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Fork tree"
|
||||
>
|
||||
@@ -93,8 +93,8 @@ export function TreeGridView({
|
||||
<Link
|
||||
to={`/trees/${tree.id}/edit`}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-2 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Edit tree"
|
||||
>
|
||||
@@ -106,8 +106,8 @@ export function TreeGridView({
|
||||
type="button"
|
||||
onClick={() => onDeleteTree(tree)}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
||||
'hover:bg-destructive/10 hover:text-destructive'
|
||||
'rounded-md border border-white/10 p-1.5 text-white/60',
|
||||
'hover:bg-red-400/10 hover:text-red-400'
|
||||
)}
|
||||
title="Delete tree"
|
||||
>
|
||||
@@ -118,8 +118,8 @@ export function TreeGridView({
|
||||
type="button"
|
||||
onClick={() => onStartSession(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Start Session
|
||||
|
||||
@@ -29,29 +29,29 @@ export function TreeListView({
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-all hover:border-primary/30 hover:shadow-sm"
|
||||
className="flex items-center gap-4 glass-card rounded-2xl p-4 transition-all hover:border-white/20 hover:shadow-sm"
|
||||
>
|
||||
{/* Left: Name and Description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-card-foreground truncate">{tree.name}</h3>
|
||||
<h3 className="font-semibold text-white truncate">{tree.name}</h3>
|
||||
{tree.status === 'draft' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400 flex-shrink-0">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400 flex-shrink-0">
|
||||
<FileText className="h-3 w-3" />
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
{tree.is_public ? (
|
||||
<span title="Public tree">
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Globe className="h-3.5 w-3.5 text-white/40 flex-shrink-0" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Private tree">
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Lock className="h-3.5 w-3.5 text-white/40 flex-shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
<p className="text-sm text-white/70 truncate">
|
||||
{tree.description || 'No description available'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@ export function TreeListView({
|
||||
{/* Center: Category and Tags */}
|
||||
<div className="hidden lg:flex items-center gap-2 min-w-0" style={{ maxWidth: '300px' }}>
|
||||
{tree.category_info && (
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground whitespace-nowrap">
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70 whitespace-nowrap">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
@@ -72,7 +72,7 @@ export function TreeListView({
|
||||
|
||||
{/* Right: Metadata and Actions */}
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<div className="hidden sm:flex flex-col items-end text-xs text-muted-foreground">
|
||||
<div className="hidden sm:flex flex-col items-end text-xs text-white/40">
|
||||
<span>v{tree.version}</span>
|
||||
<span>{tree.usage_count} uses</span>
|
||||
</div>
|
||||
@@ -84,8 +84,8 @@ export function TreeListView({
|
||||
type="button"
|
||||
onClick={() => onForkTree(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-1.5 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Fork tree"
|
||||
>
|
||||
@@ -96,8 +96,8 @@ export function TreeListView({
|
||||
<Link
|
||||
to={`/trees/${tree.id}/edit`}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-1.5 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Edit tree"
|
||||
>
|
||||
@@ -108,8 +108,8 @@ export function TreeListView({
|
||||
type="button"
|
||||
onClick={() => onStartSession(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 whitespace-nowrap'
|
||||
'rounded-md bg-white px-3 py-1.5 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
Start
|
||||
|
||||
@@ -69,12 +69,12 @@ export function TreeTableView({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border border-border">
|
||||
<div className="overflow-x-auto rounded-2xl border border-white/[0.06]">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 sticky top-0 z-10">
|
||||
<tr className="border-b border-border">
|
||||
<thead className="bg-white/[0.02] sticky top-0 z-10">
|
||||
<tr className="border-b border-white/[0.06]">
|
||||
<th
|
||||
className="px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
className="px-4 py-3 text-left text-sm font-medium text-white/50 cursor-pointer hover:text-white"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -82,11 +82,11 @@ export function TreeTableView({
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="hidden md:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground">
|
||||
<th className="hidden md:table-cell px-4 py-3 text-left text-sm font-medium text-white/50">
|
||||
Description
|
||||
</th>
|
||||
<th
|
||||
className="hidden lg:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
className="hidden lg:table-cell px-4 py-3 text-left text-sm font-medium text-white/50 cursor-pointer hover:text-white"
|
||||
onClick={() => handleSort('category')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -94,11 +94,11 @@ export function TreeTableView({
|
||||
{getSortIcon('category')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="hidden xl:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground">
|
||||
<th className="hidden xl:table-cell px-4 py-3 text-left text-sm font-medium text-white/50">
|
||||
Tags
|
||||
</th>
|
||||
<th
|
||||
className="hidden sm:table-cell px-4 py-3 text-center text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
className="hidden sm:table-cell px-4 py-3 text-center text-sm font-medium text-white/50 cursor-pointer hover:text-white"
|
||||
onClick={() => handleSort('version')}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
@@ -107,7 +107,7 @@ export function TreeTableView({
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="hidden sm:table-cell px-4 py-3 text-center text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
className="hidden sm:table-cell px-4 py-3 text-center text-sm font-medium text-white/50 cursor-pointer hover:text-white"
|
||||
onClick={() => handleSort('usage')}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
@@ -116,7 +116,7 @@ export function TreeTableView({
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="hidden md:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
className="hidden md:table-cell px-4 py-3 text-left text-sm font-medium text-white/50 cursor-pointer hover:text-white"
|
||||
onClick={() => handleSort('updated')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -124,44 +124,44 @@ export function TreeTableView({
|
||||
{getSortIcon('updated')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground">
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-white/50">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-card">
|
||||
<tbody className="bg-transparent">
|
||||
{trees.map((tree) => (
|
||||
<tr key={tree.id} className="border-b border-border last:border-0 hover:bg-accent/50">
|
||||
<tr key={tree.id} className="border-b border-white/[0.06] last:border-0 hover:bg-white/[0.04]">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-card-foreground truncate max-w-[200px]">
|
||||
<span className="font-medium text-white truncate max-w-[200px]">
|
||||
{tree.name}
|
||||
</span>
|
||||
{tree.status === 'draft' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400 flex-shrink-0">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400 flex-shrink-0">
|
||||
<FileText className="h-3 w-3" />
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
{tree.is_public ? (
|
||||
<span title="Public tree">
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Globe className="h-3.5 w-3.5 text-white/40 flex-shrink-0" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Private tree">
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Lock className="h-3.5 w-3.5 text-white/40 flex-shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden md:table-cell px-4 py-3 text-sm text-muted-foreground">
|
||||
<td className="hidden md:table-cell px-4 py-3 text-sm text-white/70">
|
||||
<span className="truncate block max-w-[250px]">
|
||||
{tree.description || 'No description'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="hidden lg:table-cell px-4 py-3">
|
||||
{tree.category_info && (
|
||||
<span className="inline-block rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
|
||||
<span className="inline-block rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
@@ -171,13 +171,13 @@ export function TreeTableView({
|
||||
<TagBadges tags={tree.tags} maxVisible={2} onTagClick={onTagClick} />
|
||||
)}
|
||||
</td>
|
||||
<td className="hidden sm:table-cell px-4 py-3 text-center text-sm text-muted-foreground">
|
||||
<td className="hidden sm:table-cell px-4 py-3 text-center text-sm text-white/70">
|
||||
v{tree.version}
|
||||
</td>
|
||||
<td className="hidden sm:table-cell px-4 py-3 text-center text-sm text-muted-foreground">
|
||||
<td className="hidden sm:table-cell px-4 py-3 text-center text-sm text-white/70">
|
||||
{tree.usage_count}
|
||||
</td>
|
||||
<td className="hidden md:table-cell px-4 py-3 text-sm text-muted-foreground">
|
||||
<td className="hidden md:table-cell px-4 py-3 text-sm text-white/70">
|
||||
{formatDate(tree.updated_at)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -188,8 +188,8 @@ export function TreeTableView({
|
||||
type="button"
|
||||
onClick={() => onForkTree(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-1.5 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Fork tree"
|
||||
>
|
||||
@@ -200,8 +200,8 @@ export function TreeTableView({
|
||||
<Link
|
||||
to={`/trees/${tree.id}/edit`}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-1.5 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Edit tree"
|
||||
>
|
||||
@@ -212,8 +212,8 @@ export function TreeTableView({
|
||||
type="button"
|
||||
onClick={() => onStartSession(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 whitespace-nowrap'
|
||||
'rounded-md bg-white px-3 py-1.5 text-xs font-medium text-black',
|
||||
'hover:bg-white/90 whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
Start
|
||||
|
||||
@@ -11,15 +11,15 @@ interface ViewToggleProps {
|
||||
|
||||
export function ViewToggle({ view, onChange, className }: ViewToggleProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1 rounded-md border border-input p-1', className)}>
|
||||
<div className={cn('flex items-center gap-1 rounded-md border border-white/10 p-1', className)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange('grid')}
|
||||
className={cn(
|
||||
'rounded p-1.5 transition-colors',
|
||||
view === 'grid'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
? 'bg-white/10 text-white border-white/20'
|
||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
title="Grid view"
|
||||
>
|
||||
@@ -31,8 +31,8 @@ export function ViewToggle({ view, onChange, className }: ViewToggleProps) {
|
||||
className={cn(
|
||||
'rounded p-1.5 transition-colors',
|
||||
view === 'list'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
? 'bg-white/10 text-white border-white/20'
|
||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
title="List view"
|
||||
>
|
||||
@@ -44,8 +44,8 @@ export function ViewToggle({ view, onChange, className }: ViewToggleProps) {
|
||||
className={cn(
|
||||
'rounded p-1.5 transition-colors',
|
||||
view === 'table'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
? 'bg-white/10 text-white border-white/20'
|
||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||
)}
|
||||
title="Table view"
|
||||
>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function ContinuationModal({
|
||||
{/* Descendant Selection */}
|
||||
{hasDescendants && (
|
||||
<div>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<p className="mb-4 text-sm text-white/70">
|
||||
Select the next step in your troubleshooting path:
|
||||
</p>
|
||||
|
||||
@@ -56,20 +56,20 @@ export function ContinuationModal({
|
||||
onClick={() => onSelectNode(node.id)}
|
||||
title={`From: ${node.parentOptionLabel}`}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-lg border border-border p-3 text-left transition-colors',
|
||||
'hover:border-primary hover:bg-accent'
|
||||
'flex w-full items-center gap-3 rounded-lg border border-white/[0.06] p-3 text-left transition-colors',
|
||||
'hover:border-white/20 hover:bg-white/10'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-muted">
|
||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white/10">
|
||||
{nodeTypeIcons[node.type]}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium">{node.label}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="truncate font-medium text-white">{node.label}</p>
|
||||
<p className="text-xs text-white/40">
|
||||
{nodeTypeLabels[node.type]}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||
<ArrowRight className="h-4 w-4 flex-shrink-0 text-white/40" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -79,11 +79,11 @@ export function ContinuationModal({
|
||||
{/* Divider */}
|
||||
{hasDescendants && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
<div className="h-px flex-1 bg-white/[0.06]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-white/40">
|
||||
Or
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<div className="h-px flex-1 bg-white/[0.06]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -100,17 +100,17 @@ export function ContinuationModal({
|
||||
<GitBranch className="h-5 w-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">Build Custom Branch</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="font-medium text-white">Build Custom Branch</p>
|
||||
<p className="text-sm text-white/70">
|
||||
Create your own troubleshooting path with custom steps
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Warning */}
|
||||
<div className="mt-3 flex items-start gap-2 rounded-md bg-amber-500/10 p-3">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500" />
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||
<div className="mt-3 flex items-start gap-2 rounded-md bg-yellow-400/10 p-3">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-400" />
|
||||
<p className="text-sm text-yellow-400">
|
||||
You'll need to complete this branch manually or mark the issue as resolved.
|
||||
Custom branches can be saved as a personal tree when your session ends.
|
||||
</p>
|
||||
|
||||
@@ -46,9 +46,9 @@ export function ExportPreviewModal({
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} title="Export Preview" size="xl">
|
||||
{/* Filename and format info */}
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
Filename: <span className="font-mono text-foreground">{filename}</span>
|
||||
<span className="ml-3 rounded bg-secondary px-2 py-0.5 text-xs">
|
||||
<p className="mb-3 text-sm text-white/70">
|
||||
Filename: <span className="font-mono text-white">{filename}</span>
|
||||
<span className="ml-3 rounded bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
||||
{format === 'markdown' ? 'Markdown' : format === 'html' ? 'HTML' : 'Plain Text'}
|
||||
</span>
|
||||
</p>
|
||||
@@ -56,8 +56,8 @@ export function ExportPreviewModal({
|
||||
{/* Content Preview */}
|
||||
<div
|
||||
className={cn(
|
||||
'max-h-96 overflow-auto rounded-md border border-input bg-muted/50 p-4',
|
||||
'font-mono text-sm text-foreground'
|
||||
'max-h-96 overflow-auto rounded-md border border-white/10 bg-black/50 p-4',
|
||||
'font-mono text-sm text-white'
|
||||
)}
|
||||
>
|
||||
<pre className="whitespace-pre-wrap">{content}</pre>
|
||||
@@ -68,14 +68,14 @@ export function ExportPreviewModal({
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium',
|
||||
'bg-background text-foreground hover:bg-accent',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
<Check className="h-4 w-4 text-emerald-400" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
@@ -88,8 +88,8 @@ export function ExportPreviewModal({
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'flex items-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ForkTreeModal({
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-2 text-sm font-medium transition-colors',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -59,8 +59,8 @@ export function ForkTreeModal({
|
||||
onClick={handleFork}
|
||||
disabled={isSaving || !name.trim()}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors',
|
||||
'hover:bg-primary/90',
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black transition-colors',
|
||||
'hover:bg-white/90',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -82,13 +82,13 @@ export function ForkTreeModal({
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Save Custom Tree?" footer={footer}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 rounded-lg bg-accent/50 p-4">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-primary/10">
|
||||
<GitFork className="h-5 w-5 text-primary" />
|
||||
<div className="flex items-start gap-3 rounded-lg bg-white/5 p-4">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-white/10">
|
||||
<GitFork className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">You've created a custom troubleshooting path!</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="font-medium text-white">You've created a custom troubleshooting path!</p>
|
||||
<p className="mt-1 text-sm text-white/70">
|
||||
Save it as your own personal tree to reuse this troubleshooting flow in the future.
|
||||
</p>
|
||||
</div>
|
||||
@@ -96,8 +96,8 @@ export function ForkTreeModal({
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium">
|
||||
Tree Name <span className="text-destructive">*</span>
|
||||
<label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium text-white">
|
||||
Tree Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="tree-name"
|
||||
@@ -106,15 +106,15 @@ export function ForkTreeModal({
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My Custom Tree"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium">
|
||||
Description <span className="text-muted-foreground">(optional)</span>
|
||||
<label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium text-white">
|
||||
Description <span className="text-white/40">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="tree-description"
|
||||
@@ -123,8 +123,8 @@ export function ForkTreeModal({
|
||||
placeholder="Describe what this tree helps troubleshoot..."
|
||||
rows={3}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20',
|
||||
'resize-none'
|
||||
)}
|
||||
/>
|
||||
@@ -132,10 +132,10 @@ export function ForkTreeModal({
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-white/40">
|
||||
The new tree will include your custom steps and will be saved to your personal tree library.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,8 @@ export function PostStepActionModal({
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="What would you like to do?">
|
||||
<div className="space-y-3">
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
You've created: <strong className="text-foreground">{step.title}</strong>
|
||||
<p className="mb-4 text-sm text-white/70">
|
||||
You've created: <strong className="text-white">{step.title}</strong>
|
||||
</p>
|
||||
|
||||
{/* Save for Later - Only show if not already from library */}
|
||||
@@ -48,8 +48,8 @@ export function PostStepActionModal({
|
||||
<Bookmark className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Save for Later</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="font-medium text-white">Save for Later</p>
|
||||
<p className="text-sm text-white/70">
|
||||
Add to your step library for future use
|
||||
</p>
|
||||
</div>
|
||||
@@ -62,8 +62,8 @@ export function PostStepActionModal({
|
||||
onClick={onUseNow}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-border p-4 text-left transition-colors',
|
||||
'hover:border-primary hover:bg-accent',
|
||||
'w-full rounded-lg border border-white/[0.06] p-4 text-left transition-colors',
|
||||
'hover:border-white/20 hover:bg-white/10',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -96,8 +96,8 @@ export function PostStepActionModal({
|
||||
<BookmarkPlus className="h-5 w-5 text-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Do Both</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="font-medium text-white">Do Both</p>
|
||||
<p className="text-sm text-white/70">
|
||||
Save to library AND use in this session
|
||||
</p>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export function PostStepActionModal({
|
||||
)}
|
||||
|
||||
{isSaving && (
|
||||
<p className="text-center text-sm text-muted-foreground">Saving...</p>
|
||||
<p className="text-center text-sm text-white/40">Saving...</p>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -33,22 +33,22 @@ export function SaveSessionAsTreeModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-lg rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="glass-card w-full max-w-lg rounded-2xl p-6 shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-foreground">Save Session as Tree</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Save Session as Tree</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||||
className="rounded-full p-1 text-white/40 hover:bg-white/10 hover:text-white disabled:opacity-50"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<p className="mb-4 text-sm text-white/70">
|
||||
Create a new tree from this session's path. The tree will be linked to the original tree as a fork.
|
||||
</p>
|
||||
|
||||
@@ -56,8 +56,8 @@ export function SaveSessionAsTreeModal({
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Tree Name */}
|
||||
<div>
|
||||
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Tree Name <span className="text-muted-foreground">(optional)</span>
|
||||
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-white">
|
||||
Tree Name <span className="text-white/40">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="treeName"
|
||||
@@ -68,9 +68,9 @@ export function SaveSessionAsTreeModal({
|
||||
disabled={isSaving}
|
||||
maxLength={255}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -78,8 +78,8 @@ export function SaveSessionAsTreeModal({
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Description <span className="text-muted-foreground">(optional)</span>
|
||||
<label htmlFor="description" className="mb-1 block text-sm font-medium text-white">
|
||||
Description <span className="text-white/40">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
@@ -89,9 +89,9 @@ export function SaveSessionAsTreeModal({
|
||||
disabled={isSaving}
|
||||
rows={3}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -99,7 +99,7 @@ export function SaveSessionAsTreeModal({
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">Status</label>
|
||||
<label className="mb-2 block text-sm font-medium text-white">Status</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
@@ -109,9 +109,9 @@ export function SaveSessionAsTreeModal({
|
||||
checked={status === 'draft'}
|
||||
onChange={() => setStatus('draft')}
|
||||
disabled={isSaving}
|
||||
className="h-4 w-4 border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 border-white/10 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-foreground">Draft</span>
|
||||
<span className="text-sm text-white">Draft</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
@@ -121,9 +121,9 @@ export function SaveSessionAsTreeModal({
|
||||
checked={status === 'published'}
|
||||
onChange={() => setStatus('published')}
|
||||
disabled={isSaving}
|
||||
className="h-4 w-4 border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 border-white/10 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-foreground">Published</span>
|
||||
<span className="text-sm text-white">Published</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,8 +135,8 @@ export function SaveSessionAsTreeModal({
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -145,8 +145,8 @@ export function SaveSessionAsTreeModal({
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save as Tree'}
|
||||
|
||||
@@ -124,8 +124,8 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
onClick={() => setIsCollapsed(false)}
|
||||
className={cn(
|
||||
'fixed right-2 top-1/2 z-40 -translate-y-1/2 rounded-md p-2.5',
|
||||
'bg-card border border-border shadow-md',
|
||||
'text-muted-foreground hover:bg-accent hover:text-foreground',
|
||||
'bg-[#0a0a0a] border border-white/[0.06] shadow-md',
|
||||
'text-white/40 hover:bg-white/10 hover:text-white',
|
||||
'transition-opacity duration-200',
|
||||
isCollapsed ? 'opacity-100' : 'pointer-events-none opacity-0'
|
||||
)}
|
||||
@@ -140,7 +140,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
{/* Mobile backdrop */}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-background/80 backdrop-blur-sm sm:hidden"
|
||||
className="fixed inset-0 z-30 bg-black/80 backdrop-blur-sm sm:hidden"
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -152,29 +152,29 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
'fixed z-40',
|
||||
'inset-0 sm:inset-auto sm:right-2 sm:top-1/2 sm:-translate-y-1/2',
|
||||
'flex w-full flex-col sm:h-[55vh] sm:w-[420px]',
|
||||
'border-border bg-card shadow-xl sm:rounded-lg sm:border',
|
||||
'border-white/[0.06] bg-[#0a0a0a]/95 backdrop-blur-md shadow-xl sm:rounded-lg sm:border',
|
||||
'transition-transform duration-200 ease-out',
|
||||
isCollapsed ? 'translate-x-full' : 'translate-x-0'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<StickyNote className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-foreground">Scratchpad</span>
|
||||
<span className="text-xs text-muted-foreground/60">Ctrl+/</span>
|
||||
<StickyNote className="h-4 w-4 text-white/40" />
|
||||
<span className="text-sm font-medium text-white">Scratchpad</span>
|
||||
<span className="text-xs text-white/30">Ctrl+/</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded p-1 text-white/40 hover:bg-white/10 hover:text-white"
|
||||
title={showPreview ? 'Edit' : 'Preview'}
|
||||
>
|
||||
{showPreview ? <Pencil className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded p-1 text-white/40 hover:bg-white/10 hover:text-white"
|
||||
title="Close scratchpad"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -189,7 +189,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
{content.trim() ? (
|
||||
<MarkdownContent content={content} className="text-sm" />
|
||||
) : (
|
||||
<p className="text-sm italic text-muted-foreground">Nothing to preview</p>
|
||||
<p className="text-sm italic text-white/40">Nothing to preview</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -200,7 +200,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
placeholder={"Capture IPs, error codes, server names, user info...\n\nSupports markdown formatting."}
|
||||
className={cn(
|
||||
'h-full min-h-[200px] w-full resize-none rounded-md border-0 bg-transparent p-0 text-sm',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:outline-none focus:ring-0'
|
||||
)}
|
||||
/>
|
||||
@@ -208,25 +208,25 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
</div>
|
||||
|
||||
{/* Save Indicator */}
|
||||
<div className="border-t border-border px-3 py-1.5">
|
||||
<div className="border-t border-white/[0.06] px-3 py-1.5">
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
{saveStatus === 'unsaved' && (
|
||||
<span className="text-muted-foreground">Unsaved changes</span>
|
||||
<span className="text-white/40">Unsaved changes</span>
|
||||
)}
|
||||
{saveStatus === 'saving' && (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Saving...</span>
|
||||
<Loader2 className="h-3 w-3 animate-spin text-white/40" />
|
||||
<span className="text-white/40">Saving...</span>
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && (
|
||||
<span className="text-green-600 dark:text-green-400">Saved</span>
|
||||
<span className="text-emerald-400">Saved</span>
|
||||
)}
|
||||
{saveStatus === 'error' && (
|
||||
<span className="text-destructive">Save failed</span>
|
||||
<span className="text-red-400">Save failed</span>
|
||||
)}
|
||||
{saveStatus === 'idle' && (
|
||||
<span className="text-muted-foreground/50">Markdown supported</span>
|
||||
<span className="text-white/30">Markdown supported</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,32 +93,32 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
{/* Ticket Number Search */}
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by ticket number..."
|
||||
value={filters.ticketNumber}
|
||||
onChange={(e) => handleFilterChange('ticketNumber', e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background py-2 pl-9 pr-3',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 py-2 pl-9 pr-3',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Client Name Search */}
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by client name..."
|
||||
value={filters.clientName}
|
||||
onChange={(e) => handleFilterChange('clientName', e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background py-2 pl-9 pr-3',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 py-2 pl-9 pr-3',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -128,8 +128,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
value={filters.treeName}
|
||||
onChange={(e) => handleFilterChange('treeName', e.target.value)}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'sm:min-w-[200px]'
|
||||
)}
|
||||
>
|
||||
@@ -148,19 +148,19 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'text-foreground hover:bg-accent',
|
||||
filters.dateRange?.from && 'border-primary'
|
||||
'flex w-full items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm',
|
||||
'text-white hover:bg-white/10',
|
||||
filters.dateRange?.from && 'border-white/30'
|
||||
)}
|
||||
>
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className={cn(!filters.dateRange?.from && 'text-muted-foreground')}>
|
||||
<Calendar className="h-4 w-4 text-white/40" />
|
||||
<span className={cn(!filters.dateRange?.from && 'text-white/40')}>
|
||||
{formatDateRange(filters.dateRange)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showDatePicker && (
|
||||
<div className="absolute left-0 top-full z-50 mt-2 rounded-lg border border-border bg-popover p-4 shadow-lg">
|
||||
<div className="absolute left-0 top-full z-50 mt-2 rounded-lg border border-white/[0.06] bg-[#0a0a0a] p-4 shadow-lg">
|
||||
{/* Date Type Toggle */}
|
||||
<div className="mb-3 flex gap-2">
|
||||
<button
|
||||
@@ -168,8 +168,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
className={cn(
|
||||
'flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
filters.dateType === 'started'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-accent text-accent-foreground hover:bg-accent/80'
|
||||
? 'bg-white text-black'
|
||||
: 'border border-white/10 text-white/60 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Started
|
||||
@@ -179,8 +179,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
className={cn(
|
||||
'flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
filters.dateType === 'completed'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-accent text-accent-foreground hover:bg-accent/80'
|
||||
? 'bg-white text-black'
|
||||
: 'border border-white/10 text-white/60 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Completed
|
||||
@@ -194,8 +194,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
key={preset.value}
|
||||
onClick={() => applyDatePreset(preset.value)}
|
||||
className={cn(
|
||||
'rounded-md bg-accent px-3 py-1.5 text-sm font-medium',
|
||||
'hover:bg-accent/80'
|
||||
'rounded-md bg-white/10 px-3 py-1.5 text-sm font-medium text-white/70',
|
||||
'hover:bg-white/20 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
@@ -227,8 +227,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex-1 rounded-md bg-white px-3 py-1.5 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Apply
|
||||
@@ -236,8 +236,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
<button
|
||||
onClick={() => setShowDatePicker(false)}
|
||||
className={cn(
|
||||
'rounded-md bg-accent px-3 py-1.5 text-sm font-medium',
|
||||
'hover:bg-accent/80'
|
||||
'rounded-md border border-white/10 px-3 py-1.5 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -252,8 +252,8 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
<button
|
||||
onClick={onClear}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Filter className="h-4 w-4" />
|
||||
@@ -265,46 +265,46 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
{/* Active Filter Chips */}
|
||||
{hasActiveFilters && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Active filters:</span>
|
||||
<span className="text-sm text-white/40">Active filters:</span>
|
||||
{filters.ticketNumber && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white/70">
|
||||
Ticket: {filters.ticketNumber}
|
||||
<button
|
||||
onClick={() => handleFilterChange('ticketNumber', '')}
|
||||
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.clientName && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white/70">
|
||||
Client: {filters.clientName}
|
||||
<button
|
||||
onClick={() => handleFilterChange('clientName', '')}
|
||||
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.treeName && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white/70">
|
||||
Tree: {filters.treeName}
|
||||
<button
|
||||
onClick={() => handleFilterChange('treeName', '')}
|
||||
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.dateRange?.from && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white/70">
|
||||
{formatDateRange(filters.dateRange)} ({filters.dateType})
|
||||
<button
|
||||
onClick={clearDateRange}
|
||||
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
@@ -77,20 +77,20 @@ export function StepRatingModal({
|
||||
const getRating = (stepId: string) => ratings.get(stepId)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
|
||||
<div className="w-full max-w-2xl max-h-[90vh] flex flex-col rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||
<div className="glass-card w-full max-w-2xl max-h-[90vh] flex flex-col rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-6 py-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Rate Your Experience</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<h2 className="text-lg font-semibold text-white">Rate Your Experience</h2>
|
||||
<p className="mt-1 text-sm text-white/70">
|
||||
Help others by rating the steps you used ({librarySteps.length} step{librarySteps.length !== 1 ? 's' : ''})
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||||
className="rounded-full p-1 text-white/40 hover:bg-white/10 hover:text-white disabled:opacity-50"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -102,14 +102,14 @@ export function StepRatingModal({
|
||||
{librarySteps.map((step) => {
|
||||
const rating = getRating(step.id)
|
||||
return (
|
||||
<div key={step.id} className="rounded-lg border border-border bg-background p-4">
|
||||
<div key={step.id} className="rounded-lg border border-white/[0.06] bg-[#0a0a0a] p-4">
|
||||
{/* Step Title */}
|
||||
<h3 className="font-medium text-foreground">{step.title}</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground capitalize">{step.step_type}</p>
|
||||
<h3 className="font-medium text-white">{step.title}</h3>
|
||||
<p className="mt-1 text-sm text-white/40 capitalize">{step.step_type}</p>
|
||||
|
||||
{/* Star Rating */}
|
||||
<div className="mt-3">
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">
|
||||
<label className="mb-1 block text-sm font-medium text-white">
|
||||
Rating
|
||||
</label>
|
||||
<StarRating
|
||||
@@ -121,7 +121,7 @@ export function StepRatingModal({
|
||||
|
||||
{/* Was this helpful? */}
|
||||
<div className="mt-3">
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
<label className="mb-2 block text-sm font-medium text-white">
|
||||
Was this helpful?
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
@@ -132,8 +132,8 @@ export function StepRatingModal({
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors',
|
||||
rating?.helpful === true
|
||||
? 'border-green-500 bg-green-500/10 text-green-600 dark:text-green-400'
|
||||
: 'border-input bg-background text-foreground hover:bg-accent',
|
||||
? 'border-emerald-400/20 bg-emerald-400/10 text-emerald-400'
|
||||
: 'border-white/10 text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -147,8 +147,8 @@ export function StepRatingModal({
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors',
|
||||
rating?.helpful === false
|
||||
? 'border-red-500 bg-red-500/10 text-red-600 dark:text-red-400'
|
||||
: 'border-input bg-background text-foreground hover:bg-accent',
|
||||
? 'border-red-400/20 bg-red-400/10 text-red-400'
|
||||
: 'border-white/10 text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -160,8 +160,8 @@ export function StepRatingModal({
|
||||
|
||||
{/* Optional Review */}
|
||||
<div className="mt-3">
|
||||
<label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-foreground">
|
||||
Review <span className="text-muted-foreground">(optional)</span>
|
||||
<label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-white">
|
||||
Review <span className="text-white/40">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id={`review-${step.id}`}
|
||||
@@ -172,13 +172,13 @@ export function StepRatingModal({
|
||||
rows={2}
|
||||
placeholder="Share your experience with this step..."
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground text-right">
|
||||
<p className="mt-1 text-xs text-white/40 text-right">
|
||||
{rating?.review?.length || 0}/500
|
||||
</p>
|
||||
</div>
|
||||
@@ -189,14 +189,14 @@ export function StepRatingModal({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-2 border-t border-border px-6 py-4">
|
||||
<div className="flex justify-end gap-2 border-t border-white/[0.06] px-6 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
Skip
|
||||
@@ -206,8 +206,8 @@ export function StepRatingModal({
|
||||
onClick={handleSubmit}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{isSaving ? 'Submitting...' : 'Submit Ratings'}
|
||||
|
||||
@@ -64,14 +64,14 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-background/80 backdrop-blur-sm sm:items-center sm:p-4">
|
||||
<div className="relative flex h-[95vh] w-full max-w-full flex-col border border-border bg-card shadow-lg sm:h-[90vh] sm:max-w-4xl sm:rounded-lg">
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/80 backdrop-blur-sm sm:items-center sm:p-4">
|
||||
<div className="relative flex h-[95vh] w-full max-w-full flex-col border border-white/[0.06] bg-[#0a0a0a] shadow-lg sm:h-[90vh] sm:max-w-4xl sm:rounded-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border p-4">
|
||||
<h2 className="text-lg font-semibold">Add Custom Step</h2>
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] p-4">
|
||||
<h2 className="text-lg font-semibold text-white">Add Custom Step</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md p-1.5 hover:bg-accent"
|
||||
className="rounded-md p-1.5 text-white/40 hover:bg-white/10 hover:text-white"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
@@ -79,15 +79,15 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-border">
|
||||
<div className="flex border-b border-white/[0.06]">
|
||||
{canCreateSteps && (
|
||||
<button
|
||||
onClick={() => setActiveTab('create')}
|
||||
className={cn(
|
||||
'flex-1 px-4 py-3 text-sm font-medium transition-colors',
|
||||
activeTab === 'create'
|
||||
? 'border-b-2 border-primary bg-primary/5 text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
|
||||
? 'border-b-2 border-white bg-white/5 text-white'
|
||||
: 'text-white/40 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Type My Own
|
||||
@@ -108,7 +108,7 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mx-4 mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="mx-4 mt-4 rounded-lg border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -132,10 +132,10 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{isSubmitting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<p className="text-sm text-muted-foreground">Creating step...</p>
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
<p className="text-sm text-white/40">Creating step...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,9 +15,9 @@ const stepTypeIcons = {
|
||||
}
|
||||
|
||||
const stepTypeColors = {
|
||||
decision: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30',
|
||||
action: 'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border-yellow-500/30',
|
||||
solution: 'bg-green-500/20 text-green-600 dark:text-green-400 border-green-500/30'
|
||||
decision: 'bg-blue-400/10 text-blue-400 border-blue-400/20',
|
||||
action: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20',
|
||||
solution: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20'
|
||||
}
|
||||
|
||||
export function StepCard({ step, onPreview, onInsert }: StepCardProps) {
|
||||
@@ -27,7 +27,7 @@ export function StepCard({ step, onPreview, onInsert }: StepCardProps) {
|
||||
const remainingTags = step.tags.length - 3
|
||||
|
||||
return (
|
||||
<div className="group rounded-lg border border-border bg-card p-4 transition-shadow hover:shadow-md">
|
||||
<div className="group rounded-lg border border-white/[0.06] bg-[#0a0a0a] p-4 transition-shadow hover:shadow-md">
|
||||
{/* Header */}
|
||||
<div className="mb-3 flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
@@ -45,19 +45,19 @@ export function StepCard({ step, onPreview, onInsert }: StepCardProps) {
|
||||
|
||||
{/* Featured Badge */}
|
||||
{step.is_featured && (
|
||||
<span className="rounded bg-amber-500/20 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||
<span className="rounded bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-semibold text-foreground line-clamp-2">{step.title}</h3>
|
||||
<h3 className="font-semibold text-white line-clamp-2">{step.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-3 space-y-1.5 text-sm text-muted-foreground">
|
||||
<div className="mb-3 space-y-1.5 text-sm text-white/40">
|
||||
{/* Category */}
|
||||
{step.category_name && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -103,7 +103,7 @@ export function StepCard({ step, onPreview, onInsert }: StepCardProps) {
|
||||
{visibleTags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
||||
className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
@@ -121,8 +121,8 @@ export function StepCard({ step, onPreview, onInsert }: StepCardProps) {
|
||||
<button
|
||||
onClick={() => onPreview(step)}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground transition-colors'
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white transition-colors'
|
||||
)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -131,8 +131,8 @@ export function StepCard({ step, onPreview, onInsert }: StepCardProps) {
|
||||
<button
|
||||
onClick={() => onInsert(step)}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 transition-colors'
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 transition-colors'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
|
||||
@@ -18,9 +18,9 @@ const stepTypeIcons = {
|
||||
}
|
||||
|
||||
const stepTypeColors = {
|
||||
decision: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30',
|
||||
action: 'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border-yellow-500/30',
|
||||
solution: 'bg-green-500/20 text-green-600 dark:text-green-400 border-green-500/30'
|
||||
decision: 'bg-blue-400/10 text-blue-400 border-blue-400/20',
|
||||
action: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20',
|
||||
solution: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20'
|
||||
}
|
||||
|
||||
export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalProps) {
|
||||
@@ -69,14 +69,14 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
const topReviews = reviews.filter(r => r.review_text).slice(0, 3)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="relative flex h-[90vh] w-full max-w-3xl flex-col rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="relative flex h-[90vh] w-full max-w-3xl flex-col glass-card rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between border-b border-border p-6 pb-4">
|
||||
<div className="flex items-start justify-between border-b border-white/[0.06] p-6 pb-4">
|
||||
{isLoading ? (
|
||||
<div className="h-6 w-48 animate-pulse rounded bg-muted" />
|
||||
<div className="h-6 w-48 animate-pulse rounded bg-white/10" />
|
||||
) : error ? (
|
||||
<h2 className="text-lg font-semibold text-destructive">{error}</h2>
|
||||
<h2 className="text-lg font-semibold text-red-400">{error}</h2>
|
||||
) : step ? (
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
@@ -90,25 +90,25 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
{step.step_type}
|
||||
</span>
|
||||
{step.category_name && (
|
||||
<span className="text-xs text-muted-foreground">📁 {step.category_name}</span>
|
||||
<span className="text-xs text-white/40">📁 {step.category_name}</span>
|
||||
)}
|
||||
{step.is_featured && (
|
||||
<span className="rounded bg-amber-500/20 px-2 py-0.5 text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||
<span className="rounded bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
{step.is_verified && (
|
||||
<span className="rounded bg-green-500/20 px-2 py-0.5 text-xs font-medium text-green-600 dark:text-green-400">
|
||||
<span className="rounded bg-emerald-400/10 px-2 py-0.5 text-xs font-medium text-emerald-400">
|
||||
✓ Verified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold">{step.title}</h2>
|
||||
<h2 className="text-xl font-semibold text-white">{step.title}</h2>
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
className="rounded-md p-1 text-white/40 hover:bg-white/10 hover:text-white"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
@@ -119,18 +119,18 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<div className="h-4 w-full animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-full animate-pulse rounded bg-white/10" />
|
||||
<div className="h-4 w-3/4 animate-pulse rounded bg-white/10" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<p className="text-sm text-muted-foreground">{error}</p>
|
||||
<p className="text-sm text-white/40">{error}</p>
|
||||
) : step ? (
|
||||
<div className="space-y-6">
|
||||
{/* Rating */}
|
||||
{hasRating && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold">Rating</h3>
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">Rating</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
@@ -139,13 +139,13 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
i <= Math.round(step.rating_average)
|
||||
? 'fill-yellow-500 text-yellow-500'
|
||||
: 'text-muted-foreground'
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'text-white/20'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/70">
|
||||
{step.rating_average.toFixed(1)} ({step.rating_count} {step.rating_count === 1 ? 'rating' : 'ratings'})
|
||||
</span>
|
||||
</div>
|
||||
@@ -155,12 +155,12 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
{/* Tags */}
|
||||
{step.tags.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold">Tags</h3>
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{step.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-muted px-2 py-1 text-xs text-muted-foreground"
|
||||
className="rounded-full bg-white/10 px-2 py-1 text-xs text-white/70"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
@@ -172,7 +172,7 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
{/* Instructions */}
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold">Instructions</h3>
|
||||
<div className="rounded-lg border border-border bg-muted/30 p-4">
|
||||
<div className="rounded-lg border border-white/[0.06] bg-white/5 p-4">
|
||||
<MarkdownContent content={step.content.instructions} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,8 +180,8 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
{/* Help Text */}
|
||||
{step.content.help_text && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold">Help Text</h3>
|
||||
<div className="rounded-lg border border-border bg-blue-500/5 p-4 text-sm">
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">Help Text</h3>
|
||||
<div className="rounded-lg border border-white/[0.06] bg-blue-400/5 p-4 text-sm">
|
||||
<MarkdownContent content={step.content.help_text} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,19 +190,19 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
{/* Commands */}
|
||||
{step.content.commands && step.content.commands.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold">Commands</h3>
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">Commands</h3>
|
||||
<div className="space-y-2">
|
||||
{step.content.commands.map((cmd, index) => (
|
||||
<div key={index} className="group relative">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">{cmd.label}</span>
|
||||
<span className="text-xs font-medium text-white/40">{cmd.label}</span>
|
||||
<button
|
||||
onClick={() => handleCopyCommand(cmd.command, index)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors',
|
||||
copiedCommandIndex === index
|
||||
? 'bg-green-500/20 text-green-600 dark:text-green-400'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
? 'bg-emerald-400/10 text-emerald-400'
|
||||
: 'bg-white/10 text-white/40 hover:bg-white/20 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{copiedCommandIndex === index ? (
|
||||
@@ -218,7 +218,7 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded bg-muted p-3 text-xs">
|
||||
<pre className="overflow-x-auto rounded bg-black/50 p-3 text-xs text-white">
|
||||
<code>{cmd.command}</code>
|
||||
</pre>
|
||||
</div>
|
||||
@@ -231,22 +231,22 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
{topReviews.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Reviews</h3>
|
||||
<h3 className="text-sm font-semibold text-white">Reviews</h3>
|
||||
{reviews.length > 3 && (
|
||||
<button className="text-xs text-primary hover:underline">
|
||||
<button className="text-xs text-white/70 hover:text-white hover:underline">
|
||||
See all {reviews.length} reviews
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topReviews.map(review => (
|
||||
<div key={review.id} className="rounded-lg border border-border bg-muted/30 p-3">
|
||||
<div key={review.id} className="rounded-lg border border-white/[0.06] bg-white/5 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<User className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">{review.user_name || 'Anonymous'}</span>
|
||||
<span className="font-medium text-white">{review.user_name || 'Anonymous'}</span>
|
||||
{review.verified_use && (
|
||||
<span className="rounded bg-green-500/20 px-1.5 py-0.5 text-xs text-green-600 dark:text-green-400">
|
||||
<span className="rounded bg-emerald-400/10 px-1.5 py-0.5 text-xs text-emerald-400">
|
||||
✓ Verified Use
|
||||
</span>
|
||||
)}
|
||||
@@ -258,15 +258,15 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
className={cn(
|
||||
'h-3 w-3',
|
||||
i <= review.rating
|
||||
? 'fill-yellow-500 text-yellow-500'
|
||||
: 'text-muted-foreground'
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'text-white/20'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{review.review_text}</p>
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<p className="text-sm text-white/70">{review.review_text}</p>
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-white/40">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{new Date(review.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
@@ -277,22 +277,22 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="rounded-lg border border-border bg-muted/30 p-4">
|
||||
<div className="rounded-lg border border-white/[0.06] bg-white/5 p-4">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Author:</span>
|
||||
<span className="ml-2 font-medium">{step.author_name || 'Unknown'}</span>
|
||||
<span className="text-white/40">Author:</span>
|
||||
<span className="ml-2 font-medium text-white">{step.author_name || 'Unknown'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Usage Count:</span>
|
||||
<span className="ml-2 font-medium">{step.usage_count}</span>
|
||||
<span className="text-white/40">Usage Count:</span>
|
||||
<span className="ml-2 font-medium text-white">{step.usage_count}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
<span className="ml-2 font-medium">{new Date(step.created_at).toLocaleDateString()}</span>
|
||||
<span className="text-white/40">Created:</span>
|
||||
<span className="ml-2 font-medium text-white">{new Date(step.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Visibility:</span>
|
||||
<span className="text-white/40">Visibility:</span>
|
||||
<span className="ml-2 font-medium capitalize">{step.visibility}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,10 +302,10 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
</div>
|
||||
|
||||
{/* Footer - Actions */}
|
||||
<div className="flex gap-2 border-t border-border p-4">
|
||||
<div className="flex gap-2 border-t border-white/[0.06] p-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent"
|
||||
className="flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -313,8 +313,8 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
onClick={handleInsert}
|
||||
disabled={!step}
|
||||
className={cn(
|
||||
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
Insert Into Session
|
||||
|
||||
@@ -136,8 +136,8 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Step Type */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium">
|
||||
Step Type <span className="text-destructive">*</span>
|
||||
<label className="mb-2 block text-sm font-medium text-white">
|
||||
Step Type <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{stepTypeOptions.map(option => {
|
||||
@@ -150,15 +150,15 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
className={cn(
|
||||
'rounded-lg border p-3 text-left transition-colors',
|
||||
stepType === option.value
|
||||
? 'border-primary bg-primary/10 ring-2 ring-primary'
|
||||
: 'border-border hover:border-primary/50'
|
||||
? 'border-white/20 bg-white/10 ring-2 ring-white/20'
|
||||
: 'border-white/[0.06] hover:border-white/20'
|
||||
)}
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="font-medium text-sm">{option.label}</span>
|
||||
<span className="font-medium text-sm text-white">{option.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{option.description}</p>
|
||||
<p className="text-xs text-white/40">{option.description}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -167,8 +167,8 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label htmlFor="title" className="mb-2 block text-sm font-medium">
|
||||
Title <span className="text-destructive">*</span>
|
||||
<label htmlFor="title" className="mb-2 block text-sm font-medium text-white">
|
||||
Title <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
@@ -177,20 +177,20 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Enter step title"
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring',
|
||||
errors.title ? 'border-destructive' : 'border-input'
|
||||
'w-full rounded-md border bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20',
|
||||
errors.title ? 'border-red-400/50' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.title}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{errors.title}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div>
|
||||
<label htmlFor="instructions" className="mb-2 block text-sm font-medium">
|
||||
Instructions <span className="text-destructive">*</span>
|
||||
<span className="ml-2 text-xs font-normal text-muted-foreground">(Markdown supported)</span>
|
||||
<label htmlFor="instructions" className="mb-2 block text-sm font-medium text-white">
|
||||
Instructions <span className="text-red-400">*</span>
|
||||
<span className="ml-2 text-xs font-normal text-white/40">(Markdown supported)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="instructions"
|
||||
@@ -199,19 +199,19 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
placeholder="Describe what to do in this step..."
|
||||
rows={6}
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring',
|
||||
errors.instructions ? 'border-destructive' : 'border-input'
|
||||
'w-full rounded-md border bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20',
|
||||
errors.instructions ? 'border-red-400/50' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{errors.instructions && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.instructions}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{errors.instructions}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div>
|
||||
<label htmlFor="helpText" className="mb-2 block text-sm font-medium">
|
||||
Help Text <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
|
||||
<label htmlFor="helpText" className="mb-2 block text-sm font-medium text-white">
|
||||
Help Text <span className="text-xs font-normal text-white/40">(Optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="helpText"
|
||||
@@ -219,20 +219,20 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
onChange={(e) => setHelpText(e.target.value)}
|
||||
placeholder="Additional context or tips..."
|
||||
rows={3}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Commands */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<label className="text-sm font-medium">
|
||||
Commands <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
|
||||
<label className="text-sm font-medium text-white">
|
||||
Commands <span className="text-xs font-normal text-white/40">(Optional)</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCommand}
|
||||
className="flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs font-medium hover:bg-muted/80"
|
||||
className="flex items-center gap-1 rounded-md bg-white/10 px-2 py-1 text-xs font-medium text-white/70 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Command
|
||||
@@ -241,13 +241,13 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
{commands.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{commands.map((cmd, index) => (
|
||||
<div key={index} className="rounded-lg border border-border bg-muted/30 p-3">
|
||||
<div key={index} className="rounded-lg border border-white/[0.06] bg-white/5 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">Command {index + 1}</span>
|
||||
<span className="text-xs font-medium text-white/40">Command {index + 1}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCommand(index)}
|
||||
className="rounded p-1 text-muted-foreground hover:bg-destructive/20 hover:text-destructive"
|
||||
className="rounded p-1 text-white/40 hover:bg-red-400/10 hover:text-red-400"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -259,8 +259,8 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
onChange={(e) => updateCommand(index, 'label', e.target.value)}
|
||||
placeholder="Command label (e.g., 'Restart service')"
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-background px-3 py-1.5 text-sm',
|
||||
errors[`command_${index}_label`] ? 'border-destructive' : 'border-input'
|
||||
'w-full rounded-md border bg-black/50 px-3 py-1.5 text-sm text-white',
|
||||
errors[`command_${index}_label`] ? 'border-red-400/50' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
@@ -269,12 +269,12 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
onChange={(e) => updateCommand(index, 'command', e.target.value)}
|
||||
placeholder="Command (e.g., 'systemctl restart nginx')"
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-background px-3 py-1.5 font-mono text-sm',
|
||||
errors[`command_${index}_command`] ? 'border-destructive' : 'border-input'
|
||||
'w-full rounded-md border bg-black/50 px-3 py-1.5 font-mono text-sm text-white',
|
||||
errors[`command_${index}_command`] ? 'border-red-400/50' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{(errors[`command_${index}_label`] || errors[`command_${index}_command`]) && (
|
||||
<p className="text-xs text-destructive">
|
||||
<p className="text-xs text-red-400">
|
||||
{errors[`command_${index}_label`] || errors[`command_${index}_command`]}
|
||||
</p>
|
||||
)}
|
||||
@@ -287,14 +287,14 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label htmlFor="category" className="mb-2 block text-sm font-medium">
|
||||
Category <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
|
||||
<label htmlFor="category" className="mb-2 block text-sm font-medium text-white">
|
||||
Category <span className="text-xs font-normal text-white/40">(Optional)</span>
|
||||
</label>
|
||||
<select
|
||||
id="category"
|
||||
value={categoryId}
|
||||
onChange={(e) => setCategoryId(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{categories.map(cat => (
|
||||
@@ -305,8 +305,8 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label htmlFor="tagInput" className="mb-2 block text-sm font-medium">
|
||||
Tags <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
|
||||
<label htmlFor="tagInput" className="mb-2 block text-sm font-medium text-white">
|
||||
Tags <span className="text-xs font-normal text-white/40">(Optional)</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -316,12 +316,12 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleTagInputKeyDown}
|
||||
placeholder="Type tag and press Enter"
|
||||
className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addTag}
|
||||
className="rounded-md bg-muted px-4 py-2 text-sm font-medium hover:bg-muted/80"
|
||||
className="rounded-md bg-white/10 px-4 py-2 text-sm font-medium text-white/70 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -331,13 +331,13 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
{tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="flex items-center gap-1 rounded-full bg-primary/10 px-2.5 py-1 text-xs text-primary"
|
||||
className="flex items-center gap-1 rounded-full bg-white/10 px-2.5 py-1 text-xs text-white/70"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="rounded-full hover:bg-primary/20"
|
||||
className="rounded-full hover:bg-white/20"
|
||||
aria-label={`Remove tag ${tag}`}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
@@ -350,14 +350,14 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
|
||||
{/* Visibility */}
|
||||
<div>
|
||||
<label htmlFor="visibility" className="mb-2 block text-sm font-medium">
|
||||
<label htmlFor="visibility" className="mb-2 block text-sm font-medium text-white">
|
||||
Visibility
|
||||
</label>
|
||||
<select
|
||||
id="visibility"
|
||||
value={visibility}
|
||||
onChange={(e) => setVisibility(e.target.value as 'private' | 'team' | 'public')}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
>
|
||||
<option value="private">Private (only me)</option>
|
||||
<option value="team">Team (my team members)</option>
|
||||
@@ -370,13 +370,13 @@ export function StepForm({ onSubmit, onCancel, initialData }: StepFormProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent"
|
||||
className="flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
Insert Step
|
||||
</button>
|
||||
|
||||
@@ -132,16 +132,16 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header - Filters */}
|
||||
<div className="space-y-4 border-b border-border p-4">
|
||||
<div className="space-y-4 border-b border-white/[0.06] p-4">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search steps..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background py-2 pl-10 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="w-full rounded-md border border-white/10 bg-black/50 py-2 pl-10 pr-4 text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +152,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Filter by category"
|
||||
value={selectedCategoryId || ''}
|
||||
onChange={(e) => setSelectedCategoryId(e.target.value || undefined)}
|
||||
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map(cat => (
|
||||
@@ -165,7 +165,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Filter by step type"
|
||||
value={selectedStepType || ''}
|
||||
onChange={(e) => setSelectedStepType((e.target.value as 'decision' | 'action' | 'solution') || undefined)}
|
||||
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="decision">Decision</option>
|
||||
@@ -178,7 +178,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Filter by minimum rating"
|
||||
value={minRating?.toString() || ''}
|
||||
onChange={(e) => setMinRating(e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
>
|
||||
<option value="">Any Rating</option>
|
||||
<option value="4">4+ Stars</option>
|
||||
@@ -191,7 +191,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Sort steps by"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as 'recent' | 'popular' | 'highest_rated' | 'most_used')}
|
||||
className="rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white focus:outline-none focus:border-white/30 focus:ring-1 focus:ring-white/20"
|
||||
>
|
||||
<option value="recent">Most Recent</option>
|
||||
<option value="popular">Most Popular</option>
|
||||
@@ -203,7 +203,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
{/* Popular Tags */}
|
||||
{popularTags.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-medium text-muted-foreground">Popular Tags:</div>
|
||||
<div className="mb-2 text-xs font-medium text-white/40">Popular Tags:</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{popularTags.map(tag => (
|
||||
<button
|
||||
@@ -212,8 +212,8 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
className={cn(
|
||||
'rounded-full px-2.5 py-1 text-xs transition-colors',
|
||||
selectedTag === tag.tag
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
)}
|
||||
>
|
||||
{tag.tag} ({tag.count})
|
||||
@@ -227,7 +227,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-sm text-primary hover:underline"
|
||||
className="text-sm text-white/70 hover:text-white hover:underline"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
@@ -238,16 +238,16 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<Loader2 className="h-8 w-8 animate-spin text-white/40" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-center text-sm text-destructive">
|
||||
<div className="rounded-lg border border-red-400/20 bg-red-400/10 p-4 text-center text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
) : steps.length === 0 ? (
|
||||
<div className="rounded-lg border border-border bg-muted/30 p-12 text-center">
|
||||
<p className="mb-2 text-lg font-medium">No steps found</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="rounded-lg border border-white/[0.06] bg-white/5 p-12 text-center">
|
||||
<p className="mb-2 text-lg font-medium text-white">No steps found</p>
|
||||
<p className="text-sm text-white/40">
|
||||
{hasActiveFilters ? 'Try adjusting your filters' : 'Create your first step to get started!'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -260,7 +260,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
onClick={() => toggleSection('private')}
|
||||
className="mb-3 flex w-full items-center justify-between"
|
||||
>
|
||||
<h3 className="text-sm font-semibold">My Steps ({groupedSteps.private.length})</h3>
|
||||
<h3 className="text-sm font-semibold text-white">My Steps ({groupedSteps.private.length})</h3>
|
||||
{collapsedSections.private ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -289,7 +289,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
onClick={() => toggleSection('team')}
|
||||
className="mb-3 flex w-full items-center justify-between"
|
||||
>
|
||||
<h3 className="text-sm font-semibold">Team Steps ({groupedSteps.team.length})</h3>
|
||||
<h3 className="text-sm font-semibold text-white">Team Steps ({groupedSteps.team.length})</h3>
|
||||
{collapsedSections.team ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -318,7 +318,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
onClick={() => toggleSection('public')}
|
||||
className="mb-3 flex w-full items-center justify-between"
|
||||
>
|
||||
<h3 className="text-sm font-semibold">Community ({groupedSteps.public.length})</h3>
|
||||
<h3 className="text-sm font-semibold text-white">Community ({groupedSteps.public.length})</h3>
|
||||
{collapsedSections.public ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -345,10 +345,10 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
|
||||
{/* Footer - Optional Create Button */}
|
||||
{showCreateButton && onCreateNew && (
|
||||
<div className="border-t border-border p-4">
|
||||
<div className="border-t border-white/[0.06] p-4">
|
||||
<button
|
||||
onClick={onCreateNew}
|
||||
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="w-full rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
+ Create New Step
|
||||
</button>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function CheckoutButton({ plan, className }: CheckoutButtonProps) {
|
||||
disabled
|
||||
title="Billing coming soon"
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function DynamicArrayField<T>({
|
||||
type="button"
|
||||
onClick={() => handleMoveUp(index)}
|
||||
disabled={index === 0}
|
||||
className="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-30"
|
||||
className="rounded p-0.5 text-white/50 hover:bg-white/[0.06] hover:text-white disabled:opacity-30"
|
||||
title="Move up"
|
||||
>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
@@ -60,7 +60,7 @@ export function DynamicArrayField<T>({
|
||||
type="button"
|
||||
onClick={() => handleMoveDown(index)}
|
||||
disabled={index === items.length - 1}
|
||||
className="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-30"
|
||||
className="rounded p-0.5 text-white/50 hover:bg-white/[0.06] hover:text-white disabled:opacity-30"
|
||||
title="Move down"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
@@ -76,7 +76,7 @@ export function DynamicArrayField<T>({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove(index)}
|
||||
className="mt-1 rounded p-1 text-muted-foreground hover:bg-destructive/20 hover:text-destructive"
|
||||
className="mt-1 rounded p-1 text-white/50 hover:bg-red-400/20 hover:text-red-400"
|
||||
title="Remove"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -91,9 +91,9 @@ export function DynamicArrayField<T>({
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-input',
|
||||
'px-3 py-2 text-sm text-muted-foreground',
|
||||
'hover:border-primary hover:text-primary'
|
||||
'flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-white/10',
|
||||
'px-3 py-2 text-sm text-white/50',
|
||||
'hover:border-white/30 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -103,7 +103,7 @@ export function DynamicArrayField<T>({
|
||||
|
||||
{/* Empty state */}
|
||||
{items.length === 0 && !canAdd && (
|
||||
<p className="text-center text-sm text-muted-foreground">No items</p>
|
||||
<p className="text-center text-sm text-white/40">No items</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -68,14 +68,14 @@ export function NodeEditorModal({ node, onClose, isNewNode = false }: NodeEditor
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
@@ -85,8 +85,8 @@ export function NodeEditorModal({ node, onClose, isNewNode = false }: NodeEditor
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose} title={getTitle()} size="lg" footer={footerContent}>
|
||||
{/* Node ID display */}
|
||||
<div className="mb-4 text-xs text-muted-foreground">
|
||||
Node ID: <code className="rounded bg-muted px-1 py-0.5">{node.id}</code>
|
||||
<div className="mb-4 text-xs text-white/40">
|
||||
Node ID: <code className="rounded bg-white/10 px-1 py-0.5">{node.id}</code>
|
||||
</div>
|
||||
|
||||
{/* Validation errors */}
|
||||
@@ -97,8 +97,8 @@ export function NodeEditorModal({ node, onClose, isNewNode = false }: NodeEditor
|
||||
key={i}
|
||||
className={`rounded-md px-3 py-2 text-sm ${
|
||||
error.severity === 'error'
|
||||
? 'bg-destructive/10 text-destructive'
|
||||
: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400'
|
||||
? 'bg-red-400/10 text-red-400'
|
||||
: 'bg-yellow-400/10 text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{error.message}
|
||||
|
||||
@@ -52,8 +52,8 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
<div className="space-y-4">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
Title <span className="text-destructive">*</span>
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Title <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -62,37 +62,37 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
placeholder="e.g., Restart the Service"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
titleError ? 'border-destructive' : 'border-input'
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
titleError ? 'border-red-400' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{titleError && (
|
||||
<p className="mt-1 text-xs text-destructive">{titleError.message}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{titleError.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Description
|
||||
</label>
|
||||
{node.description && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
className="text-xs text-white/50 hover:text-white hover:underline"
|
||||
>
|
||||
{showPreview ? 'Edit' : 'Preview'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-1 text-xs text-muted-foreground">
|
||||
<p className="mb-1 text-xs text-white/40">
|
||||
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||||
</p>
|
||||
{showPreview && node.description ? (
|
||||
<div className="mt-1 rounded-md border border-input bg-muted/50 p-3 text-sm">
|
||||
<div className="mt-1 rounded-md border border-white/10 bg-white/[0.04] p-3 text-sm">
|
||||
<MarkdownContent content={node.description} />
|
||||
</div>
|
||||
) : (
|
||||
@@ -108,7 +108,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
**Note:** Important information here"
|
||||
rows={5}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'mt-1 block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
@@ -118,10 +118,10 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
|
||||
{/* Commands */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Commands
|
||||
</label>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
<p className="mb-2 text-xs text-white/40">
|
||||
PowerShell or CLI commands to execute
|
||||
</p>
|
||||
<DynamicArrayField
|
||||
@@ -137,7 +137,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
onChange={(e) => handleUpdateCommand(index, e.target.value)}
|
||||
placeholder="e.g., Get-Service BrokerAgent"
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-input px-3 py-2 font-mono text-sm',
|
||||
'block w-full rounded-md border border-white/10 px-3 py-2 font-mono text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
@@ -148,7 +148,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
|
||||
{/* Expected Outcome */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Expected Outcome
|
||||
</label>
|
||||
<input
|
||||
@@ -157,7 +157,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
onChange={(e) => onUpdate({ expected_outcome: e.target.value })}
|
||||
placeholder="e.g., Service should show as Running"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'mt-1 block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
|
||||
@@ -64,10 +64,10 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
<Play className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-blue-600 dark:text-blue-400">
|
||||
<h3 className="font-semibold text-blue-400">
|
||||
Starting Question
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
This is the first question users will see when they start this troubleshooting tree.
|
||||
Each option below creates a different troubleshooting path.
|
||||
</p>
|
||||
@@ -78,11 +78,11 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
|
||||
{/* Question */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
{isRootNode ? 'Starting Question' : 'Question'} <span className="text-destructive">*</span>
|
||||
<label className="block text-sm font-medium text-white">
|
||||
{isRootNode ? 'Starting Question' : 'Question'} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
{isRootNode && (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
<p className="mt-0.5 text-xs text-white/40">
|
||||
What's the main question to diagnose the issue?
|
||||
</p>
|
||||
)}
|
||||
@@ -95,19 +95,19 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
: "e.g., Can you ping the server?"}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
questionError ? 'border-destructive' : 'border-input'
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
questionError ? 'border-red-400' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{questionError && (
|
||||
<p className="mt-1 text-xs text-destructive">{questionError.message}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{questionError.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Help Text
|
||||
</label>
|
||||
<textarea
|
||||
@@ -116,7 +116,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
placeholder="Additional context or instructions for this decision..."
|
||||
rows={2}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'mt-1 block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
@@ -125,20 +125,20 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
|
||||
{/* Options */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
{isRootNode ? 'Answer Options (Branches)' : 'Options'} <span className="text-destructive">*</span>
|
||||
<label className="block text-sm font-medium text-white">
|
||||
{isRootNode ? 'Answer Options (Branches)' : 'Options'} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
{isRootNode ? (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
<p className="mt-0.5 text-xs text-white/40">
|
||||
Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
<p className="mt-0.5 text-xs text-white/40">
|
||||
Each option can branch to a different next step.
|
||||
</p>
|
||||
)}
|
||||
{optionsError && (
|
||||
<p className="mt-1 text-xs text-destructive">{optionsError.message}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{optionsError.message}</p>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<DynamicArrayField
|
||||
@@ -158,14 +158,14 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
const letter = indexToLetter(index)
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-input bg-muted/30 p-3">
|
||||
<div className="rounded-md border border-white/10 bg-white/[0.04] p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
{/* Letter badge */}
|
||||
<span className={cn(
|
||||
'flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold',
|
||||
isRootNode
|
||||
? 'bg-blue-500/20 text-blue-600 dark:text-blue-400'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
? 'bg-blue-500/20 text-blue-400'
|
||||
: 'bg-white/10 text-white/50'
|
||||
)}>
|
||||
{letter}
|
||||
</span>
|
||||
@@ -180,12 +180,12 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
'block flex-1 rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
optionLabelError ? 'border-destructive' : 'border-input'
|
||||
optionLabelError ? 'border-red-400' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{optionLabelError && (
|
||||
<p className="mb-2 text-xs text-destructive">{optionLabelError.message}</p>
|
||||
<p className="mb-2 text-xs text-red-400">{optionLabelError.message}</p>
|
||||
)}
|
||||
<div className="pl-8">
|
||||
<NodePicker
|
||||
@@ -207,7 +207,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
|
||||
{/* Example hint for root node */}
|
||||
{isRootNode && (node.options?.length || 0) < 2 && (
|
||||
<div className="mt-3 rounded-md border border-dashed border-muted-foreground/30 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||||
<div className="mt-3 rounded-md border border-dashed border-white/10 bg-white/[0.02] p-3 text-xs text-white/40">
|
||||
<strong>Tip:</strong> Most troubleshooting trees start with 2-5 main branches.
|
||||
For example: "Connection Issues", "Performance Problems", "Error Messages", "Other".
|
||||
</div>
|
||||
|
||||
@@ -47,8 +47,8 @@ export function NodeFormResolution({ node, onUpdate }: NodeFormResolutionProps)
|
||||
<div className="space-y-4">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
Title <span className="text-destructive">*</span>
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Title <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -57,37 +57,37 @@ export function NodeFormResolution({ node, onUpdate }: NodeFormResolutionProps)
|
||||
placeholder="e.g., VDA Successfully Registered"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
titleError ? 'border-destructive' : 'border-input'
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
titleError ? 'border-red-400' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{titleError && (
|
||||
<p className="mt-1 text-xs text-destructive">{titleError.message}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{titleError.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Description
|
||||
</label>
|
||||
{node.description && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
className="text-xs text-white/50 hover:text-white hover:underline"
|
||||
>
|
||||
{showPreview ? 'Edit' : 'Preview'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-1 text-xs text-muted-foreground">
|
||||
<p className="mb-1 text-xs text-white/40">
|
||||
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||||
</p>
|
||||
{showPreview && node.description ? (
|
||||
<div className="mt-1 rounded-md border border-input bg-muted/50 p-3 text-sm">
|
||||
<div className="mt-1 rounded-md border border-white/10 bg-white/[0.04] p-3 text-sm">
|
||||
<MarkdownContent content={node.description} />
|
||||
</div>
|
||||
) : (
|
||||
@@ -102,7 +102,7 @@ Document what was done and the outcome.
|
||||
**Close ticket as:** Resolved"
|
||||
rows={5}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'mt-1 block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
@@ -112,10 +112,10 @@ Document what was done and the outcome.
|
||||
|
||||
{/* Resolution Steps */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Resolution Steps
|
||||
</label>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
<p className="mb-2 text-xs text-white/40">
|
||||
Step-by-step instructions for resolving the issue
|
||||
</p>
|
||||
<DynamicArrayField
|
||||
@@ -126,7 +126,7 @@ Document what was done and the outcome.
|
||||
addLabel="Add Step"
|
||||
renderItem={(step, index) => (
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="mt-2 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
|
||||
<span className="mt-2 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-400/10 text-xs font-medium text-emerald-400">
|
||||
{index + 1}
|
||||
</span>
|
||||
<input
|
||||
@@ -135,7 +135,7 @@ Document what was done and the outcome.
|
||||
onChange={(e) => handleUpdateStep(index, e.target.value)}
|
||||
placeholder={`Step ${index + 1}`}
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
@@ -146,7 +146,7 @@ Document what was done and the outcome.
|
||||
</div>
|
||||
|
||||
{/* Note about terminal node */}
|
||||
<div className="rounded-md bg-green-500/10 p-3 text-sm text-green-600 dark:text-green-400">
|
||||
<div className="rounded-md bg-emerald-400/10 p-3 text-sm text-emerald-400">
|
||||
<strong>Note:</strong> Solution nodes are terminal - they end the troubleshooting flow.
|
||||
The session will be marked complete when the user reaches this node.
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,7 @@ export function NodePicker({
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">
|
||||
<label className="mb-1 block text-sm font-medium text-white">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
@@ -147,8 +147,8 @@ export function NodePicker({
|
||||
{/* Inline node creation UI */}
|
||||
{creatingNodeType ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 rounded-md border border-primary bg-primary/5 p-2">
|
||||
<span className="text-xs font-medium text-primary">
|
||||
<div className="flex items-center gap-2 rounded-md border border-white/20 bg-white/[0.04] p-2">
|
||||
<span className="text-xs font-medium text-white">
|
||||
New {NODE_TYPE_LABELS[creatingNodeType]}:
|
||||
</span>
|
||||
<input
|
||||
@@ -159,9 +159,9 @@ export function NodePicker({
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={creatingNodeType === 'decision' ? 'Enter question...' : 'Enter title...'}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input px-2 py-1 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 px-2 py-1 text-sm',
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ export function NodePicker({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelCreate}
|
||||
className="flex-1 rounded-md border border-input px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
className="flex-1 rounded-md border border-white/10 px-3 py-1.5 text-xs font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -179,7 +179,7 @@ export function NodePicker({
|
||||
disabled={!newNodeTitle.trim()}
|
||||
className={cn(
|
||||
'flex-1 rounded-md px-3 py-1.5 text-xs font-medium',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
@@ -194,9 +194,9 @@ export function NodePicker({
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
className={cn(
|
||||
'block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
error ? 'border-destructive' : 'border-input'
|
||||
'bg-black/50 text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
error ? 'border-red-400' : 'border-white/10'
|
||||
)}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
@@ -242,14 +242,14 @@ export function NodePicker({
|
||||
|
||||
{/* Show what's selected */}
|
||||
{value && selectedNode && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
→ {selectedNode.label}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
||||
{error && <p className="mt-1 text-xs text-red-400">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
|
||||
{/* Left Panel - Form Editor */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col overflow-y-auto border-border bg-background',
|
||||
'flex flex-col overflow-y-auto border-white/[0.06]',
|
||||
isMobile ? 'h-full w-full border-b' : 'w-3/5 border-r'
|
||||
)}
|
||||
>
|
||||
@@ -31,7 +31,7 @@ export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
|
||||
{/* Right Panel - Preview */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 overflow-hidden bg-muted/30',
|
||||
'flex-1 overflow-hidden bg-white/[0.02]',
|
||||
isMobile ? 'hidden' : 'block'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -56,13 +56,13 @@ export function TreeMetadataForm() {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
|
||||
<h2 className="text-sm font-semibold text-card-foreground">Tree Details</h2>
|
||||
<div className="space-y-4 glass-card rounded-2xl p-4">
|
||||
<h2 className="text-sm font-semibold text-white">Tree Details</h2>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="tree-name" className="block text-sm font-medium text-foreground">
|
||||
Name <span className="text-destructive">*</span>
|
||||
<label htmlFor="tree-name" className="block text-sm font-medium text-white">
|
||||
Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="tree-name"
|
||||
@@ -72,17 +72,17 @@ export function TreeMetadataForm() {
|
||||
placeholder="e.g., VDA Registration Troubleshooting"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
nameError ? 'border-destructive' : 'border-input'
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
nameError ? 'border-red-400' : 'border-white/10'
|
||||
)}
|
||||
/>
|
||||
{nameError && <p className="mt-1 text-xs text-destructive">{nameError.message}</p>}
|
||||
{nameError && <p className="mt-1 text-xs text-red-400">{nameError.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label htmlFor="tree-description" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="tree-description" className="block text-sm font-medium text-white">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
@@ -92,16 +92,16 @@ export function TreeMetadataForm() {
|
||||
placeholder="Brief description of what this tree troubleshoots..."
|
||||
rows={2}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label htmlFor="tree-category" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="tree-category" className="block text-sm font-medium text-white">
|
||||
Category
|
||||
</label>
|
||||
{!customCategory ? (
|
||||
@@ -110,9 +110,9 @@ export function TreeMetadataForm() {
|
||||
value={categoryId || ''}
|
||||
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
|
||||
'bg-background text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-black/50 text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="">No category</option>
|
||||
@@ -132,9 +132,9 @@ export function TreeMetadataForm() {
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
placeholder="Enter new category"
|
||||
className={cn(
|
||||
'block flex-1 rounded-md border border-input px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'block flex-1 rounded-md border border-white/10 px-3 py-2 text-sm',
|
||||
'bg-black/50 text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
@@ -144,7 +144,7 @@ export function TreeMetadataForm() {
|
||||
setCategory('')
|
||||
setCategoryId(null)
|
||||
}}
|
||||
className="rounded-md border border-input px-3 py-2 text-sm hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-3 py-2 text-sm text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -154,7 +154,7 @@ export function TreeMetadataForm() {
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">Tags</label>
|
||||
<label className="block text-sm font-medium text-white">Tags</label>
|
||||
<div className="mt-1">
|
||||
<TagInput tags={tags} onChange={setTags} maxTags={10} placeholder="Add tags..." />
|
||||
</div>
|
||||
@@ -162,13 +162,13 @@ export function TreeMetadataForm() {
|
||||
|
||||
{/* Visibility */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">Visibility</label>
|
||||
<label className="block text-sm font-medium text-white">Visibility</label>
|
||||
<div className="mt-2 flex gap-4">
|
||||
<label
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md border px-4 py-2',
|
||||
'transition-colors',
|
||||
!isPublic ? 'border-primary bg-primary/10' : 'border-input hover:bg-accent'
|
||||
!isPublic ? 'border-white/30 bg-white/10 text-white' : 'border-white/10 text-white/60 hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -185,7 +185,7 @@ export function TreeMetadataForm() {
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md border px-4 py-2',
|
||||
'transition-colors',
|
||||
isPublic ? 'border-primary bg-primary/10' : 'border-input hover:bg-accent'
|
||||
isPublic ? 'border-white/30 bg-white/10 text-white' : 'border-white/10 text-white/60 hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -199,7 +199,7 @@ export function TreeMetadataForm() {
|
||||
<span className="text-sm">Public</span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
{isPublic
|
||||
? 'Anyone can view this tree'
|
||||
: 'Only you and your team can view this tree'}
|
||||
|
||||
@@ -27,16 +27,16 @@ export function ValidationSummary({ errors, onSelectNode }: ValidationSummaryPro
|
||||
className={cn(
|
||||
'rounded-lg border',
|
||||
errorItems.length > 0
|
||||
? 'border-destructive/50 bg-destructive/5'
|
||||
: 'border-yellow-500/50 bg-yellow-50 dark:bg-yellow-900/10'
|
||||
? 'border-red-400/30 bg-red-400/5'
|
||||
: 'border-yellow-400/30 bg-yellow-400/5'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between p-3 text-left transition-colors hover:bg-black/5 dark:hover:bg-white/5',
|
||||
errorItems.length > 0 ? 'text-destructive' : 'text-yellow-700 dark:text-yellow-500'
|
||||
'flex w-full items-center justify-between p-3 text-left transition-colors hover:bg-white/5',
|
||||
errorItems.length > 0 ? 'text-red-400' : 'text-yellow-400'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -73,15 +73,15 @@ export function ValidationSummary({ errors, onSelectNode }: ValidationSummaryPro
|
||||
className={cn(
|
||||
'flex w-full items-start gap-2 rounded p-2 text-left text-sm transition-colors',
|
||||
error.nodeId
|
||||
? 'cursor-pointer hover:bg-destructive/10'
|
||||
? 'cursor-pointer hover:bg-red-400/10'
|
||||
: 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-destructive" />
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-red-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-destructive">{error.message}</p>
|
||||
<p className="text-red-400">{error.message}</p>
|
||||
{error.nodeId && (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
<p className="mt-0.5 text-xs text-white/40">
|
||||
Click to select node: {error.nodeId}
|
||||
</p>
|
||||
)}
|
||||
@@ -97,15 +97,15 @@ export function ValidationSummary({ errors, onSelectNode }: ValidationSummaryPro
|
||||
className={cn(
|
||||
'flex w-full items-start gap-2 rounded p-2 text-left text-sm transition-colors',
|
||||
warning.nodeId
|
||||
? 'cursor-pointer hover:bg-yellow-100 dark:hover:bg-yellow-900/20'
|
||||
? 'cursor-pointer hover:bg-yellow-400/10'
|
||||
: 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-600 dark:text-yellow-500" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-yellow-700 dark:text-yellow-500">{warning.message}</p>
|
||||
<p className="text-yellow-400">{warning.message}</p>
|
||||
{warning.nodeId && (
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
<p className="mt-0.5 text-xs text-white/40">
|
||||
Click to select node: {warning.nodeId}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -154,8 +154,8 @@ export function TreePreviewNode({
|
||||
<div className="relative">
|
||||
{/* From option label */}
|
||||
{fromOption && (
|
||||
<div className="mb-1 text-xs font-medium text-muted-foreground">
|
||||
<span className="rounded bg-muted px-1.5 py-0.5">{fromOption}</span>
|
||||
<div className="mb-1 text-xs font-medium text-white/40">
|
||||
<span className="rounded bg-white/10 px-1.5 py-0.5">{fromOption}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function TreePreviewNode({
|
||||
<div className="rounded-full bg-blue-500/30 p-1.5">
|
||||
<Play className="h-4 w-4 text-blue-500" />
|
||||
</div>
|
||||
<span className="text-xs font-bold uppercase tracking-wide text-blue-600 dark:text-blue-400">
|
||||
<span className="text-xs font-bold uppercase tracking-wide text-blue-400">
|
||||
Starting Question
|
||||
</span>
|
||||
</div>
|
||||
@@ -206,12 +206,12 @@ export function TreePreviewNode({
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCollapse}
|
||||
className="mt-0.5 rounded p-0.5 hover:bg-muted"
|
||||
className="mt-0.5 rounded p-0.5 hover:bg-white/10"
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
<ChevronRight className="h-4 w-4 text-white/50" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
<ChevronDown className="h-4 w-4 text-white/50" />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
@@ -222,14 +222,14 @@ export function TreePreviewNode({
|
||||
{isRootNode && <HelpCircle className="h-4 w-4 text-blue-500" />}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground leading-tight">
|
||||
<p className="text-sm font-medium text-white leading-tight">
|
||||
{getNodeLabel()}
|
||||
</p>
|
||||
|
||||
{/* Node ID with copy button */}
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<span
|
||||
className="text-xs text-muted-foreground cursor-help"
|
||||
className="text-xs text-white/40 cursor-help"
|
||||
title={`Full ID: ${node.id}`}
|
||||
>
|
||||
{node.id === 'root' ? 'root' : node.id.slice(0, 8) + '...'}
|
||||
@@ -237,7 +237,7 @@ export function TreePreviewNode({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyId}
|
||||
className="rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
className="rounded p-0.5 text-white/40 hover:bg-white/10 hover:text-white"
|
||||
title="Copy full ID"
|
||||
>
|
||||
{copiedId ? (
|
||||
@@ -252,8 +252,8 @@ export function TreePreviewNode({
|
||||
|
||||
{/* Show options for decision nodes */}
|
||||
{node.type === 'decision' && node.options && node.options.length > 0 && (
|
||||
<div className="mt-2 space-y-1 border-t border-border/50 pt-2">
|
||||
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">Options:</p>
|
||||
<div className="mt-2 space-y-1 border-t border-white/[0.06] pt-2">
|
||||
<p className="text-[10px] uppercase tracking-wide text-white/40">Options:</p>
|
||||
{node.options.map((opt, i) => {
|
||||
const leadsToSolution = nodeLeadsToSolution(opt.next_node_id)
|
||||
return (
|
||||
@@ -261,15 +261,15 @@ export function TreePreviewNode({
|
||||
key={opt.id}
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-xs rounded px-1 py-0.5 -mx-1',
|
||||
opt.next_node_id && 'hover:bg-muted cursor-pointer'
|
||||
opt.next_node_id && 'hover:bg-white/[0.06] cursor-pointer'
|
||||
)}
|
||||
onMouseEnter={() => opt.next_node_id && onHoverNodeId?.(opt.next_node_id)}
|
||||
onMouseLeave={() => onHoverNodeId?.(null)}
|
||||
>
|
||||
<span className="inline-flex h-4 w-4 items-center justify-center rounded bg-muted text-[10px] font-medium">
|
||||
<span className="inline-flex h-4 w-4 items-center justify-center rounded bg-white/10 text-[10px] font-medium text-white/50">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="truncate text-foreground">{opt.label || 'Untitled'}</span>
|
||||
<span className="truncate text-white/70">{opt.label || 'Untitled'}</span>
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
{leadsToSolution && (
|
||||
<span title="Leads to solution">
|
||||
@@ -279,7 +279,7 @@ export function TreePreviewNode({
|
||||
{opt.next_node_id ? (
|
||||
<span className="text-blue-500">→</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground/50 text-[10px]">(no link)</span>
|
||||
<span className="text-white/30 text-[10px]">(no link)</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -308,12 +308,12 @@ export function TreePreviewNode({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mt-2 text-xs border-t border-border/50 pt-2 hover:bg-muted/50 cursor-pointer rounded px-1 -mx-1"
|
||||
className="mt-2 text-xs border-t border-white/[0.06] pt-2 hover:bg-white/[0.04] cursor-pointer rounded px-1 -mx-1"
|
||||
onMouseEnter={() => onHoverNodeId?.(node.next_node_id!)}
|
||||
onMouseLeave={() => onHoverNodeId?.(null)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-muted-foreground">Next:</span>
|
||||
<span className="text-white/40">Next:</span>
|
||||
{isSharedTarget && (
|
||||
<span title={sharedTooltip} className="flex items-center">
|
||||
<Users className="h-3 w-3 text-purple-500" />
|
||||
@@ -321,7 +321,7 @@ export function TreePreviewNode({
|
||||
)}
|
||||
<span className={cn(
|
||||
'truncate',
|
||||
nextNode?.type === 'solution' ? 'text-green-500 font-medium' : 'text-foreground'
|
||||
nextNode?.type === 'solution' ? 'text-green-500 font-medium' : 'text-white/70'
|
||||
)}>
|
||||
{nextNodeLabel.slice(0, 30)}{nextNodeLabel.length > 30 ? '...' : ''}
|
||||
</span>
|
||||
@@ -347,7 +347,7 @@ export function TreePreviewNode({
|
||||
|
||||
{/* Children - show as branches */}
|
||||
{hasChildren && !isCollapsed && (
|
||||
<div className="relative mt-3 ml-6 pl-6 border-l-2 border-border">
|
||||
<div className="relative mt-3 ml-6 pl-6 border-l-2 border-white/[0.06]">
|
||||
<div className="space-y-4">
|
||||
{node.children!.map((child) => {
|
||||
const optionLabel = getOptionLabelForChild(child.id)
|
||||
@@ -355,7 +355,7 @@ export function TreePreviewNode({
|
||||
return (
|
||||
<div key={child.id} className="relative">
|
||||
{/* Horizontal connector line */}
|
||||
<div className="absolute -left-6 top-6 h-0.5 w-6 bg-border" />
|
||||
<div className="absolute -left-6 top-6 h-0.5 w-6 bg-white/[0.06]" />
|
||||
|
||||
<TreePreviewNode
|
||||
node={child}
|
||||
@@ -377,8 +377,8 @@ export function TreePreviewNode({
|
||||
|
||||
{/* Show collapsed indicator */}
|
||||
{hasChildren && isCollapsed && (
|
||||
<div className="mt-2 ml-6 text-xs text-muted-foreground">
|
||||
<span className="rounded bg-muted px-2 py-1">
|
||||
<div className="mt-2 ml-6 text-xs text-white/40">
|
||||
<span className="rounded bg-white/10 px-2 py-1">
|
||||
{node.children!.length} child node{node.children!.length !== 1 ? 's' : ''} hidden
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function TreePreviewPanel() {
|
||||
|
||||
if (!treeStructure) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
|
||||
<div className="flex h-full items-center justify-center p-4 text-sm text-white/40">
|
||||
No tree structure to preview
|
||||
</div>
|
||||
)
|
||||
@@ -65,11 +65,11 @@ export function TreePreviewPanel() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border bg-background px-4 py-2">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
<div className="border-b border-white/[0.06] px-4 py-2">
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
Preview: {name || 'Untitled Tree'}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-white/40">
|
||||
Click a node to select • Hover options to highlight targets
|
||||
</p>
|
||||
</div>
|
||||
@@ -91,8 +91,8 @@ export function TreePreviewPanel() {
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="border-t border-border bg-background px-4 py-2">
|
||||
<div className="flex flex-wrap gap-4 text-xs text-muted-foreground">
|
||||
<div className="border-t border-white/[0.06] px-4 py-2">
|
||||
<div className="flex flex-wrap gap-4 text-xs text-white/40">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-3 w-3 rounded bg-blue-500/50" />
|
||||
<span>Decision</span>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||||
),
|
||||
// Style bold text
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-foreground">{children}</strong>
|
||||
<strong className="font-semibold text-white">{children}</strong>
|
||||
),
|
||||
// Style ordered lists
|
||||
ol: ({ children }) => (
|
||||
@@ -33,7 +33,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||||
),
|
||||
// Style list items
|
||||
li: ({ children }) => (
|
||||
<li className="text-muted-foreground">{children}</li>
|
||||
<li className="text-white/60">{children}</li>
|
||||
),
|
||||
// Style inline code
|
||||
code: ({ className, children, ...props }) => {
|
||||
@@ -43,7 +43,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
'block rounded bg-muted p-3 font-mono text-sm overflow-x-auto',
|
||||
'block rounded bg-white/10 p-3 font-mono text-sm overflow-x-auto',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -54,7 +54,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||||
}
|
||||
return (
|
||||
<code
|
||||
className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm"
|
||||
className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-sm"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -63,25 +63,25 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||||
},
|
||||
// Style code blocks (pre)
|
||||
pre: ({ children }) => (
|
||||
<pre className="mb-3 overflow-x-auto rounded bg-muted p-0 last:mb-0">
|
||||
<pre className="mb-3 overflow-x-auto rounded bg-white/10 p-0 last:mb-0">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
// Style headers
|
||||
h1: ({ children }) => (
|
||||
<h1 className="mb-3 text-lg font-bold text-foreground">{children}</h1>
|
||||
<h1 className="mb-3 text-lg font-bold text-white">{children}</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="mb-2 text-base font-bold text-foreground">{children}</h2>
|
||||
<h2 className="mb-2 text-base font-bold text-white">{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="mb-2 text-sm font-bold text-foreground">{children}</h3>
|
||||
<h3 className="mb-2 text-sm font-bold text-white">{children}</h3>
|
||||
),
|
||||
// Style horizontal rules
|
||||
hr: () => <hr className="my-4 border-border" />,
|
||||
hr: () => <hr className="my-4 border-white/[0.06]" />,
|
||||
// Style blockquotes
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="mb-3 border-l-4 border-primary/50 pl-4 italic text-muted-foreground last:mb-0">
|
||||
<blockquote className="mb-3 border-l-4 border-white/20 pl-4 italic text-white/50 last:mb-0">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
@@ -4,50 +4,27 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Light mode (fallback) */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 243 75% 59%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 243 75% 59%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* ResolutionFlow Dark Theme */
|
||||
--background: 240 10% 3.9%;
|
||||
/* Monochrome Design System — Dark Only */
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 100%;
|
||||
--card: 240 10% 9.4%;
|
||||
--card: 0 0% 4%;
|
||||
--card-foreground: 0 0% 100%;
|
||||
--popover: 240 10% 9.4%;
|
||||
--popover: 0 0% 4%;
|
||||
--popover-foreground: 0 0% 100%;
|
||||
--primary: 243 75% 59%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 240 5.9% 15%;
|
||||
--primary: 0 0% 100%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--secondary: 0 0% 10%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
--muted: 240 5.9% 15%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 5.9% 15%;
|
||||
--muted: 0 0% 10%;
|
||||
--muted-foreground: 0 0% 50%;
|
||||
--accent: 0 0% 8%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 240 5.9% 15%;
|
||||
--input: 240 5.9% 15%;
|
||||
--ring: 243 75% 59%;
|
||||
--border: 0 0% 12%;
|
||||
--input: 0 0% 12%;
|
||||
--ring: 0 0% 100%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +55,7 @@
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
@@ -111,10 +88,6 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-gradient-brand {
|
||||
@apply bg-gradient-brand bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 200ms ease-out;
|
||||
}
|
||||
@@ -135,45 +108,67 @@
|
||||
animation: scale-in 150ms ease-out;
|
||||
}
|
||||
|
||||
/* Button press feedback for primary action buttons */
|
||||
/* Button press feedback */
|
||||
.btn-press {
|
||||
@apply active:scale-[0.98] transition-transform;
|
||||
}
|
||||
|
||||
/* Glass card effect */
|
||||
.glass-card {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.glass-card-hover {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Active/highlighted card glow */
|
||||
.glass-card-glow {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 40px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Stat card */
|
||||
.glass-stat {
|
||||
background: rgba(20, 20, 25, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sonner Toast Customization - ResolutionFlow Design System */
|
||||
/* Sonner Toast Customization */
|
||||
@layer components {
|
||||
/* Base toast styling matching Modal/Card components */
|
||||
:where([data-sonner-toast]) {
|
||||
@apply bg-card text-card-foreground;
|
||||
@apply border border-border shadow-lg;
|
||||
@apply rounded-lg;
|
||||
@apply rounded-xl;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Toast title using heading font */
|
||||
:where([data-sonner-toast]) [data-title] {
|
||||
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Success toast - uses primary brand color */
|
||||
:where([data-sonner-toast][data-type="success"]) {
|
||||
@apply border-primary/30;
|
||||
border-color: rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
:where([data-sonner-toast][data-type="success"]) [data-icon] {
|
||||
@apply text-primary;
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
/* Error toast - uses destructive color */
|
||||
:where([data-sonner-toast][data-type="error"]) {
|
||||
@apply border-destructive/30;
|
||||
border-color: rgba(248, 113, 113, 0.3);
|
||||
}
|
||||
:where([data-sonner-toast][data-type="error"]) [data-icon] {
|
||||
@apply text-destructive;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* Info toast - uses muted color */
|
||||
:where([data-sonner-toast][data-type="info"]) {
|
||||
@apply border-border;
|
||||
}
|
||||
@@ -181,26 +176,23 @@
|
||||
@apply text-muted-foreground;
|
||||
}
|
||||
|
||||
/* Warning toast - uses amber color */
|
||||
:where([data-sonner-toast][data-type="warning"]) {
|
||||
border-color: hsl(38 92% 50% / 0.3);
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
:where([data-sonner-toast][data-type="warning"]) [data-icon] {
|
||||
color: hsl(38 92% 50%);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* Close button matching Modal close button */
|
||||
:where([data-sonner-toast]) [data-close-button] {
|
||||
@apply text-muted-foreground hover:bg-accent hover:text-accent-foreground;
|
||||
@apply rounded-md transition-colors;
|
||||
}
|
||||
|
||||
/* Loading spinner uses primary color */
|
||||
:where([data-sonner-toast]) [data-icon][data-loading] {
|
||||
@apply text-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
/* React Day Picker Customization - ResolutionFlow Design System */
|
||||
/* React Day Picker Customization */
|
||||
.rdp-custom {
|
||||
@apply text-foreground;
|
||||
}
|
||||
@@ -242,7 +234,7 @@
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_selected {
|
||||
@apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground;
|
||||
@apply bg-white text-black hover:bg-white/90 hover:text-black;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_today {
|
||||
|
||||
@@ -110,7 +110,7 @@ export function AccountSettingsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export function AccountSettingsPage() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
{error}
|
||||
@@ -134,23 +134,23 @@ export function AccountSettingsPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Building2 className="h-8 w-8 text-primary" />
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Account Settings</h1>
|
||||
<Building2 className="h-8 w-8 text-white/50" />
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Account Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage your account, subscription, and team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Account Info Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Account Information</h2>
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Account Information</h2>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Account Name */}
|
||||
<div>
|
||||
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Account Name
|
||||
</label>
|
||||
{isEditingName ? (
|
||||
@@ -160,9 +160,9 @@ export function AccountSettingsPage() {
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
@@ -177,8 +177,8 @@ export function AccountSettingsPage() {
|
||||
onClick={handleSaveName}
|
||||
disabled={isSavingName}
|
||||
className={cn(
|
||||
'rounded-md bg-primary p-2 text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'rounded-md bg-white p-2 text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{isSavingName ? (
|
||||
@@ -192,18 +192,18 @@ export function AccountSettingsPage() {
|
||||
setEditedName(account?.name ?? '')
|
||||
setIsEditingName(false)
|
||||
}}
|
||||
className="rounded-md border border-input p-2 text-muted-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 p-2 text-white/40 hover:bg-white/10"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<span className="text-sm text-foreground">{account?.name}</span>
|
||||
<span className="text-sm text-white">{account?.name}</span>
|
||||
{isAccountOwner && (
|
||||
<button
|
||||
onClick={() => setIsEditingName(true)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
className="text-xs text-white hover:underline"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -214,10 +214,10 @@ export function AccountSettingsPage() {
|
||||
|
||||
{/* Display Code */}
|
||||
<div>
|
||||
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Display Code
|
||||
</label>
|
||||
<p className="mt-1 text-sm font-mono text-muted-foreground">
|
||||
<p className="mt-1 text-sm font-mono text-white/40">
|
||||
{account?.display_code}
|
||||
</p>
|
||||
</div>
|
||||
@@ -225,8 +225,8 @@ export function AccountSettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Subscription Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Subscription</h2>
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Subscription</h2>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Plan & Status */}
|
||||
@@ -234,9 +234,9 @@ export function AccountSettingsPage() {
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
|
||||
plan === 'free' && 'bg-secondary text-secondary-foreground',
|
||||
plan === 'pro' && 'bg-primary/10 text-primary',
|
||||
plan === 'team' && 'bg-primary/20 text-primary'
|
||||
plan === 'free' && 'bg-white/10 text-white/70',
|
||||
plan === 'pro' && 'bg-white/10 text-white',
|
||||
plan === 'team' && 'bg-white/10 text-white'
|
||||
)}
|
||||
>
|
||||
<Crown className="h-3.5 w-3.5" />
|
||||
@@ -246,11 +246,11 @@ export function AccountSettingsPage() {
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||
sub.status === 'active' && 'bg-green-500/10 text-green-600',
|
||||
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-600',
|
||||
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-600',
|
||||
sub.status === 'canceled' && 'bg-destructive/10 text-destructive',
|
||||
sub.status === 'orphaned' && 'bg-muted text-muted-foreground'
|
||||
sub.status === 'active' && 'bg-green-500/10 text-emerald-400',
|
||||
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-400',
|
||||
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-400',
|
||||
sub.status === 'canceled' && 'bg-red-400/10 text-red-400',
|
||||
sub.status === 'orphaned' && 'bg-white/10 text-white/40'
|
||||
)}
|
||||
>
|
||||
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
|
||||
@@ -259,7 +259,7 @@ export function AccountSettingsPage() {
|
||||
</div>
|
||||
|
||||
{sub?.current_period_end && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-white/40">
|
||||
Current period ends: {new Date(sub.current_period_end).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
@@ -302,45 +302,45 @@ export function AccountSettingsPage() {
|
||||
|
||||
{/* Team Members Section (owners only) */}
|
||||
{isAccountOwner && (
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Team Members</h2>
|
||||
<Users className="h-5 w-5 text-white/50" />
|
||||
<h2 className="text-lg font-semibold text-white">Team Members</h2>
|
||||
</div>
|
||||
|
||||
{members.length === 0 ? (
|
||||
<p className="mt-4 text-sm text-muted-foreground">No team members yet.</p>
|
||||
<p className="mt-4 text-sm text-white/40">No team members yet.</p>
|
||||
) : (
|
||||
<div className="mt-4 divide-y divide-border">
|
||||
<div className="mt-4 divide-y divide-white/[0.06]">
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{member.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{member.email}</p>
|
||||
<p className="text-sm font-medium text-white">{member.name}</p>
|
||||
<p className="text-xs text-white/40">{member.email}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||
member.account_role === 'owner' && 'bg-primary/10 text-primary',
|
||||
member.account_role === 'engineer' && 'bg-secondary text-secondary-foreground',
|
||||
member.account_role === 'viewer' && 'bg-muted text-muted-foreground'
|
||||
member.account_role === 'owner' && 'bg-white/10 text-white',
|
||||
member.account_role === 'engineer' && 'bg-white/10 text-white/70',
|
||||
member.account_role === 'viewer' && 'bg-white/10 text-white/40'
|
||||
)}
|
||||
>
|
||||
{member.account_role}
|
||||
</span>
|
||||
{!member.is_active && (
|
||||
<span className="rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
||||
<span className="rounded-full bg-red-400/10 px-2 py-0.5 text-xs text-red-400">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
{member.account_role !== 'owner' && (
|
||||
<button
|
||||
onClick={() => handleRemoveMember(member.id)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
className="text-white/40 hover:text-red-400"
|
||||
title="Remove member"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -356,10 +356,10 @@ export function AccountSettingsPage() {
|
||||
|
||||
{/* Invite Member Section (owners only) */}
|
||||
{isAccountOwner && (
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Invite Member</h2>
|
||||
<Mail className="h-5 w-5 text-white/50" />
|
||||
<h2 className="text-lg font-semibold text-white">Invite Member</h2>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleInvite} className="mt-4 space-y-3">
|
||||
@@ -371,17 +371,17 @@ export function AccountSettingsPage() {
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
required
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
<select
|
||||
value={inviteRole}
|
||||
onChange={(e) => setInviteRole(e.target.value)}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="engineer">Engineer</option>
|
||||
@@ -391,8 +391,8 @@ export function AccountSettingsPage() {
|
||||
type="submit"
|
||||
disabled={isInviting || !inviteEmail.trim()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isInviting ? (
|
||||
@@ -407,18 +407,18 @@ export function AccountSettingsPage() {
|
||||
</div>
|
||||
|
||||
{inviteError && (
|
||||
<p className="text-sm text-destructive">{inviteError}</p>
|
||||
<p className="text-sm text-red-400">{inviteError}</p>
|
||||
)}
|
||||
{inviteSuccess && (
|
||||
<p className="text-sm text-green-600">{inviteSuccess}</p>
|
||||
<p className="text-sm text-emerald-400">{inviteSuccess}</p>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{/* Pending Invites */}
|
||||
{invites.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-sm font-medium text-card-foreground">Pending Invites</h3>
|
||||
<div className="mt-2 divide-y divide-border">
|
||||
<h3 className="text-sm font-medium text-white">Pending Invites</h3>
|
||||
<div className="mt-2 divide-y divide-white/[0.06]">
|
||||
{invites
|
||||
.filter((inv) => !inv.used_at)
|
||||
.map((invite) => (
|
||||
@@ -427,12 +427,12 @@ export function AccountSettingsPage() {
|
||||
className="flex items-center justify-between py-2"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm text-foreground">{invite.email}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-sm text-white">{invite.email}</p>
|
||||
<p className="text-xs text-white/40">
|
||||
Expires {new Date(invite.expires_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs text-secondary-foreground">
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
|
||||
{invite.role}
|
||||
</span>
|
||||
</div>
|
||||
@@ -463,25 +463,25 @@ function UsageStat({
|
||||
const isAtLimit = !isUnlimited && current >= max
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-background p-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||
<div className="glass-stat rounded-md p-3">
|
||||
<p className="text-xs font-medium text-white/40">{label}</p>
|
||||
<p
|
||||
className={cn(
|
||||
'mt-1 text-lg font-semibold',
|
||||
isAtLimit ? 'text-destructive' : isNearLimit ? 'text-yellow-600' : 'text-foreground'
|
||||
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-white'
|
||||
)}
|
||||
>
|
||||
{current}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
<span className="text-sm font-normal text-white/40">
|
||||
{' '}/ {isUnlimited ? 'Unlimited' : max}
|
||||
</span>
|
||||
</p>
|
||||
{!isUnlimited && (
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/10">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all',
|
||||
isAtLimit ? 'bg-destructive' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
|
||||
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-white'
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
|
||||
@@ -144,7 +144,7 @@ export function AdminCategoriesPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -154,18 +154,18 @@ export function AdminCategoriesPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">
|
||||
Step Categories
|
||||
</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage categories for organizing step library
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -180,16 +180,16 @@ export function AdminCategoriesPage() {
|
||||
type="checkbox"
|
||||
checked={includeArchived}
|
||||
onChange={(e) => setIncludeArchived(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 rounded border-white/10 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">Show archived categories</span>
|
||||
<span className="text-sm text-white/40">Show archived categories</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Categories List */}
|
||||
{categories.length === 0 ? (
|
||||
<div className="rounded-lg border border-border bg-card p-12 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl p-12 text-center">
|
||||
<p className="text-white/40">
|
||||
No categories found. Create your first category to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function LoginPage() {
|
||||
@@ -35,33 +34,38 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="flex min-h-screen items-center justify-center bg-black px-4">
|
||||
{/* Subtle radial overlay */}
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
||||
|
||||
<div className="relative w-full max-w-md space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex justify-center sm:mb-6">
|
||||
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
|
||||
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
</div>
|
||||
<h1>
|
||||
<BrandWordmark size="lg" />
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</h1>
|
||||
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
|
||||
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
|
||||
Decision Tree Platform
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
|
||||
<p className="mt-1 text-sm text-white/40 sm:mt-2">
|
||||
Sign in to your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="glass-card rounded-2xl p-6 space-y-4">
|
||||
{(error || localError) && (
|
||||
<div className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{localError || error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-1 block text-sm font-medium text-foreground">
|
||||
<label htmlFor="email" className="mb-1 block text-sm font-medium text-white">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
@@ -73,9 +77,9 @@ export function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20',
|
||||
'block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
@@ -83,7 +87,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="mb-1 block text-sm font-medium text-foreground">
|
||||
<label htmlFor="password" className="mb-1 block text-sm font-medium text-white">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
@@ -95,9 +99,9 @@ export function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20',
|
||||
'block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
@@ -108,20 +112,20 @@ export function LoginPage() {
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
|
||||
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-black',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'shadow-lg shadow-primary/20'
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
<p className="text-center text-sm text-white/40">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" className="font-medium text-gradient-brand hover:underline">
|
||||
<Link to="/register" className="font-medium text-white hover:underline">
|
||||
Register
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -108,10 +108,8 @@ export function MyTreesPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="font-heading text-3xl font-bold sm:text-4xl">
|
||||
<span className="text-gradient-brand">My Trees</span>
|
||||
</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">My Trees</h1>
|
||||
<p className="mt-2 text-white/40">
|
||||
Your forked and custom decision trees
|
||||
</p>
|
||||
</div>
|
||||
@@ -119,20 +117,20 @@ export function MyTreesPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-card/50 px-4 py-12 text-center">
|
||||
<FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground opacity-50" />
|
||||
<h2 className="mb-2 text-lg font-semibold text-foreground">No personal trees yet</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="rounded-lg border border-dashed border-white/10 bg-white/[0.02] px-4 py-12 text-center">
|
||||
<FolderTree className="mx-auto mb-4 h-12 w-12 text-white/20" />
|
||||
<h2 className="mb-2 text-lg font-semibold text-white">No personal trees yet</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
Fork a tree from the library to customize it for your workflow
|
||||
</p>
|
||||
<Link
|
||||
to="/trees"
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'inline-flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Browse Trees
|
||||
@@ -143,32 +141,32 @@ export function MyTreesPage() {
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
|
||||
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover sm:p-6"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="mb-3 flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
|
||||
<h3 className="font-semibold text-white">{tree.name}</h3>
|
||||
{tree.category_info && (
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
|
||||
<p className="mb-3 text-sm text-white/40 line-clamp-2">
|
||||
{tree.description || 'No description available'}
|
||||
</p>
|
||||
|
||||
{/* Fork Badge */}
|
||||
{tree.parent_tree_id && (
|
||||
<div className="mb-3 flex items-center gap-2 rounded-md bg-accent/50 px-2 py-1.5 text-sm">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
<div className="mb-3 flex items-center gap-2 rounded-md bg-white/5 px-2 py-1.5 text-sm">
|
||||
<GitBranch className="h-4 w-4 text-white/40" />
|
||||
<span className="text-white/40">
|
||||
Forked from{' '}
|
||||
<Link
|
||||
to={`/trees/${tree.parent_tree_id}/navigate`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
className="font-medium text-white hover:underline"
|
||||
>
|
||||
original
|
||||
</Link>
|
||||
@@ -184,7 +182,7 @@ export function MyTreesPage() {
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-4 flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="mb-4 flex items-center gap-4 text-xs text-white/30">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{formatDate(tree.lastUsed)}</span>
|
||||
@@ -201,8 +199,8 @@ export function MyTreesPage() {
|
||||
type="button"
|
||||
onClick={() => handleStartSession(tree.id)}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
@@ -212,8 +210,8 @@ export function MyTreesPage() {
|
||||
<Link
|
||||
to={`/trees/${tree.id}/edit`}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-2 text-white/40',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Edit tree"
|
||||
>
|
||||
@@ -227,8 +225,8 @@ export function MyTreesPage() {
|
||||
setShowShareModal(true)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-2 text-white/40',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Share tree"
|
||||
>
|
||||
@@ -241,8 +239,8 @@ export function MyTreesPage() {
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-destructive/10 hover:text-destructive'
|
||||
'rounded-md border border-white/10 p-2 text-white/40',
|
||||
'hover:bg-red-400/10 hover:text-red-400'
|
||||
)}
|
||||
title="Delete tree"
|
||||
>
|
||||
|
||||
315
frontend/src/pages/QuickStartPage-Enhanced.tsx
Normal file
315
frontend/src/pages/QuickStartPage-Enhanced.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Search, Clock, ArrowRight, Play, Loader2, TrendingUp, Sparkles, Zap } from 'lucide-react'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import type { Session } from '@/types/session'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = Date.now()
|
||||
const then = new Date(dateStr).getTime()
|
||||
const diffMs = now - then
|
||||
const minutes = Math.floor(diffMs / 60000)
|
||||
if (minutes < 1) return 'just now'
|
||||
if (minutes < 60) return `${minutes}m ago`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d ago`
|
||||
}
|
||||
|
||||
export function QuickStartPage() {
|
||||
const navigate = useNavigate()
|
||||
const [query, setQuery] = useState('')
|
||||
const [searchResults, setSearchResults] = useState<TreeListItem[]>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [showResults, setShowResults] = useState(false)
|
||||
const [activeSessions, setActiveSessions] = useState<Session[]>([])
|
||||
const [recentTrees, setRecentTrees] = useState<{ tree_id: string; name: string; lastUsed: string }[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const searchRef = useRef<HTMLDivElement>(null)
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// Load sessions on mount
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
const [active, recent] = await Promise.all([
|
||||
sessionsApi.list({ completed: false, size: 5 }),
|
||||
sessionsApi.list({ size: 10 }),
|
||||
])
|
||||
setActiveSessions(active.slice(0, 3))
|
||||
|
||||
// Deduplicate recent sessions by tree_id, max 5
|
||||
const seen = new Set<string>()
|
||||
const deduped: { tree_id: string; name: string; lastUsed: string }[] = []
|
||||
for (const s of recent) {
|
||||
if (!seen.has(s.tree_id) && deduped.length < 5) {
|
||||
seen.add(s.tree_id)
|
||||
deduped.push({
|
||||
tree_id: s.tree_id,
|
||||
name: s.tree_snapshot?.name || 'Unnamed Tree',
|
||||
lastUsed: s.started_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
setRecentTrees(deduped)
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// Debounced search
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||
|
||||
if (query.length < 2) {
|
||||
setSearchResults([])
|
||||
setShowResults(false)
|
||||
setIsSearching(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSearching(true)
|
||||
setShowResults(true)
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await treesApi.search(query, 8)
|
||||
setSearchResults(results)
|
||||
} catch (err) {
|
||||
console.error('Search failed:', err)
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||
}
|
||||
}, [query])
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
||||
setShowResults(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
||||
{/* Animated background grid */}
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,#4f4f4f12_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f12_1px,transparent_1px)] bg-[size:64px_64px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_110%)]" />
|
||||
|
||||
<div className="relative container mx-auto px-4 py-12">
|
||||
{/* Hero Section */}
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{/* Badge */}
|
||||
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-violet-500/10 to-purple-500/10 px-4 py-2 border border-violet-500/20 backdrop-blur-sm">
|
||||
<Sparkles className="h-4 w-4 text-violet-400" />
|
||||
<span className="text-sm font-medium text-violet-300">AI-Powered Troubleshooting</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="font-heading text-5xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-br from-white via-slate-200 to-slate-400 leading-tight animate-in fade-in slide-in-from-bottom-4 duration-700 delay-100">
|
||||
What are you troubleshooting?
|
||||
</h1>
|
||||
|
||||
<p className="text-center text-slate-400 mt-4 text-lg animate-in fade-in slide-in-from-bottom-4 duration-700 delay-200">
|
||||
Search our library of proven decision trees or continue where you left off
|
||||
</p>
|
||||
|
||||
{/* Enhanced Search Bar */}
|
||||
<div ref={searchRef} className="relative mt-8 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-300">
|
||||
<div className="relative group">
|
||||
{/* Glow effect */}
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-violet-600 to-purple-600 rounded-xl blur opacity-20 group-hover:opacity-40 transition duration-300" />
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400 transition-colors group-hover:text-violet-400" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setShowResults(true)}
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className={cn(
|
||||
'w-full rounded-xl border border-slate-700/50 bg-slate-900/90 backdrop-blur-xl py-5 pl-14 pr-5 text-lg',
|
||||
'text-white placeholder:text-slate-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-violet-500/50 focus:border-violet-500/50',
|
||||
'transition-all duration-300'
|
||||
)}
|
||||
/>
|
||||
{query && (
|
||||
<Zap className="absolute right-5 top-1/2 h-5 w-5 -translate-y-1/2 text-violet-400 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Search Results Dropdown */}
|
||||
{showResults && (
|
||||
<div className="absolute z-10 mt-2 w-full rounded-xl border border-slate-700/50 bg-slate-900/95 backdrop-blur-xl shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-violet-400" />
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center">
|
||||
<div className="text-slate-400 text-sm">No results found</div>
|
||||
<div className="text-slate-500 text-xs mt-1">Try a different search term</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="max-h-96 overflow-y-auto py-2">
|
||||
{searchResults.map((tree, idx) => (
|
||||
<li key={tree.id} style={{ animationDelay: `${idx * 50}ms` }} className="animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<button
|
||||
onClick={() => navigate(`/trees/${tree.id}/navigate`)}
|
||||
className="w-full px-5 py-4 text-left transition-all hover:bg-slate-800/50 group border-b border-slate-800/50 last:border-0"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-white group-hover:text-violet-300 transition-colors">
|
||||
{tree.name}
|
||||
</div>
|
||||
{tree.description && (
|
||||
<div className="mt-1 line-clamp-2 text-xs text-slate-400">
|
||||
{tree.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 flex-shrink-0 text-slate-600 group-hover:text-violet-400 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Continue Session Section */}
|
||||
{activeSessions.length > 0 && (
|
||||
<div className="mx-auto mt-16 max-w-6xl animate-in fade-in slide-in-from-bottom-4 duration-700 delay-500">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-1 bg-gradient-to-b from-violet-500 to-purple-600 rounded-full" />
|
||||
<h2 className="font-heading text-xl font-bold text-white">
|
||||
Continue Session
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-slate-700/50 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{activeSessions.map((session, idx) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() =>
|
||||
navigate(`/trees/${session.tree_id}/navigate`, {
|
||||
state: { sessionId: session.id },
|
||||
})
|
||||
}
|
||||
style={{ animationDelay: `${(idx + 5) * 100}ms` }}
|
||||
className="group relative rounded-xl border border-slate-700/50 bg-slate-900/50 backdrop-blur-sm p-5 text-left transition-all hover:border-violet-500/50 hover:shadow-lg hover:shadow-violet-500/10 hover:-translate-y-1 animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
>
|
||||
{/* Animated corner accent */}
|
||||
<div className="absolute top-0 right-0 h-16 w-16 bg-gradient-to-bl from-violet-500/20 to-transparent rounded-tr-xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-base font-semibold text-white group-hover:text-violet-300 transition-colors">
|
||||
{session.tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</div>
|
||||
{(session.ticket_number || session.client_name) && (
|
||||
<div className="mt-1.5 truncate text-sm text-slate-400">
|
||||
{[session.ticket_number, session.client_name]
|
||||
.filter(Boolean)
|
||||
.join(' • ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 rounded-full bg-violet-500/10 p-2 group-hover:bg-violet-500/20 transition-colors">
|
||||
<Play className="h-4 w-4 text-violet-400 group-hover:scale-110 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-2 text-xs text-slate-500">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>Started {timeAgo(session.started_at)}</span>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator (optional - you can remove this) */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-800/50 rounded-b-xl overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-violet-500 to-purple-600 w-2/3" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Trees Section */}
|
||||
{!isLoading && recentTrees.length > 0 && (
|
||||
<div className="mx-auto mt-12 max-w-6xl animate-in fade-in slide-in-from-bottom-4 duration-700 delay-700">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-violet-400" />
|
||||
<h2 className="font-heading text-xl font-bold text-white">
|
||||
Recent Trees
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-slate-700/50 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||
{recentTrees.map((tree, idx) => (
|
||||
<button
|
||||
key={tree.tree_id}
|
||||
onClick={() => navigate(`/trees/${tree.tree_id}/navigate`)}
|
||||
style={{ animationDelay: `${(idx + 8) * 100}ms` }}
|
||||
className="group relative rounded-xl border border-slate-700/50 bg-slate-900/30 backdrop-blur-sm p-4 text-left transition-all hover:border-violet-500/50 hover:bg-slate-900/50 hover:-translate-y-1 animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
>
|
||||
<div className="truncate text-sm font-medium text-white group-hover:text-violet-300 transition-colors">
|
||||
{tree.name}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-slate-500">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo(tree.lastUsed)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer CTA */}
|
||||
<div className="mx-auto mt-16 max-w-4xl text-center animate-in fade-in duration-700 delay-1000">
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-violet-600 to-purple-600 text-white font-medium hover:from-violet-500 hover:to-purple-500 transition-all hover:shadow-lg hover:shadow-violet-500/25 hover:scale-105"
|
||||
>
|
||||
Browse All Trees
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuickStartPage
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Search, Clock, ArrowRight, Play, Loader2 } from 'lucide-react'
|
||||
import { Search, Clock, ArrowRight, Play, Loader2, Sparkles } from 'lucide-react'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import type { Session } from '@/types/session'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = Date.now()
|
||||
@@ -107,39 +107,61 @@ export function QuickStartPage() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
{/* Hero Section */}
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h1 className="font-heading text-3xl font-bold text-foreground">
|
||||
What are you troubleshooting?
|
||||
<div className="mb-16 text-center max-w-4xl mx-auto">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
|
||||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-sm text-white/70 font-medium">DECISION TREE PLATFORM</span>
|
||||
</div>
|
||||
|
||||
{/* Main heading */}
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tight leading-tight">
|
||||
What are you<br />
|
||||
<span className="text-white/60">troubleshooting?</span>
|
||||
</h1>
|
||||
<div ref={searchRef} className="relative mt-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setShowResults(true)}
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-border bg-card py-3 pl-12 pr-4 text-lg',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary/50'
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-lg text-white/40 mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
Search our library of proven decision trees or continue where you left off
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div ref={searchRef} className="relative max-w-2xl mx-auto group">
|
||||
<div className="absolute inset-0 bg-white/5 rounded-2xl blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="relative glass-card rounded-2xl p-1">
|
||||
<div className="flex items-center bg-black/50 rounded-xl">
|
||||
<Search className="ml-5 w-5 h-5 text-blue-400" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setShowResults(true)}
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
|
||||
/>
|
||||
{query.length >= 2 && (
|
||||
<button
|
||||
onClick={() => {/* search already fires on type */}}
|
||||
className="mr-2 px-5 py-2.5 bg-white text-black font-semibold rounded-lg hover:bg-white/90 transition-all"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{showResults && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="absolute z-10 mt-2 w-full glass-card rounded-2xl shadow-[0_0_40px_rgba(0,0,0,0.5)] overflow-hidden">
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-white/40" />
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
<div className="px-4 py-8 text-center text-sm text-white/40">
|
||||
No results found
|
||||
</div>
|
||||
) : (
|
||||
@@ -148,13 +170,13 @@ export function QuickStartPage() {
|
||||
<li key={tree.id}>
|
||||
<button
|
||||
onClick={() => navigate(`/trees/${tree.id}/navigate`)}
|
||||
className="w-full px-4 py-3 text-left transition-colors hover:bg-accent"
|
||||
className="w-full px-5 py-3.5 text-left transition-all hover:bg-white/[0.06]"
|
||||
>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-sm font-medium text-white">
|
||||
{tree.name}
|
||||
</div>
|
||||
{tree.description && (
|
||||
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">
|
||||
<div className="mt-0.5 line-clamp-1 text-xs text-white/40">
|
||||
{tree.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -170,65 +192,115 @@ export function QuickStartPage() {
|
||||
|
||||
{/* Continue Session Section */}
|
||||
{activeSessions.length > 0 && (
|
||||
<div className="mx-auto mt-12 max-w-4xl">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">
|
||||
Continue Session
|
||||
</h2>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{activeSessions.map((session) => (
|
||||
<div className="mx-auto max-w-4xl mb-12">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Active Sessions</h2>
|
||||
</div>
|
||||
|
||||
{/* Primary active session — Bright Glow card */}
|
||||
<div className="glass-card-glow backdrop-blur-xl rounded-2xl p-8 mb-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-white/15 border border-white/30 flex items-center justify-center">
|
||||
<Play className="w-6 h-6 text-violet-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/70 font-semibold uppercase tracking-wider mb-1">
|
||||
Active Session
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white">
|
||||
{activeSessions[0].tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() =>
|
||||
navigate(`/trees/${session.tree_id}/navigate`, {
|
||||
state: { sessionId: session.id },
|
||||
navigate(`/trees/${activeSessions[0].tree_id}/navigate`, {
|
||||
state: { sessionId: activeSessions[0].id },
|
||||
})
|
||||
}
|
||||
className="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent"
|
||||
className="px-5 py-2.5 bg-white text-black rounded-xl font-semibold hover:bg-white/90 transition-all hover:scale-105"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{session.tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</div>
|
||||
{(session.ticket_number || session.client_name) && (
|
||||
<div className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{[session.ticket_number, session.client_name]
|
||||
.filter(Boolean)
|
||||
.join(' - ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo(session.started_at)}</span>
|
||||
</div>
|
||||
Continue
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-white/50 mt-1">
|
||||
{[activeSessions[0].ticket_number, activeSessions[0].client_name]
|
||||
.filter(Boolean)
|
||||
.join(' \u2022 ')}
|
||||
{activeSessions[0].started_at && ` \u2022 Started ${timeAgo(activeSessions[0].started_at)}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Additional active sessions */}
|
||||
{activeSessions.length > 1 && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{activeSessions.slice(1).map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() =>
|
||||
navigate(`/trees/${session.tree_id}/navigate`, {
|
||||
state: { sessionId: session.id },
|
||||
})
|
||||
}
|
||||
className="glass-card hover:glass-card-hover rounded-2xl p-5 text-left transition-all hover:scale-[1.02] cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-bold text-white">
|
||||
{session.tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</div>
|
||||
{(session.ticket_number || session.client_name) && (
|
||||
<div className="mt-1 truncate text-xs text-white/40">
|
||||
{[session.ticket_number, session.client_name]
|
||||
.filter(Boolean)
|
||||
.join(' - ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-violet-400" />
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs text-white/30">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{timeAgo(session.started_at)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Trees Section */}
|
||||
{!isLoading && recentTrees.length > 0 && (
|
||||
<div className="mx-auto mt-10 max-w-4xl">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">
|
||||
Recent Trees
|
||||
</h2>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="mx-auto max-w-4xl mb-12">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Recent Trees</h2>
|
||||
<Link
|
||||
to="/trees"
|
||||
className="text-sm text-white/60 hover:text-white font-medium transition-colors"
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{recentTrees.map((tree) => (
|
||||
<button
|
||||
key={tree.tree_id}
|
||||
onClick={() => navigate(`/trees/${tree.tree_id}/navigate`)}
|
||||
className="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent"
|
||||
className="glass-card hover:glass-card-hover rounded-2xl p-5 text-left transition-all hover:scale-[1.02] cursor-pointer"
|
||||
>
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<Search className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="truncate text-sm font-bold text-white mb-2">
|
||||
{tree.name}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo(tree.lastUsed)}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs text-white/30">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>Last used {timeAgo(tree.lastUsed)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -237,10 +309,10 @@ export function QuickStartPage() {
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mx-auto mt-12 max-w-4xl text-center">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-white/10 border border-white/20 text-white font-medium rounded-xl hover:bg-white/20 transition-all"
|
||||
>
|
||||
Browse All Trees
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { inviteApi } from '@/api'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function RegisterPage() {
|
||||
@@ -76,33 +75,38 @@ export function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="flex min-h-screen items-center justify-center bg-black px-4">
|
||||
{/* Subtle radial overlay */}
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
||||
|
||||
<div className="relative w-full max-w-md space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex justify-center sm:mb-6">
|
||||
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
|
||||
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
</div>
|
||||
<h1>
|
||||
<BrandWordmark size="lg" />
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</h1>
|
||||
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
|
||||
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
|
||||
Decision Tree Platform
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
|
||||
<p className="mt-1 text-sm text-white/40 sm:mt-2">
|
||||
Create your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<div className="glass-card rounded-2xl p-6 space-y-4">
|
||||
{(error || localError) && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{localError || error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="inviteCode" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="inviteCode" className="block text-sm font-medium text-white">
|
||||
Invite code
|
||||
</label>
|
||||
<input
|
||||
@@ -116,29 +120,29 @@ export function RegisterPage() {
|
||||
}}
|
||||
onBlur={(e) => validateInviteCode(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border bg-background px-3 py-2 font-mono tracking-wider',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'mt-1 block w-full rounded-xl border bg-black/50 px-3 py-2 font-mono tracking-wider',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:outline-none focus:ring-1',
|
||||
inviteCodeStatus === 'valid' && 'border-green-500 focus:border-green-500 focus:ring-green-500',
|
||||
inviteCodeStatus === 'invalid' && 'border-destructive focus:border-destructive focus:ring-destructive',
|
||||
inviteCodeStatus === 'idle' && 'border-input focus:border-primary focus:ring-primary',
|
||||
inviteCodeStatus === 'checking' && 'border-input focus:border-primary focus:ring-primary'
|
||||
inviteCodeStatus === 'valid' && 'border-emerald-400/50 focus:border-emerald-400 focus:ring-emerald-400/30',
|
||||
inviteCodeStatus === 'invalid' && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/30',
|
||||
inviteCodeStatus === 'idle' && 'border-white/10 focus:border-white/30 focus:ring-white/20',
|
||||
inviteCodeStatus === 'checking' && 'border-white/10 focus:border-white/30 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="ABCD1234"
|
||||
/>
|
||||
{inviteCodeStatus === 'checking' && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Validating...</p>
|
||||
<p className="mt-1 text-xs text-white/40">Validating...</p>
|
||||
)}
|
||||
{inviteCodeStatus === 'valid' && (
|
||||
<p className="mt-1 text-xs text-green-600">{inviteCodeMessage}</p>
|
||||
<p className="mt-1 text-xs text-emerald-400">{inviteCodeMessage}</p>
|
||||
)}
|
||||
{inviteCodeStatus === 'invalid' && (
|
||||
<p className="mt-1 text-xs text-destructive">{inviteCodeMessage}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{inviteCodeMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-white">
|
||||
Full name
|
||||
</label>
|
||||
<input
|
||||
@@ -150,16 +154,16 @@ export function RegisterPage() {
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="John Smith"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-white">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
@@ -171,16 +175,16 @@ export function RegisterPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-white">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
@@ -192,19 +196,19 @@ export function RegisterPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/30">
|
||||
Must be at least 10 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
@@ -216,9 +220,9 @@ export function RegisterPage() {
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
/>
|
||||
@@ -228,20 +232,20 @@ export function RegisterPage() {
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
|
||||
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-black',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'shadow-lg shadow-primary/20'
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Creating account...' : 'Create account'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
<p className="text-center text-sm text-white/40">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="font-medium text-gradient-brand hover:underline">
|
||||
<Link to="/login" className="font-medium text-white hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -223,7 +223,7 @@ export function SessionDetailPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -231,12 +231,12 @@ export function SessionDetailPage() {
|
||||
if (error || !session) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
|
||||
{error || 'Session not found'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="mt-4 text-primary hover:underline"
|
||||
className="mt-4 text-white hover:underline"
|
||||
>
|
||||
Back to sessions
|
||||
</button>
|
||||
@@ -252,18 +252,18 @@ export function SessionDetailPage() {
|
||||
<div>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
className="mb-2 text-sm text-white/40 hover:text-white"
|
||||
>
|
||||
← Back to sessions
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">
|
||||
{session.ticket_number || 'Session Details'}
|
||||
</h1>
|
||||
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="mt-2 flex items-center gap-4 text-sm text-white/40">
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-1',
|
||||
session.completed_at ? 'text-green-600' : 'text-yellow-600'
|
||||
session.completed_at ? 'text-emerald-400' : 'text-yellow-400'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
@@ -286,8 +286,8 @@ export function SessionDetailPage() {
|
||||
onClick={() => setShowSaveAsTreeModal(true)}
|
||||
disabled={isSavingTree}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 bg-transparent px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
@@ -299,8 +299,8 @@ export function SessionDetailPage() {
|
||||
<button
|
||||
onClick={handleCopyForTicket}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
{copiedPsa ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
@@ -314,8 +314,8 @@ export function SessionDetailPage() {
|
||||
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
|
||||
aria-label="Export format"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm sm:w-auto',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white sm:w-auto',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="markdown">Markdown</option>
|
||||
@@ -328,18 +328,18 @@ export function SessionDetailPage() {
|
||||
disabled={isExporting}
|
||||
title="Copy to clipboard"
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'rounded-md border border-white/10 bg-transparent p-2 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePreview}
|
||||
disabled={isExporting}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -352,37 +352,37 @@ export function SessionDetailPage() {
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="mb-8">
|
||||
<h2 className="mb-4 text-lg font-semibold text-foreground">Decision Timeline</h2>
|
||||
<h2 className="mb-4 text-lg font-semibold text-white">Decision Timeline</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="h-3 w-3 rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">
|
||||
<span className="h-3 w-3 rounded-full bg-white" />
|
||||
<span className="text-white/40">
|
||||
Session started: {formatDate(session.started_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{session.decisions.map((decision, index) => (
|
||||
<div key={index} className="ml-1 border-l-2 border-border pl-6">
|
||||
<div key={index} className="ml-1 border-l-2 border-white/[0.06] pl-6">
|
||||
<div className="relative">
|
||||
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-border" />
|
||||
<div className="rounded-lg border border-border bg-card p-4">
|
||||
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-white/20" />
|
||||
<div className="glass-card rounded-xl p-4">
|
||||
{decision.question && (
|
||||
<p className="font-medium text-card-foreground">{decision.question}</p>
|
||||
<p className="font-medium text-white">{decision.question}</p>
|
||||
)}
|
||||
{decision.answer && (
|
||||
<p className="mt-1 text-sm text-primary">Answer: {decision.answer}</p>
|
||||
<p className="mt-1 text-sm text-white">Answer: {decision.answer}</p>
|
||||
)}
|
||||
{decision.action_performed && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
Action: {decision.action_performed}
|
||||
</p>
|
||||
)}
|
||||
{decision.notes && (
|
||||
<p className="mt-2 rounded bg-muted/50 p-2 text-sm text-muted-foreground">
|
||||
<p className="mt-2 rounded bg-white/5 p-2 text-sm text-white/40">
|
||||
Notes: {decision.notes}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
<p className="mt-2 text-xs text-white/40">
|
||||
{formatDate(decision.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -393,7 +393,7 @@ export function SessionDetailPage() {
|
||||
{session.completed_at && (
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="h-3 w-3 rounded-full bg-green-500" />
|
||||
<span className="text-green-600">
|
||||
<span className="text-emerald-400">
|
||||
Session completed: {formatDate(session.completed_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -142,14 +142,14 @@ export function SessionHistoryPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Session History</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Session History</h1>
|
||||
<p className="mt-2 text-white/40">
|
||||
Search and filter your troubleshooting sessions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="mb-6 flex gap-2 border-b border-border">
|
||||
<div className="mb-6 flex gap-2 border-b border-white/[0.06]">
|
||||
{(['all', 'active', 'completed'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -157,8 +157,8 @@ export function SessionHistoryPage() {
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium transition-colors',
|
||||
filter === tab
|
||||
? 'border-b-2 border-primary text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
? 'border-b-2 border-white text-white'
|
||||
: 'text-white/40 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||
@@ -179,22 +179,22 @@ export function SessionHistoryPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<div className="py-12 text-center text-white/40">
|
||||
No sessions found.{' '}
|
||||
{filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from ? (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="text-primary hover:underline"
|
||||
className="text-white hover:underline"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="text-primary hover:underline"
|
||||
className="text-white hover:underline"
|
||||
>
|
||||
Start a new session
|
||||
</button>
|
||||
@@ -205,7 +205,7 @@ export function SessionHistoryPage() {
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md"
|
||||
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex-1">
|
||||
@@ -217,23 +217,23 @@ export function SessionHistoryPage() {
|
||||
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium text-card-foreground">
|
||||
<span className="font-medium text-white">
|
||||
{session.ticket_number || 'No ticket'}
|
||||
</span>
|
||||
{session.client_name && (
|
||||
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs font-medium">
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs font-medium text-white">
|
||||
{session.client_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tree Name */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
<span className="font-medium">Tree:</span> {getTreeName(session)}
|
||||
</p>
|
||||
|
||||
{/* Timestamps */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
Started: {formatDate(session.started_at)}
|
||||
{session.completed_at && (
|
||||
<> · Completed: {formatDate(session.completed_at)}</>
|
||||
@@ -241,7 +241,7 @@ export function SessionHistoryPage() {
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
{session.decisions.length} decision{session.decisions.length !== 1 ? 's' : ''} recorded
|
||||
{session.scratchpad && session.scratchpad.trim() && (
|
||||
<span> · Has notes</span>
|
||||
@@ -254,8 +254,8 @@ export function SessionHistoryPage() {
|
||||
<button
|
||||
onClick={() => navigate(`/sessions/${session.id}`)}
|
||||
className={cn(
|
||||
'rounded-md border border-input px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
View Details
|
||||
@@ -264,8 +264,8 @@ export function SessionHistoryPage() {
|
||||
<button
|
||||
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Resume
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Settings } from 'lucide-react'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { useThemeStore } from '@/store/themeStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
export function SettingsPage() {
|
||||
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
||||
const { theme } = useThemeStore()
|
||||
|
||||
const handleExportFormatChange = (format: 'markdown' | 'text' | 'html') => {
|
||||
setDefaultExportFormat(format)
|
||||
@@ -18,50 +15,30 @@ export function SettingsPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="h-8 w-8 text-primary" />
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Settings</h1>
|
||||
<Settings className="h-8 w-8 text-white/50" />
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage your application preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl space-y-6">
|
||||
{/* Appearance Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Customize how ResolutionFlow looks on your device
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||
Theme
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current: {theme.charAt(0).toUpperCase() + theme.slice(1)}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Preferences Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Export Preferences</h2>
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
Configure default settings for session exports
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="export-format"
|
||||
className="block font-label text-sm font-medium text-card-foreground"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
Default Export Format
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-white/40">
|
||||
This format will be pre-selected when exporting sessions
|
||||
</p>
|
||||
<select
|
||||
@@ -69,9 +46,9 @@ export function SettingsPage() {
|
||||
value={defaultExportFormat}
|
||||
onChange={(e) => handleExportFormatChange(e.target.value as 'markdown' | 'text' | 'html')}
|
||||
className={cn(
|
||||
'mt-2 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-sm text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-2 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-sm text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="markdown">Markdown (.md)</option>
|
||||
@@ -82,12 +59,12 @@ export function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">About</h2>
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
ResolutionFlow - Decision Tree Platform
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
<p className="mt-2 text-sm text-white/40">
|
||||
Transform troubleshooting into guided workflows
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -248,7 +248,7 @@ export function TreeEditorPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -257,16 +257,16 @@ export function TreeEditorPage() {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center px-6 text-center">
|
||||
<Monitor className="mb-4 h-12 w-12 text-muted-foreground" />
|
||||
<h2 className="mb-2 text-xl font-semibold text-foreground">Desktop Required</h2>
|
||||
<p className="mb-6 max-w-sm text-sm text-muted-foreground">
|
||||
<Monitor className="mb-4 h-12 w-12 text-white/50" />
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">Desktop Required</h2>
|
||||
<p className="mb-6 max-w-sm text-sm text-white/40">
|
||||
The tree editor requires a larger screen for the best experience. Please open this page on a desktop or tablet in landscape mode.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Back to Library
|
||||
@@ -280,18 +280,18 @@ export function TreeEditorPage() {
|
||||
|
||||
{/* Draft Restore Prompt */}
|
||||
{showDraftPrompt && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold">Restore Draft?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="glass-card rounded-2xl w-full max-w-md p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold text-white">Restore Draft?</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
You have an unsaved draft from a previous session. Would you like to restore it?
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleRestoreDraft}
|
||||
className={cn(
|
||||
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Restore Draft
|
||||
@@ -299,8 +299,8 @@ export function TreeEditorPage() {
|
||||
<button
|
||||
onClick={handleDiscardDraft}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent'
|
||||
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Start Fresh
|
||||
@@ -312,18 +312,18 @@ export function TreeEditorPage() {
|
||||
|
||||
{/* Unsaved Changes Dialog */}
|
||||
{blocker.state === 'blocked' && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold">Unsaved Changes</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="glass-card rounded-2xl w-full max-w-md p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold text-white">Unsaved Changes</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
You have unsaved changes. Are you sure you want to leave?
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleBlockerReset}
|
||||
className={cn(
|
||||
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Stay
|
||||
@@ -331,8 +331,8 @@ export function TreeEditorPage() {
|
||||
<button
|
||||
onClick={handleBlockerProceed}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-destructive',
|
||||
'hover:bg-accent'
|
||||
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-red-400',
|
||||
'hover:bg-white/10'
|
||||
)}
|
||||
>
|
||||
Leave Without Saving
|
||||
@@ -343,17 +343,17 @@ export function TreeEditorPage() {
|
||||
)}
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between border-b border-border bg-card px-4 py-2">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] bg-black px-4 py-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
className="text-sm text-white/50 hover:text-white"
|
||||
>
|
||||
← Back to Library
|
||||
</button>
|
||||
<h1 className="text-lg font-semibold">
|
||||
<h1 className="text-lg font-semibold text-white">
|
||||
{isEditMode ? 'Edit Tree' : 'Create New Tree'}
|
||||
{name && <span className="ml-2 text-muted-foreground">- {name}</span>}
|
||||
{name && <span className="ml-2 text-white/40">- {name}</span>}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
{treeStatus === 'draft' && (
|
||||
@@ -372,7 +372,7 @@ export function TreeEditorPage() {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Undo/Redo */}
|
||||
<div className="flex items-center rounded-md border border-border">
|
||||
<div className="flex items-center rounded-md border border-white/[0.06]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => undo()}
|
||||
@@ -381,13 +381,13 @@ export function TreeEditorPage() {
|
||||
className={cn(
|
||||
'rounded-l-md p-2 transition-colors',
|
||||
pastStates.length > 0
|
||||
? 'text-foreground hover:bg-accent'
|
||||
: 'text-muted-foreground/40 cursor-not-allowed'
|
||||
? 'text-white hover:bg-white/[0.06]'
|
||||
: 'text-white/20 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Undo2 className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="h-6 w-px bg-border" />
|
||||
<div className="h-6 w-px bg-white/[0.06]" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => redo()}
|
||||
@@ -396,15 +396,15 @@ export function TreeEditorPage() {
|
||||
className={cn(
|
||||
'rounded-r-md p-2 transition-colors',
|
||||
futureStates.length > 0
|
||||
? 'text-foreground hover:bg-accent'
|
||||
: 'text-muted-foreground/40 cursor-not-allowed'
|
||||
? 'text-white hover:bg-white/[0.06]'
|
||||
: 'text-white/20 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Redo2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mx-2 h-6 w-px bg-border" />
|
||||
<div className="mx-2 h-6 w-px bg-white/[0.06]" />
|
||||
|
||||
{/* Validate */}
|
||||
<button
|
||||
@@ -412,8 +412,8 @@ export function TreeEditorPage() {
|
||||
disabled={isSaving}
|
||||
title="Validate tree structure (checks for errors and warnings)"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent disabled:opacity-50'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
@@ -426,8 +426,8 @@ export function TreeEditorPage() {
|
||||
disabled={isSaving || !isDirty}
|
||||
title="Save as draft (Ctrl+S when draft or has errors)"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
@@ -440,8 +440,8 @@ export function TreeEditorPage() {
|
||||
disabled={isSaving || !isDirty || hasBlockingErrors}
|
||||
title={hasBlockingErrors ? 'Fix validation errors before publishing (Ctrl+S when no errors)' : 'Publish tree (Ctrl+S when no errors)'}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
|
||||
@@ -199,8 +199,8 @@ export function TreeLibraryPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Decision Trees</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Decision Trees</h1>
|
||||
<p className="mt-2 text-white/40">
|
||||
Select a troubleshooting tree to start a new session
|
||||
</p>
|
||||
</div>
|
||||
@@ -208,8 +208,8 @@ export function TreeLibraryPage() {
|
||||
<Link
|
||||
to="/trees/new"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -225,9 +225,9 @@ export function TreeLibraryPage() {
|
||||
<button
|
||||
onClick={() => setMobileFolderOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium md:hidden',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
selectedFolderId && 'border-primary text-primary'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium md:hidden',
|
||||
'text-white/40 hover:bg-white/10 hover:text-white',
|
||||
selectedFolderId && 'border-white/30 text-white'
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
@@ -241,16 +241,16 @@ export function TreeLibraryPage() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Search
|
||||
@@ -262,8 +262,8 @@ export function TreeLibraryPage() {
|
||||
onChange={(e) => setSelectedCategoryId(e.target.value)}
|
||||
aria-label="Filter by category"
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
@@ -284,9 +284,9 @@ export function TreeLibraryPage() {
|
||||
type="checkbox"
|
||||
checked={showDrafts}
|
||||
onChange={(e) => setShowDrafts(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 rounded border-white/20 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-2"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">Show my drafts</span>
|
||||
<span className="text-sm text-white/40">Show my drafts</span>
|
||||
</label>
|
||||
</div>
|
||||
<ViewToggle view={treeLibraryView} onChange={setTreeLibraryView} />
|
||||
@@ -296,24 +296,24 @@ export function TreeLibraryPage() {
|
||||
{/* Active Filters */}
|
||||
{hasActiveFilters && (
|
||||
<div className="mb-6 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Filters:</span>
|
||||
<span className="text-sm text-white/40">Filters:</span>
|
||||
{selectedFolderId && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
|
||||
Folder
|
||||
<button
|
||||
onClick={() => setSelectedFolderId(null)}
|
||||
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{selectedCategoryId && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-secondary px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
|
||||
{categories.find((c) => c.id === selectedCategoryId)?.name}
|
||||
<button
|
||||
onClick={() => setSelectedCategoryId('')}
|
||||
className="rounded-full p-0.5 hover:bg-secondary-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -322,12 +322,12 @@ export function TreeLibraryPage() {
|
||||
{selectedTags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-sm text-primary"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => removeTagFilter(tag)}
|
||||
className="rounded-full p-0.5 hover:bg-primary/20"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -335,7 +335,7 @@ export function TreeLibraryPage() {
|
||||
))}
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
className="text-sm text-white/40 hover:text-white"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
@@ -345,10 +345,10 @@ export function TreeLibraryPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<div className="py-12 text-center text-white/40">
|
||||
No trees found.{' '}
|
||||
{(searchQuery || hasActiveFilters) && 'Try adjusting your filters.'}
|
||||
</div>
|
||||
|
||||
@@ -267,7 +267,7 @@ export function TreeNavigationPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -275,12 +275,12 @@ export function TreeNavigationPage() {
|
||||
if (error || !tree) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md bg-red-400/10 p-4 text-red-400">
|
||||
{error || 'Tree not found'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="mt-4 text-primary hover:underline"
|
||||
className="mt-4 text-white/50 hover:text-white hover:underline"
|
||||
>
|
||||
Back to trees
|
||||
</button>
|
||||
@@ -292,17 +292,17 @@ export function TreeNavigationPage() {
|
||||
if (showMetadataForm) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-lg px-4 py-8">
|
||||
<h1 className="mb-2 text-2xl font-bold text-foreground">{tree.name}</h1>
|
||||
<p className="mb-6 text-muted-foreground">{tree.description}</p>
|
||||
<h1 className="mb-2 text-2xl font-bold text-white">{tree.name}</h1>
|
||||
<p className="mb-6 text-white/40">{tree.description}</p>
|
||||
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-6">
|
||||
<h2 className="font-semibold text-card-foreground">Session Details</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl space-y-4 p-6">
|
||||
<h2 className="font-semibold text-white">Session Details</h2>
|
||||
<p className="text-sm text-white/40">
|
||||
Optional: Add ticket and client info for easier tracking
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Ticket Number
|
||||
</label>
|
||||
<input
|
||||
@@ -311,15 +311,15 @@ export function TreeNavigationPage() {
|
||||
onChange={(e) => setTicketNumber(e.target.value)}
|
||||
placeholder="e.g., INC0012345"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Client Name
|
||||
</label>
|
||||
<input
|
||||
@@ -328,9 +328,9 @@ export function TreeNavigationPage() {
|
||||
onChange={(e) => setClientName(e.target.value)}
|
||||
placeholder="e.g., Acme Corp"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -338,8 +338,8 @@ export function TreeNavigationPage() {
|
||||
<button
|
||||
onClick={startSession}
|
||||
className={cn(
|
||||
'w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'w-full rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Start Troubleshooting
|
||||
@@ -352,7 +352,7 @@ export function TreeNavigationPage() {
|
||||
if (!currentNode && !currentCustomStep) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md bg-red-400/10 p-4 text-red-400">
|
||||
Invalid tree structure
|
||||
</div>
|
||||
</div>
|
||||
@@ -367,9 +367,9 @@ export function TreeNavigationPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">{tree.name}</h1>
|
||||
<h1 className="text-xl font-bold text-white">{tree.name}</h1>
|
||||
{(ticketNumber || clientName) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-white/40">
|
||||
{ticketNumber && `Ticket: ${ticketNumber}`}
|
||||
{ticketNumber && clientName && ' · '}
|
||||
{clientName && `Client: ${clientName}`}
|
||||
@@ -378,7 +378,7 @@ export function TreeNavigationPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded-md px-3 py-1.5 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
Exit
|
||||
</button>
|
||||
@@ -392,12 +392,12 @@ export function TreeNavigationPage() {
|
||||
const label = node?.question || node?.title || customStep?.step_data.title || nodeId
|
||||
return (
|
||||
<span key={nodeId} className="flex items-center gap-2 whitespace-nowrap">
|
||||
{index > 0 && <span className="text-muted-foreground">→</span>}
|
||||
{index > 0 && <span className="text-white/40">→</span>}
|
||||
<span
|
||||
className={cn(
|
||||
index === pathTaken.length - 1
|
||||
? 'font-medium text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
? 'font-medium text-white'
|
||||
: 'text-white/40'
|
||||
)}
|
||||
>
|
||||
{label.length > 30 ? `${label.slice(0, 30)}...` : label}
|
||||
@@ -408,15 +408,15 @@ export function TreeNavigationPage() {
|
||||
</div>
|
||||
|
||||
{/* Current Node */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<div className="glass-card rounded-2xl p-6 shadow-sm">
|
||||
{/* Decision Node */}
|
||||
{currentNode && currentNode.type === 'decision' && (
|
||||
<>
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentNode.question}
|
||||
</h2>
|
||||
{currentNode.help_text && (
|
||||
<div className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="mb-4 text-sm text-white/50">
|
||||
<MarkdownContent content={currentNode.help_text} />
|
||||
</div>
|
||||
)}
|
||||
@@ -426,13 +426,13 @@ export function TreeNavigationPage() {
|
||||
key={option.id}
|
||||
onClick={() => handleSelectOption(option.id, option.label, option.next_node_id)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input p-3 text-left transition-colors',
|
||||
'hover:border-primary hover:bg-accent',
|
||||
'w-full rounded-md border border-white/10 p-3 text-left text-white transition-colors',
|
||||
'hover:border-white/30 hover:bg-white/[0.06]',
|
||||
'flex items-center gap-3'
|
||||
)}
|
||||
>
|
||||
{index < 9 && (
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-muted text-xs font-medium text-muted-foreground">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 text-xs font-medium text-white/50">
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
@@ -443,7 +443,7 @@ export function TreeNavigationPage() {
|
||||
{/* Previously-created custom steps at this node */}
|
||||
{customStepFlow.customSteps.filter(cs => cs.inserted_after_node_id === currentNodeId).length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-white/40">
|
||||
Your Custom Steps
|
||||
</p>
|
||||
{customStepFlow.customSteps
|
||||
@@ -454,13 +454,12 @@ export function TreeNavigationPage() {
|
||||
key={cs.id}
|
||||
onClick={() => customStepFlow.handleNavigateToCustomStep(cs)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-purple-300 bg-purple-50 p-3 text-left transition-colors',
|
||||
'hover:border-purple-500 hover:bg-purple-100',
|
||||
'dark:border-purple-700 dark:bg-purple-900/20 dark:hover:border-purple-500 dark:hover:bg-purple-900/40',
|
||||
'w-full rounded-md border border-purple-700 bg-purple-900/20 p-3 text-left text-white transition-colors',
|
||||
'hover:border-purple-500 hover:bg-purple-900/40',
|
||||
'flex items-center gap-3'
|
||||
)}
|
||||
>
|
||||
<span className="flex-shrink-0 rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
|
||||
<span className="flex-shrink-0 rounded-full bg-purple-900 px-2 py-0.5 text-xs font-medium text-purple-100">
|
||||
Custom
|
||||
</span>
|
||||
<span>{cs.step_data.title}</span>
|
||||
@@ -472,7 +471,7 @@ export function TreeNavigationPage() {
|
||||
{/* Add Custom Step Button */}
|
||||
<button
|
||||
onClick={() => customStepFlow.setShowCustomStepModal(true)}
|
||||
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-primary hover:bg-primary/10"
|
||||
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-white/50 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
Add Custom Step
|
||||
@@ -482,18 +481,18 @@ export function TreeNavigationPage() {
|
||||
|
||||
{/* Custom Step Node */}
|
||||
{currentCustomStep && (
|
||||
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-900/20">
|
||||
<div className="rounded-lg border border-purple-800 bg-purple-900/20 p-4">
|
||||
{/* Custom Step Badge */}
|
||||
<span className="mb-2 inline-block rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
|
||||
<span className="mb-2 inline-block rounded-full bg-purple-900 px-2 py-1 text-xs font-medium text-purple-100">
|
||||
Custom Step
|
||||
</span>
|
||||
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentCustomStep.step_data.title}
|
||||
</h2>
|
||||
|
||||
{currentCustomStep.step_data.content.instructions && (
|
||||
<div className="mb-4 text-muted-foreground">
|
||||
<div className="mb-4 text-white/60">
|
||||
<MarkdownContent content={currentCustomStep.step_data.content.instructions} />
|
||||
</div>
|
||||
)}
|
||||
@@ -506,12 +505,12 @@ export function TreeNavigationPage() {
|
||||
|
||||
{currentCustomStep.step_data.content.commands && currentCustomStep.step_data.content.commands.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
|
||||
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
|
||||
<div className="space-y-2">
|
||||
{currentCustomStep.step_data.content.commands.map((cmd, index) => (
|
||||
<div key={index}>
|
||||
<p className="mb-1 text-xs text-muted-foreground">{cmd.label}</p>
|
||||
<code className="block rounded bg-muted p-2 text-sm font-mono">
|
||||
<p className="mb-1 text-xs text-white/40">{cmd.label}</p>
|
||||
<code className="block rounded bg-white/10 p-2 text-sm font-mono">
|
||||
{cmd.command}
|
||||
</code>
|
||||
</div>
|
||||
@@ -525,13 +524,13 @@ export function TreeNavigationPage() {
|
||||
const targetNode = findNode(customStepFlow.pendingContinuationNodeId, tree?.tree_structure)
|
||||
const targetLabel = targetNode?.question || targetNode?.title || 'next step'
|
||||
return (
|
||||
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
||||
<div className="mt-6 border-t border-purple-700 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={customStepFlow.handleContinueToDescendant}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-md bg-primary px-4 py-3 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex w-full items-center justify-between rounded-md bg-white px-4 py-3 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<span>Continue to: {targetLabel.length > 50 ? `${targetLabel.slice(0, 50)}...` : targetLabel}</span>
|
||||
@@ -543,16 +542,16 @@ export function TreeNavigationPage() {
|
||||
|
||||
{/* Custom Branch Controls */}
|
||||
{customStepFlow.customBranchMode && (
|
||||
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
||||
<p className="mb-3 text-sm text-amber-600 dark:text-amber-400">
|
||||
<div className="mt-6 border-t border-purple-700 pt-4">
|
||||
<p className="mb-3 text-sm text-amber-400">
|
||||
Building custom branch - add steps until the issue is resolved
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => customStepFlow.setShowCustomStepModal(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-4 py-2 text-sm font-medium',
|
||||
'bg-background hover:bg-accent hover:text-accent-foreground'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -578,22 +577,22 @@ export function TreeNavigationPage() {
|
||||
{/* Action Node */}
|
||||
{currentNode && currentNode.type === 'action' && (
|
||||
<>
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentNode.title}
|
||||
</h2>
|
||||
{currentNode.description && (
|
||||
<div className="mb-4 text-muted-foreground">
|
||||
<div className="mb-4 text-white/60">
|
||||
<MarkdownContent content={currentNode.description} />
|
||||
</div>
|
||||
)}
|
||||
{currentNode.commands && currentNode.commands.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
|
||||
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
|
||||
<div className="space-y-1">
|
||||
{currentNode.commands.map((cmd, index) => (
|
||||
<code
|
||||
key={index}
|
||||
className="block rounded bg-muted p-2 text-sm font-mono"
|
||||
className="block rounded bg-white/10 p-2 text-sm font-mono"
|
||||
>
|
||||
{cmd}
|
||||
</code>
|
||||
@@ -602,7 +601,7 @@ export function TreeNavigationPage() {
|
||||
</div>
|
||||
)}
|
||||
{currentNode.expected_outcome && (
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
<strong>Expected outcome:</strong> {currentNode.expected_outcome}
|
||||
</p>
|
||||
)}
|
||||
@@ -610,8 +609,8 @@ export function TreeNavigationPage() {
|
||||
<button
|
||||
onClick={() => handleContinue()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Continue
|
||||
@@ -624,22 +623,22 @@ export function TreeNavigationPage() {
|
||||
{currentNode && currentNode.type === 'solution' && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<span className="rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
|
||||
<span className="rounded-full bg-green-900/30 px-2 py-1 text-xs font-medium text-green-400">
|
||||
Solution
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentNode.title}
|
||||
</h2>
|
||||
{currentNode.description && (
|
||||
<div className="mb-4 text-muted-foreground">
|
||||
<div className="mb-4 text-white/60">
|
||||
<MarkdownContent content={currentNode.description} />
|
||||
</div>
|
||||
)}
|
||||
{currentNode.resolution_steps && currentNode.resolution_steps.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Resolution steps:</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-sm text-muted-foreground">
|
||||
<p className="mb-2 text-sm font-medium text-white">Resolution steps:</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-sm text-white/40">
|
||||
{currentNode.resolution_steps.map((step, index) => (
|
||||
<li key={index}>{step}</li>
|
||||
))}
|
||||
@@ -660,8 +659,8 @@ export function TreeNavigationPage() {
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div className="mt-6 border-t border-border pt-4">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<div className="mt-6 border-t border-white/[0.06] pt-4">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Notes (optional)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -670,9 +669,9 @@ export function TreeNavigationPage() {
|
||||
placeholder="Add any notes for this step..."
|
||||
rows={2}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -681,7 +680,7 @@ export function TreeNavigationPage() {
|
||||
{pathTaken.length > 1 && (
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="mt-4 text-sm text-muted-foreground hover:text-foreground"
|
||||
className="mt-4 text-sm text-white/50 hover:text-white"
|
||||
>
|
||||
← Go back
|
||||
</button>
|
||||
@@ -689,7 +688,7 @@ export function TreeNavigationPage() {
|
||||
|
||||
{/* Keyboard Shortcuts Hint */}
|
||||
{currentNode && (
|
||||
<div className="mt-4 border-t border-border pt-3 text-xs text-muted-foreground">
|
||||
<div className="mt-4 border-t border-white/[0.06] pt-3 text-xs text-white/40">
|
||||
<span className="font-medium">Keyboard:</span>{' '}
|
||||
{currentNode.type === 'decision' && currentOptions.length > 0 && (
|
||||
<span>1-{Math.min(currentOptions.length, 9)} select option</span>
|
||||
|
||||
@@ -77,16 +77,16 @@ export function TeamCategoriesPage() {
|
||||
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
|
||||
}
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
|
||||
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="font-heading text-2xl font-bold text-foreground">Team Categories</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Manage tree categories for your team</p>
|
||||
<h1 className="text-2xl font-bold text-white">Team Categories</h1>
|
||||
<p className="mt-1 text-sm text-white/40">Manage tree categories for your team</p>
|
||||
</div>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Category
|
||||
</button>
|
||||
@@ -95,30 +95,30 @@ export function TeamCategoriesPage() {
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-white/10" />
|
||||
))}
|
||||
</div>
|
||||
) : categories.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-border bg-card py-16">
|
||||
<FolderTree className="h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 className="mt-4 font-medium text-foreground">No team categories</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Create categories to organize your team's trees.</p>
|
||||
<div className="flex flex-col items-center justify-center glass-card rounded-2xl py-16">
|
||||
<FolderTree className="h-12 w-12 text-white/30" />
|
||||
<h3 className="mt-4 font-medium text-white">No team categories</h3>
|
||||
<p className="mt-1 text-sm text-white/40">Create categories to organize your team's trees.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{categories.map((cat) => (
|
||||
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-border bg-card px-4 py-3">
|
||||
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-white/[0.06] px-4 py-3">
|
||||
<div>
|
||||
<span className="font-medium text-foreground">{cat.name}</span>
|
||||
<span className="ml-3 text-sm text-muted-foreground">{cat.slug}</span>
|
||||
{cat.description && <span className="ml-3 text-sm text-muted-foreground">- {cat.description}</span>}
|
||||
<span className="ml-3 text-xs text-muted-foreground">{cat.tree_count} trees</span>
|
||||
<span className="font-medium text-white">{cat.name}</span>
|
||||
<span className="ml-3 text-sm text-white/40">{cat.slug}</span>
|
||||
{cat.description && <span className="ml-3 text-sm text-white/40">- {cat.description}</span>}
|
||||
<span className="ml-3 text-xs text-white/40">{cat.tree_count} trees</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground">
|
||||
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-white/50 hover:bg-white/[0.06] hover:text-white">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive">
|
||||
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-white/50 hover:bg-red-400/10 hover:text-red-400">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -131,22 +131,22 @@ export function TeamCategoriesPage() {
|
||||
<Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Category" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Description</label>
|
||||
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,22 +156,22 @@ export function TeamCategoriesPage() {
|
||||
<Modal isOpen={!!editCategory} onClose={() => setEditCategory(null)} title="Edit Category" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Save</button>
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Save</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Description</label>
|
||||
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ export function AuditLogsPage() {
|
||||
render: (log) => (
|
||||
<button
|
||||
onClick={() => setExpandedId(expandedId === log.id ? null : log.id)}
|
||||
className="p-1 text-muted-foreground hover:text-foreground"
|
||||
className="p-1 text-white/50 hover:text-white"
|
||||
>
|
||||
{expandedId === log.id ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -78,14 +78,14 @@ export function AuditLogsPage() {
|
||||
key: 'action',
|
||||
header: 'Action',
|
||||
render: (log) => (
|
||||
<span className="text-sm font-medium text-foreground">{log.action}</span>
|
||||
<span className="text-sm font-medium text-white">{log.action}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'resource',
|
||||
header: 'Resource',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{log.resource_type}{log.resource_id ? ` (${log.resource_id.slice(0, 8)}...)` : ''}
|
||||
</span>
|
||||
),
|
||||
@@ -94,14 +94,14 @@ export function AuditLogsPage() {
|
||||
key: 'user',
|
||||
header: 'User',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">{log.user_email || 'System'}</span>
|
||||
<span className="text-sm text-white/40">{log.user_email || 'System'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
header: 'Time',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{new Date(log.created_at).toLocaleString()}
|
||||
</span>
|
||||
),
|
||||
@@ -117,8 +117,8 @@ export function AuditLogsPage() {
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium',
|
||||
'text-card-foreground hover:bg-accent'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
@@ -134,8 +134,8 @@ export function AuditLogsPage() {
|
||||
onChange={(e) => { setActionFilter(e.target.value); setPage(1) }}
|
||||
placeholder="Filter by action..."
|
||||
className={cn(
|
||||
'h-9 rounded-md border border-border bg-background px-3 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'h-9 rounded-md border border-white/10 bg-black/50 px-3 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
@@ -144,8 +144,8 @@ export function AuditLogsPage() {
|
||||
onChange={(e) => { setResourceFilter(e.target.value); setPage(1) }}
|
||||
placeholder="Filter by resource type..."
|
||||
className={cn(
|
||||
'h-9 rounded-md border border-border bg-background px-3 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'h-9 rounded-md border border-white/10 bg-black/50 px-3 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,9 +166,9 @@ export function AuditLogsPage() {
|
||||
|
||||
{/* Expanded details row */}
|
||||
{expandedId && logs.find(l => l.id === expandedId)?.details && (
|
||||
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-foreground">Details</h4>
|
||||
<pre className="overflow-x-auto rounded bg-muted p-3 text-xs text-muted-foreground">
|
||||
<div className="rounded-md border border-white/[0.06] bg-white/[0.02] p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-white">Details</h4>
|
||||
<pre className="overflow-x-auto rounded bg-black/50 p-3 text-xs text-white/40">
|
||||
{JSON.stringify(logs.find(l => l.id === expandedId)?.details, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -14,13 +14,13 @@ interface MetricCardProps {
|
||||
|
||||
function MetricCard({ label, value, icon }: MetricCardProps) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="glass-card rounded-2xl p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 text-3xl font-bold text-foreground">{value}</p>
|
||||
<p className="text-sm text-white/40">{label}</p>
|
||||
<p className="mt-1 text-3xl font-bold text-white">{value}</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/50 p-3 text-muted-foreground">{icon}</div>
|
||||
<div className="rounded-lg bg-white/[0.06] p-3 text-white/50">{icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -56,7 +56,7 @@ export function DashboardPage() {
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-32 animate-pulse rounded-lg bg-muted" />
|
||||
<div key={i} className="h-32 animate-pulse rounded-lg bg-white/10" />
|
||||
))}
|
||||
</div>
|
||||
) : metrics && (
|
||||
@@ -71,18 +71,18 @@ export function DashboardPage() {
|
||||
{/* Recent Activity */}
|
||||
{activity.length > 0 && (
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Recent Activity</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Recent Activity</h2>
|
||||
<div className="mt-3 space-y-2">
|
||||
{activity.slice(0, 10).map((entry) => (
|
||||
<div key={entry.id} className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3 text-sm">
|
||||
<div key={entry.id} className="flex items-center justify-between rounded-md border border-white/[0.06] px-4 py-3 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-foreground">{entry.action}</span>
|
||||
<span className="ml-2 text-muted-foreground">{entry.resource_type}</span>
|
||||
<span className="font-medium text-white">{entry.action}</span>
|
||||
<span className="ml-2 text-white/40">{entry.resource_type}</span>
|
||||
{entry.user_email && (
|
||||
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
|
||||
<span className="ml-2 text-white/40">by {entry.user_email}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-white/40">
|
||||
{new Date(entry.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -93,18 +93,18 @@ export function DashboardPage() {
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Quick Links</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Quick Links</h2>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{quickLinks.map((link) => (
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg border border-border bg-card p-4',
|
||||
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
|
||||
'flex items-center gap-3 glass-card rounded-2xl p-4',
|
||||
'text-sm font-medium text-white transition-colors hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
<link.icon className="h-5 w-5 text-muted-foreground" />
|
||||
<link.icon className="h-5 w-5 text-white/50" />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -93,11 +93,11 @@ export function FeatureFlagsPage() {
|
||||
const flagColumns: Column<FeatureFlagResponse>[] = [
|
||||
{ key: 'name', header: 'Name', render: (f) => (
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{f.display_name}</div>
|
||||
<div className="text-xs text-muted-foreground">{f.flag_key}</div>
|
||||
<div className="font-medium text-white">{f.display_name}</div>
|
||||
<div className="text-xs text-white/40">{f.flag_key}</div>
|
||||
</div>
|
||||
)},
|
||||
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-muted-foreground">{f.description || '-'}</span> },
|
||||
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-white/40">{f.description || '-'}</span> },
|
||||
...PLANS.map(plan => ({
|
||||
key: plan,
|
||||
header: plan.charAt(0).toUpperCase() + plan.slice(1),
|
||||
@@ -109,7 +109,7 @@ export function FeatureFlagsPage() {
|
||||
onClick={() => handleTogglePlan(f.id, plan, enabled)}
|
||||
className={cn(
|
||||
'h-6 w-10 rounded-full transition-colors',
|
||||
enabled ? 'bg-green-500' : 'bg-muted'
|
||||
enabled ? 'bg-emerald-400' : 'bg-white/10'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
@@ -131,10 +131,10 @@ export function FeatureFlagsPage() {
|
||||
]
|
||||
|
||||
const overrideColumns: Column<AccountFeatureOverrideResponse>[] = [
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-muted-foreground">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-white">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-white/40">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
|
||||
{ key: 'enabled', header: 'Enabled', render: (o) => <StatusBadge variant={o.enabled ? 'success' : 'destructive'}>{o.enabled ? 'Yes' : 'No'}</StatusBadge> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-white/40">{o.note || '-'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (o) => (
|
||||
@@ -145,7 +145,7 @@ export function FeatureFlagsPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
|
||||
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -153,7 +153,7 @@ export function FeatureFlagsPage() {
|
||||
title="Feature Flags"
|
||||
description="Manage feature availability per plan and account"
|
||||
action={
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Flag
|
||||
</button>
|
||||
@@ -161,7 +161,7 @@ export function FeatureFlagsPage() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Feature Matrix</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Feature Matrix</h2>
|
||||
<div className="mt-3">
|
||||
<DataTable columns={flagColumns} data={flags} keyExtractor={(f) => f.id} isLoading={loading}
|
||||
emptyState={<EmptyState icon={<ToggleLeft className="h-12 w-12" />} title="No feature flags" description="Create feature flags to control availability per plan." />}
|
||||
@@ -171,8 +171,8 @@ export function FeatureFlagsPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Account Overrides</h2>
|
||||
<button onClick={() => setOverrideOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<h2 className="text-lg font-semibold text-white">Account Overrides</h2>
|
||||
<button onClick={() => setOverrideOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Override
|
||||
</button>
|
||||
@@ -188,22 +188,22 @@ export function FeatureFlagsPage() {
|
||||
<Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Feature Flag" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Flag Key</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Flag Key</label>
|
||||
<input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Display Name</label>
|
||||
<input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Description</label>
|
||||
<input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,29 +213,29 @@ export function FeatureFlagsPage() {
|
||||
<Modal isOpen={overrideOpen} onClose={() => setOverrideOpen(false)} title="Add Account Override" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
|
||||
<input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Feature Flag</label>
|
||||
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={inputCn}>
|
||||
<option value="">Select a flag...</option>
|
||||
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-border" />
|
||||
<label htmlFor="override-enabled" className="text-sm font-medium text-foreground">Enabled</label>
|
||||
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-white/10" />
|
||||
<label htmlFor="override-enabled" className="text-sm font-medium text-white">Enabled</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Note</label>
|
||||
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,10 +72,10 @@ export function GlobalCategoriesPage() {
|
||||
}
|
||||
|
||||
const columns: Column<AdminCategory>[] = [
|
||||
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-foreground">{c.name}</span> },
|
||||
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-muted-foreground">{c.slug}</span> },
|
||||
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-muted-foreground">{c.description || '-'}</span> },
|
||||
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-muted-foreground">{c.tree_count}</span> },
|
||||
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-white">{c.name}</span> },
|
||||
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-white/40">{c.slug}</span> },
|
||||
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-white/40">{c.description || '-'}</span> },
|
||||
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-white/40">{c.tree_count}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (c) => (
|
||||
@@ -87,7 +87,7 @@ export function GlobalCategoriesPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
|
||||
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -95,7 +95,7 @@ export function GlobalCategoriesPage() {
|
||||
title="Global Categories"
|
||||
description="Manage tree categories available to all accounts"
|
||||
action={
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Category
|
||||
</button>
|
||||
@@ -118,22 +118,22 @@ export function GlobalCategoriesPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Description</label>
|
||||
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,22 +147,22 @@ export function GlobalCategoriesPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Save</button>
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Save</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Description</label>
|
||||
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function InviteCodesPage() {
|
||||
key: 'code',
|
||||
header: 'Code',
|
||||
render: (c) => (
|
||||
<code className="rounded bg-muted px-2 py-1 text-sm font-mono">{c.code}</code>
|
||||
<code className="rounded bg-white/10 px-2 py-1 text-sm font-mono text-white/70">{c.code}</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -89,7 +89,7 @@ export function InviteCodesPage() {
|
||||
key: 'expires_at',
|
||||
header: 'Expires',
|
||||
render: (c) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{c.expires_at ? new Date(c.expires_at).toLocaleDateString() : 'Never'}
|
||||
</span>
|
||||
),
|
||||
@@ -98,7 +98,7 @@ export function InviteCodesPage() {
|
||||
key: 'created_at',
|
||||
header: 'Created',
|
||||
render: (c) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{new Date(c.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
@@ -135,7 +135,7 @@ export function InviteCodesPage() {
|
||||
onClick={() => setCreateOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
'bg-white text-black hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -167,13 +167,13 @@ export function InviteCodesPage() {
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setCreateOpen(false)}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
@@ -182,15 +182,15 @@ export function InviteCodesPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Expires in (days)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={expiresInDays}
|
||||
onChange={(e) => setExpiresInDays(e.target.value)}
|
||||
placeholder="Leave empty for no expiry"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -75,16 +75,16 @@ export function PlanLimitsPage() {
|
||||
}
|
||||
|
||||
const planColumns: Column<PlanLimitConfig>[] = [
|
||||
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-foreground capitalize">{p.plan}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-muted-foreground">{p.max_trees ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-muted-foreground">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-muted-foreground">{p.max_users ?? 'Unlimited'}</span> },
|
||||
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-white capitalize">{p.plan}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-white/40">{p.max_trees ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-white/40">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-white/40">{p.max_users ?? 'Unlimited'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (p) => (
|
||||
<button
|
||||
onClick={() => setEditPlan({ ...p })}
|
||||
className="rounded-md px-3 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded-md px-3 py-1 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -93,11 +93,11 @@ export function PlanLimitsPage() {
|
||||
]
|
||||
|
||||
const overrideColumns: Column<AccountOverrideResponse>[] = [
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_trees ?? '-'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_sessions_per_month ?? '-'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_users ?? '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-white">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-white/40">{o.override_max_trees ?? '-'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-white/40">{o.override_max_sessions_per_month ?? '-'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-white/40">{o.override_max_users ?? '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-white/40">{o.note || '-'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (o) => (
|
||||
@@ -109,8 +109,8 @@ export function PlanLimitsPage() {
|
||||
]
|
||||
|
||||
const inputCn = cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -118,7 +118,7 @@ export function PlanLimitsPage() {
|
||||
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
|
||||
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Plan Defaults</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Plan Defaults</h2>
|
||||
<div className="mt-3">
|
||||
<DataTable columns={planColumns} data={plans} keyExtractor={(p) => p.plan} isLoading={loading} />
|
||||
</div>
|
||||
@@ -126,10 +126,10 @@ export function PlanLimitsPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Account Overrides</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Account Overrides</h2>
|
||||
<button
|
||||
onClick={() => setCreateOverride(true)}
|
||||
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}
|
||||
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Override
|
||||
@@ -154,23 +154,23 @@ export function PlanLimitsPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setEditPlan(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleSavePlan} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save</button>
|
||||
<button onClick={() => setEditPlan(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleSavePlan} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90">Save</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{editPlan && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees (empty = unlimited)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Max Trees (empty = unlimited)</label>
|
||||
<input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Max Sessions/Month (empty = unlimited)</label>
|
||||
<input type="number" value={editPlan.max_sessions_per_month ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Max Users (empty = unlimited)</label>
|
||||
<input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,30 +185,30 @@ export function PlanLimitsPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
|
||||
<input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Max Trees Override</label>
|
||||
<input type="number" value={overrideForm.override_max_trees ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_trees: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Max Sessions/Month Override</label>
|
||||
<input type="number" value={overrideForm.override_max_sessions_per_month ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Max Users Override</label>
|
||||
<input type="number" value={overrideForm.override_max_users ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Note</label>
|
||||
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function SettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="Platform Settings" description="Global platform configuration" />
|
||||
<div className="h-40 animate-pulse rounded-lg bg-muted" />
|
||||
<div className="h-40 animate-pulse rounded-lg bg-white/10" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -45,11 +45,11 @@ export function SettingsPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="Platform Settings" description="Global platform configuration" />
|
||||
|
||||
<div className="max-w-xl space-y-6 rounded-lg border border-border bg-card p-6">
|
||||
<div className="max-w-xl space-y-6 glass-card rounded-2xl p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h3 className="font-medium text-white">Maintenance Mode</h3>
|
||||
<p className="text-sm text-white/40">
|
||||
When enabled, users will see a maintenance message instead of the app.
|
||||
</p>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export function SettingsPage() {
|
||||
onClick={() => setSettings({ ...settings, maintenance_mode: !maintenanceMode })}
|
||||
className={cn(
|
||||
'h-6 w-10 rounded-full transition-colors',
|
||||
maintenanceMode ? 'bg-destructive' : 'bg-muted'
|
||||
maintenanceMode ? 'bg-red-400' : 'bg-white/10'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
@@ -69,27 +69,27 @@ export function SettingsPage() {
|
||||
|
||||
{maintenanceMode && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Maintenance Message</label>
|
||||
<textarea
|
||||
value={maintenanceMessage}
|
||||
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="We're performing scheduled maintenance. Please check back later."
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<div className="border-t border-white/[0.06] pt-4">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-2 text-sm font-medium',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -98,8 +98,8 @@ export function UsersPage() {
|
||||
sortable: true,
|
||||
render: (u) => (
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{u.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.email}</div>
|
||||
<div className="font-medium text-white">{u.name}</div>
|
||||
<div className="text-xs text-white/40">{u.email}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -129,7 +129,7 @@ export function UsersPage() {
|
||||
header: 'Joined',
|
||||
sortable: true,
|
||||
render: (u) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{new Date(u.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
@@ -197,13 +197,13 @@ export function UsersPage() {
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setRoleModalUser(null)}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRoleChange}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -211,15 +211,15 @@ export function UsersPage() {
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Changing role for <span className="font-medium text-foreground">{roleModalUser?.name}</span>
|
||||
<p className="text-sm text-white/70">
|
||||
Changing role for <span className="font-medium text-white">{roleModalUser?.name}</span>
|
||||
</p>
|
||||
<select
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="engineer">Engineer</option>
|
||||
@@ -238,14 +238,14 @@ export function UsersPage() {
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setMoveModalUser(null)}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveAccount}
|
||||
disabled={!displayCode}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
@@ -253,19 +253,19 @@ export function UsersPage() {
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Moving <span className="font-medium text-foreground">{moveModalUser?.name}</span> to a new account.
|
||||
<p className="text-sm text-white/70">
|
||||
Moving <span className="font-medium text-white">{moveModalUser?.name}</span> to a new account.
|
||||
</p>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
|
||||
<input
|
||||
type="text"
|
||||
value={displayCode}
|
||||
onChange={(e) => setDisplayCode(e.target.value)}
|
||||
placeholder="e.g. ABC-1234"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
@@ -8,25 +7,7 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// ResolutionFlow Brand Colors
|
||||
brand: {
|
||||
gradient: {
|
||||
from: '#818cf8',
|
||||
to: '#a78bfa',
|
||||
},
|
||||
dark: {
|
||||
DEFAULT: '#09090b',
|
||||
card: '#18181b',
|
||||
surface: '#12121c',
|
||||
},
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#a1a1aa',
|
||||
muted: '#52525b',
|
||||
},
|
||||
border: '#27272a',
|
||||
},
|
||||
// shadcn/ui color system
|
||||
// shadcn/ui color system (monochrome)
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
@@ -68,12 +49,6 @@ export default {
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
heading: ['Plus Jakarta Sans', 'system-ui', 'sans-serif'],
|
||||
label: ['Outfit', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-brand': 'linear-gradient(90deg, #818cf8 0%, #a78bfa 100%)',
|
||||
'gradient-brand-hover': 'linear-gradient(90deg, #6366f1 0%, #9333ea 100%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user