diff --git a/CLAUDE.md b/CLAUDE.md
index e28bb710..40dd0e05 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,10 +24,10 @@
- **Logo:** Inline SVG in `BrandLogo.tsx` (decision-tree icon with gradient). Wordmark: "Resolution" in white + "Flow" in `text-gradient-brand`
- **Brand assets:** `brand-assets/` (source SVGs + brand-guide.html), `frontend/src/assets/brand/` (app assets), `frontend/public/icons/` (favicon)
- **CSS utilities:** `text-gradient-brand`, `bg-gradient-brand`, `bg-gradient-brand-hover` (defined in `tailwind.config.js` and `index.css`)
-- **Layout:** App shell with persistent sidebar + top bar + main workspace (CSS Grid). See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
-- **Workspace system:** Top-level context switcher (Troubleshooting, Procedures, Policies, Finance). Sidebar categories, tags, stats, and content adapt per workspace. See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
+- **Layout:** App shell with persistent sidebar + top bar + main content (CSS Grid). See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
+- **Navigation:** Sidebar nav with type sub-items (All Flows → Troubleshooting / Projects). Pinned flows section for quick access. NO workspace switcher. See [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md)
+- **Terminology:** User-facing label is "Flows" (not "Trees"). Procedural flows are called "Projects" in the UI. `tree_type` column values unchanged in DB.
- **Rebrand guide:** [REBRAND-IMPLEMENTATION-GUIDE.md](REBRAND-IMPLEMENTATION-GUIDE.md)
-- **Interactive mockup:** `docs/mockups/resolutionflow-workspaces-mockup.html` (open in browser for visual reference)
**Component styling rules:**
- Primary buttons: `bg-gradient-brand` with `shadow-lg shadow-primary/20`, hover lifts with stronger shadow
@@ -40,7 +40,7 @@
- Cards: `bg-card border-border rounded-xl`, hover brightens border
- Section labels: `font-label text-[0.6875rem] uppercase tracking-wide text-muted-foreground`
-When adding new pages/components: use "ResolutionFlow" branding, purple gradient accent theme, `bg-card` containers, `text-foreground`/`text-muted-foreground` hierarchy. Primary actions use `bg-gradient-brand`. Pages render inside the app shell (CSS Grid: topbar + sidebar + main). Reference [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) for layout patterns, workspace context, and component specs.
+When adding new pages/components: use "ResolutionFlow" branding, purple gradient accent theme, `bg-card` containers, `text-foreground`/`text-muted-foreground` hierarchy. Primary actions use `bg-gradient-brand`. Pages render inside the app shell (CSS Grid: topbar + sidebar + main). Use "Flows" not "Trees" in all user-facing text; use "Projects" not "Procedures" for procedural flows. Reference [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) for layout patterns, navigation, and component specs.
---
diff --git a/UI-DESIGN-SYSTEM.md b/UI-DESIGN-SYSTEM.md
index dfc31804..241dbb01 100644
--- a/UI-DESIGN-SYSTEM.md
+++ b/UI-DESIGN-SYSTEM.md
@@ -1,10 +1,49 @@
# ResolutionFlow UI Design System & Layout Architecture
-> **Purpose:** This document defines the new app-shell layout, design tokens, component patterns, and workspace architecture for ResolutionFlow. It is the single source of truth for all frontend UI work going forward.
+> **Purpose:** This document defines the app-shell layout, design tokens, component patterns, and navigation architecture for ResolutionFlow. It is the single source of truth for all frontend UI work going forward.
> **Last Updated:** February 15, 2026
-> **Status:** Approved for implementation
-> **Reference Mockup:** `docs/mockups/resolutionflow-workspaces-mockup.html` (interactive, open in browser)
-> **Note:** If the mockup file is not yet in the repo, it should be placed at the path above. The mockup is also available as a project output artifact.
+> **Status:** Approved for implementation (v2 — workspace system removed, replaced with flat nav + type sub-items)
+> **Reference Mockup:** `docs/mockups/resolutionflow-workspaces-mockup.html` (interactive — note: mockup shows old workspace switcher design which has been superseded by this document)
+
+---
+
+## IMPORTANT: Changes from Previous Version
+
+This is version 2 of the design system. The following changes were made based on UX research and product decisions:
+
+1. **Workspace system REMOVED.** The workspace switcher, workspace table, workspace API, `workspaceStore`, and all workspace-related components should be removed. Workspaces added unnecessary cognitive overhead and context-switching cost for the current scale of the product.
+2. **"Procedures" renamed to "Projects"** in all user-facing labels. The `tree_type` database column retains the value `'procedural'` — only UI labels change.
+3. **"Trees" renamed to "Flows"** in all user-facing labels. "Tree" is an internal/technical term. Users see "Flows" everywhere.
+4. **Flow type filtering** is handled via nav sub-items (All Flows → Troubleshooting / Projects), not workspaces.
+5. **Pinned Flows** section added to sidebar for quick access to favorites.
+6. **Folder sidebar panel REMOVED** from the flow library page. Folders remain in the database and API but are not displayed as a persistent second panel. Folder organization is deferred to a future release.
+7. **Search bar centered** in the top bar (flex spacers on both sides).
+8. **Keyboard shortcut badge** is platform-aware: `⌘K` on Mac, `Ctrl+K` on Windows/Linux.
+
+### Files to Remove or Deprecate
+
+These files were created for the workspace system and should be **deleted**:
+
+```
+frontend/src/store/workspaceStore.ts → DELETE (replace with flowFilterStore.ts)
+frontend/src/api/workspaces.ts → DELETE
+frontend/src/types/workspace.ts → DELETE (remove Workspace type)
+frontend/src/components/workspace/ → DELETE entire directory
+frontend/src/constants/workspaceLabels.ts → DELETE (replace with flowLabels.ts)
+backend/app/models/workspace.py → DELETE
+backend/app/api/endpoints/workspaces.py → DELETE
+backend/app/schemas/workspace.py → DELETE (if exists)
+```
+
+### Database Migration
+
+Create a new migration that:
+1. Drops the `workspace_id` column from `trees` table (if added)
+2. Drops the `workspace_id` column from `tree_categories` table (if added)
+3. Drops the `workspaces` table
+4. **Does NOT touch** the existing `tree_type` column — it stays as-is
+
+Remove the workspace router from `backend/app/api/router.py`.
---
@@ -12,16 +51,15 @@
1. [Layout Architecture](#1-layout-architecture)
2. [Design Tokens](#2-design-tokens)
-3. [Workspace System](#3-workspace-system)
+3. [Naming & Terminology](#3-naming--terminology)
4. [Sidebar Components](#4-sidebar-components)
5. [Top Bar](#5-top-bar)
6. [Main Content Area](#6-main-content-area)
7. [Component Patterns](#7-component-patterns)
8. [Icon System](#8-icon-system)
9. [Animation & Transitions](#9-animation--transitions)
-10. [Data Model Changes](#10-data-model-changes)
-11. [Migration Strategy](#11-migration-strategy)
-12. [Implementation Phases](#12-implementation-phases)
+10. [Migration Strategy](#10-migration-strategy)
+11. [Implementation Phases](#11-implementation-phases)
---
@@ -29,21 +67,22 @@
### Overview
-ResolutionFlow transitions from a top-nav + full-width content layout to a **persistent sidebar + top bar + main workspace** layout. This mirrors the UX patterns MSP engineers already use in ConnectWise Automate, Datto RMM, and HaloPSA.
+ResolutionFlow uses a **persistent sidebar + top bar + main content** layout. This mirrors the UX patterns MSP engineers already use in ConnectWise Automate, Datto RMM, and HaloPSA.
### Shell Structure
```
┌──────────────────────────────────────────────────────┐
-│ TOP BAR (56px) — Logo, Search, Quick Actions, User │
+│ TOP BAR (56px) — Logo, ── Search (centered) ──, User│
├──────────────┬───────────────────────────────────────┤
│ SIDEBAR │ MAIN CONTENT AREA │
│ (260px) │ │
│ │ Page Header + Actions │
-│ Workspace │ Quick Stats Row │
-│ Switcher │ Filters Bar │
+│ Pinned │ Quick Stats Row │
+│ Flows │ Filters Bar │
│ │ Content Sections │
-│ Navigation │ (trees/flows list, sessions, etc.) │
+│ Navigation │ (flow list, sessions, etc.) │
+│ + sub-items │ │
│ │ │
│ Categories │ │
│ Tags │ │
@@ -57,7 +96,7 @@ ResolutionFlow transitions from a top-nav + full-width content layout to a **per
### CSS Grid Implementation
```tsx
-// AppLayout.tsx - new shell structure
+// AppLayout.tsx
...
@@ -97,8 +136,6 @@ ResolutionFlow transitions from a top-nav + full-width content layout to a **per
}
```
-This variable should be used consistently for sidebar width, logo area width calculations, and responsive breakpoints.
-
---
## 2. Design Tokens
@@ -134,29 +171,23 @@ These are already implemented and MUST be used. Do NOT introduce new color value
#### shadcn/ui CSS Variables (from index.css)
-All shadcn component tokens are defined via HSL CSS variables (`--background`, `--foreground`, `--card`, `--primary`, etc.) with both light and dark mode values. Continue using these for shadcn components.
+All shadcn component tokens are defined via HSL CSS variables (`--background`, `--foreground`, `--card`, `--primary`, etc.) in `:root`. The app is dark-only — all tokens live in `:root`, not `.dark`.
-### NEW Design Tokens for App Shell
-
-These are already added to `index.css` within the `:root` block (the app is dark-only, all tokens live in `:root`):
+### App Shell Tokens (in index.css `:root`)
```css
:root {
- /* Existing tokens... */
-
/* App Shell tokens */
- --sidebar-bg: 240 10% 4.5%; /* #0c0c0f - slightly lighter than app bg */
- --sidebar-hover: 240 6% 12%; /* #1e1e23 */
- --sidebar-active: 243 75% 59% / 0.08; /* brand purple at 8% opacity */
- --border-subtle: 240 6% 12%; /* #1f1f23 - lighter border for sections */
- --text-dimmed: 240 4% 24%; /* #3f3f46 - very dim text for timestamps */
+ --sidebar-bg: 240 10% 4.5%;
+ --sidebar-hover: 240 6% 12%;
+ --sidebar-active: 243 75% 59% / 0.08;
+ --border-subtle: 240 6% 12%;
+ --text-dimmed: 240 4% 24%;
}
```
### Semantic Status Colors
-These map to session/tree status indicators and MUST be used consistently:
-
| Status | Color | Tailwind Class | Usage |
|--------|-------|---------------|-------|
| Success / Resolved | `#22c55e` | `text-green-500` / `bg-green-500` | Completed sessions, resolved |
@@ -166,8 +197,6 @@ These map to session/tree status indicators and MUST be used consistently:
### Category Colors
-Categories use a fixed palette of distinguishable colors for their dot indicators:
-
```typescript
// constants/categoryColors.ts
export const CATEGORY_COLORS = [
@@ -184,84 +213,72 @@ export const CATEGORY_COLORS = [
] as const;
```
-Categories are assigned colors based on their creation order (index % length). Colors are stored on the category model in the database.
-
-> **Migration needed:** The `tree_categories` table already has `display_order` and `is_active` columns. Add a `color` column:
+> **Migration needed:** Add `color` column to `tree_categories`:
> ```sql
> ALTER TABLE tree_categories ADD COLUMN color VARCHAR(7) DEFAULT '#3b82f6';
> ```
-> Backfill existing categories with colors from the palette based on their `display_order`.
---
-## 3. Workspace System
+## 3. Naming & Terminology
-### Concept
+### User-Facing Labels
-Workspaces are the top-level organizational context. They sit **above** the existing folder system — a workspace scopes which trees/flows are visible, while user folders remain available within each workspace for personal organization. When a user switches workspace, the sidebar categories, tags, stats, filters, and main content all adapt.
+| Internal / Database | User-Facing Label | Context |
+|--------------------|--------------------|---------|
+| `tree` | **Flow** | Universal term for all flow types |
+| `tree_type = 'troubleshooting'` | **Troubleshooting** | Branching decision-tree flows |
+| `tree_type = 'procedural'` | **Project** | Linear step-by-step flows (onboarding, migrations, etc.) |
+| `tree_structure` | Flow structure | Internal — never shown to users |
+| `tree_editor` | **Flow Editor** | The editor page |
+| `session` | **Session** (troubleshooting) / **Run** (projects) | Active use of a flow |
-> **Important:** Workspaces do NOT replace the existing `UserFolder` system. Folders are per-user personal organization that continue to work within the active workspace context. The folder sidebar, drag-and-drop, and folder-tree junction table are preserved. Workspaces filter what content is available; folders organize it.
+### Label Constants
-The layout, navigation structure, and component patterns stay identical — only the DATA changes.
+Replace `workspaceLabels.ts` with a simpler type-based label system:
-### Workspace Types (Initial Set)
+```typescript
+// constants/flowLabels.ts
+export interface FlowTypeLabels {
+ navLabel: string;
+ newButton: string;
+ editorLabel: string;
+ searchPlaceholder: string;
+}
-| Workspace | Description | Icon | Accent Color |
-|-----------|-------------|------|-------------|
-| Troubleshooting | Break/fix decision trees | 🔧 | `#ef4444` (red) |
-| Procedures | Step-by-step operational flows | 📋 | `#3b82f6` (blue) |
-| Policies | Compliance & policy builders | 📜 | `#8b5cf6` (violet) |
-| Finance | Billing & procurement flows | 💰 | `#22c55e` (green) |
+export const FLOW_TYPE_LABELS: Record = {
+ all: {
+ navLabel: 'All Flows',
+ newButton: '+ Create Flow',
+ editorLabel: 'Flow Editor',
+ searchPlaceholder: 'Search flows, sessions, tags…',
+ },
+ troubleshooting: {
+ navLabel: 'Troubleshooting',
+ newButton: '+ New Troubleshooting Flow',
+ editorLabel: 'Flow Editor',
+ searchPlaceholder: 'Search troubleshooting flows…',
+ },
+ procedural: {
+ navLabel: 'Projects',
+ newButton: '+ New Project',
+ editorLabel: 'Flow Editor',
+ searchPlaceholder: 'Search projects, runbooks…',
+ },
+};
-**Users and admins can create custom workspaces.** The four above are defaults.
-
-### Workspace Switcher Component
-
-Located at the top of the sidebar, below the nav section label. It's a dropdown that shows:
-
-1. **Current workspace** — icon, name, description, chevron
-2. **Dropdown options** — all available workspaces with flow counts
-3. **"Add workspace…"** — link at bottom to create new
-
-```
-┌─────────────────────────────┐
-│ 🔧 Troubleshooting │
-│ Break/fix decision trees │
-│ ▼ │
-├─────────────────────────────┤
-│ 🔧 Troubleshooting 42 │ ← active (highlighted)
-│ 📋 Procedures 18 │
-│ 📜 Policies 7 │
-│ 💰 Finance 4 │
-│─────────────────────────────│
-│ + Add workspace… │
-└─────────────────────────────┘
+export function getFlowLabels(typeFilter: string): FlowTypeLabels {
+ return FLOW_TYPE_LABELS[typeFilter] || FLOW_TYPE_LABELS.all;
+}
```
-### Behavior on Workspace Switch
+### Where Labels Appear
-When the user selects a new workspace:
-
-1. Sidebar content fades out (200ms opacity transition)
-2. Data updates:
- - Categories list → filtered to workspace
- - Tags cloud → filtered to workspace
- - Nav badge counts → reflect workspace totals
- - "All Trees" label → changes (e.g., "All Procedures")
- - "Tree Editor" label → changes (e.g., "Flow Editor")
- - Search placeholder → adapts
-3. Main content fades out/in (200ms opacity transition)
- - Page title changes
- - Stats row updates
- - Filter chips update
- - Tree/flow list filters to workspace
- - Sessions panel filters to workspace
-4. "New Tree" button label → adapts (e.g., "New Procedure")
-5. Toast notification confirms switch: "Switched to **Procedures**"
-
-### Data Model
-
-See [Section 10: Data Model Changes](#10-data-model-changes) for backend schema.
+- Sidebar nav: "All Flows" (parent), "Troubleshooting" (sub), "Projects" (sub)
+- Page title on flow library: "Flow Library" (always — does not change with filter)
+- "+ Create Flow" button in page header (adapts based on active type filter)
+- Search bar placeholder in top bar (adapts based on active type filter)
+- Flow Editor nav item: Always "Flow Editor"
---
@@ -270,15 +287,19 @@ See [Section 10: Data Model Changes](#10-data-model-changes) for backend schema.
### Structure (top to bottom)
```
-1. Workspace Switcher (dropdown)
+1. ─── PINNED FLOWS ─── (section, collapsible)
+ - Pinned flow items (star icon, click to start session)
+ - "Pin a flow…" hint if empty
2. ─── divider ───
3. Primary Navigation
- - Dashboard (grid icon)
- - All Trees/Flows (cube icon) [badge: count]
- - Tree/Flow Editor (pencil icon)
- - Sessions (clock icon) [badge: active count]
- - Exports (file icon)
- - Step Library (bookmark icon) [dot: new items]
+ - Dashboard (LayoutGrid icon)
+ - All Flows (Box icon) [badge: total count]
+ ├─ Troubleshooting (sub-item, indented)
+ └─ Projects (sub-item, indented)
+ - Flow Editor (PenLine icon)
+ - Sessions (Clock icon) [badge: active count]
+ - Exports (FileText icon)
+ - Step Library (Bookmark icon) [dot: new items]
4. ─── divider ───
5. Categories Section
- Section label: "CATEGORIES"
@@ -289,10 +310,45 @@ See [Section 10: Data Model Changes](#10-data-model-changes) for backend schema.
- Tag chips in a flex-wrap cloud
8. ─── spacer (flex-grow) ───
9. Footer (pinned to bottom)
- - Team (users icon)
- - Settings (gear icon)
+ - Team (Users icon)
+ - Settings (Settings icon)
```
+### Pinned Flows Section
+
+```tsx
+interface PinnedFlow {
+ id: string;
+ name: string;
+ treeType: 'troubleshooting' | 'procedural';
+ categoryEmoji?: string;
+}
+
+interface PinnedFlowsSectionProps {
+ pinnedFlows: PinnedFlow[];
+ onFlowClick: (id: string) => void;
+ onUnpin: (id: string) => void;
+}
+```
+
+**Visual design:**
+- Section label: `PINNED` in `font-label text-[0.6875rem] uppercase tracking-wide text-muted-foreground`
+- Each pinned flow: single line with emoji/icon + flow name (truncated) + quick-start button on hover
+- Max 10 pinned flows visible; if more, show "View all pinned" link
+- Empty state: subtle hint text "Star a flow to pin it here" with `text-muted-foreground text-xs`
+- Collapsible via section header chevron
+
+**Data model:**
+- New junction table: `user_pinned_trees` (user_id UUID, tree_id UUID, pinned_at TIMESTAMPTZ, display_order INTEGER)
+- API: `POST /api/v1/trees/{id}/pin`, `DELETE /api/v1/trees/{id}/pin`, `GET /api/v1/trees/pinned`
+- Store: add `pinnedFlows` array to existing auth or a new lightweight `pinnedStore`
+
+**Pin/unpin interaction:**
+- Pin via: star icon on flow cards in library, three-dot menu → "Pin to sidebar", right-click context menu
+- Unpin via: right-click pinned item → "Unpin", or three-dot menu on flow card
+- Toast on pin: "📌 Pinned **Flow Name** to sidebar"
+- Toast on unpin: "Unpinned **Flow Name**"
+
### Nav Item Component
```tsx
@@ -302,13 +358,21 @@ interface NavItemProps {
label: string;
badge?: number | 'dot';
isActive?: boolean;
+ children?: NavSubItem[]; // for expandable sub-items
+}
+
+interface NavSubItem {
+ href: string;
+ label: string;
+ count?: number;
+ isActive?: boolean;
}
```
**Active state styling:**
- Background: `var(--sidebar-active)` (brand purple at 8% opacity)
- Left border accent: 3px gradient bar (brand gradient, rounded right corners)
-- Text: `text-foreground` (white in dark mode)
+- Text: `text-foreground` (white)
- Icon: full opacity
**Inactive state styling:**
@@ -317,6 +381,37 @@ interface NavItemProps {
- Icon: 70% opacity
- Hover: `var(--sidebar-hover)` background
+### Nav Sub-Items (Flow Type Filter)
+
+Sub-items appear indented under "All Flows":
+
+```
+📦 All Flows 47
+ 🔧 Troubleshooting 29
+ 📋 Projects 18
+```
+
+**Sub-item styling:**
+- Indented: `pl-9` (aligns text with parent label, past the icon)
+- No icon of their own (the emoji/text suffices, or use a small dot)
+- Font: `text-[0.8125rem] text-muted-foreground`
+- Active: same purple highlight as parent but without the left accent bar
+- Count: right-aligned in `font-label text-xs text-muted-foreground`
+
+**Behavior:**
+- Clicking "All Flows" navigates to `/flows` (or `/trees` — use existing route) with no type filter
+- Clicking "Troubleshooting" navigates to `/flows?type=troubleshooting`
+- Clicking "Projects" navigates to `/flows?type=procedural`
+- The active state highlights whichever is currently selected
+- When a sub-item is active, the parent "All Flows" stays visually highlighted (dimmer) to show context
+- Sub-items are always visible (not collapsible) — there are only 2
+
+**Flow library page behavior:**
+- Reads `type` from URL search params (already implemented in `TreeLibraryPage.tsx`)
+- Filters the flow list accordingly
+- Updates the page subtitle and "Create" button label via `getFlowLabels(typeFilter)`
+- Categories, tags, stats, and sessions in the sidebar do NOT change — they show data across all types
+
### Category Item Component
```tsx
@@ -332,7 +427,7 @@ Renders as: `[●] Category Name .............. 12`
- 8px color dot (rounded full)
- Name in `text-sm text-muted-foreground`
-- Count right-aligned in `font-label text-xs` (Outfit font)
+- Count right-aligned in `font-label text-xs`
- Hover: `var(--sidebar-hover)` background, text brightens
### Tag Cloud Component
@@ -345,9 +440,7 @@ interface TagCloudProps {
}
```
-Renders as flex-wrap container of small chip elements:
-- Background: `bg-card`
-- Border: `border-border`
+- Background: `bg-card`, Border: `border-border`
- Text: `font-label text-xs` (Outfit)
- Active: purple-tinted background and border
- Gap: 4px
@@ -359,22 +452,28 @@ Renders as flex-wrap container of small chip elements:
### Structure (left to right)
```
-[Logo + Wordmark] | [Search Input (⌘K)] | ──flex grow── | [Quick Launch] [Notifications] [Avatar]
+[Logo + Wordmark] | ──flex grow── | [Search Input (Ctrl+K / ⌘K)] | ──flex grow── | [Quick Launch] [Notifications] [Avatar]
```
+The search bar is **centered** between the logo and action buttons using flex spacers on both sides.
+
### Logo Area
-- Width: `calc(var(--sidebar-w) - 40px)` — aligns with sidebar content
+- Width: fits content (not fixed to sidebar width)
- Contains: ResolutionFlow logo SVG (32x32) + branded wordmark
- Wordmark: "Resolution" in white + "Flow" in gradient text
### Search Bar
-- Flex: 1, max-width 480px
-- Placeholder adapts to workspace context (e.g., "Search trees, sessions, tags…")
+- Width: 100%, max-width 480px
+- **Centered** in the top bar via `flex-1` spacers on both sides
+- Placeholder: "Search flows, sessions, tags…" (static — does NOT change with type filter)
- Left icon: magnifying glass (Lucide `Search`)
-- Right badge: keyboard shortcut hint `⌘K` (styled as small pill)
-- Border: `border-border`, focus: `border-brand-gradient-from`
+- Right badge: platform-aware keyboard shortcut hint
+ ```tsx
+ {navigator.platform?.toLowerCase().includes('mac') ? '⌘K' : 'Ctrl+K'}
+ ```
+- Border: `border-border`, focus: `border-primary/30`
### Action Buttons
@@ -392,16 +491,14 @@ Avatar: 32px circle with brand gradient background, white initials in `font-head
### Page Structure
-Every page in the main content follows this vertical flow:
-
```
1. Page Header (title + action buttons)
2. Quick Stats Row (4 cards)
3. Filters Bar (chips)
-4. Content Sections (grouped lists, panels)
+4. Content Sections (flow list, sessions panel)
```
-Not every page needs all sections. The Dashboard uses all four. The Tree Editor page would only use the page header and then the editor workspace.
+Not every page uses all sections. Dashboard/Flow Library uses all four. Flow Editor uses only the header.
### Quick Stats Row
@@ -410,41 +507,32 @@ Not every page needs all sections. The Dashboard uses all four. The Tree Editor
Each card:
- Background: `bg-card`
- Border: `border` (subtle, brightens on hover)
-- Border radius: `rounded-xl` (10px)
+- Border radius: `rounded-xl`
- Padding: 16px 18px
- Label: `font-label text-[0.6875rem] uppercase tracking-wide text-muted-foreground`
- Value: `font-heading text-2xl font-bold`
- Meta: `text-[0.6875rem] text-[var(--text-dimmed)]`
-Special value styles:
-- `.gradient` → `text-gradient-brand`
-- Colored → direct color style (e.g., amber for warnings)
+Stat cards for the Flow Library page:
+
+| Stat | Label | Value Source |
+|------|-------|-------------|
+| 1 | Active Flows | Total published flows (use `text-gradient-brand`) |
+| 2 | Sessions Today | Sessions started today (number) |
+| 3 | Open Sessions | In-progress sessions (use `text-amber-500` if > 0) |
+| 4 | Docs Generated | Export count this month |
### Filters Bar
-Horizontal row of filter chips with a divider before "More Filters":
-
```
-[All*] [Recently Used] [My Trees] [Team Trees] [Defaults] | [⊗ More Filters]
+[All] [Recently Used] [My Flows] [Team Flows] [Defaults] | [⊗ More Filters]
```
-Active chip: purple-tinted background/border (same as sidebar tag active state).
+Active chip: purple-tinted background/border.
-### Section Groups
+> **Note:** The "Troubleshooting" / "Projects" type filter is handled by the **sidebar sub-items**, NOT by filter chips in this bar. The filters bar is for orthogonal filters (ownership, recency, status).
-Collapsible sections with header:
-
-```
-[●] SECTION TITLE 5 [▼]
-─────────────────────────────
-```
-
-- Dot: 8px gradient circle
-- Title: `font-heading text-[0.8125rem] font-bold uppercase tracking-wide`
-- Count: pill with `bg-surface` background
-- Collapse: chevron button (toggles section visibility)
-
-### Tree/Flow List Items
+### Flow List Items
Grid layout per item:
@@ -459,15 +547,15 @@ Each item:
- Border radius: `rounded-lg`
- Padding: 12px 16px
-**Icon box:** 36x36px rounded-lg with category-tinted background and emoji.
+**Pin star:** Show a subtle star icon (Lucide `Star`) on hover, filled if pinned. Click to toggle pin.
-**Info column:**
-- Name: `font-heading text-sm font-semibold`
-- Meta row: tags (small chips) + step/solution count
-
-**Category column:** color dot + name in `font-label text-xs`
-
-**Actions:** Three-dot menu, opacity 0 until row hover.
+**Actions three-dot menu includes:**
+- Start Session
+- Edit
+- Duplicate
+- Pin to Sidebar / Unpin from Sidebar
+- Share
+- Delete
### Sessions Panel
@@ -481,12 +569,6 @@ Contained card with header and rows:
└──────────────────────────────────────┘
```
-Session row grid:
-```
-grid-template-columns: 8px 1fr 140px 80px 100px
- dot name progress ticket time
-```
-
Status dot colors: `bg-amber-500` (in-progress), `bg-green-500` (completed).
---
@@ -507,17 +589,15 @@ Both: `rounded-lg px-4 py-2 text-sm font-semibold`, with icon support (16px Luci
- **Count badge:** `bg-card border border-border rounded-full px-2 text-[0.6875rem] font-label`
- **Active count badge:** purple-tinted background/border
- **Dot badge:** 6px circle, `bg-brand-gradient-from`
-- **Notification badge:** 8px red circle with sidebar-bg border (creates "cut out" effect)
+- **Notification badge:** 8px red circle with sidebar-bg border
### Toast Notifications
-Fixed-position toast notification (current app uses top-right via Sonner — keep this position for consistency):
+Fixed-position, **top-right** (current Sonner config — keep as-is):
- Background: `bg-card`
- Border: brand gradient border
- Border radius: `rounded-xl`
-- Shadow: `shadow-xl` with heavy dark shadow
-- Content: emoji icon + message text (supports `` for emphasis)
-- Auto-dismiss: 2000ms with fade out
+- Auto-dismiss: 2000ms
---
@@ -525,23 +605,21 @@ Fixed-position toast notification (current app uses top-right via Sonner — kee
### Primary Source: Lucide React
-All icons use [Lucide React](https://lucide.dev) at consistent sizes:
-
-| Context | Size | Lucide Prop |
-|---------|------|-------------|
-| Nav items | 18px | `size={18}` |
-| Buttons | 16px | `size={16}` |
-| Top bar actions | 18px | `size={18}` |
-| Section headers | 14px | `size={14}` |
-| Filter chips | 14px | `size={14}` |
+| Context | Size |
+|---------|------|
+| Nav items | 18px |
+| Buttons | 16px |
+| Top bar actions | 18px |
+| Section headers | 14px |
+| Filter chips | 14px |
### Navigation Icon Mapping
| Nav Item | Lucide Icon |
|----------|-------------|
| Dashboard | `LayoutGrid` |
-| All Trees/Flows | `Box` |
-| Tree/Flow Editor | `PenLine` |
+| All Flows | `Box` |
+| Flow Editor | `PenLine` |
| Sessions | `Clock` |
| Exports | `FileText` |
| Step Library | `Bookmark` |
@@ -550,27 +628,15 @@ All icons use [Lucide React](https://lucide.dev) at consistent sizes:
| Search | `Search` |
| Quick Launch | `Zap` |
| Notifications | `Bell` |
-| New Tree | `Plus` |
-| Import | `Upload` |
-| Filter | `Filter` |
-| Collapse | `ChevronDown` |
+| Create Flow | `Plus` |
+| Pin/Favorite | `Star` |
| More Actions | `MoreHorizontal` |
### Category Emoji Icons
-Categories in the tree list use emoji as visual anchors inside tinted icon boxes. These are set per-tree or per-category and stored as a string field.
-
Default emoji per common categories:
-- Networking: 🌐
-- Active Directory: 🔒
-- Email/Exchange: 📧
-- Server Issues: 🖥️
-- VPN/Remote: 🔌
-- Citrix/RDS: 🖥️
-- Printers: 🖨️
-- Backup/DR: 🔄
-- Onboarding: 👤
-- Security: 🔐
+- Networking: 🌐, Active Directory: 🔒, Email/Exchange: 📧, Server Issues: 🖥️
+- VPN/Remote: 🔌, Printers: 🖨️, Backup/DR: 🔄, Onboarding: 👤, Security: 🔐
---
@@ -578,253 +644,148 @@ Default emoji per common categories:
### Page/Section Load
-Staggered fade-in using CSS animation:
-
-```css
-.fade-in {
- animation: fadeIn 0.3s ease forwards;
-}
-
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(6px); }
- to { opacity: 1; transform: translateY(0); }
-}
-```
-
-Apply with incrementing `animation-delay` on child sections:
+Staggered fade-in with incrementing `animation-delay`:
- Stats row: 50ms
- Filters: 100ms
- Section 1: 150ms
- Section 2: 200ms
-### Workspace Switch
-
-Coordinated fade transition:
-1. Sidebar content: `opacity → 0` (200ms)
-2. Main content: `opacity → 0` (200ms)
-3. Data swap (instant)
-4. Both: `opacity → 1` (200ms)
-
### Interactive Elements
| Element | Property | Duration | Easing |
|---------|----------|----------|--------|
| Nav item hover | background-color | 120ms | ease |
| Button hover | transform, box-shadow | 150ms | ease |
-| Tree item hover | background, border-color | 120ms | ease |
+| Flow item hover | background, border-color | 120ms | ease |
| Dropdown open | opacity, transform | 150ms | ease |
| Action buttons (three-dot) | opacity | 120ms | ease |
| Toast appear/disappear | opacity, transform | 300ms | ease |
-
-### Workspace Dropdown
-
-```css
-@keyframes dropIn {
- from { opacity: 0; transform: translateY(-4px); }
- to { opacity: 1; transform: translateY(0); }
-}
-```
+| Pin star toggle | scale + color | 200ms | ease |
---
-## 10. Data Model Changes
+## 10. Migration Strategy
-### New: `workspaces` Table
+### What to Remove
-```sql
-CREATE TABLE workspaces (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- name VARCHAR(100) NOT NULL,
- slug VARCHAR(100) NOT NULL UNIQUE,
- description TEXT,
- icon VARCHAR(10), -- emoji character
- accent_color VARCHAR(7), -- hex color (e.g., '#ef4444')
- account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
- is_default BOOLEAN DEFAULT FALSE,
- sort_order INTEGER DEFAULT 0,
- created_at TIMESTAMPTZ DEFAULT NOW(),
- updated_at TIMESTAMPTZ DEFAULT NOW()
-);
-```
+1. **WorkspaceSwitcher component** and all workspace UI
+2. **workspaceStore.ts** — replace with `flowFilterStore.ts` (or just use URL params, which `TreeLibraryPage.tsx` already does)
+3. **workspaceLabels.ts** — replace with `flowLabels.ts`
+4. **Workspace API** (`backend/app/api/endpoints/workspaces.py`) and model
+5. **Folder sidebar panel** from `TreeLibraryPage` — remove `FolderSidebar` import and the column it occupies. The `UserFolder` model, API, and database table remain untouched for future use.
+6. All `workspace_id` references in queries
-**Default workspaces (seeded):**
+### What to Add
-| name | slug | icon | accent_color | is_default |
-|------|------|------|-------------|------------|
-| Troubleshooting | troubleshooting | 🔧 | #ef4444 | true |
-| Procedures | procedures | 📋 | #3b82f6 | false |
-| Policies | policies | 📜 | #8b5cf6 | false |
-| Finance | finance | 💰 | #22c55e | false |
+1. **Pinned flows** — `user_pinned_trees` table, API endpoints, sidebar section
+2. **Nav sub-items** — update `Sidebar.tsx` to render "Troubleshooting" and "Projects" as indented children of "All Flows"
+3. **flowLabels.ts** — simple type-based label lookup
+4. **Pin star** on flow cards in the library grid/list views
-### Modified: `trees` Table
+### What to Keep (Already Built)
-Add column:
+1. **AppLayout.tsx** CSS Grid shell — keep as-is
+2. **TopBar.tsx** — keep, but verify search is centered and keyboard shortcut is platform-aware (already fixed)
+3. **Sidebar.tsx** — keep structure, remove workspace switcher, add pinned flows section and nav sub-items
+4. **NavItem.tsx** — keep, extend to support `children` sub-items
+5. **CategoryList, TagCloud** — keep as-is
+6. **All existing pages** — keep rendering inside the shell
-```sql
-ALTER TABLE trees ADD COLUMN workspace_id UUID REFERENCES workspaces(id);
-```
+### Label Replacements (Global Find & Replace)
-All existing trees get assigned to the "troubleshooting" workspace via data migration.
+In all frontend files:
-> **Note:** The `trees` table already has a `tree_type` column with values `'troubleshooting'` and `'procedural'`. The workspace system is a superset of this — `tree_type` controls tree behavior (branching vs linear), while `workspace_id` controls organizational context. Both fields coexist. The data migration should map existing `tree_type='troubleshooting'` trees to the Troubleshooting workspace and `tree_type='procedural'` trees to the Procedures workspace.
+| Find | Replace With |
+|------|-------------|
+| `All Trees` | `All Flows` |
+| `Tree Editor` | `Flow Editor` |
+| `New Tree` | `Create Flow` |
+| `All Procedures` | `Projects` |
+| `New Procedure` | `New Project` |
+| `Flow Library` (page title) | `Flow Library` (keep) |
-### Modified: `tree_categories` Table
-
-Add column:
-
-```sql
-ALTER TABLE tree_categories ADD COLUMN workspace_id UUID REFERENCES workspaces(id);
-```
-
-Categories become workspace-scoped. A category belongs to one workspace. The existing `account_id` column remains for tenancy — `workspace_id` is an additional organizational dimension within an account.
-
-### Modified: `tree_tags` Table
-
-Tags remain global (cross-workspace) but the popular tags query filters by workspace context. The existing `account_id` scoping on tags is preserved.
-
-### API Changes
-
-**New endpoints:**
-
-```
-GET /api/v1/workspaces -- list all workspaces for user's account
-POST /api/v1/workspaces -- create workspace (admin)
-PATCH /api/v1/workspaces/{id} -- update workspace
-DELETE /api/v1/workspaces/{id} -- soft delete workspace
-```
-
-**Modified endpoints:**
-
-```
-GET /api/v1/trees?workspace_id={uuid} -- add workspace filter
-GET /api/v1/categories?workspace_id={uuid} -- categories are a separate router (categories.py)
-GET /api/v1/sessions?workspace_id={uuid}
-```
-
-> **Note:** Categories have their own dedicated router at `/api/v1/categories` (not nested under `/trees/categories`). Tags are at `/api/v1/tags`. Folders are at `/api/v1/folders`. See `backend/app/api/router.py` for the full route registry.
-
-### Frontend State
-
-```typescript
-// store/workspaceStore.ts
-interface WorkspaceState {
- workspaces: Workspace[];
- activeWorkspaceId: string | null;
- setActiveWorkspace: (id: string) => void;
- // Persisted in localStorage as 'active-workspace-id'
-}
-```
-
-All tree/session/category queries include the active workspace ID as a filter parameter.
+In `Sidebar.tsx` nav items:
+- Remove workspace switcher
+- Add pinned flows section at top
+- "All Flows" with sub-items "Troubleshooting" and "Projects"
---
-## 11. Migration Strategy
+## 11. Implementation Phases
-### From Current Layout to App Shell
+### Phase A: Cleanup & Labels
-The current `AppLayout.tsx` uses a top-nav bar with horizontal nav links and an `` for full-width page content. The migration:
+1. Remove workspace-related files (see list above)
+2. Create down-migration to drop workspace tables/columns
+3. Remove workspace router from `backend/app/api/router.py`
+4. Create `constants/flowLabels.ts`
+5. Update all UI labels: Trees → Flows, Procedures → Projects
+6. Remove `FolderSidebar` from `TreeLibraryPage` (keep folder model/API)
+7. Verify search bar is centered and shortcut is platform-aware in `TopBar.tsx`
-1. **AppLayout.tsx** → Complete rewrite to CSS Grid shell (topbar + sidebar + main)
-2. **BrandLogo + BrandWordmark** → Move into topbar logo area
-3. **Nav links** → Move into sidebar as vertical nav items with icons
-4. **ThemeToggle** → Move into sidebar footer or top bar
-5. **User menu** → Avatar in top bar with dropdown
-6. **Outlet** → Renders inside `` grid cell
+### Phase B: Nav Sub-Items
-### Page-by-Page Adaptation
+1. Extend `NavItem` component to support `children` sub-items
+2. Update `Sidebar.tsx` to render "All Flows" with "Troubleshooting" and "Projects" children
+3. Sub-items link to `/trees?type=troubleshooting` and `/trees?type=procedural`
+4. Verify `TreeLibraryPage.tsx` correctly reads `type` from URL params (already does)
+5. Add flow counts to sub-item badges
-| Page | Changes Needed |
-|------|---------------|
-| TreeLibraryPage | Becomes the "Dashboard" or "All Trees" view in main content area. Add stats row, filters, and grouped list layout |
-| TreeNavigationPage | Renders in main content. Scratchpad overlay unchanged (fixed position) |
-| TreeEditorPage | Renders in main content. Full-width editor workspace |
-| SessionHistoryPage | Renders in main content. Add filters and session list |
-| SessionDetailPage | Renders in main content. Minimal changes |
-| SettingsPage | Renders in main content. Minimal changes |
-| LoginPage / RegisterPage | **No change** — these render outside the app shell (no sidebar) |
+### Phase C: Pinned Flows
----
+1. Create `user_pinned_trees` table and Alembic migration
+2. Create pin/unpin API endpoints (`POST /api/v1/trees/{id}/pin`, `DELETE /api/v1/trees/{id}/pin`, `GET /api/v1/trees/pinned`)
+3. Create Pydantic schemas for pinned flows
+4. Build `PinnedFlowsSection` sidebar component
+5. Add pin star to flow cards in library (visible on hover, filled if pinned)
+6. Add "Pin to Sidebar" / "Unpin" to flow card three-dot menu
+7. Toast notifications for pin/unpin actions
-## 12. Implementation Phases
+### Phase D: Polish
-### Phase A: Foundation (Backend + Shell)
-
-1. Create `workspace` table, model, schemas, migration
-2. Add `workspace_id` to `tree` and `tree_category` tables
-3. Seed default workspaces
-4. Data migration: assign all existing trees to "troubleshooting" workspace
-5. Create workspace API endpoints
-6. Create `workspaceStore` (Zustand + localStorage)
-7. **Rewrite `AppLayout.tsx`** to CSS Grid shell with topbar + sidebar + main
-8. Move existing nav items into sidebar
-9. Add workspace switcher component (static, no data yet)
-
-### Phase B: Sidebar Components
-
-1. Build `NavItem` component with active state + badge
-2. Build `CategoryList` component with color dots
-3. Build `TagCloud` component
-4. Build `WorkspaceSwitcher` dropdown with animations
-5. Wire workspace switcher to `workspaceStore`
-6. Update tree/session queries to include workspace filter
-
-### Phase C: Main Content Redesign
-
-1. Build `QuickStats` row component
-2. Build `FiltersBar` component
-3. Build `SectionGroup` collapsible component
-4. Build `TreeListItem` component (grid layout)
-5. Build `SessionsPanel` component
-6. Redesign `TreeLibraryPage` as Dashboard using new components
-7. Add search bar with ⌘K shortcut
-
-### Phase D: Polish & Integration
-
-1. Add workspace switch animations (fade transitions)
-2. Add toast notifications for workspace switch
+1. Add category color column migration + backfill
+2. Verify all animations and transitions
3. Responsive breakpoints (collapse sidebar on mobile)
-4. Update all page components to work within new shell
-5. E2E testing of workspace switching flow
-6. Performance testing (workspace switch should be <200ms)
+4. E2E testing of navigation flow
+5. Performance testing
---
-## Appendix A: File Structure for New Components
+## Appendix A: File Structure for New/Modified Components
```
frontend/src/
├── components/
│ ├── layout/
-│ │ ├── AppLayout.tsx ← REWRITE (CSS Grid shell)
-│ │ ├── TopBar.tsx ← NEW
-│ │ ├── Sidebar.tsx ← NEW
-│ │ ├── NavItem.tsx ← NEW
-│ │ └── ProtectedRoute.tsx (unchanged)
-│ ├── workspace/
-│ │ ├── WorkspaceSwitcher.tsx ← NEW
-│ │ ├── CategoryList.tsx ← NEW
-│ │ └── TagCloud.tsx ← NEW
+│ │ ├── AppLayout.tsx (keep — CSS Grid shell)
+│ │ ├── TopBar.tsx (keep — search centered, shortcut fixed)
+│ │ ├── Sidebar.tsx (MODIFY — remove workspace switcher, add pinned + sub-items)
+│ │ ├── NavItem.tsx (MODIFY — add children sub-item support)
+│ │ └── ProtectedRoute.tsx (keep)
+│ ├── sidebar/
+│ │ ├── PinnedFlowsSection.tsx ← NEW
+│ │ ├── CategoryList.tsx (keep or move from workspace/)
+│ │ └── TagCloud.tsx (keep or move from workspace/)
+│ ├── workspace/ ← DELETE entire directory
│ ├── dashboard/
-│ │ ├── QuickStats.tsx ← NEW
-│ │ ├── FiltersBar.tsx ← NEW
-│ │ ├── SectionGroup.tsx ← NEW
-│ │ ├── TreeListItem.tsx ← NEW
-│ │ └── SessionsPanel.tsx ← NEW
+│ │ ├── QuickStats.tsx (keep)
+│ │ ├── FiltersBar.tsx (keep)
+│ │ └── ...
│ └── common/
-│ ├── Toast.tsx ← NEW
-│ └── ... (existing)
+│ └── ...
├── store/
-│ ├── workspaceStore.ts ← NEW
-│ └── ... (existing)
+│ ├── workspaceStore.ts ← DELETE
+│ └── ...
├── api/
-│ ├── workspaces.ts ← NEW
-│ └── ... (existing)
-├── types/
-│ ├── workspace.ts ← NEW
-│ └── ... (existing)
-└── constants/
- └── categoryColors.ts ← NEW
+│ ├── workspaces.ts ← DELETE
+│ ├── pinnedFlows.ts ← NEW
+│ └── ...
+├── constants/
+│ ├── workspaceLabels.ts ← DELETE
+│ ├── flowLabels.ts ← NEW
+│ └── categoryColors.ts (keep)
+└── types/
+ └── ... (remove Workspace type if exists)
```
## Appendix B: Typography Quick Reference
@@ -834,25 +795,12 @@ frontend/src/
| Page titles | Plus Jakarta Sans (`font-heading`) | 700 | 1.375rem (22px) | -0.01em |
| Section titles | Plus Jakarta Sans | 700 | 0.8125rem (13px) | 0.04em, uppercase |
| Nav items | Inter (`font-sans`) | 500 | 0.8125rem (13px) | normal |
-| Tree names | Plus Jakarta Sans | 600 | 0.875rem (14px) | -0.005em |
+| Nav sub-items | Inter (`font-sans`) | 400 | 0.8125rem (13px) | normal |
+| Flow names | Plus Jakarta Sans | 600 | 0.875rem (14px) | -0.005em |
| Stat values | Plus Jakarta Sans | 700 | 1.5rem (24px) | -0.02em |
| Stat labels | Outfit (`font-label`) | 600 | 0.6875rem (11px) | 0.05em, uppercase |
| Badges/counts | Outfit | 400 | 0.6875rem (11px) | normal |
| Timestamps | Inter | 400 | 0.6875rem (11px) | normal |
| Tags/chips | Outfit | 400 | 0.625rem (10px) | normal |
+| Pinned flow names | Inter | 500 | 0.8125rem (13px) | normal |
| Search input | Inter | 400 | 0.8125rem (13px) | normal |
-
-## Appendix C: Workspace-Specific Adaptations
-
-When a workspace is active, these elements adapt:
-
-| Element | Troubleshooting | Procedures | Policies | Finance |
-|---------|----------------|------------|----------|---------|
-| "All ___" nav label | All Trees | All Procedures | All Policies | All Finance Flows |
-| Editor nav label | Tree Editor | Flow Editor | Policy Editor | Flow Editor |
-| New button label | New Tree | New Procedure | New Policy | New Flow |
-| Search placeholder | Search trees, sessions, tags… | Search procedures, runbooks… | Search policies, compliance… | Search billing, procurement… |
-| Stat 1 label | Active Trees | Active Procedures | Active Policies | Active Flows |
-| Stat 2 label | Sessions Today | Runs This Week | Pending Review | Runs This Month |
-| Stat 3 label | Open Sessions | In Progress | Compliance Score | Cost Saved |
-| Stat 4 label | Docs Generated | Avg Completion | Last Audit | Pending Approvals |
diff --git a/WORKSPACE-REMOVAL-PLAN.md b/WORKSPACE-REMOVAL-PLAN.md
new file mode 100644
index 00000000..572bd50e
--- /dev/null
+++ b/WORKSPACE-REMOVAL-PLAN.md
@@ -0,0 +1,608 @@
+# Workspace Removal & Navigation Refactor — Implementation Plan
+
+> **Purpose:** Combined implementation plan for removing the workspace system, renaming UI labels (Trees→Flows, Procedures→Projects), adding pinned flows, and restructuring sidebar navigation.
+> **Source of Truth:** [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) v2
+> **Date:** February 15, 2026
+> **Tailwind Version:** v3 only — do not use v4 syntax or patterns (see `package.json` line 56)
+
+---
+
+## Why This Change
+
+Workspaces added unnecessary cognitive overhead for the target MSP audience. UX research (Hick's Law, context-switching studies) shows that at the current product scale (10-15 beta testers, <50 flows per account), a workspace switcher creates friction without organizational benefit. The replacement is a flat navigation model with type sub-items and pinned favorites.
+
+---
+
+## Phase 1 — Backend: Add Pinned Flows (ship BEFORE removing workspaces)
+
+> **Important sequencing:** Add the pinned flows feature first and verify it works. Then remove workspaces in Phase 2. This ensures the replacement feature is stable before tearing out the old one. If both are done in the same session, at minimum run the pinned flows tests before starting workspace removal.
+
+### 1a. Add Pinned Flows Table
+
+```bash
+cd backend
+alembic revision --autogenerate -m "add_user_pinned_trees"
+```
+
+```sql
+CREATE TABLE user_pinned_trees (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ tree_id UUID NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
+ display_order INTEGER NOT NULL DEFAULT 0,
+ pinned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT uq_user_pinned_tree UNIQUE (user_id, tree_id)
+);
+
+CREATE INDEX idx_user_pinned_trees_user ON user_pinned_trees(user_id);
+CREATE INDEX idx_user_pinned_trees_tree ON user_pinned_trees(tree_id);
+```
+
+### 1b. Pinned Flows API Contract
+
+**Endpoints:**
+
+| Method | Path | Description | Auth |
+|--------|------|-------------|------|
+| `GET` | `/api/v1/trees/pinned` | List user's pinned flows (ordered by display_order) | Required |
+| `POST` | `/api/v1/trees/{id}/pin` | Pin a flow to sidebar | Required |
+| `DELETE` | `/api/v1/trees/{id}/pin` | Unpin a flow from sidebar | Required |
+| `PATCH` | `/api/v1/trees/pinned/reorder` | Update display_order for all pinned flows | Required |
+
+**Constraints & Limits:**
+- Max 15 pinned flows per user (return 409 if exceeded)
+- `UNIQUE(user_id, tree_id)` — pinning an already-pinned flow returns 200 (idempotent), not an error
+- Unpinning an already-unpinned flow returns 200 (idempotent)
+- When a tree is deleted, cascade removes the pin automatically (FK ON DELETE CASCADE)
+- Pins are per-user, not per-account — each user has their own pinned set
+
+**Response Shapes:**
+
+```typescript
+// GET /api/v1/trees/pinned
+interface PinnedFlowsResponse {
+ items: PinnedFlow[];
+ count: number;
+}
+
+interface PinnedFlow {
+ id: string; // pin record id
+ tree_id: string;
+ tree_name: string;
+ tree_type: 'troubleshooting' | 'procedural';
+ category_emoji?: string;
+ category_name?: string;
+ pinned_at: string; // ISO datetime
+ display_order: number;
+}
+
+// POST /api/v1/trees/{id}/pin → returns PinnedFlow
+// DELETE /api/v1/trees/{id}/pin → returns { success: true }
+// PATCH /api/v1/trees/pinned/reorder
+// body: { order: [{ tree_id: string, display_order: number }] }
+// returns: PinnedFlowsResponse
+```
+
+**Error Codes:**
+- `404` — tree not found or user lacks access
+- `409` — max pins reached (15)
+- `200` — idempotent success (pin already exists / already unpinned)
+
+### 1c. Add Pinned Flows Backend Files
+
+```
+CREATE: backend/app/models/user_pinned_tree.py
+MODIFY: backend/app/models/__init__.py — add UserPinnedTree import
+MODIFY: backend/app/api/endpoints/trees.py — add pin/unpin/list-pinned/reorder endpoints
+MODIFY: backend/app/schemas/tree.py — add PinnedFlow schema, add is_pinned to TreeListItem
+MODIFY: backend/app/api/router.py — register pin routes (nested under trees router)
+```
+
+**Verify pinned flows work before proceeding:**
+
+```bash
+cd backend
+alembic upgrade head
+pytest tests/ -k "pin" -v # Run pin-related tests
+# Manual: POST a pin, GET pinned list, verify response shapes
+```
+
+---
+
+## Phase 2 — Backend: Remove Workspace System
+
+### 2a. New Forward Migration (DO NOT use downgrade path)
+
+> **⚠️ Critical:** The existing `036_add_workspaces.py` downgrade function drops `tree_categories.color`, which we want to keep. Create a new forward migration instead.
+
+```bash
+cd backend
+alembic revision --autogenerate -m "remove_workspace_system"
+```
+
+The migration must:
+
+```python
+def upgrade():
+ # 1. Drop workspace_id FK from trees (if column exists)
+ op.drop_constraint('trees_workspace_id_fkey', 'trees', type_='foreignkey')
+ op.drop_column('trees', 'workspace_id')
+
+ # 2. Drop workspace_id FK from tree_categories (if column exists)
+ op.drop_constraint('tree_categories_workspace_id_fkey', 'tree_categories', type_='foreignkey')
+ op.drop_column('tree_categories', 'workspace_id')
+
+ # 3. Drop workspaces table
+ op.drop_table('workspaces')
+
+ # DO NOT drop tree_categories.color — we still use it
+
+def downgrade():
+ # Recreate workspaces table and FKs if needed (reverse of above)
+ pass
+```
+
+### 2b. Delete Workspace Backend Files
+
+```
+DELETE: backend/app/api/endpoints/workspaces.py
+DELETE: backend/app/models/workspace.py
+DELETE: backend/app/schemas/workspace.py (if exists)
+```
+
+### 2c. Clean Up Orphaned Workspace References
+
+> **⚠️ Important:** Workspace references exist beyond the obvious files. Scrub these:
+
+| File | What to Remove |
+|------|---------------|
+| `backend/app/api/router.py` | Remove workspace route registration |
+| `backend/app/models/__init__.py` (line 23, 55) | Remove `Workspace` import and `__all__` entry |
+| `backend/app/models/account.py` (line 18, 49) | Remove `workspaces` relationship on Account model |
+| `backend/app/models/category.py` (line 40) | Remove `workspace_id` column if present |
+| `backend/app/models/tree.py` | Remove `workspace_id` column and relationship if present |
+
+**Verification:**
+
+```bash
+cd backend
+grep -r "workspace" app/ --include="*.py" -l
+# Should only return this plan file and alembic migration history — no active code
+pytest --override-ini="addopts="
+# All tests must pass
+```
+
+**Migration smoke test (run against BOTH clean and existing DB):**
+
+```bash
+# Test on existing DB with workspace data:
+alembic upgrade head
+# Verify no errors, workspace tables gone, tree_categories.color still exists
+
+# Test on clean DB (full migration chain):
+dropdb patherly_test && createdb patherly_test
+DATABASE_URL=postgresql://...patherly_test alembic upgrade head
+# Verify clean run through all migrations
+```
+
+---
+
+## Phase 3 — Frontend: Remove Workspace System
+
+### 3a. Move sidebarCollapsed State First
+
+The `sidebarCollapsed` state currently lives in `workspaceStore.ts`. Move it before deleting:
+
+```typescript
+// frontend/src/store/userPreferencesStore.ts — ADD these:
+sidebarCollapsed: boolean;
+toggleSidebar: () => void;
+
+// Implementation:
+sidebarCollapsed: localStorage.getItem('sidebar-collapsed') === 'true',
+toggleSidebar: () => {
+ const next = !get().sidebarCollapsed;
+ localStorage.setItem('sidebar-collapsed', String(next));
+ set({ sidebarCollapsed: next });
+},
+```
+
+> Note: `userPreferencesStore` already uses Zustand persist — verify the localStorage key name doesn't conflict.
+
+### 3b. Delete Workspace Frontend Files
+
+```
+DELETE: frontend/src/store/workspaceStore.ts
+DELETE: frontend/src/components/workspace/WorkspaceSwitcher.tsx
+DELETE: frontend/src/components/workspace/WorkspaceCreateModal.tsx
+DELETE: frontend/src/constants/workspaceLabels.ts
+DELETE: frontend/src/types/workspace.ts (or remove Workspace type if shared file)
+DELETE: frontend/src/api/workspaces.ts
+DELETE: docs/mockups/resolutionflow-workspaces-mockup.html
+```
+
+**Before deleting `workspace/` directory**, move these components:
+
+```
+MOVE: frontend/src/components/workspace/CategoryList.tsx → frontend/src/components/sidebar/CategoryList.tsx
+MOVE: frontend/src/components/workspace/TagCloud.tsx → frontend/src/components/sidebar/TagCloud.tsx
+```
+
+### 3c. Update Shell Files That Import Workspace Store
+
+> **⚠️ Important:** These files are marked "keep" in the design system doc but they currently import workspace code. Each needs internal refactoring:
+
+| File | Lines to Change | Action |
+|------|----------------|--------|
+| `AppLayout.tsx` (line 6, 17) | `import { useWorkspaceStore }` | Replace with `import { useUserPreferencesStore }` — use `sidebarCollapsed` and `toggleSidebar` from there |
+| `TopBar.tsx` (line 6, 20) | `import { useWorkspaceStore }` | Replace with `useUserPreferencesStore` for `sidebarCollapsed`. Remove `getActiveWorkspace()` and `labels` — use static labels or `getFlowLabels()` |
+| `Sidebar.tsx` (line 4, 111+) | `WorkspaceSwitcher` import and render | Remove workspace switcher component. Add pinned flows section and nav sub-items (Phase 4) |
+
+### 3d. Clean Up Orphaned Frontend References
+
+> **⚠️ Important:** Additional workspace references exist beyond the obvious files:
+
+| File | What to Change |
+|------|---------------|
+| `frontend/src/types/index.ts` (line 11, 14) | Remove `Workspace` type export |
+| `frontend/src/components/layout/QuickLaunch.tsx` | Replace workspace-dependent labels with static "New Flow" / "New Project" |
+| `frontend/src/components/layout/CommandPalette.tsx` | Replace workspace-dependent search placeholder and result labels |
+
+**Verification:**
+
+```bash
+cd frontend
+grep -r "workspace" src/ --include="*.ts" --include="*.tsx" -l
+# Should return nothing except possibly test files or comments
+grep -r "workspaceStore\|WorkspaceSwitcher\|workspacesApi\|workspaceLabels" src/ -l
+# Must return nothing
+npm run build
+# Must compile clean with zero errors
+```
+
+---
+
+## Phase 4 — Label Renames (Repo-Wide Audit)
+
+### 4a. Create Flow Type Labels
+
+```typescript
+// frontend/src/constants/flowLabels.ts (NEW — replaces workspaceLabels.ts)
+
+export interface FlowTypeLabels {
+ navLabel: string;
+ singular: string;
+ plural: string;
+ newButton: string;
+ searchPlaceholder: string;
+ icon: string;
+}
+
+export const FLOW_TYPE_LABELS: Record = {
+ all: {
+ navLabel: 'All Flows',
+ singular: 'Flow',
+ plural: 'Flows',
+ newButton: '+ Create Flow',
+ searchPlaceholder: 'Search flows, sessions, tags…',
+ icon: '📦',
+ },
+ troubleshooting: {
+ navLabel: 'Troubleshooting',
+ singular: 'Flow',
+ plural: 'Flows',
+ newButton: '+ New Troubleshooting Flow',
+ searchPlaceholder: 'Search troubleshooting flows…',
+ icon: '🔧',
+ },
+ procedural: {
+ navLabel: 'Projects',
+ singular: 'Project',
+ plural: 'Projects',
+ newButton: '+ New Project',
+ searchPlaceholder: 'Search projects, runbooks…',
+ icon: '📋',
+ },
+};
+
+export function getFlowLabels(typeFilter?: string): FlowTypeLabels {
+ if (typeFilter && typeFilter in FLOW_TYPE_LABELS) {
+ return FLOW_TYPE_LABELS[typeFilter];
+ }
+ return FLOW_TYPE_LABELS.all;
+}
+```
+
+### 4b. Label Audit — Files Requiring Changes
+
+Every instance of old terminology must be found and replaced. This is a **repo-wide pass**, not a targeted find-replace.
+
+**User-facing label changes:**
+
+| Old Label | New Label | Notes |
+|-----------|-----------|-------|
+| "All Trees" | "All Flows" | Sidebar nav, page titles |
+| "Tree Editor" | "Flow Editor" | Sidebar nav |
+| "New Tree" | "Create Flow" | Buttons, menus |
+| "All Procedures" | "Projects" | Sub-nav item |
+| "New Procedure" | "New Project" | Buttons, menus |
+| "Procedure" (singular) | "Project" | Throughout UI |
+
+**Known files with old labels (non-exhaustive):**
+
+| File | Old Text | New Text |
+|------|----------|----------|
+| `Sidebar.tsx` | "All Trees", "Tree Editor" | "All Flows", "Flow Editor" |
+| `TreeLibraryPage.tsx` (line 279, 298) | "All Trees", "Tree" references | "All Flows", "Flow" |
+| `QuickStartPage.tsx` (line 150) | Workspace/procedure labels | Flow/Project labels |
+| `QuickLaunch.tsx` | "New Tree", "New Procedure" | "Create Flow", "New Project" |
+| `CommandPalette.tsx` | Search labels | Flow-based labels |
+| `TopBar.tsx` | Search placeholder | Use `getFlowLabels()` or static "Search flows, sessions, tags…" |
+
+**Catch-all verification:**
+
+```bash
+cd frontend
+# Find any remaining user-facing "Tree" or "Procedure" labels (excluding variable names and imports)
+grep -rn '".*Tree.*"' src/ --include="*.tsx" --include="*.ts" | grep -v "import\|//\|interface\|type \|treesApi\|tree_type\|treeId\|TreeNav\|TreeGrid\|TreeList\|TreeTable"
+grep -rn '".*Procedure.*"' src/ --include="*.tsx" --include="*.ts" | grep -v "import\|//\|type "
+```
+
+> **Important:** Only rename **user-facing strings** (UI text, placeholder text, toast messages, page titles). Do NOT rename: variable names (`treesApi`, `TreeListItem`), route paths (`/trees`), database columns (`tree_type`), or API endpoints (`/api/v1/trees`). Internal code can say "tree" — users never see it.
+
+### 4c. Acceptance Criteria for Label Audit
+
+- [ ] No user-visible text says "Tree" (except proper nouns or technical docs)
+- [ ] No user-visible text says "Procedure" — all say "Project"
+- [ ] Search bar placeholder says "Search flows, sessions, tags…"
+- [ ] Page title on library page says "Flow Library"
+- [ ] "+ Create Flow" button on library page (or "+ New Project" when filtered to projects)
+- [ ] Sidebar nav says "All Flows", "Flow Editor"
+- [ ] Empty states use "flow" / "project" language
+- [ ] Toast messages use "flow" / "project" language
+
+---
+
+## Phase 5 — Sidebar: Nav Sub-Items & Pinned Flows UI
+
+### 5a. Extend NavItem for Children
+
+```tsx
+// frontend/src/components/layout/NavItem.tsx — extend props
+interface NavItemProps {
+ href: string;
+ icon: LucideIcon;
+ label: string;
+ badge?: number | 'dot';
+ isActive?: boolean;
+ children?: NavSubItem[]; // NEW
+}
+
+interface NavSubItem {
+ href: string;
+ label: string;
+ count?: number;
+ isActive?: boolean;
+}
+```
+
+**Sub-item rendering:**
+- Indented `pl-9` (past parent icon)
+- No icon, just text + optional count badge
+- Font: `text-[0.8125rem] text-muted-foreground`, active: `text-foreground`
+- Active: `bg-[var(--sidebar-active)]` but without the left gradient bar (only parent gets that)
+- Sub-items always visible (not collapsible) — there are only 2
+
+### 5b. Sidebar Structure
+
+```
+── PINNED ─────────────────── (collapsible section)
+ 📧 Email Delivery Issues (click → start session or go to flow)
+ 🔒 AD Account Lockout
+ 👤 New User Onboarding
+───────────────────────────────
+📊 Dashboard
+📦 All Flows 47
+ 🔧 Troubleshooting 29
+ 📋 Projects 18
+✏️ Flow Editor
+⏱️ Sessions 4
+📄 Exports
+📚 Step Library •
+───────────────────────────────
+CATEGORIES
+ ● Networking 12
+ ● Active Directory 8
+ ● Email 11
+───────────────────────────────
+POPULAR TAGS
+ [vpn] [dns] [exchange] [onboarding]
+═══════════════════════════════
+👥 Team
+⚙️ Settings
+```
+
+**Behavior:**
+- Clicking "All Flows" → `/trees` (no type filter)
+- Clicking "Troubleshooting" → `/trees?type=troubleshooting`
+- Clicking "Projects" → `/trees?type=procedural`
+- When a sub-item is active, parent "All Flows" stays highlighted (dimmer state)
+- Badge counts update based on actual tree counts by type
+
+### 5c. Pinned Flows Section
+
+```tsx
+// frontend/src/components/sidebar/PinnedFlowsSection.tsx (NEW)
+interface PinnedFlowsSectionProps {
+ flows: PinnedFlow[];
+ onFlowClick: (treeId: string) => void;
+ onUnpin: (treeId: string) => void;
+}
+```
+
+**Behavior:**
+- Each pinned item: emoji + name (truncated with ellipsis) + hover reveals quick-start button
+- Right-click context menu: "Start Session", "Edit Flow", "Unpin from Sidebar"
+- Empty state: "⭐ Pin your most-used flows here" in `text-xs text-muted-foreground`
+- Section header has collapse chevron
+- Max 15 items shown; section scrolls internally if needed
+- Drag-to-reorder (calls `PATCH /api/v1/trees/pinned/reorder`)
+
+### 5d. Pinned Flows Frontend Wiring
+
+```
+CREATE: frontend/src/api/pinnedFlows.ts — pinTree(id), unpinTree(id), listPinned(), reorderPinned(order)
+CREATE: frontend/src/components/sidebar/PinnedFlowsSection.tsx
+MODIFY: frontend/src/types/tree.ts (or index.ts) — add is_pinned?: boolean to TreeListItem, add PinnedFlow type
+MODIFY: frontend/src/api/trees.ts — add is_pinned to list response handling
+MODIFY: frontend/src/components/layout/Sidebar.tsx — import and render PinnedFlowsSection above nav
+MODIFY: frontend/src/pages/TreeLibraryPage.tsx — add pin star to flow cards (visible on hover, filled if pinned)
+MODIFY: flow card three-dot menu — add "Pin to Sidebar" / "Unpin from Sidebar" action
+```
+
+**Pin/unpin interaction:**
+- Toast on pin: "📌 Pinned **{name}** to sidebar"
+- Toast on unpin: "Unpinned **{name}**"
+- Library flow cards show subtle star icon on hover; filled star if pinned
+- Pin star click calls API, optimistically updates UI
+
+---
+
+## Phase 6 — Library Page Cleanup
+
+### 6a. Remove Folder Sidebar Panel
+
+> **⚠️ Important:** `TreeLibraryPage.tsx` has deep folder state dependencies (lines 9, 34, 261+). This is not a simple component removal.
+
+**What to remove:**
+- `FolderSidebar` component import and rendering (the persistent left panel)
+- `FolderEditModal` import and state
+- The CSS column that gives FolderSidebar its own grid track
+- `mobileFolderOpen` state
+
+**What to KEEP:**
+- `selectedFolderId` state — keep for now, wire to a future "Filter by Folder" dropdown
+- `folders` state and `foldersApi.list()` call — keep data available
+- `FolderSidebar.tsx` and `FolderEditModal.tsx` files — do not delete, just stop rendering them
+- All folder-related backend code (models, API, database tables) — untouched
+
+**Replacement UX (deferred but noted):**
+- Future: "Filter by Folder" dropdown in the filters bar, or "Move to Folder" in the three-dot menu
+- For now: folder filtering is simply not visible in the UI. The data model and API remain intact.
+
+**Layout change:**
+- Library page becomes full-width within the main content area (no second sidebar column)
+- Grid goes from `sidebar | folders | content` → `sidebar | content`
+
+### 6b. Verification
+
+```bash
+cd frontend && npm run build
+# Must compile clean
+```
+
+Manual checks:
+- [ ] Library page is full-width (no left folder panel)
+- [ ] No JavaScript errors in browser console related to folder state
+- [ ] Category filtering from sidebar clicks still works
+- [ ] Tag filtering from sidebar clicks still works
+
+---
+
+## Complete File Manifest
+
+### Delete (11 files)
+
+| File | Reason |
+|------|--------|
+| `frontend/src/store/workspaceStore.ts` | Replaced by userPreferencesStore (sidebarCollapsed) |
+| `frontend/src/components/workspace/WorkspaceSwitcher.tsx` | Feature removed |
+| `frontend/src/components/workspace/WorkspaceCreateModal.tsx` | Feature removed |
+| `frontend/src/constants/workspaceLabels.ts` | Replaced by flowLabels.ts |
+| `frontend/src/types/workspace.ts` | Type no longer needed |
+| `frontend/src/api/workspaces.ts` | API removed |
+| `backend/app/api/endpoints/workspaces.py` | API removed |
+| `backend/app/models/workspace.py` | Model removed |
+| `backend/app/schemas/workspace.py` | Schema removed (if exists) |
+| `docs/mockups/resolutionflow-workspaces-mockup.html` | Outdated mockup |
+
+### Move (2 files)
+
+| From | To |
+|------|----|
+| `frontend/src/components/workspace/CategoryList.tsx` | `frontend/src/components/sidebar/CategoryList.tsx` |
+| `frontend/src/components/workspace/TagCloud.tsx` | `frontend/src/components/sidebar/TagCloud.tsx` |
+
+Then delete the empty `frontend/src/components/workspace/` directory.
+
+### Create (6+ files)
+
+| File | Purpose |
+|------|---------|
+| `backend/alembic/versions/0XX_remove_workspace_system.py` | Drop workspace tables/columns |
+| `backend/alembic/versions/0XX_add_user_pinned_trees.py` | New pinned flows table |
+| `backend/app/models/user_pinned_tree.py` | UserPinnedTree SQLAlchemy model |
+| `frontend/src/constants/flowLabels.ts` | Flow type label constants |
+| `frontend/src/api/pinnedFlows.ts` | Pin/unpin API client |
+| `frontend/src/components/sidebar/PinnedFlowsSection.tsx` | Pinned flows sidebar component |
+
+### Modify (14 files — key changes only)
+
+| File | Changes |
+|------|---------|
+| `backend/app/api/router.py` | Remove workspace routes, add pin routes |
+| `backend/app/models/__init__.py` | Remove Workspace import (line 23, 55), add UserPinnedTree |
+| `backend/app/models/account.py` | Remove `workspaces` relationship (line 18, 49) |
+| `backend/app/models/category.py` | Remove `workspace_id` column (line 40) if present |
+| `backend/app/models/tree.py` | Remove `workspace_id` column/relationship if present |
+| `backend/app/api/endpoints/trees.py` | Add pin/unpin/list-pinned/reorder endpoints |
+| `backend/app/schemas/tree.py` | Add PinnedFlow schema, add `is_pinned` to tree list |
+| `frontend/src/store/userPreferencesStore.ts` | Absorb `sidebarCollapsed` + `toggleSidebar` |
+| `frontend/src/components/layout/AppLayout.tsx` | Replace workspaceStore with userPreferencesStore |
+| `frontend/src/components/layout/TopBar.tsx` | Replace workspaceStore, static search labels |
+| `frontend/src/components/layout/Sidebar.tsx` | Remove workspace switcher, add pinned section + nav sub-items |
+| `frontend/src/components/layout/NavItem.tsx` | Add `children` sub-item support |
+| `frontend/src/pages/TreeLibraryPage.tsx` | Remove FolderSidebar, update labels, add pin star |
+| `frontend/src/components/layout/QuickLaunch.tsx` | Update action labels |
+| `frontend/src/components/layout/CommandPalette.tsx` | Update search labels |
+| `frontend/src/pages/QuickStartPage.tsx` | Update workspace/procedure labels (line 150) |
+| `frontend/src/types/index.ts` | Remove Workspace export (line 11, 14), add PinnedFlow |
+
+---
+
+## Verification Checklist
+
+### Automated
+
+```bash
+# Backend
+cd backend
+alembic upgrade head # Migration applies clean
+pytest --override-ini="addopts=" # All tests pass
+grep -r "workspace" app/ --include="*.py" -l # No active workspace refs
+
+# Frontend
+cd frontend
+npm run build # Compiles clean, zero errors
+grep -r "workspaceStore\|WorkspaceSwitcher\|workspacesApi\|workspaceLabels" src/ -l
+ # Returns nothing
+```
+
+### Manual UI Checks
+
+- [ ] Sidebar shows "All Flows" with "Troubleshooting" and "Projects" sub-items — no workspace switcher
+- [ ] Clicking "Troubleshooting" filters library to `?type=troubleshooting`
+- [ ] Clicking "Projects" filters library to `?type=procedural`
+- [ ] Clicking "All Flows" removes type filter
+- [ ] Sub-item counts reflect actual flow counts per type
+- [ ] Pin a flow from the library card three-dot menu → appears in sidebar "PINNED" section
+- [ ] Unpin a flow → disappears from sidebar
+- [ ] Pinned flow click navigates to that flow
+- [ ] Library page is full-width (no folder sidebar panel)
+- [ ] Search bar is centered in top bar
+- [ ] Keyboard shortcut shows "Ctrl+K" on Windows, "⌘K" on Mac
+- [ ] All user-visible labels say "Flow" / "Project", never "Tree" / "Procedure"
+- [ ] Empty states use correct terminology
+- [ ] Toast messages use correct terminology
+- [ ] Command palette search works with new labels
+- [ ] Quick Launch actions show correct labels
diff --git a/backend/alembic/versions/037_add_user_pinned_trees.py b/backend/alembic/versions/037_add_user_pinned_trees.py
new file mode 100644
index 00000000..27e498c8
--- /dev/null
+++ b/backend/alembic/versions/037_add_user_pinned_trees.py
@@ -0,0 +1,40 @@
+"""Add user_pinned_trees table
+
+Revision ID: 037
+Revises: 036
+Create Date: 2026-02-15
+
+Adds pinned flows feature for sidebar favorites.
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import UUID
+
+
+# revision identifiers, used by Alembic.
+revision: str = '037'
+down_revision: Union[str, None] = '036'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ op.create_table(
+ 'user_pinned_trees',
+ sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')),
+ sa.Column('user_id', UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
+ sa.Column('tree_id', UUID(as_uuid=True), sa.ForeignKey('trees.id', ondelete='CASCADE'), nullable=False),
+ sa.Column('display_order', sa.Integer(), nullable=False, server_default='0'),
+ sa.Column('pinned_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()')),
+ sa.UniqueConstraint('user_id', 'tree_id', name='uq_user_pinned_tree'),
+ )
+ op.create_index('idx_user_pinned_trees_user', 'user_pinned_trees', ['user_id'])
+ op.create_index('idx_user_pinned_trees_tree', 'user_pinned_trees', ['tree_id'])
+
+
+def downgrade() -> None:
+ op.drop_index('idx_user_pinned_trees_tree', 'user_pinned_trees')
+ op.drop_index('idx_user_pinned_trees_user', 'user_pinned_trees')
+ op.drop_table('user_pinned_trees')
diff --git a/backend/alembic/versions/038_remove_workspace_system.py b/backend/alembic/versions/038_remove_workspace_system.py
new file mode 100644
index 00000000..a4a7fcc3
--- /dev/null
+++ b/backend/alembic/versions/038_remove_workspace_system.py
@@ -0,0 +1,69 @@
+"""Remove workspace system
+
+Revision ID: 038
+Revises: 037
+Create Date: 2026-02-15
+
+Drops workspace tables and columns. Keeps tree_categories.color.
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import UUID
+
+
+# revision identifiers, used by Alembic.
+revision: str = '038'
+down_revision: Union[str, None] = '037'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # 1. Drop workspace_id FK and column from trees
+ op.drop_index('ix_trees_workspace_id', 'trees')
+ op.drop_constraint('fk_trees_workspace_id', 'trees', type_='foreignkey')
+ op.drop_column('trees', 'workspace_id')
+
+ # 2. Drop workspace_id FK and column from tree_categories
+ op.drop_index('ix_tree_categories_workspace_id', 'tree_categories')
+ op.drop_constraint('fk_tree_categories_workspace_id', 'tree_categories', type_='foreignkey')
+ op.drop_column('tree_categories', 'workspace_id')
+
+ # 3. Drop workspaces table
+ op.drop_index('ix_workspaces_account_id', 'workspaces')
+ op.drop_index('ix_workspaces_slug', 'workspaces')
+ op.drop_table('workspaces')
+
+ # DO NOT drop tree_categories.color — we still use it
+
+
+def downgrade() -> None:
+ # Recreate workspaces table
+ op.create_table(
+ 'workspaces',
+ sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')),
+ sa.Column('name', sa.String(100), nullable=False),
+ sa.Column('slug', sa.String(100), nullable=False),
+ sa.Column('description', sa.Text(), nullable=True),
+ sa.Column('icon', sa.String(10), nullable=True),
+ sa.Column('accent_color', sa.String(7), nullable=True),
+ sa.Column('account_id', UUID(as_uuid=True), sa.ForeignKey('accounts.id', ondelete='CASCADE'), nullable=False),
+ sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false'),
+ sa.Column('sort_order', sa.Integer(), nullable=False, server_default='0'),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()')),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()')),
+ sa.UniqueConstraint('slug', 'account_id', name='uq_workspaces_slug_account'),
+ )
+ op.create_index('ix_workspaces_slug', 'workspaces', ['slug'])
+ op.create_index('ix_workspaces_account_id', 'workspaces', ['account_id'])
+
+ # Re-add workspace_id columns
+ op.add_column('trees', sa.Column('workspace_id', UUID(as_uuid=True), nullable=True))
+ op.create_foreign_key('fk_trees_workspace_id', 'trees', 'workspaces', ['workspace_id'], ['id'], ondelete='SET NULL')
+ op.create_index('ix_trees_workspace_id', 'trees', ['workspace_id'])
+
+ op.add_column('tree_categories', sa.Column('workspace_id', UUID(as_uuid=True), nullable=True))
+ op.create_foreign_key('fk_tree_categories_workspace_id', 'tree_categories', 'workspaces', ['workspace_id'], ['id'], ondelete='SET NULL')
+ op.create_index('ix_tree_categories_workspace_id', 'tree_categories', ['workspace_id'])
diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py
index 4f943fcb..9a54e36e 100644
--- a/backend/app/api/endpoints/trees.py
+++ b/backend/app/api/endpoints/trees.py
@@ -17,8 +17,10 @@ from app.models.folder import UserFolder, user_folder_trees
from app.schemas.tree import (
TreeCreate, TreeUpdate, TreeResponse, TreeListResponse, CategoryInfo,
ForkCreate, ForkInfo, TreeShareCreate, TreeShareResponse,
- TreeVisibilityUpdate, SharedTreeResponse, TreeValidationResponse, ValidationError
+ TreeVisibilityUpdate, SharedTreeResponse, TreeValidationResponse, ValidationError,
+ PinnedFlowResponse, PinnedFlowsListResponse, PinnedFlowReorderRequest
)
+from app.models.user_pinned_tree import UserPinnedTree
from app.api.deps import get_current_active_user, require_engineer_or_admin, require_admin
from app.core.permissions import can_edit_tree, can_access_tree
from app.core.filters import build_tree_access_filter
@@ -1030,3 +1032,185 @@ async def check_tree_can_publish(
can_publish=can_publish,
errors=[ValidationError(**error) for error in validation_errors]
)
+
+
+# --- Pinned Flows Endpoints ---
+
+MAX_PINNED_FLOWS = 15
+
+
+@router.get("/pinned", response_model=PinnedFlowsListResponse)
+async def list_pinned_flows(
+ db: Annotated[AsyncSession, Depends(get_db)],
+ current_user: Annotated[User, Depends(get_current_active_user)]
+):
+ """List user's pinned flows, ordered by display_order."""
+ result = await db.execute(
+ select(UserPinnedTree, Tree)
+ .join(Tree, UserPinnedTree.tree_id == Tree.id)
+ .options(selectinload(Tree.category_rel))
+ .where(
+ UserPinnedTree.user_id == current_user.id,
+ Tree.is_active == True,
+ Tree.deleted_at.is_(None)
+ )
+ .order_by(UserPinnedTree.display_order, UserPinnedTree.pinned_at)
+ )
+ rows = result.all()
+
+ items = []
+ for pin, tree in rows:
+ items.append(PinnedFlowResponse(
+ id=pin.id,
+ tree_id=tree.id,
+ tree_name=tree.name,
+ tree_type=tree.tree_type,
+ category_emoji=None,
+ category_name=tree.category_rel.name if tree.category_rel else None,
+ pinned_at=pin.pinned_at,
+ display_order=pin.display_order,
+ ))
+
+ return PinnedFlowsListResponse(items=items, count=len(items))
+
+
+@router.post("/{tree_id}/pin", response_model=PinnedFlowResponse)
+async def pin_flow(
+ tree_id: UUID,
+ db: Annotated[AsyncSession, Depends(get_db)],
+ current_user: Annotated[User, Depends(get_current_active_user)]
+):
+ """Pin a flow to the user's sidebar."""
+ # Check tree exists and user can access it
+ tree_result = await db.execute(
+ select(Tree)
+ .options(selectinload(Tree.category_rel))
+ .where(Tree.id == tree_id, Tree.is_active == True)
+ )
+ tree = tree_result.scalar_one_or_none()
+ if not tree:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tree not found")
+ if not can_access_tree(current_user, tree):
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have access to this tree")
+
+ # Check if already pinned (idempotent)
+ existing = await db.execute(
+ select(UserPinnedTree).where(
+ UserPinnedTree.user_id == current_user.id,
+ UserPinnedTree.tree_id == tree_id
+ )
+ )
+ pin = existing.scalar_one_or_none()
+ if pin:
+ return PinnedFlowResponse(
+ id=pin.id,
+ tree_id=tree.id,
+ tree_name=tree.name,
+ tree_type=tree.tree_type,
+ category_emoji=None,
+ category_name=tree.category_rel.name if tree.category_rel else None,
+ pinned_at=pin.pinned_at,
+ display_order=pin.display_order,
+ )
+
+ # Check max pins
+ count_result = await db.execute(
+ select(func.count(UserPinnedTree.id)).where(
+ UserPinnedTree.user_id == current_user.id
+ )
+ )
+ count = count_result.scalar() or 0
+ if count >= MAX_PINNED_FLOWS:
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail=f"Maximum of {MAX_PINNED_FLOWS} pinned flows reached"
+ )
+
+ # Create pin
+ pin = UserPinnedTree(
+ user_id=current_user.id,
+ tree_id=tree_id,
+ display_order=count, # Append at end
+ )
+ db.add(pin)
+ await db.commit()
+ await db.refresh(pin)
+
+ return PinnedFlowResponse(
+ id=pin.id,
+ tree_id=tree.id,
+ tree_name=tree.name,
+ tree_type=tree.tree_type,
+ category_emoji=None,
+ category_name=tree.category_rel.name if tree.category_rel else None,
+ pinned_at=pin.pinned_at,
+ display_order=pin.display_order,
+ )
+
+
+@router.delete("/{tree_id}/pin")
+async def unpin_flow(
+ tree_id: UUID,
+ db: Annotated[AsyncSession, Depends(get_db)],
+ current_user: Annotated[User, Depends(get_current_active_user)]
+):
+ """Unpin a flow from the user's sidebar."""
+ result = await db.execute(
+ select(UserPinnedTree).where(
+ UserPinnedTree.user_id == current_user.id,
+ UserPinnedTree.tree_id == tree_id
+ )
+ )
+ pin = result.scalar_one_or_none()
+ if pin:
+ await db.delete(pin)
+ await db.commit()
+ return {"success": True}
+
+
+@router.patch("/pinned/reorder", response_model=PinnedFlowsListResponse)
+async def reorder_pinned_flows(
+ reorder_data: PinnedFlowReorderRequest,
+ db: Annotated[AsyncSession, Depends(get_db)],
+ current_user: Annotated[User, Depends(get_current_active_user)]
+):
+ """Update display_order for all pinned flows."""
+ for item in reorder_data.order:
+ await db.execute(
+ update(UserPinnedTree)
+ .where(
+ UserPinnedTree.user_id == current_user.id,
+ UserPinnedTree.tree_id == item.tree_id
+ )
+ .values(display_order=item.display_order)
+ )
+ await db.commit()
+
+ # Return updated list
+ result = await db.execute(
+ select(UserPinnedTree, Tree)
+ .join(Tree, UserPinnedTree.tree_id == Tree.id)
+ .options(selectinload(Tree.category_rel))
+ .where(
+ UserPinnedTree.user_id == current_user.id,
+ Tree.is_active == True,
+ Tree.deleted_at.is_(None)
+ )
+ .order_by(UserPinnedTree.display_order, UserPinnedTree.pinned_at)
+ )
+ rows = result.all()
+
+ items = []
+ for pin, tree in rows:
+ items.append(PinnedFlowResponse(
+ id=pin.id,
+ tree_id=tree.id,
+ tree_name=tree.name,
+ tree_type=tree.tree_type,
+ category_emoji=None,
+ category_name=tree.category_rel.name if tree.category_rel else None,
+ pinned_at=pin.pinned_at,
+ display_order=pin.display_order,
+ ))
+
+ return PinnedFlowsListResponse(items=items, count=len(items))
diff --git a/backend/app/api/endpoints/workspaces.py b/backend/app/api/endpoints/workspaces.py
deleted file mode 100644
index d4a5405e..00000000
--- a/backend/app/api/endpoints/workspaces.py
+++ /dev/null
@@ -1,154 +0,0 @@
-from typing import Annotated
-from uuid import UUID
-from fastapi import APIRouter, Depends, HTTPException, status
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, func
-
-from app.core.database import get_db
-from app.models.workspace import Workspace
-from app.models.tree import Tree
-from app.models.user import User
-from app.schemas.workspace import WorkspaceCreate, WorkspaceUpdate, WorkspaceResponse
-from app.api.deps import get_current_active_user
-
-router = APIRouter(prefix="/workspaces", tags=["workspaces"])
-
-
-@router.get("", response_model=list[WorkspaceResponse])
-async def list_workspaces(
- db: Annotated[AsyncSession, Depends(get_db)],
- current_user: Annotated[User, Depends(get_current_active_user)],
-):
- """List all workspaces for the user's account."""
- if not current_user.account_id:
- return []
-
- # Get workspaces with tree counts
- query = (
- select(
- Workspace,
- func.count(Tree.id).label("tree_count")
- )
- .outerjoin(Tree, (Tree.workspace_id == Workspace.id) & (Tree.deleted_at.is_(None)))
- .where(Workspace.account_id == current_user.account_id)
- .group_by(Workspace.id)
- .order_by(Workspace.sort_order, Workspace.name)
- )
- result = await db.execute(query)
- rows = result.all()
-
- return [
- WorkspaceResponse(
- **{c.key: getattr(ws, c.key) for c in Workspace.__table__.columns},
- tree_count=tree_count
- )
- for ws, tree_count in rows
- ]
-
-
-@router.post("", response_model=WorkspaceResponse, status_code=status.HTTP_201_CREATED)
-async def create_workspace(
- data: WorkspaceCreate,
- db: Annotated[AsyncSession, Depends(get_db)],
- current_user: Annotated[User, Depends(get_current_active_user)],
-):
- """Create a new workspace."""
- if not current_user.account_id:
- raise HTTPException(status_code=400, detail="No account found")
-
- # Check slug uniqueness within account
- existing = await db.execute(
- select(Workspace).where(
- Workspace.account_id == current_user.account_id,
- Workspace.slug == data.slug
- )
- )
- if existing.scalar_one_or_none():
- raise HTTPException(status_code=409, detail="Workspace slug already exists")
-
- workspace = Workspace(
- name=data.name,
- slug=data.slug,
- description=data.description,
- icon=data.icon,
- accent_color=data.accent_color,
- account_id=current_user.account_id,
- sort_order=data.sort_order,
- )
- db.add(workspace)
- await db.flush()
- await db.commit()
-
- return WorkspaceResponse(
- **{c.key: getattr(workspace, c.key) for c in Workspace.__table__.columns},
- tree_count=0
- )
-
-
-@router.patch("/{workspace_id}", response_model=WorkspaceResponse)
-async def update_workspace(
- workspace_id: UUID,
- data: WorkspaceUpdate,
- db: Annotated[AsyncSession, Depends(get_db)],
- current_user: Annotated[User, Depends(get_current_active_user)],
-):
- """Update a workspace."""
- workspace = await db.get(Workspace, workspace_id)
- if not workspace or workspace.account_id != current_user.account_id:
- raise HTTPException(status_code=404, detail="Workspace not found")
-
- update_data = data.model_dump(exclude_unset=True)
- if "slug" in update_data:
- existing = await db.execute(
- select(Workspace).where(
- Workspace.account_id == current_user.account_id,
- Workspace.slug == update_data["slug"],
- Workspace.id != workspace_id
- )
- )
- if existing.scalar_one_or_none():
- raise HTTPException(status_code=409, detail="Workspace slug already exists")
-
- for key, value in update_data.items():
- setattr(workspace, key, value)
-
- await db.commit()
-
- # Get tree count
- count_result = await db.execute(
- select(func.count(Tree.id)).where(
- Tree.workspace_id == workspace_id,
- Tree.deleted_at.is_(None)
- )
- )
- tree_count = count_result.scalar() or 0
-
- return WorkspaceResponse(
- **{c.key: getattr(workspace, c.key) for c in Workspace.__table__.columns},
- tree_count=tree_count
- )
-
-
-@router.delete("/{workspace_id}", status_code=status.HTTP_204_NO_CONTENT)
-async def delete_workspace(
- workspace_id: UUID,
- db: Annotated[AsyncSession, Depends(get_db)],
- current_user: Annotated[User, Depends(get_current_active_user)],
-):
- """Delete a workspace. Trees are unassigned, not deleted."""
- workspace = await db.get(Workspace, workspace_id)
- if not workspace or workspace.account_id != current_user.account_id:
- raise HTTPException(status_code=404, detail="Workspace not found")
-
- if workspace.is_default:
- raise HTTPException(status_code=400, detail="Cannot delete the default workspace")
-
- # Unassign trees (set workspace_id to NULL)
- await db.execute(
- Tree.__table__.update()
- .where(Tree.workspace_id == workspace_id)
- .values(workspace_id=None)
- )
-
- await db.delete(workspace)
- await db.commit()
diff --git a/backend/app/api/router.py b/backend/app/api/router.py
index 5e9db764..5537529f 100644
--- a/backend/app/api/router.py
+++ b/backend/app/api/router.py
@@ -1,5 +1,5 @@
from fastapi import APIRouter
-from app.api.endpoints import auth, trees, sessions, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown, workspaces
+from app.api.endpoints import auth, trees, sessions, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown
from app.api.endpoints import admin_dashboard, admin_audit, admin_plan_limits, admin_feature_flags, admin_settings, admin_categories
api_router = APIRouter()
@@ -25,4 +25,3 @@ api_router.include_router(webhooks.router)
api_router.include_router(shares.router)
api_router.include_router(shared.router) # Public endpoints (no auth)
api_router.include_router(tree_markdown.router)
-api_router.include_router(workspaces.router)
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index e677771a..7ba8536d 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -20,7 +20,7 @@ from .session_share import SessionShare, SessionShareView
from .account_limit_override import AccountLimitOverride
from .feature_flag import FeatureFlag, PlanFeatureDefault, AccountFeatureOverride
from .platform_setting import PlatformSetting
-from .workspace import Workspace
+from .user_pinned_tree import UserPinnedTree
__all__ = [
"User",
@@ -52,5 +52,5 @@ __all__ = [
"PlanFeatureDefault",
"AccountFeatureOverride",
"PlatformSetting",
- "Workspace",
+ "UserPinnedTree",
]
diff --git a/backend/app/models/account.py b/backend/app/models/account.py
index 272b8f9a..f40d5041 100644
--- a/backend/app/models/account.py
+++ b/backend/app/models/account.py
@@ -15,7 +15,6 @@ if TYPE_CHECKING:
from app.models.step_category import StepCategory
from app.models.step_library import StepLibrary
from app.models.account_limit_override import AccountLimitOverride
- from app.models.workspace import Workspace
class Account(Base):
@@ -46,4 +45,3 @@ class Account(Base):
step_categories: Mapped[list["StepCategory"]] = relationship("StepCategory", foreign_keys="[StepCategory.account_id]", back_populates="account")
step_library: Mapped[list["StepLibrary"]] = relationship("StepLibrary", foreign_keys="[StepLibrary.account_id]", back_populates="account")
limit_override: Mapped[Optional["AccountLimitOverride"]] = relationship("AccountLimitOverride", back_populates="account", uselist=False)
- workspaces: Mapped[list["Workspace"]] = relationship("Workspace", back_populates="account")
diff --git a/backend/app/models/category.py b/backend/app/models/category.py
index 8923ecb0..eb3a56a6 100644
--- a/backend/app/models/category.py
+++ b/backend/app/models/category.py
@@ -11,7 +11,6 @@ if TYPE_CHECKING:
from app.models.team import Team
from app.models.account import Account
from app.models.user import User
- from app.models.workspace import Workspace
class TreeCategory(Base):
@@ -52,13 +51,6 @@ class TreeCategory(Base):
String(7), nullable=True, default='#3b82f6',
comment="Hex color for category dot indicator"
)
- workspace_id: Mapped[Optional[uuid.UUID]] = mapped_column(
- UUID(as_uuid=True),
- ForeignKey("workspaces.id", ondelete="SET NULL"),
- nullable=True,
- index=True,
- comment="Workspace this category belongs to"
- )
created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"),
@@ -79,7 +71,6 @@ class TreeCategory(Base):
account: Mapped[Optional["Account"]] = relationship("Account", foreign_keys=[account_id], back_populates="categories")
creator: Mapped[Optional["User"]] = relationship("User", foreign_keys=[created_by])
trees: Mapped[list["Tree"]] = relationship("Tree", back_populates="category_rel")
- workspace: Mapped[Optional["Workspace"]] = relationship("Workspace", back_populates="categories")
@property
def is_global(self) -> bool:
diff --git a/backend/app/models/tree.py b/backend/app/models/tree.py
index a8dcd51a..49486a21 100644
--- a/backend/app/models/tree.py
+++ b/backend/app/models/tree.py
@@ -15,7 +15,6 @@ if TYPE_CHECKING:
from app.models.tag import TreeTag
from app.models.folder import UserFolder
from app.models.tree_share import TreeShare
- from app.models.workspace import Workspace
class Tree(Base):
@@ -121,14 +120,6 @@ class Tree(Base):
onupdate=lambda: datetime.now(timezone.utc)
)
usage_count: Mapped[int] = mapped_column(Integer, default=0)
- workspace_id: Mapped[Optional[uuid.UUID]] = mapped_column(
- UUID(as_uuid=True),
- ForeignKey("workspaces.id", ondelete="SET NULL"),
- nullable=True,
- index=True,
- comment="Workspace this tree belongs to (organizational context)"
- )
-
# Fork tracking
parent_tree_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
@@ -192,9 +183,7 @@ class Tree(Base):
cascade="all, delete-orphan"
)
- workspace: Mapped[Optional["Workspace"]] = relationship("Workspace", back_populates="trees")
-
- # New organization relationships
+ # Organization relationships
category_rel: Mapped[Optional["TreeCategory"]] = relationship("TreeCategory", back_populates="trees")
tags: Mapped[list["TreeTag"]] = relationship(
"TreeTag",
diff --git a/backend/app/models/user_pinned_tree.py b/backend/app/models/user_pinned_tree.py
new file mode 100644
index 00000000..c27edd08
--- /dev/null
+++ b/backend/app/models/user_pinned_tree.py
@@ -0,0 +1,37 @@
+import uuid
+from datetime import datetime, timezone
+from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint
+from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy.dialects.postgresql import UUID
+from app.core.database import Base
+
+
+class UserPinnedTree(Base):
+ """Tracks which trees a user has pinned to their sidebar."""
+ __tablename__ = "user_pinned_trees"
+ __table_args__ = (
+ UniqueConstraint('user_id', 'tree_id', name='uq_user_pinned_tree'),
+ )
+
+ id: Mapped[uuid.UUID] = mapped_column(
+ UUID(as_uuid=True),
+ primary_key=True,
+ default=uuid.uuid4
+ )
+ user_id: Mapped[uuid.UUID] = mapped_column(
+ UUID(as_uuid=True),
+ ForeignKey("users.id", ondelete="CASCADE"),
+ nullable=False,
+ index=True
+ )
+ tree_id: Mapped[uuid.UUID] = mapped_column(
+ UUID(as_uuid=True),
+ ForeignKey("trees.id", ondelete="CASCADE"),
+ nullable=False,
+ index=True
+ )
+ display_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
+ pinned_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True),
+ default=lambda: datetime.now(timezone.utc)
+ )
diff --git a/backend/app/models/workspace.py b/backend/app/models/workspace.py
deleted file mode 100644
index 7e11c4bd..00000000
--- a/backend/app/models/workspace.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import uuid
-from datetime import datetime, timezone
-from typing import Optional, TYPE_CHECKING
-from sqlalchemy import String, Text, DateTime, ForeignKey, Boolean, Integer, UniqueConstraint
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from sqlalchemy.dialects.postgresql import UUID
-from app.core.database import Base
-
-if TYPE_CHECKING:
- from app.models.account import Account
- from app.models.tree import Tree
- from app.models.category import TreeCategory
-
-
-class Workspace(Base):
- """Workspaces are the top-level organizational context for trees/flows.
-
- They sit above the folder system — a workspace scopes which trees/flows
- are visible, while folders remain for personal organization within.
- """
- __tablename__ = "workspaces"
- __table_args__ = (
- UniqueConstraint('slug', 'account_id', name='uq_workspaces_slug_account'),
- )
-
- id: Mapped[uuid.UUID] = mapped_column(
- UUID(as_uuid=True),
- primary_key=True,
- default=uuid.uuid4
- )
- name: Mapped[str] = mapped_column(String(100), nullable=False)
- slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
- description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
- icon: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
- accent_color: Mapped[Optional[str]] = mapped_column(String(7), nullable=True)
- account_id: Mapped[uuid.UUID] = mapped_column(
- UUID(as_uuid=True),
- ForeignKey("accounts.id", ondelete="CASCADE"),
- nullable=False,
- index=True
- )
- is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
- sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
- created_at: Mapped[datetime] = mapped_column(
- DateTime(timezone=True),
- default=lambda: datetime.now(timezone.utc)
- )
- updated_at: Mapped[datetime] = mapped_column(
- DateTime(timezone=True),
- default=lambda: datetime.now(timezone.utc),
- onupdate=lambda: datetime.now(timezone.utc)
- )
-
- # Relationships
- account: Mapped["Account"] = relationship("Account", back_populates="workspaces")
- trees: Mapped[list["Tree"]] = relationship("Tree", back_populates="workspace")
- categories: Mapped[list["TreeCategory"]] = relationship("TreeCategory", back_populates="workspace")
diff --git a/backend/app/schemas/category.py b/backend/app/schemas/category.py
index 11475d23..3a332cf1 100644
--- a/backend/app/schemas/category.py
+++ b/backend/app/schemas/category.py
@@ -37,7 +37,6 @@ class CategoryResponse(CategoryBase):
display_order: int
is_active: bool
color: Optional[str] = None
- workspace_id: Optional[UUID] = None
created_at: datetime
updated_at: datetime
tree_count: int = 0 # Computed field
@@ -55,7 +54,6 @@ class CategoryListResponse(BaseModel):
display_order: int
is_active: bool
color: Optional[str] = None
- workspace_id: Optional[UUID] = None
tree_count: int = 0
class Config:
diff --git a/backend/app/schemas/tree.py b/backend/app/schemas/tree.py
index 0da6c79b..236b6290 100644
--- a/backend/app/schemas/tree.py
+++ b/backend/app/schemas/tree.py
@@ -245,3 +245,37 @@ class TreeValidationResponse(BaseModel):
"""Response for tree validation endpoint."""
can_publish: bool
errors: list[ValidationError] = []
+
+
+# --- Pinned Flows Schemas ---
+
+class PinnedFlowResponse(BaseModel):
+ """A pinned flow in the sidebar."""
+ id: UUID
+ tree_id: UUID
+ tree_name: str
+ tree_type: str
+ category_emoji: Optional[str] = None
+ category_name: Optional[str] = None
+ pinned_at: datetime
+ display_order: int
+
+ class Config:
+ from_attributes = True
+
+
+class PinnedFlowsListResponse(BaseModel):
+ """List of pinned flows."""
+ items: list[PinnedFlowResponse]
+ count: int
+
+
+class PinnedFlowReorderItem(BaseModel):
+ """Single item in a reorder request."""
+ tree_id: UUID
+ display_order: int
+
+
+class PinnedFlowReorderRequest(BaseModel):
+ """Request to reorder pinned flows."""
+ order: list[PinnedFlowReorderItem]
diff --git a/backend/app/schemas/workspace.py b/backend/app/schemas/workspace.py
deleted file mode 100644
index a32aea26..00000000
--- a/backend/app/schemas/workspace.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import uuid
-from datetime import datetime
-from typing import Optional
-from pydantic import BaseModel, Field
-
-
-class WorkspaceCreate(BaseModel):
- name: str = Field(..., min_length=1, max_length=100)
- slug: str = Field(..., min_length=1, max_length=100, pattern=r'^[a-z0-9-]+$')
- description: Optional[str] = None
- icon: Optional[str] = Field(None, max_length=10)
- accent_color: Optional[str] = Field(None, pattern=r'^#[0-9a-fA-F]{6}$')
- sort_order: int = 0
-
-
-class WorkspaceUpdate(BaseModel):
- name: Optional[str] = Field(None, min_length=1, max_length=100)
- slug: Optional[str] = Field(None, min_length=1, max_length=100, pattern=r'^[a-z0-9-]+$')
- description: Optional[str] = None
- icon: Optional[str] = Field(None, max_length=10)
- accent_color: Optional[str] = Field(None, pattern=r'^#[0-9a-fA-F]{6}$')
- sort_order: Optional[int] = None
-
-
-class WorkspaceResponse(BaseModel):
- id: uuid.UUID
- name: str
- slug: str
- description: Optional[str] = None
- icon: Optional[str] = None
- accent_color: Optional[str] = None
- account_id: uuid.UUID
- is_default: bool
- sort_order: int
- tree_count: int = 0
- created_at: datetime
- updated_at: datetime
-
- model_config = {"from_attributes": True}
diff --git a/docs/mockups/resolutionflow-workspaces-mockup.html b/docs/mockups/resolutionflow-workspaces-mockup.html
deleted file mode 100644
index 00d71e66..00000000
--- a/docs/mockups/resolutionflow-workspaces-mockup.html
+++ /dev/null
@@ -1,1107 +0,0 @@
-
-
-
-
-
-ResolutionFlow — Adaptive Workspaces Mockup
-
-
-
-
-
-
-
-
-
-
-
- ResolutionFlow
-
-
-
-
- ⌘K
-
-
-
-
-
MC
-
-
-
-
-
-
-
-
-
-
-
-
Troubleshooting Dashboard
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Click the workspace switcher in the sidebar to see it adapt
-
-
-
ResolutionFlow — Adaptive Workspaces Mockup
-
-
-
-
-
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
index fdea0cc7..21daddd1 100644
--- a/frontend/src/api/index.ts
+++ b/frontend/src/api/index.ts
@@ -11,4 +11,4 @@ export { default as stepCategoriesApi } from './stepCategories'
export { default as accountsApi } from './accounts'
export { default as adminApi } from './admin'
export { treeMarkdownApi } from './treeMarkdown'
-export { default as workspacesApi } from './workspaces'
+export { default as pinnedFlowsApi } from './pinnedFlows'
diff --git a/frontend/src/api/pinnedFlows.ts b/frontend/src/api/pinnedFlows.ts
new file mode 100644
index 00000000..41b3754a
--- /dev/null
+++ b/frontend/src/api/pinnedFlows.ts
@@ -0,0 +1,40 @@
+import { apiClient } from './client'
+
+export interface PinnedFlow {
+ id: string
+ tree_id: string
+ tree_name: string
+ tree_type: 'troubleshooting' | 'procedural'
+ category_emoji?: string
+ category_name?: string
+ pinned_at: string
+ display_order: number
+}
+
+export interface PinnedFlowsResponse {
+ items: PinnedFlow[]
+ count: number
+}
+
+export const pinnedFlowsApi = {
+ list: async (): Promise => {
+ const { data } = await apiClient.get('/trees/pinned')
+ return data
+ },
+
+ pin: async (treeId: string): Promise => {
+ const { data } = await apiClient.post(`/trees/${treeId}/pin`)
+ return data
+ },
+
+ unpin: async (treeId: string): Promise => {
+ await apiClient.delete(`/trees/${treeId}/pin`)
+ },
+
+ reorder: async (order: { tree_id: string; display_order: number }[]): Promise => {
+ const { data } = await apiClient.patch('/trees/pinned/reorder', { order })
+ return data
+ },
+}
+
+export default pinnedFlowsApi
diff --git a/frontend/src/api/workspaces.ts b/frontend/src/api/workspaces.ts
deleted file mode 100644
index 75e09e38..00000000
--- a/frontend/src/api/workspaces.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { apiClient } from './client'
-import type { Workspace, WorkspaceCreate, WorkspaceUpdate } from '@/types'
-
-export const workspacesApi = {
- list: async (): Promise => {
- const { data } = await apiClient.get('/workspaces')
- return data
- },
-
- create: async (payload: WorkspaceCreate): Promise => {
- const { data } = await apiClient.post('/workspaces', payload)
- return data
- },
-
- update: async (id: string, payload: WorkspaceUpdate): Promise => {
- const { data } = await apiClient.patch(`/workspaces/${id}`, payload)
- return data
- },
-
- delete: async (id: string): Promise => {
- await apiClient.delete(`/workspaces/${id}`)
- },
-}
-
-export default workspacesApi
diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx
index 78253560..9e6671bf 100644
--- a/frontend/src/components/layout/AppLayout.tsx
+++ b/frontend/src/components/layout/AppLayout.tsx
@@ -3,7 +3,7 @@ import { Outlet, useLocation, useNavigate, Link } from 'react-router-dom'
import { Menu, X, LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, Users, Settings, LogOut, Shield } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
-import { useWorkspaceStore } from '@/store/workspaceStore'
+import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { BrandLogo } from '@/components/common/BrandLogo'
import { TopBar } from './TopBar'
import { Sidebar } from './Sidebar'
@@ -14,15 +14,9 @@ export function AppLayout() {
const navigate = useNavigate()
const { user, logout } = useAuthStore()
const { effectiveRole } = usePermissions()
- const fetchWorkspaces = useWorkspaceStore(s => s.fetchWorkspaces)
- const sidebarCollapsed = useWorkspaceStore(s => s.sidebarCollapsed)
+ const sidebarCollapsed = useUserPreferencesStore(s => s.sidebarCollapsed)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
- // Fetch workspaces on mount
- useEffect(() => {
- fetchWorkspaces()
- }, [fetchWorkspaces])
-
// Close mobile menu on route change
const [prevPath, setPrevPath] = useState(location.pathname)
if (prevPath !== location.pathname) {
diff --git a/frontend/src/components/layout/NavItem.tsx b/frontend/src/components/layout/NavItem.tsx
index fd70704f..7e04bef6 100644
--- a/frontend/src/components/layout/NavItem.tsx
+++ b/frontend/src/components/layout/NavItem.tsx
@@ -2,6 +2,13 @@ import { Link, useLocation } from 'react-router-dom'
import type { LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
+interface NavSubItem {
+ href: string
+ label: string
+ count?: number
+ isActive?: boolean
+}
+
interface NavItemProps {
href: string
icon: LucideIcon
@@ -9,16 +16,22 @@ interface NavItemProps {
badge?: number | 'dot'
matchPaths?: string[]
collapsed?: boolean
+ children?: NavSubItem[]
}
-export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed }: NavItemProps) {
+export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed, children }: NavItemProps) {
const location = useLocation()
+ const fullPath = location.pathname + location.search
const isActive = matchPaths
? matchPaths.some(p => location.pathname.startsWith(p))
: href === '/'
? location.pathname === '/'
: location.pathname.startsWith(href)
+ // Check if any child is specifically active
+ const activeChild = children?.find(c => fullPath === c.href || fullPath.startsWith(c.href + '&'))
+ const isParentDimmed = !!activeChild && isActive
+
if (collapsed) {
return (
- {/* Active indicator bar */}
- {isActive && (
-
- )}
+