refactor: Design System v4 — flat dark theme with icon rail sidebar #119
66
CLAUDE.md
66
CLAUDE.md
@@ -19,29 +19,29 @@
|
||||
| Repository / directory / database / Docker | `patherly` / `patherly_postgres` |
|
||||
| Backend, frontend UI, production URLs | **ResolutionFlow** |
|
||||
|
||||
- **Design:** Dark glassmorphism with ice-cyan accent gradient (`#06b6d4` → `#22d3ee`). Charcoal backgrounds, frosted-glass cards with `backdrop-filter: blur()`, orchestrated page-load animations, bold display typography. Design doc: [docs/plans/2026-03-03-aesthetic-redesign-design.md](docs/plans/2026-03-03-aesthetic-redesign-design.md)
|
||||
- **Fonts:** Bricolage Grotesque (`font-heading`, headings/titles), IBM Plex Sans (`font-sans`, body text), JetBrains Mono (`font-label`, labels/badges/timestamps) — loaded via Google Fonts
|
||||
- **Logo:** Inline SVG in `BrandLogo.tsx` (decision-tree icon with cyan gradient). Wordmark: "Resolution" in `text-foreground` + "Flow" in `text-gradient-brand`
|
||||
- **Brand assets:** `brand-assets/` (source SVGs + brand-guide.html), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
|
||||
- **CSS utilities:** `text-gradient-brand`, `bg-gradient-brand`, `bg-gradient-brand-hover` (defined in `index.css` via `@theme`). Glass utilities: `.glass-card` (interactive, `scale(1.02)` hover), `.glass-card-static` (no hover transform), `.active-glow` (breathing cyan shadow)
|
||||
- **Layout:** App shell with persistent sidebar + top bar + main content (CSS Grid). Two fixed atmosphere orbs (cyan top-right, purple bottom-left) behind the shell for ambient glow. See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
|
||||
- **Navigation:** Sidebar nav with type sub-items (All Flows → Troubleshooting / Projects / Maintenance). Pinned flows section for quick access. NO workspace switcher. See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
|
||||
- **Design system:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — THE source of truth for all design decisions
|
||||
- **Design aesthetic:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no gradients on surfaces, no ambient effects. Light mode planned.
|
||||
- **Accent color:** Cyan (#22d3ee / #06b6d4). Used sparingly — ≤5% of the UI.
|
||||
- **Fonts:** IBM Plex Sans (`font-sans`, body), Bricolage Grotesque (`font-heading`, headings), JetBrains Mono (`font-mono`, code) — loaded via Google Fonts
|
||||
- **Logo:** 30px gradient square (cyan) + "ResolutionFlow" in Bricolage Grotesque 700
|
||||
- **Layout:** Icon rail sidebar (72px default) with hover flyout panels. Pinnable to full 260px sidebar. See [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md)
|
||||
- **Brand assets:** `brand-assets/` (source SVGs), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
|
||||
- **Terminology:** User-facing label is "Flows" (not "Trees"). Procedural flows are called "Projects" in the UI. Maintenance flows are called "Maintenance" in the UI. `tree_type` column values unchanged in DB.
|
||||
- **Rebrand guide:** [REBRAND-IMPLEMENTATION-GUIDE.md](docs/archive/REBRAND-IMPLEMENTATION-GUIDE.md)
|
||||
- **Reference mockups:** `docs/mockups/` (HTML files, open in browser)
|
||||
|
||||
**Component styling rules:**
|
||||
|
||||
- Primary buttons: `bg-gradient-brand` (cyan `135deg`) with `shadow-lg shadow-primary/20`, hover `opacity-0.9`, active `scale(0.97)`
|
||||
- Secondary buttons: `bg-[rgba(255,255,255,0.04)]` with `border-[rgba(255,255,255,0.06)]`, hover brightens border
|
||||
- Active nav items: `bg-primary/10` background + 3px left cyan gradient accent bar
|
||||
- Stat values: use `text-gradient-brand` for highlighted metrics
|
||||
- Status colors: emerald-400 (success), amber-400 (in-progress), rose-500 (error/critical)
|
||||
- Category dots: 8px colored circles using the category color palette
|
||||
- Tags/badges: `font-label` (JetBrains Mono), small rounded chips with `bg-card border-border`
|
||||
- Cards: `.glass-card` (interactive) or `.glass-card-static` (non-interactive) — semi-transparent bg with `backdrop-filter: blur(16px)`, `border-radius: 16px`
|
||||
- Section labels: `font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground`
|
||||
- Primary buttons: solid `accent` background (#22d3ee), white text, 5px radius
|
||||
- Ghost buttons: transparent with 1px `border-default`, hover `bg-elevated`
|
||||
- Cards: `bg-card` with 1px `border-default`, 8px radius. NO shadows, NO blur, NO gradients.
|
||||
- Badges: pill-shaped (20px radius), semantic dim background + matching text color
|
||||
- Active nav: `accent-dim` background + `accent-text` color + 3px left accent bar
|
||||
- Stat cards: 3px colored left border (accent/success/warning by position)
|
||||
- Code blocks: `bg-code` with JetBrains Mono, material-inspired syntax highlighting
|
||||
- Status colors: green/`#34d399` (success), amber/`#fbbf24` (warning), red/`#f87171` (danger) — ONLY for semantic meaning
|
||||
- Section labels: 10px, 600 weight, uppercase, `text-muted`, 1.2px letter-spacing
|
||||
|
||||
When adding new pages/components: use "ResolutionFlow" branding, ice-cyan gradient accent theme, `.glass-card` / `.glass-card-static` containers, `text-foreground`/`text-muted-foreground` hierarchy. Primary actions use `bg-gradient-brand`. Pages render inside the app shell (CSS Grid: topbar + sidebar + main). Use "Flows" not "Trees" in all user-facing text; use "Projects" not "Procedures" for procedural flows. Reference [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) for layout patterns, navigation, and component specs.
|
||||
When adding new pages/components: reference [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md). Use flat dark surfaces, 1px borders, no decorative effects. All colors via CSS variables. Use "Flows" not "Trees" in all user-facing text; use "Projects" not "Procedures" for procedural flows.
|
||||
|
||||
## Implementation Principles
|
||||
|
||||
@@ -97,7 +97,7 @@ When adding new pages/components: use "ResolutionFlow" branding, ice-cyan gradie
|
||||
### Frontend
|
||||
|
||||
- **Framework:** React 19 + Vite + TypeScript
|
||||
- **Styling:** Tailwind CSS v4 (`@tailwindcss/vite` plugin, CSS-only config in `index.css`) — dark-first with ice-cyan gradient accents (see Branding section)
|
||||
- **Styling:** Tailwind CSS v4 (`@tailwindcss/vite` plugin, CSS-only config in `index.css`) — flat dark theme with cyan accent (see [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md))
|
||||
- **State:** Zustand (with immer + zundo for undo/redo)
|
||||
- **Routing:** React Router v7
|
||||
- **API Client:** Axios with token refresh interceptor
|
||||
@@ -379,20 +379,20 @@ gh run view <id> --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
|
||||
|
||||
---
|
||||
|
||||
## Design System (Slate & Ice Modern)
|
||||
## Design System
|
||||
|
||||
- **Theme:** Dark glassmorphism with ice-cyan accent (`#06b6d4` → `#22d3ee`). Uses `.glass-card` / `.glass-card-static` for card surfaces
|
||||
- **Backgrounds:** `bg-background` (`#101114` page), glass surfaces use `rgba(24, 26, 31, 0.55)` with `backdrop-filter: blur()`
|
||||
- **Cards:** `.glass-card` (interactive, hover `scale(1.02)` + border/shadow upgrade) or `.glass-card-static` (no hover). Both have `border-radius: 16px`, semi-transparent bg, backdrop blur
|
||||
- **Buttons:** Primary: `bg-gradient-brand text-[#101114] font-semibold rounded-[10px] hover:opacity-90 active:scale-[0.97]`. Secondary: `bg-[rgba(255,255,255,0.04)] border-[rgba(255,255,255,0.06)] text-foreground rounded-[10px]`
|
||||
- **Inputs:** `border-border bg-card text-foreground placeholder:text-muted-foreground` + focus: `focus:border-[rgba(6,182,212,0.3)]`
|
||||
- **Text:** `text-foreground` (`#f8fafc`) → `text-muted-foreground` (`#8891a0`) → `text-[#5a6170]` (dim, for section labels/timestamps)
|
||||
- **Borders:** `var(--glass-border)` (`rgba(255,255,255,0.06)`) default, `rgba(255,255,255,0.12)` on hover
|
||||
- **Hover states:** Border brightens to `rgba(255,255,255,0.12)`, shadow upgrades to `--shadow-float-hover`
|
||||
- **Active/selected:** `bg-primary/10 text-foreground` or cyan gradient accent bar
|
||||
- **Functional colors:** emerald-400 (success), rose-500 (error), amber-400 (warning), blue-400 (info). Always pair with icons, not color alone.
|
||||
- **CSS variables:** Glass system vars (`--glass-bg`, `--glass-border`, `--glass-blur`), shadow system (`--shadow-float`, `--shadow-float-hover`, `--shadow-cyan-glow`), easing (`--ease-out-smooth`) — all in `index.css` `:root`
|
||||
- **Animations:** Orchestrated page-load sequence (slideDown, slideInLeft, fadeInUp cascade, fadeInRight). `breatheGlow` on first stat card. `bellWobble` on notification hover. See design doc for full spec.
|
||||
**Source of truth:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — always read this before making visual or UI decisions.
|
||||
|
||||
- **Theme:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. Light mode planned.
|
||||
- **Backgrounds:** `bg-page` (`#0c0d10`), `bg-sidebar` (`#0f1118`), `bg-card` (`#14161d`), `bg-elevated` (`#1c1f2a`)
|
||||
- **Cards:** `bg-card` with 1px `border-default` (`#1e2130`), 8px radius. No shadows, no blur, no gradients. Hover: `border-hover` (`#2a2f3d`)
|
||||
- **Buttons:** Primary: solid `accent` (#22d3ee), white text, 5px radius. Ghost: transparent + 1px border, hover `bg-elevated`
|
||||
- **Inputs:** `bg-input` (`#191c25`) with 1px `border-default`, 5px radius. Focus: `border-color: accent` + `box-shadow: 0 0 0 2px accent-dim`
|
||||
- **Text:** `text-heading` (`#f0f2f5`) → `text-primary` (`#e2e5eb`) → `text-secondary` (`#848b9b`) → `text-muted` (`#4f5666`)
|
||||
- **Borders:** `border-default` (`#1e2130`), `border-hover` (`#2a2f3d`)
|
||||
- **Functional colors:** `#34d399` (success), `#fbbf24` (warning), `#f87171` (danger) — each with `-dim` variant at 10% opacity
|
||||
- **Accent:** Cyan `#22d3ee` — used sparingly (≤5% of UI). `accent-dim` = `rgba(34,211,238,0.10)`, `accent-text` = `#67e8f9`
|
||||
- **Deprecated:** Do NOT use `glass-card`, `glass-stat`, `bg-gradient-brand`, `text-gradient-brand`, `backdrop-filter: blur()`, ambient orbs, or purple gradients
|
||||
|
||||
---
|
||||
|
||||
@@ -518,5 +518,5 @@ Use `/browse` from gstack for **all web browsing** — never use `mcp__claude-in
|
||||
| GitHub Issues | `gh issue list --state open` |
|
||||
| Bugs & Fixes | CLAUDE.md → Critical Lessons Learned section |
|
||||
| Feature Specs | [04-FEATURE-SPECIFICATIONS.md](04-FEATURE-SPECIFICATIONS.md) |
|
||||
| Design System | [docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md](docs/plans/Frontend/DESIGN_SYSTEM_GUIDE.md) |
|
||||
| Design System | [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) |
|
||||
| Dev Environment | [DEV-ENV.md](DEV-ENV.md) — devserver01 setup, Docker, CORS, networking |
|
||||
|
||||
404
DESIGN-SYSTEM.md
Normal file
404
DESIGN-SYSTEM.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# ResolutionFlow Design System v4
|
||||
|
||||
> **Status:** ACTIVE — This document is the single source of truth for all frontend design decisions.
|
||||
> **Supersedes:** All previous design system docs including `DESIGN_SYSTEM_GUIDE.md`, `UI-DESIGN-SYSTEM.md`, `REBRAND-IMPLEMENTATION-GUIDE.md`, and any `COMPONENT_EXAMPLES.md` files.
|
||||
> **Last Updated:** March 21, 2026
|
||||
|
||||
---
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
ResolutionFlow uses a **flat, high-contrast dark theme** inspired by Sentry and PostHog. The aesthetic is premium, clean, and minimal — no glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. The accent color appears in ≤5% of the UI. Every design decision prioritizes **readability over decoration**.
|
||||
|
||||
**Light mode** is a planned addition (dark/light toggle). Design all components with CSS variables so theming is a variable swap, not a rewrite.
|
||||
|
||||
---
|
||||
|
||||
## Color System
|
||||
|
||||
All colors are defined as CSS custom properties in `index.css` inside the `@theme` block (Tailwind v4) or `:root` / `.dark` blocks (Tailwind v3).
|
||||
|
||||
### Dark Mode (Default)
|
||||
|
||||
```
|
||||
Page background: #0c0d10
|
||||
Sidebar background: #0f1118
|
||||
Card background: #14161d
|
||||
Card hover: #191c25
|
||||
Input background: #191c25
|
||||
Code background: #0e1017
|
||||
Elevated surface: #1c1f2a
|
||||
|
||||
Text primary: #e2e5eb
|
||||
Text heading: #f0f2f5
|
||||
Text secondary: #848b9b
|
||||
Text muted: #4f5666
|
||||
Text rail label: #6b7280
|
||||
|
||||
Border default: #1e2130
|
||||
Border hover: #2a2f3d
|
||||
|
||||
Accent (cyan): #22d3ee
|
||||
Accent hover: #06b6d4
|
||||
Accent dim (8-10%): rgba(34,211,238,0.10)
|
||||
Accent text: #67e8f9
|
||||
|
||||
Success: #34d399
|
||||
Success dim: rgba(52,211,153,0.10)
|
||||
Warning: #fbbf24
|
||||
Warning dim: rgba(251,191,36,0.10)
|
||||
Danger: #f87171
|
||||
Danger dim: rgba(248,113,113,0.10)
|
||||
```
|
||||
|
||||
### Light Mode (Planned)
|
||||
|
||||
```
|
||||
Page background: #f3f4f7
|
||||
Sidebar background: #ffffff
|
||||
Card background: #ffffff
|
||||
Card hover: #f8f9fb
|
||||
Input background: #eef0f4
|
||||
Code background: #f5f6f9
|
||||
Elevated surface: #e8eaef
|
||||
|
||||
Text primary: #1a1d24
|
||||
Text heading: #0d0f13
|
||||
Text secondary: #5a6274
|
||||
Text muted: #8b92a1
|
||||
|
||||
Border default: #dde0e7
|
||||
Border hover: #c5c9d3
|
||||
|
||||
Accent: #0891b2
|
||||
Accent dim: rgba(8,145,178,0.07)
|
||||
Accent text: #0e7490
|
||||
```
|
||||
|
||||
### What NOT To Use
|
||||
|
||||
- No glass morphism (`backdrop-filter: blur`)
|
||||
- No gradient backgrounds on cards or surfaces
|
||||
- No ambient orbs or floating glow elements
|
||||
- No `bg-white/[0.04]` opacity-based backgrounds
|
||||
- No purple gradient accent (`#818cf8 → #a78bfa`) — this is deprecated
|
||||
- No `text-gradient-brand` utility — replaced by solid `accent-text` color
|
||||
- No `glass-card`, `glass-stat`, `glass-card-glow` CSS utilities
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Stack
|
||||
|
||||
| Role | Font | Weights | CSS Variable |
|
||||
|------|------|---------|-------------|
|
||||
| Body text | IBM Plex Sans | 400, 500, 600, 700 | `--font-sans` / `font-sans` |
|
||||
| Headings | Bricolage Grotesque | 600, 700, 800 | `--font-heading` / `font-heading` |
|
||||
| Code / Monospace | JetBrains Mono | 400, 500 | `--font-mono` / `font-mono` |
|
||||
|
||||
### Typography Scale
|
||||
|
||||
| Element | Size | Weight | Color | Letter Spacing |
|
||||
|---------|------|--------|-------|---------------|
|
||||
| Page title (h1) | 22px | 700 (Bricolage) | text-heading | -0.03em |
|
||||
| Section title | 15px | 600 (Bricolage) | text-heading | normal |
|
||||
| Card title | 14px | 600 (IBM Plex) | text-heading | normal |
|
||||
| Body text | 14px | 400 (IBM Plex) | text-primary | normal |
|
||||
| Secondary text | 13px | 400 (IBM Plex) | text-secondary | normal |
|
||||
| Label / hint | 12px | 500 (IBM Plex) | text-muted | normal |
|
||||
| Nav section header | 10px | 600 (IBM Plex) | text-muted | 1.2px, uppercase |
|
||||
| Rail icon label | 10px | 500 (IBM Plex) | text-rail-label | normal |
|
||||
| Code | 12px | 400 (JetBrains) | text-primary | normal |
|
||||
| Stat number | 28px | 700 (Bricolage) | text-heading | -0.03em |
|
||||
| Badge | 11px | 550 (IBM Plex) | varies | normal |
|
||||
|
||||
### What NOT To Use
|
||||
|
||||
- No Inter, Plus Jakarta Sans, or Outfit — these are deprecated
|
||||
- No `font-label` utility — use `font-mono` for monospace or `font-sans` at small size
|
||||
- No font sizes below 10px
|
||||
|
||||
---
|
||||
|
||||
## Layout Architecture
|
||||
|
||||
### App Shell
|
||||
|
||||
The app uses a CSS Grid layout with two states:
|
||||
|
||||
**Icon Rail (Default):**
|
||||
```
|
||||
[icon-rail: 72px] [main-content: 1fr]
|
||||
```
|
||||
|
||||
**Pinned Sidebar:**
|
||||
```
|
||||
[full-sidebar: 260px] [main-content: 1fr]
|
||||
```
|
||||
|
||||
### Icon Rail Sidebar
|
||||
|
||||
The default navigation is a narrow icon rail (72px) with:
|
||||
- Logo mark at top (30px square, gradient background, lightning bolt icon)
|
||||
- Nav items as vertical column: icon (20px) + label text (10px) underneath
|
||||
- Dividers between nav sections
|
||||
- User avatar at bottom
|
||||
- Pin/expand button below avatar
|
||||
|
||||
**Hover behavior:** Hovering a rail item opens a flyout panel (220px) to the right of the rail with sub-navigation items for that section. The flyout stays open while the cursor is on either the rail item or the flyout panel.
|
||||
|
||||
**Pinned state:** Clicking the pin button expands to a traditional 260px sidebar with full text labels, section headers, badges, and the active left-bar accent. An unpin button in the header returns to the icon rail.
|
||||
|
||||
### Active Nav Indicator
|
||||
|
||||
- **Icon rail:** `accent-dim` background on the item, `accent-text` color on icon and label
|
||||
- **Pinned sidebar:** Same as above, plus a 3px left border (`border-radius: 0 3px 3px 0`) in `accent` color
|
||||
|
||||
### Main Content Area
|
||||
|
||||
- Padding: 28px top/bottom, 36px left/right
|
||||
- Max content width: not constrained (fills available space)
|
||||
- Page title: Bricolage Grotesque, 22px, weight 700
|
||||
|
||||
---
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Stat Cards
|
||||
|
||||
```
|
||||
Background: bg-card
|
||||
Border: 1px solid border-default
|
||||
Border-left: 3px solid [varies by position - accent, success, warning, accent]
|
||||
Border-radius: 8px
|
||||
Padding: 18px 16px
|
||||
```
|
||||
|
||||
- Label: 11px, uppercase, 600 weight, text-muted
|
||||
- Value: 28px, Bricolage Grotesque 700, text-heading
|
||||
- Delta: 12px, 500 weight, success/danger color with ↑/↓ prefix
|
||||
|
||||
### Cards
|
||||
|
||||
```
|
||||
Background: bg-card
|
||||
Border: 1px solid border-default
|
||||
Border-radius: 8px
|
||||
Padding: 20px
|
||||
Hover: border-color transitions to border-hover
|
||||
```
|
||||
|
||||
No shadows. No gradients. No glow effects.
|
||||
|
||||
### Flow List Items
|
||||
|
||||
```
|
||||
Layout: flex row, 12px gap
|
||||
Padding: 10px 8px
|
||||
Border-bottom: 1px solid border-default (divider style, not bordered cards)
|
||||
Hover: bg-card-hover
|
||||
```
|
||||
|
||||
- Icon: 34px square, border-radius 5px, semantic dim background + colored stroke
|
||||
- Name: 13px, 500 weight, text-heading
|
||||
- Meta: 11.5px, text-muted
|
||||
- Badge: right-aligned status badge
|
||||
|
||||
### Badges
|
||||
|
||||
```
|
||||
Font: 11px, 550 weight
|
||||
Padding: 3px 9px
|
||||
Border-radius: 20px (pill)
|
||||
```
|
||||
|
||||
| Type | Background | Text Color |
|
||||
|------|-----------|------------|
|
||||
| Info/Accent | accent-dim | accent-text |
|
||||
| Success | success-dim | success |
|
||||
| Warning | warning-dim | warning |
|
||||
| Danger | danger-dim | danger |
|
||||
|
||||
### Form Inputs
|
||||
|
||||
```
|
||||
Background: bg-input
|
||||
Border: 1px solid border-default
|
||||
Border-radius: 5px
|
||||
Padding: 9px 12px
|
||||
Font: 13px, IBM Plex Sans
|
||||
Focus: border-color accent, box-shadow 0 0 0 2px accent-dim
|
||||
Placeholder: text-muted
|
||||
```
|
||||
|
||||
### Buttons
|
||||
|
||||
**Primary:**
|
||||
```
|
||||
Background: accent (#22d3ee)
|
||||
Color: #fff
|
||||
Border: none
|
||||
Border-radius: 5px
|
||||
Padding: 9px 16px
|
||||
Font: 13px, 550 weight
|
||||
Hover: brightness(1.1)
|
||||
```
|
||||
|
||||
**Ghost:**
|
||||
```
|
||||
Background: transparent
|
||||
Color: text-secondary
|
||||
Border: 1px solid border-default
|
||||
Hover: bg-elevated, text-primary, border-hover
|
||||
```
|
||||
|
||||
### Code Blocks
|
||||
|
||||
```
|
||||
Background: bg-code (#0e1017)
|
||||
Border: 1px solid border-default
|
||||
Border-radius: 8px
|
||||
Padding: 18px 20px
|
||||
Font: JetBrains Mono, 12px, line-height 1.7
|
||||
```
|
||||
|
||||
**Syntax colors (dark mode):**
|
||||
|
||||
| Token | Color |
|
||||
|-------|-------|
|
||||
| Comment | #4a5568 |
|
||||
| Keyword | #c792ea |
|
||||
| Function/Cmdlet | #82aaff |
|
||||
| String | #c3e88d |
|
||||
| Variable | #89ddff |
|
||||
| Parameter | #8c93a4 |
|
||||
| Number | #f78c6c |
|
||||
|
||||
### Chips / Tags
|
||||
|
||||
```
|
||||
Font: 11.5px, 500 weight
|
||||
Padding: 4px 11px
|
||||
Border-radius: 20px
|
||||
Background: accent-dim
|
||||
Color: accent-text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logo
|
||||
|
||||
- **Mark:** 30-32px square, border-radius 8px, `linear-gradient(135deg, #06b6d4, #22d3ee)`, white lightning bolt SVG
|
||||
- **Wordmark:** "ResolutionFlow" in Bricolage Grotesque, 16-17px, weight 700, text-heading color
|
||||
- **Combined:** Mark + wordmark horizontally, 10px gap
|
||||
|
||||
---
|
||||
|
||||
## Spacing System
|
||||
|
||||
- Component internal gaps: 6px, 8px, 12px, 14px, 16px
|
||||
- Card padding: 20px
|
||||
- Section gaps: 24px between major sections
|
||||
- Page padding: 28px vertical, 36px horizontal
|
||||
- Stat grid gap: 12px
|
||||
- Two-column gap: 16px
|
||||
- Nav item padding: 8px 10px (pinned), 8px 4px (rail)
|
||||
|
||||
---
|
||||
|
||||
## Border Radius
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| radius-sm | 5px | Inputs, buttons, small elements |
|
||||
| radius | 8px | Cards, stat cards, code blocks |
|
||||
| radius-lg | 12px | Large cards, modals |
|
||||
| radius-xl | 16px | Hero elements, CTA boxes |
|
||||
| radius-pill | 100px | Badges, chips, toggle tracks |
|
||||
|
||||
---
|
||||
|
||||
## Shadows
|
||||
|
||||
Shadows communicate **interaction state**, not decoration. On dark backgrounds, traditional black shadows are invisible — use **brighter surfaces + faint accent glow** instead.
|
||||
|
||||
**Resting state:** No shadows. Elements are flat with 1px borders.
|
||||
|
||||
**Elevation on dark backgrounds (the principle):** Instead of shadow = darker, elevation = lighter. A "raised" element gets a brighter surface color (`bg-elevated` / `#1c1f2a`) and optionally a very faint cyan glow. This creates perceived depth through contrast.
|
||||
|
||||
**Hover state (buttons):** Lift effect with accent glow.
|
||||
|
||||
- Primary button hover: `0 2px 10px rgba(34,211,238,0.2)` + `translateY(-1px)` — cyan glow
|
||||
- Ghost button hover: brighter border (`border-hover`) + `translateY(-1px)`, no shadow
|
||||
- Active/click: glow fades, element "presses down" to `translateY(0)`
|
||||
|
||||
**Active/selected state (tabs, toggles):** Elevated surface + faint accent glow.
|
||||
|
||||
- Active tab: `bg-elevated` + `box-shadow: 0 1px 4px rgba(34,211,238,0.08)` — class: `tab-active-shadow`
|
||||
|
||||
**Card hover lift (optional):** For clickable cards.
|
||||
|
||||
- Hover: brighter border + `0 2px 8px rgba(34,211,238,0.06)` + `translateY(-2px)` — class: `card-lift`
|
||||
|
||||
**Overlays:** Flyouts, dropdowns, modals get stronger shadows (they overlay lighter content).
|
||||
|
||||
- Drawer/flyout: `4px 0 12px rgba(0,0,0,0.2)`
|
||||
- Dropdown: `0 4px 12px rgba(0,0,0,0.3)`
|
||||
- Modal: `0 8px 24px rgba(0,0,0,0.4)`
|
||||
|
||||
**What NOT to do:**
|
||||
|
||||
- No `rgba(0,0,0,...)` shadows on resting elements (invisible on dark bg)
|
||||
- No permanent decorative shadows
|
||||
- No heavy glow effects — accent glow should be barely perceptible (≤ 0.1 opacity)
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
All icons use Lucide React (`lucide-react` package).
|
||||
- Default size: 16-18px
|
||||
- Stroke width: 1.6-1.8
|
||||
- Color: `currentColor` (inherits from parent text color)
|
||||
- Nav icons: 20px at stroke-width 1.6
|
||||
- Rail icon opacity: 0.6 default, 0.85 on hover, 1.0 when active
|
||||
|
||||
---
|
||||
|
||||
## Landing Page
|
||||
|
||||
The marketing landing page at `/` shares the same color system and typography but has its own layout (no sidebar, full-width sections). Key differences:
|
||||
|
||||
- Navigation: fixed top bar with logo, links, and CTAs
|
||||
- Hero: centered layout with glow effect (radial gradient, accent at 15% opacity)
|
||||
- Sections: 100px vertical padding, max-width 1120px container
|
||||
- Section labels: JetBrains Mono, 12px, uppercase, accent-text color
|
||||
- Section titles: Bricolage Grotesque, clamp(28px, 4vw, 42px), weight 800
|
||||
- Pricing cards: same card pattern but with featured state (accent border + subtle glow)
|
||||
- Product mockup: embedded in hero, shows the actual app UI with browser chrome
|
||||
|
||||
---
|
||||
|
||||
## Files That Define The Design System
|
||||
|
||||
These are the source-of-truth files in the codebase:
|
||||
|
||||
| File | What It Controls |
|
||||
|------|-----------------|
|
||||
| `frontend/src/index.css` | CSS variables, `@theme` block, base styles |
|
||||
| `frontend/tailwind.config.js` (v3) or inline in index.css (v4) | Color tokens, font families, spacing extensions |
|
||||
| `frontend/index.html` | Google Font imports |
|
||||
| `DESIGN-SYSTEM.md` (this file) | Design decisions, component specs, rules |
|
||||
|
||||
---
|
||||
|
||||
## Deprecated Files — DO NOT Reference
|
||||
|
||||
These files contain outdated design information and should be ignored:
|
||||
|
||||
- `DESIGN_SYSTEM_GUIDE.md` — Old monochrome glass-morphism system
|
||||
- `UI-DESIGN-SYSTEM.md` — Old workspace/purple gradient system
|
||||
- `REBRAND-IMPLEMENTATION-GUIDE.md` — Old purple rebrand from Patherly
|
||||
- `COMPONENT_EXAMPLES.md` — Old monochrome component patterns
|
||||
- Any file referencing `glass-card`, `glass-stat`, `bg-gradient-brand`, or `text-gradient-brand`
|
||||
128
Design/DESIGN-SYSTEM-SETUP-GUIDE.md
Normal file
128
Design/DESIGN-SYSTEM-SETUP-GUIDE.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# How To Make Claude Code Follow The New Design System
|
||||
|
||||
## The Problem
|
||||
|
||||
Your project currently has MULTIPLE contradicting design documents:
|
||||
- CLAUDE.md references purple gradients, glass-morphism, AND cyan accents
|
||||
- Old docs like DESIGN_SYSTEM_GUIDE.md describe monochrome glass-card patterns
|
||||
- UI-DESIGN-SYSTEM.md describes a purple gradient workspace system
|
||||
- REBRAND-IMPLEMENTATION-GUIDE.md has the old Patherly → ResolutionFlow rebrand
|
||||
- Your actual CSS has Tailwind v4 with OKLCH cyan tokens
|
||||
|
||||
Claude Code sees ALL of these and gets confused about which to follow.
|
||||
|
||||
## The Fix — 4 Steps
|
||||
|
||||
### Step 1: Place the new DESIGN-SYSTEM.md in your project root
|
||||
|
||||
Copy `DESIGN-SYSTEM.md` (the file I created alongside this one) to:
|
||||
```
|
||||
C:\Dev\Projects\patherly\DESIGN-SYSTEM.md
|
||||
```
|
||||
|
||||
This is the SINGLE SOURCE OF TRUTH for all design decisions going forward.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Update CLAUDE.md — Branding Section
|
||||
|
||||
Find the `### Branding` section and REPLACE EVERYTHING between `**Brand details:**`
|
||||
and the next `---` separator with this:
|
||||
|
||||
```markdown
|
||||
**Brand details:**
|
||||
|
||||
- **Design system:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — THE source of truth for all design decisions
|
||||
- **Design aesthetic:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no gradients on surfaces, no ambient effects.
|
||||
- **Accent color:** Cyan (#22d3ee / #06b6d4). Used sparingly — ≤5% of the UI.
|
||||
- **Fonts:** IBM Plex Sans (body), Bricolage Grotesque (headings), JetBrains Mono (code) — loaded via Google Fonts
|
||||
- **Logo:** 30px gradient square (cyan) + "ResolutionFlow" in Bricolage Grotesque 700
|
||||
- **Layout:** Icon rail sidebar (72px, default) with hover flyout panels. Pinnable to full 260px sidebar.
|
||||
- **Brand assets:** `brand-assets/` (source SVGs), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
|
||||
- **Landing page:** Shares color system and typography. Full-width marketing layout at `/` route.
|
||||
- **Reference mockups:** `docs/mockups/` (HTML files, open in browser)
|
||||
|
||||
**Component rules:**
|
||||
- Primary buttons: solid `accent` background (#22d3ee), white text
|
||||
- Cards: `bg-card` with 1px `border-default`, 8px radius. NO shadows, NO blur, NO gradients.
|
||||
- Badges: pill-shaped (20px radius), semantic dim background + matching text color
|
||||
- Active nav: `accent-dim` background + `accent-text` color + 3px left accent bar
|
||||
- Stat cards: 3px colored left border (accent/success/warning by position)
|
||||
- Code blocks: `bg-code` with JetBrains Mono, material-inspired syntax highlighting
|
||||
- Status colors: green (success), amber (warning), red (danger) — ONLY for semantic meaning
|
||||
|
||||
When adding new pages/components: reference DESIGN-SYSTEM.md. Use flat dark surfaces, 1px borders, no decorative effects. All colors via CSS variables.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Update CLAUDE.md — Tech Stack Line
|
||||
|
||||
Find the styling line under Tech Stack → Frontend and change it to:
|
||||
|
||||
```markdown
|
||||
- **Styling:** Tailwind CSS v4 — flat dark theme with cyan accent (see DESIGN-SYSTEM.md)
|
||||
```
|
||||
|
||||
Remove any references to "monochrome glass-morphism" or "dark-only" limitations.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Archive Old Design Docs
|
||||
|
||||
Move these files into an `archive/` folder (don't delete — you might want to reference them later):
|
||||
|
||||
```
|
||||
mkdir archive
|
||||
mkdir archive\design-docs
|
||||
|
||||
move DESIGN_SYSTEM_GUIDE.md archive\design-docs\
|
||||
move UI-DESIGN-SYSTEM.md archive\design-docs\
|
||||
move REBRAND-IMPLEMENTATION-GUIDE.md archive\design-docs\
|
||||
move COMPONENT_EXAMPLES.md archive\design-docs\ (if it exists in root)
|
||||
move docs\plans\Frontend\DESIGN_SYSTEM_GUIDE.md archive\design-docs\ (if it exists here)
|
||||
```
|
||||
|
||||
If any of these files are referenced elsewhere in CLAUDE.md, remove those references.
|
||||
|
||||
---
|
||||
|
||||
## Why This Works
|
||||
|
||||
Claude Code prioritizes files in this order:
|
||||
1. **CLAUDE.md** — reads this first, every time
|
||||
2. **Files referenced in CLAUDE.md** — follows links to read referenced docs
|
||||
3. **Files in the project root** — scans these for context
|
||||
4. **Files deeper in the tree** — only reads if specifically asked
|
||||
|
||||
By putting DESIGN-SYSTEM.md in the root AND linking it from CLAUDE.md's branding section,
|
||||
Claude Code will find it immediately and treat it as authoritative. By archiving the old
|
||||
docs, Claude Code won't stumble on contradicting information.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After making these changes, test by asking Claude Code:
|
||||
1. "What color system does this project use?" — Should say cyan/teal flat dark theme, NOT purple gradients or monochrome
|
||||
2. "Create a new card component" — Should use bg-card with 1px border, NOT glass-card with backdrop-blur
|
||||
3. "What fonts does this project use?" — Should say IBM Plex Sans + Bricolage Grotesque, NOT Inter + Plus Jakarta Sans
|
||||
4. "Add a sidebar nav item" — Should follow the icon rail pattern, NOT a traditional full sidebar
|
||||
|
||||
If any of these come back wrong, check that the old docs are actually archived and not still being found.
|
||||
|
||||
---
|
||||
|
||||
## Place These Mockup Files in docs/mockups/
|
||||
|
||||
Also copy the HTML mockups we created into your project so Claude Code has visual references:
|
||||
|
||||
```
|
||||
mkdir docs\mockups
|
||||
|
||||
copy resolutionflow-redesign.html docs\mockups\
|
||||
copy resolutionflow-sidebar-final.html docs\mockups\
|
||||
copy resolutionflow-landing.html docs\mockups\
|
||||
```
|
||||
|
||||
Then Claude Code can open these in a browser to see exactly what the target looks like.
|
||||
378
Design/DESIGN-SYSTEM.md
Normal file
378
Design/DESIGN-SYSTEM.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# ResolutionFlow Design System v4
|
||||
|
||||
> **Status:** ACTIVE — This document is the single source of truth for all frontend design decisions.
|
||||
> **Supersedes:** All previous design system docs including `DESIGN_SYSTEM_GUIDE.md`, `UI-DESIGN-SYSTEM.md`, `REBRAND-IMPLEMENTATION-GUIDE.md`, and any `COMPONENT_EXAMPLES.md` files.
|
||||
> **Last Updated:** March 21, 2026
|
||||
|
||||
---
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
ResolutionFlow uses a **flat, high-contrast dark theme** inspired by Sentry and PostHog. The aesthetic is premium, clean, and minimal — no glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. The accent color appears in ≤5% of the UI. Every design decision prioritizes **readability over decoration**.
|
||||
|
||||
**Light mode** is a planned addition (dark/light toggle). Design all components with CSS variables so theming is a variable swap, not a rewrite.
|
||||
|
||||
---
|
||||
|
||||
## Color System
|
||||
|
||||
All colors are defined as CSS custom properties in `index.css` inside the `@theme` block (Tailwind v4) or `:root` / `.dark` blocks (Tailwind v3).
|
||||
|
||||
### Dark Mode (Default)
|
||||
|
||||
```
|
||||
Page background: #0c0d10
|
||||
Sidebar background: #0f1118
|
||||
Card background: #14161d
|
||||
Card hover: #191c25
|
||||
Input background: #191c25
|
||||
Code background: #0e1017
|
||||
Elevated surface: #1c1f2a
|
||||
|
||||
Text primary: #e2e5eb
|
||||
Text heading: #f0f2f5
|
||||
Text secondary: #848b9b
|
||||
Text muted: #4f5666
|
||||
Text rail label: #6b7280
|
||||
|
||||
Border default: #1e2130
|
||||
Border hover: #2a2f3d
|
||||
|
||||
Accent (cyan): #22d3ee
|
||||
Accent hover: #06b6d4
|
||||
Accent dim (8-10%): rgba(34,211,238,0.10)
|
||||
Accent text: #67e8f9
|
||||
|
||||
Success: #34d399
|
||||
Success dim: rgba(52,211,153,0.10)
|
||||
Warning: #fbbf24
|
||||
Warning dim: rgba(251,191,36,0.10)
|
||||
Danger: #f87171
|
||||
Danger dim: rgba(248,113,113,0.10)
|
||||
```
|
||||
|
||||
### Light Mode (Planned)
|
||||
|
||||
```
|
||||
Page background: #f3f4f7
|
||||
Sidebar background: #ffffff
|
||||
Card background: #ffffff
|
||||
Card hover: #f8f9fb
|
||||
Input background: #eef0f4
|
||||
Code background: #f5f6f9
|
||||
Elevated surface: #e8eaef
|
||||
|
||||
Text primary: #1a1d24
|
||||
Text heading: #0d0f13
|
||||
Text secondary: #5a6274
|
||||
Text muted: #8b92a1
|
||||
|
||||
Border default: #dde0e7
|
||||
Border hover: #c5c9d3
|
||||
|
||||
Accent: #0891b2
|
||||
Accent dim: rgba(8,145,178,0.07)
|
||||
Accent text: #0e7490
|
||||
```
|
||||
|
||||
### What NOT To Use
|
||||
|
||||
- No glass morphism (`backdrop-filter: blur`)
|
||||
- No gradient backgrounds on cards or surfaces
|
||||
- No ambient orbs or floating glow elements
|
||||
- No `bg-white/[0.04]` opacity-based backgrounds
|
||||
- No purple gradient accent (`#818cf8 → #a78bfa`) — this is deprecated
|
||||
- No `text-gradient-brand` utility — replaced by solid `accent-text` color
|
||||
- No `glass-card`, `glass-stat`, `glass-card-glow` CSS utilities
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Stack
|
||||
|
||||
| Role | Font | Weights | CSS Variable |
|
||||
|------|------|---------|-------------|
|
||||
| Body text | IBM Plex Sans | 400, 500, 600, 700 | `--font-sans` / `font-sans` |
|
||||
| Headings | Bricolage Grotesque | 600, 700, 800 | `--font-heading` / `font-heading` |
|
||||
| Code / Monospace | JetBrains Mono | 400, 500 | `--font-mono` / `font-mono` |
|
||||
|
||||
### Typography Scale
|
||||
|
||||
| Element | Size | Weight | Color | Letter Spacing |
|
||||
|---------|------|--------|-------|---------------|
|
||||
| Page title (h1) | 22px | 700 (Bricolage) | text-heading | -0.03em |
|
||||
| Section title | 15px | 600 (Bricolage) | text-heading | normal |
|
||||
| Card title | 14px | 600 (IBM Plex) | text-heading | normal |
|
||||
| Body text | 14px | 400 (IBM Plex) | text-primary | normal |
|
||||
| Secondary text | 13px | 400 (IBM Plex) | text-secondary | normal |
|
||||
| Label / hint | 12px | 500 (IBM Plex) | text-muted | normal |
|
||||
| Nav section header | 10px | 600 (IBM Plex) | text-muted | 1.2px, uppercase |
|
||||
| Rail icon label | 10px | 500 (IBM Plex) | text-rail-label | normal |
|
||||
| Code | 12px | 400 (JetBrains) | text-primary | normal |
|
||||
| Stat number | 28px | 700 (Bricolage) | text-heading | -0.03em |
|
||||
| Badge | 11px | 550 (IBM Plex) | varies | normal |
|
||||
|
||||
### What NOT To Use
|
||||
|
||||
- No Inter, Plus Jakarta Sans, or Outfit — these are deprecated
|
||||
- No `font-label` utility — use `font-mono` for monospace or `font-sans` at small size
|
||||
- No font sizes below 10px
|
||||
|
||||
---
|
||||
|
||||
## Layout Architecture
|
||||
|
||||
### App Shell
|
||||
|
||||
The app uses a CSS Grid layout with two states:
|
||||
|
||||
**Icon Rail (Default):**
|
||||
```
|
||||
[icon-rail: 72px] [main-content: 1fr]
|
||||
```
|
||||
|
||||
**Pinned Sidebar:**
|
||||
```
|
||||
[full-sidebar: 260px] [main-content: 1fr]
|
||||
```
|
||||
|
||||
### Icon Rail Sidebar
|
||||
|
||||
The default navigation is a narrow icon rail (72px) with:
|
||||
- Logo mark at top (30px square, gradient background, lightning bolt icon)
|
||||
- Nav items as vertical column: icon (20px) + label text (10px) underneath
|
||||
- Dividers between nav sections
|
||||
- User avatar at bottom
|
||||
- Pin/expand button below avatar
|
||||
|
||||
**Hover behavior:** Hovering a rail item opens a flyout panel (220px) to the right of the rail with sub-navigation items for that section. The flyout stays open while the cursor is on either the rail item or the flyout panel.
|
||||
|
||||
**Pinned state:** Clicking the pin button expands to a traditional 260px sidebar with full text labels, section headers, badges, and the active left-bar accent. An unpin button in the header returns to the icon rail.
|
||||
|
||||
### Active Nav Indicator
|
||||
|
||||
- **Icon rail:** `accent-dim` background on the item, `accent-text` color on icon and label
|
||||
- **Pinned sidebar:** Same as above, plus a 3px left border (`border-radius: 0 3px 3px 0`) in `accent` color
|
||||
|
||||
### Main Content Area
|
||||
|
||||
- Padding: 28px top/bottom, 36px left/right
|
||||
- Max content width: not constrained (fills available space)
|
||||
- Page title: Bricolage Grotesque, 22px, weight 700
|
||||
|
||||
---
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Stat Cards
|
||||
|
||||
```
|
||||
Background: bg-card
|
||||
Border: 1px solid border-default
|
||||
Border-left: 3px solid [varies by position - accent, success, warning, accent]
|
||||
Border-radius: 8px
|
||||
Padding: 18px 16px
|
||||
```
|
||||
|
||||
- Label: 11px, uppercase, 600 weight, text-muted
|
||||
- Value: 28px, Bricolage Grotesque 700, text-heading
|
||||
- Delta: 12px, 500 weight, success/danger color with ↑/↓ prefix
|
||||
|
||||
### Cards
|
||||
|
||||
```
|
||||
Background: bg-card
|
||||
Border: 1px solid border-default
|
||||
Border-radius: 8px
|
||||
Padding: 20px
|
||||
Hover: border-color transitions to border-hover
|
||||
```
|
||||
|
||||
No shadows. No gradients. No glow effects.
|
||||
|
||||
### Flow List Items
|
||||
|
||||
```
|
||||
Layout: flex row, 12px gap
|
||||
Padding: 10px 8px
|
||||
Border-bottom: 1px solid border-default (divider style, not bordered cards)
|
||||
Hover: bg-card-hover
|
||||
```
|
||||
|
||||
- Icon: 34px square, border-radius 5px, semantic dim background + colored stroke
|
||||
- Name: 13px, 500 weight, text-heading
|
||||
- Meta: 11.5px, text-muted
|
||||
- Badge: right-aligned status badge
|
||||
|
||||
### Badges
|
||||
|
||||
```
|
||||
Font: 11px, 550 weight
|
||||
Padding: 3px 9px
|
||||
Border-radius: 20px (pill)
|
||||
```
|
||||
|
||||
| Type | Background | Text Color |
|
||||
|------|-----------|------------|
|
||||
| Info/Accent | accent-dim | accent-text |
|
||||
| Success | success-dim | success |
|
||||
| Warning | warning-dim | warning |
|
||||
| Danger | danger-dim | danger |
|
||||
|
||||
### Form Inputs
|
||||
|
||||
```
|
||||
Background: bg-input
|
||||
Border: 1px solid border-default
|
||||
Border-radius: 5px
|
||||
Padding: 9px 12px
|
||||
Font: 13px, IBM Plex Sans
|
||||
Focus: border-color accent, box-shadow 0 0 0 2px accent-dim
|
||||
Placeholder: text-muted
|
||||
```
|
||||
|
||||
### Buttons
|
||||
|
||||
**Primary:**
|
||||
```
|
||||
Background: accent (#22d3ee)
|
||||
Color: #fff
|
||||
Border: none
|
||||
Border-radius: 5px
|
||||
Padding: 9px 16px
|
||||
Font: 13px, 550 weight
|
||||
Hover: brightness(1.1)
|
||||
```
|
||||
|
||||
**Ghost:**
|
||||
```
|
||||
Background: transparent
|
||||
Color: text-secondary
|
||||
Border: 1px solid border-default
|
||||
Hover: bg-elevated, text-primary, border-hover
|
||||
```
|
||||
|
||||
### Code Blocks
|
||||
|
||||
```
|
||||
Background: bg-code (#0e1017)
|
||||
Border: 1px solid border-default
|
||||
Border-radius: 8px
|
||||
Padding: 18px 20px
|
||||
Font: JetBrains Mono, 12px, line-height 1.7
|
||||
```
|
||||
|
||||
**Syntax colors (dark mode):**
|
||||
|
||||
| Token | Color |
|
||||
|-------|-------|
|
||||
| Comment | #4a5568 |
|
||||
| Keyword | #c792ea |
|
||||
| Function/Cmdlet | #82aaff |
|
||||
| String | #c3e88d |
|
||||
| Variable | #89ddff |
|
||||
| Parameter | #8c93a4 |
|
||||
| Number | #f78c6c |
|
||||
|
||||
### Chips / Tags
|
||||
|
||||
```
|
||||
Font: 11.5px, 500 weight
|
||||
Padding: 4px 11px
|
||||
Border-radius: 20px
|
||||
Background: accent-dim
|
||||
Color: accent-text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logo
|
||||
|
||||
- **Mark:** 30-32px square, border-radius 8px, `linear-gradient(135deg, #06b6d4, #22d3ee)`, white lightning bolt SVG
|
||||
- **Wordmark:** "ResolutionFlow" in Bricolage Grotesque, 16-17px, weight 700, text-heading color
|
||||
- **Combined:** Mark + wordmark horizontally, 10px gap
|
||||
|
||||
---
|
||||
|
||||
## Spacing System
|
||||
|
||||
- Component internal gaps: 6px, 8px, 12px, 14px, 16px
|
||||
- Card padding: 20px
|
||||
- Section gaps: 24px between major sections
|
||||
- Page padding: 28px vertical, 36px horizontal
|
||||
- Stat grid gap: 12px
|
||||
- Two-column gap: 16px
|
||||
- Nav item padding: 8px 10px (pinned), 8px 4px (rail)
|
||||
|
||||
---
|
||||
|
||||
## Border Radius
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| radius-sm | 5px | Inputs, buttons, small elements |
|
||||
| radius | 8px | Cards, stat cards, code blocks |
|
||||
| radius-lg | 12px | Large cards, modals |
|
||||
| radius-xl | 16px | Hero elements, CTA boxes |
|
||||
| radius-pill | 100px | Badges, chips, toggle tracks |
|
||||
|
||||
---
|
||||
|
||||
## Shadows
|
||||
|
||||
Minimal to none. No decorative shadows.
|
||||
|
||||
- Cards: none (border only)
|
||||
- Dropdowns/flyouts: `0 4px 12px rgba(0,0,0,0.3)` if needed
|
||||
- Logo mark: `0 2px 8px rgba(14,165,233,0.25)` (subtle brand glow on logo only)
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
All icons use Lucide React (`lucide-react` package).
|
||||
- Default size: 16-18px
|
||||
- Stroke width: 1.6-1.8
|
||||
- Color: `currentColor` (inherits from parent text color)
|
||||
- Nav icons: 20px at stroke-width 1.6
|
||||
- Rail icon opacity: 0.6 default, 0.85 on hover, 1.0 when active
|
||||
|
||||
---
|
||||
|
||||
## Landing Page
|
||||
|
||||
The marketing landing page at `/` shares the same color system and typography but has its own layout (no sidebar, full-width sections). Key differences:
|
||||
|
||||
- Navigation: fixed top bar with logo, links, and CTAs
|
||||
- Hero: centered layout with glow effect (radial gradient, accent at 15% opacity)
|
||||
- Sections: 100px vertical padding, max-width 1120px container
|
||||
- Section labels: JetBrains Mono, 12px, uppercase, accent-text color
|
||||
- Section titles: Bricolage Grotesque, clamp(28px, 4vw, 42px), weight 800
|
||||
- Pricing cards: same card pattern but with featured state (accent border + subtle glow)
|
||||
- Product mockup: embedded in hero, shows the actual app UI with browser chrome
|
||||
|
||||
---
|
||||
|
||||
## Files That Define The Design System
|
||||
|
||||
These are the source-of-truth files in the codebase:
|
||||
|
||||
| File | What It Controls |
|
||||
|------|-----------------|
|
||||
| `frontend/src/index.css` | CSS variables, `@theme` block, base styles |
|
||||
| `frontend/tailwind.config.js` (v3) or inline in index.css (v4) | Color tokens, font families, spacing extensions |
|
||||
| `frontend/index.html` | Google Font imports |
|
||||
| `DESIGN-SYSTEM.md` (this file) | Design decisions, component specs, rules |
|
||||
|
||||
---
|
||||
|
||||
## Deprecated Files — DO NOT Reference
|
||||
|
||||
These files contain outdated design information and should be ignored:
|
||||
|
||||
- `DESIGN_SYSTEM_GUIDE.md` — Old monochrome glass-morphism system
|
||||
- `UI-DESIGN-SYSTEM.md` — Old workspace/purple gradient system
|
||||
- `REBRAND-IMPLEMENTATION-GUIDE.md` — Old purple rebrand from Patherly
|
||||
- `COMPONENT_EXAMPLES.md` — Old monochrome component patterns
|
||||
- Any file referencing `glass-card`, `glass-stat`, `bg-gradient-brand`, or `text-gradient-brand`
|
||||
70
SESSION-HANDOFF.md
Normal file
70
SESSION-HANDOFF.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Session Handoff — Design System v4 Migration
|
||||
|
||||
> **For the next Claude session:** Read this file completely, internalize the context, then delete it (`rm SESSION-HANDOFF.md`). This is a one-time context transfer.
|
||||
|
||||
---
|
||||
|
||||
## What Was Done This Session
|
||||
|
||||
### 1. FlowPilot Message Bar + AI Script Builder (MERGED to main)
|
||||
- PR #118 merged. Always-visible message bar in FlowPilot sessions, AI Script Builder at `/script-builder`, library reorg (My/Team Scripts tabs), FlowPilot-to-Script-Builder handoff, session abandon/close, unified session history.
|
||||
- Eng review completed: normalized `script_builder_messages` table, typed content helpers, 6 edge case tests.
|
||||
|
||||
### 2. Design System v4 Migration (PR #119, open, branch: `refactor/design-system-v4`)
|
||||
- Complete frontend redesign from glassmorphism to flat dark theme (Sentry/PostHog-inspired)
|
||||
- **CSS Foundation:** New color tokens in `index.css`, all via CSS custom properties. Light mode ready (just needs `.light` class values).
|
||||
- **Icon Rail Sidebar:** 72px rail with 5 grouped icons (Home, Work, Knowledge, Insights, Help). Full-height resizable drawer on hover. Pin-to-expand to 260px. Mobile hamburger overlay.
|
||||
- **Component Sweep:** ~200 files migrated. All hardcoded hex replaced with semantic Tailwind tokens (bg-card, text-foreground, border-border, etc.).
|
||||
- **Landing Page:** Flat surfaces, no glow, solid buttons.
|
||||
- **Interactive Shadows:** Dark-mode-aware — elevated surfaces + faint cyan accent glow (black shadows invisible on dark bg).
|
||||
- **Stat Cards:** 3px colored left borders.
|
||||
- **Tab Toggles:** Active state uses `tab-active-shadow` (elevated bg + faint glow).
|
||||
|
||||
### 3. GTM Strategy (from /office-hours)
|
||||
- Shadow & Ship approach: Michael uses ResolutionFlow on real tickets for 2 weeks, then hands logins to 5 MSP colleagues. Key metric: unprompted return.
|
||||
- Design doc at `~/.gstack/projects/patherly-patherly/`
|
||||
|
||||
---
|
||||
|
||||
## What Needs To Be Done Next
|
||||
|
||||
### Immediate (Design System v4 polish)
|
||||
1. **Home icon color fix:** The Home icon in the sidebar shouldn't have a cyan background when not active. Instead, the Home icon itself should always be cyan (brand accent), and only show the `bg-accent-dim` background when the route is actually `/`. Michael specifically requested this.
|
||||
2. **Visual QA pass:** Michael hasn't done a full page-by-page walkthrough yet. Expect feedback on individual pages once he does.
|
||||
3. **`font-label` cleanup:** ~10 files still reference `font-label` (deprecated alias for `font-mono`). Each needs inspection — some should be `font-mono`, others `font-sans text-xs`.
|
||||
4. **Inline `style` attributes:** ~29 instances still use hardcoded hex in inline styles (sidebar, drawer, badges). Should be converted to CSS variable references or Tailwind classes where possible.
|
||||
|
||||
### Before Merging PR #119
|
||||
- Run migrations: `docker exec resolutionflow_backend alembic upgrade head` (new tables from the Script Builder PR are on main now)
|
||||
- Full visual QA with backend running
|
||||
- Test mobile responsive (hamburger menu)
|
||||
- Test FlowPilot session with new message bar + action bar positioning
|
||||
|
||||
### Future
|
||||
- **Light mode toggle:** CSS variables are ready. Need to add `.light` class values in `index.css` + toggle in user settings/account page.
|
||||
- **Script Builder testing:** The AI Script Builder hasn't been tested end-to-end with the backend running yet.
|
||||
|
||||
---
|
||||
|
||||
## Key Files to Know
|
||||
|
||||
| File | What it does |
|
||||
|------|-------------|
|
||||
| `DESIGN-SYSTEM.md` | Single source of truth for all design decisions |
|
||||
| `frontend/src/index.css` | CSS tokens, component utilities, shadow patterns |
|
||||
| `frontend/src/components/layout/Sidebar.tsx` | Icon rail + drawer + pinned sidebar |
|
||||
| `frontend/src/components/layout/AppLayout.tsx` | CSS Grid shell |
|
||||
| `frontend/src/components/dashboard/StartSessionInput.tsx` | The Guided/Chat toggle |
|
||||
| `frontend/src/components/dashboard/PerformanceCards.tsx` | Stat cards with colored borders |
|
||||
|
||||
## Key Lessons From This Session
|
||||
|
||||
- The component sweep agents missed `editor-ai/`, `guides/`, `maintenance/`, `scripts/`, `settings/` directories and `text-brand-dark` references. Always do a final grep audit after sweeps.
|
||||
- `bg-[#hex]` hardcoding defeats the purpose of CSS variables. We had to do a second pass to replace 3,200+ hardcoded values with semantic tokens.
|
||||
- Black shadows (`rgba(0,0,0,...)`) are invisible on dark backgrounds. Use elevated surfaces + faint accent glow instead.
|
||||
- The sidebar flyout needed `position: fixed` to escape the CSS Grid cell clipping — `absolute` positioning was hidden behind the main content area.
|
||||
- Flyout hover timing: individual item `onMouseLeave` was killing the flyout before the mouse reached the drawer. Only the outer wrapper should handle `onMouseLeave`.
|
||||
|
||||
---
|
||||
|
||||
> **After reading this file:** Save relevant context to your session memory, then run `rm SESSION-HANDOFF.md` and `git add -A && git commit -m "chore: remove session handoff file"`.
|
||||
665
docs/superpowers/plans/2026-03-22-design-system-v4-migration.md
Normal file
665
docs/superpowers/plans/2026-03-22-design-system-v4-migration.md
Normal file
@@ -0,0 +1,665 @@
|
||||
# Design System v4 Migration — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Migrate ResolutionFlow's frontend from glassmorphism/gradient aesthetic to a flat, high-contrast dark theme with an icon rail sidebar, as defined in DESIGN-SYSTEM.md.
|
||||
|
||||
**Architecture:** Five phases executed sequentially. Phase 1 rewrites CSS tokens and adds compatibility shims. Phase 2 rebuilds the sidebar as an icon rail. Phase 3 sweeps ~200 component files replacing old patterns. Phase 4 updates the landing page. Phase 5 removes shims and cleans up.
|
||||
|
||||
**Tech Stack:** React 19, Tailwind CSS v4, CSS custom properties, Lucide React
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-22-design-system-v4-migration.md`
|
||||
|
||||
**Design system reference:** `DESIGN-SYSTEM.md` (project root)
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### Phase 1 — CSS Foundation
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Rewrite | `frontend/src/index.css` |
|
||||
|
||||
### Phase 2 — Icon Rail Sidebar
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Rewrite | `frontend/src/components/layout/Sidebar.tsx` |
|
||||
| Modify | `frontend/src/components/layout/AppLayout.tsx` |
|
||||
| Modify | `frontend/src/components/layout/TopBar.tsx` |
|
||||
| Modify | `frontend/src/components/common/BrandLogo.tsx` and `BrandWordmark.tsx` |
|
||||
|
||||
### Phase 3 — Component Sweep (~200 files)
|
||||
| Action | Files |
|
||||
|--------|-------|
|
||||
| Modify | All `.tsx` files in `components/` and `pages/` that reference old patterns |
|
||||
|
||||
### Phase 4 — Landing Page
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Modify | `frontend/src/pages/LandingPage.tsx` |
|
||||
| Modify | `frontend/src/styles/landing.css` |
|
||||
|
||||
### Phase 5 — Cleanup
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Modify | `frontend/src/index.css` (remove shims) |
|
||||
| Modify | `CLAUDE.md` (verify no stale references) |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Rewrite CSS Foundation (index.css)
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/index.css`
|
||||
- Reference: `DESIGN-SYSTEM.md` (project root)
|
||||
|
||||
- [ ] **Step 1: Rewrite the `@theme` block with new color tokens**
|
||||
|
||||
Replace the entire `@theme { ... }` block. New tokens from DESIGN-SYSTEM.md:
|
||||
|
||||
```css
|
||||
@theme {
|
||||
/* ── Surface colors ────────────────────────────── */
|
||||
--color-bg-page: #0c0d10;
|
||||
--color-bg-sidebar: #0f1118;
|
||||
--color-bg-card: #14161d;
|
||||
--color-bg-card-hover: #191c25;
|
||||
--color-bg-input: #191c25;
|
||||
--color-bg-code: #0e1017;
|
||||
--color-bg-elevated: #1c1f2a;
|
||||
|
||||
/* ── Text colors ───────────────────────────────── */
|
||||
--color-text-heading: #f0f2f5;
|
||||
--color-text-primary: #e2e5eb;
|
||||
--color-text-secondary: #848b9b;
|
||||
--color-text-muted: #4f5666;
|
||||
--color-text-rail-label: #6b7280;
|
||||
|
||||
/* ── Border colors ─────────────────────────────── */
|
||||
--color-border-default: #1e2130;
|
||||
--color-border-hover: #2a2f3d;
|
||||
|
||||
/* ── Accent (cyan) ─────────────────────────────── */
|
||||
--color-accent: #22d3ee;
|
||||
--color-accent-hover: #06b6d4;
|
||||
--color-accent-dim: rgba(34,211,238,0.10);
|
||||
--color-accent-text: #67e8f9;
|
||||
|
||||
/* ── Semantic colors ───────────────────────────── */
|
||||
--color-success: #34d399;
|
||||
--color-success-dim: rgba(52,211,153,0.10);
|
||||
--color-warning: #fbbf24;
|
||||
--color-warning-dim: rgba(251,191,36,0.10);
|
||||
--color-danger: #f87171;
|
||||
--color-danger-dim: rgba(248,113,113,0.10);
|
||||
|
||||
/* ── Tailwind semantic mappings ─────────────────── */
|
||||
--color-background: #0c0d10;
|
||||
--color-foreground: #e2e5eb;
|
||||
--color-card: #14161d;
|
||||
--color-card-foreground: #e2e5eb;
|
||||
--color-popover: #14161d;
|
||||
--color-popover-foreground: #e2e5eb;
|
||||
--color-primary: #22d3ee;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-secondary: #1c1f2a;
|
||||
--color-secondary-foreground: #e2e5eb;
|
||||
--color-muted: #1c1f2a;
|
||||
--color-muted-foreground: #848b9b;
|
||||
--color-accent-tw: #1c1f2a;
|
||||
--color-accent-foreground: #e2e5eb;
|
||||
--color-destructive: #f87171;
|
||||
--color-destructive-foreground: #ffffff;
|
||||
--color-border: #1e2130;
|
||||
--color-input: #191c25;
|
||||
--color-ring: #22d3ee;
|
||||
|
||||
/* ── Radii ─────────────────────────────────────── */
|
||||
--radius-sm: 5px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
|
||||
/* ── Fonts ─────────────────────────────────────── */
|
||||
--font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
|
||||
--font-heading: 'Bricolage Grotesque', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
/* Deprecated alias — remove in Phase 5 */
|
||||
--font-label: 'JetBrains Mono', monospace;
|
||||
|
||||
/* ── Animations ────────────────────────────────── */
|
||||
--animate-fade-in: fade-in 200ms ease-out;
|
||||
--animate-fade-in-up: fade-in-up 200ms ease-out;
|
||||
--animate-slide-in-left: slide-in-from-left 200ms ease-out;
|
||||
--animate-slide-in-bottom: slide-in-from-bottom 200ms ease-out;
|
||||
--animate-scale-in: scale-in 150ms ease-out;
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; } to { opacity: 1; }
|
||||
}
|
||||
@keyframes fade-in-up {
|
||||
from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes slide-in-from-left {
|
||||
from { transform: translateX(-100%); } to { transform: translateX(0); }
|
||||
}
|
||||
@keyframes slide-in-from-bottom {
|
||||
from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes scale-in {
|
||||
from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace `:root` variables**
|
||||
|
||||
Replace the `:root` block (lines 168-192) with:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--sidebar-w: 72px;
|
||||
--sidebar-w-pinned: 260px;
|
||||
--ease-out-smooth: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add compatibility shims**
|
||||
|
||||
Replace the old `@utility` blocks (glass-card, glass-card-static, active-glow, text-gradient-brand) with shims:
|
||||
|
||||
```css
|
||||
/* ── Deprecated shims — remove after Phase 3 sweep ── */
|
||||
@utility glass-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 8px;
|
||||
transition: border-color 200ms ease;
|
||||
&:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@utility glass-card-static {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@utility text-gradient-brand {
|
||||
color: var(--color-accent-text);
|
||||
}
|
||||
|
||||
@utility active-glow {
|
||||
/* no-op — glow removed in v4 */
|
||||
}
|
||||
|
||||
@utility bg-gradient-brand-hover {
|
||||
/* Shim — maps to brightness hover until components are swept */
|
||||
&:hover { filter: brightness(1.1); }
|
||||
}
|
||||
```
|
||||
|
||||
Remove the old gradient `--background-image-*` theme values. Remove `breatheGlow`, `bellWobble`, `slideDown`, `slideInRight`, `fadeInRight`, `pulse-dot`, `stagger-fade-in` keyframes. Remove `stagger-item` utility.
|
||||
|
||||
- [ ] **Step 4: Add new base component classes**
|
||||
|
||||
Add after the shims:
|
||||
|
||||
```css
|
||||
/* ── New component base classes ─────────────────── */
|
||||
@utility card-flat {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@utility card-interactive {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 8px;
|
||||
transition: border-color 200ms ease;
|
||||
&:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@utility btn-primary {
|
||||
background: var(--color-accent);
|
||||
color: #ffffff;
|
||||
font-weight: 550;
|
||||
border-radius: 5px;
|
||||
padding: 9px 16px;
|
||||
font-size: 13px;
|
||||
transition: filter 150ms ease;
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@utility btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 5px;
|
||||
padding: 9px 16px;
|
||||
font-size: 13px;
|
||||
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
||||
&:hover {
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update layout utilities**
|
||||
|
||||
Update `.app-shell` grid columns:
|
||||
|
||||
```css
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-w) 1fr;
|
||||
grid-template-rows: 1fr; /* No topbar row — topbar is inside main content now, or handled differently */
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
transition: grid-template-columns 200ms ease;
|
||||
}
|
||||
|
||||
.app-shell--pinned {
|
||||
--sidebar-w: 260px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update base body styles**
|
||||
|
||||
```css
|
||||
body {
|
||||
@apply bg-background text-foreground font-sans;
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-page);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Keep React Flow, Sonner, scrollbar, and accessibility styles**
|
||||
|
||||
These stay mostly unchanged — just verify they reference the correct new variable names. Update any references to removed variables.
|
||||
|
||||
- [ ] **Step 8: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
Expected: Build succeeds. App looks different (shims mapping old classes to flat styles) but is functional.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/index.css
|
||||
git commit -m "refactor: rewrite CSS foundation for Design System v4
|
||||
|
||||
New flat dark color tokens, remove glass/gradient utilities,
|
||||
add compatibility shims for phased migration.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Icon Rail Sidebar
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/components/layout/Sidebar.tsx`
|
||||
- Modify: `frontend/src/components/layout/AppLayout.tsx`
|
||||
- Modify: `frontend/src/components/layout/TopBar.tsx`
|
||||
- Modify: `frontend/src/components/common/BrandLogo.tsx`
|
||||
- Reference: `DESIGN-SYSTEM.md` → Layout Architecture, Icon Rail Sidebar sections
|
||||
|
||||
This is a complex task. The sidebar has three states: icon rail (72px), flyout (hovering), and pinned (260px). Read the current `Sidebar.tsx` and `AppLayout.tsx` fully before starting.
|
||||
|
||||
- [ ] **Step 1: Read current sidebar and layout**
|
||||
|
||||
Read `frontend/src/components/layout/Sidebar.tsx` and `frontend/src/components/layout/AppLayout.tsx` completely to understand current nav structure, items, mobile handling, and grid layout.
|
||||
|
||||
- [ ] **Step 2: Implement icon rail sidebar**
|
||||
|
||||
Rewrite `Sidebar.tsx` with:
|
||||
- Default: 72px icon rail with `bg-[var(--color-bg-sidebar)]`
|
||||
- Logo mark at top: 30px cyan gradient square with lightning bolt SVG
|
||||
- Nav items: 20px Lucide icon + 10px label text underneath, centered
|
||||
- Horizontal dividers between RESOLVE / KNOWLEDGE / INSIGHTS sections
|
||||
- Active item: `bg-[var(--color-accent-dim)]` background, `text-[var(--color-accent-text)]` on icon + label
|
||||
- Inactive: `text-[var(--color-text-rail-label)]`, opacity 0.6 default, 0.85 hover
|
||||
- User avatar at bottom
|
||||
- Pin toggle button below avatar
|
||||
|
||||
Nav items (same as current, just icon + short label):
|
||||
- Dashboard (LayoutDashboard)
|
||||
- Sessions (Activity) — with count badge
|
||||
- Escalations (AlertTriangle)
|
||||
- Divider
|
||||
- Flows (GitBranch)
|
||||
- Steps (Layers)
|
||||
- Scripts (Code2)
|
||||
- Builder (Wand2)
|
||||
- Review (ListChecks)
|
||||
- Divider
|
||||
- Exports (Download)
|
||||
- Analytics (BarChart3)
|
||||
- FP Analytics (Rocket)
|
||||
|
||||
Footer: User Guides, Feedback, Account (as icons), Pin/Unpin toggle
|
||||
|
||||
- [ ] **Step 3: Implement flyout panels**
|
||||
|
||||
On hover of a nav item, show a 220px flyout panel to the right:
|
||||
- `bg-[var(--color-bg-card)]` with `border border-[var(--color-border-default)]`
|
||||
- `box-shadow: 0 4px 12px rgba(0,0,0,0.3)`
|
||||
- 150ms ease-out transition (opacity + translateX)
|
||||
- Also opens on keyboard focus/Enter
|
||||
- Closes on Escape, mouse leave from both rail item and flyout, or focus-out
|
||||
- `max-height: 70vh; overflow-y: auto` for long sub-item lists
|
||||
- Sub-items match current sidebar children (e.g., Flows → All Flows, Troubleshooting, Projects, Maintenance)
|
||||
|
||||
- [ ] **Step 4: Implement pinned state**
|
||||
|
||||
Clicking pin button:
|
||||
- Expands sidebar to 260px with full text labels, section headers, badges
|
||||
- Updates `--sidebar-w` CSS variable to `260px`
|
||||
- Shows unpin button in sidebar header
|
||||
- Persists preference to localStorage (`sidebarPinned: true/false`)
|
||||
- Active item gets 3px left accent bar
|
||||
|
||||
- [ ] **Step 5: Mobile handling**
|
||||
|
||||
Below `sm` breakpoint:
|
||||
- Icon rail hidden
|
||||
- Hamburger button in top-left corner
|
||||
- Opens full-screen overlay with pinned sidebar content
|
||||
- Close button (X) in overlay header
|
||||
|
||||
- [ ] **Step 6: Update AppLayout.tsx**
|
||||
|
||||
- Grid layout changes from `grid-template-rows: 56px 1fr` to `grid-template-rows: auto 1fr` — TopBar remains a grid row spanning both columns (same as current), but with flat styling
|
||||
- Grid columns: `var(--sidebar-w) 1fr` (72px default, 260px when pinned via `.app-shell--pinned` class)
|
||||
- Sidebar pin state: read from `localStorage.getItem('sidebarPinned')` on mount, toggle sets `localStorage.setItem('sidebarPinned', 'true'/'false')` and updates `--sidebar-w` CSS variable on the `.app-shell` element
|
||||
- Ensure `--sidebar-w` transition is coordinated with sidebar width transition to prevent layout jump on pin/unpin (both use `transition: 200ms ease`)
|
||||
- Mobile: single column, no sidebar
|
||||
|
||||
- [ ] **Step 7: Update TopBar.tsx**
|
||||
|
||||
- Remove backdrop blur
|
||||
- Flat `bg-[var(--color-bg-sidebar)]` or `bg-[var(--color-bg-page)]` background
|
||||
- `border-b border-[var(--color-border-default)]`
|
||||
- Remove any glass/gradient styling
|
||||
|
||||
- [ ] **Step 8: Update BrandLogo.tsx**
|
||||
|
||||
- Replace decision-tree icon SVG with 30px gradient square (cyan, border-radius 8px) + white lightning bolt
|
||||
- Wordmark: "ResolutionFlow" in Bricolage Grotesque 700, `text-[var(--color-text-heading)]` (no gradient)
|
||||
- Remove `text-gradient-brand` usage
|
||||
|
||||
- [ ] **Step 9: Verify build and test**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
Expected: Build succeeds. Sidebar renders as icon rail. Flyout works on hover. Pin/unpin works.
|
||||
|
||||
- [ ] **Step 10: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/layout/ frontend/src/components/common/BrandLogo.tsx
|
||||
git commit -m "refactor: implement icon rail sidebar for Design System v4
|
||||
|
||||
72px icon rail with hover flyouts, pin-to-expand toggle,
|
||||
keyboard accessible, mobile hamburger overlay.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Component Sweep — Layout & Dashboard
|
||||
|
||||
**Files:**
|
||||
- Modify: All files in `frontend/src/components/dashboard/` (~16 files)
|
||||
- Modify: Any remaining layout components
|
||||
|
||||
Apply the pattern replacement map from the spec to every file. For each file:
|
||||
|
||||
1. Replace `glass-card` / `glass-card-static` → `card-flat` or `card-interactive`
|
||||
2. Replace `bg-gradient-brand` → `btn-primary` class or `bg-[var(--color-accent)]`
|
||||
3. Replace `text-gradient-brand` → `text-[var(--color-accent-text)]`
|
||||
4. Replace `text-foreground` → `text-[var(--color-text-primary)]`
|
||||
5. Replace `text-muted-foreground` → `text-[var(--color-text-secondary)]`
|
||||
6. Replace `font-label` on code/monospace → `font-mono`; on labels/badges → `font-sans text-xs`
|
||||
7. Remove `backdrop-filter`, `shadow-lg shadow-primary/20`, `scale(1.02)` hover
|
||||
8. Remove atmosphere orb JSX elements
|
||||
9. Replace `rounded-[10px]` / `rounded-[16px]` → `rounded-lg` (8px)
|
||||
10. Replace inline `style={{ background: 'rgba(...)' }}` glass surfaces → CSS variable classes
|
||||
|
||||
- [ ] **Step 1: Read and update all dashboard components**
|
||||
|
||||
Process all `.tsx` files in `frontend/src/components/dashboard/` (~18 files). Apply the pattern replacement map to each.
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/dashboard/ frontend/src/components/layout/
|
||||
git commit -m "refactor: migrate dashboard components to Design System v4
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Component Sweep — FlowPilot
|
||||
|
||||
**Files:**
|
||||
- Modify: All files in `frontend/src/components/flowpilot/` (~12 files)
|
||||
|
||||
Same pattern replacement map as Task 3.
|
||||
|
||||
- [ ] **Step 1: Read and update all FlowPilot components**
|
||||
|
||||
Process all `.tsx` files in `frontend/src/components/flowpilot/` (~17 files). Apply the pattern replacement map to each.
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/flowpilot/
|
||||
git commit -m "refactor: migrate FlowPilot components to Design System v4
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Component Sweep — Pages
|
||||
|
||||
**Files:**
|
||||
- Modify: All files in `frontend/src/pages/` (~25 files)
|
||||
|
||||
Same pattern replacement map.
|
||||
|
||||
- [ ] **Step 1: Read and update all page components**
|
||||
|
||||
Process all `.tsx` files in `frontend/src/pages/` (~41 files). Apply the pattern replacement map to each.
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/pages/
|
||||
git commit -m "refactor: migrate page components to Design System v4
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Component Sweep — Session, Script Builder, Account
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/components/session/` (~21 files)
|
||||
- Modify: `frontend/src/components/script-builder/` (~5 files)
|
||||
- Modify: `frontend/src/components/account/` (~5 files)
|
||||
|
||||
- [ ] **Step 1: Update session components**
|
||||
- [ ] **Step 2: Update script-builder components**
|
||||
- [ ] **Step 3: Update account components**
|
||||
- [ ] **Step 4: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/session/ frontend/src/components/script-builder/ frontend/src/components/account/
|
||||
git commit -m "refactor: migrate session, script-builder, account to Design System v4
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Component Sweep — All Remaining
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/components/common/` (~22 files)
|
||||
- Modify: `frontend/src/components/tree-editor/` (~20 files)
|
||||
- Modify: `frontend/src/components/kb-accelerator/` (~5 files)
|
||||
- Modify: `frontend/src/components/copilot/` (~3 files)
|
||||
- Modify: `frontend/src/components/assistant/` (~3 files)
|
||||
- Modify: `frontend/src/components/analytics/` (~3 files)
|
||||
- Modify: `frontend/src/components/library/` (~3 files)
|
||||
- Modify: `frontend/src/components/procedural/` (~3 files)
|
||||
- Modify: `frontend/src/components/public/` (~3 files)
|
||||
- Modify: `frontend/src/components/script-editor/` (~6 files)
|
||||
- Modify: `frontend/src/components/ui/` (any using old patterns)
|
||||
- Modify: `frontend/src/components/admin/` (~12 files)
|
||||
|
||||
- [ ] **Step 1: Update all remaining component directories**
|
||||
|
||||
Process each directory. Use the same pattern replacement map.
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/
|
||||
git commit -m "refactor: migrate remaining components to Design System v4
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Landing Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/pages/LandingPage.tsx`
|
||||
- Modify: Landing page CSS (check if `landing.css` exists or if styles are in `index.css`)
|
||||
|
||||
- [ ] **Step 1: Find landing page styles**
|
||||
|
||||
```bash
|
||||
find frontend/src -name "landing*" -o -name "Landing*" | head -10
|
||||
grep -r "landing" frontend/src/index.css | head -5
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update landing page**
|
||||
|
||||
- Remove ambient glow effects, gradient overlays
|
||||
- Hero: subtle radial gradient at 15% accent opacity
|
||||
- Section labels: `font-mono`, 12px, uppercase, `text-[var(--color-accent-text)]`
|
||||
- Section titles: `font-heading`, `clamp(28px, 4vw, 42px)`, weight 800
|
||||
- Pricing cards: `card-flat` with featured card getting `border-[var(--color-accent)]`
|
||||
- Top nav: flat, no blur
|
||||
- CTAs: `btn-primary` (solid accent)
|
||||
|
||||
- [ ] **Step 3: Verify build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/pages/LandingPage.tsx frontend/src/styles/landing.css
|
||||
git commit -m "refactor: migrate landing page to Design System v4
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Cleanup & Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/index.css`
|
||||
- Modify: `CLAUDE.md` (verify)
|
||||
|
||||
- [ ] **Step 1: Remove compatibility shims**
|
||||
|
||||
Remove from `index.css`:
|
||||
- The `glass-card` shim utility
|
||||
- The `glass-card-static` shim utility
|
||||
- The `text-gradient-brand` shim utility
|
||||
- The `active-glow` shim utility
|
||||
- The `--font-label` alias (keep only `--font-mono`)
|
||||
- Any remaining old gradient references
|
||||
|
||||
- [ ] **Step 1.5: Update BrandWordmark.tsx**
|
||||
|
||||
Check `frontend/src/components/common/BrandWordmark.tsx` for `text-gradient-brand` usage. Replace with `text-[var(--color-accent-text)]`.
|
||||
|
||||
- [ ] **Step 2: Verify no old patterns remain**
|
||||
|
||||
```bash
|
||||
cd frontend && grep -r "glass-card\|glass-stat\|bg-gradient-brand\|text-gradient-brand\|backdrop-filter.*blur\|atmosphere-orb\|active-glow\|breatheGlow\|bellWobble" src/ --include="*.tsx" --include="*.css" | head -20
|
||||
```
|
||||
|
||||
Expected: Zero results (or only in comments/string literals).
|
||||
|
||||
- [ ] **Step 3: Verify CLAUDE.md**
|
||||
|
||||
Grep CLAUDE.md for any stale references to old design system patterns. Should already be clean from the doc-swap commit, but verify.
|
||||
|
||||
- [ ] **Step 4: Final build verification**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
Expected: Clean build with zero errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/index.css CLAUDE.md
|
||||
git commit -m "chore: remove design system v3 compatibility shims
|
||||
|
||||
All components migrated to v4 flat dark theme.
|
||||
No glass-card, gradient, or blur references remain.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
276
docs/superpowers/specs/2026-03-22-design-system-v4-migration.md
Normal file
276
docs/superpowers/specs/2026-03-22-design-system-v4-migration.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Design System v4 Migration — Flat Dark Theme
|
||||
|
||||
> **Goal:** Migrate ResolutionFlow's frontend from glassmorphism/gradient aesthetic to a flat, high-contrast dark theme (Sentry/PostHog-inspired) with an icon rail sidebar, as defined in DESIGN-SYSTEM.md.
|
||||
|
||||
## Scope
|
||||
|
||||
~130 files across 15+ directories use old design patterns (`glass-card`, `bg-gradient-brand`, `text-gradient-brand`, `backdrop-filter: blur()`, ambient orbs). All must be updated to the new flat dark system.
|
||||
|
||||
## Approach
|
||||
|
||||
**Foundation first, then sweep.** Three phases:
|
||||
|
||||
1. **Phase 1: CSS Foundation** — Rewrite `index.css` tokens, remove old utilities, add new variables
|
||||
2. **Phase 2: Layout Shell** — Icon rail sidebar + app layout restructure
|
||||
3. **Phase 3: Component Sweep** — Update all 132 files directory-by-directory
|
||||
|
||||
Each phase produces deployable commits. The app will look inconsistent during Phase 3 but will always be functional.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: CSS Foundation
|
||||
|
||||
### index.css @theme Block
|
||||
|
||||
Rewrite the `@theme` block with new color tokens matching DESIGN-SYSTEM.md:
|
||||
|
||||
```
|
||||
Page background: #0c0d10 → --bg-page
|
||||
Sidebar background: #0f1118 → --bg-sidebar
|
||||
Card background: #14161d → --bg-card
|
||||
Card hover: #191c25 → --bg-card-hover
|
||||
Input background: #191c25 → --bg-input
|
||||
Code background: #0e1017 → --bg-code
|
||||
Elevated surface: #1c1f2a → --bg-elevated
|
||||
|
||||
Text heading: #f0f2f5 → --text-heading
|
||||
Text primary: #e2e5eb → --text-primary
|
||||
Text secondary: #848b9b → --text-secondary
|
||||
Text muted: #4f5666 → --text-muted
|
||||
|
||||
Border default: #1e2130 → --border-default
|
||||
Border hover: #2a2f3d → --border-hover
|
||||
|
||||
Accent: #22d3ee → --accent
|
||||
Accent hover: #06b6d4 → --accent-hover
|
||||
Accent dim (10%): rgba(34,211,238,0.10) → --accent-dim
|
||||
Accent text: #67e8f9 → --accent-text
|
||||
|
||||
Success: #34d399 + dim at 10%
|
||||
Warning: #fbbf24 + dim at 10%
|
||||
Danger: #f87171 + dim at 10%
|
||||
|
||||
Rail label: #6b7280 → --text-rail-label
|
||||
```
|
||||
|
||||
### Variables Structure
|
||||
|
||||
All colors in `:root` (dark is default). Light mode values prepared as comments — implementing the toggle is a future follow-up that just adds a `.light` class block.
|
||||
|
||||
### Compatibility Shims (Critical)
|
||||
|
||||
To avoid breaking the entire app while Phase 3 sweeps components, Phase 1 keeps the old utility class names as **deprecated aliases** mapping to the new values:
|
||||
|
||||
```css
|
||||
/* Deprecated — remove after Phase 3 sweep completes */
|
||||
.glass-card, .glass-card-static { @apply bg-[var(--bg-card)] border border-[var(--border-default)] rounded-lg; }
|
||||
.glass-card:hover { @apply border-[var(--border-hover)]; }
|
||||
```
|
||||
|
||||
These shims are removed in a final cleanup commit after Phase 3 is complete.
|
||||
|
||||
### Remove Old Utilities
|
||||
|
||||
Delete from `index.css` (after Phase 3 sweep — shims keep the app functional until then):
|
||||
- `.glass-card`, `.glass-card-static`, `.glass-stat`, `.glass-card-glow`
|
||||
- `.active-glow`, `.breatheGlow` keyframes
|
||||
- `bg-gradient-brand`, `text-gradient-brand`, `bg-gradient-brand-hover`
|
||||
- `--glass-bg`, `--glass-border`, `--glass-blur` variables
|
||||
- `--shadow-float`, `--shadow-float-hover`, `--shadow-cyan-glow` shadow variables
|
||||
- Atmosphere orb styles (`.atmosphere-orb`)
|
||||
- Orchestrated page-load animation keyframes (slideDown, slideInLeft, fadeInUp, fadeInRight)
|
||||
|
||||
### Rename
|
||||
|
||||
- `--font-label` → `--font-mono` (JetBrains Mono, same font, clearer name)
|
||||
|
||||
### Keep
|
||||
|
||||
- Google Fonts imports (same three fonts)
|
||||
- React Flow import
|
||||
- Tailwind v4 `@theme` approach
|
||||
- Base animation keyframes: `fade-in`, `fade-in-up`, `slide-in-from-left`, `slide-in-from-bottom` — all others removed (no `breatheGlow`, `bellWobble`, `slideDown`, orchestrated page-load sequence)
|
||||
|
||||
### New Base Classes
|
||||
|
||||
Add reusable component classes:
|
||||
|
||||
```css
|
||||
.card { /* bg-card, 1px border-default, 8px radius, no shadow */ }
|
||||
.card-interactive { /* extends .card + hover border-hover transition */ }
|
||||
.stat-card { /* extends .card + 3px left border */ }
|
||||
.badge { /* 11px, pill shape, dim bg + matching text */ }
|
||||
.btn-primary { /* solid accent, white text, 5px radius — NOTE: text changes from dark (#101114) to white */ }
|
||||
.btn-ghost { /* transparent, 1px border, hover bg-elevated */ }
|
||||
```
|
||||
|
||||
Implement each class per DESIGN-SYSTEM.md Component Patterns section for exact values.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Icon Rail Sidebar
|
||||
|
||||
### New Component: IconRailSidebar
|
||||
|
||||
Replaces current `Sidebar.tsx`. Two states:
|
||||
|
||||
**Icon Rail (72px, default):**
|
||||
- `bg-sidebar` background, full viewport height
|
||||
- Logo mark at top: 30px cyan gradient square with lightning bolt
|
||||
- Nav items: vertical column, 20px icon + 10px label underneath
|
||||
- Horizontal dividers between sections (RESOLVE / KNOWLEDGE / INSIGHTS)
|
||||
- User avatar at bottom
|
||||
- Pin/expand toggle below avatar
|
||||
- Active item: `accent-dim` background, `accent-text` on icon + label
|
||||
|
||||
**Hover Flyout (220px panel):**
|
||||
- Opens to the right of the hovered rail item with 150ms ease-out transition
|
||||
- Also opens on keyboard focus/Enter for accessibility; closes on Escape or focus-out
|
||||
- Shows sub-navigation for that section
|
||||
- If section has many sub-items, flyout scrolls with `max-height: 70vh; overflow-y: auto`
|
||||
- `bg-card` with `border-default` and `box-shadow: 0 4px 12px rgba(0,0,0,0.3)`
|
||||
- Stays open while cursor is on rail item OR flyout panel
|
||||
- Closes on mouse leave from both
|
||||
|
||||
**Pinned State (260px full sidebar):**
|
||||
- Click pin → expands to traditional sidebar with text labels, section headers, badges
|
||||
- 3px left accent bar on active item
|
||||
- Unpin button in sidebar header
|
||||
- Preference persisted in localStorage / userPreferences store
|
||||
|
||||
**Mobile:**
|
||||
- Icon rail hidden below `sm` breakpoint
|
||||
- Hamburger menu opens full overlay sidebar (same as pinned content)
|
||||
|
||||
### Layout Changes
|
||||
|
||||
`AppLayout.tsx` CSS Grid:
|
||||
- Default: `grid-template-columns: 72px 1fr`
|
||||
- Pinned: `grid-template-columns: 260px 1fr`
|
||||
- Mobile: `grid-template-columns: 1fr`
|
||||
- CSS variable `--sidebar-w` updated accordingly (used by FlowPilotMessageBar, FlowPilotActionBar)
|
||||
- Ensure `--sidebar-w` transition is coordinated with sidebar width transition to prevent layout jump on pin/unpin
|
||||
|
||||
**Sidebar commits:** Phase 2 produces 2-3 commits (icon rail core, flyout behavior, pinned state + persistence).
|
||||
|
||||
### BrandLogo Update
|
||||
|
||||
Replace current decision-tree icon SVG in `BrandLogo.tsx` with 30px gradient square (cyan, `border-radius: 8px`) + white lightning bolt mark. Wordmark: "ResolutionFlow" in Bricolage Grotesque 700, `text-heading` color (no gradient).
|
||||
|
||||
### Files
|
||||
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Rewrite | `frontend/src/components/layout/Sidebar.tsx` → icon rail with flyout + pinned state |
|
||||
| Modify | `frontend/src/components/layout/AppLayout.tsx` → grid columns, mobile handling |
|
||||
| Modify | `frontend/src/components/layout/TopBar.tsx` → flat styling, remove blur |
|
||||
| Keep | `FlowPilotMessageBar.tsx`, `FlowPilotActionBar.tsx` — `left: var(--sidebar-w)` still works |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Component Sweep
|
||||
|
||||
### Pattern Replacement Map
|
||||
|
||||
| Old Pattern | New Pattern |
|
||||
|------------|-------------|
|
||||
| `glass-card` / `glass-card-static` | `bg-[var(--bg-card)] border border-[var(--border-default)] rounded-lg` (or `.card` class) |
|
||||
| `bg-gradient-brand` | `bg-[var(--accent)]` (solid cyan) |
|
||||
| `text-gradient-brand` | `text-[var(--accent-text)]` |
|
||||
| `bg-gradient-brand-hover` | `hover:brightness-110` |
|
||||
| `text-foreground` | `text-[var(--text-primary)]` |
|
||||
| `text-muted-foreground` | `text-[var(--text-secondary)]` |
|
||||
| `bg-background` | `bg-[var(--bg-page)]` |
|
||||
| `bg-card` (old token) | `bg-[var(--bg-card)]` |
|
||||
| `border-border` | `border-[var(--border-default)]` |
|
||||
| `font-label` (on code/monospace content) | `font-mono` |
|
||||
| `font-label` (on labels/badges/timestamps) | `font-sans text-xs` — inspect each usage |
|
||||
| `backdrop-filter: blur(...)` | Remove |
|
||||
| `shadow-lg shadow-primary/20` on buttons | Remove |
|
||||
| `rounded-[10px]` / `rounded-[16px]` on cards | `rounded-lg` (8px) |
|
||||
| `scale(1.02)` hover on cards | Border color transition |
|
||||
| `.atmosphere-orb` elements | Remove from JSX |
|
||||
| `style={{ background: 'rgba(...)' }}` glass surfaces | Use CSS variable classes |
|
||||
|
||||
### Sweep Order (one commit per directory group, ~10 commits total)
|
||||
|
||||
Phase 3 covers app components only; landing page is handled in Phase 4.
|
||||
|
||||
1. **`components/layout/`** (3 files) — AppLayout, TopBar, BrandLogo
|
||||
2. **`components/dashboard/`** (16 files) — highest visibility
|
||||
3. **`components/flowpilot/`** (12 files) — core product
|
||||
4. **`pages/`** (25 files) — all page components
|
||||
5. **`components/session/`** (8 files)
|
||||
6. **`components/script-builder/`** (4 files)
|
||||
7. **`components/kb-accelerator/`** (5 files)
|
||||
8. **`components/account/`** (4 files)
|
||||
9. **`components/tree-editor/`** (4 files)
|
||||
10. **All remaining directories** (~51 files: admin, analytics, assistant, common, copilot, library, procedural, public, script-editor, ui)
|
||||
|
||||
### What Stays the Same
|
||||
|
||||
- Component logic, state management, hooks, API calls — untouched
|
||||
- Lucide icons — same
|
||||
- `cn()` utility — same
|
||||
- All non-design Tailwind utilities (flex, grid, padding, margin, etc.)
|
||||
- React Router, Zustand stores, type definitions
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Landing Page
|
||||
|
||||
- Remove ambient glow effects and gradient overlays from `landing.css`
|
||||
- Hero: subtle radial gradient at 15% accent opacity (replaces heavy glow)
|
||||
- Section labels: JetBrains Mono, 12px, uppercase, accent-text
|
||||
- Section titles: Bricolage Grotesque, `clamp(28px, 4vw, 42px)`, weight 800
|
||||
- Pricing cards: flat `bg-card` + `border-default`, featured card gets accent border
|
||||
- Top nav: flat, no blur backdrop
|
||||
- CTAs: solid accent buttons
|
||||
- Product mockup: update screenshot to show new flat UI
|
||||
|
||||
**Files:** `landing.css`, `LandingPage.tsx`, landing-specific components
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Cleanup & Documentation
|
||||
|
||||
- Remove compatibility shims from `index.css` (deprecated `glass-card` aliases from Phase 1)
|
||||
- Update CLAUDE.md: verify branding section and design system section reflect the new flat theme (already partially done in the doc-swap commit — verify no stale references remain)
|
||||
- Update `BrandWordmark.tsx` if it references `text-gradient-brand`
|
||||
- Verify `npm run build` passes with zero references to removed utilities
|
||||
|
||||
**Light mode note:** All colors use CSS custom properties. Light mode values from DESIGN-SYSTEM.md are stored as comments in `index.css`. Adding light mode later = add `.light` class values + toggle in user settings. Not implemented in this migration.
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Light mode toggle (future follow-up — just variable swap + settings toggle)
|
||||
- React Flow canvas theme updates (separate concern, works with any color scheme)
|
||||
- Backend changes (none needed)
|
||||
- New features or functionality (purely visual migration)
|
||||
- Test changes (no visual tests exist)
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
- Each phase produces deployable commits
|
||||
- The app is functional throughout (just visually inconsistent during Phase 3)
|
||||
- Old CSS utilities removed in Phase 1 — if a component breaks visually, it's obvious
|
||||
- `npm run build` verified after each phase
|
||||
- No logic changes = no new bugs in functionality
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After each phase:
|
||||
1. `npm run build` passes
|
||||
2. Visual spot-check of key pages (Dashboard, FlowPilot session, Script Builder, Landing)
|
||||
|
||||
After all phases:
|
||||
1. Full app walkthrough — every sidebar nav item
|
||||
2. Mobile responsive check (icon rail hidden, hamburger menu works)
|
||||
3. Landing page check
|
||||
4. FlowPilot session check (message bar, action bar positioning)
|
||||
3
frontend/package-lock.json
generated
3
frontend/package-lock.json
generated
@@ -18,7 +18,6 @@
|
||||
"@sentry/vite-plugin": "^5.1.1",
|
||||
"@stripe/stripe-js": "^8.7.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"axios": "^1.13.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -51,6 +50,7 @@
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"eslint": "^9.39.1",
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"@sentry/vite-plugin": "^5.1.1",
|
||||
"@stripe/stripe-js": "^8.7.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"axios": "^1.13.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -64,6 +63,7 @@
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"eslint": "^9.39.1",
|
||||
|
||||
@@ -36,7 +36,7 @@ export function DeleteAccountModal({ onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||
<div className="glass-card-static w-full max-w-md p-6">
|
||||
<div className="card-flat w-full max-w-md p-6">
|
||||
<div className="flex items-center gap-2 text-rose-500 mb-4">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold font-heading text-foreground">Delete Account</h2>
|
||||
@@ -55,7 +55,7 @@ export function DeleteAccountModal({ onClose }: Props) {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'mt-1 block w-full rounded-lg border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
/>
|
||||
@@ -68,8 +68,8 @@ export function DeleteAccountModal({ onClose }: Props) {
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'bg-white/[0.04] border border-brand-border text-foreground'
|
||||
'rounded-lg px-4 py-2 text-sm font-medium',
|
||||
'bg-input border border-border text-foreground'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -78,7 +78,7 @@ export function DeleteAccountModal({ onClose }: Props) {
|
||||
type="submit"
|
||||
disabled={isSubmitting || !password}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-semibold',
|
||||
'rounded-lg px-4 py-2 text-sm font-semibold',
|
||||
'bg-rose-500 text-white hover:bg-rose-400 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function LeaveAccountModal({ accountName, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||
<div className="glass-card-static w-full max-w-md p-6">
|
||||
<div className="card-flat w-full max-w-md p-6">
|
||||
<div className="flex items-center gap-2 text-amber-400 mb-4">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold font-heading text-foreground">Leave Account</h2>
|
||||
@@ -44,8 +44,8 @@ export function LeaveAccountModal({ accountName, onClose }: Props) {
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'bg-white/[0.04] border border-brand-border text-foreground'
|
||||
'rounded-lg px-4 py-2 text-sm font-medium',
|
||||
'bg-input border border-border text-foreground'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -54,7 +54,7 @@ export function LeaveAccountModal({ accountName, onClose }: Props) {
|
||||
onClick={handleLeave}
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-semibold',
|
||||
'rounded-lg px-4 py-2 text-sm font-semibold',
|
||||
'bg-rose-500 text-white hover:bg-rose-400 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -181,7 +181,7 @@ export function NotificationSettings() {
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)] transition-all'
|
||||
)}
|
||||
@@ -220,7 +220,7 @@ export function NotificationSettings() {
|
||||
|
||||
{/* Empty state */}
|
||||
{!loading && configs.length === 0 && !addingChannel && (
|
||||
<div className="glass-card-static p-6 text-center">
|
||||
<div className="card-flat p-6 text-center">
|
||||
<Bell className="mx-auto h-8 w-8 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No notification channels configured. Add a channel to receive alerts for session events.
|
||||
@@ -234,7 +234,7 @@ export function NotificationSettings() {
|
||||
{configs.map(config => {
|
||||
const Icon = CHANNEL_ICONS[config.channel]
|
||||
return (
|
||||
<div key={config.id} className="glass-card-static p-5">
|
||||
<div key={config.id} className="card-flat p-5">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Icon className="h-5 w-5 text-muted-foreground" />
|
||||
@@ -248,7 +248,7 @@ export function NotificationSettings() {
|
||||
)}
|
||||
/>
|
||||
<span className={cn(
|
||||
'text-xs font-label',
|
||||
'text-xs font-sans text-xs',
|
||||
config.is_active ? 'text-emerald-400' : 'text-muted-foreground'
|
||||
)}>
|
||||
{config.is_active ? 'Active' : 'Inactive'}
|
||||
@@ -259,7 +259,7 @@ export function NotificationSettings() {
|
||||
<div className="mb-4">
|
||||
{config.webhook_url && (
|
||||
<div>
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Webhook URL
|
||||
</span>
|
||||
<p className="mt-0.5 text-sm text-foreground font-mono">
|
||||
@@ -269,7 +269,7 @@ export function NotificationSettings() {
|
||||
)}
|
||||
{config.email_addresses && config.email_addresses.length > 0 && (
|
||||
<div>
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Email Addresses
|
||||
</span>
|
||||
<p className="mt-0.5 text-sm text-foreground">
|
||||
@@ -281,7 +281,7 @@ export function NotificationSettings() {
|
||||
|
||||
{/* Event toggles */}
|
||||
<div className="mb-4">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Events
|
||||
</span>
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-2">
|
||||
@@ -307,7 +307,7 @@ export function NotificationSettings() {
|
||||
<button
|
||||
onClick={() => handleToggleActive(config)}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-[10px] px-3 py-1.5 text-sm font-medium',
|
||||
'inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)] transition-all'
|
||||
)}
|
||||
@@ -324,7 +324,7 @@ export function NotificationSettings() {
|
||||
onClick={() => handleTest(config.id)}
|
||||
disabled={testingId === config.id}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-[10px] px-3 py-1.5 text-sm font-medium',
|
||||
'inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)] transition-all',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -362,7 +362,7 @@ export function NotificationSettings() {
|
||||
|
||||
{/* Inline add form */}
|
||||
{addingChannel && (
|
||||
<div className="glass-card-static p-5">
|
||||
<div className="card-flat p-5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{(() => {
|
||||
const Icon = CHANNEL_ICONS[addingChannel]
|
||||
@@ -375,7 +375,7 @@ export function NotificationSettings() {
|
||||
|
||||
{addingChannel === 'email' ? (
|
||||
<div>
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Email Addresses
|
||||
</label>
|
||||
<Input
|
||||
@@ -391,7 +391,7 @@ export function NotificationSettings() {
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Webhook URL
|
||||
</label>
|
||||
<Input
|
||||
@@ -413,9 +413,9 @@ export function NotificationSettings() {
|
||||
onClick={handleSaveNew}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-[10px] px-5 py-2.5 text-sm font-semibold',
|
||||
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 active:scale-[0.97] transition-all',
|
||||
'inline-flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-semibold',
|
||||
'bg-primary text-white',
|
||||
'hover:brightness-110 active:scale-[0.98] transition-all',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||
<div className="glass-card-static w-full max-w-md p-6">
|
||||
<div className="card-flat w-full max-w-md p-6">
|
||||
<div className="flex items-center gap-2 text-amber-400 mb-4">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold font-heading text-foreground">Transfer Ownership</h2>
|
||||
@@ -59,7 +59,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
value={targetUserId}
|
||||
onChange={(e) => setTargetUserId(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'mt-1 block w-full rounded-lg border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
>
|
||||
@@ -76,7 +76,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'mt-1 block w-full rounded-lg border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
/>
|
||||
@@ -89,8 +89,8 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'bg-white/[0.04] border border-brand-border text-foreground'
|
||||
'rounded-lg px-4 py-2 text-sm font-medium',
|
||||
'bg-input border border-border text-foreground'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -99,8 +99,8 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
type="submit"
|
||||
disabled={isSubmitting || !password}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-semibold',
|
||||
'bg-amber-500 text-brand-dark hover:bg-amber-400',
|
||||
'rounded-lg px-4 py-2 text-sm font-semibold',
|
||||
'bg-amber-500 text-white hover:bg-amber-400',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function AdminLayout() {
|
||||
{mobileOpen && (
|
||||
<div className="fixed inset-0 z-40 md:hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-card/80 backdrop-blur-xs"
|
||||
className="absolute inset-0 bg-card/80"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 w-60 border-r border-border bg-card shadow-xl">
|
||||
|
||||
@@ -58,7 +58,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
|
||||
btnBase,
|
||||
'px-2',
|
||||
p === page
|
||||
? 'bg-gradient-brand text-white shadow-lg shadow-primary/20'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ function getFlowCountStyle(count: number) {
|
||||
export default function CoverageHeatmap({ data }: CoverageHeatmapProps) {
|
||||
if (data.domains.length === 0) {
|
||||
return (
|
||||
<div className="glass-card-static p-6">
|
||||
<div className="card-flat p-6">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<MapPin size={16} className="text-foreground" />
|
||||
<h3 className="font-heading text-sm font-semibold text-foreground">Domain Coverage</h3>
|
||||
@@ -52,7 +52,7 @@ export default function CoverageHeatmap({ data }: CoverageHeatmapProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass-card-static p-3 sm:p-5">
|
||||
<div className="card-flat p-3 sm:p-5">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<MapPin size={16} className="text-foreground" />
|
||||
@@ -67,25 +67,25 @@ export default function CoverageHeatmap({ data }: CoverageHeatmapProps) {
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-3 py-2 text-left font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<th className="px-3 py-2 text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Domain
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Flows
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Sessions
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Resolution %
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Escalation %
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Guided %
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground"
|
||||
<th className="px-3 py-2 text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground"
|
||||
title="Average time to resolve sessions in this domain. Green: <10 min, Amber: 10–20 min, Red: >20 min. Lower is better.">
|
||||
Avg Resolution
|
||||
</th>
|
||||
@@ -176,7 +176,7 @@ export default function CoverageHeatmap({ data }: CoverageHeatmapProps) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mt-3 text-[0.625rem] font-label text-muted-foreground">
|
||||
<div className="flex flex-wrap gap-4 mt-3 text-[0.625rem] font-sans text-xs text-muted-foreground">
|
||||
<span><span className="inline-block w-2 h-2 rounded-full bg-emerald-400 mr-1" />Good</span>
|
||||
<span><span className="inline-block w-2 h-2 rounded-full bg-amber-400 mr-1" />Needs Improvement</span>
|
||||
<span><span className="inline-block w-2 h-2 rounded-full bg-rose-500 mr-1" />Critical</span>
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function FlowQualityTable({ data }: FlowQualityTableProps) {
|
||||
|
||||
if (data.flows.length === 0) {
|
||||
return (
|
||||
<div className="glass-card-static p-3 sm:p-5">
|
||||
<div className="card-flat p-3 sm:p-5">
|
||||
<div className="flex items-center justify-center min-h-[200px]">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No flows found for this account. Create your first flow to start tracking quality.
|
||||
@@ -130,7 +130,7 @@ export default function FlowQualityTable({ data }: FlowQualityTableProps) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="glass-card-static p-3 sm:p-5">
|
||||
<div className="card-flat p-3 sm:p-5">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-heading text-sm font-semibold text-foreground">
|
||||
@@ -160,7 +160,7 @@ export default function FlowQualityTable({ data }: FlowQualityTableProps) {
|
||||
return (
|
||||
<th
|
||||
key={col.key}
|
||||
className="text-left py-2 px-2 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground cursor-pointer select-none hover:text-foreground transition-colors"
|
||||
className="text-left py-2 px-2 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground cursor-pointer select-none hover:text-foreground transition-colors"
|
||||
onClick={() => handleSort(col.key)}
|
||||
title={col.title}
|
||||
>
|
||||
@@ -221,11 +221,11 @@ function FlowRow({
|
||||
>
|
||||
{flow.name}
|
||||
</Link>
|
||||
<span className={cn('font-label text-[0.5rem] uppercase tracking-wider ml-2 shrink-0', TYPE_LABELS[flow.tree_type]?.color || 'text-muted-foreground')}>
|
||||
<span className={cn('font-sans text-xs text-[0.5rem] uppercase tracking-wider ml-2 shrink-0', TYPE_LABELS[flow.tree_type]?.color || 'text-muted-foreground')}>
|
||||
{TYPE_LABELS[flow.tree_type]?.label || flow.tree_type}
|
||||
</span>
|
||||
{needsAttention && (
|
||||
<span className="shrink-0 bg-amber-400/10 text-amber-400 font-label text-[0.625rem] px-1.5 py-0.5 rounded">
|
||||
<span className="shrink-0 bg-amber-400/10 text-amber-400 font-sans text-xs text-[0.625rem] px-1.5 py-0.5 rounded">
|
||||
Needs attention
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) {
|
||||
|
||||
if (!hasActivity) {
|
||||
return (
|
||||
<div className="glass-card-static p-3 sm:p-5">
|
||||
<div className="card-flat p-3 sm:p-5">
|
||||
<div className="flex items-center justify-center min-h-[200px]">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No PSA activity data for this period. Link sessions to PSA tickets to see metrics.
|
||||
@@ -33,19 +33,19 @@ export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) {
|
||||
<div className="space-y-4">
|
||||
{/* Row 1 — Metric cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="glass-card-static p-4">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-[#5a6170]">Time Entries</p>
|
||||
<p className="text-gradient-brand font-heading text-2xl mt-1">{data.total_time_entries}</p>
|
||||
<div className="card-flat p-4">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-text-muted">Time Entries</p>
|
||||
<p className="text-accent-text font-heading text-2xl mt-1">{data.total_time_entries}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">logged to PSA</p>
|
||||
</div>
|
||||
<div className="glass-card-static p-4">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-[#5a6170]">Hours Logged</p>
|
||||
<p className="text-gradient-brand font-heading text-2xl mt-1">{data.total_hours_logged.toFixed(1)}</p>
|
||||
<div className="card-flat p-4">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-text-muted">Hours Logged</p>
|
||||
<p className="text-accent-text font-heading text-2xl mt-1">{data.total_hours_logged.toFixed(1)}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">total hours tracked</p>
|
||||
</div>
|
||||
<div className="glass-card-static p-4">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-[#5a6170]">Avg Hours/Session</p>
|
||||
<p className="text-gradient-brand font-heading text-2xl mt-1">{data.avg_hours_per_session.toFixed(2)}</p>
|
||||
<div className="card-flat p-4">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-text-muted">Avg Hours/Session</p>
|
||||
<p className="text-accent-text font-heading text-2xl mt-1">{data.avg_hours_per_session.toFixed(2)}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">per resolved session</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) {
|
||||
|
||||
{/* Row 3 — Daily Trend Chart */}
|
||||
{data.daily_trend.length > 0 && (
|
||||
<div className="glass-card-static p-3 sm:p-5">
|
||||
<div className="card-flat p-3 sm:p-5">
|
||||
<h3 className="font-heading text-sm font-semibold text-foreground mb-4">
|
||||
PSA Activity Trend
|
||||
</h3>
|
||||
@@ -129,7 +129,7 @@ function FunnelCard({ funnel }: { funnel: PsaFunnel }) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="glass-card-static p-3 sm:p-5">
|
||||
<div className="card-flat p-3 sm:p-5">
|
||||
<h3 className="font-heading text-sm font-semibold text-foreground mb-4">
|
||||
Documentation Push Funnel
|
||||
</h3>
|
||||
@@ -139,7 +139,7 @@ function FunnelCard({ funnel }: { funnel: PsaFunnel }) {
|
||||
{steps.map((step, i) => (
|
||||
<div key={step.label} className="flex items-center gap-2 flex-1">
|
||||
<div className="bg-primary/5 border border-primary/20 rounded-lg p-3 text-center flex-1">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{step.label}
|
||||
</p>
|
||||
<p className="text-lg font-heading text-foreground">{step.count}</p>
|
||||
@@ -147,7 +147,7 @@ function FunnelCard({ funnel }: { funnel: PsaFunnel }) {
|
||||
{i < steps.length - 1 && (
|
||||
<div className="flex flex-col items-center shrink-0 px-1">
|
||||
<ArrowRight size={14} className="text-muted-foreground" />
|
||||
<span className="text-[0.625rem] text-muted-foreground font-label">
|
||||
<span className="text-[0.625rem] text-muted-foreground font-sans text-xs">
|
||||
{funnelPct(steps[i].count, steps[i + 1].count)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ function FunnelCard({ funnel }: { funnel: PsaFunnel }) {
|
||||
{steps.map((step, i) => (
|
||||
<div key={step.label} className="flex flex-col items-center gap-2 w-full">
|
||||
<div className="bg-primary/5 border border-primary/20 rounded-lg p-3 text-center w-full">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{step.label}
|
||||
</p>
|
||||
<p className="text-lg font-heading text-foreground">{step.count}</p>
|
||||
@@ -169,7 +169,7 @@ function FunnelCard({ funnel }: { funnel: PsaFunnel }) {
|
||||
{i < steps.length - 1 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowDown size={14} className="text-muted-foreground" />
|
||||
<span className="text-[0.625rem] text-muted-foreground font-label">
|
||||
<span className="text-[0.625rem] text-muted-foreground font-sans text-xs">
|
||||
{funnelPct(steps[i].count, steps[i + 1].count)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps)
|
||||
className={`rounded-2xl px-4 py-3 text-[0.875rem] leading-relaxed ${
|
||||
role === 'user'
|
||||
? 'bg-primary/15 text-foreground'
|
||||
: 'bg-white/[0.04] text-foreground border border-brand-border'
|
||||
: 'bg-input text-foreground border border-border'
|
||||
}`}
|
||||
>
|
||||
<MarkdownContent content={content} className="text-[0.875rem] leading-relaxed" />
|
||||
@@ -38,7 +38,7 @@ export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps)
|
||||
{/* Suggested flows (assistant only) */}
|
||||
{role === 'assistant' && suggestedFlows && suggestedFlows.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
Related Flows
|
||||
</span>
|
||||
{suggestedFlows.map(flow => (
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ChatSidebar({
|
||||
<div className="px-4 py-3 border-b shrink-0" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<button
|
||||
onClick={onNewChat}
|
||||
className="w-full flex items-center justify-center gap-2 bg-gradient-brand text-brand-dark font-semibold text-sm rounded-[10px] px-4 py-2.5 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
className="w-full flex items-center justify-center gap-2 bg-primary text-white font-semibold text-sm rounded-lg px-4 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
<Plus size={16} />
|
||||
New Chat
|
||||
@@ -42,7 +42,7 @@ export function ChatSidebar({
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
{pinnedChats.length > 0 && (
|
||||
<div className="px-3 mb-1">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
Pinned
|
||||
</span>
|
||||
</div>
|
||||
@@ -102,8 +102,8 @@ function ChatItem({
|
||||
className={cn(
|
||||
'group flex items-center gap-2 px-3 py-2.5 mx-1.5 rounded-lg cursor-pointer transition-colors',
|
||||
isActive
|
||||
? 'bg-primary/10 text-foreground'
|
||||
: 'text-muted-foreground hover:bg-white/[0.04] hover:text-foreground'
|
||||
? 'bg-accent-dim text-foreground'
|
||||
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<MessageSquare size={14} className="shrink-0" />
|
||||
|
||||
@@ -136,13 +136,13 @@ export function ConcludeSessionModal({
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-xs"
|
||||
className="absolute inset-0 bg-black/60"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div
|
||||
className="relative w-full max-w-2xl mx-4 glass-card-static overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
||||
className="relative w-full max-w-2xl mx-4 card-flat overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 4rem)',
|
||||
display: 'flex',
|
||||
@@ -155,7 +155,7 @@ export function ConcludeSessionModal({
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<div className="w-9 h-9 rounded-xl bg-accent-dim flex items-center justify-center">
|
||||
<ClipboardList size={18} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -169,7 +169,7 @@ export function ConcludeSessionModal({
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -194,9 +194,9 @@ export function ConcludeSessionModal({
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'w-6 h-6 rounded-full flex items-center justify-center text-[0.6875rem] font-label font-medium transition-colors',
|
||||
'w-6 h-6 rounded-full flex items-center justify-center text-[0.6875rem] font-sans text-xs font-medium transition-colors',
|
||||
step === s
|
||||
? 'bg-gradient-brand text-brand-dark'
|
||||
? 'bg-primary text-white'
|
||||
: (i < ['select-outcome', 'add-notes', 'summary'].indexOf(step))
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-brand-border text-muted-foreground'
|
||||
@@ -206,7 +206,7 @@ export function ConcludeSessionModal({
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-label',
|
||||
'text-xs font-sans text-xs',
|
||||
step === s ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
@@ -233,8 +233,8 @@ export function ConcludeSessionModal({
|
||||
className={cn(
|
||||
'w-full flex items-center gap-4 p-4 rounded-xl border transition-all text-left',
|
||||
'hover:scale-[1.01] active:scale-[0.99]',
|
||||
'bg-white/[0.02] border-brand-border',
|
||||
'hover:border-white/[0.12] hover:bg-white/[0.04]'
|
||||
'bg-card border-border',
|
||||
'hover:border-border-hover hover:bg-input'
|
||||
)}
|
||||
>
|
||||
<div className={cn('w-10 h-10 rounded-xl flex items-center justify-center', o.bg)}>
|
||||
@@ -255,7 +255,7 @@ export function ConcludeSessionModal({
|
||||
<div className="space-y-4">
|
||||
{/* Selected outcome badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn('px-3 py-1.5 rounded-lg flex items-center gap-2 text-xs font-label', selectedOutcome.bg, selectedOutcome.border, 'border')}>
|
||||
<div className={cn('px-3 py-1.5 rounded-lg flex items-center gap-2 text-xs font-sans text-xs', selectedOutcome.bg, selectedOutcome.border, 'border')}>
|
||||
<selectedOutcome.icon size={14} className={selectedOutcome.color} />
|
||||
<span className={selectedOutcome.color}>{selectedOutcome.label}</span>
|
||||
</div>
|
||||
@@ -268,7 +268,7 @@ export function ConcludeSessionModal({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground block mb-2">
|
||||
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground block mb-2">
|
||||
Additional Notes (optional)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -300,7 +300,7 @@ export function ConcludeSessionModal({
|
||||
<div className="space-y-4">
|
||||
{/* Outcome badge */}
|
||||
{selectedOutcome && (
|
||||
<div className={cn('px-3 py-1.5 rounded-lg inline-flex items-center gap-2 text-xs font-label', selectedOutcome.bg, selectedOutcome.border, 'border')}>
|
||||
<div className={cn('px-3 py-1.5 rounded-lg inline-flex items-center gap-2 text-xs font-sans text-xs', selectedOutcome.bg, selectedOutcome.border, 'border')}>
|
||||
<selectedOutcome.icon size={14} className={selectedOutcome.color} />
|
||||
<span className={selectedOutcome.color}>{selectedOutcome.label}</span>
|
||||
</div>
|
||||
@@ -308,11 +308,11 @@ export function ConcludeSessionModal({
|
||||
|
||||
{/* Generated summary */}
|
||||
<div
|
||||
className="rounded-xl border p-5 bg-white/[0.02]"
|
||||
className="rounded-xl border p-5 bg-card"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||
<Sparkles size={10} className="text-primary" />
|
||||
Generated Ticket Notes
|
||||
</span>
|
||||
@@ -335,7 +335,7 @@ export function ConcludeSessionModal({
|
||||
<div />
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-white/[0.04] border border-brand-border hover:border-white/[0.12] transition-all"
|
||||
className="px-4 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground bg-input border border-border hover:border-border-hover transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -346,14 +346,14 @@ export function ConcludeSessionModal({
|
||||
<>
|
||||
<button
|
||||
onClick={() => setStep('select-outcome')}
|
||||
className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-white/[0.04] border border-brand-border hover:border-white/[0.12] transition-all"
|
||||
className="px-4 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground bg-input border border-border hover:border-border-hover transition-all"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
className="flex items-center gap-2 bg-gradient-brand text-brand-dark font-semibold text-sm rounded-[10px] px-5 py-2.5 hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-50"
|
||||
className="flex items-center gap-2 bg-primary text-white font-semibold text-sm rounded-lg px-5 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-50"
|
||||
>
|
||||
{generating ? (
|
||||
<>
|
||||
@@ -376,7 +376,7 @@ export function ConcludeSessionModal({
|
||||
{outcome === 'paused' && (
|
||||
<button
|
||||
onClick={handleResumeNew}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-[10px] text-sm font-medium text-blue-400 bg-blue-400/10 border border-blue-400/20 hover:bg-blue-400/15 transition-all"
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium text-blue-400 bg-blue-400/10 border border-blue-400/20 hover:bg-blue-400/15 transition-all"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
Resume in New Chat
|
||||
@@ -387,10 +387,10 @@ export function ConcludeSessionModal({
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2.5 rounded-[10px] text-sm font-semibold transition-all',
|
||||
'flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-semibold transition-all',
|
||||
copied
|
||||
? 'bg-emerald-400/15 text-emerald-400 border border-emerald-400/30'
|
||||
: 'bg-gradient-brand text-brand-dark hover:opacity-90 active:scale-[0.97]'
|
||||
: 'bg-primary text-white hover:brightness-110 active:scale-[0.98]'
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
@@ -407,7 +407,7 @@ export function ConcludeSessionModal({
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2.5 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-white/[0.04] border border-brand-border hover:border-white/[0.12] transition-all"
|
||||
className="px-4 py-2.5 rounded-lg text-sm text-muted-foreground hover:text-foreground bg-input border border-border hover:border-border-hover transition-all"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function SuggestedFlowCard({ flow }: SuggestedFlowCardProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="w-full text-left glass-card-static p-3 rounded-xl hover:border-white/[0.12] transition-colors group"
|
||||
className="w-full text-left card-flat p-3 rounded-xl hover:border-border-hover transition-colors group"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<Box size={14} className="text-primary mt-0.5 shrink-0" />
|
||||
@@ -27,7 +27,7 @@ export function SuggestedFlowCard({ flow }: SuggestedFlowCardProps) {
|
||||
<span className="text-[0.8125rem] font-medium text-foreground truncate">
|
||||
{flow.tree_name}
|
||||
</span>
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||
{flow.tree_type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -5,37 +5,36 @@ interface BrandLogoProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Brand logo mark: gradient cyan square with rounded corners
|
||||
* containing a white lightning bolt.
|
||||
*/
|
||||
export function BrandLogo({ size = 'sm', className }: BrandLogoProps) {
|
||||
const sizeClasses = size === 'sm' ? 'h-8 w-8' : 'h-20 w-20'
|
||||
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 }
|
||||
const vb = size === 'sm' ? '0 0 40 40' : '0 0 80 80'
|
||||
const s = size === 'sm' ? 1 : 2
|
||||
const gradId = size === 'sm' ? 'logoGradSm' : 'logoGradLg'
|
||||
const gradEnd = String(40 * (size === 'sm' ? 1 : 2))
|
||||
const dim = size === 'sm' ? 30 : 64
|
||||
|
||||
return (
|
||||
<svg viewBox={vb} fill="none" className={cn(sizeClasses, className)}>
|
||||
<defs>
|
||||
<linearGradient id={gradId} x1="0" y1="0" x2={gradEnd} y2={gradEnd} gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stopColor="#06b6d4" />
|
||||
<stop offset="100%" stopColor="#22d3ee" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx={5 * s} cy={7 * s} r={nodeR.outer} fill={`url(#${gradId})`} opacity="0.5" />
|
||||
<circle cx={5 * s} cy={15 * s} r={nodeR.inner} fill={`url(#${gradId})`} opacity="0.7" />
|
||||
<circle cx={5 * s} cy={25 * s} r={nodeR.inner} fill={`url(#${gradId})`} opacity="0.7" />
|
||||
<circle cx={5 * s} cy={33 * s} r={nodeR.outer} fill={`url(#${gradId})`} opacity="0.5" />
|
||||
<path d={`M${7.5 * s} ${7 * s}L${14 * s} ${17 * s}`} stroke={`url(#${gradId})`} strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.4" />
|
||||
<path d={`M${7.75 * s} ${15 * s}L${14 * s} ${19 * s}`} stroke={`url(#${gradId})`} strokeWidth={strokeBase} strokeLinecap="round" opacity="0.5" />
|
||||
<path d={`M${7.75 * s} ${25 * s}L${14 * s} ${21 * s}`} stroke={`url(#${gradId})`} strokeWidth={strokeBase} strokeLinecap="round" opacity="0.5" />
|
||||
<path d={`M${7.5 * s} ${33 * s}L${14 * s} ${23 * s}`} stroke={`url(#${gradId})`} strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.4" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.glow} fill={`url(#${gradId})`} opacity="0.15" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.solid} fill={`url(#${gradId})`} opacity="0.9" />
|
||||
<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(#${gradId})`} strokeWidth={strokeThick} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<div
|
||||
className={cn('shrink-0 flex items-center justify-center', className)}
|
||||
style={{
|
||||
width: dim,
|
||||
height: dim,
|
||||
borderRadius: size === 'sm' ? 8 : 14,
|
||||
background: 'linear-gradient(135deg, #06b6d4, #22d3ee)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
style={{ width: dim * 0.5, height: dim * 0.5 }}
|
||||
>
|
||||
<path
|
||||
d="M13 2L4.5 13.5H12L11 22L19.5 10.5H12L13 2Z"
|
||||
fill="white"
|
||||
stroke="white"
|
||||
strokeWidth="0.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,13 +9,12 @@ export function BrandWordmark({ size = 'sm', className }: BrandWordmarkProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-heading font-bold tracking-tight',
|
||||
'font-heading font-bold tracking-tight text-text-heading',
|
||||
size === 'sm' ? 'text-xl' : 'text-3xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground">Resolution</span>
|
||||
<span className="text-gradient-brand">Flow</span>
|
||||
ResolutionFlow
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ConfirmDialog({
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
confirmVariant === 'destructive'
|
||||
? 'bg-red-400/10 text-red-400 hover:bg-red-400/20 border border-red-400/20'
|
||||
: 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90'
|
||||
: 'bg-primary text-white hover:brightness-110'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Processing...' : confirmLabel}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function ContextMenu({ position, items, onClose }: ContextMenuProps) {
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
}}
|
||||
className="min-w-[200px] rounded-xl border border-border bg-card p-1 shadow-lg backdrop-blur-md"
|
||||
className="min-w-[200px] rounded-xl border border-border bg-card p-1 shadow-lg"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={item.id}>
|
||||
@@ -87,7 +87,7 @@ export function ContextMenu({ position, items, onClose }: ContextMenuProps) {
|
||||
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors',
|
||||
item.variant === 'danger'
|
||||
? 'text-rose-400 hover:bg-rose-500/10'
|
||||
: 'text-foreground hover:bg-brand-border'
|
||||
: 'text-foreground hover:bg-border'
|
||||
)}
|
||||
>
|
||||
{item.icon && (
|
||||
|
||||
@@ -65,7 +65,7 @@ export function CreateFlowDropdown({
|
||||
<div className={cn('relative', className)}>
|
||||
<button
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className="flex items-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity"
|
||||
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-white hover:brightness-110 transition-opacity"
|
||||
>
|
||||
<Plus size={16} />
|
||||
{label}
|
||||
@@ -74,7 +74,7 @@ export function CreateFlowDropdown({
|
||||
{showMenu && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setShowMenu(false)} />
|
||||
<div className="absolute right-0 z-20 mt-1 w-64 rounded-lg border border-border bg-card p-1 shadow-xl backdrop-blur-xs">
|
||||
<div className="absolute right-0 z-20 mt-1 w-64 rounded-lg border border-border bg-card p-1 shadow-xl">
|
||||
{/* Troubleshooting */}
|
||||
<Link
|
||||
to="/trees/new"
|
||||
|
||||
@@ -125,7 +125,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-xs"
|
||||
className="absolute inset-0 bg-black/80"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function TagBadges({
|
||||
}}
|
||||
disabled={!onTagClick}
|
||||
className={cn(
|
||||
'rounded-full font-label transition-colors',
|
||||
'rounded-full font-sans text-xs transition-colors',
|
||||
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||
variant === 'default'
|
||||
? 'bg-accent text-muted-foreground hover:bg-accent'
|
||||
@@ -48,7 +48,7 @@ export function TagBadges({
|
||||
{hiddenCount > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-full font-label',
|
||||
'rounded-full font-sans text-xs',
|
||||
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||
'bg-accent/50 text-muted-foreground'
|
||||
)}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-1.5 rounded-lg hover:bg-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
@@ -124,7 +124,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${
|
||||
msg.role === 'user'
|
||||
? 'bg-primary/15 text-foreground'
|
||||
: 'bg-white/[0.04] text-foreground border border-brand-border'
|
||||
: 'bg-input text-foreground border border-border'
|
||||
}`}
|
||||
>
|
||||
<MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" />
|
||||
@@ -133,7 +133,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white/[0.04] border border-brand-border rounded-xl px-3.5 py-2.5">
|
||||
<div className="bg-input border border-border rounded-xl px-3.5 py-2.5">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,7 +142,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
{/* Suggested flows */}
|
||||
{suggestedFlows.length > 0 && (
|
||||
<div className="space-y-2 pt-2">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
Related Flows
|
||||
</span>
|
||||
{suggestedFlows.map(flow => (
|
||||
@@ -171,7 +171,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || loading || initializing}
|
||||
className="bg-gradient-brand text-brand-dark p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
className="bg-primary text-white p-2.5 rounded-xl hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-40"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function CopilotToggle({ isOpen, onToggle }: CopilotToggleProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="fixed bottom-6 right-6 z-40 bg-gradient-brand text-brand-dark p-3.5 rounded-full shadow-lg shadow-primary/30 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
className="fixed bottom-6 right-6 z-40 bg-primary text-white p-3.5 rounded-full hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
title="Open AI Copilot"
|
||||
>
|
||||
<MessageCircle size={22} />
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ActiveFlowPilotSessions() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="glass-card-static">
|
||||
<div className="card-flat">
|
||||
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ export function ActiveFlowPilotSessions() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass-card-static">
|
||||
<div className="card-flat">
|
||||
<div
|
||||
className="flex items-center justify-between px-5 py-3"
|
||||
style={{ borderBottom: '1px solid var(--glass-border)' }}
|
||||
@@ -51,7 +51,7 @@ export function ActiveFlowPilotSessions() {
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
|
||||
{sessions.length > 0 && (
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary/10 px-1.5 text-[0.625rem] font-bold text-primary">
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent-dim px-1.5 text-[0.625rem] font-bold text-primary">
|
||||
{sessions.length}
|
||||
</span>
|
||||
)}
|
||||
@@ -67,7 +67,7 @@ export function ActiveFlowPilotSessions() {
|
||||
{sessions.length === 0 ? (
|
||||
<div className="px-5 py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">No active sessions</p>
|
||||
<p className="mt-1 text-[0.6875rem] text-[#5a6170]">Start typing above to begin troubleshooting</p>
|
||||
<p className="mt-1 text-[0.6875rem] text-text-muted">Start typing above to begin troubleshooting</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
|
||||
@@ -75,13 +75,13 @@ export function ActiveFlowPilotSessions() {
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() => navigate(`/pilot/${session.id}`)}
|
||||
className="glass-card p-4 text-left"
|
||||
className="card-interactive p-4 text-left"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<Sparkles size={14} className="shrink-0 text-primary mt-0.5" />
|
||||
<span
|
||||
className={cn(
|
||||
'font-label text-[0.5625rem] uppercase px-1.5 py-0.5 rounded',
|
||||
'font-sans text-xs text-[0.5625rem] uppercase px-1.5 py-0.5 rounded',
|
||||
session.confidence_tier === 'guided' && 'bg-emerald-400/10 text-emerald-400',
|
||||
session.confidence_tier === 'exploring' && 'bg-amber-400/10 text-amber-400',
|
||||
session.confidence_tier === 'discovery' && 'bg-blue-400/10 text-blue-400',
|
||||
|
||||
@@ -22,7 +22,7 @@ export function FiltersBar({ filters, activeFilter, onFilterChange }: FiltersBar
|
||||
className={cn(
|
||||
'shrink-0 rounded-lg border px-3 py-1.5 text-[0.8125rem] font-medium transition-colors',
|
||||
activeFilter === f.id
|
||||
? 'border-primary/30 bg-primary/10 text-primary'
|
||||
? 'border-[#22d3ee]/30 bg-accent-dim text-primary'
|
||||
: 'border-border bg-card text-muted-foreground hover:border-border/80 hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function KnowledgeBaseCards() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="glass-card-static">
|
||||
<div className="card-flat">
|
||||
<div
|
||||
className="flex items-center justify-between px-5 py-3"
|
||||
style={{ borderBottom: '1px solid var(--glass-border)' }}
|
||||
@@ -38,11 +38,14 @@ export function KnowledgeBaseCards() {
|
||||
<button
|
||||
key={item.label}
|
||||
onClick={() => navigate(item.href)}
|
||||
className="flex flex-col items-center gap-2 py-5 hover:bg-[rgba(255,255,255,0.02)] transition-colors"
|
||||
className="flex flex-col items-center gap-2 py-5 rounded-lg hover:bg-card-hover transition-all duration-350"
|
||||
style={{ transition: 'transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1), background 200ms ease' }}
|
||||
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)' }}
|
||||
>
|
||||
<item.icon size={20} style={{ color: item.color }} />
|
||||
<p className="font-heading text-xl font-extrabold text-foreground">{item.value}</p>
|
||||
<p className="font-label text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{item.label}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -69,11 +69,11 @@ export function OnboardingChecklist() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass-card overflow-hidden fade-in" style={{ animationDelay: '150ms' }}>
|
||||
<div className="card-interactive overflow-hidden fade-in" style={{ animationDelay: '150ms' }}>
|
||||
{/* Progress bar */}
|
||||
<div className="h-1 w-full bg-[rgba(255,255,255,0.04)]">
|
||||
<div
|
||||
className="h-full bg-gradient-brand transition-all duration-500 ease-out"
|
||||
className="h-full bg-primary transition-all duration-500 ease-out"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -82,15 +82,15 @@ export function OnboardingChecklist() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Getting Started
|
||||
</p>
|
||||
<p className="text-sm text-foreground mt-0.5">
|
||||
{isAllDone ? (
|
||||
<span className="text-gradient-brand font-semibold">You're all set!</span>
|
||||
<span className="text-accent-text font-semibold">You're all set!</span>
|
||||
) : (
|
||||
<span>
|
||||
<span className="text-gradient-brand font-semibold">{completedCount}</span>
|
||||
<span className="text-accent-text font-semibold">{completedCount}</span>
|
||||
{' '}of {totalCount} complete
|
||||
</span>
|
||||
)}
|
||||
@@ -126,11 +126,11 @@ export function OnboardingChecklist() {
|
||||
className={cn(
|
||||
'flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition-colors',
|
||||
done
|
||||
? 'bg-gradient-brand border-transparent'
|
||||
? 'bg-primary border-transparent'
|
||||
: 'border-border'
|
||||
)}
|
||||
>
|
||||
{done && <Check size={12} className="text-[#101114]" />}
|
||||
{done && <Check size={12} className="text-white" />}
|
||||
</span>
|
||||
|
||||
{/* Label */}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface OpenSessionsProps {
|
||||
|
||||
export function OpenSessions({ sessions }: OpenSessionsProps) {
|
||||
return (
|
||||
<div className="glass-card-static flex flex-col h-full">
|
||||
<div className="card-flat flex flex-col h-full">
|
||||
<div className="flex items-center justify-between px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">My Open Sessions</h3>
|
||||
<Link to="/sessions" className="text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors">
|
||||
@@ -46,13 +46,13 @@ export function OpenSessions({ sessions }: OpenSessionsProps) {
|
||||
? `Step ${session.stepNumber} of ${session.totalSteps}`
|
||||
: 'In progress'}
|
||||
<span className="mx-1.5 text-[var(--text-dimmed)]">·</span>
|
||||
<span className="font-label text-[0.625rem]">{session.timeAgo}</span>
|
||||
<span className="font-sans text-xs text-[0.625rem]">{session.timeAgo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={getTreeNavigatePath(session.treeId, session.treeType)}
|
||||
state={{ sessionId: session.id }}
|
||||
className="shrink-0 rounded-lg bg-gradient-brand px-3 py-1 text-[0.6875rem] font-medium text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
className="shrink-0 rounded-lg border border-primary/40 px-3 py-1 text-[0.6875rem] font-medium text-primary hover:bg-primary/10 hover:border-primary/60 transition-colors"
|
||||
>
|
||||
Resume
|
||||
</Link>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function PendingEscalations() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="glass-card-static overflow-hidden"
|
||||
className="card-flat overflow-hidden"
|
||||
style={{ borderColor: 'rgba(251, 191, 36, 0.2)' }}
|
||||
>
|
||||
<div
|
||||
@@ -70,7 +70,7 @@ export function PendingEscalations() {
|
||||
<div className="text-[0.6875rem] text-muted-foreground">
|
||||
{esc.problem_domain || 'General'}
|
||||
<span className="mx-1.5 text-[var(--text-dimmed)]">·</span>
|
||||
<span className="font-label text-[0.625rem]">{timeAgo(esc.created_at)}</span>
|
||||
<span className="font-sans text-xs">{timeAgo(esc.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -70,21 +70,21 @@ export function PerformanceCards() {
|
||||
<button
|
||||
key={card.label}
|
||||
onClick={() => navigate(card.href)}
|
||||
className={cn(
|
||||
'glass-card p-4 text-left fade-in',
|
||||
i === 0 && 'active-glow'
|
||||
)}
|
||||
style={{ animationDelay: `${400 + i * 60}ms` }}
|
||||
className="card-interactive p-4 text-left fade-in"
|
||||
style={{
|
||||
animationDelay: `${400 + i * 60}ms`,
|
||||
borderLeft: `3px solid ${card.iconColor}`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="font-label text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{card.label}
|
||||
</p>
|
||||
<card.icon size={14} style={{ color: card.iconColor }} />
|
||||
</div>
|
||||
<p className={cn(
|
||||
'font-heading text-2xl font-extrabold tracking-tight',
|
||||
card.highlight ? 'text-gradient-brand' : 'text-foreground'
|
||||
card.highlight ? 'text-accent-text' : 'text-foreground'
|
||||
)}>
|
||||
{card.value}
|
||||
</p>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function PreparedSessions() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass-card-static p-5 fade-in" style={{ animationDelay: '200ms' }}>
|
||||
<div className="card-flat p-5 fade-in" style={{ animationDelay: '200ms' }}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClipboardList className="h-4 w-4 text-cyan-400" />
|
||||
|
||||
@@ -12,7 +12,7 @@ export function QuickActions() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="glass-card-static flex flex-col h-full">
|
||||
<div className="card-flat flex flex-col h-full">
|
||||
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Quick Actions</h3>
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@ export function QuickActions() {
|
||||
<button
|
||||
key={label}
|
||||
onClick={() => navigate(href)}
|
||||
className="glass-card flex items-center gap-3 px-4 py-3 text-left"
|
||||
className="card-interactive flex items-center gap-3 px-4 py-3 text-left"
|
||||
>
|
||||
<span
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
|
||||
|
||||
@@ -18,16 +18,16 @@ export function QuickStats({ stats }: QuickStatsProps) {
|
||||
{stats.map((stat, i) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className={cn('glass-card p-4 fade-in', i === 0 && 'active-glow')}
|
||||
className={cn('card-interactive p-4 fade-in')}
|
||||
style={{ animationDelay: `${50 + i * 30}ms` }}
|
||||
>
|
||||
<p className="font-label text-[0.625rem] font-medium uppercase tracking-widest text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.625rem] font-medium uppercase tracking-widest text-muted-foreground">
|
||||
{stat.label}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
'mt-1 font-heading text-2xl font-extrabold tracking-tight',
|
||||
stat.gradient && 'text-gradient-brand',
|
||||
stat.gradient && 'text-accent-text',
|
||||
stat.color
|
||||
)}
|
||||
style={stat.color && !stat.color.startsWith('text-') ? { color: stat.color } : undefined}
|
||||
|
||||
@@ -24,7 +24,7 @@ const DEFAULT_ACTIVITIES: ActivityItem[] = [
|
||||
|
||||
export function RecentActivity({ activities = DEFAULT_ACTIVITIES }: RecentActivityProps) {
|
||||
return (
|
||||
<div className="glass-card-static">
|
||||
<div className="card-flat">
|
||||
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Recent Activity</h3>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@ export function RecentActivity({ activities = DEFAULT_ACTIVITIES }: RecentActivi
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px]"
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{ background: item.iconBg }}
|
||||
>
|
||||
<item.icon size={16} style={{ color: item.iconColor }} />
|
||||
@@ -47,7 +47,7 @@ export function RecentActivity({ activities = DEFAULT_ACTIVITIES }: RecentActivi
|
||||
<div className="flex-1 min-w-0 pt-0.5">
|
||||
<p className="text-sm text-foreground">{item.description}</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-label text-[0.625rem] text-muted-foreground pt-1">
|
||||
<span className="shrink-0 font-sans text-xs text-[0.625rem] text-muted-foreground pt-1">
|
||||
{item.timestamp}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function RecentFlowPilotSessions() {
|
||||
if (sessions.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="glass-card-static">
|
||||
<div className="card-flat">
|
||||
<div
|
||||
className="flex items-center justify-between px-5 py-3"
|
||||
style={{ borderBottom: '1px solid var(--glass-border)' }}
|
||||
@@ -73,7 +73,7 @@ export function RecentFlowPilotSessions() {
|
||||
{session.problem_summary || 'Session'}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-label text-[0.625rem] text-muted-foreground">
|
||||
<span className="shrink-0 font-sans text-xs text-muted-foreground">
|
||||
{timeAgo(session.resolved_at || session.created_at)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -19,12 +19,12 @@ export function SectionGroup({ title, count, defaultOpen = true, delay = 150, ch
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex w-full items-center gap-2 py-2"
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-gradient-brand" />
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
|
||||
<span className="font-heading text-[0.8125rem] font-bold uppercase tracking-[0.04em] text-foreground">
|
||||
{title}
|
||||
</span>
|
||||
{count !== undefined && (
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 font-label text-[0.6875rem] text-muted-foreground">
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 font-sans text-xs text-muted-foreground">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SessionsPanel({ sessions, delay = 200 }: SessionsPanelProps) {
|
||||
if (sessions.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="glass-card-static fade-in" style={{ animationDelay: `${delay}ms` }}>
|
||||
<div className="card-flat fade-in" style={{ animationDelay: `${delay}ms` }}>
|
||||
<div className="flex items-center justify-between px-4 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<h3 className="font-heading text-sm font-semibold text-foreground">Recent Sessions</h3>
|
||||
<Link to="/sessions" className="text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors">
|
||||
@@ -42,7 +42,7 @@ export function SessionsPanel({ sessions, delay = 200 }: SessionsPanelProps) {
|
||||
'h-2 w-2 rounded-full',
|
||||
session.status === 'completed' ? 'bg-green-500' :
|
||||
session.status === 'in_progress' ? 'bg-amber-500' :
|
||||
'bg-muted-foreground'
|
||||
'bg-[#848b9b]'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function SessionsPanel({ sessions, delay = 200 }: SessionsPanelProps) {
|
||||
</span>
|
||||
|
||||
{/* Ticket */}
|
||||
<span className="font-label text-[0.6875rem] text-muted-foreground truncate">
|
||||
<span className="font-sans text-xs text-[0.6875rem] text-muted-foreground truncate">
|
||||
{session.ticketNumber || '—'}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function StartSessionInput() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass-card-static overflow-hidden">
|
||||
<div className="card-flat overflow-hidden">
|
||||
<div className="px-5 py-4 sm:px-6 sm:py-5">
|
||||
<div className="relative">
|
||||
<Sparkles
|
||||
@@ -50,14 +50,14 @@ export function StartSessionInput() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 rounded-lg bg-[rgba(255,255,255,0.04)] p-0.5">
|
||||
<div className="flex items-center gap-1 rounded-lg bg-secondary p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('guided')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all duration-150',
|
||||
mode === 'guided'
|
||||
? 'bg-primary/10 text-foreground'
|
||||
? 'bg-accent-dim text-foreground tab-active-shadow'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
@@ -68,9 +68,9 @@ export function StartSessionInput() {
|
||||
type="button"
|
||||
onClick={() => setMode('chat')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all duration-150',
|
||||
mode === 'chat'
|
||||
? 'bg-primary/10 text-foreground'
|
||||
? 'bg-accent-dim text-foreground tab-active-shadow'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function TeamSummary() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="glass-card-static">
|
||||
<div className="card-flat">
|
||||
<div
|
||||
className="flex items-center justify-between px-5 py-3"
|
||||
style={{ borderBottom: '1px solid var(--glass-border)' }}
|
||||
@@ -43,11 +43,14 @@ export function TeamSummary() {
|
||||
<button
|
||||
key={item.label}
|
||||
onClick={() => navigate(item.href)}
|
||||
className="flex flex-col items-center gap-2 py-5 hover:bg-[rgba(255,255,255,0.02)] transition-colors"
|
||||
className="flex flex-col items-center gap-2 py-5 rounded-lg hover:bg-card-hover transition-all duration-350"
|
||||
style={{ transition: 'transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1), background 200ms ease' }}
|
||||
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)' }}
|
||||
>
|
||||
<item.icon size={20} style={{ color: item.color }} />
|
||||
<p className="font-heading text-xl font-extrabold text-foreground">{item.value}</p>
|
||||
<p className="font-label text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{item.label}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function TreeListItem({
|
||||
<p className="font-heading text-sm font-semibold text-foreground truncate">{name}</p>
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
{tags.slice(0, 3).map(tag => (
|
||||
<span key={tag} className="rounded border border-border bg-secondary px-1.5 py-px font-label text-[0.625rem] text-muted-foreground">
|
||||
<span key={tag} className="rounded border border-border bg-secondary px-1.5 py-px font-sans text-xs text-[0.625rem] text-muted-foreground">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -64,13 +64,13 @@ export function TreeListItem({
|
||||
{category && (
|
||||
<>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: categoryColor }} />
|
||||
<span className="font-label text-xs text-muted-foreground truncate">{category.name}</span>
|
||||
<span className="font-sans text-xs text-muted-foreground truncate">{category.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Usage count */}
|
||||
<div className="text-right font-label text-xs text-muted-foreground">
|
||||
<div className="text-right font-sans text-xs text-muted-foreground">
|
||||
{usageCount} uses
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
|
||||
const days = useMemo(() => getWeekDays(), [])
|
||||
|
||||
return (
|
||||
<div className="glass-card-static flex flex-col h-full">
|
||||
<div className="card-flat flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<Calendar size={16} className="text-muted-foreground" />
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">This Week</h3>
|
||||
@@ -56,7 +56,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
|
||||
borderBottom: day.isToday ? '2px solid #06b6d4' : '1px solid var(--glass-border)',
|
||||
}}
|
||||
>
|
||||
<span className={`font-label text-[0.625rem] uppercase tracking-widest ${day.isToday ? 'text-cyan-400' : 'text-muted-foreground'}`}>
|
||||
<span className={`font-sans text-xs text-[0.625rem] uppercase tracking-widest ${day.isToday ? 'text-cyan-400' : 'text-muted-foreground'}`}>
|
||||
{day.label}
|
||||
</span>
|
||||
<div className={`text-sm font-heading font-bold ${day.isToday ? 'text-foreground' : 'text-muted-foreground'}`}>
|
||||
@@ -77,7 +77,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
|
||||
}}
|
||||
>
|
||||
<div className="font-medium text-foreground truncate">{event.title}</div>
|
||||
<div className="font-label text-[0.5625rem] text-muted-foreground">{event.time}</div>
|
||||
<div className="font-sans text-xs text-[0.5625rem] text-muted-foreground">{event.time}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function AIPromptDialog({
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="glass-card-static relative w-full max-w-lg p-6 shadow-xl">
|
||||
<div className="card-flat relative w-full max-w-lg p-6 shadow-xl">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Sparkles className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-heading font-semibold text-foreground">
|
||||
@@ -83,14 +83,14 @@ export function AIPromptDialog({
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isGenerating}
|
||||
className="rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm text-foreground hover:border-white/[0.12] transition-colors disabled:opacity-50"
|
||||
className="rounded-[10px] bg-input border border-border px-4 py-2 text-sm text-foreground hover:border-border-hover transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={!prompt.trim() || isGenerating}
|
||||
className="flex items-center gap-2 rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-brand-dark shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-50"
|
||||
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-50"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
|
||||
@@ -50,7 +50,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
|
||||
className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${
|
||||
msg.role === 'user'
|
||||
? 'bg-primary/15 text-foreground'
|
||||
: 'bg-white/[0.04] text-foreground border border-brand-border'
|
||||
: 'bg-input text-foreground border border-border'
|
||||
}`}
|
||||
>
|
||||
<MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" />
|
||||
@@ -59,7 +59,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white/[0.04] border border-brand-border rounded-xl px-3.5 py-2.5">
|
||||
<div className="bg-input border border-border rounded-xl px-3.5 py-2.5">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={!input.trim() || isLoading}
|
||||
className="bg-gradient-brand text-brand-dark p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
className="bg-primary text-white p-2.5 rounded-xl hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-40"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function EditorAIPanel({
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-1.5 rounded-lg hover:bg-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function NodeSummary({ node, flowName, flowType, nodeCount }: NodeSummary
|
||||
{flowName || 'Untitled Flow'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<div className="mt-1 flex items-center gap-3 font-sans text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<span>{flowType || 'flow'}</span>
|
||||
{nodeCount !== undefined && <span>{nodeCount} nodes</span>}
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@ export function NodeSummary({ node, flowName, flowType, nodeCount }: NodeSummary
|
||||
<div className="border-b px-3 py-2.5" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={`h-3.5 w-3.5 ${colorClass}`} />
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<span className="font-sans text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
{node.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -26,9 +26,9 @@ export function SuggestionsTab({ suggestions }: SuggestionsTabProps) {
|
||||
const config = STATUS_CONFIG[s.status]
|
||||
const StatusIcon = config.icon
|
||||
return (
|
||||
<div key={s.id} className="rounded-lg border border-border bg-white/[0.02] px-3 py-2">
|
||||
<div key={s.id} className="rounded-lg border border-border bg-card px-3 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<span className="font-sans text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
{s.action_type.replace(/_/g, ' ')}
|
||||
</span>
|
||||
<span className={`flex items-center gap-1 text-xs ${config.color}`}>
|
||||
@@ -39,7 +39,7 @@ export function SuggestionsTab({ suggestions }: SuggestionsTabProps) {
|
||||
{s.target_node_id && (
|
||||
<p className="mt-1 text-xs text-muted-foreground truncate">Node: {s.target_node_id}</p>
|
||||
)}
|
||||
<p className="mt-0.5 font-label text-[0.625rem] text-brand-text-muted">
|
||||
<p className="mt-0.5 font-sans text-[0.625rem] text-text-muted">
|
||||
{new Date(s.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const STATUS_CONFIG = {
|
||||
paused: { icon: Pause, color: 'text-amber-400', label: 'Paused' },
|
||||
resolved: { icon: CheckCircle2, color: 'text-emerald-400', label: 'Resolved' },
|
||||
escalated: { icon: ArrowUpRight, color: 'text-amber-400', label: 'Escalated' },
|
||||
abandoned: { icon: AlertCircle, color: 'text-[#5a6170]', label: 'Abandoned' },
|
||||
abandoned: { icon: AlertCircle, color: 'text-text-muted', label: 'Abandoned' },
|
||||
} as const
|
||||
|
||||
export function AISessionListItem({ session }: AISessionListItemProps) {
|
||||
@@ -22,7 +22,7 @@ export function AISessionListItem({ session }: AISessionListItemProps) {
|
||||
return (
|
||||
<Link
|
||||
to={`/pilot/${session.id}`}
|
||||
className="glass-card block p-4 transition-all"
|
||||
className="card-interactive block p-4 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -31,7 +31,7 @@ export function AISessionListItem({ session }: AISessionListItemProps) {
|
||||
</p>
|
||||
<div className="mt-1.5 flex items-center gap-3 flex-wrap">
|
||||
{session.problem_domain && (
|
||||
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
{session.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
@@ -42,7 +42,7 @@ export function AISessionListItem({ session }: AISessionListItemProps) {
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{session.step_count} steps
|
||||
</span>
|
||||
<span className="text-xs text-[#5a6170]">
|
||||
<span className="text-xs text-text-muted">
|
||||
{new Date(session.created_at).toLocaleDateString(undefined, {
|
||||
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
||||
})}
|
||||
@@ -50,7 +50,7 @@ export function AISessionListItem({ session }: AISessionListItemProps) {
|
||||
</div>
|
||||
</div>
|
||||
{session.session_rating && (
|
||||
<span className="font-label text-xs text-amber-400">
|
||||
<span className="font-sans text-xs text-xs text-amber-400">
|
||||
{'★'.repeat(session.session_rating)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -30,12 +30,12 @@ export function ConfidenceIndicator({ tier, score, className }: ConfidenceIndica
|
||||
return (
|
||||
<div className={cn('group relative inline-flex items-center gap-2', className)}>
|
||||
<span className={cn('h-2 w-2 rounded-full', config.color)} />
|
||||
<span className="font-label text-xs text-muted-foreground">{config.label}</span>
|
||||
<span className="font-sans text-xs text-xs text-muted-foreground">{config.label}</span>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="pointer-events-none absolute left-0 top-full z-50 mt-2 w-56 rounded-lg border border-border bg-card p-3 opacity-0 shadow-lg transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<p className="text-xs text-muted-foreground">{config.description}</p>
|
||||
<p className="mt-1 font-label text-[0.625rem] text-[#5a6170]">
|
||||
<p className="mt-1 font-sans text-xs text-[0.625rem] text-text-muted">
|
||||
Confidence: {Math.round(score * 100)}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ export function EscalateModal({ open, onClose, onEscalate, isProcessing, hasPsaT
|
||||
placeholder="e.g. I've exhausted all networking diagnostics and suspect this is a firewall policy issue that requires senior admin access..."
|
||||
rows={4}
|
||||
/>
|
||||
<p className="mt-1 text-[0.625rem] text-[#5a6170]">
|
||||
<p className="mt-1 text-[0.625rem] text-text-muted">
|
||||
Minimum 5 characters. This will be shown to the engineer who picks up.
|
||||
</p>
|
||||
</div>
|
||||
@@ -70,7 +70,7 @@ export function EscalateModal({ open, onClose, onEscalate, isProcessing, hasPsaT
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!reason.trim() || reason.trim().length < 5 || isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-amber-500/90 px-4 py-2.5 min-h-[44px] text-sm font-semibold text-[#101114] hover:bg-amber-500 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-amber-500/90 px-4 py-2.5 min-h-[44px] text-sm font-semibold text-white hover:bg-amber-500 active:scale-[0.98] disabled:opacity-40 transition-all"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
|
||||
@@ -80,7 +80,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<h3 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170]">
|
||||
<h3 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted">
|
||||
Awaiting pickup ({sessions.length})
|
||||
</h3>
|
||||
<button
|
||||
@@ -93,7 +93,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
||||
</div>
|
||||
|
||||
{sessions.map((session) => (
|
||||
<div key={session.id} className="glass-card p-3 sm:p-4 space-y-3">
|
||||
<div key={session.id} className="card-interactive p-3 sm:p-4 space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{session.problem_summary || 'Untitled session'}
|
||||
@@ -107,7 +107,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
{session.problem_domain && (
|
||||
<span className="font-label rounded-md bg-primary/10 px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary">
|
||||
{session.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
@@ -129,7 +129,7 @@ export function EscalationQueue({ onPickup }: EscalationQueueProps) {
|
||||
|
||||
<button
|
||||
onClick={() => handlePickup(session.id)}
|
||||
className="w-full min-h-[44px] rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
className="w-full min-h-[44px] rounded-lg bg-primary text-white px-4 py-2 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
Pick Up Session
|
||||
</button>
|
||||
|
||||
@@ -70,8 +70,8 @@ export function FlowPilotActionBar({
|
||||
<>
|
||||
{/* Bottom bar — fixed to viewport bottom, works regardless of height chain */}
|
||||
<div
|
||||
className="fixed bottom-0 right-0 z-40 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3 border-t px-3 py-3 sm:px-5"
|
||||
style={{ borderColor: 'var(--glass-border)', background: 'rgba(16, 17, 20, 0.95)', backdropFilter: 'blur(16px)', left: 'var(--sidebar-w, 0px)' }}
|
||||
className="fixed bottom-0 right-0 z-40 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3 border-t border-border bg-card px-3 py-3 sm:px-5"
|
||||
style={{ left: 'var(--sidebar-w, 0px)' }}
|
||||
>
|
||||
<div className="flex gap-2 sm:gap-3">
|
||||
<button
|
||||
@@ -118,7 +118,7 @@ export function FlowPilotActionBar({
|
||||
{/* Resolve modal */}
|
||||
{showResolve && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="glass-card-static w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 p-4 sm:p-6 rounded-t-2xl sm:rounded-2xl">
|
||||
<div className="card-flat w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 p-4 sm:p-6 rounded-t-2xl sm:rounded-2xl">
|
||||
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Resolve Session</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">Summarize what fixed the issue. This will be included in the auto-generated documentation.</p>
|
||||
<textarea
|
||||
@@ -151,7 +151,7 @@ export function FlowPilotActionBar({
|
||||
{/* Close/Abandon confirmation */}
|
||||
{showAbandon && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="glass-card-static w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 p-4 sm:p-6 rounded-t-2xl sm:rounded-2xl">
|
||||
<div className="card-flat w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 p-4 sm:p-6 rounded-t-2xl sm:rounded-2xl">
|
||||
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Close Session</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Are you sure you want to close this session? The session history will be kept but it won't count as resolved.
|
||||
|
||||
@@ -100,7 +100,7 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[50vh]">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-accent-dim">
|
||||
<Sparkles size={24} className="text-primary animate-pulse" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-foreground">Analyzing your issue...</p>
|
||||
@@ -126,7 +126,7 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static p-3 sm:p-5 space-y-4">
|
||||
<div className="card-flat p-3 sm:p-5 space-y-4">
|
||||
{/* Selected ticket card */}
|
||||
{selectedTicket && selectedTicketId && (
|
||||
<div className="rounded-xl border border-primary/20 bg-primary/5 p-4">
|
||||
@@ -141,13 +141,13 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
|
||||
{selectedTicket.company_name && <span>{selectedTicket.company_name}</span>}
|
||||
{selectedTicket.priority_name && (
|
||||
<>
|
||||
<span className="text-[#5a6170]">•</span>
|
||||
<span className="text-text-muted">•</span>
|
||||
<span>{selectedTicket.priority_name}</span>
|
||||
</>
|
||||
)}
|
||||
{selectedTicket.status_name && (
|
||||
<>
|
||||
<span className="text-[#5a6170]">•</span>
|
||||
<span className="text-text-muted">•</span>
|
||||
<span>{selectedTicket.status_name}</span>
|
||||
</>
|
||||
)}
|
||||
@@ -190,7 +190,7 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
|
||||
onClick={() => setShowLogs(!showLogs)}
|
||||
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
showLogs
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
? 'bg-accent-dim text-primary border border-primary/20'
|
||||
: 'bg-card/50 text-muted-foreground border border-border hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
@@ -207,7 +207,7 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
|
||||
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
psaConnection
|
||||
? 'bg-card/50 text-muted-foreground border border-border hover:text-foreground hover:border-primary/20'
|
||||
: 'bg-card/50 text-[#5a6170] border border-border opacity-50 cursor-not-allowed'
|
||||
: 'bg-card/50 text-text-muted border border-border opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title={!psaConnection ? 'Connect your PSA in Settings → Integrations' : 'Search for a ConnectWise ticket'}
|
||||
>
|
||||
@@ -236,13 +236,13 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
|
||||
|
||||
{/* Submit */}
|
||||
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-[0.6875rem] text-[#5a6170] text-center sm:text-left">
|
||||
<p className="text-[0.6875rem] text-text-muted text-center sm:text-left">
|
||||
FlowPilot will analyze your input and guide you through diagnosis
|
||||
</p>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!hasContent}
|
||||
className="w-full sm:w-auto min-h-[44px] rounded-lg bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:shadow-none transition-all whitespace-nowrap"
|
||||
className="w-full sm:w-auto min-h-[44px] rounded-lg bg-primary text-white px-5 py-2.5 text-sm font-semibold hover:brightness-110 active:scale-[0.98] disabled:opacity-40 transition-all whitespace-nowrap"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing
|
||||
? 'border-border/50 opacity-50'
|
||||
: 'border-border focus-within:border-[rgba(6,182,212,0.3)]'
|
||||
)}
|
||||
style={{ background: 'rgba(16, 17, 20, 0.95)', backdropFilter: 'blur(16px)' }}
|
||||
style={{ background: 'var(--color-bg-card)' }}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
@@ -70,7 +70,7 @@ export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing
|
||||
className={cn(
|
||||
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-all',
|
||||
message.trim() && !isDisabled
|
||||
? 'bg-gradient-brand text-[#101114] hover:opacity-90 active:scale-[0.97]'
|
||||
? 'bg-primary text-white hover:brightness-110 active:scale-[0.98]'
|
||||
: 'bg-[rgba(255,255,255,0.04)] text-muted-foreground cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function FlowPilotOptions({ options, onSelect, disabled }: FlowPilotOptio
|
||||
'hover:border-[rgba(6,182,212,0.3)] hover:shadow-[0_0_20px_rgba(6,182,212,0.08)]',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40',
|
||||
isSelected
|
||||
? 'border-primary/40 bg-primary/10'
|
||||
? 'border-primary/40 bg-accent-dim'
|
||||
: 'border-border bg-card/50',
|
||||
disabled && 'pointer-events-none opacity-60'
|
||||
)}
|
||||
|
||||
@@ -143,7 +143,7 @@ export function FlowPilotSession({
|
||||
>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground overflow-x-auto">
|
||||
{session.problem_domain && (
|
||||
<span className="font-label rounded-md bg-primary/10 px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary shrink-0">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary shrink-0">
|
||||
{session.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
@@ -177,7 +177,7 @@ export function FlowPilotSession({
|
||||
) : null}
|
||||
{session.problem_summary && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Problem</h4>
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">Problem</h4>
|
||||
<p className="text-sm text-foreground">{session.problem_summary}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -238,7 +238,7 @@ export function FlowPilotSession({
|
||||
{/* Problem summary */}
|
||||
{session.problem_summary && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
|
||||
Problem
|
||||
</h4>
|
||||
<p className="text-sm text-foreground">{session.problem_summary}</p>
|
||||
@@ -248,10 +248,10 @@ export function FlowPilotSession({
|
||||
{/* Domain */}
|
||||
{session.problem_domain && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
|
||||
Domain
|
||||
</h4>
|
||||
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
{session.problem_domain}
|
||||
</span>
|
||||
</div>
|
||||
@@ -259,7 +259,7 @@ export function FlowPilotSession({
|
||||
|
||||
{/* Confidence */}
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
|
||||
Confidence
|
||||
</h4>
|
||||
<ConfidenceIndicator
|
||||
@@ -271,7 +271,7 @@ export function FlowPilotSession({
|
||||
{/* Matched flow */}
|
||||
{session.matched_flow_id && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
|
||||
Matched flow
|
||||
</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -330,13 +330,12 @@ export function FlowPilotSession({
|
||||
{/* Paused banner */}
|
||||
{session.status === 'paused' && onResume && (
|
||||
<div
|
||||
className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between border-t px-3 py-3 sm:px-5"
|
||||
style={{ borderColor: 'var(--glass-border)', background: 'rgba(16, 17, 20, 0.8)', backdropFilter: 'blur(12px)' }}
|
||||
className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between border-t border-border bg-card px-3 py-3 sm:px-5"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">Session paused</span>
|
||||
<button
|
||||
onClick={onResume}
|
||||
className="flex items-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
className="flex items-center gap-2 rounded-lg bg-primary text-white px-4 py-2 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
<Play size={14} />
|
||||
Resume Session
|
||||
|
||||
@@ -62,7 +62,7 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsCollapsed(false)}
|
||||
className="w-full text-left glass-card-static p-3 sm:p-4 opacity-70 hover:opacity-90 transition-opacity"
|
||||
className="w-full text-left card-flat p-3 sm:p-4 opacity-70 hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon size={16} className="shrink-0 text-muted-foreground" />
|
||||
@@ -76,14 +76,14 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
// Expanded completed step
|
||||
if (!isCurrentStep && !isCollapsed) {
|
||||
return (
|
||||
<div className="glass-card-static p-3 sm:p-4 opacity-80">
|
||||
<div className="card-flat p-3 sm:p-4 opacity-80">
|
||||
<button
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
className="mb-2 flex w-full items-center justify-between text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={16} className="text-muted-foreground" />
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||
Step {step.step_order + 1}
|
||||
</span>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'glass-card-static p-3 sm:p-4 lg:p-5',
|
||||
'card-flat p-3 sm:p-4 lg:p-5',
|
||||
isResolutionSuggestion && 'border-emerald-500/30'
|
||||
)}
|
||||
>
|
||||
@@ -113,7 +113,7 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<span className={cn(
|
||||
'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg',
|
||||
isResolutionSuggestion ? 'bg-emerald-500/10 text-emerald-400' : 'bg-primary/10 text-primary'
|
||||
isResolutionSuggestion ? 'bg-emerald-500/10 text-emerald-400' : 'bg-accent-dim text-primary'
|
||||
)}>
|
||||
<Icon size={14} />
|
||||
</span>
|
||||
@@ -180,7 +180,7 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
window.open('/script-builder?from=flowpilot', '_blank')
|
||||
onRespond({ action_result: { success: true, details: 'Opened Script Builder' } })
|
||||
}}
|
||||
className="flex-1 min-h-[44px] rounded-lg bg-gradient-brand px-4 py-2.5 text-sm font-semibold text-[#101114] hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
className="flex-1 min-h-[44px] rounded-lg bg-primary text-white px-4 py-2.5 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
Open Script Builder
|
||||
</button>
|
||||
@@ -191,7 +191,7 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<button
|
||||
onClick={() => handleActionComplete(true)}
|
||||
className="flex-1 min-h-[44px] rounded-lg bg-primary/10 border border-primary/20 px-4 py-2.5 text-sm font-medium text-primary hover:bg-primary/20 transition-colors"
|
||||
className="flex-1 min-h-[44px] rounded-lg bg-accent-dim border border-primary/20 px-4 py-2.5 text-sm font-medium text-primary hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
I've completed this action
|
||||
</button>
|
||||
@@ -208,7 +208,7 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
|
||||
{!isResolutionSuggestion && step.allow_skip && (
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="flex items-center gap-1.5 text-xs text-[#5a6170] hover:text-muted-foreground transition-colors"
|
||||
className="flex items-center gap-1.5 text-xs text-text-muted hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<SkipForward size={12} />
|
||||
I can't check this right now
|
||||
|
||||
@@ -66,7 +66,7 @@ export function InSessionScriptGenerator({
|
||||
<div className="mt-3 rounded-xl border border-primary/20 bg-primary/5 p-3 sm:p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal size={14} className="text-primary" />
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
Script Generator
|
||||
</span>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export function InSessionScriptGenerator({
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
className="w-full min-h-[44px] flex items-center justify-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
className="w-full min-h-[44px] flex items-center justify-center gap-2 rounded-lg bg-primary text-white px-4 py-2 text-sm font-semibold hover:brightness-110 active:scale-[0.98] disabled:opacity-40 transition-all"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
@@ -141,7 +141,7 @@ export function InSessionScriptGenerator({
|
||||
<div className="flex flex-col gap-2 pt-1 sm:flex-row">
|
||||
<button
|
||||
onClick={() => handleContinue(true)}
|
||||
className="flex-1 min-h-[44px] rounded-lg bg-primary/10 border border-primary/20 px-4 py-2 text-sm font-medium text-primary hover:bg-primary/20 transition-colors"
|
||||
className="flex-1 min-h-[44px] rounded-lg bg-accent-dim border border-primary/20 px-4 py-2 text-sm font-medium text-primary hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
Script worked — continue
|
||||
</button>
|
||||
|
||||
@@ -24,12 +24,12 @@ export function ProposalCard({ proposal, isSelected, onClick }: ProposalCardProp
|
||||
className={`w-full text-left rounded-xl border p-3 space-y-2 transition-all ${
|
||||
isSelected
|
||||
? 'border-primary/30 bg-primary/5'
|
||||
: 'border-[rgba(255,255,255,0.06)] bg-[rgba(24,26,31,0.55)] hover:border-[rgba(255,255,255,0.12)]'
|
||||
: 'border-[rgba(255,255,255,0.06)] bg-card hover:border-[rgba(255,255,255,0.12)]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm font-semibold text-foreground line-clamp-2">{proposal.title}</p>
|
||||
<span className={`shrink-0 flex items-center gap-1 rounded-md border px-1.5 py-0.5 font-label text-[0.5625rem] uppercase tracking-wider ${typeConfig.color}`}>
|
||||
<span className={`shrink-0 flex items-center gap-1 rounded-md border px-1.5 py-0.5 font-sans text-xs text-[0.5625rem] uppercase tracking-wider ${typeConfig.color}`}>
|
||||
<TypeIcon size={10} />
|
||||
{typeConfig.label}
|
||||
</span>
|
||||
@@ -41,7 +41,7 @@ export function ProposalCard({ proposal, isSelected, onClick }: ProposalCardProp
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
{proposal.problem_domain && (
|
||||
<span className="font-label rounded-md bg-primary/10 px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary">
|
||||
{proposal.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
)}
|
||||
<div className="mt-3 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||
{proposal.problem_domain && (
|
||||
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
{proposal.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
@@ -89,8 +89,8 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-5">
|
||||
{/* Source session link */}
|
||||
<div className="glass-card-static p-4">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-2">Source Session</h4>
|
||||
<div className="card-flat p-4">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">Source Session</h4>
|
||||
<Link
|
||||
to={`/pilot/${proposal.source_session_id}`}
|
||||
target="_blank"
|
||||
@@ -105,8 +105,8 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
{proposal.proposed_diff && (() => {
|
||||
const diff = proposal.proposed_diff as { diff_description?: string; new_nodes?: Array<{ title?: string; question?: string; description?: string }> }
|
||||
return (
|
||||
<div className="glass-card-static border-l-2 border-l-amber-500 p-4">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-2">Proposed Changes</h4>
|
||||
<div className="card-flat border-l-2 border-l-amber-500 p-4">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">Proposed Changes</h4>
|
||||
{diff.diff_description && (
|
||||
<p className="text-sm text-foreground">{diff.diff_description}</p>
|
||||
)}
|
||||
@@ -126,10 +126,10 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
})()}
|
||||
|
||||
{/* Flow data preview */}
|
||||
<div className="glass-card-static p-4">
|
||||
<div className="card-flat p-4">
|
||||
<button
|
||||
onClick={() => setShowFlowData(!showFlowData)}
|
||||
className="flex items-center gap-1.5 font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] hover:text-foreground transition-colors"
|
||||
className="flex items-center gap-1.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{showFlowData ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
Flow Data (JSON)
|
||||
@@ -143,8 +143,8 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
|
||||
{/* Supporting sessions */}
|
||||
{proposal.supporting_session_ids.length > 1 && (
|
||||
<div className="glass-card-static p-4">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-2">
|
||||
<div className="card-flat p-4">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">
|
||||
Supporting Sessions ({proposal.supporting_session_ids.length})
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
@@ -164,8 +164,8 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
|
||||
{/* Review info (for already-reviewed proposals) */}
|
||||
{proposal.reviewed_at && (
|
||||
<div className="glass-card-static p-4">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-2">Review</h4>
|
||||
<div className="card-flat p-4">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-2">Review</h4>
|
||||
<p className="text-sm text-foreground">
|
||||
<span className="capitalize">{proposal.status}</span> on{' '}
|
||||
{new Date(proposal.reviewed_at).toLocaleString()}
|
||||
@@ -180,8 +180,7 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
|
||||
{/* Review actions bar */}
|
||||
{canReview && (
|
||||
<div
|
||||
className="border-t px-5 py-3 space-y-3"
|
||||
style={{ borderColor: 'var(--glass-border)', background: 'rgba(16, 17, 20, 0.8)', backdropFilter: 'blur(12px)' }}
|
||||
className="border-t border-border bg-card px-5 py-3 space-y-3"
|
||||
>
|
||||
{/* Notes input */}
|
||||
<input
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SessionBriefing({
|
||||
const pkg = escalationPackage
|
||||
|
||||
return (
|
||||
<div className="glass-card-static border-l-2 border-l-amber-500 p-5 space-y-4">
|
||||
<div className="card-flat border-l-2 border-l-amber-500 p-5 space-y-4">
|
||||
<div>
|
||||
<h3 className="font-heading text-base font-semibold text-foreground">
|
||||
Escalation from {originalEngineerName || 'another engineer'}
|
||||
@@ -51,7 +51,7 @@ export function SessionBriefing({
|
||||
{/* Problem */}
|
||||
{pkg.problem_summary && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Problem</h4>
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">Problem</h4>
|
||||
<p className="text-sm text-foreground">{pkg.problem_summary}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -59,7 +59,7 @@ export function SessionBriefing({
|
||||
{/* Escalation reason */}
|
||||
{pkg.escalation_reason && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Why escalated</h4>
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">Why escalated</h4>
|
||||
<p className="text-sm text-amber-400">{pkg.escalation_reason}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -69,7 +69,7 @@ export function SessionBriefing({
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowSteps(!showSteps)}
|
||||
className="flex items-center gap-1.5 font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] hover:text-foreground transition-colors"
|
||||
className="flex items-center gap-1.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{showSteps ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
Steps taken ({pkg.steps_tried.length})
|
||||
@@ -92,7 +92,7 @@ export function SessionBriefing({
|
||||
{/* Remaining hypotheses */}
|
||||
{pkg.remaining_hypotheses && pkg.remaining_hypotheses.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Remaining hypotheses</h4>
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">Remaining hypotheses</h4>
|
||||
<ul className="space-y-1">
|
||||
{pkg.remaining_hypotheses.map((h, i) => (
|
||||
<li key={i} className="text-sm text-foreground flex items-start gap-2">
|
||||
@@ -107,7 +107,7 @@ export function SessionBriefing({
|
||||
{/* Suggested next steps */}
|
||||
{pkg.suggested_next_steps && pkg.suggested_next_steps.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170] mb-1">Suggested next steps</h4>
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">Suggested next steps</h4>
|
||||
<ul className="space-y-1">
|
||||
{pkg.suggested_next_steps.map((s, i) => (
|
||||
<li key={i} className="text-sm text-foreground flex items-start gap-2">
|
||||
@@ -125,7 +125,7 @@ export function SessionBriefing({
|
||||
<button
|
||||
onClick={onContinue}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-gradient-brand px-4 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-primary text-white px-4 py-2.5 text-sm font-semibold hover:brightness-110 active:scale-[0.98] disabled:opacity-40 transition-all"
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
Continue Where They Left Off
|
||||
@@ -160,7 +160,7 @@ export function SessionBriefing({
|
||||
<button
|
||||
onClick={() => freshContext.trim() && onFresh(freshContext.trim())}
|
||||
disabled={!freshContext.trim() || isProcessing}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 transition-all"
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-primary text-white px-4 py-2 text-sm font-semibold hover:brightness-110 active:scale-[0.98] disabled:opacity-40 transition-all"
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
Start Diagnosis
|
||||
|
||||
@@ -107,9 +107,9 @@ export function SessionDocView({
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="glass-card-static p-3 sm:p-4 lg:p-5">
|
||||
<div className="card-flat p-3 sm:p-4 lg:p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-accent-dim text-primary">
|
||||
<FileText size={16} />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
@@ -117,7 +117,7 @@ export function SessionDocView({
|
||||
<p className="mt-1 text-sm text-muted-foreground">{documentation.problem_summary}</p>
|
||||
<div className="mt-2 flex items-center gap-3 flex-wrap">
|
||||
{documentation.problem_domain && (
|
||||
<span className="font-label rounded-md bg-primary/10 px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
{documentation.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
@@ -137,14 +137,14 @@ export function SessionDocView({
|
||||
|
||||
{/* Outcome */}
|
||||
{(documentation.resolution_summary || documentation.escalation_reason) && (
|
||||
<div className={`glass-card-static p-4 border-l-2 ${documentation.resolution_summary ? 'border-l-emerald-500' : 'border-l-amber-500'}`}>
|
||||
<div className={`card-flat p-4 border-l-2 ${documentation.resolution_summary ? 'border-l-emerald-500' : 'border-l-amber-500'}`}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{documentation.resolution_summary ? (
|
||||
<CheckCircle2 size={14} className="text-emerald-400" />
|
||||
) : (
|
||||
<ArrowUpRight size={14} className="text-amber-400" />
|
||||
)}
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground">
|
||||
{documentation.resolution_summary ? 'Resolved' : 'Escalated'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -155,8 +155,8 @@ export function SessionDocView({
|
||||
)}
|
||||
|
||||
{/* Intake summary */}
|
||||
<div className="glass-card-static p-3 sm:p-4">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground mb-2">
|
||||
<div className="card-flat p-3 sm:p-4">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground mb-2">
|
||||
Original intake
|
||||
</h4>
|
||||
<p className="text-sm text-foreground whitespace-pre-wrap">{documentation.intake_summary}</p>
|
||||
@@ -164,13 +164,13 @@ export function SessionDocView({
|
||||
|
||||
{/* Diagnostic steps */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-muted-foreground px-1">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-muted-foreground px-1">
|
||||
Diagnostic trail
|
||||
</h4>
|
||||
{documentation.diagnostic_steps.map((step) => (
|
||||
<div key={step.step_number} className="glass-card-static p-3 sm:p-4">
|
||||
<div key={step.step_number} className="card-flat p-3 sm:p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="font-label flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-card text-[0.625rem] text-muted-foreground border border-border">
|
||||
<span className="font-sans text-xs flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-card text-[0.625rem] text-muted-foreground border border-border">
|
||||
{step.step_number}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
@@ -189,7 +189,7 @@ export function SessionDocView({
|
||||
|
||||
{/* Rating */}
|
||||
{onRate && (
|
||||
<div className="glass-card-static p-3 sm:p-4 text-center">
|
||||
<div className="card-flat p-3 sm:p-4 text-center">
|
||||
<p className="text-sm text-muted-foreground mb-2">How helpful was this session?</p>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
|
||||
@@ -36,7 +36,7 @@ export function SessionTicketCard({ ticketId, ticketData, siteUrl }: SessionTick
|
||||
return (
|
||||
<div className="rounded-xl border border-primary/20 bg-primary/5 p-3 space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-wider text-[#5a6170]">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted">
|
||||
Linked Ticket
|
||||
</h4>
|
||||
{ticketUrl && (
|
||||
@@ -68,13 +68,13 @@ export function SessionTicketCard({ ticketId, ticketData, siteUrl }: SessionTick
|
||||
)}
|
||||
{ticket?.priority && (
|
||||
<>
|
||||
<span className="text-[#5a6170]">•</span>
|
||||
<span className="text-text-muted">•</span>
|
||||
<span>{ticket.priority}</span>
|
||||
</>
|
||||
)}
|
||||
{ticket?.status && (
|
||||
<>
|
||||
<span className="text-[#5a6170]">•</span>
|
||||
<span className="text-text-muted">•</span>
|
||||
<span>{ticket.status}</span>
|
||||
</>
|
||||
)}
|
||||
@@ -82,7 +82,7 @@ export function SessionTicketCard({ ticketId, ticketData, siteUrl }: SessionTick
|
||||
|
||||
{configs && configs.length > 0 && (
|
||||
<div className="border-t border-border/50 pt-2 mt-2">
|
||||
<p className="font-label text-[0.5625rem] uppercase tracking-wider text-[#5a6170] mb-1">
|
||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-wider text-text-muted mb-1">
|
||||
Devices
|
||||
</p>
|
||||
<div className="space-y-0.5">
|
||||
@@ -90,11 +90,11 @@ export function SessionTicketCard({ ticketId, ticketData, siteUrl }: SessionTick
|
||||
<div key={i} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Cpu size={10} />
|
||||
<span>{cfg.device_identifier}</span>
|
||||
{cfg.type && <span className="text-[#5a6170]">({cfg.type})</span>}
|
||||
{cfg.type && <span className="text-text-muted">({cfg.type})</span>}
|
||||
</div>
|
||||
))}
|
||||
{configs.length > 3 && (
|
||||
<p className="text-[0.625rem] text-[#5a6170]">
|
||||
<p className="text-[0.625rem] text-text-muted">
|
||||
+{configs.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function SimilarSessions({ sessionId }: SimilarSessionsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 py-1">
|
||||
<Loader2 size={10} className="animate-spin text-muted-foreground" />
|
||||
<span className="text-[0.625rem] text-muted-foreground font-label">Loading similar sessions…</span>
|
||||
<span className="text-[0.625rem] text-muted-foreground font-sans text-xs">Loading similar sessions…</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -47,20 +47,20 @@ export function SimilarSessions({ sessionId }: SimilarSessionsProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Similar Past Sessions
|
||||
</h4>
|
||||
{sessions.map((session) => (
|
||||
<Link
|
||||
key={session.id}
|
||||
to={`/pilot/${session.id}`}
|
||||
className="glass-card p-3 block hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
className="card-interactive p-3 block hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-xs text-foreground line-clamp-2">
|
||||
{session.problem_summary || 'Untitled session'}
|
||||
</p>
|
||||
<span className="text-[0.625rem] font-label text-primary shrink-0">
|
||||
<span className="text-[0.625rem] font-sans text-xs text-primary shrink-0">
|
||||
{Math.round(session.similarity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -71,13 +71,13 @@ export function SimilarSessions({ sessionId }: SimilarSessionsProps) {
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-1.5">
|
||||
{session.problem_domain && (
|
||||
<span className="text-[0.5rem] font-label uppercase tracking-wider text-muted-foreground/70">
|
||||
<span className="text-[0.5rem] font-sans text-xs uppercase tracking-wider text-muted-foreground/70">
|
||||
{session.problem_domain}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'text-[0.5rem] font-label uppercase',
|
||||
'text-[0.5rem] font-sans text-xs uppercase',
|
||||
session.status === 'resolved'
|
||||
? 'text-emerald-400'
|
||||
: session.status === 'escalated'
|
||||
|
||||
@@ -11,7 +11,7 @@ export function GuideCard({ guide }: GuideCardProps) {
|
||||
return (
|
||||
<Link
|
||||
to={`/guides/${guide.slug}`}
|
||||
className="glass-card block rounded-2xl p-5 transition-all"
|
||||
className="card-interactive block rounded-lg p-5 transition-all"
|
||||
>
|
||||
<div className="flex items-start gap-3.5">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
@@ -24,7 +24,7 @@ export function GuideCard({ guide }: GuideCardProps) {
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{guide.summary}
|
||||
</p>
|
||||
<span className="mt-2 inline-block font-label text-[0.625rem] uppercase tracking-widest text-primary">
|
||||
<span className="mt-2 inline-block font-sans text-[0.625rem] uppercase tracking-widest text-primary">
|
||||
{guide.sections.length} {guide.sections.length === 1 ? 'section' : 'sections'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function GuideSection({ section, index }: GuideSectionProps) {
|
||||
<ol className="space-y-3 pl-8">
|
||||
{section.steps.map((step, i) => (
|
||||
<li key={i} className="relative">
|
||||
<span className="absolute -left-6 top-0.5 font-label text-[0.625rem] text-muted-foreground">
|
||||
<span className="absolute -left-6 top-0.5 font-sans text-[0.625rem] text-muted-foreground">
|
||||
{i + 1}.
|
||||
</span>
|
||||
<p
|
||||
|
||||
@@ -55,7 +55,7 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'glass-card-static border-l-4 p-4 transition-all',
|
||||
'card-flat border-l-4 p-4 transition-all',
|
||||
confidenceColor(node.confidence_score),
|
||||
node.user_approved && 'opacity-75',
|
||||
)}
|
||||
@@ -66,17 +66,17 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{node.node_type}
|
||||
</span>
|
||||
<span className={cn('font-label text-[0.625rem]', confidenceTextColor(node.confidence_score))}>
|
||||
<span className={cn('font-sans text-xs text-[0.625rem]', confidenceTextColor(node.confidence_score))}>
|
||||
{confidenceLabel(node.confidence_score)} ({Math.round(node.confidence_score * 100)}%)
|
||||
</span>
|
||||
{node.user_approved && (
|
||||
<span className="font-label text-[0.625rem] text-emerald-400">Approved</span>
|
||||
<span className="font-sans text-xs text-[0.625rem] text-emerald-400">Approved</span>
|
||||
)}
|
||||
{node.user_edited && (
|
||||
<span className="font-label text-[0.625rem] text-blue-400">Edited</span>
|
||||
<span className="font-sans text-xs text-[0.625rem] text-blue-400">Edited</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
<button
|
||||
onClick={() => handleAction('edit', { content: { ...node.content, question: editContent, content: editContent } })}
|
||||
disabled={busy}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-gradient-brand text-[#101114] hover:opacity-90"
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-white hover:brightness-110"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -143,7 +143,7 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
<button
|
||||
onClick={() => handleAction('regenerate')}
|
||||
disabled={busy}
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-accent-dim transition-colors"
|
||||
title="Regenerate"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
@@ -151,7 +151,7 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
<button
|
||||
onClick={() => handleAction('insert_after', { content: { question: 'New node', type: node.node_type } })}
|
||||
disabled={busy}
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-accent-dim transition-colors"
|
||||
title="Insert after"
|
||||
>
|
||||
<Plus size={14} />
|
||||
@@ -182,7 +182,7 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
<div className="mt-2 space-y-1 pl-3 border-l border-border">
|
||||
{options.map((opt, i) => (
|
||||
<p key={i} className="text-xs text-muted-foreground">
|
||||
{opt.label} {opt.next_node_id && <span className="text-[#5a6170]">→ {opt.next_node_id}</span>}
|
||||
{opt.label} {opt.next_node_id && <span className="text-text-muted">→ {opt.next_node_id}</span>}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -192,7 +192,7 @@ export function NodeCard({ node, onEdit, onHighlight }: NodeCardProps) {
|
||||
|
||||
{/* Source excerpt */}
|
||||
{node.source_excerpt && (
|
||||
<p className="mt-2 text-xs text-[#5a6170] italic truncate" title={node.source_excerpt}>
|
||||
<p className="mt-2 text-xs text-text-muted italic truncate" title={node.source_excerpt}>
|
||||
Source: {node.source_excerpt}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ReviewScreen({ kbImport, onEditNode, onApproveAll, onCommit, onD
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
{/* Header */}
|
||||
<div className="glass-card-static p-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="card-flat p-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-heading font-semibold text-foreground">{flowTitle}</h2>
|
||||
{flowDescription && (
|
||||
@@ -88,10 +88,10 @@ export function ReviewScreen({ kbImport, onEditNode, onApproveAll, onCommit, onD
|
||||
</div>
|
||||
|
||||
{/* Nodes panel */}
|
||||
<div className="flex flex-col glass-card-static overflow-hidden">
|
||||
<div className="flex flex-col card-flat overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<span className="text-sm font-medium text-foreground">Generated Flow</span>
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground ml-auto">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground ml-auto">
|
||||
{kbImport.target_type === 'troubleshooting' ? 'Troubleshooting' : 'Project'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ export function ReviewScreen({ kbImport, onEditNode, onApproveAll, onCommit, onD
|
||||
onClick={onDiscard}
|
||||
disabled={loading}
|
||||
className={cn(
|
||||
'px-4 py-2.5 rounded-[10px] text-sm font-medium transition-colors',
|
||||
'px-4 py-2.5 rounded-lg text-sm font-medium transition-colors',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-muted-foreground',
|
||||
'hover:text-foreground hover:border-[rgba(255,255,255,0.12)]',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
@@ -131,9 +131,9 @@ export function ReviewScreen({ kbImport, onEditNode, onApproveAll, onCommit, onD
|
||||
onClick={() => onCommit()}
|
||||
disabled={loading || nodes.length === 0}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-semibold transition-all',
|
||||
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 active:scale-[0.97]',
|
||||
'flex items-center gap-2 px-6 py-2.5 rounded-lg text-sm font-semibold transition-all',
|
||||
'bg-primary text-white',
|
||||
'hover:brightness-110 active:scale-[0.98]',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -24,11 +24,11 @@ export function SourcePanel({ sourceText, sourceFormat, highlightExcerpt }: Sour
|
||||
}, [sourceText, highlightExcerpt])
|
||||
|
||||
return (
|
||||
<div className="glass-card-static flex flex-col h-full">
|
||||
<div className="card-flat flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<FileText size={16} className="text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-foreground">Source Document</span>
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground ml-auto">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground ml-auto">
|
||||
{sourceFormat}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +31,9 @@ export function SuccessScreen({ result, onViewFlow, onConvertAnother }: SuccessS
|
||||
<button
|
||||
onClick={onViewFlow}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-semibold transition-all',
|
||||
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 active:scale-[0.97]'
|
||||
'flex items-center gap-2 px-6 py-2.5 rounded-lg text-sm font-semibold transition-all',
|
||||
'bg-primary text-white',
|
||||
'hover:brightness-110 active:scale-[0.98]'
|
||||
)}
|
||||
>
|
||||
View Flow
|
||||
@@ -42,7 +42,7 @@ export function SuccessScreen({ result, onViewFlow, onConvertAnother }: SuccessS
|
||||
<button
|
||||
onClick={onConvertAnother}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-medium transition-colors',
|
||||
'flex items-center gap-2 px-6 py-2.5 rounded-lg text-sm font-medium transition-colors',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)]'
|
||||
)}
|
||||
|
||||
@@ -82,7 +82,7 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{/* Quota info */}
|
||||
{quota && (
|
||||
<div className="glass-card-static p-4 flex items-center justify-between">
|
||||
<div className="card-flat p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sparkles size={18} className="text-primary" />
|
||||
<div>
|
||||
@@ -110,9 +110,9 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
<button
|
||||
onClick={() => setMode('paste')}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-[10px] text-sm font-medium transition-colors',
|
||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
mode === 'paste'
|
||||
? 'bg-primary/10 text-foreground border border-primary/30'
|
||||
? 'bg-accent-dim text-foreground border border-primary/30'
|
||||
: 'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)]'
|
||||
)}
|
||||
>
|
||||
@@ -123,9 +123,9 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
<button
|
||||
onClick={() => setMode('file')}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-[10px] text-sm font-medium transition-colors',
|
||||
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
mode === 'file'
|
||||
? 'bg-primary/10 text-foreground border border-primary/30'
|
||||
? 'bg-accent-dim text-foreground border border-primary/30'
|
||||
: 'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)]'
|
||||
)}
|
||||
>
|
||||
@@ -136,11 +136,11 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="glass-card-static p-5 space-y-4">
|
||||
<div className="card-flat p-5 space-y-4">
|
||||
{mode === 'paste' ? (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="kb-title" className="block font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-1.5">
|
||||
<label htmlFor="kb-title" className="block font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-1.5">
|
||||
Title (optional)
|
||||
</label>
|
||||
<Input
|
||||
@@ -152,7 +152,7 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="kb-content" className="block font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-1.5">
|
||||
<label htmlFor="kb-content" className="block font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-1.5">
|
||||
KB Article Content
|
||||
</label>
|
||||
<Textarea
|
||||
@@ -223,7 +223,7 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
|
||||
{/* Target type selector */}
|
||||
<div>
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-2">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-2">
|
||||
Target Flow Type
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
@@ -232,7 +232,7 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
key={t.value}
|
||||
onClick={() => setTargetType(t.value)}
|
||||
className={cn(
|
||||
'glass-card-static p-4 text-left transition-all',
|
||||
'card-flat p-4 text-left transition-all',
|
||||
targetType === t.value
|
||||
? 'border-primary/30 bg-primary/5'
|
||||
: 'hover:border-[rgba(255,255,255,0.12)]'
|
||||
@@ -250,9 +250,9 @@ export function UploadScreen({ quota, onSubmitText, onSubmitFile, loading }: Upl
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit || loading || (quota != null && !quota.can_convert)}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-center gap-2 px-6 py-3 rounded-[10px] text-sm font-semibold transition-all',
|
||||
'bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 active:scale-[0.97]',
|
||||
'w-full flex items-center justify-center gap-2 px-6 py-3 rounded-lg text-sm font-semibold transition-all',
|
||||
'bg-primary text-white',
|
||||
'hover:brightness-110 active:scale-[0.98]',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useLocation, useNavigate, Link } from 'react-router-dom'
|
||||
import { Menu, X, LayoutGrid, Clock, Network, AlertTriangle, Code2, Wand2, BarChart3, Settings, LogOut, Shield, Library } from 'lucide-react'
|
||||
import { Menu, X, LayoutGrid, Clock, AlertTriangle, GitBranch, Code2, Wand2, BarChart3, Settings, LogOut, Shield, Layers } from 'lucide-react'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
@@ -16,7 +16,7 @@ export function AppLayout() {
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuthStore()
|
||||
const { effectiveRole } = usePermissions()
|
||||
const sidebarCollapsed = useUserPreferencesStore(s => s.sidebarCollapsed)
|
||||
const sidebarPinned = useUserPreferencesStore(s => s.sidebarPinned)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
// Close mobile menu on route change
|
||||
@@ -54,8 +54,8 @@ export function AppLayout() {
|
||||
{ path: '/', label: 'Dashboard', icon: LayoutGrid },
|
||||
{ path: '/sessions', label: 'Active Sessions', icon: Clock },
|
||||
{ path: '/escalations', label: 'Escalations', icon: AlertTriangle },
|
||||
{ path: '/trees', label: 'Flows', icon: Network },
|
||||
{ path: '/step-library', label: 'Step Library', icon: Library },
|
||||
{ path: '/trees', label: 'Flows', icon: GitBranch },
|
||||
{ path: '/step-library', label: 'Step Library', icon: Layers },
|
||||
{ path: '/scripts', label: 'Scripts', icon: Code2 },
|
||||
{ path: '/script-builder', label: 'Script Builder', icon: Wand2 },
|
||||
{ path: '/analytics', label: 'Analytics', icon: BarChart3 },
|
||||
@@ -64,46 +64,21 @@ export function AppLayout() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Atmosphere orbs — ambient light behind glass */}
|
||||
<div
|
||||
className="pointer-events-none fixed z-0"
|
||||
style={{
|
||||
top: '-120px',
|
||||
right: '-80px',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(6, 182, 212, 0.15) 0%, rgba(6, 182, 212, 0.04) 40%, transparent 70%)',
|
||||
filter: 'blur(60px)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none fixed z-0"
|
||||
style={{
|
||||
bottom: '-100px',
|
||||
left: '-60px',
|
||||
width: '500px',
|
||||
height: '500px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, rgba(99, 102, 241, 0.02) 40%, transparent 70%)',
|
||||
filter: 'blur(50px)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn('app-shell relative z-1', sidebarCollapsed && 'app-shell--collapsed')}
|
||||
className={cn('app-shell relative z-1', sidebarPinned && 'app-shell--pinned')}
|
||||
data-testid="app-shell"
|
||||
>
|
||||
{/* Top Bar - spans full width */}
|
||||
<TopBar />
|
||||
|
||||
{/* Sidebar - desktop only */}
|
||||
<div className="hidden md:block">
|
||||
{/* Sidebar - desktop only, must fill grid row */}
|
||||
<div className="hidden md:flex md:flex-col md:min-h-0 md:h-full">
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
{/* Mobile hamburger - overlaid on topbar */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="fixed left-4 top-3.5 z-50 rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors md:hidden"
|
||||
aria-label="Open menu"
|
||||
@@ -119,15 +94,17 @@ export function AppLayout() {
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-[var(--sidebar-bg)] shadow-2xl animate-slide-in-left">
|
||||
<div className="flex h-14 items-center justify-between border-b border-border px-4">
|
||||
<nav
|
||||
className="absolute inset-y-0 left-0 w-72 shadow-2xl animate-slide-in-left"
|
||||
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<div className="flex h-14 items-center justify-between px-4" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
||||
<Link to="/" className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-brand">
|
||||
<BrandLogo size="sm" className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm font-heading font-bold">ResolutionFlow</span>
|
||||
<BrandLogo size="sm" />
|
||||
<span className="text-sm font-heading font-bold text-text-heading">ResolutionFlow</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground"
|
||||
aria-label="Close menu"
|
||||
@@ -138,7 +115,7 @@ export function AppLayout() {
|
||||
|
||||
<div className="flex flex-col p-3">
|
||||
{/* User info */}
|
||||
<div className="mb-3 border-b border-border pb-3 px-3">
|
||||
<div className="mb-3 pb-3 px-3" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
||||
<p className="text-sm font-medium text-foreground">{user?.name || user?.email}</p>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
@@ -162,8 +139,8 @@ export function AppLayout() {
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-[var(--sidebar-active)] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
? 'bg-accent-dim text-foreground'
|
||||
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon size={18} />
|
||||
@@ -174,10 +151,11 @@ export function AppLayout() {
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-3 border-t border-border pt-3">
|
||||
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--color-border-default)' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground transition-colors"
|
||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground hover:bg-input hover:text-foreground transition-colors"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
Logout
|
||||
|
||||
@@ -329,7 +329,7 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
|
||||
placeholder="Search flows, ask a question, navigate…"
|
||||
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-hidden"
|
||||
/>
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-label text-[0.625rem] text-muted-foreground">
|
||||
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[0.625rem] text-muted-foreground">
|
||||
ESC
|
||||
</kbd>
|
||||
</div>
|
||||
@@ -354,7 +354,7 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
|
||||
<div key={group.type}>
|
||||
{/* Section label */}
|
||||
<div className="px-3 pt-2 pb-1">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<span className="font-sans text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{group.label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -451,11 +451,11 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
|
||||
{flatItems.length > 0 && (
|
||||
<div className="flex items-center gap-4 border-t border-border px-4 py-2">
|
||||
<span className="flex items-center gap-1 text-[0.625rem] text-muted-foreground">
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-label">↑↓</kbd>
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-mono">↑↓</kbd>
|
||||
Navigate
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-[0.625rem] text-muted-foreground">
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-label">↵</kbd>
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-mono">↵</kbd>
|
||||
Open
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function NavItem({ href, icon: Icon, label, badge, iconColor, matchPaths,
|
||||
title={label}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
|
||||
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-primary" />
|
||||
)}
|
||||
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} style={iconColor ? { color: iconColor } : undefined} />
|
||||
{badge !== undefined && badge !== 0 && badge !== 'dot' && (
|
||||
@@ -76,7 +76,7 @@ export function NavItem({ href, icon: Icon, label, badge, iconColor, matchPaths,
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{isActive && !isParentDimmed && (
|
||||
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
|
||||
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-primary" />
|
||||
)}
|
||||
|
||||
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} style={iconColor ? { color: iconColor } : undefined} />
|
||||
@@ -87,7 +87,7 @@ export function NavItem({ href, icon: Icon, label, badge, iconColor, matchPaths,
|
||||
badge === 'dot' ? (
|
||||
<span className="ml-auto h-1.5 w-1.5 shrink-0 rounded-full bg-brand-gradient-from" />
|
||||
) : (
|
||||
<span className="ml-auto shrink-0 rounded-full bg-card border border-border px-2 text-[0.6875rem] font-label text-muted-foreground">
|
||||
<span className="ml-auto shrink-0 rounded-full bg-card border border-border px-2 text-[0.6875rem] font-mono text-muted-foreground">
|
||||
{badge}
|
||||
</span>
|
||||
)
|
||||
@@ -117,7 +117,7 @@ export function NavItem({ href, icon: Icon, label, badge, iconColor, matchPaths,
|
||||
>
|
||||
<span className="truncate">{child.label}</span>
|
||||
{child.count !== undefined && (
|
||||
<span className="ml-auto shrink-0 rounded-full bg-card border border-border px-2 text-[0.6875rem] font-label text-muted-foreground">
|
||||
<span className="ml-auto shrink-0 rounded-full bg-card border border-border px-2 text-[0.6875rem] font-mono text-muted-foreground">
|
||||
{child.count}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -144,11 +144,11 @@ export function QuickLaunch({ open, onClose }: QuickLaunchProps) {
|
||||
|
||||
<div className="flex items-center gap-4 border-t border-border px-4 py-2">
|
||||
<span className="flex items-center gap-1 text-[0.625rem] text-muted-foreground">
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-label">↑↓</kbd>
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-mono">↑↓</kbd>
|
||||
Navigate
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-[0.625rem] text-muted-foreground">
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-label">↵</kbd>
|
||||
<kbd className="rounded border border-border bg-background px-1 py-px font-mono">↵</kbd>
|
||||
Open
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,175 +1,497 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
LayoutGrid, Network, Clock, FileOutput, BarChart3, TrendingUp,
|
||||
Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, ListChecks,
|
||||
BookOpen, Code2, Library, AlertTriangle, Wand2,
|
||||
LayoutGrid, Clock, AlertTriangle, GitBranch, Layers, Code2, Wand2,
|
||||
ListChecks, Download, BarChart3, Rocket,
|
||||
Settings, Pin, PinOff,
|
||||
Zap, Database, HelpCircle,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { sidebarApi } from '@/api'
|
||||
import type { SidebarStatsResponse } from '@/api/sidebar'
|
||||
import { NavItem } from './NavItem'
|
||||
import { prefetchForRoute } from '@/lib/routePrefetch'
|
||||
|
||||
// Semantic icon colors — each nav item gets a unique color for visual landmarks
|
||||
const NAV_COLORS = {
|
||||
dashboard: '#22d3ee', // cyan-400
|
||||
flows: '#a78bfa', // violet-400
|
||||
sessions: '#34d399', // emerald-400
|
||||
exports: '#60a5fa', // blue-400
|
||||
stepLib: '#fb923c', // orange-400
|
||||
scripts: '#2dd4bf', // teal-400
|
||||
scriptBuilder: '#e879f9', // fuchsia-400
|
||||
analytics: '#38bdf8', // sky-400
|
||||
guides: '#a3e635', // lime-400
|
||||
feedback: '#818cf8', // indigo-400
|
||||
} as const
|
||||
/* ── Types ──────────────────────────────────────────── */
|
||||
|
||||
interface NavSubItem {
|
||||
href: string
|
||||
label: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
interface NavEntry {
|
||||
href: string
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
shortLabel: string
|
||||
badge?: number
|
||||
matchPaths?: string[]
|
||||
children?: NavSubItem[]
|
||||
}
|
||||
|
||||
interface NavSection {
|
||||
title: string
|
||||
items: NavEntry[]
|
||||
}
|
||||
|
||||
/* ── Sidebar component ──────────────────────────────── */
|
||||
|
||||
export function Sidebar() {
|
||||
const sidebarCollapsed = useUserPreferencesStore(s => s.sidebarCollapsed)
|
||||
const toggleSidebar = useUserPreferencesStore(s => s.toggleSidebar)
|
||||
const location = useLocation()
|
||||
const sidebarPinned = useUserPreferencesStore(s => s.sidebarPinned)
|
||||
const toggleSidebarPinned = useUserPreferencesStore(s => s.toggleSidebarPinned)
|
||||
|
||||
const [stats, setStats] = useState<SidebarStatsResponse | null>(null)
|
||||
const [flyoutIndex, setFlyoutIndex] = useState<string | null>(null)
|
||||
const flyoutTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const sidebarRef = useRef<HTMLElement>(null)
|
||||
|
||||
/* ── Stats fetching ───────────────────────────────── */
|
||||
|
||||
const refreshStats = useCallback(() => {
|
||||
sidebarApi.getStats().then(setStats).catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Fetch sidebar stats — refreshes on navigation
|
||||
useEffect(() => {
|
||||
refreshStats()
|
||||
}, [location.pathname, refreshStats])
|
||||
useEffect(() => { refreshStats() }, [location.pathname, refreshStats])
|
||||
|
||||
// Refresh when sessions are created or completed
|
||||
useEffect(() => {
|
||||
window.addEventListener('session-changed', refreshStats)
|
||||
return () => window.removeEventListener('session-changed', refreshStats)
|
||||
}, [refreshStats])
|
||||
|
||||
const handleSidebarWheel = (e: React.WheelEvent<HTMLElement>) => {
|
||||
const sidebar = e.currentTarget
|
||||
const canSidebarScroll = sidebar.scrollHeight > sidebar.clientHeight
|
||||
const atTop = sidebar.scrollTop <= 0
|
||||
const atBottom = sidebar.scrollTop + sidebar.clientHeight >= sidebar.scrollHeight - 1
|
||||
/* ── Navigation data ──────────────────────────────── */
|
||||
|
||||
// If sidebar can't consume wheel movement, forward it to main content scroller.
|
||||
if (!canSidebarScroll || (e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
|
||||
const main = document.querySelector('.main-content') as HTMLElement | null
|
||||
if (main) {
|
||||
main.scrollTop += e.deltaY
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ── Grouped nav: 5 top-level icons (Sentry-style) ── */
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="sidebar flex flex-col border-r"
|
||||
style={{
|
||||
background: 'rgba(16, 17, 20, 0.5)',
|
||||
backdropFilter: 'var(--glass-blur-light)',
|
||||
WebkitBackdropFilter: 'var(--glass-blur-light)',
|
||||
borderColor: 'var(--glass-border)',
|
||||
}}
|
||||
onWheel={handleSidebarWheel}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<>
|
||||
{/* Collapsed: icon-only nav */}
|
||||
<div className="flex flex-col items-center px-1.5 py-3 space-y-1">
|
||||
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} collapsed />
|
||||
<NavItem href="/sessions" icon={Clock} label="Active Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} collapsed />
|
||||
<NavItem href="/escalations" icon={AlertTriangle} label="Escalations" badge={stats?.escalation_count || undefined} iconColor="#fbbf24" collapsed />
|
||||
<NavItem href="/trees" icon={Network} label="Flows" matchPaths={['/trees', '/flows', '/my-trees']} iconColor={NAV_COLORS.flows} collapsed />
|
||||
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} collapsed />
|
||||
<NavItem href="/scripts" icon={Code2} label="Scripts" iconColor={NAV_COLORS.scripts} collapsed />
|
||||
<NavItem href="/script-builder" icon={Wand2} label="Script Builder" iconColor={NAV_COLORS.scriptBuilder} collapsed />
|
||||
<NavItem href="/review-queue" icon={ListChecks} label="Review Queue" iconColor="#fbbf24" collapsed />
|
||||
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} collapsed />
|
||||
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} collapsed />
|
||||
<NavItem href="/analytics/flowpilot" icon={TrendingUp} label="FlowPilot Analytics" iconColor="#2dd4bf" collapsed />
|
||||
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} collapsed />
|
||||
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} collapsed />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Navigation */}
|
||||
<div className="px-3 py-2 space-y-0.5">
|
||||
{/* Dashboard */}
|
||||
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} />
|
||||
|
||||
{/* Resolve */}
|
||||
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
||||
Resolve
|
||||
</div>
|
||||
<NavItem href="/sessions" icon={Clock} label="Active Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} matchPaths={['/sessions']} />
|
||||
<NavItem href="/escalations" icon={AlertTriangle} label="Escalations" badge={stats?.escalation_count || undefined} iconColor="#fbbf24" />
|
||||
|
||||
{/* Knowledge */}
|
||||
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
||||
Knowledge
|
||||
</div>
|
||||
<NavItem
|
||||
href="/trees"
|
||||
icon={Network}
|
||||
label="Flows"
|
||||
badge={stats?.tree_counts.total || undefined}
|
||||
iconColor={NAV_COLORS.flows}
|
||||
matchPaths={['/trees', '/flows', '/my-trees']}
|
||||
children={[
|
||||
const railGroups: NavEntry[] = [
|
||||
{
|
||||
href: '/', icon: LayoutGrid, label: 'Home', shortLabel: 'Home',
|
||||
matchPaths: ['/'],
|
||||
},
|
||||
{
|
||||
href: '/sessions', icon: Zap, label: 'Work', shortLabel: 'Work',
|
||||
badge: (stats?.active_count || 0) + (stats?.escalation_count || 0) || undefined,
|
||||
matchPaths: ['/sessions', '/escalations', '/pilot'],
|
||||
children: [
|
||||
{ href: '/sessions', label: 'Active Sessions', count: stats?.active_count || undefined },
|
||||
{ href: '/escalations', label: 'Escalations', count: stats?.escalation_count || undefined },
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/trees', icon: Database, label: 'Knowledge', shortLabel: 'Know',
|
||||
badge: stats?.tree_counts.total || undefined,
|
||||
matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/scripts', '/script-builder', '/review-queue'],
|
||||
children: [
|
||||
{ href: '/trees', label: 'All Flows', count: stats?.tree_counts.total || undefined },
|
||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
{ href: '/trees?type=maintenance', label: 'Maintenance', count: stats?.tree_counts.maintenance || undefined },
|
||||
]}
|
||||
/>
|
||||
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} />
|
||||
<NavItem href="/scripts" icon={Code2} label="Scripts" iconColor={NAV_COLORS.scripts} />
|
||||
<NavItem href="/script-builder" icon={Wand2} label="Script Builder" iconColor={NAV_COLORS.scriptBuilder} />
|
||||
<NavItem href="/review-queue" icon={ListChecks} label="Review Queue" iconColor="#fbbf24" />
|
||||
{ href: '/step-library', label: 'Step Library' },
|
||||
{ href: '/scripts', label: 'Scripts' },
|
||||
{ href: '/script-builder', label: 'Script Builder' },
|
||||
{ href: '/review-queue', label: 'Review Queue' },
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/analytics', icon: BarChart3, label: 'Insights', shortLabel: 'Data',
|
||||
matchPaths: ['/analytics', '/shares'],
|
||||
children: [
|
||||
{ href: '/shares', label: 'Exports' },
|
||||
{ href: '/analytics', label: 'Analytics' },
|
||||
{ href: '/analytics/flowpilot', label: 'FlowPilot Analytics' },
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/guides', icon: HelpCircle, label: 'Help', shortLabel: 'Help',
|
||||
matchPaths: ['/guides', '/feedback'],
|
||||
children: [
|
||||
{ href: '/guides', label: 'User Guides' },
|
||||
{ href: '/feedback', label: 'Feedback' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
{/* Insights */}
|
||||
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
||||
Insights
|
||||
</div>
|
||||
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} />
|
||||
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} />
|
||||
<NavItem href="/analytics/flowpilot" icon={TrendingUp} label="FlowPilot Analytics" iconColor="#2dd4bf" />
|
||||
</div>
|
||||
</>
|
||||
/* Pinned mode still uses the detailed section layout */
|
||||
const sections: NavSection[] = [
|
||||
{
|
||||
title: 'RESOLVE',
|
||||
items: [
|
||||
{ href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash' },
|
||||
{ href: '/sessions', icon: Clock, label: 'Active Sessions', shortLabel: 'Sessions', badge: stats?.active_count || undefined, matchPaths: ['/sessions'] },
|
||||
{ href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal', badge: stats?.escalation_count || undefined },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'KNOWLEDGE',
|
||||
items: [
|
||||
{
|
||||
href: '/trees', icon: GitBranch, label: 'Flows', shortLabel: 'Flows',
|
||||
badge: stats?.tree_counts.total || undefined,
|
||||
matchPaths: ['/trees', '/flows', '/my-trees'],
|
||||
children: [
|
||||
{ href: '/trees', label: 'All Flows' },
|
||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
{ href: '/trees?type=maintenance', label: 'Maintenance', count: stats?.tree_counts.maintenance || undefined },
|
||||
],
|
||||
},
|
||||
{ href: '/step-library', icon: Layers, label: 'Step Library', shortLabel: 'Steps' },
|
||||
{ href: '/scripts', icon: Code2, label: 'Scripts', shortLabel: 'Scripts' },
|
||||
{ href: '/script-builder', icon: Wand2, label: 'Script Builder', shortLabel: 'Builder' },
|
||||
{ href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'INSIGHTS',
|
||||
items: [
|
||||
{ href: '/shares', icon: Download, label: 'Exports', shortLabel: 'Export' },
|
||||
{ href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats' },
|
||||
{ href: '/analytics/flowpilot', icon: Rocket, label: 'FlowPilot Analytics', shortLabel: 'FPilot' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const footerItems: NavEntry[] = [
|
||||
{ href: '/account', icon: Settings, label: 'Account', shortLabel: 'Acct' },
|
||||
]
|
||||
|
||||
/* ── Active detection ─────────────────────────────── */
|
||||
|
||||
const isActive = (item: NavEntry) => {
|
||||
if (item.matchPaths) return item.matchPaths.some(p =>
|
||||
p === '/' ? location.pathname === '/' : location.pathname.startsWith(p)
|
||||
)
|
||||
if (item.href === '/') return location.pathname === '/'
|
||||
return location.pathname.startsWith(item.href)
|
||||
}
|
||||
|
||||
const isChildActive = (child: NavSubItem) => {
|
||||
const fullPath = location.pathname + location.search
|
||||
return fullPath === child.href || fullPath.startsWith(child.href + '&')
|
||||
}
|
||||
|
||||
/* ── Flyout management ────────────────────────────── */
|
||||
|
||||
const openFlyout = (key: string) => {
|
||||
if (flyoutTimeout.current) clearTimeout(flyoutTimeout.current)
|
||||
setFlyoutIndex(key)
|
||||
}
|
||||
|
||||
const closeFlyout = () => {
|
||||
flyoutTimeout.current = setTimeout(() => setFlyoutIndex(null), 400)
|
||||
}
|
||||
|
||||
const keepFlyout = () => {
|
||||
if (flyoutTimeout.current) clearTimeout(flyoutTimeout.current)
|
||||
}
|
||||
|
||||
/* ── Drawer resize ───────────────────────────────── */
|
||||
|
||||
const [drawerWidth, setDrawerWidth] = useState(240)
|
||||
const isResizing = useRef(false)
|
||||
|
||||
const handleResizeStart = (e: ReactPointerEvent) => {
|
||||
e.preventDefault()
|
||||
isResizing.current = true
|
||||
const startX = e.clientX
|
||||
const startWidth = drawerWidth
|
||||
|
||||
const onMove = (ev: globalThis.PointerEvent) => {
|
||||
if (!isResizing.current) return
|
||||
const newWidth = Math.max(180, Math.min(400, startWidth + (ev.clientX - startX)))
|
||||
setDrawerWidth(newWidth)
|
||||
}
|
||||
|
||||
const onUp = () => {
|
||||
isResizing.current = false
|
||||
document.removeEventListener('pointermove', onMove)
|
||||
document.removeEventListener('pointerup', onUp)
|
||||
}
|
||||
|
||||
document.addEventListener('pointermove', onMove)
|
||||
document.addEventListener('pointerup', onUp)
|
||||
}
|
||||
|
||||
/* Close flyout on Escape */
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setFlyoutIndex(null)
|
||||
}
|
||||
document.addEventListener('keydown', handleKey)
|
||||
return () => document.removeEventListener('keydown', handleKey)
|
||||
}, [])
|
||||
|
||||
/* ── Wheel forwarding (when sidebar can't scroll) ── */
|
||||
|
||||
const handleWheel = (e: React.WheelEvent<HTMLElement>) => {
|
||||
const el = e.currentTarget
|
||||
const canScroll = el.scrollHeight > el.clientHeight
|
||||
const atTop = el.scrollTop <= 0
|
||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1
|
||||
if (!canScroll || (e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
|
||||
const main = document.querySelector('.main-content') as HTMLElement | null
|
||||
if (main) { main.scrollTop += e.deltaY; e.preventDefault() }
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Render helpers ───────────────────────────────── */
|
||||
|
||||
const renderRailItem = (item: NavEntry, key: string) => {
|
||||
const active = isActive(item)
|
||||
const Icon = item.icon
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="relative w-full"
|
||||
onMouseEnter={() => hasChildren && !sidebarPinned ? openFlyout(key) : undefined}
|
||||
>
|
||||
<Link
|
||||
to={item.href}
|
||||
onMouseEnter={() => prefetchForRoute(item.href)}
|
||||
onFocus={() => hasChildren && !sidebarPinned ? openFlyout(key) : undefined}
|
||||
className={cn(
|
||||
'group relative flex flex-col items-center justify-center rounded-lg px-2 py-3 transition-all duration-150',
|
||||
active
|
||||
? 'bg-accent-dim text-accent-text'
|
||||
: 'text-text-rail-label hover:text-foreground'
|
||||
)}
|
||||
title={item.label}
|
||||
>
|
||||
<span className="relative">
|
||||
<Icon size={24} strokeWidth={1.6} className={active ? 'opacity-100' : 'opacity-60 group-hover:opacity-85'} />
|
||||
{item.badge !== undefined && item.badge > 0 && (
|
||||
<span className="absolute -right-2 -top-1.5 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary px-1 text-[0.5rem] font-bold text-background">
|
||||
{item.badge > 99 ? '99+' : item.badge}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-1.5 text-[0.625rem] font-sans font-medium leading-tight truncate max-w-[64px]">
|
||||
{item.shortLabel}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Flyout rendered as drawer below */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPinnedItem = (item: NavEntry, key: string) => {
|
||||
const active = isActive(item)
|
||||
const Icon = item.icon
|
||||
const fullPath = location.pathname + location.search
|
||||
const activeChild = item.children?.find(c => fullPath === c.href || fullPath.startsWith(c.href + '&'))
|
||||
const isParentDimmed = !!activeChild && active
|
||||
|
||||
return (
|
||||
<div key={key} className="group/nav">
|
||||
<Link
|
||||
to={item.href}
|
||||
onMouseEnter={() => prefetchForRoute(item.href)}
|
||||
className={cn(
|
||||
'group relative flex items-center gap-3 rounded-lg px-3 py-2 text-[0.8125rem] font-medium transition-all duration-150',
|
||||
active
|
||||
? isParentDimmed
|
||||
? 'bg-[rgba(34,211,238,0.05)] text-foreground/70'
|
||||
: 'bg-accent-dim text-foreground'
|
||||
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{active && !isParentDimmed && (
|
||||
<div
|
||||
className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2"
|
||||
style={{ background: 'var(--color-accent)', borderRadius: '0 3px 3px 0' }}
|
||||
/>
|
||||
)}
|
||||
<Icon size={18} className={cn('shrink-0', active ? 'opacity-100' : 'opacity-70')} />
|
||||
<span className="truncate">{item.label}</span>
|
||||
{item.badge !== undefined && item.badge > 0 && (
|
||||
<span className="ml-auto shrink-0 rounded-full px-2 text-[0.6875rem] font-mono text-text-muted"
|
||||
style={{ background: 'var(--color-bg-card)', border: '1px solid var(--color-border-default)' }}>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Sub-items for pinned mode */}
|
||||
{item.children && item.children.length > 0 && (
|
||||
<div className={cn(
|
||||
'mt-0.5 space-y-0.5 overflow-hidden transition-all duration-200',
|
||||
active || activeChild
|
||||
? 'max-h-40 opacity-100'
|
||||
: 'max-h-0 opacity-0 group-hover/nav:max-h-40 group-hover/nav:opacity-100'
|
||||
)}>
|
||||
{item.children.map(child => {
|
||||
const childActive = isChildActive(child)
|
||||
return (
|
||||
<Link
|
||||
key={child.href}
|
||||
to={child.href}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg pl-9 pr-3 py-1.5 text-[0.8125rem] font-medium transition-colors',
|
||||
childActive
|
||||
? 'bg-accent-dim text-foreground'
|
||||
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{child.label}</span>
|
||||
{child.count !== undefined && (
|
||||
<span className="ml-auto shrink-0 rounded-full px-2 text-[0.6875rem] font-mono text-text-muted"
|
||||
style={{ background: 'var(--color-bg-card)', border: '1px solid var(--color-border-default)' }}>
|
||||
{child.count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ── Find active flyout group for drawer ── */
|
||||
|
||||
const activeFlyoutGroup = flyoutIndex && !sidebarPinned
|
||||
? railGroups.find((_, i) => `rail-${i}` === flyoutIndex) ||
|
||||
footerItems.find((_, i) => `footer-${i}` === flyoutIndex)
|
||||
: null
|
||||
|
||||
/* ── Main render ──────────────────────────────────── */
|
||||
|
||||
if (sidebarPinned) {
|
||||
return (
|
||||
<nav
|
||||
ref={sidebarRef}
|
||||
className="sidebar flex flex-col h-full"
|
||||
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
{/* Pinned sidebar content */}
|
||||
<div className="px-3 py-2 space-y-0.5">
|
||||
{sections.map((section, si) => (
|
||||
<div key={section.title}>
|
||||
{si > 0 && (
|
||||
<div className="font-mono text-[0.5625rem] uppercase tracking-[0.12em] text-text-muted px-3 pt-3 pb-1">
|
||||
{section.title}
|
||||
</div>
|
||||
)}
|
||||
{section.items.map((item, ii) => renderPinnedItem(item, `${si}-${ii}`))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Spacer — pushes footer to bottom */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
className={cn(
|
||||
"border-t",
|
||||
sidebarCollapsed ? "px-1.5 py-2 flex flex-col items-center" : "px-3 py-2 space-y-0.5"
|
||||
)}
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} />
|
||||
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} />
|
||||
<NavItem href="/account" icon={Settings} label="Account" />
|
||||
</>
|
||||
)}
|
||||
<div className="px-3 pt-2 pb-4 space-y-0.5" style={{ borderTop: '1px solid var(--color-border-default)' }}>
|
||||
{footerItems.map((item, i) => renderPinnedItem(item, `footer-${i}`))}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-lg text-[0.8125rem] font-medium text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground transition-colors",
|
||||
sidebarCollapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2"
|
||||
)}
|
||||
title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
type="button"
|
||||
onClick={toggleSidebarPinned}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-[0.8125rem] font-medium text-muted-foreground hover:bg-input hover:text-foreground transition-colors"
|
||||
title="Unpin sidebar"
|
||||
>
|
||||
{sidebarCollapsed ? <PanelLeftOpen size={20} /> : <PanelLeftClose size={18} />}
|
||||
{!sidebarCollapsed && <span>Collapse</span>}
|
||||
<PinOff size={18} className="shrink-0" />
|
||||
<span>Unpin</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
/* Icon Rail (default) — 5 grouped icons, Sentry-style */
|
||||
return (
|
||||
<div
|
||||
className="flex h-full"
|
||||
onMouseLeave={closeFlyout}
|
||||
>
|
||||
{/* Rail */}
|
||||
<nav
|
||||
ref={sidebarRef}
|
||||
className="sidebar flex flex-col items-center h-full shrink-0"
|
||||
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)', width: '72px' }}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
{/* Grouped nav items */}
|
||||
<div className="flex flex-col items-center w-full px-1 pt-4 space-y-1.5">
|
||||
{railGroups.map((item, i) => renderRailItem(item, `rail-${i}`))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Footer: Account + Pin */}
|
||||
<div className="flex flex-col items-center w-full px-1 pb-5 pt-3 space-y-1.5" style={{ borderTop: '1px solid var(--color-border-default)' }}>
|
||||
{footerItems.map((item, i) => renderRailItem(item, `footer-${i}`))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSidebarPinned}
|
||||
className="flex flex-col items-center justify-center rounded-lg px-2 py-3 text-text-rail-label hover:text-muted-foreground transition-colors"
|
||||
title="Pin sidebar"
|
||||
>
|
||||
<Pin size={22} strokeWidth={1.6} className="opacity-60 hover:opacity-85" />
|
||||
<span className="mt-1.5 text-[0.625rem] font-sans font-medium leading-tight">Pin</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Drawer panel — fixed position, full height, resizable, overlays main content */}
|
||||
{activeFlyoutGroup && activeFlyoutGroup.children && (
|
||||
<div
|
||||
className="fixed bottom-0 z-50 flex"
|
||||
style={{ top: 'var(--topbar-h)', left: '72px' }}
|
||||
onMouseEnter={keepFlyout}
|
||||
onMouseLeave={closeFlyout}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col h-full overflow-y-auto py-4 px-2"
|
||||
style={{
|
||||
width: drawerWidth,
|
||||
background: 'var(--color-bg-sidebar)',
|
||||
borderRight: '1px solid var(--color-border-default)',
|
||||
boxShadow: '4px 0 12px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
>
|
||||
{/* Drawer header */}
|
||||
<div className="px-3 mb-3">
|
||||
<h3 className="text-[0.6875rem] font-mono uppercase tracking-[0.12em] text-[#fbbf24]">
|
||||
{activeFlyoutGroup.label}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Drawer items */}
|
||||
<div className="space-y-0.5 px-1">
|
||||
{activeFlyoutGroup.children.map(child => (
|
||||
<Link
|
||||
key={child.href}
|
||||
to={child.href}
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-md px-3 py-2 text-[0.8125rem] transition-colors',
|
||||
isChildActive(child)
|
||||
? 'bg-accent-dim text-accent-text'
|
||||
: 'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<span>{child.label}</span>
|
||||
{child.count !== undefined && (
|
||||
<span className="text-[0.6875rem] font-mono text-text-muted">{child.count}</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
className="w-1 cursor-col-resize hover:bg-primary/20 active:bg-primary/30 transition-colors shrink-0"
|
||||
onPointerDown={handleResizeStart}
|
||||
title="Drag to resize"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function TopBar() {
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [userMenuOpen])
|
||||
|
||||
// ⌘K / Ctrl+K global shortcut
|
||||
// Cmd+K / Ctrl+K global shortcut
|
||||
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
@@ -55,12 +55,10 @@ export function TopBar() {
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className="topbar relative z-10 flex items-center gap-4 border-b px-4"
|
||||
className="topbar relative z-10 flex items-center gap-4 px-4 pl-14 md:pl-4"
|
||||
style={{
|
||||
background: 'rgba(16, 17, 20, 0.6)',
|
||||
backdropFilter: 'var(--glass-blur-strong)',
|
||||
WebkitBackdropFilter: 'var(--glass-blur-strong)',
|
||||
borderColor: 'var(--glass-border)',
|
||||
background: 'var(--color-bg-sidebar)',
|
||||
borderBottom: '1px solid var(--color-border-default)',
|
||||
}}
|
||||
>
|
||||
{/* Logo area */}
|
||||
@@ -68,10 +66,9 @@ export function TopBar() {
|
||||
to="/"
|
||||
className="flex items-center gap-2.5 pr-4 transition-all duration-200"
|
||||
>
|
||||
<BrandLogo size="sm" className="h-7 w-7 shrink-0" />
|
||||
<span className="text-sm font-heading font-bold tracking-tight whitespace-nowrap">
|
||||
<span className="text-foreground">Resolution</span>
|
||||
<span className="text-gradient-brand">Flow</span>
|
||||
<BrandLogo size="sm" />
|
||||
<span className="text-sm font-heading font-bold tracking-tight whitespace-nowrap text-text-heading">
|
||||
ResolutionFlow
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -85,16 +82,27 @@ export function TopBar() {
|
||||
style={{ maxWidth: '480px' }}
|
||||
>
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<div className="w-full rounded-lg border border-border bg-card py-2 pl-9 pr-14 text-[0.8125rem] text-muted-foreground cursor-pointer hover:border-primary/30 transition-colors">
|
||||
Search flows, sessions, tags…
|
||||
<div
|
||||
className="w-full rounded-md py-2 pl-9 pr-14 text-[0.8125rem] text-muted-foreground cursor-pointer transition-colors"
|
||||
style={{
|
||||
background: 'var(--color-bg-card)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border-hover)' }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border-default)' }}
|
||||
>
|
||||
Search flows, sessions, tags...
|
||||
</div>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 rounded border border-border bg-background px-1.5 py-0.5 font-label text-[0.625rem] text-muted-foreground">
|
||||
{navigator.platform?.toLowerCase().includes('mac') ? '⌘K' : 'Ctrl+K'}
|
||||
<span
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 rounded px-1.5 py-0.5 font-mono text-[0.625rem] text-text-muted"
|
||||
style={{ background: 'var(--color-bg-page)', border: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
{navigator.platform?.toLowerCase().includes('mac') ? '\u2318K' : 'Ctrl+K'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCommandPaletteOpen(true)}
|
||||
className="sm:hidden rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors"
|
||||
className="sm:hidden rounded-lg p-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="Search"
|
||||
>
|
||||
<Search size={18} />
|
||||
@@ -125,15 +133,19 @@ export function TopBar() {
|
||||
<div className="relative ml-2" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[10px] bg-gradient-brand text-xs font-heading font-bold text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[10px] text-xs font-heading font-bold text-white hover:opacity-90 transition-opacity"
|
||||
style={{ background: 'linear-gradient(135deg, #06b6d4, #22d3ee)' }}
|
||||
title={user?.name || user?.email || 'User'}
|
||||
>
|
||||
{initials}
|
||||
</button>
|
||||
|
||||
{userMenuOpen && (
|
||||
<div className="absolute right-0 z-50 mt-2 w-56 rounded-lg border border-border bg-card p-1 shadow-xl animate-scale-in">
|
||||
<div className="border-b border-border px-3 py-2.5 mb-1">
|
||||
<div
|
||||
className="absolute right-0 z-50 mt-2 w-56 rounded-lg p-1 shadow-xl animate-scale-in"
|
||||
style={{ background: 'var(--color-bg-card)', border: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<div className="px-3 py-2.5 mb-1" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
||||
<p className="text-sm font-medium text-foreground truncate">{user?.name || user?.email}</p>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
@@ -145,7 +157,7 @@ export function TopBar() {
|
||||
<Link
|
||||
to="/account"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-input hover:text-foreground"
|
||||
>
|
||||
<Settings size={14} />
|
||||
Account
|
||||
@@ -154,18 +166,18 @@ export function TopBar() {
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-input hover:text-foreground"
|
||||
>
|
||||
<Shield size={14} />
|
||||
Admin Panel
|
||||
</Link>
|
||||
)}
|
||||
<div className="border-t border-border mt-1 pt-1">
|
||||
<div className="mt-1 pt-1" style={{ borderTop: '1px solid var(--color-border-default)' }}>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm',
|
||||
'text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||
'text-muted-foreground hover:bg-input hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<LogOut size={14} />
|
||||
|
||||
@@ -102,7 +102,7 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-full z-20 mt-1 w-48 rounded-md border border-border',
|
||||
'bg-card backdrop-blur-xs py-1 shadow-lg'
|
||||
'bg-card py-1 shadow-lg'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ExportFlowModal({ treeId, treeName, onClose }: ExportFlowModalPr
|
||||
{/* Body */}
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Export <span className="font-medium text-foreground">{treeName}</span> as a <code className="text-xs font-label">.rfflow</code> file (JSON format).
|
||||
Export <span className="font-medium text-foreground">{treeName}</span> as a <code className="text-xs font-sans text-xs">.rfflow</code> file (JSON format).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ export function FolderEditModal({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-xs" onClick={onClose} />
|
||||
<div className="absolute inset-0 bg-black/80" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative z-10 w-full max-w-md bg-card border border-border rounded-2xl p-6 shadow-lg">
|
||||
|
||||
@@ -162,7 +162,7 @@ function FolderItem({
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-full z-10 mt-1 w-40 rounded-md border border-border',
|
||||
'bg-card backdrop-blur-xs py-1 shadow-lg'
|
||||
'bg-card py-1 shadow-lg'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -362,7 +362,7 @@ export function FolderSidebar({
|
||||
{/* Mobile backdrop */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/80 backdrop-blur-xs md:hidden"
|
||||
className="fixed inset-0 z-40 bg-black/80 md:hidden"
|
||||
onClick={onMobileClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -461,7 +461,7 @@ export function FolderSidebar({
|
||||
<div
|
||||
className={cn(
|
||||
'fixed z-50 w-44 rounded-md border border-border',
|
||||
'bg-card backdrop-blur-xs py-1 shadow-lg'
|
||||
'bg-card py-1 shadow-lg'
|
||||
)}
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -122,7 +122,7 @@ export function ImportFlowModal({ onClose }: ImportFlowModalProps) {
|
||||
'flex flex-col items-center justify-center rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors cursor-pointer',
|
||||
isDragging
|
||||
? 'border-primary/50 bg-primary/5'
|
||||
: 'border-border hover:border-white/[0.12]'
|
||||
: 'border-border hover:border-border-hover'
|
||||
)}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); setIsDragging(true) }}
|
||||
@@ -175,7 +175,7 @@ export function ImportFlowModal({ onClose }: ImportFlowModalProps) {
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Type:</span>
|
||||
<span className="rounded bg-primary/10 px-2 py-0.5 font-label text-primary">
|
||||
<span className="rounded bg-accent-dim px-2 py-0.5 font-sans text-xs text-primary">
|
||||
{TYPE_LABELS[parsed.flow.tree_type] || parsed.flow.tree_type}
|
||||
</span>
|
||||
</div>
|
||||
@@ -198,7 +198,7 @@ export function ImportFlowModal({ onClose }: ImportFlowModalProps) {
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-muted-foreground">Tags:</span>
|
||||
{parsed.flow.tags.map((tag) => (
|
||||
<span key={tag} className="rounded bg-card border border-border px-2 py-0.5 font-label text-foreground">
|
||||
<span key={tag} className="rounded bg-card border border-border px-2 py-0.5 font-sans text-xs text-foreground">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -115,7 +115,7 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-xs"
|
||||
className="absolute inset-0 bg-black/80"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export function TreeGridView({
|
||||
</span>
|
||||
)}
|
||||
{tree.tree_type === 'maintenance' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wide text-amber-400">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wide text-amber-400">
|
||||
<Wrench className="h-3 w-3" />
|
||||
Maintenance
|
||||
</span>
|
||||
@@ -68,7 +68,7 @@ export function TreeGridView({
|
||||
</span>
|
||||
)}
|
||||
{tree.category_info && (
|
||||
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
|
||||
<span className="rounded-full bg-primary/10 border border-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
@@ -163,8 +163,8 @@ export function TreeGridView({
|
||||
type="button"
|
||||
onClick={() => onStartSession(tree.id, tree.tree_type)}
|
||||
className={cn(
|
||||
'rounded-md bg-gradient-brand px-3 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90'
|
||||
'rounded-md border border-primary/40 px-3 py-2 text-sm font-medium text-primary',
|
||||
'hover:bg-primary/10 hover:border-primary/60 transition-colors'
|
||||
)}
|
||||
>
|
||||
Start Session
|
||||
|
||||
@@ -46,7 +46,7 @@ export function TreeListView({
|
||||
</span>
|
||||
)}
|
||||
{tree.tree_type === 'maintenance' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wide text-amber-400 shrink-0">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wide text-amber-400 shrink-0">
|
||||
<Wrench className="h-3 w-3" />
|
||||
Maintenance
|
||||
</span>
|
||||
@@ -74,7 +74,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-accent px-2 py-0.5 text-xs text-muted-foreground whitespace-nowrap">
|
||||
<span className="rounded-full bg-primary/10 border border-primary/20 px-2 py-0.5 text-xs text-primary whitespace-nowrap">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
@@ -166,8 +166,8 @@ export function TreeListView({
|
||||
type="button"
|
||||
onClick={() => onStartSession(tree.id, tree.tree_type)}
|
||||
className={cn(
|
||||
'rounded-md bg-gradient-brand px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 whitespace-nowrap'
|
||||
'rounded-md border border-primary/40 px-3 py-1.5 text-sm font-medium text-primary',
|
||||
'hover:bg-primary/10 hover:border-primary/60 transition-colors whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
Start
|
||||
|
||||
@@ -148,7 +148,7 @@ export function TreeTableView({
|
||||
</span>
|
||||
)}
|
||||
{tree.tree_type === 'maintenance' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wide text-amber-400 shrink-0">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wide text-amber-400 shrink-0">
|
||||
<Wrench className="h-3 w-3" />
|
||||
Maintenance
|
||||
</span>
|
||||
@@ -176,7 +176,7 @@ export function TreeTableView({
|
||||
</td>
|
||||
<td className="hidden lg:table-cell px-4 py-3">
|
||||
{tree.category_info && (
|
||||
<span className="inline-block rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
|
||||
<span className="inline-block rounded-full bg-primary/10 border border-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
@@ -270,8 +270,8 @@ export function TreeTableView({
|
||||
type="button"
|
||||
onClick={() => onStartSession(tree.id, tree.tree_type)}
|
||||
className={cn(
|
||||
'rounded-md bg-gradient-brand px-3 py-1.5 text-xs font-medium text-white shadow-lg shadow-primary/20',
|
||||
'hover:opacity-90 whitespace-nowrap'
|
||||
'rounded-md border border-primary/40 px-3 py-1.5 text-xs font-medium text-primary',
|
||||
'hover:bg-primary/10 hover:border-primary/60 transition-colors whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
Start
|
||||
|
||||
@@ -96,7 +96,7 @@ export function BatchLaunchModal({ treeId, treeName, onClose, onLaunched }: Batc
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 font-label text-[0.6875rem] uppercase tracking-wide transition-colors",
|
||||
"flex items-center gap-1.5 px-3 py-2 font-sans text-[0.6875rem] uppercase tracking-wide transition-colors",
|
||||
activeTab === tab.id
|
||||
? "border-b-2 border-primary text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
@@ -112,7 +112,7 @@ export function BatchLaunchModal({ treeId, treeName, onClose, onLaunched }: Batc
|
||||
<div className="p-6">
|
||||
{activeTab === 'manual' && (
|
||||
<div className="space-y-2">
|
||||
<label className="font-label text-[0.6875rem] uppercase tracking-wide text-muted-foreground">
|
||||
<label className="font-sans text-[0.6875rem] uppercase tracking-wide text-muted-foreground">
|
||||
Server names (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -192,7 +192,7 @@ export function BatchLaunchModal({ treeId, treeName, onClose, onLaunched }: Batc
|
||||
<button
|
||||
onClick={handleLaunch}
|
||||
disabled={isLaunching || targets.length === 0}
|
||||
className="rounded-lg bg-gradient-brand px-4 py-2 text-[0.875rem] font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-50"
|
||||
className="rounded-lg bg-primary px-4 py-2 text-[0.875rem] font-medium text-white hover:brightness-110 disabled:opacity-50"
|
||||
>
|
||||
{isLaunching ? 'Launching\u2026' : targets.length > 0 ? `Launch ${targets.length} Sessions` : 'Launch'}
|
||||
</button>
|
||||
|
||||
@@ -86,7 +86,7 @@ export function BatchStatusCard({ session, targetLabel, treeId, batchId }: Batch
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{isComplete && session?.outcome && (
|
||||
<span className={cn(
|
||||
"font-label text-[0.625rem] uppercase tracking-wide rounded-full px-2 py-0.5",
|
||||
"font-sans text-[0.625rem] uppercase tracking-wide rounded-full px-2 py-0.5",
|
||||
OUTCOME_COLORS[session.outcome] ?? OUTCOME_COLORS.unresolved
|
||||
)}>
|
||||
{OUTCOME_LABELS[session.outcome] ?? session.outcome}
|
||||
@@ -132,7 +132,7 @@ export function BatchStatusCard({ session, targetLabel, treeId, batchId }: Batch
|
||||
{isNotStarted && (
|
||||
<button
|
||||
onClick={handleStart}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-gradient-brand px-3 py-1.5 text-[0.8125rem] font-medium text-white shadow-xs shadow-primary/20 hover:opacity-90 transition-opacity"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-primary/40 px-3 py-1.5 text-[0.8125rem] font-medium text-primary hover:bg-primary/10 hover:border-primary/60 transition-colors"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
Start
|
||||
|
||||
@@ -226,7 +226,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
|
||||
treeId
|
||||
? 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90'
|
||||
? 'bg-primary text-white hover:brightness-110'
|
||||
: 'cursor-not-allowed border border-border bg-card text-muted-foreground opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -64,7 +64,7 @@ export function FallbackSteps({
|
||||
key={fbStep.id}
|
||||
className={cn(
|
||||
'rounded-lg border p-3 transition-colors',
|
||||
'bg-white/[0.02] border-border/50',
|
||||
'bg-card border-border/50',
|
||||
isCompleted && 'border-emerald-500/30 bg-emerald-500/5'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -212,7 +212,7 @@ export function IntakeFormModal({ isOpen, fields, treeName, onSubmit, onCancel }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4 bg-black/60 backdrop-blur-xs">
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4 bg-black/60">
|
||||
<div className="w-full max-w-full rounded-t-2xl border border-border bg-background shadow-xl sm:max-w-lg sm:rounded-2xl">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border px-6 py-4">
|
||||
|
||||
@@ -89,7 +89,7 @@ export function PrepareSessionModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4">
|
||||
<div className="absolute inset-0 bg-background/60 backdrop-blur-xs" onClick={onClose} />
|
||||
<div className="absolute inset-0 bg-background/60" onClick={onClose} />
|
||||
<div className="relative w-full max-w-full rounded-t-2xl border border-border bg-card shadow-2xl sm:max-w-lg sm:rounded-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
||||
@@ -160,7 +160,7 @@ export function PrepareSessionModal({
|
||||
{/* Intake form fields */}
|
||||
{intakeFields.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Variables (optional — can be filled later)
|
||||
</h4>
|
||||
{Array.from(grouped.entries()).map(([groupName, fields]) => (
|
||||
@@ -217,7 +217,7 @@ export function PrepareSessionModal({
|
||||
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-[10px] px-4 py-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
className="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -225,8 +225,8 @@ export function PrepareSessionModal({
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
'rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-[#101114]',
|
||||
'shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97]',
|
||||
'rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-white',
|
||||
'hover:brightness-110 active:scale-[0.98]',
|
||||
'disabled:opacity-40'
|
||||
)}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user