chore: Tailwind CSS v3 → v4 migration #99
633
docs/tailwind-v4-migration.md
Normal file
633
docs/tailwind-v4-migration.md
Normal file
@@ -0,0 +1,633 @@
|
||||
# ResolutionFlow — Tailwind v4 Migration & Feature Guide
|
||||
**Claude Code Handoff Document · Pre-Investor Pitch Sprint**
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
This document gives Claude Code everything needed to:
|
||||
1. Execute the mechanical Tailwind v3 → v4 upgrade
|
||||
2. Know which new v4 features to use going forward so the ResolutionFlow UI looks and feels premium for the investor pitch demo
|
||||
|
||||
## Branch
|
||||
|
||||
This migration touches nearly every file in the frontend. **Always work on a dedicated branch:**
|
||||
|
||||
```bash
|
||||
git checkout -b feat/tailwind-v4-upgrade
|
||||
```
|
||||
|
||||
Do not merge to `main` until all phases are complete and the full visual QA pass is done.
|
||||
|
||||
---
|
||||
|
||||
## Stack Context
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Frontend | React 19.2 + Vite 7 + TypeScript 5.9 |
|
||||
| Current Tailwind | v3.4.19 |
|
||||
| Component Library | shadcn/ui (target: new-york style post-upgrade) |
|
||||
| Canvas Editor | React Flow (@xyflow/react) |
|
||||
| Brand Color | Cyan — `#06b6d4 → #22d3ee` (hsl 187 72% 43%) |
|
||||
| Background | `hsl(228 12% 7%)` — dark theme only, no light mode |
|
||||
| Body Font | IBM Plex Sans |
|
||||
| Heading Font | Bricolage Grotesque |
|
||||
| Label/Mono Font | JetBrains Mono |
|
||||
| Brand Gradient | `linear-gradient(135deg, #06b6d4 0%, #22d3ee 100%)` |
|
||||
| Animation Library | tailwindcss-animate → **replace with tw-animate-css** |
|
||||
| Tailwind Plugins | None |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# PART 1: Mechanical Migration
|
||||
|
||||
Execute phases in order. Verify the app builds and renders correctly after each phase before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Update Dependencies
|
||||
|
||||
Run the official Tailwind upgrade tool from the `/frontend` directory. It handles the majority of migration automatically:
|
||||
|
||||
```bash
|
||||
# Run from /frontend
|
||||
npx @tailwindcss/upgrade@latest
|
||||
```
|
||||
|
||||
This tool will:
|
||||
- Update `tailwindcss` to v4
|
||||
- Install `@tailwindcss/vite` (replaces the PostCSS plugin)
|
||||
- Migrate `tailwind.config.js` → CSS `@theme` block
|
||||
- Update `@tailwind` directives to `@import`
|
||||
- Handle renamed utility classes in template files
|
||||
|
||||
After running, verify `package.json` reflects:
|
||||
|
||||
```json
|
||||
// devDependencies should now include:
|
||||
"tailwindcss": "^4.x.x",
|
||||
"@tailwindcss/vite": "^4.x.x"
|
||||
|
||||
// These are no longer needed and can be removed:
|
||||
// "autoprefixer" (bundled in v4 via Lightning CSS)
|
||||
// "postcss" (unless used for something other than Tailwind)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Update Vite Config
|
||||
|
||||
Replace the PostCSS-based Tailwind setup with the first-party Vite plugin:
|
||||
|
||||
```ts
|
||||
// vite.config.ts — BEFORE
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
|
||||
// vite.config.ts — AFTER
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
||||
```
|
||||
|
||||
Remove `postcss.config.js` if it exists and was only used for Tailwind. Lightning CSS is now bundled inside Tailwind v4.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Migrate CSS Entry Point
|
||||
|
||||
Update `index.css`:
|
||||
|
||||
```css
|
||||
/* BEFORE — v3 directives */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* AFTER — v4 single import */
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
/* ResolutionFlow is dark-only. Replace darkMode: ["class"] from tailwind.config.js
|
||||
with this custom variant declaration. This is the v4 equivalent. */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* React Flow styles must now live in CSS, not imported in App.tsx */
|
||||
@layer base {
|
||||
@import "@xyflow/react/dist/style.css";
|
||||
}
|
||||
```
|
||||
|
||||
> **Important:** Remove `import '@xyflow/react/dist/style.css'` from `App.tsx` after adding it here.
|
||||
> **Important:** Remove `darkMode: ["class"]` from `tailwind.config.js` — it is now handled by `@custom-variant dark` above. Since ResolutionFlow is dark-only with no light mode toggle, verify that the `.dark` class is still present on the `<html>` element in `index.html`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Migrate Theme Configuration
|
||||
|
||||
The upgrade tool migrates `tailwind.config.js` automatically, but verify the ResolutionFlow design tokens are correctly expressed. The `@theme` block lives in `index.css` after the imports.
|
||||
|
||||
### OKLCH Color Values
|
||||
|
||||
Claude Code flagged that the cyan range benefits from OKLCH for better gradient interpolation. Use these OKLCH equivalents for brand colors inside `@theme` — they render more accurately on wide-gamut displays (MacBooks, modern monitors used in pitch settings):
|
||||
|
||||
| Hex | OKLCH |
|
||||
|---|---|
|
||||
| `#06b6d4` (brand-from) | `oklch(72% 0.15 195)` |
|
||||
| `#22d3ee` (brand-to) | `oklch(82% 0.13 195)` |
|
||||
| `#0891b2` (brand-dark) | `oklch(62% 0.14 195)` |
|
||||
|
||||
### Full `@theme` Block
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
/* ResolutionFlow Brand — Cyan system (OKLCH for wide-gamut accuracy) */
|
||||
--color-brand-from: oklch(72% 0.15 195); /* #06b6d4 */
|
||||
--color-brand-to: oklch(82% 0.13 195); /* #22d3ee */
|
||||
--color-brand-dark: oklch(62% 0.14 195); /* #0891b2 */
|
||||
|
||||
/* Dark surface palette */
|
||||
--color-dark-DEFAULT: #101114;
|
||||
--color-dark-card: #14161a;
|
||||
--color-dark-surface: #14161a;
|
||||
|
||||
/* Text palette */
|
||||
--color-text-primary: #f8fafc;
|
||||
--color-text-secondary: #8891a0;
|
||||
--color-text-muted: #5a6170;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
|
||||
--font-heading: 'Bricolage Grotesque', system-ui, sans-serif;
|
||||
--font-label: 'JetBrains Mono', monospace;
|
||||
|
||||
/* Border radius */
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-md: calc(0.75rem - 2px);
|
||||
--radius-sm: calc(0.75rem - 4px);
|
||||
|
||||
/* Keyframe animations — move ALL @keyframes from index.css into @theme */
|
||||
--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;
|
||||
--animate-breathe-glow: breatheGlow 3s ease-in-out infinite alternate;
|
||||
--animate-bell-wobble: bellWobble 0.5s ease-in-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 breatheGlow {
|
||||
from { box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 20px oklch(72% 0.15 195 / 0.04); }
|
||||
to { box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 30px oklch(72% 0.15 195 / 0.12); }
|
||||
}
|
||||
@keyframes bellWobble {
|
||||
0% { transform: rotate(0deg); }
|
||||
20% { transform: rotate(8deg); }
|
||||
40% { transform: rotate(-6deg); }
|
||||
60% { transform: rotate(4deg); }
|
||||
80% { transform: rotate(-2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Important:** Remove the loose `@keyframes` blocks from `index.css` after moving them into `@theme`. They should not exist in both places.
|
||||
|
||||
### shadcn/ui CSS Variable Bridge
|
||||
|
||||
Keep the existing `:root` block as-is — shadcn still uses HSL format for its variables:
|
||||
|
||||
```css
|
||||
/* shadcn/ui CSS variable bridge — keep as-is from existing index.css */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 228 12% 7%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 220 10% 10%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 220 10% 10%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 187 72% 43%;
|
||||
--primary-foreground: 228 12% 7%;
|
||||
--secondary: 220 8% 14%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 220 8% 14%;
|
||||
--muted-foreground: 215 10% 58%;
|
||||
--accent: 220 8% 14%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 350 81% 55%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 220 8% 14%;
|
||||
--input: 220 8% 14%;
|
||||
--ring: 187 72% 43%;
|
||||
--radius: 0.75rem;
|
||||
/* ... rest of existing tokens ... */
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Replace Animation Library
|
||||
|
||||
```bash
|
||||
# Remove old plugin
|
||||
npm uninstall tailwindcss-animate
|
||||
|
||||
# Install replacement
|
||||
npm install -D tw-animate-css
|
||||
```
|
||||
|
||||
Remove any `@plugin 'tailwindcss-animate'` references from CSS files. The `@import "tw-animate-css"` added in Phase 3 replaces it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Consolidate Hardcoded Inline Colors
|
||||
|
||||
Claude Code identified **128 instances of `style={{}}` inline color values across 44 files**, including atmosphere orbs in `AppLayout.tsx` using hardcoded `rgba(6,182,212,...)`. Now that brand colors are proper CSS variables in `@theme`, replace all hardcoded cyan/brand references with variables.
|
||||
|
||||
**Search and replace targets:**
|
||||
|
||||
```ts
|
||||
// FIND these hardcoded patterns:
|
||||
rgba(6, 182, 212, ...) // brand-from with opacity
|
||||
rgba(34, 211, 238, ...) // brand-to with opacity
|
||||
#06b6d4 // brand-from hex
|
||||
#22d3ee // brand-to hex
|
||||
#0891b2 // brand-dark hex
|
||||
|
||||
// REPLACE with CSS variable equivalents:
|
||||
oklch(72% 0.15 195 / 0.XX) // brand-from with opacity (XX = your alpha)
|
||||
oklch(82% 0.13 195 / 0.XX) // brand-to with opacity
|
||||
var(--color-brand-from) // brand-from solid
|
||||
var(--color-brand-to) // brand-to solid
|
||||
var(--color-brand-dark) // brand-dark solid
|
||||
```
|
||||
|
||||
**Specifically in `AppLayout.tsx`** — atmosphere orbs should become:
|
||||
|
||||
```tsx
|
||||
// BEFORE
|
||||
style={{ background: 'rgba(6, 182, 212, 0.08)' }}
|
||||
|
||||
// AFTER — references the theme token, respects any future brand color changes
|
||||
style={{ background: 'oklch(72% 0.15 195 / 0.08)' }}
|
||||
|
||||
// Or better yet, move to a CSS utility class entirely:
|
||||
// .atmosphere-orb { background: oklch(72% 0.15 195 / 0.08); }
|
||||
```
|
||||
|
||||
> This is the right time to do this cleanup — the theme is already being touched, and it means future brand color changes only require updating `@theme` instead of hunting through 44 files.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Reinstall shadcn/ui Components
|
||||
|
||||
shadcn/ui components have been updated for Tailwind v4 and React 19. Commit any custom component modifications first, then reinstall to get refreshed versions.
|
||||
|
||||
```bash
|
||||
# Re-initialize with v4 defaults
|
||||
npx shadcn@latest init
|
||||
|
||||
# When prompted:
|
||||
# Style: new-york (more refined than 'default')
|
||||
# Base color: cyan (matches ResolutionFlow brand)
|
||||
|
||||
# Reinstall actively used components:
|
||||
npx shadcn@latest add button card dialog dropdown-menu
|
||||
npx shadcn@latest add badge input label select textarea
|
||||
npx shadcn@latest add table tabs tooltip separator
|
||||
npx shadcn@latest add sheet command popover
|
||||
```
|
||||
|
||||
**What's new in shadcn v4 components:**
|
||||
- `forwardRef` removed from all components (React 19 handles ref natively)
|
||||
- `data-slot` attributes added for easier CSS targeting
|
||||
- HSL colors converted to OKLCH for wider color gamut
|
||||
- Dark mode colors revisited for better accessibility
|
||||
- `tailwindcss-animate` deprecated in favor of `tw-animate-css`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Visual QA Checklist
|
||||
|
||||
Work through the app after Phases 1–6. These are the most likely regression areas:
|
||||
|
||||
- [ ] **Borders** — default border color changed from `gray-200` to `currentColor`. Check all card, table, and input borders against the `hsl(var(--border))` token.
|
||||
- [ ] **Focus rings** — default ring changed from `3px blue` to `1px currentColor`. Verify form fields and button focus states look intentional.
|
||||
- [ ] **Placeholder text** — changed from `gray-400` to `currentColor` at 50% opacity. Check all inputs.
|
||||
- [ ] **Button cursors** — now `cursor:default` (browser standard). Add `cursor-pointer` explicitly where desired.
|
||||
- [ ] **React Flow canvas** — verify nodes, handles, edges, and controls render correctly with CSS import moved to `@layer base`.
|
||||
- [ ] **Animations** — check dialog open/close, sheet slide, dropdown appear animations after the animation library swap.
|
||||
- [ ] **Keyframe animations** — verify `breatheGlow` on stat cards, `bellWobble` on notification bell, and all `fade-in-*` utilities still work after moving keyframes into `@theme`.
|
||||
- [ ] **Cyan brand gradient** — verify `bg-gradient-brand` still renders correctly. Check it looks at least as good (ideally better) with OKLCH values.
|
||||
- [ ] **Atmosphere orbs** — verify `AppLayout.tsx` orbs render correctly after inline style replacement with OKLCH variables.
|
||||
- [ ] **Glass morphism** — verify `.glass-card` and `.glass-card-static` backdrop blur still applies on all surfaces.
|
||||
- [ ] **JetBrains Mono** — verify label/code elements still use mono font correctly.
|
||||
- [ ] **Sonner toasts** — verify custom toast styling (card bg, border, icon colors) still applies.
|
||||
- [ ] **Dark variant** — verify `.dark` class on `<html>` is present and the `@custom-variant dark` replacement behaves identically to the old `darkMode: ["class"]` config.
|
||||
|
||||
---
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
```bash
|
||||
git checkout -b feat/tailwind-v4-upgrade
|
||||
|
||||
git commit -m "chore: upgrade Tailwind v3 → v4, update Vite plugin"
|
||||
git commit -m "chore: migrate CSS entry point — @import, @custom-variant dark"
|
||||
git commit -m "chore: migrate theme to @theme block — OKLCH colors, keyframes"
|
||||
git commit -m "chore: swap tailwindcss-animate for tw-animate-css"
|
||||
git commit -m "chore: consolidate 128 hardcoded inline colors to CSS variables"
|
||||
git commit -m "chore: reinstall shadcn/ui components for v4 (new-york style, cyan)"
|
||||
git commit -m "fix: visual QA pass — borders, rings, placeholders, glass, animations"
|
||||
git commit -m "feat: apply v4 feature enhancements across components"
|
||||
|
||||
# When complete and QA passes:
|
||||
git checkout main
|
||||
git merge feat/tailwind-v4-upgrade
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# PART 2: Tailwind v4 Feature Guide
|
||||
|
||||
**These are new capabilities available after the upgrade. Apply them proactively when building or modifying components — do not default to v3 patterns when a v4 equivalent is listed here.**
|
||||
|
||||
---
|
||||
|
||||
## Feature 1 — Auto-Resizing Textareas (`field-sizing-content`)
|
||||
|
||||
Apply to every multi-line text input in the app. Engineers typing session notes and documentation should never fight a fixed-height box.
|
||||
|
||||
**Where to apply:** Session runner documentation field · step notes input · FlowPilot prompt input · any multi-line input in the flow editor
|
||||
|
||||
```tsx
|
||||
// BEFORE — v3, fixed height or JS resize logic
|
||||
<textarea
|
||||
className="w-full resize-none h-32 rounded-md border p-3"
|
||||
/>
|
||||
|
||||
// AFTER — v4, grows automatically as the engineer types
|
||||
<textarea
|
||||
className="w-full field-sizing-content min-h-[80px] max-h-[400px]
|
||||
rounded-md border border-input bg-background p-3
|
||||
text-sm focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 2 — CSS-Native Enter Animations (`@starting-style`)
|
||||
|
||||
Use for elements that appear in the session runner — step transitions, panel reveals, status banners. No JavaScript animation library needed for these patterns.
|
||||
|
||||
**Where to apply:** Step cards appearing in session runner · status/alert banners · completion state reveals · FlowPilot suggestion panels
|
||||
|
||||
```css
|
||||
/* index.css — define the utility */
|
||||
@utility fade-in-up {
|
||||
transition: opacity 300ms ease, transform 300ms ease;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
@utility scale-in {
|
||||
transition: opacity 150ms ease, transform 150ms ease;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Component usage — animates in on mount with no JS
|
||||
<div className="fade-in-up">
|
||||
<StepCard step={currentStep} />
|
||||
</div>
|
||||
|
||||
<div className="scale-in">
|
||||
<FlowPilotPanel />
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 3 — Container Queries (`@container`)
|
||||
|
||||
Use for components that need to adapt based on where they are rendered — not the viewport. Critical for ResolutionFlow where the same component may appear in a sidebar, modal, or full-width panel.
|
||||
|
||||
**Where to apply:** StepCard (sidebar vs full-width) · FlowPilot panel (collapsed vs expanded) · documentation preview panel · step library items
|
||||
|
||||
```tsx
|
||||
// Wrap the parent with @container
|
||||
<div className="@container w-full">
|
||||
<StepCard step={step} />
|
||||
</div>
|
||||
|
||||
// Inside StepCard — responds to its container, not the viewport
|
||||
function StepCard({ step }: { step: Step }) {
|
||||
return (
|
||||
<div className="
|
||||
flex flex-col gap-2
|
||||
@sm:flex-row @sm:items-center
|
||||
@lg:gap-4
|
||||
p-4 rounded-lg border border-border bg-card
|
||||
">
|
||||
<StepIcon type={step.type} />
|
||||
<StepContent step={step} />
|
||||
<div className="@sm:ml-auto">
|
||||
<StepActions step={step} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 4 — Enhanced Gradient APIs
|
||||
|
||||
The cyan brand gradient is ResolutionFlow's primary visual identity. v4 unlocks radial gradients, angle control, and OKLCH interpolation for richer rendering.
|
||||
|
||||
**Where to apply:** Hero/header sections · card hover states · active step indicators · FlowPilot branding elements · progress indicators
|
||||
|
||||
```tsx
|
||||
// Linear — explicit angle control (v4)
|
||||
<div className="bg-linear-135 from-[#06b6d4] to-[#22d3ee]">
|
||||
|
||||
// Radial — great for glow effects on active/highlighted elements (v4)
|
||||
<div className="bg-radial from-[#06b6d4]/20 to-transparent">
|
||||
|
||||
// Radial with position — spotlight effect (v4)
|
||||
<div className="bg-radial-[at_30%_50%] from-[#06b6d4]/15 via-[#22d3ee]/8 to-transparent">
|
||||
|
||||
// OKLCH interpolation — richer, more accurate gradient transition (v4)
|
||||
<div className="bg-linear-to-r from-[#06b6d4] to-[#22d3ee] [color-interpolation-method:oklch]">
|
||||
|
||||
// Combine with glass card for the premium demo look
|
||||
<div className="glass-card-static bg-radial-[at_top_left] from-[#06b6d4]/10 to-transparent">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 5 — Dynamic Utility Values
|
||||
|
||||
In v3, non-standard values required bracket notation. In v4 the scale is continuous — use values directly.
|
||||
|
||||
```tsx
|
||||
// v3 — arbitrary brackets for anything off-scale
|
||||
<div className="w-[18px] h-[18px] z-[60] grid-cols-[repeat(7,1fr)]">
|
||||
|
||||
// v4 — direct values work without brackets
|
||||
<div className="w-4.5 h-4.5 z-60 grid-cols-7">
|
||||
|
||||
// Grid columns support any number directly
|
||||
<div className="grid grid-cols-7"> // 7-column grid, no config needed
|
||||
<div className="grid grid-cols-15"> // 15-column, works out of the box
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 6 — `not-*` Variant
|
||||
|
||||
Style elements only when they don't match a condition. Useful in the session runner step list.
|
||||
|
||||
```tsx
|
||||
// Bottom border on all steps except the last
|
||||
<div className="not-last:border-b border-border pb-4 mb-4">
|
||||
<StepCard />
|
||||
</div>
|
||||
|
||||
// Dim inactive steps
|
||||
<div className="not-[.active]:opacity-60 not-[.active]:hover:opacity-80 transition-opacity">
|
||||
<StepCard />
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 7 — Theme Tokens as Native CSS Variables
|
||||
|
||||
After the v4 migration, all `@theme` tokens are exposed as native CSS custom properties. This means you can access ResolutionFlow design tokens in TypeScript — useful for React Flow node styling and Recharts chart colors.
|
||||
|
||||
```ts
|
||||
// Access brand colors in JS/TS — no more hardcoded hex values
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
const brandColor = style.getPropertyValue('--color-brand-from').trim() // '#06b6d4'
|
||||
|
||||
// Use directly in React Flow node styles
|
||||
const nodeStyles = {
|
||||
background: 'var(--color-dark-card)',
|
||||
border: '1px solid var(--color-brand-from)',
|
||||
boxShadow: '0 0 12px rgba(6, 182, 212, 0.15)',
|
||||
}
|
||||
|
||||
// Use in Recharts chartConfig — no more hsl() wrapper needed
|
||||
const chartConfig = {
|
||||
sessions: {
|
||||
label: 'Sessions',
|
||||
color: 'var(--color-brand-from)',
|
||||
},
|
||||
resolved: {
|
||||
label: 'Resolved',
|
||||
color: 'var(--color-brand-to)',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 8 — Color Scheme Utilities
|
||||
|
||||
Control native browser UI element theming (scrollbars, form controls) to match the dark theme — previously required custom CSS.
|
||||
|
||||
```tsx
|
||||
// Apply to the root html element in index.html or App.tsx
|
||||
<html className="scheme-dark">
|
||||
|
||||
// This makes native browser scrollbars, select dropdowns,
|
||||
// date inputs, and form controls render in dark mode automatically.
|
||||
// ResolutionFlow is dark-only so this should be applied globally.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature 9 — `inert` Utility
|
||||
|
||||
Disable interaction on an entire subtree without JavaScript. Useful for locking the flow editor or step list during FlowPilot AI generation.
|
||||
|
||||
```tsx
|
||||
// Disable the step list while FlowPilot is generating
|
||||
<div className={isGenerating ? 'inert' : ''}>
|
||||
<StepList steps={steps} />
|
||||
</div>
|
||||
|
||||
// The inert attribute disables all pointer events, focus, and
|
||||
// accessibility interaction on the entire subtree at once.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## General Rules for v4 Going Forward
|
||||
|
||||
1. **Use `field-sizing-content` on every `<textarea>`** — no exceptions
|
||||
2. **Use `@starting-style` animations** instead of adding/removing CSS classes for enter effects
|
||||
3. **Wrap repeated components in `@container`** when they appear in variable-width contexts
|
||||
4. **Use `not-last:` instead of custom `:not(:last-child)` selectors** in lists
|
||||
5. **Reference `var(--color-brand-from)` and `var(--color-brand-to)`** in JS instead of hardcoded `#06b6d4` or `rgba(6,182,212,...)`
|
||||
6. **Use OKLCH for any new color values** — `oklch(72% 0.15 195)` not `#06b6d4`
|
||||
7. **All new keyframe animations go inside `@theme`** — not loose in `index.css`
|
||||
8. **Do not use `tailwindcss-animate`** — replaced by `tw-animate-css`
|
||||
9. **Do not add `@tailwind base/components/utilities` directives** — replaced by `@import "tailwindcss"`
|
||||
10. **Do not add `darkMode: ["class"]` to any config** — replaced by `@custom-variant dark`
|
||||
11. **Prefer `bg-linear-135` syntax** over `bg-gradient-to-r` for the brand gradient — more explicit and v4-native
|
||||
|
||||
---
|
||||
|
||||
*ResolutionFlow · Tailwind v4 Migration Doc · Pre-Investor Pitch Sprint*
|
||||
1433
frontend/package-lock.json
generated
1433
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@
|
||||
"@sentry/react": "^10.42.0",
|
||||
"@sentry/vite-plugin": "^5.1.1",
|
||||
"@stripe/stripe-js": "^8.7.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"axios": "^1.13.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -50,14 +51,13 @@
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Outlet } from 'react-router-dom'
|
||||
|
||||
export function AccountLayout() {
|
||||
return (
|
||||
<div className="container mx-auto max-w-screen-lg px-4 py-6">
|
||||
<div className="container mx-auto max-w-(--breakpoint-lg) px-4 py-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -56,7 +56,7 @@ export function DeleteAccountModal({ onClose }: Props) {
|
||||
required
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none'
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@ export function DeleteAccountModal({ onClose }: Props) {
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-[10px] 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'
|
||||
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -45,7 +45,7 @@ export function LeaveAccountModal({ accountName, onClose }: Props) {
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-[10px] 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'
|
||||
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -60,7 +60,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
onChange={(e) => setTargetUserId(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none'
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
>
|
||||
{nonOwnerMembers.map((m) => (
|
||||
@@ -77,7 +77,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
required
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none'
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-[10px] 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'
|
||||
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
@@ -100,7 +100,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
|
||||
disabled={isSubmitting || !password}
|
||||
className={cn(
|
||||
'rounded-[10px] px-4 py-2 text-sm font-semibold',
|
||||
'bg-amber-500 text-[#101114] hover:bg-amber-400',
|
||||
'bg-amber-500 text-brand-dark hover:bg-amber-400',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function AdminLayout() {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Desktop sidebar */}
|
||||
<div className="hidden w-60 flex-shrink-0 border-r border-border bg-card md:block">
|
||||
<div className="hidden w-60 shrink-0 border-r border-border bg-card md:block">
|
||||
<AdminSidebar />
|
||||
</div>
|
||||
|
||||
@@ -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-sm"
|
||||
className="absolute inset-0 bg-card/80 backdrop-blur-xs"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 w-60 border-r border-border bg-card shadow-xl">
|
||||
@@ -63,7 +63,7 @@ export function AdminLayout() {
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-screen-2xl p-6">
|
||||
<div className="mx-auto max-w-(--breakpoint-2xl) p-6">
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMobileOpen(true)}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function CreateCategoryModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs">
|
||||
<div className="w-full max-w-md bg-card border border-border rounded-xl p-6 shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -98,7 +98,7 @@ export function CreateCategoryModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -122,7 +122,7 @@ export function CreateCategoryModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function EditCategoryModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs">
|
||||
<div className="w-full max-w-md bg-card border border-border rounded-xl p-6 shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -107,7 +107,7 @@ export function EditCategoryModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -131,7 +131,7 @@ export function EditCategoryModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function SearchInput({ value = '', onSearch, placeholder = 'Search...', c
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'h-9 w-full rounded-md border border-border bg-card pl-9 pr-8 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
{localValue && (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function FlowAnalyticsPanel({ treeId }: FlowAnalyticsPanelProps) {
|
||||
<select
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value as AnalyticsPeriod)}
|
||||
className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:outline-hidden focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{PERIOD_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
@@ -113,31 +113,31 @@ export function FlowAnalyticsPanel({ treeId }: FlowAnalyticsPanelProps) {
|
||||
<AreaChart data={time_series}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="hsl(var(--border))"
|
||||
stroke="var(--color-border)"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 11 }}
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 11 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: 'hsl(var(--border))' }}
|
||||
axisLine={{ stroke: 'var(--color-border)' }}
|
||||
tickFormatter={(value) => {
|
||||
const d = new Date(String(value))
|
||||
return `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 11 }}
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 11 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
backgroundColor: 'var(--color-card)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '8px',
|
||||
color: 'hsl(var(--foreground))',
|
||||
color: 'var(--color-foreground)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
labelFormatter={(value) => {
|
||||
|
||||
@@ -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-[rgba(255,255,255,0.04)] text-foreground border border-[rgba(255,255,255,0.06)]'
|
||||
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-brand-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-[0.1em] text-muted-foreground">
|
||||
<span className="font-label 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-[#101114] 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-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"
|
||||
>
|
||||
<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-[0.1em] text-muted-foreground">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
Pinned
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +136,7 @@ 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-sm"
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-xs"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
@@ -169,7 +169,7 @@ export function ConcludeSessionModal({
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg hover:bg-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -188,7 +188,7 @@ export function ConcludeSessionModal({
|
||||
'w-8 h-px',
|
||||
step === s || (i === 1 && step === 'summary') || (i === 2 && step === 'summary')
|
||||
? 'bg-primary/40'
|
||||
: 'bg-[rgba(255,255,255,0.06)]'
|
||||
: 'bg-brand-border'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -196,10 +196,10 @@ export function ConcludeSessionModal({
|
||||
className={cn(
|
||||
'w-6 h-6 rounded-full flex items-center justify-center text-[0.6875rem] font-label font-medium transition-colors',
|
||||
step === s
|
||||
? 'bg-gradient-brand text-[#101114]'
|
||||
? 'bg-gradient-brand text-brand-dark'
|
||||
: (i < ['select-outcome', 'add-notes', 'summary'].indexOf(step))
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-[rgba(255,255,255,0.06)] text-muted-foreground'
|
||||
: 'bg-brand-border text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{i + 1}
|
||||
@@ -233,7 +233,7 @@ 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-[rgba(255,255,255,0.02)] border-[rgba(255,255,255,0.06)]',
|
||||
'bg-[rgba(255,255,255,0.02)] border-brand-border',
|
||||
'hover:border-[rgba(255,255,255,0.12)] hover:bg-[rgba(255,255,255,0.04)]'
|
||||
)}
|
||||
>
|
||||
@@ -268,7 +268,7 @@ export function ConcludeSessionModal({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground block mb-2">
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground block mb-2">
|
||||
Additional Notes (optional)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -282,7 +282,7 @@ export function ConcludeSessionModal({
|
||||
: 'What still needs to be done, where you left off...'
|
||||
}
|
||||
rows={4}
|
||||
className="w-full resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="w-full resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -312,7 +312,7 @@ export function ConcludeSessionModal({
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground flex items-center gap-1.5">
|
||||
<span className="font-label 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-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-[rgba(255,255,255,0.04)] border border-brand-border hover:border-[rgba(255,255,255,0.12)] 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-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-[rgba(255,255,255,0.04)] border border-brand-border hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
className="flex items-center gap-2 bg-gradient-brand text-[#101114] 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-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"
|
||||
>
|
||||
{generating ? (
|
||||
<>
|
||||
@@ -390,7 +390,7 @@ export function ConcludeSessionModal({
|
||||
'flex items-center gap-2 px-4 py-2.5 rounded-[10px] text-sm font-semibold transition-all',
|
||||
copied
|
||||
? 'bg-emerald-400/15 text-emerald-400 border border-emerald-400/30'
|
||||
: 'bg-gradient-brand text-[#101114] hover:opacity-90 active:scale-[0.97]'
|
||||
: 'bg-gradient-brand text-brand-dark hover:opacity-90 active:scale-[0.97]'
|
||||
)}
|
||||
>
|
||||
{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-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
className="px-4 py-2.5 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-[rgba(255,255,255,0.04)] border border-brand-border hover:border-[rgba(255,255,255,0.12)] transition-all"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
|
||||
@@ -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-[rgba(255,255,255,0.06)]'
|
||||
: 'text-foreground hover:bg-brand-border'
|
||||
)}
|
||||
>
|
||||
{item.icon && (
|
||||
|
||||
@@ -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-sm">
|
||||
<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">
|
||||
{/* 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-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-xs"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -139,13 +139,13 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a
|
||||
isFullScreen
|
||||
? 'fixed inset-4 max-w-none w-auto h-auto rounded-2xl'
|
||||
: cn(
|
||||
'max-h-[100vh] rounded-t-2xl sm:max-h-[85vh] sm:rounded-2xl',
|
||||
'max-h-screen rounded-t-2xl sm:max-h-[85vh] sm:rounded-2xl',
|
||||
sizeClasses[size]
|
||||
)
|
||||
)}
|
||||
>
|
||||
{/* Header - Fixed at top */}
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<h2 id="modal-title" className="text-lg font-semibold text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -168,7 +168,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a
|
||||
className={cn(
|
||||
'rounded-md p-1.5 text-muted-foreground transition-colors sm:p-1',
|
||||
'hover:bg-accent hover:text-foreground',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||
'focus:outline-hidden focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
@@ -184,7 +184,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a
|
||||
|
||||
{/* Footer - Fixed at bottom */}
|
||||
{footer && (
|
||||
<div className="flex-shrink-0 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<div className="shrink-0 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -186,7 +186,7 @@ export function TagInput({
|
||||
className={cn(
|
||||
'flex-1 min-w-[80px] border-0 bg-transparent px-1 py-0.5 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:ring-0'
|
||||
'focus:outline-hidden focus:ring-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg hover:bg-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-1.5 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
@@ -122,7 +122,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-[rgba(255,255,255,0.04)] text-foreground border border-[rgba(255,255,255,0.06)]'
|
||||
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-brand-border'
|
||||
}`}
|
||||
>
|
||||
<MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" />
|
||||
@@ -131,7 +131,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] rounded-xl px-3.5 py-2.5">
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-xl px-3.5 py-2.5">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,7 +140,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-[0.1em] text-muted-foreground">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
Related Flows
|
||||
</span>
|
||||
{suggestedFlows.map(flow => (
|
||||
@@ -162,14 +162,14 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about this step..."
|
||||
rows={1}
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
disabled={loading || initializing}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || loading || initializing}
|
||||
className="bg-gradient-brand text-[#101114] p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
className="bg-gradient-brand text-brand-dark p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] 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-[#101114] 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-gradient-brand text-brand-dark p-3.5 rounded-full shadow-lg shadow-primary/30 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
title="Open AI Copilot"
|
||||
>
|
||||
<MessageCircle size={22} />
|
||||
|
||||
@@ -45,7 +45,7 @@ export function OpenSessions({ sessions }: OpenSessionsProps) {
|
||||
{session.stepNumber && session.totalSteps
|
||||
? `Step ${session.stepNumber} of ${session.totalSteps}`
|
||||
: 'In progress'}
|
||||
<span className="mx-1.5 text-[hsl(var(--text-dimmed))]">·</span>
|
||||
<span className="mx-1.5 text-[var(--text-dimmed)]">·</span>
|
||||
<span className="font-label text-[0.625rem]">{session.timeAgo}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function QuickStats({ stats }: QuickStatsProps) {
|
||||
className={cn('glass-card p-4 fade-in', i === 0 && 'active-glow')}
|
||||
style={{ animationDelay: `${50 + i * 30}ms` }}
|
||||
>
|
||||
<p className="font-label text-[0.625rem] font-medium uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<p className="font-label text-[0.625rem] font-medium uppercase tracking-widest text-muted-foreground">
|
||||
{stat.label}
|
||||
</p>
|
||||
<p
|
||||
@@ -35,7 +35,7 @@ export function QuickStats({ stats }: QuickStatsProps) {
|
||||
{stat.value}
|
||||
</p>
|
||||
{stat.meta && (
|
||||
<p className="mt-0.5 text-[0.6875rem] text-[hsl(var(--text-dimmed))]">{stat.meta}</p>
|
||||
<p className="mt-0.5 text-[0.6875rem] text-[var(--text-dimmed)]">{stat.meta}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function SessionsPanel({ sessions, delay = 200 }: SessionsPanelProps) {
|
||||
</span>
|
||||
|
||||
{/* Time */}
|
||||
<span className="text-right text-[0.6875rem] text-[hsl(var(--text-dimmed))]">
|
||||
<span className="text-right text-[0.6875rem] text-[var(--text-dimmed)]">
|
||||
{session.timeAgo}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function TreeListItem({
|
||||
return (
|
||||
<div
|
||||
onClick={() => navigate(getTreeNavigatePath(id, treeType))}
|
||||
className="group grid cursor-pointer items-center gap-3 rounded-lg border border-transparent bg-card px-4 py-3 transition-colors hover:border-border hover:bg-[hsl(var(--sidebar-hover))]"
|
||||
className="group grid cursor-pointer items-center gap-3 rounded-lg border border-transparent bg-card px-4 py-3 transition-colors hover:border-border hover:bg-[var(--sidebar-hover)]"
|
||||
style={{ gridTemplateColumns: '40px 1fr 130px 80px 100px 40px' }}
|
||||
>
|
||||
{/* Icon box */}
|
||||
@@ -75,7 +75,7 @@ export function TreeListItem({
|
||||
</div>
|
||||
|
||||
{/* Updated */}
|
||||
<div className="text-right text-[0.6875rem] text-[hsl(var(--text-dimmed))]">
|
||||
<div className="text-right text-[0.6875rem] text-[var(--text-dimmed)]">
|
||||
{timeAgo}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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-[0.1em] ${day.isToday ? 'text-cyan-400' : 'text-muted-foreground'}`}>
|
||||
<span className={`font-label 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'}`}>
|
||||
@@ -65,7 +65,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-1.5 space-y-1">
|
||||
{dayEvents.length === 0 ? (
|
||||
<p className="text-[0.625rem] text-[hsl(var(--text-dimmed))] text-center py-2">No events</p>
|
||||
<p className="text-[0.625rem] text-[var(--text-dimmed)] text-center py-2">No events</p>
|
||||
) : (
|
||||
dayEvents.map(event => (
|
||||
<div
|
||||
|
||||
@@ -45,7 +45,7 @@ export function AIPromptDialog({
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-xs"
|
||||
onClick={() => !isGenerating && onClose()}
|
||||
/>
|
||||
|
||||
@@ -68,7 +68,7 @@ export function AIPromptDialog({
|
||||
placeholder={`Example: "A flow for troubleshooting VPN connectivity issues when users can't connect to the corporate network"`}
|
||||
rows={4}
|
||||
disabled={isGenerating}
|
||||
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none disabled:opacity-50"
|
||||
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden resize-none disabled:opacity-50"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -83,14 +83,14 @@ export function AIPromptDialog({
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isGenerating}
|
||||
className="rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-4 py-2 text-sm text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors disabled:opacity-50"
|
||||
className="rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-4 py-2 text-sm text-foreground hover:border-[rgba(255,255,255,0.12)] 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-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-50"
|
||||
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"
|
||||
>
|
||||
{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-[rgba(255,255,255,0.04)] text-foreground border border-[rgba(255,255,255,0.06)]'
|
||||
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-brand-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-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] rounded-xl px-3.5 py-2.5">
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-xl px-3.5 py-2.5">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,14 +77,14 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask AI to help..."
|
||||
rows={1}
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={!input.trim() || isLoading}
|
||||
className="bg-gradient-brand text-[#101114] p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
className="bg-gradient-brand text-brand-dark p-2.5 rounded-xl hover:opacity-90 active:scale-[0.97] 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-[rgba(255,255,255,0.06)] text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-1.5 rounded-lg hover:bg-brand-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-[0.1em] text-muted-foreground">
|
||||
<div className="mt-1 flex items-center gap-3 font-label 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-[0.1em] text-muted-foreground">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
{node.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function SuggestionsTab({ suggestions }: SuggestionsTabProps) {
|
||||
return (
|
||||
<div key={s.id} className="rounded-lg border border-border bg-[rgba(255,255,255,0.02)] px-3 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
<span className="font-label 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-[#5a6170]">
|
||||
<p className="mt-0.5 font-label text-[0.625rem] text-brand-text-muted">
|
||||
{new Date(s.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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-[0.1em] text-primary">
|
||||
<span className="mt-2 inline-block font-label text-[0.625rem] uppercase tracking-widest text-primary">
|
||||
{guide.sections.length} {guide.sections.length === 1 ? 'section' : 'sections'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ export function AppLayout() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={cn('app-shell relative z-[1]', sidebarCollapsed && 'app-shell--collapsed')}>
|
||||
<div className={cn('app-shell relative z-1', sidebarCollapsed && 'app-shell--collapsed')}>
|
||||
{/* Top Bar - spans full width */}
|
||||
<TopBar />
|
||||
|
||||
@@ -109,11 +109,11 @@ export function AppLayout() {
|
||||
{mobileMenuOpen && (
|
||||
<div className="fixed inset-0 z-50 md:hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm animate-fade-in"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-xs animate-fade-in"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-[hsl(var(--sidebar-bg))] shadow-2xl animate-slide-in-left">
|
||||
<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">
|
||||
<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">
|
||||
@@ -156,8 +156,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-[hsl(var(--sidebar-active))] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
||||
? 'bg-[var(--sidebar-active)] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon size={18} />
|
||||
@@ -171,7 +171,7 @@ export function AppLayout() {
|
||||
<div className="mt-3 border-t border-border pt-3">
|
||||
<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-[hsl(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-[var(--sidebar-hover)] hover:text-foreground transition-colors"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
Logout
|
||||
|
||||
@@ -124,10 +124,10 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[20vh]">
|
||||
<div className="fixed inset-0 z-100 flex items-start justify-center pt-[20vh]">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-fade-in"
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-xs animate-fade-in"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
|
||||
onChange={e => { setQuery(e.target.value); setSelectedIndex(0) }}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search flows, sessions…"
|
||||
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none"
|
||||
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">
|
||||
ESC
|
||||
|
||||
@@ -33,7 +33,7 @@ export function EmailVerificationBanner() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b border-amber-400/20 bg-amber-400/5 px-4 py-2 text-sm">
|
||||
<AlertTriangle className="h-4 w-4 flex-shrink-0 text-amber-400" />
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 text-amber-400" />
|
||||
<span className="text-amber-200">
|
||||
Your email is not verified.
|
||||
</span>
|
||||
|
||||
@@ -41,8 +41,8 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
|
||||
className={cn(
|
||||
'group relative flex items-center justify-center rounded-lg p-2 transition-all duration-120',
|
||||
isActive
|
||||
? 'bg-[hsl(var(--sidebar-active))] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
||||
? 'bg-[var(--sidebar-active)] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
title={label}
|
||||
>
|
||||
@@ -68,9 +68,9 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
|
||||
'group relative flex items-center gap-3 rounded-lg px-3 py-2 text-[0.8125rem] font-medium transition-all duration-120',
|
||||
isActive
|
||||
? isParentDimmed
|
||||
? 'bg-[hsl(var(--sidebar-active))]/50 text-foreground/70'
|
||||
: 'bg-[hsl(var(--sidebar-active))] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
||||
? 'bg-[var(--sidebar-active)]/50 text-foreground/70'
|
||||
: 'bg-[var(--sidebar-active)] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
@@ -110,8 +110,8 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
|
||||
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-[hsl(var(--sidebar-active))] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
||||
? 'bg-[var(--sidebar-active)] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{child.label}</span>
|
||||
|
||||
@@ -70,8 +70,8 @@ export function QuickLaunch({ open, onClose }: QuickLaunchProps) {
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose} />
|
||||
<div className="fixed inset-0 z-100 flex items-start justify-center pt-[15vh]">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-xs animate-fade-in" onClick={onClose} />
|
||||
<div ref={containerRef} className="relative w-full max-w-md rounded-xl border border-border bg-card shadow-2xl animate-scale-in">
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
<h3 className="text-sm font-heading font-semibold text-foreground">Quick Launch</h3>
|
||||
|
||||
@@ -141,7 +141,7 @@ export function Sidebar() {
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-lg text-[0.8125rem] font-medium text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground transition-colors",
|
||||
"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'}
|
||||
|
||||
@@ -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-sm py-1 shadow-lg'
|
||||
'bg-card backdrop-blur-xs py-1 shadow-lg'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -174,7 +174,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-sm" onClick={onClose} />
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-xs" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative z-10 w-full max-w-md bg-card border border-border rounded-2xl p-6 shadow-lg">
|
||||
@@ -202,7 +202,7 @@ export function FolderEditModal({
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'border-border'
|
||||
)}
|
||||
autoFocus
|
||||
@@ -221,7 +221,7 @@ export function FolderEditModal({
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'border-border'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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-sm py-1 shadow-lg'
|
||||
'bg-card backdrop-blur-xs 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-sm md:hidden"
|
||||
className="fixed inset-0 z-40 bg-black/80 backdrop-blur-xs 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-sm py-1 shadow-lg'
|
||||
'bg-card backdrop-blur-xs py-1 shadow-lg'
|
||||
)}
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -87,7 +87,7 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
||||
maxLength={255}
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -105,7 +105,7 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
||||
placeholder="e.g. customizing for a specific client…"
|
||||
className={cn(
|
||||
'w-full resize-none rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function ImportFlowModal({ onClose }: ImportFlowModalProps) {
|
||||
/>
|
||||
{parseError && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-rose-500/20 bg-rose-500/5 px-3 py-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-rose-400" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-rose-400" />
|
||||
<p className="text-xs text-rose-400">{parseError}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -165,7 +165,7 @@ export function ImportFlowModal({ onClose }: ImportFlowModalProps) {
|
||||
maxLength={255}
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-xs"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
@@ -221,7 +221,7 @@ export function ShareTreeModal({ tree, isOpen, onClose }: ShareTreeModalProps) {
|
||||
type="text"
|
||||
value={activeShare.share_url}
|
||||
readOnly
|
||||
className="flex-1 bg-transparent text-sm text-foreground outline-none"
|
||||
className="flex-1 bg-transparent text-sm text-foreground outline-hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function SortDropdown({ value, onChange, className }: SortDropdownProps)
|
||||
onChange={(e) => onChange(e.target.value as SortBy)}
|
||||
className={cn(
|
||||
'rounded-md border border-border bg-card px-3 py-1.5 text-sm',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
|
||||
@@ -37,20 +37,20 @@ export function TreeListView({
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="flex items-center gap-4 bg-card border border-border rounded-2xl p-4 transition-all hover:border-primary/30 hover:shadow-sm"
|
||||
className="flex items-center gap-4 bg-card border border-border rounded-2xl p-4 transition-all hover:border-primary/30 hover:shadow-xs"
|
||||
>
|
||||
{/* Left: Name and Description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-foreground truncate">{tree.name}</h3>
|
||||
{tree.status === 'draft' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400 flex-shrink-0">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400 shrink-0">
|
||||
<FileText className="h-3 w-3" />
|
||||
Draft
|
||||
</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 flex-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-label text-[0.625rem] uppercase tracking-wide text-amber-400 shrink-0">
|
||||
<Wrench className="h-3 w-3" />
|
||||
Maintenance
|
||||
</span>
|
||||
@@ -62,11 +62,11 @@ export function TreeListView({
|
||||
)}
|
||||
{tree.is_public ? (
|
||||
<span title="Public tree">
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Private tree">
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ export function TreeListView({
|
||||
</div>
|
||||
|
||||
{/* Right: Metadata and Actions */}
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="hidden sm:flex flex-col items-end text-xs text-muted-foreground">
|
||||
<span>v{tree.version}</span>
|
||||
<span>{tree.usage_count} uses</span>
|
||||
|
||||
@@ -173,13 +173,13 @@ export function TreeTableView({
|
||||
{tree.name}
|
||||
</span>
|
||||
{tree.status === 'draft' && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400 flex-shrink-0">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400 shrink-0">
|
||||
<FileText className="h-3 w-3" />
|
||||
Draft
|
||||
</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 flex-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-label text-[0.625rem] uppercase tracking-wide text-amber-400 shrink-0">
|
||||
<Wrench className="h-3 w-3" />
|
||||
Maintenance
|
||||
</span>
|
||||
@@ -191,11 +191,11 @@ export function TreeTableView({
|
||||
)}
|
||||
{tree.is_public ? (
|
||||
<span title="Public tree">
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
</span>
|
||||
) : (
|
||||
<span title="Private tree">
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||
<Lock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function BatchLaunchModal({ treeId, treeName, onClose, onLaunched }: Batc
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs">
|
||||
<div className="w-full max-w-lg rounded-xl border border-border bg-card shadow-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
@@ -116,7 +116,7 @@ export function BatchLaunchModal({ treeId, treeName, onClose, onLaunched }: Batc
|
||||
Server names (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
className="h-40 w-full rounded-lg border border-border bg-card px-3 py-2 text-[0.875rem] text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="h-40 w-full rounded-lg border border-border bg-card px-3 py-2 text-[0.875rem] text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
placeholder={"RDS-01\nRDS-02\nRDS-03"}
|
||||
value={manualInput}
|
||||
onChange={e => setManualInput(e.target.value)}
|
||||
|
||||
@@ -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-sm shadow-primary/20 hover:opacity-90 transition-opacity"
|
||||
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"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
Start
|
||||
|
||||
@@ -36,13 +36,13 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
value={field.label}
|
||||
onChange={(e) => onUpdate({ label: e.target.value })}
|
||||
placeholder="Field label"
|
||||
className="min-w-0 flex-1 rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="min-w-0 flex-1 rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
|
||||
<select
|
||||
value={field.field_type}
|
||||
onChange={(e) => onUpdate({ field_type: e.target.value as IntakeFieldType })}
|
||||
className="rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
{FIELD_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
@@ -84,7 +84,7 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
value={field.variable_name}
|
||||
onChange={(e) => onUpdate({ variable_name: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '') })}
|
||||
placeholder="e.g. server_name"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">Used as [VAR:{field.variable_name}]</p>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
value={field.placeholder || ''}
|
||||
onChange={(e) => onUpdate({ placeholder: e.target.value || undefined })}
|
||||
placeholder="Hint text"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,7 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
value={field.help_text || ''}
|
||||
onChange={(e) => onUpdate({ help_text: e.target.value || undefined })}
|
||||
placeholder="Description or instructions"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
value={field.default_value || ''}
|
||||
onChange={(e) => onUpdate({ default_value: e.target.value || undefined })}
|
||||
placeholder="Pre-filled value"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,7 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
value={field.group_name || ''}
|
||||
onChange={(e) => onUpdate({ group_name: e.target.value || undefined })}
|
||||
placeholder="e.g. Network Settings"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +144,7 @@ export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEdit
|
||||
}}
|
||||
placeholder="Option 1 Option 2 Option 3"
|
||||
rows={3}
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function IntakeFormBuilder() {
|
||||
</div>
|
||||
|
||||
{intakeForm.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-white/[0.02] py-8 text-center">
|
||||
<div className="rounded-lg border border-dashed border-border bg-white/2 py-8 text-center">
|
||||
<FileText className="mx-auto mb-2 h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No intake form fields yet</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
|
||||
@@ -154,7 +154,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
|
||||
<select
|
||||
value={frequency}
|
||||
onChange={(e) => setFrequency(e.target.value)}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
{FREQUENCY_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
@@ -172,7 +172,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
|
||||
max={23}
|
||||
value={hour}
|
||||
onChange={(e) => setHour(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -183,7 +183,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
|
||||
max={59}
|
||||
value={minute}
|
||||
onChange={(e) => setMinute(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,7 +194,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
|
||||
<select
|
||||
value={timezone}
|
||||
onChange={(e) => setTimezone(e.target.value)}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
{TIMEZONE_OPTIONS.map(tz => (
|
||||
<option key={tz} value={tz}>{tz}</option>
|
||||
@@ -209,7 +209,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
|
||||
<select
|
||||
value={selectedTargetListId}
|
||||
onChange={(e) => setSelectedTargetListId(e.target.value)}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">None — manual targets only</option>
|
||||
{targetLists.map(tl => (
|
||||
|
||||
@@ -41,7 +41,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
value={step.title}
|
||||
onChange={(e) => onUpdate({ title: e.target.value })}
|
||||
placeholder="Section title"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
type="text"
|
||||
value={step.title}
|
||||
onChange={(e) => onUpdate({ title: e.target.value })}
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
onChange={(e) => onUpdate({ estimated_minutes: e.target.value ? parseInt(e.target.value) : undefined })}
|
||||
placeholder="—"
|
||||
min={1}
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
onChange={(e) => onUpdate({ description: e.target.value })}
|
||||
placeholder="Step instructions. Use [VAR:name] for variables."
|
||||
rows={4}
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
{availableVariables.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
@@ -131,7 +131,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
onChange={(e) => onUpdate({ commands: e.target.value || undefined })}
|
||||
placeholder="Install-WindowsFeature AD-Domain-Services -IncludeManagementTools"
|
||||
rows={3}
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -181,7 +181,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
onChange={(e) => onUpdate({ warning_text: e.target.value || undefined })}
|
||||
placeholder="Caution: This will restart the service..."
|
||||
rows={2}
|
||||
className="w-full rounded border border-yellow-400/20 bg-yellow-400/5 px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-yellow-400/30 focus:outline-none focus:ring-1 focus:ring-yellow-400/20"
|
||||
className="w-full rounded border border-yellow-400/20 bg-yellow-400/5 px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-yellow-400/30 focus:outline-hidden focus:ring-1 focus:ring-yellow-400/20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -194,7 +194,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
value={step.expected_outcome || ''}
|
||||
onChange={(e) => onUpdate({ expected_outcome: e.target.value || undefined })}
|
||||
placeholder="Server should respond with..."
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -210,7 +210,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
value={step.verification_prompt || ''}
|
||||
onChange={(e) => onUpdate({ verification_prompt: e.target.value || undefined })}
|
||||
placeholder="Confirm the role was installed"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -218,7 +218,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
<select
|
||||
value={step.verification_type || ''}
|
||||
onChange={(e) => onUpdate({ verification_type: e.target.value as 'checkbox' | 'text_input' || undefined })}
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">None</option>
|
||||
<option value="checkbox">Checkbox (confirm done)</option>
|
||||
@@ -239,7 +239,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
value={step.reference_url || ''}
|
||||
onChange={(e) => onUpdate({ reference_url: e.target.value || undefined })}
|
||||
placeholder="https://learn.microsoft.com/..."
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end pb-1">
|
||||
@@ -266,7 +266,7 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa
|
||||
onChange={(e) => onUpdate({
|
||||
library_visibility: e.target.value === '' ? undefined : e.target.value as 'team' | 'public'
|
||||
})}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">Inherit from flow</option>
|
||||
<option value="team">Team only</option>
|
||||
|
||||
@@ -145,7 +145,7 @@ export function StepList({ onStepContextMenu }: StepListProps) {
|
||||
type="text"
|
||||
value={step.title}
|
||||
onChange={(e) => updateStep(step.id, { title: e.target.value })}
|
||||
className="flex-1 bg-transparent text-sm text-muted-foreground focus:outline-none"
|
||||
className="flex-1 bg-transparent text-sm text-muted-foreground focus:outline-hidden"
|
||||
placeholder="Procedure Complete"
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">END</span>
|
||||
@@ -239,7 +239,7 @@ export function StepList({ onStepContextMenu }: StepListProps) {
|
||||
className={cn(
|
||||
'group flex flex-col rounded-xl border border-border px-3 py-2.5 transition-colors',
|
||||
'hover:border-primary/30 hover:bg-accent/50',
|
||||
isGhost && 'border-l-2 border-dashed !border-l-primary/40 opacity-60'
|
||||
isGhost && 'border-l-2 border-dashed border-l-primary/40! opacity-60'
|
||||
)}
|
||||
onContextMenu={(e) => onStepContextMenu?.(e, step.id)}
|
||||
>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function IntakeFormModal({ isOpen, fields, treeName, onSubmit, onCancel }
|
||||
const error = errors[field.variable_name]
|
||||
|
||||
const baseInputClass = cn(
|
||||
'w-full rounded-lg border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1',
|
||||
'w-full rounded-lg border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-hidden focus:ring-1',
|
||||
error
|
||||
? 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20'
|
||||
: 'border-border focus:border-primary focus:ring-primary/20'
|
||||
@@ -211,7 +211,7 @@ export function IntakeFormModal({ isOpen, fields, treeName, onSubmit, onCancel }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs">
|
||||
<div className="mx-4 w-full max-w-lg rounded-2xl border border-border bg-[#0a0a0a] shadow-xl">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border px-6 py-4">
|
||||
|
||||
@@ -154,7 +154,7 @@ export function StepDetail({
|
||||
|
||||
{/* Expected outcome */}
|
||||
{'expected_outcome' in step && step.expected_outcome && (
|
||||
<div className="rounded-lg border border-border bg-white/[0.02] p-3">
|
||||
<div className="rounded-lg border border-border bg-white/2 p-3">
|
||||
<h4 className="mb-1 text-xs font-medium text-muted-foreground">Expected Outcome</h4>
|
||||
<p className="text-sm text-muted-foreground">{resolve(step.expected_outcome)}</p>
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@ export function StepDetail({
|
||||
|
||||
{/* Verification */}
|
||||
{verificationPrompt && (
|
||||
<div className="rounded-lg border border-border bg-white/[0.02] p-3">
|
||||
<div className="rounded-lg border border-border bg-white/2 p-3">
|
||||
<h4 className="mb-2 text-xs font-medium text-muted-foreground">Verification</h4>
|
||||
{verificationType === 'checkbox' ? (
|
||||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
@@ -184,7 +184,7 @@ export function StepDetail({
|
||||
onChange={(e) => onVerificationChange(e.target.value)}
|
||||
disabled={isCompleted}
|
||||
placeholder="Enter observed value..."
|
||||
className="w-full rounded border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20 disabled:opacity-50"
|
||||
className="w-full rounded border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20 disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -200,7 +200,7 @@ export function StepDetail({
|
||||
onChange={(e) => onNotesChange(e.target.value)}
|
||||
placeholder="Add notes for this step..."
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function CSATModal({ isOpen, onClose, sessionId }: CSATModalProps) {
|
||||
placeholder="Any comments? (optional)"
|
||||
maxLength={500}
|
||||
rows={3}
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20 resize-none"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20 resize-none"
|
||||
/>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function ContinuationModal({
|
||||
'hover:border-border hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-accent">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent">
|
||||
{nodeTypeIcons[node.type]}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -71,7 +71,7 @@ export function ContinuationModal({
|
||||
{nodeTypeLabels[node.type]}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ export function ContinuationModal({
|
||||
'hover:border-amber-500 hover:bg-amber-500/10'
|
||||
)}
|
||||
>
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-amber-500/10">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/10">
|
||||
<GitBranch className="h-5 w-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -111,7 +111,7 @@ export function ContinuationModal({
|
||||
|
||||
{/* Warning */}
|
||||
<div className="mt-3 flex items-start gap-2 rounded-md bg-yellow-400/10 p-3">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-400" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-yellow-400" />
|
||||
<p className="text-sm text-yellow-400">
|
||||
You'll need to complete this branch manually or mark the issue as resolved.
|
||||
Custom branches can be saved as a personal tree when your session ends.
|
||||
|
||||
@@ -141,7 +141,7 @@ export function ExportPreviewModal({
|
||||
className={cn(
|
||||
'h-96 w-full resize-y rounded-md border border-border bg-card p-4',
|
||||
'font-mono text-sm text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -153,7 +153,7 @@ export function ExportPreviewModal({
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-foreground',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||
'focus:outline-hidden focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
@@ -173,7 +173,7 @@ export function ExportPreviewModal({
|
||||
onClick={handleDownload}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium',
|
||||
'hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||
'hover:opacity-90 focus:outline-hidden focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ForkTreeModal({
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Save Custom Tree?" footer={footer}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 rounded-lg bg-accent/50 p-4">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-accent">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent">
|
||||
<GitFork className="h-5 w-5 text-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -107,7 +107,7 @@ export function ForkTreeModal({
|
||||
placeholder="My Custom Tree"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20'
|
||||
'focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@ export function ForkTreeModal({
|
||||
rows={3}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20',
|
||||
'focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
|
||||
'resize-none'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function SaveSessionAsTreeModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs">
|
||||
<div className="bg-card border border-border w-full max-w-lg rounded-2xl p-6 shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
@@ -70,7 +70,7 @@ export function SaveSessionAsTreeModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
@@ -91,7 +91,7 @@ export function SaveSessionAsTreeModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -141,7 +141,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
{/* Mobile backdrop */}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/80 backdrop-blur-sm sm:hidden"
|
||||
className="fixed inset-0 z-30 bg-black/80 backdrop-blur-xs sm:hidden"
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -203,7 +203,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
||||
className={cn(
|
||||
'h-full min-h-[200px] w-full resize-none rounded-md border-0 bg-transparent p-0 text-sm',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:ring-0'
|
||||
'focus:outline-hidden focus:ring-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -102,7 +102,7 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card py-2 pl-9 pr-3',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card py-2 pl-9 pr-3',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
|
||||
onChange={(e) => handleFilterChange('treeName', e.target.value)}
|
||||
className={cn(
|
||||
'rounded-md border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'sm:min-w-[200px]'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -111,7 +111,7 @@ export function SessionOutcomeModal({
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border bg-card px-3 py-2',
|
||||
'text-sm text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@ export function SessionOutcomeModal({
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border bg-card px-3 py-2',
|
||||
'text-sm text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ export function SessionTimeline({
|
||||
{decisions.map((decision, index) => (
|
||||
<div key={index} className="ml-1 border-l-2 border-border pl-6">
|
||||
<div className="relative">
|
||||
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-muted-foreground" />
|
||||
<span className="absolute -left-6.5 top-1 h-2 w-2 rounded-full bg-muted-foreground" />
|
||||
<div className="bg-card border border-border rounded-xl p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -181,7 +181,7 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-xs"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
@@ -265,7 +265,7 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
|
||||
placeholder="e.g. Training link, Customer escalation"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
maxLength={100}
|
||||
/>
|
||||
@@ -299,8 +299,8 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
|
||||
onChange={(e) => setCustomDatetime(e.target.value)}
|
||||
className={cn(
|
||||
'mt-2 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'[color-scheme:dark]'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'scheme-dark'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function SharedSessionTreePreview({
|
||||
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-2xl">
|
||||
<div className="sticky top-0 z-10 rounded-t-2xl border-b border-border bg-black/80 px-6 py-4 backdrop-blur">
|
||||
<div className="sticky top-0 z-10 rounded-t-2xl border-b border-border bg-black/80 px-6 py-4 backdrop-blur-sm">
|
||||
<h3 className="text-sm font-semibold text-foreground">Tree Structure</h3>
|
||||
</div>
|
||||
<div className="max-h-[600px] overflow-y-auto py-2">
|
||||
|
||||
@@ -77,7 +77,7 @@ export function StepRatingModal({
|
||||
const getRating = (stepId: string) => ratings.get(stepId)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs p-4">
|
||||
<div className="bg-card border border-border w-full max-w-2xl max-h-[90vh] flex flex-col rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
@@ -174,7 +174,7 @@ export function StepRatingModal({
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function VariablePromptModal({ prompt, onSubmit, onCancel }: VariableProm
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs">
|
||||
<div className="bg-card border border-border w-full max-w-md rounded-2xl p-6 shadow-lg">
|
||||
<h2 className="mb-1 text-lg font-semibold text-foreground">Input Required</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
@@ -40,7 +40,7 @@ export function VariablePromptModal({ prompt, onSubmit, onCancel }: VariableProm
|
||||
autoFocus
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ export function CategoryList({ categories, activeId, onSelect }: CategoryListPro
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded-lg px-3 py-1.5 text-sm transition-colors',
|
||||
activeId === cat.id
|
||||
? 'bg-[hsl(var(--sidebar-active))] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
||||
? 'bg-[var(--sidebar-active)] text-foreground'
|
||||
: 'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
@@ -47,13 +47,13 @@ export function CategoryList({ categories, activeId, onSelect }: CategoryListPro
|
||||
style={{ backgroundColor: cat.color }}
|
||||
/>
|
||||
<span className="flex-1 truncate text-left">{cat.name}</span>
|
||||
<span className="font-label text-[0.6875rem] text-[hsl(var(--text-dimmed))]">{cat.count}</span>
|
||||
<span className="font-label text-[0.6875rem] text-[var(--text-dimmed)]">{cat.count}</span>
|
||||
</button>
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={() => setExpanded(v => !v)}
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-1.5 text-[0.8125rem] text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground transition-colors"
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-1.5 text-[0.8125rem] text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground transition-colors"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function PinnedFlowsSection({ flows, onUnpin }: PinnedFlowsSectionProps)
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="overflow-hidden transition-[max-height] duration-[250ms] ease-out"
|
||||
className="overflow-hidden transition-[max-height] duration-250 ease-out"
|
||||
style={{
|
||||
maxHeight: collapsed ? 0 : showAll
|
||||
? `${flows.length * 36 + 40}px`
|
||||
@@ -70,7 +70,7 @@ export function PinnedFlowsSection({ flows, onUnpin }: PinnedFlowsSectionProps)
|
||||
}}
|
||||
className={cn(
|
||||
'group flex w-full items-center gap-2.5 rounded-lg px-3 py-1.5 text-[0.8125rem] font-medium transition-colors',
|
||||
'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
||||
'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
title={`${flow.tree_name} (right-click to unpin)`}
|
||||
>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/80 backdrop-blur-sm sm:items-center sm:p-4">
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/80 backdrop-blur-xs sm:items-center sm:p-4">
|
||||
<div className="relative flex h-[95vh] w-full max-w-full flex-col border border-border bg-card shadow-lg sm:h-[90vh] sm:max-w-4xl sm:rounded-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border p-4">
|
||||
@@ -133,7 +133,7 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{isSubmitting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/80 backdrop-blur-xs">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Spinner className="border-t-foreground" />
|
||||
<p className="text-sm text-muted-foreground">Creating step...</p>
|
||||
|
||||
@@ -75,7 +75,7 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr
|
||||
const visibleReviews = showAllReviews ? allTextReviews : allTextReviews.slice(0, 3)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs">
|
||||
<div className="relative flex h-[90vh] w-full max-w-3xl flex-col bg-card border border-border rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between border-b border-border p-6 pb-4">
|
||||
|
||||
@@ -179,7 +179,7 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Enter step title"
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20',
|
||||
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
|
||||
errors.title ? 'border-red-400/50' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
@@ -201,7 +201,7 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
||||
placeholder="Describe what to do in this step..."
|
||||
rows={6}
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20',
|
||||
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
|
||||
errors.instructions ? 'border-red-400/50' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
@@ -221,7 +221,7 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
||||
onChange={(e) => setHelpText(e.target.value)}
|
||||
placeholder="Additional context or tips..."
|
||||
rows={3}
|
||||
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -296,7 +296,7 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
||||
id="category"
|
||||
value={categoryId}
|
||||
onChange={(e) => setCategoryId(e.target.value)}
|
||||
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{categories.map(cat => (
|
||||
@@ -318,7 +318,7 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleTagInputKeyDown}
|
||||
placeholder="Type tag and press Enter"
|
||||
className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -359,7 +359,7 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
||||
id="visibility"
|
||||
value={visibility}
|
||||
onChange={(e) => setVisibility(e.target.value as 'private' | 'team' | 'public')}
|
||||
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="private">Private (only me)</option>
|
||||
<option value="team">Team (my team members)</option>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function StepFormModal({ isOpen, onClose, onSuccess, editingStep }: StepF
|
||||
} : undefined
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs p-4">
|
||||
<div className="relative flex h-[90vh] w-full max-w-2xl flex-col bg-card border border-border rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border p-6 pb-4">
|
||||
|
||||
@@ -151,7 +151,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
placeholder="Search steps..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-md border border-border bg-card py-2 pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full rounded-md border border-border bg-card py-2 pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Filter by category"
|
||||
value={selectedCategoryId || ''}
|
||||
onChange={(e) => setSelectedCategoryId(e.target.value || undefined)}
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map(cat => (
|
||||
@@ -175,7 +175,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Filter by step type"
|
||||
value={selectedStepType || ''}
|
||||
onChange={(e) => setSelectedStepType((e.target.value as 'decision' | 'action' | 'solution') || undefined)}
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="decision">Decision</option>
|
||||
@@ -188,7 +188,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Filter by minimum rating"
|
||||
value={minRating?.toString() || ''}
|
||||
onChange={(e) => setMinRating(e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="">Any Rating</option>
|
||||
<option value="4">4+ Stars</option>
|
||||
@@ -201,7 +201,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
|
||||
aria-label="Sort steps by"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as 'recent' | 'popular' | 'highest_rated' | 'most_used')}
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
className="rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
|
||||
>
|
||||
<option value="recent">Most Recent</option>
|
||||
<option value="popular">Most Popular</option>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AIFixReviewModal({ fixes, onApply, onApplyAll, onClose }: AIFixR
|
||||
const allHandled = pendingFixes.length === 0
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs p-4">
|
||||
<div className="relative flex h-[80vh] w-full max-w-2xl flex-col bg-card border border-border rounded-2xl shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
@@ -127,7 +127,7 @@ export function AIFixReviewModal({ fixes, onApply, onApplyAll, onClose }: AIFixR
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={() => handleApply(fix)}
|
||||
className="flex items-center gap-1 rounded-md bg-gradient-brand px-3 py-1.5 text-xs font-medium text-white shadow-sm shadow-primary/20 hover:opacity-90"
|
||||
className="flex items-center gap-1 rounded-md bg-gradient-brand px-3 py-1.5 text-xs font-medium text-white shadow-xs shadow-primary/20 hover:opacity-90"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
Apply
|
||||
|
||||
@@ -139,14 +139,14 @@ function FlowCanvasInner({ selectedNodeId, onNodeSelect, onSelectAnswerType, onN
|
||||
proOptions={{ hideAttribution: true }}
|
||||
className="dark bg-accent/30"
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1.5} color="hsl(var(--muted-foreground) / 0.25)" />
|
||||
<Controls showInteractive={false} className="!bg-card !border-border !shadow-lg" />
|
||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1.5} color="oklch(0.63 0.02 260 / 0.25)" />
|
||||
<Controls showInteractive={false} className="bg-card! border-border! shadow-lg!" />
|
||||
{minimapVisible && (
|
||||
<MiniMap
|
||||
pannable
|
||||
zoomable
|
||||
nodeColor={minimapNodeColor}
|
||||
className="!bg-card !border-border"
|
||||
className="bg-card! border-border!"
|
||||
nodeStrokeWidth={2}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -16,7 +16,7 @@ function FlowCanvasAnswerNodeComponent({ data, selected }: NodeProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} className="!bg-border !w-2 !h-2 !border-0" />
|
||||
<Handle type="target" position={Position.Top} className="bg-border! w-2! h-2! border-0!" />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
@@ -61,7 +61,7 @@ function FlowCanvasAnswerNodeComponent({ data, selected }: NodeProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} className="!bg-border !w-2 !h-2 !border-0" />
|
||||
<Handle type="source" position={Position.Bottom} className="bg-border! w-2! h-2! border-0!" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,15 +62,15 @@ function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Target handle at top */}
|
||||
<Handle type="target" position={Position.Top} className="!bg-border !w-2 !h-2 !border-0" />
|
||||
<Handle type="target" position={Position.Top} className="bg-border! w-2! h-2! border-0!" />
|
||||
|
||||
<div
|
||||
onContextMenu={(e) => onContextMenu?.(e, node.id)}
|
||||
className={cn(
|
||||
'w-[280px] rounded-xl border border-border bg-card shadow-sm cursor-pointer transition-all',
|
||||
'w-[280px] rounded-xl border border-border bg-card shadow-xs cursor-pointer transition-all',
|
||||
config.borderClass,
|
||||
selected && 'ring-1 ring-primary shadow-md',
|
||||
isGhost && 'border-dashed !border-primary/40 opacity-60'
|
||||
isGhost && 'border-dashed border-primary/40! opacity-60'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
@@ -175,7 +175,7 @@ function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
|
||||
</div>
|
||||
|
||||
{/* Source handle at bottom */}
|
||||
<Handle type="source" position={Position.Bottom} className="!bg-border !w-2 !h-2 !border-0" />
|
||||
<Handle type="source" position={Position.Bottom} className="bg-border! w-2! h-2! border-0!" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function MetadataSidePanel({ isOpen, onClose }: MetadataSidePanelProps) {
|
||||
<>
|
||||
{/* Backdrop — click to close */}
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-background/40 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-40 bg-background/40 backdrop-blur-xs"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
titleError ? 'border-red-400' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
@@ -107,7 +107,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -134,7 +134,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-border px-3 py-2 font-mono text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -154,7 +154,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -195,7 +195,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
<option value="">Link to existing node...</option>
|
||||
|
||||
@@ -104,7 +104,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
questionError ? 'border-red-400' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
@@ -126,7 +126,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||
className={cn(
|
||||
'block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary',
|
||||
optionLabelError ? 'border-red-400' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function NodeFormResolution({ node, onUpdate }: NodeFormResolutionProps)
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
titleError ? 'border-red-400' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
@@ -103,7 +103,7 @@ Document what was done and the outcome.
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -134,7 +134,7 @@ Document what was done and the outcome.
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +184,7 @@ function NodeListItem({
|
||||
'group flex items-center gap-1 rounded-md px-2 py-1.5 text-sm transition-colors cursor-pointer',
|
||||
isRootNode
|
||||
? isSelected
|
||||
? 'bg-blue-500/20 ring-2 ring-blue-500 shadow-sm'
|
||||
? 'bg-blue-500/20 ring-2 ring-blue-500 shadow-xs'
|
||||
: 'bg-blue-500/10 border border-blue-500/30 hover:bg-blue-500/15'
|
||||
: isSelected
|
||||
? 'bg-primary/10 ring-1 ring-primary'
|
||||
@@ -581,7 +581,7 @@ export function NodeList() {
|
||||
|
||||
{/* Add Node Type Selector */}
|
||||
{addingToParent && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-xs">
|
||||
<div className="w-full max-w-xs rounded-lg border border-border bg-card p-4 shadow-lg">
|
||||
<h3 className="mb-3 text-sm font-semibold">Select Node Type</h3>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -167,7 +167,7 @@ export function NodePicker({
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-border px-2 py-1 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -201,7 +201,7 @@ export function NodePicker({
|
||||
className={cn(
|
||||
'block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
error ? 'border-red-400' : 'border-border'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -64,7 +64,7 @@ interface AddNodePickerProps {
|
||||
|
||||
function AddNodePicker({ onSelect, onCancel }: AddNodePickerProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-dashed border-primary/40 bg-card px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center gap-2 rounded-xl border border-dashed border-primary/40 bg-card px-3 py-2 shadow-xs">
|
||||
<span className="text-xs text-muted-foreground shrink-0">Add:</span>
|
||||
|
||||
<button
|
||||
@@ -691,7 +691,7 @@ export function TreeCanvas() {
|
||||
style={{
|
||||
// Subtle dot grid background
|
||||
backgroundImage:
|
||||
'radial-gradient(circle, hsl(var(--border)) 1px, transparent 1px)',
|
||||
'radial-gradient(circle, var(--color-border) 1px, transparent 1px)',
|
||||
backgroundSize: '24px 24px',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -168,7 +168,7 @@ export function TreeCanvasNode({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded-xl border border-border bg-card shadow-sm transition-all duration-150',
|
||||
'relative rounded-xl border border-border bg-card shadow-xs transition-all duration-150',
|
||||
config.borderClass,
|
||||
isExpanded && 'ring-1 ring-primary shadow-md',
|
||||
isSelected && !isExpanded && 'ring-1 ring-primary/50',
|
||||
|
||||
@@ -73,7 +73,7 @@ export function TreeMetadataForm() {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
nameError ? 'border-red-400' : 'border-border'
|
||||
)}
|
||||
/>
|
||||
@@ -94,7 +94,7 @@ export function TreeMetadataForm() {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@ export function TreeMetadataForm() {
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
<option value="">No category</option>
|
||||
@@ -134,7 +134,7 @@ export function TreeMetadataForm() {
|
||||
className={cn(
|
||||
'block min-w-0 flex-1 rounded-md border border-border px-3 py-2 text-sm',
|
||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function ValidationSummary({ errors, onSelectNode, onFixWithAI, isFixing
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors',
|
||||
isFixing
|
||||
? 'bg-primary/10 text-primary cursor-wait'
|
||||
: 'bg-gradient-brand text-white shadow-sm shadow-primary/20 hover:opacity-90'
|
||||
: 'bg-gradient-brand text-white shadow-xs shadow-primary/20 hover:opacity-90'
|
||||
)}
|
||||
>
|
||||
{isFixing ? (
|
||||
@@ -109,7 +109,7 @@ export function ValidationSummary({ errors, onSelectNode, onFixWithAI, isFixing
|
||||
: 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-red-400" />
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-red-400">{error.message}</p>
|
||||
{error.nodeId && (
|
||||
@@ -133,7 +133,7 @@ export function ValidationSummary({ errors, onSelectNode, onFixWithAI, isFixing
|
||||
: 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-400" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-yellow-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-yellow-400">{warning.message}</p>
|
||||
{warning.nodeId && (
|
||||
|
||||
@@ -155,10 +155,10 @@ export function useTreeLayout(): UseTreeLayoutResult {
|
||||
target: child.id,
|
||||
type: 'smoothstep',
|
||||
label: edgeLabel,
|
||||
labelStyle: { fill: 'hsl(var(--muted-foreground))', fontSize: 11 },
|
||||
labelBgStyle: { fill: 'hsl(var(--card))', fillOpacity: 0.9 },
|
||||
labelStyle: { fill: 'var(--color-muted-foreground)', fontSize: 11 },
|
||||
labelBgStyle: { fill: 'var(--color-card)', fillOpacity: 0.9 },
|
||||
labelBgPadding: [4, 2] as [number, number],
|
||||
style: { stroke: 'hsl(var(--border))' },
|
||||
style: { stroke: 'var(--color-border)' },
|
||||
})
|
||||
|
||||
walk(child, node.id)
|
||||
@@ -183,17 +183,17 @@ export function useTreeLayout(): UseTreeLayoutResult {
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
label: ref.label ? truncateLabel(ref.label) : undefined,
|
||||
labelStyle: { fill: 'hsl(var(--primary))', fontSize: 10, fontWeight: 500 },
|
||||
labelBgStyle: { fill: 'hsl(var(--card))', fillOpacity: 0.95 },
|
||||
labelStyle: { fill: 'var(--color-primary)', fontSize: 10, fontWeight: 500 },
|
||||
labelBgStyle: { fill: 'var(--color-card)', fillOpacity: 0.95 },
|
||||
labelBgPadding: [4, 2] as [number, number],
|
||||
style: {
|
||||
stroke: 'hsl(var(--primary))',
|
||||
stroke: 'var(--color-primary)',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '6 3',
|
||||
},
|
||||
markerEnd: {
|
||||
type: 'arrowclosed' as const,
|
||||
color: 'hsl(var(--primary))',
|
||||
color: 'var(--color-primary)',
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@ export function TreePreviewNode({
|
||||
{/* Solution path indicator - shows when this branch leads to a solution */}
|
||||
{leadsTosolution && (
|
||||
<div
|
||||
className="absolute -top-1.5 -right-1.5 flex items-center justify-center rounded-full bg-green-500/90 p-0.5 shadow-sm"
|
||||
className="absolute -top-1.5 -right-1.5 flex items-center justify-center rounded-full bg-green-500/90 p-0.5 shadow-xs"
|
||||
title="This branch leads to a solution"
|
||||
>
|
||||
<CheckCircle className="h-3 w-3 text-foreground" />
|
||||
|
||||
@@ -3,14 +3,14 @@ import { cn } from '@/lib/utils'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97]',
|
||||
'inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97]',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
'bg-gradient-brand text-[#101114] font-semibold shadow-lg shadow-primary/20 hover:opacity-90',
|
||||
'bg-gradient-brand text-brand-dark font-semibold shadow-lg shadow-primary/20 hover:opacity-90',
|
||||
secondary:
|
||||
'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)] hover:bg-[rgba(255,255,255,0.06)]',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground hover:border-[rgba(255,255,255,0.12)] hover:bg-brand-border',
|
||||
destructive:
|
||||
'bg-red-400/10 text-red-400 border border-red-400/20 hover:bg-red-400/20',
|
||||
ghost:
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Input({ className, error, id, ...props }: InputProps) {
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20',
|
||||
className
|
||||
|
||||
@@ -6,7 +6,7 @@ export function Skeleton({ className, ...props }: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'animate-pulse rounded-lg bg-[rgba(255,255,255,0.06)]',
|
||||
'animate-pulse rounded-lg bg-brand-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Textarea({ className, error, id, ...props }: TextareaProps) {
|
||||
className={cn(
|
||||
'flex w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20',
|
||||
className
|
||||
|
||||
@@ -1,63 +1,187 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* ResolutionFlow Dark Theme — Slate & Ice Modern */
|
||||
--background: 228 12% 7%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 220 10% 10%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 220 10% 10%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 187 72% 43%;
|
||||
--primary-foreground: 228 12% 7%;
|
||||
--secondary: 220 8% 14%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 220 8% 14%;
|
||||
--muted-foreground: 215 10% 58%;
|
||||
--accent: 220 8% 14%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 350 81% 55%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 220 8% 14%;
|
||||
--input: 220 8% 14%;
|
||||
--ring: 187 72% 43%;
|
||||
--radius: 0.75rem;
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* App Shell tokens */
|
||||
--sidebar-w: 260px;
|
||||
--sidebar-bg: 228 12% 6%;
|
||||
--sidebar-hover: 220 8% 14%;
|
||||
--sidebar-active: 187 72% 43% / 0.10;
|
||||
--border-subtle: 220 8% 12%;
|
||||
--text-dimmed: 218 10% 39%;
|
||||
@theme {
|
||||
/* ── Brand tokens ─────────────────────────────────── */
|
||||
--color-brand-gradient-from: oklch(0.65 0.13 195); /* #06b6d4 cyan-500 */
|
||||
--color-brand-gradient-to: oklch(0.74 0.12 195); /* #22d3ee cyan-400 */
|
||||
--color-brand-dark: oklch(0.145 0.013 264); /* #101114 */
|
||||
--color-brand-dark-card: oklch(0.17 0.01 264); /* #14161a */
|
||||
--color-brand-dark-surface: oklch(0.17 0.01 264); /* #14161a */
|
||||
--color-brand-text-primary: oklch(0.98 0.005 264); /* #f8fafc */
|
||||
--color-brand-text-secondary: oklch(0.63 0.02 260); /* #8891a0 */
|
||||
--color-brand-text-muted: oklch(0.45 0.015 260); /* #5a6170 */
|
||||
--color-brand-border: oklch(0.35 0 0 / 0.06);
|
||||
|
||||
/* Glass system */
|
||||
--glass-bg: rgba(24, 26, 31, 0.55);
|
||||
--glass-bg-hover: rgba(24, 26, 31, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.06);
|
||||
--glass-border-hover: rgba(255, 255, 255, 0.12);
|
||||
--glass-blur: blur(16px);
|
||||
--glass-blur-strong: blur(20px);
|
||||
--glass-blur-light: blur(12px);
|
||||
/* ── Semantic color tokens (OKLCH, direct values) ── */
|
||||
--color-background: oklch(0.145 0.013 264); /* dark charcoal #101114 */
|
||||
--color-foreground: oklch(0.98 0.005 264); /* near-white #f8fafc */
|
||||
|
||||
/* Shadow system */
|
||||
--shadow-float: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
--shadow-float-hover: 0 12px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
--shadow-cyan-glow: 0 8px 32px rgba(6, 182, 212, 0.08);
|
||||
--color-card: oklch(0.178 0.008 264); /* #17191d */
|
||||
--color-card-foreground: oklch(0.98 0.005 264);
|
||||
|
||||
/* Easing */
|
||||
--ease-out-smooth: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--color-popover: oklch(0.178 0.008 264);
|
||||
--color-popover-foreground: oklch(0.98 0.005 264);
|
||||
|
||||
--color-primary: oklch(0.65 0.13 195); /* cyan #1ea8c4 */
|
||||
--color-primary-foreground: oklch(0.145 0.013 264); /* dark bg for contrast */
|
||||
|
||||
--color-secondary: oklch(0.22 0.008 264); /* #212329 */
|
||||
--color-secondary-foreground: oklch(0.98 0.005 264);
|
||||
|
||||
--color-muted: oklch(0.22 0.008 264);
|
||||
--color-muted-foreground: oklch(0.63 0.02 260); /* #8891a0 */
|
||||
|
||||
--color-accent: oklch(0.22 0.008 264);
|
||||
--color-accent-foreground: oklch(0.98 0.005 264);
|
||||
|
||||
--color-destructive: oklch(0.55 0.22 15); /* rose #e63359 */
|
||||
--color-destructive-foreground: oklch(0.98 0.005 264);
|
||||
|
||||
--color-border: oklch(0.22 0.008 264);
|
||||
--color-input: oklch(0.22 0.008 264);
|
||||
--color-ring: oklch(0.65 0.13 195); /* cyan, matches primary */
|
||||
|
||||
/* ── Radii ───────────────────────────────────────── */
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.625rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
|
||||
/* ── Fonts ───────────────────────────────────────── */
|
||||
--font-sans:
|
||||
'IBM Plex Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
Roboto, sans-serif;
|
||||
--font-heading: 'Bricolage Grotesque', system-ui, sans-serif;
|
||||
--font-label: 'JetBrains Mono', monospace;
|
||||
|
||||
/* ── Gradients ───────────────────────────────────── */
|
||||
--background-image-gradient-brand: linear-gradient(
|
||||
135deg,
|
||||
oklch(0.65 0.13 195) 0%,
|
||||
oklch(0.74 0.12 195) 100%
|
||||
);
|
||||
--background-image-gradient-brand-hover: linear-gradient(
|
||||
135deg,
|
||||
oklch(0.58 0.12 195) 0%,
|
||||
oklch(0.65 0.13 195) 100%
|
||||
);
|
||||
|
||||
/* ── Animations (keyframes inside @theme) ────────── */
|
||||
--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;
|
||||
--animate-slide-down: slideDown 400ms ease-out;
|
||||
--animate-slide-in-right: slideInRight 300ms ease-out;
|
||||
--animate-fade-in-right: fadeInRight 400ms ease-out;
|
||||
--animate-breathe-glow: breatheGlow 3s ease-in-out infinite alternate;
|
||||
--animate-bell-wobble: bellWobble 500ms ease-in-out;
|
||||
--animate-fade: fadeIn 300ms ease forwards;
|
||||
|
||||
@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 slideDown {
|
||||
from { transform: translateY(-100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
from { transform: translateX(30px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes breatheGlow {
|
||||
from {
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 20px oklch(0.65 0.13 195 / 0.04);
|
||||
}
|
||||
to {
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 30px oklch(0.65 0.13 195 / 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bellWobble {
|
||||
0% { transform: rotate(0deg); }
|
||||
20% { transform: rotate(8deg); }
|
||||
40% { transform: rotate(-6deg); }
|
||||
60% { transform: rotate(4deg); }
|
||||
80% { transform: rotate(-2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Root CSS variables (non-theme: glass, shadows, layout) ── */
|
||||
:root {
|
||||
/* App Shell tokens */
|
||||
--sidebar-w: 260px;
|
||||
--sidebar-bg: oklch(0.13 0.013 264);
|
||||
--sidebar-hover: var(--color-secondary);
|
||||
--sidebar-active: oklch(0.65 0.13 195 / 0.10);
|
||||
--text-dimmed: oklch(0.42 0.015 260);
|
||||
|
||||
/* Glass system */
|
||||
--glass-bg: oklch(0.16 0.01 264 / 0.55);
|
||||
--glass-bg-hover: oklch(0.16 0.01 264 / 0.7);
|
||||
--glass-border: oklch(1 0 0 / 0.06);
|
||||
--glass-border-hover: oklch(1 0 0 / 0.12);
|
||||
--glass-blur: blur(16px);
|
||||
--glass-blur-strong: blur(20px);
|
||||
--glass-blur-light: blur(12px);
|
||||
|
||||
/* Shadow system */
|
||||
--shadow-float: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
--shadow-float-hover: 0 12px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px oklch(1 0 0 / 0.08);
|
||||
--shadow-cyan-glow: 0 8px 32px oklch(0.65 0.13 195 / 0.08);
|
||||
|
||||
/* Easing */
|
||||
--ease-out-smooth: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ── Base styles ─────────────────────────────────────── */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
scrollbar-color: var(--color-border) transparent;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -67,333 +191,201 @@
|
||||
background: transparent;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--border));
|
||||
background-color: var(--color-border);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--muted-foreground));
|
||||
background-color: var(--color-muted-foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'IBM Plex Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
@apply bg-background text-foreground font-sans;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Bricolage Grotesque', system-ui, sans-serif;
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
/* App Shell Grid Layout */
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-w) 1fr;
|
||||
grid-template-rows: 56px 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
transition: grid-template-columns 200ms ease;
|
||||
/* ── Custom utilities ────────────────────────────────── */
|
||||
@utility btn-press {
|
||||
@apply active:scale-[0.98] transition-transform;
|
||||
}
|
||||
|
||||
.app-shell--collapsed {
|
||||
grid-template-columns: 56px 1fr;
|
||||
@utility text-gradient-brand {
|
||||
@apply bg-gradient-brand bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Mobile: single column */
|
||||
@media (max-width: 767px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Staggered fade-in for page sections */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
/* Animations */
|
||||
@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 slideDown {
|
||||
from { transform: translateY(-100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInRight {
|
||||
from { transform: translateX(30px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes breatheGlow {
|
||||
from { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 20px rgba(6, 182, 212, 0.04); }
|
||||
to { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 30px rgba(6, 182, 212, 0.12); }
|
||||
}
|
||||
|
||||
@keyframes bellWobble {
|
||||
0% { transform: rotate(0deg); }
|
||||
20% { transform: rotate(8deg); }
|
||||
40% { transform: rotate(-6deg); }
|
||||
60% { transform: rotate(4deg); }
|
||||
80% { transform: rotate(-2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animate-fade-in {
|
||||
animation: fade-in 200ms ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 200ms ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slide-in-from-left 200ms ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-bottom {
|
||||
animation: slide-in-from-bottom 200ms ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scale-in 150ms ease-out;
|
||||
}
|
||||
|
||||
/* Button press feedback */
|
||||
.btn-press {
|
||||
@apply active:scale-[0.98] transition-transform;
|
||||
}
|
||||
|
||||
/* Brand gradient text */
|
||||
.text-gradient-brand {
|
||||
@apply bg-gradient-brand bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
/* Glass card — interactive with hover lift */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-float);
|
||||
transition: transform 200ms var(--ease-out-smooth),
|
||||
border-color 200ms var(--ease-out-smooth),
|
||||
box-shadow 200ms var(--ease-out-smooth);
|
||||
}
|
||||
.glass-card:hover {
|
||||
@utility glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-float);
|
||||
transition:
|
||||
transform 200ms var(--ease-out-smooth),
|
||||
border-color 200ms var(--ease-out-smooth),
|
||||
box-shadow 200ms var(--ease-out-smooth);
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
border-color: var(--glass-border-hover);
|
||||
box-shadow: var(--shadow-float-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* Glass card — static, no hover transform */
|
||||
.glass-card-static {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-float);
|
||||
@utility glass-card-static {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-float);
|
||||
}
|
||||
|
||||
@utility active-glow {
|
||||
animation: breatheGlow 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@utility rdp-custom {
|
||||
@apply text-foreground;
|
||||
& .rdp-month { @apply w-full; }
|
||||
& .rdp-caption { @apply flex justify-center items-center mb-4; }
|
||||
& .rdp-caption_label { @apply text-sm font-medium; }
|
||||
& .rdp-nav { @apply flex gap-1; }
|
||||
& .rdp-nav_button { @apply h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100; }
|
||||
& .rdp-table { @apply w-full border-collapse; }
|
||||
& .rdp-head_cell { @apply text-muted-foreground font-normal text-xs; }
|
||||
& .rdp-cell { @apply text-center text-sm p-0; }
|
||||
& .rdp-day { @apply h-9 w-9 p-0 font-normal hover:bg-accent rounded-md transition-colors; }
|
||||
& .rdp-day_selected { @apply bg-primary text-primary-foreground hover:bg-primary/90; }
|
||||
& .rdp-day_today { @apply bg-accent text-accent-foreground; }
|
||||
& .rdp-day_outside { @apply text-muted-foreground opacity-50; }
|
||||
& .rdp-day_disabled { @apply text-muted-foreground opacity-50; }
|
||||
& .rdp-day_range_middle { @apply bg-accent text-accent-foreground; }
|
||||
& .rdp-day_hidden { @apply invisible; }
|
||||
}
|
||||
|
||||
/* ── Layout utilities ────────────────────────────────── */
|
||||
@layer utilities {
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-w) 1fr;
|
||||
grid-template-rows: 56px 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
transition: grid-template-columns 200ms ease;
|
||||
}
|
||||
|
||||
/* Breathing glow for highlighted stat cards */
|
||||
.active-glow {
|
||||
animation: breatheGlow 3s ease-in-out infinite alternate;
|
||||
.app-shell--collapsed {
|
||||
grid-template-columns: 56px 1fr;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Staggered fade-in helper */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sonner Toast Customization — outside @layer for higher specificity */
|
||||
/* ── Sonner toast overrides ──────────────────────────── */
|
||||
[data-sonner-toast] {
|
||||
background-color: hsl(var(--card)) !important;
|
||||
color: hsl(var(--card-foreground)) !important;
|
||||
border: 1px solid hsl(var(--border)) !important;
|
||||
background-color: var(--color-card) !important;
|
||||
color: var(--color-card-foreground) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3) !important;
|
||||
border-radius: 0.75rem;
|
||||
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-title] {
|
||||
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="success"] {
|
||||
border-color: rgba(52, 211, 153, 0.3) !important;
|
||||
[data-sonner-toast][data-type='success'] {
|
||||
border-color: oklch(0.76 0.15 163 / 0.3) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="success"] [data-icon] {
|
||||
color: #34d399;
|
||||
[data-sonner-toast][data-type='success'] [data-icon] {
|
||||
color: oklch(0.76 0.15 163);
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="error"] {
|
||||
border-color: rgba(248, 113, 113, 0.3) !important;
|
||||
[data-sonner-toast][data-type='error'] {
|
||||
border-color: oklch(0.7 0.16 22 / 0.3) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="error"] [data-icon] {
|
||||
color: #f87171;
|
||||
[data-sonner-toast][data-type='error'] [data-icon] {
|
||||
color: oklch(0.7 0.16 22);
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="info"] {
|
||||
border-color: hsl(var(--border)) !important;
|
||||
[data-sonner-toast][data-type='info'] {
|
||||
border-color: var(--color-border) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="info"] [data-icon] {
|
||||
color: hsl(var(--muted-foreground));
|
||||
[data-sonner-toast][data-type='info'] [data-icon] {
|
||||
color: var(--color-muted-foreground);
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="warning"] {
|
||||
border-color: rgba(251, 191, 36, 0.3) !important;
|
||||
[data-sonner-toast][data-type='warning'] {
|
||||
border-color: oklch(0.82 0.16 85 / 0.3) !important;
|
||||
}
|
||||
[data-sonner-toast][data-type="warning"] [data-icon] {
|
||||
color: #fbbf24;
|
||||
[data-sonner-toast][data-type='warning'] [data-icon] {
|
||||
color: oklch(0.82 0.16 85);
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-close-button] {
|
||||
color: hsl(var(--muted-foreground));
|
||||
color: var(--color-muted-foreground);
|
||||
border-radius: 0.375rem;
|
||||
transition: color 150ms, background-color 150ms;
|
||||
}
|
||||
[data-sonner-toast] [data-close-button]:hover {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-accent-foreground);
|
||||
}
|
||||
|
||||
/* React Day Picker Customization */
|
||||
@layer components {
|
||||
.rdp-custom {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-month {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-caption {
|
||||
@apply flex justify-center items-center mb-4;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-caption_label {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-nav {
|
||||
@apply flex gap-1;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-nav_button {
|
||||
@apply h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-table {
|
||||
@apply w-full border-collapse;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-head_cell {
|
||||
@apply text-muted-foreground font-normal text-xs;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-cell {
|
||||
@apply text-center text-sm p-0;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day {
|
||||
@apply h-9 w-9 p-0 font-normal hover:bg-accent rounded-md transition-colors;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_selected {
|
||||
@apply bg-primary text-primary-foreground hover:bg-primary/90;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_today {
|
||||
@apply bg-accent text-accent-foreground;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_outside {
|
||||
@apply text-muted-foreground opacity-50;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_disabled {
|
||||
@apply text-muted-foreground opacity-50;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_range_middle {
|
||||
@apply bg-accent text-accent-foreground;
|
||||
}
|
||||
|
||||
.rdp-custom .rdp-day_hidden {
|
||||
@apply invisible;
|
||||
}
|
||||
}
|
||||
|
||||
/* React Flow dark theme overrides */
|
||||
/* ── React Flow dark theme overrides ─────────────────── */
|
||||
.react-flow__background {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.react-flow__controls {
|
||||
background-color: hsl(var(--card)) !important;
|
||||
border: 1px solid hsl(var(--border)) !important;
|
||||
background-color: var(--color-card) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
background-color: hsl(var(--card)) !important;
|
||||
border-color: hsl(var(--border)) !important;
|
||||
fill: hsl(var(--muted-foreground)) !important;
|
||||
color: hsl(var(--muted-foreground)) !important;
|
||||
background-color: var(--color-card) !important;
|
||||
border-color: var(--color-border) !important;
|
||||
fill: var(--color-muted-foreground) !important;
|
||||
color: var(--color-muted-foreground) !important;
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background-color: hsl(var(--accent)) !important;
|
||||
fill: hsl(var(--foreground)) !important;
|
||||
background-color: var(--color-accent) !important;
|
||||
fill: var(--color-foreground) !important;
|
||||
}
|
||||
|
||||
.react-flow__controls-button svg {
|
||||
@@ -401,34 +393,32 @@
|
||||
}
|
||||
|
||||
.react-flow__minimap {
|
||||
background-color: hsl(var(--card)) !important;
|
||||
border: 1px solid hsl(var(--border)) !important;
|
||||
background-color: var(--color-card) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
|
||||
.react-flow__edge-path {
|
||||
stroke: hsl(var(--border));
|
||||
stroke: var(--color-border);
|
||||
}
|
||||
|
||||
.react-flow__edge-text {
|
||||
fill: hsl(var(--muted-foreground));
|
||||
fill: var(--color-muted-foreground);
|
||||
}
|
||||
|
||||
.react-flow__edge-textbg {
|
||||
fill: hsl(var(--card));
|
||||
fill: var(--color-card);
|
||||
}
|
||||
|
||||
/* Hide default React Flow attribution */
|
||||
.react-flow__attribution {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Handle styles */
|
||||
.react-flow__handle {
|
||||
background-color: hsl(var(--border));
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* Accessibility: Reduce motion for users who prefer it */
|
||||
/* ── Accessibility: Reduce motion ────────────────────── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -189,7 +189,7 @@ export function AccountSettingsPage() {
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-border bg-card px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
@@ -367,7 +367,7 @@ export function AccountSettingsPage() {
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md border border-border bg-card px-2 py-0.5 text-xs',
|
||||
'text-foreground focus:border-primary focus:outline-none'
|
||||
'text-foreground focus:border-primary focus:outline-hidden'
|
||||
)}
|
||||
>
|
||||
<option value="engineer">engineer</option>
|
||||
@@ -415,7 +415,7 @@ export function AccountSettingsPage() {
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-border bg-card px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
<select
|
||||
@@ -423,7 +423,7 @@ export function AccountSettingsPage() {
|
||||
onChange={(e) => setInviteRole(e.target.value)}
|
||||
className={cn(
|
||||
'rounded-md border border-border bg-card px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
<option value="engineer">Engineer</option>
|
||||
@@ -609,7 +609,7 @@ export function AccountSettingsPage() {
|
||||
className={cn(
|
||||
'mt-2 block w-full max-w-xs rounded-xl border border-border bg-card px-3 py-2',
|
||||
'text-sm text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
<option value="markdown">Markdown (.md)</option>
|
||||
|
||||
@@ -223,7 +223,7 @@ export default function AssistantChatPage() {
|
||||
<div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center">
|
||||
<Sparkles size={14} className="text-primary" />
|
||||
</div>
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] rounded-2xl px-4 py-3">
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-2xl px-4 py-3">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,7 +242,7 @@ export default function AssistantChatPage() {
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about IT, networking, troubleshooting..."
|
||||
rows={3}
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-none focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
disabled={loading}
|
||||
/>
|
||||
@@ -250,7 +250,7 @@ export default function AssistantChatPage() {
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || loading}
|
||||
className="bg-gradient-brand text-[#101114] p-3 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
className="bg-gradient-brand text-brand-dark p-3 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
title="Send message"
|
||||
>
|
||||
<Send size={18} />
|
||||
@@ -286,7 +286,7 @@ export default function AssistantChatPage() {
|
||||
</p>
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="bg-gradient-brand text-[#101114] font-semibold text-sm rounded-[10px] px-6 py-2.5 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
className="bg-gradient-brand text-brand-dark font-semibold text-sm rounded-[10px] px-6 py-2.5 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
>
|
||||
Start a Conversation
|
||||
</button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user