feat: sidebar redesign — activity feed, grouped nav, AI split #107
178
backend/app/api/endpoints/sidebar.py
Normal file
178
backend/app/api/endpoints/sidebar.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""Sidebar stats and activity feed endpoint."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.models.session import Session
|
||||
from app.models.tree import Tree
|
||||
from app.models.user import User
|
||||
from app.schemas.sidebar import (
|
||||
SidebarActiveSession,
|
||||
SidebarRecentSession,
|
||||
SidebarStatsResponse,
|
||||
SidebarTreeCounts,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/sessions", tags=["sidebar"])
|
||||
|
||||
|
||||
@router.get("/sidebar-stats", response_model=SidebarStatsResponse)
|
||||
async def get_sidebar_stats(
|
||||
tz_offset: int = Query(..., description="Client UTC offset in minutes (e.g. -300 for EST)"),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> SidebarStatsResponse:
|
||||
"""Get sidebar stats and activity feed for the current user.
|
||||
|
||||
Returns daily stats (resolved count, active count, time in session)
|
||||
and lists of active + recently completed sessions.
|
||||
"""
|
||||
# Compute "today" start in user's timezone, then convert to UTC
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
user_tz = timezone(timedelta(minutes=-tz_offset))
|
||||
now_local = now_utc.astimezone(user_tz)
|
||||
today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_start_utc = today_start_local.astimezone(timezone.utc)
|
||||
|
||||
user_filter = Session.user_id == current_user.id
|
||||
|
||||
# --- Resolved today ---
|
||||
resolved_result = await db.execute(
|
||||
select(func.count()).where(
|
||||
and_(
|
||||
user_filter,
|
||||
Session.completed_at >= today_start_utc,
|
||||
Session.outcome == "resolved",
|
||||
)
|
||||
)
|
||||
)
|
||||
resolved_today = resolved_result.scalar() or 0
|
||||
|
||||
# --- Active count (all time, not just today) ---
|
||||
active_result = await db.execute(
|
||||
select(func.count()).where(
|
||||
and_(
|
||||
user_filter,
|
||||
Session.started_at.isnot(None),
|
||||
Session.completed_at.is_(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
active_count = active_result.scalar() or 0
|
||||
|
||||
# --- Completed session minutes today (active session time computed client-side) ---
|
||||
duration_expr = func.extract(
|
||||
"epoch",
|
||||
Session.completed_at - Session.started_at,
|
||||
) / 60.0
|
||||
duration_result = await db.execute(
|
||||
select(func.coalesce(func.sum(duration_expr), 0)).where(
|
||||
and_(
|
||||
user_filter,
|
||||
Session.started_at.isnot(None),
|
||||
Session.completed_at.isnot(None),
|
||||
Session.started_at >= today_start_utc,
|
||||
)
|
||||
)
|
||||
)
|
||||
total_minutes = int(duration_result.scalar() or 0)
|
||||
|
||||
# --- Active sessions (max 5, most recent first) ---
|
||||
active_sessions_result = await db.execute(
|
||||
select(
|
||||
Session.id,
|
||||
Session.tree_id,
|
||||
Session.started_at,
|
||||
Session.ticket_number,
|
||||
Session.psa_ticket_id,
|
||||
Session.tree_snapshot["name"].as_string().label("tree_name"),
|
||||
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
user_filter,
|
||||
Session.started_at.isnot(None),
|
||||
Session.completed_at.is_(None),
|
||||
)
|
||||
)
|
||||
.order_by(Session.started_at.desc())
|
||||
.limit(5)
|
||||
)
|
||||
active_sessions = [
|
||||
SidebarActiveSession(
|
||||
session_id=row.id,
|
||||
tree_name=row.tree_name or "Unknown Flow",
|
||||
tree_id=row.tree_id,
|
||||
tree_type=row.tree_type or "troubleshooting",
|
||||
started_at=row.started_at,
|
||||
ticket_number=row.ticket_number,
|
||||
psa_ticket_id=row.psa_ticket_id,
|
||||
)
|
||||
for row in active_sessions_result.all()
|
||||
]
|
||||
|
||||
# --- Recent completions (max 3, completed today, most recent first) ---
|
||||
recent_result = await db.execute(
|
||||
select(
|
||||
Session.id,
|
||||
Session.tree_id,
|
||||
Session.completed_at,
|
||||
Session.tree_snapshot["name"].as_string().label("tree_name"),
|
||||
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
user_filter,
|
||||
Session.completed_at.isnot(None),
|
||||
Session.completed_at >= today_start_utc,
|
||||
)
|
||||
)
|
||||
.order_by(Session.completed_at.desc())
|
||||
.limit(3)
|
||||
)
|
||||
recent_completions = [
|
||||
SidebarRecentSession(
|
||||
session_id=row.id,
|
||||
tree_name=row.tree_name or "Unknown Flow",
|
||||
tree_id=row.tree_id,
|
||||
tree_type=row.tree_type or "troubleshooting",
|
||||
completed_at=row.completed_at,
|
||||
)
|
||||
for row in recent_result.all()
|
||||
]
|
||||
|
||||
# --- Tree counts (for All Flows sub-items) ---
|
||||
tree_counts_result = await db.execute(
|
||||
select(
|
||||
func.count().label("total"),
|
||||
func.count().filter(Tree.tree_type == "troubleshooting").label("troubleshooting"),
|
||||
func.count().filter(Tree.tree_type == "procedural").label("procedural"),
|
||||
func.count().filter(Tree.tree_type == "maintenance").label("maintenance"),
|
||||
).where(
|
||||
and_(
|
||||
Tree.account_id == current_user.account_id,
|
||||
Tree.is_active.is_(True),
|
||||
Tree.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
tc = tree_counts_result.one()
|
||||
|
||||
return SidebarStatsResponse(
|
||||
resolved_today=resolved_today,
|
||||
active_count=active_count,
|
||||
total_session_minutes_today=total_minutes,
|
||||
tree_counts=SidebarTreeCounts(
|
||||
total=tc.total,
|
||||
troubleshooting=tc.troubleshooting,
|
||||
procedural=tc.procedural,
|
||||
maintenance=tc.maintenance,
|
||||
),
|
||||
active_sessions=active_sessions,
|
||||
recent_completions=recent_completions,
|
||||
)
|
||||
@@ -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
|
||||
from app.api.endpoints import auth, trees, sessions, sidebar, 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
|
||||
from app.api.endpoints import ratings, analytics
|
||||
from app.api.endpoints import target_lists
|
||||
@@ -23,6 +23,7 @@ api_router = APIRouter()
|
||||
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(trees.router)
|
||||
api_router.include_router(sidebar.router)
|
||||
api_router.include_router(sessions.router)
|
||||
api_router.include_router(invite.router)
|
||||
api_router.include_router(categories.router)
|
||||
|
||||
45
backend/app/schemas/sidebar.py
Normal file
45
backend/app/schemas/sidebar.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Schemas for sidebar stats endpoint."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SidebarActiveSession(BaseModel):
|
||||
"""An active or paused session for the activity feed."""
|
||||
session_id: UUID
|
||||
tree_name: str
|
||||
tree_id: UUID
|
||||
tree_type: str
|
||||
started_at: datetime
|
||||
ticket_number: Optional[str] = None
|
||||
psa_ticket_id: Optional[str] = None
|
||||
|
||||
|
||||
class SidebarRecentSession(BaseModel):
|
||||
"""A recently completed session for the activity feed."""
|
||||
session_id: UUID
|
||||
tree_name: str
|
||||
tree_id: UUID
|
||||
tree_type: str
|
||||
completed_at: datetime
|
||||
|
||||
|
||||
class SidebarTreeCounts(BaseModel):
|
||||
"""Tree counts for All Flows sub-items."""
|
||||
total: int
|
||||
troubleshooting: int
|
||||
procedural: int
|
||||
maintenance: int
|
||||
|
||||
|
||||
class SidebarStatsResponse(BaseModel):
|
||||
"""Response for GET /sessions/sidebar-stats."""
|
||||
resolved_today: int
|
||||
active_count: int
|
||||
total_session_minutes_today: int
|
||||
tree_counts: SidebarTreeCounts
|
||||
active_sessions: list[SidebarActiveSession]
|
||||
recent_completions: list[SidebarRecentSession]
|
||||
142
backend/tests/test_sidebar_stats.py
Normal file
142
backend/tests/test_sidebar_stats.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Integration tests for sidebar stats endpoint."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestSidebarStats:
|
||||
"""Tests for GET /sessions/sidebar-stats."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_no_sessions(
|
||||
self, client: AsyncClient, auth_headers: dict
|
||||
):
|
||||
"""Empty stats when user has no sessions."""
|
||||
response = await client.get(
|
||||
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["resolved_today"] == 0
|
||||
assert data["active_count"] == 0
|
||||
assert data["total_session_minutes_today"] == 0
|
||||
assert data["active_sessions"] == []
|
||||
assert data["recent_completions"] == []
|
||||
assert "tree_counts" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_with_active_session(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Active session appears in activity feed."""
|
||||
create_resp = await client.post(
|
||||
"/api/v1/sessions",
|
||||
json={"tree_id": test_tree["id"], "ticket_number": "TK-100"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert create_resp.status_code == 201
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["active_count"] == 1
|
||||
assert len(data["active_sessions"]) == 1
|
||||
assert data["active_sessions"][0]["ticket_number"] == "TK-100"
|
||||
assert data["active_sessions"][0]["tree_id"] == test_tree["id"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_resolved_today(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Resolved session counts in resolved_today."""
|
||||
create_resp = await client.post(
|
||||
"/api/v1/sessions",
|
||||
json={"tree_id": test_tree["id"]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
session_id = create_resp.json()["id"]
|
||||
|
||||
# Complete with resolved outcome
|
||||
await client.post(
|
||||
f"/api/v1/sessions/{session_id}/complete",
|
||||
json={"outcome": "resolved", "outcome_notes": "Fixed it"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["resolved_today"] >= 1
|
||||
assert data["active_count"] == 0
|
||||
assert len(data["recent_completions"]) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_max_active_sessions(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Active sessions capped at 5."""
|
||||
for i in range(7):
|
||||
await client.post(
|
||||
"/api/v1/sessions",
|
||||
json={"tree_id": test_tree["id"], "ticket_number": f"TK-{i}"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["active_count"] == 7
|
||||
assert len(data["active_sessions"]) == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_recent_completions_max_3(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Recent completions capped at 3."""
|
||||
for i in range(5):
|
||||
create_resp = await client.post(
|
||||
"/api/v1/sessions",
|
||||
json={"tree_id": test_tree["id"]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
session_id = create_resp.json()["id"]
|
||||
await client.post(
|
||||
f"/api/v1/sessions/{session_id}/complete",
|
||||
json={"outcome": "resolved", "outcome_notes": "Done"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["recent_completions"]) == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_requires_auth(self, client: AsyncClient):
|
||||
"""Endpoint requires authentication."""
|
||||
response = await client.get("/api/v1/sessions/sidebar-stats?tz_offset=0")
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sidebar_stats_requires_tz_offset(
|
||||
self, client: AsyncClient, auth_headers: dict
|
||||
):
|
||||
"""tz_offset query param is required."""
|
||||
response = await client.get(
|
||||
"/api/v1/sessions/sidebar-stats",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 422
|
||||
630
docs/plans/Frontend/sidebar-grouping-concepts.html
Normal file
630
docs/plans/Frontend/sidebar-grouping-concepts.html
Normal file
@@ -0,0 +1,630 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ResolutionFlow — Sidebar Grouping Concepts</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #101114;
|
||||
--card: #17191d;
|
||||
--fg: #f8fafc;
|
||||
--fg-muted: #8891a0;
|
||||
--fg-dim: #5a6170;
|
||||
--border: rgba(255,255,255,0.06);
|
||||
--border-hover: rgba(255,255,255,0.12);
|
||||
--cyan-500: #06b6d4;
|
||||
--cyan-400: #22d3ee;
|
||||
--sidebar-hover: #212329;
|
||||
--sidebar-active: rgba(6,182,212,0.10);
|
||||
--glass-bg: rgba(22,24,28,0.55);
|
||||
--glass-blur: blur(16px);
|
||||
--amber: #f59e0b;
|
||||
--emerald: #34d399;
|
||||
--violet: #a78bfa;
|
||||
--rose: #fb7185;
|
||||
--blue: #60a5fa;
|
||||
--orange: #fb923c;
|
||||
--teal: #2dd4bf;
|
||||
--sky: #38bdf8;
|
||||
--lime: #a3e635;
|
||||
--indigo: #818cf8;
|
||||
--fuchsia: #e879f9;
|
||||
--pink: #f472b6;
|
||||
--yellow: #facc15;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
min-height: 100vh;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 2rem; letter-spacing: -0.03em; margin-bottom: 6px; }
|
||||
h1 span { color: var(--cyan-400); }
|
||||
.subtitle { color: var(--fg-muted); font-size: 0.9375rem; margin-bottom: 48px; max-width: 820px; line-height: 1.6; }
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 24px; margin-top: 56px;
|
||||
padding-bottom: 16px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.section-header:first-of-type { margin-top: 0; }
|
||||
.section-header h2 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 1.5rem; letter-spacing: -0.02em; margin-bottom: 4px; }
|
||||
.section-header p { font-size: 0.8125rem; color: var(--fg-dim); line-height: 1.5; max-width: 700px; }
|
||||
|
||||
.variants-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 28px;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.variant {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: border-color 300ms, box-shadow 300ms;
|
||||
}
|
||||
.variant:hover { border-color: var(--border-hover); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
|
||||
|
||||
.variant-header {
|
||||
padding: 16px 20px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.variant-tag {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.5625rem; text-transform: uppercase;
|
||||
letter-spacing: 0.12em; color: var(--cyan-400); margin-bottom: 4px;
|
||||
}
|
||||
.variant-title {
|
||||
font-family: 'Bricolage Grotesque', sans-serif;
|
||||
font-weight: 700; font-size: 1.05rem; letter-spacing: -0.02em; margin-bottom: 2px;
|
||||
}
|
||||
.variant-desc { font-size: 0.75rem; color: var(--fg-dim); line-height: 1.4; }
|
||||
|
||||
/* ── Sidebar simulation ── */
|
||||
.sidebar-sim { padding: 6px 12px 12px; }
|
||||
|
||||
.nav-group-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.5625rem; text-transform: uppercase;
|
||||
letter-spacing: 0.12em; color: var(--fg-dim);
|
||||
padding: 14px 12px 5px;
|
||||
}
|
||||
.nav-group-label:first-child { padding-top: 8px; }
|
||||
|
||||
.nav-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 6px 12px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 8px 12px; border-radius: 10px;
|
||||
font-size: 0.8125rem; font-weight: 500;
|
||||
color: var(--fg-muted); cursor: pointer;
|
||||
transition: all 150ms ease; position: relative;
|
||||
}
|
||||
.nav-item:hover { background: var(--sidebar-hover); color: var(--fg); }
|
||||
.nav-item.active { background: var(--sidebar-active); color: var(--fg); }
|
||||
.nav-item.active::before {
|
||||
content: ''; position: absolute; left: 0; top: 50%;
|
||||
transform: translateY(-50%); width: 3px; height: 24px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
background: linear-gradient(135deg, var(--cyan-500), var(--cyan-400));
|
||||
}
|
||||
.nav-item .badge {
|
||||
margin-left: auto;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 0.6875rem;
|
||||
color: var(--fg-dim); background: var(--card);
|
||||
border: 1px solid var(--border); padding: 1px 8px; border-radius: 9999px;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 20px; height: 20px; flex-shrink: 0;
|
||||
}
|
||||
.icon-wrap svg {
|
||||
width: 18px; height: 18px; stroke-width: 1.75;
|
||||
fill: none; stroke: currentColor;
|
||||
stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Icon colors */
|
||||
.ic-dashboard svg { color: var(--cyan-400); }
|
||||
.ic-flows svg { color: var(--violet); }
|
||||
.ic-editor svg { color: var(--amber); }
|
||||
.ic-sessions svg { color: var(--emerald); }
|
||||
.ic-exports svg { color: var(--blue); }
|
||||
.ic-ai svg { color: var(--fuchsia); }
|
||||
.ic-ai-build svg { color: var(--pink); }
|
||||
.ic-ai-copilot svg { color: var(--fuchsia); }
|
||||
.ic-steplib svg { color: var(--orange); }
|
||||
.ic-scripts svg { color: var(--teal); }
|
||||
.ic-kb svg { color: var(--rose); }
|
||||
.ic-analytics svg { color: var(--sky); }
|
||||
.ic-guides svg { color: var(--lime); }
|
||||
.ic-feedback svg { color: var(--indigo); }
|
||||
.ic-settings svg { color: var(--fg-dim); }
|
||||
|
||||
/* ── Footer section styling ── */
|
||||
.footer-section {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 8px 12px 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ── Callout box ── */
|
||||
.ai-naming {
|
||||
margin-top: 56px; max-width: 1000px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 24px 28px;
|
||||
}
|
||||
.ai-naming h2 {
|
||||
font-family: 'Bricolage Grotesque', sans-serif;
|
||||
font-weight: 700; font-size: 1.25rem; letter-spacing: -0.02em; margin-bottom: 16px;
|
||||
}
|
||||
.naming-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (max-width: 700px) { .naming-grid { grid-template-columns: 1fr; } }
|
||||
.naming-card {
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.naming-card h3 {
|
||||
font-family: 'Bricolage Grotesque', sans-serif;
|
||||
font-weight: 700; font-size: 0.9375rem; margin-bottom: 4px;
|
||||
}
|
||||
.naming-card .purpose {
|
||||
font-size: 0.75rem; color: var(--fg-dim); margin-bottom: 12px; line-height: 1.4;
|
||||
}
|
||||
.naming-option {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 6px 0;
|
||||
font-size: 0.8125rem; color: var(--fg-muted);
|
||||
}
|
||||
.naming-option .name { color: var(--fg); font-weight: 600; min-width: 140px; }
|
||||
.naming-option .desc { font-size: 0.75rem; color: var(--fg-dim); }
|
||||
.naming-option.recommended .name { color: var(--cyan-400); }
|
||||
.naming-option .icon-sm {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 18px; height: 18px; flex-shrink: 0;
|
||||
}
|
||||
.naming-option .icon-sm svg {
|
||||
width: 16px; height: 16px; stroke-width: 1.75;
|
||||
fill: none; stroke: currentColor;
|
||||
stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* SVG icon templates — reused inline */
|
||||
/* Dashboard: LayoutGrid */
|
||||
/* Flows: Network */
|
||||
/* Editor: Wrench */
|
||||
/* Sessions: Clock */
|
||||
/* Exports: FileOutput */
|
||||
/* AI: BotMessageSquare */
|
||||
/* AI Build: Wand2 / Hammer / MessageSquareCode */
|
||||
/* AI Copilot: Brain / BotMessageSquare / Sparkles */
|
||||
/* StepLib: Library */
|
||||
/* Scripts: Code2 -->
|
||||
/* KB: Lightbulb */
|
||||
/* Analytics: BarChart3 */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Sidebar <span>Grouping Concepts</span></h1>
|
||||
<p class="subtitle">
|
||||
Reorganizing the navigation around workflow stages — what engineers do when they're <strong>working a ticket</strong> vs
|
||||
<strong>building flows</strong> vs <strong>reviewing results</strong>. Also explores splitting AI Assistant into two
|
||||
distinct tools with clearer purpose.
|
||||
</p>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════
|
||||
CONCEPT A — Three Groups (Resolve / Build / Insights)
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-header">
|
||||
<h2>Concept A — Resolve / Build / Insights</h2>
|
||||
<p>Three clear workflow stages. "Resolve" is front and center because that's what engineers spend most time doing. AI is split: copilot lives in Resolve, builder lives in Build.</p>
|
||||
</div>
|
||||
|
||||
<div class="variants-grid">
|
||||
|
||||
<!-- A1: Clean section labels -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept A1</div>
|
||||
<div class="variant-title">With Section Labels</div>
|
||||
<div class="variant-desc">Explicit uppercase labels separate each group. Clear visual hierarchy.</div>
|
||||
</div>
|
||||
<div class="sidebar-sim">
|
||||
<div class="nav-group-label">Resolve</div>
|
||||
<div class="nav-item active ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">32</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-copilot">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span>FlowPilot</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Build</div>
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-build">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span>Flow Assist</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Insights</div>
|
||||
<div class="nav-item ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<div class="nav-item ic-guides">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
|
||||
<span>User Guides</span>
|
||||
</div>
|
||||
<div class="nav-item ic-feedback">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
|
||||
<span>Feedback</span>
|
||||
</div>
|
||||
<div class="nav-item ic-settings">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
|
||||
<span>Account</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- A2: Dividers instead of labels -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept A2</div>
|
||||
<div class="variant-title">With Dividers Only</div>
|
||||
<div class="variant-desc">Same grouping but uses subtle divider lines instead of text labels. Cleaner, less visual noise. Groups are implied by proximity.</div>
|
||||
</div>
|
||||
<div class="sidebar-sim">
|
||||
<div class="nav-item active ic-sessions" style="margin-top: 6px">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">32</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-copilot">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span>FlowPilot</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-build">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span>Flow Assist</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
<div class="nav-item ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<div class="nav-item ic-guides">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
|
||||
<span>User Guides</span>
|
||||
</div>
|
||||
<div class="nav-item ic-feedback">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
|
||||
<span>Feedback</span>
|
||||
</div>
|
||||
<div class="nav-item ic-settings">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
|
||||
<span>Account</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- A3: Dashboard at top, then workflow groups -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept A3</div>
|
||||
<div class="variant-title">Dashboard First</div>
|
||||
<div class="variant-desc">Dashboard stays at the top as the "home" landing, then workflow groups follow. Some engineers want the overview first before diving into work.</div>
|
||||
</div>
|
||||
<div class="sidebar-sim">
|
||||
<div class="nav-item ic-dashboard" style="margin-top: 6px">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Resolve</div>
|
||||
<div class="nav-item active ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">32</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-copilot">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span>FlowPilot</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Build</div>
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-build">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span>Flow Assist</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Insights</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<div class="nav-item ic-guides">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
|
||||
<span>User Guides</span>
|
||||
</div>
|
||||
<div class="nav-item ic-feedback">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
|
||||
<span>Feedback</span>
|
||||
</div>
|
||||
<div class="nav-item ic-settings">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
|
||||
<span>Account</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════
|
||||
CONCEPT B — Two Groups (Work / Build) + Dashboard
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-header">
|
||||
<h2>Concept B — Work / Build (Simpler Split)</h2>
|
||||
<p>Only two groups instead of three. Dashboard top, then "Work" (active troubleshooting) and "Build" (authoring + review). Exports and Analytics move into Build since they're about improving process, not resolving tickets.</p>
|
||||
</div>
|
||||
|
||||
<div class="variants-grid">
|
||||
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept B1</div>
|
||||
<div class="variant-title">Two Groups + Dashboard</div>
|
||||
<div class="variant-desc">Fewer sections = less cognitive load. "Work" is what you do when tickets come in. "Build" is everything else.</div>
|
||||
</div>
|
||||
<div class="sidebar-sim">
|
||||
<div class="nav-item ic-dashboard" style="margin-top: 6px">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Work</div>
|
||||
<div class="nav-item active ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">32</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-copilot">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span>FlowPilot</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-group-label">Build</div>
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai-build">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span>Flow Assist</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<div class="nav-item ic-guides">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
|
||||
<span>User Guides</span>
|
||||
</div>
|
||||
<div class="nav-item ic-feedback">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>
|
||||
<span>Feedback</span>
|
||||
</div>
|
||||
<div class="nav-item ic-settings">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div>
|
||||
<span>Account</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════
|
||||
AI NAMING OPTIONS
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="ai-naming">
|
||||
<h2>AI Assistant Split — Naming Options</h2>
|
||||
<div class="naming-grid">
|
||||
|
||||
<div class="naming-card">
|
||||
<h3 style="color: var(--fuchsia)">Troubleshooting AI (Copilot)</h3>
|
||||
<p class="purpose">Used during active sessions. Suggests next steps, explains errors, provides context from ticket data and knowledge base.</p>
|
||||
<div class="naming-option recommended">
|
||||
<div class="icon-sm" style="color: var(--fuchsia)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span class="name">FlowPilot</span>
|
||||
<span class="desc">Brain icon — suggests intelligence guiding you. Already used in the copilot panel.</span>
|
||||
</div>
|
||||
<div class="naming-option">
|
||||
<div class="icon-sm" style="color: var(--fuchsia)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
|
||||
<span class="name">AI Copilot</span>
|
||||
<span class="desc">Current robot icon — generic but clear purpose</span>
|
||||
</div>
|
||||
<div class="naming-option">
|
||||
<div class="icon-sm" style="color: var(--fuchsia)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></div>
|
||||
<span class="name">Resolve AI</span>
|
||||
<span class="desc">Sparkle icon — ties to the "Resolve" section name</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="naming-card">
|
||||
<h3 style="color: var(--pink)">Flow Builder AI (Authoring)</h3>
|
||||
<p class="purpose">Used when building new flows from scratch. Conversational interface to generate decision trees, procedural steps, and intake forms.</p>
|
||||
<div class="naming-option recommended">
|
||||
<div class="icon-sm" style="color: var(--pink)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span class="name">Flow Assist</span>
|
||||
<span class="desc">Wand icon — creation/magic. Already the name of the embedded editor AI.</span>
|
||||
</div>
|
||||
<div class="naming-option">
|
||||
<div class="icon-sm" style="color: var(--pink)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
|
||||
<span class="name">AI Builder</span>
|
||||
<span class="desc">Robot icon — explicit "builder" label</span>
|
||||
</div>
|
||||
<div class="naming-option">
|
||||
<div class="icon-sm" style="color: var(--pink)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span class="name">Flow Forge</span>
|
||||
<span class="desc">Wrench icon — "forge" = crafting/building</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.addEventListener('click', () => item.classList.toggle('active'));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
734
docs/plans/Frontend/sidebar-icon-concepts.html
Normal file
734
docs/plans/Frontend/sidebar-icon-concepts.html
Normal file
@@ -0,0 +1,734 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ResolutionFlow — Sidebar Icon Concepts (Refined)</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #101114;
|
||||
--card: #17191d;
|
||||
--fg: #f8fafc;
|
||||
--fg-muted: #8891a0;
|
||||
--fg-dim: #5a6170;
|
||||
--border: rgba(255,255,255,0.06);
|
||||
--border-hover: rgba(255,255,255,0.12);
|
||||
--cyan-500: #06b6d4;
|
||||
--cyan-400: #22d3ee;
|
||||
--sidebar-hover: #212329;
|
||||
--sidebar-active: rgba(6,182,212,0.10);
|
||||
--glass-bg: rgba(22,24,28,0.55);
|
||||
--glass-blur: blur(16px);
|
||||
--amber: #f59e0b; --amber-soft: rgba(245,158,11,0.12);
|
||||
--emerald: #34d399; --emerald-soft: rgba(52,211,153,0.12);
|
||||
--violet: #a78bfa; --violet-soft: rgba(167,139,250,0.12);
|
||||
--rose: #fb7185; --rose-soft: rgba(251,113,133,0.12);
|
||||
--blue: #60a5fa; --blue-soft: rgba(96,165,250,0.12);
|
||||
--orange: #fb923c; --orange-soft: rgba(251,146,60,0.12);
|
||||
--teal: #2dd4bf; --teal-soft: rgba(45,212,191,0.12);
|
||||
--sky: #38bdf8; --sky-soft: rgba(56,189,248,0.12);
|
||||
--lime: #a3e635; --lime-soft: rgba(163,230,53,0.12);
|
||||
--indigo: #818cf8; --indigo-soft: rgba(129,140,248,0.12);
|
||||
--fuchsia: #e879f9; --fuchsia-soft: rgba(232,121,249,0.12);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
min-height: 100vh;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 2rem; letter-spacing: -0.03em; margin-bottom: 6px; }
|
||||
h1 span { color: var(--cyan-400); }
|
||||
.subtitle { color: var(--fg-muted); font-size: 0.9375rem; margin-bottom: 48px; max-width: 780px; line-height: 1.6; }
|
||||
|
||||
/* ── Section headers ── */
|
||||
.section-header {
|
||||
margin-bottom: 24px;
|
||||
margin-top: 56px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.section-header:first-of-type { margin-top: 0; }
|
||||
.section-header h2 {
|
||||
font-family: 'Bricolage Grotesque', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.section-header p { font-size: 0.8125rem; color: var(--fg-dim); line-height: 1.5; }
|
||||
|
||||
/* ── Grid of variants ── */
|
||||
.variants-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
@media (max-width: 1000px) { .variants-grid { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 680px) { .variants-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.variant {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: border-color 300ms, box-shadow 300ms;
|
||||
}
|
||||
.variant:hover { border-color: var(--border-hover); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
|
||||
|
||||
.variant-header {
|
||||
padding: 16px 20px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.variant-tag {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.5625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--cyan-400);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.variant-title {
|
||||
font-family: 'Bricolage Grotesque', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.variant-desc { font-size: 0.75rem; color: var(--fg-dim); line-height: 1.4; }
|
||||
|
||||
/* ── Sidebar simulation ── */
|
||||
.sidebar-sim { padding: 8px 12px; }
|
||||
.nav-section-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.5625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--fg-dim);
|
||||
padding: 10px 12px 4px;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 8px 12px; border-radius: 10px;
|
||||
font-size: 0.8125rem; font-weight: 500;
|
||||
color: var(--fg-muted); cursor: pointer;
|
||||
transition: all 150ms ease; position: relative;
|
||||
}
|
||||
.nav-item:hover { background: var(--sidebar-hover); color: var(--fg); }
|
||||
.nav-item.active { background: var(--sidebar-active); color: var(--fg); }
|
||||
.nav-item .badge {
|
||||
margin-left: auto;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 0.6875rem;
|
||||
color: var(--fg-dim); background: var(--card);
|
||||
border: 1px solid var(--border); padding: 1px 8px; border-radius: 9999px;
|
||||
}
|
||||
.nav-item.active::before {
|
||||
content: ''; position: absolute; left: 0; top: 50%;
|
||||
transform: translateY(-50%); width: 3px; height: 24px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
background: linear-gradient(135deg, var(--cyan-500), var(--cyan-400));
|
||||
}
|
||||
|
||||
/* ── Icon wrap ── */
|
||||
.icon-wrap {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 20px; height: 20px; flex-shrink: 0; position: relative;
|
||||
}
|
||||
.icon-wrap svg {
|
||||
width: 18px; height: 18px; stroke-width: 1.75;
|
||||
fill: none; stroke: currentColor;
|
||||
stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* ── CONCEPT 1: Semantic Colored Icons ── */
|
||||
.c1 .icon-wrap { transition: transform 150ms ease; }
|
||||
.c1 .nav-item:hover .icon-wrap { transform: scale(1.1); }
|
||||
.c1 .ic-dashboard svg { stroke: var(--cyan-400); }
|
||||
.c1 .ic-flows svg { stroke: var(--violet); }
|
||||
.c1 .ic-editor svg { stroke: var(--amber); }
|
||||
.c1 .ic-sessions svg { stroke: var(--emerald); }
|
||||
.c1 .ic-exports svg { stroke: var(--blue); }
|
||||
.c1 .ic-ai svg { stroke: var(--fuchsia); }
|
||||
.c1 .ic-steplib svg { stroke: var(--orange); }
|
||||
.c1 .ic-scripts svg { stroke: var(--teal); }
|
||||
.c1 .ic-kb svg { stroke: var(--rose); }
|
||||
.c1 .ic-analytics svg{ stroke: var(--sky); }
|
||||
|
||||
/* ── CONCEPT 2: Tinted Pill Backgrounds ── */
|
||||
.c2 .icon-wrap {
|
||||
width: 28px; height: 28px; border-radius: 8px;
|
||||
transition: transform 150ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
.c2 .nav-item:hover .icon-wrap { transform: scale(1.08); }
|
||||
.c2 .icon-wrap svg { width: 16px; height: 16px; }
|
||||
|
||||
.c2 .ic-dashboard .icon-wrap { background: rgba(6,182,212,0.12); }
|
||||
.c2 .ic-dashboard svg { stroke: var(--cyan-400); }
|
||||
.c2 .ic-flows .icon-wrap { background: var(--violet-soft); }
|
||||
.c2 .ic-flows svg { stroke: var(--violet); }
|
||||
.c2 .ic-editor .icon-wrap { background: var(--amber-soft); }
|
||||
.c2 .ic-editor svg { stroke: var(--amber); }
|
||||
.c2 .ic-sessions .icon-wrap { background: var(--emerald-soft); }
|
||||
.c2 .ic-sessions svg { stroke: var(--emerald); }
|
||||
.c2 .ic-exports .icon-wrap { background: var(--blue-soft); }
|
||||
.c2 .ic-exports svg { stroke: var(--blue); }
|
||||
.c2 .ic-ai .icon-wrap { background: var(--fuchsia-soft); }
|
||||
.c2 .ic-ai svg { stroke: var(--fuchsia); }
|
||||
.c2 .ic-steplib .icon-wrap { background: var(--orange-soft); }
|
||||
.c2 .ic-steplib svg { stroke: var(--orange); }
|
||||
.c2 .ic-scripts .icon-wrap { background: var(--teal-soft); }
|
||||
.c2 .ic-scripts svg { stroke: var(--teal); }
|
||||
.c2 .ic-kb .icon-wrap { background: var(--rose-soft); }
|
||||
.c2 .ic-kb svg { stroke: var(--rose); }
|
||||
.c2 .ic-analytics .icon-wrap { background: var(--sky-soft); }
|
||||
.c2 .ic-analytics svg { stroke: var(--sky); }
|
||||
|
||||
.c2 .nav-item.active.ic-dashboard .icon-wrap { box-shadow: 0 0 12px rgba(6,182,212,0.3); }
|
||||
.c2 .nav-item.active.ic-sessions .icon-wrap { box-shadow: 0 0 12px rgba(52,211,153,0.3); }
|
||||
.c2 .nav-item.active.ic-flows .icon-wrap { box-shadow: 0 0 12px rgba(167,139,250,0.3); }
|
||||
|
||||
/* ── Icon label under each nav section ── */
|
||||
.icon-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.5rem;
|
||||
color: var(--fg-dim);
|
||||
opacity: 0.5;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-item:hover .icon-label { opacity: 0.8; }
|
||||
|
||||
/* ── Comparison table ── */
|
||||
.comparison {
|
||||
margin-top: 56px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
.comparison h2 {
|
||||
font-family: 'Bricolage Grotesque', sans-serif;
|
||||
font-weight: 700; font-size: 1.25rem;
|
||||
letter-spacing: -0.02em; margin-bottom: 16px;
|
||||
}
|
||||
.comp-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.comp-table th {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--fg-dim);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.comp-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--fg-muted);
|
||||
vertical-align: top;
|
||||
}
|
||||
.comp-table td:first-child {
|
||||
color: var(--fg);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.comp-table .current { color: var(--fg-dim); }
|
||||
.comp-table .pick { color: var(--cyan-400); font-weight: 500; }
|
||||
.comp-table .alt { color: var(--fg-muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Sidebar Icons — <span>Style × Shape Matrix</span></h1>
|
||||
<p class="subtitle">
|
||||
Concept 1 (Semantic Colors) and Concept 2 (Tinted Pills) shown with 3 different icon sets each.
|
||||
<strong>Current</strong> = existing Lucide icons, <strong>Set A</strong> = more descriptive/metaphorical icons,
|
||||
<strong>Set B</strong> = minimal/geometric alternatives. Click any nav item to toggle active state.
|
||||
</p>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════
|
||||
CONCEPT 1 — SEMANTIC COLORED ICONS × 3 icon sets
|
||||
════════════════════════════════════════════════════════ -->
|
||||
<div class="section-header">
|
||||
<h2>Concept 1 — Semantic Colored Icons</h2>
|
||||
<p>Always-on color per icon. No pill background — just the stroke color creates landmarks.</p>
|
||||
</div>
|
||||
|
||||
<div class="variants-grid">
|
||||
|
||||
<!-- C1 × Current Icons -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept 1 × Current Icons</div>
|
||||
<div class="variant-title">Original Lucide Set</div>
|
||||
<div class="variant-desc">LayoutGrid, Box, PenLine, Clock, FileText, BotMessageSquare, Bookmark, Terminal, Sparkles, BarChart3</div>
|
||||
</div>
|
||||
<div class="sidebar-sim c1">
|
||||
<div class="nav-section-label">Navigation</div>
|
||||
<!-- Dashboard: LayoutGrid -->
|
||||
<div class="nav-item active ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<!-- All Flows: Box -->
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">12</span>
|
||||
</div>
|
||||
<!-- Flow Editor: PenLine -->
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<!-- Sessions: Clock -->
|
||||
<div class="nav-item ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<!-- Exports: FileText -->
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<!-- AI Assistant: BotMessageSquare -->
|
||||
<div class="nav-item ic-ai">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
|
||||
<span>AI Assistant</span>
|
||||
</div>
|
||||
<!-- Step Library: Bookmark -->
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<!-- Script Library: Terminal -->
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<!-- KB Accelerator: Sparkles -->
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<!-- Analytics: BarChart3 -->
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- C1 × Icon Set A — Descriptive/Metaphorical -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept 1 × Icon Set A</div>
|
||||
<div class="variant-title">Descriptive / Metaphorical</div>
|
||||
<div class="variant-desc">Gauge, GitFork, Wrench, Zap, Share2, Brain, Layers, Code2, Rocket, TrendingUp</div>
|
||||
</div>
|
||||
<div class="sidebar-sim c1">
|
||||
<div class="nav-section-label">Navigation</div>
|
||||
<!-- Dashboard: Gauge (speedometer) -->
|
||||
<div class="nav-item active ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<!-- All Flows: GitFork (branching paths) -->
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"/><path d="M12 12v3"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">12</span>
|
||||
</div>
|
||||
<!-- Flow Editor: Wrench (builder) -->
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<!-- Sessions: Zap (active/energy) -->
|
||||
<div class="nav-item ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<!-- Exports: Share2 (share network) -->
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<!-- AI Assistant: Brain -->
|
||||
<div class="nav-item ic-ai">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span>AI Assistant</span>
|
||||
</div>
|
||||
<!-- Step Library: Layers (stacked) -->
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<!-- Script Library: Code2 (code brackets) -->
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<!-- KB Accelerator: Rocket -->
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<!-- Analytics: TrendingUp -->
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- C1 × Icon Set B — Minimal/Geometric -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept 1 × Icon Set B</div>
|
||||
<div class="variant-title">Minimal / Geometric</div>
|
||||
<div class="variant-desc">Compass, Network, PenTool, Radio, FileOutput, Wand2, Library, ScrollText, Lightbulb, PieChart</div>
|
||||
</div>
|
||||
<div class="sidebar-sim c1">
|
||||
<div class="nav-section-label">Navigation</div>
|
||||
<!-- Dashboard: Compass (navigation hub) -->
|
||||
<div class="nav-item active ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<!-- All Flows: Network (connected nodes) -->
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">12</span>
|
||||
</div>
|
||||
<!-- Flow Editor: PenTool (vector pen) -->
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 19 7-7 3 3-7 7-3-3z"/><path d="m18 13-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="m2 2 7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<!-- Sessions: Radio (live/active) -->
|
||||
<div class="nav-item ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.4"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.4"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/><circle cx="12" cy="12" r="2"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<!-- Exports: FileOutput (file with arrow) -->
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<!-- AI Assistant: Wand2 (magic wand) -->
|
||||
<div class="nav-item ic-ai">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span>AI Assistant</span>
|
||||
</div>
|
||||
<!-- Step Library: Library -->
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<!-- Script Library: ScrollText -->
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<!-- KB Accelerator: Lightbulb -->
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<!-- Analytics: PieChart -->
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════
|
||||
CONCEPT 2 — TINTED PILL BACKGROUNDS × 3 icon sets
|
||||
════════════════════════════════════════════════════════ -->
|
||||
<div class="section-header">
|
||||
<h2>Concept 2 — Tinted Pill Backgrounds</h2>
|
||||
<p>Colored icon inside a soft tinted rounded-square. Creates visual weight — feels like an app dock.</p>
|
||||
</div>
|
||||
|
||||
<div class="variants-grid">
|
||||
|
||||
<!-- C2 × Current Icons -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept 2 × Current Icons</div>
|
||||
<div class="variant-title">Original Lucide Set</div>
|
||||
<div class="variant-desc">LayoutGrid, Box, PenLine, Clock, FileText, BotMessageSquare, Bookmark, Terminal, Sparkles, BarChart3</div>
|
||||
</div>
|
||||
<div class="sidebar-sim c2">
|
||||
<div class="nav-section-label">Navigation</div>
|
||||
<div class="nav-item active ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">12</span>
|
||||
</div>
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></div>
|
||||
<span>AI Assistant</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- C2 × Icon Set A — Descriptive/Metaphorical -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept 2 × Icon Set A</div>
|
||||
<div class="variant-title">Descriptive / Metaphorical</div>
|
||||
<div class="variant-desc">Gauge, GitFork, Wrench, Zap, Share2, Brain, Layers, Code2, Rocket, TrendingUp</div>
|
||||
</div>
|
||||
<div class="sidebar-sim c2">
|
||||
<div class="nav-section-label">Navigation</div>
|
||||
<div class="nav-item active ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"/><path d="M12 12v3"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">12</span>
|
||||
</div>
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M12 18v-5"/><path d="M12 5v1"/></svg></div>
|
||||
<span>AI Assistant</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- C2 × Icon Set B — Minimal/Geometric -->
|
||||
<div class="variant">
|
||||
<div class="variant-header">
|
||||
<div class="variant-tag">Concept 2 × Icon Set B</div>
|
||||
<div class="variant-title">Minimal / Geometric</div>
|
||||
<div class="variant-desc">Compass, Network, PenTool, Radio, FileOutput, Wand2, Library, ScrollText, Lightbulb, PieChart</div>
|
||||
</div>
|
||||
<div class="sidebar-sim c2">
|
||||
<div class="nav-section-label">Navigation</div>
|
||||
<div class="nav-item active ic-dashboard">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg></div>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<div class="nav-item ic-flows">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></div>
|
||||
<span>All Flows</span>
|
||||
<span class="badge">12</span>
|
||||
</div>
|
||||
<div class="nav-item ic-editor">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m12 19 7-7 3 3-7 7-3-3z"/><path d="m18 13-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="m2 2 7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg></div>
|
||||
<span>Flow Editor</span>
|
||||
</div>
|
||||
<div class="nav-item ic-sessions">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.4"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.4"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/><circle cx="12" cy="12" r="2"/></svg></div>
|
||||
<span>Sessions</span>
|
||||
<span class="badge">3</span>
|
||||
</div>
|
||||
<div class="nav-item ic-exports">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"/><path d="M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"/><path d="m10 18 3-3-3-3"/><path d="M4 15h9"/></svg></div>
|
||||
<span>Exports</span>
|
||||
</div>
|
||||
<div class="nav-item ic-ai">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></div>
|
||||
<span>AI Assistant</span>
|
||||
</div>
|
||||
<div class="nav-item ic-steplib">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg></div>
|
||||
<span>Step Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-scripts">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg></div>
|
||||
<span>Script Library</span>
|
||||
</div>
|
||||
<div class="nav-item ic-kb">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
|
||||
<span>KB Accelerator</span>
|
||||
</div>
|
||||
<div class="nav-item ic-analytics">
|
||||
<div class="icon-wrap"><svg viewBox="0 0 24 24"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg></div>
|
||||
<span>Analytics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════
|
||||
ICON COMPARISON TABLE
|
||||
════════════════════════════════════════════════════════ -->
|
||||
<div class="comparison">
|
||||
<h2>Icon Comparison Reference</h2>
|
||||
<table class="comp-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nav Item</th>
|
||||
<th>Current</th>
|
||||
<th>Set A (Descriptive)</th>
|
||||
<th>Set B (Minimal)</th>
|
||||
<th>Color</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Dashboard</td>
|
||||
<td class="current">LayoutGrid — 4 squares</td>
|
||||
<td class="pick">Gauge — speedometer, "command center"</td>
|
||||
<td class="alt">Compass — navigation hub</td>
|
||||
<td style="color: var(--cyan-400)">Cyan</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>All Flows</td>
|
||||
<td class="current">Box — generic 3D cube</td>
|
||||
<td class="pick">GitFork — branching paths (literally what flows are)</td>
|
||||
<td class="alt">Network — connected nodes</td>
|
||||
<td style="color: var(--violet)">Violet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Flow Editor</td>
|
||||
<td class="current">PenLine — pen tip</td>
|
||||
<td class="alt">Wrench — builder tool</td>
|
||||
<td class="alt">PenTool — vector/design pen</td>
|
||||
<td style="color: var(--amber)">Amber</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sessions</td>
|
||||
<td class="current">Clock — time-based</td>
|
||||
<td class="pick">Zap — active energy, live sessions</td>
|
||||
<td class="alt">Radio — broadcasting/live signal</td>
|
||||
<td style="color: var(--emerald)">Emerald</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exports</td>
|
||||
<td class="current">FileText — generic doc</td>
|
||||
<td class="alt">Share2 — share network nodes</td>
|
||||
<td class="alt">FileOutput — file with arrow out</td>
|
||||
<td style="color: var(--blue)">Blue</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AI Assistant</td>
|
||||
<td class="current">BotMessageSquare — robot chat</td>
|
||||
<td class="pick">Brain — intelligence, organic feel</td>
|
||||
<td class="alt">Wand2 — magic wand with sparkles</td>
|
||||
<td style="color: var(--fuchsia)">Fuchsia</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Step Library</td>
|
||||
<td class="current">Bookmark — generic ribbon</td>
|
||||
<td class="pick">Layers — stacked layers = reusable steps</td>
|
||||
<td class="alt">Library — book spines</td>
|
||||
<td style="color: var(--orange)">Orange</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Script Library</td>
|
||||
<td class="current">Terminal — prompt cursor</td>
|
||||
<td class="alt">Code2 — angle brackets with slash</td>
|
||||
<td class="alt">ScrollText — script/scroll with text</td>
|
||||
<td style="color: var(--teal)">Teal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>KB Accelerator</td>
|
||||
<td class="current">Sparkles — magic star</td>
|
||||
<td class="pick">Rocket — acceleration/speed</td>
|
||||
<td class="alt">Lightbulb — ideas/insight</td>
|
||||
<td style="color: var(--rose)">Rose</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Analytics</td>
|
||||
<td class="current">BarChart3 — vertical bars</td>
|
||||
<td class="alt">TrendingUp — growth line with arrow</td>
|
||||
<td class="alt">PieChart — pie segments</td>
|
||||
<td style="color: var(--sky)">Sky</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.addEventListener('click', () => item.classList.toggle('active'));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
83
docs/plans/Frontend/sidebar-redesign-context.md
Normal file
83
docs/plans/Frontend/sidebar-redesign-context.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Sidebar Redesign — Context & Decisions
|
||||
|
||||
> Branch: `design/sidebar-icon-concepts`
|
||||
> Date: 2026-03-15
|
||||
|
||||
## What's Already Implemented
|
||||
|
||||
### Icon Changes (committed)
|
||||
Swapped generic icons for more descriptive ones:
|
||||
- Dashboard: `LayoutGrid` (kept)
|
||||
- All Flows: `Box` → `Network`
|
||||
- Flow Editor: `PenLine` → `Wrench`
|
||||
- Sessions: `Clock` (kept)
|
||||
- Exports: `FileText` → `FileOutput`
|
||||
- AI Assistant: `BotMessageSquare` (kept)
|
||||
- Step Library: `Bookmark` → `Library`
|
||||
- Script Library: `Terminal` → `Code2`
|
||||
- KB Accelerator: `Sparkles` → `Lightbulb`
|
||||
- Analytics: `BarChart3` (kept)
|
||||
|
||||
### Semantic Icon Colors (committed)
|
||||
Each nav icon now has a permanent color (Concept 1 style):
|
||||
| Item | Color | Hex |
|
||||
|------|-------|-----|
|
||||
| Dashboard | Cyan | `#22d3ee` |
|
||||
| All Flows | Violet | `#a78bfa` |
|
||||
| Flow Editor | Amber | `#f59e0b` |
|
||||
| Sessions | Emerald | `#34d399` |
|
||||
| Exports | Blue | `#60a5fa` |
|
||||
| AI Assistant | Fuchsia | `#e879f9` |
|
||||
| Step Library | Orange | `#fb923c` |
|
||||
| Script Library | Teal | `#2dd4bf` |
|
||||
| KB Accelerator | Rose | `#fb7185` |
|
||||
| Analytics | Sky | `#38bdf8` |
|
||||
| User Guides | Lime | `#a3e635` |
|
||||
| Feedback | Indigo | `#818cf8` |
|
||||
| Account/Collapse | No color (stays muted) |
|
||||
|
||||
Colors defined in `NAV_COLORS` constant in `Sidebar.tsx`, applied via `iconColor` prop on `NavItem`.
|
||||
|
||||
### Session History Tab Reorder (committed)
|
||||
- Default tab: `Active` (was `All`)
|
||||
- Tab order: Active → Prepared → Completed → All
|
||||
|
||||
## Still Deciding
|
||||
|
||||
### Nav Grouping
|
||||
Mockups in `sidebar-grouping-concepts.html`. Four options explored:
|
||||
|
||||
**Concept A1 — Three labeled groups:**
|
||||
- Resolve: Sessions, All Flows, FlowPilot, Script Library
|
||||
- Build: Flow Editor, Flow Assist, Step Library, KB Accelerator
|
||||
- Insights: Dashboard, Exports, Analytics
|
||||
|
||||
**Concept A2 — Same groups, divider lines only (no labels)**
|
||||
|
||||
**Concept A3 — Dashboard first, then Resolve / Build / Insights**
|
||||
|
||||
**Concept B1 — Two groups only (Work / Build) with Dashboard on top**
|
||||
|
||||
### AI Assistant Split
|
||||
Currently one "AI Assistant" nav item does two jobs. Considering splitting into:
|
||||
|
||||
**In-session copilot (Resolve group):**
|
||||
- Recommended name: **FlowPilot** (already used in copilot panel)
|
||||
- Icon: Brain (fuchsia)
|
||||
- Purpose: helps during active troubleshooting sessions
|
||||
|
||||
**Flow builder (Build group):**
|
||||
- Recommended name: **Flow Assist** (already used in embedded editor AI)
|
||||
- Icon: Wand2 (pink)
|
||||
- Purpose: conversational flow creation/authoring
|
||||
|
||||
Other naming options explored: AI Copilot, Resolve AI, AI Builder, Flow Forge
|
||||
|
||||
## Files Changed
|
||||
- `frontend/src/components/layout/Sidebar.tsx` — new icons, colors, NAV_COLORS constant
|
||||
- `frontend/src/components/layout/NavItem.tsx` — added `iconColor` prop
|
||||
- `frontend/src/pages/SessionHistoryPage.tsx` — default tab + tab order
|
||||
|
||||
## Mockup Files (open in browser)
|
||||
- `docs/plans/Frontend/sidebar-icon-concepts.html` — icon + color comparison (6 panels)
|
||||
- `docs/plans/Frontend/sidebar-grouping-concepts.html` — grouping + AI naming options
|
||||
1269
docs/superpowers/plans/2026-03-15-sidebar-redesign.md
Normal file
1269
docs/superpowers/plans/2026-03-15-sidebar-redesign.md
Normal file
File diff suppressed because it is too large
Load Diff
271
docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md
Normal file
271
docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Sidebar Redesign — Design Spec
|
||||
|
||||
> **Date:** 2026-03-15
|
||||
> **Branch:** `design/sidebar-icon-concepts`
|
||||
> **Status:** Approved for implementation
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Redesign the ResolutionFlow sidebar from a flat nav list with a pinned flows section into a structured, activity-aware navigation organized around engineer workflow stages. The sidebar becomes a "shift dashboard" — showing what you're working on, how your day is going, and organizing tools by what you do with them.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Surface active work** — engineers should see their in-progress sessions and CW ticket context without navigating away
|
||||
2. **Organize by workflow** — group nav items into Resolve (working tickets) / Build (authoring flows) / Insights (reviewing results)
|
||||
3. **Provide daily pulse** — lightweight stats (resolved today, active, time in session) visible at a glance
|
||||
4. **Clarify AI tools** — split the single "AI Assistant" into two purpose-specific tools (FlowPilot for troubleshooting, Flow Assist for authoring)
|
||||
5. **Remove low-value features** — pinned/quick access flows are too static for the dynamic MSP workflow
|
||||
|
||||
## Full Sidebar Layout (top to bottom)
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Stats Bar │ ← 3 counters: Resolved / Active / In Session
|
||||
│ [7 done] [2 open] [2h] │
|
||||
├─────────────────────────┤
|
||||
│ ● Activity │ ← "Activity" label with emerald clock icon
|
||||
│ ● O365 Mail Flow #4829 │ ← green dot = active, blue ticket #
|
||||
│ ● DHCP Scope #4828 │ ← amber dot = paused/idle
|
||||
│ · · · · · · · · · · · │ ← subtle sub-divider
|
||||
│ · DNS Resolution 2h │ ← muted dot + relative time
|
||||
│ · Printer Spooler 5h │
|
||||
├─────────────────────────┤ ← glass-border divider
|
||||
│ ▣ Dashboard │ ← standalone, no group label
|
||||
│ │
|
||||
│ RESOLVE │ ← group label (JetBrains Mono, uppercase)
|
||||
│ ◷ Sessions [3] │
|
||||
│ ⬡ All Flows [32] │
|
||||
│ ◉ FlowPilot │
|
||||
│ </> Script Library │
|
||||
│ │
|
||||
│ BUILD │
|
||||
│ 🔧 Flow Editor │
|
||||
│ ✦ Flow Assist │
|
||||
│ ⊞ Step Library │
|
||||
│ 💡 KB Accelerator │
|
||||
│ │
|
||||
│ INSIGHTS │
|
||||
│ 📄 Exports │
|
||||
│ 📊 Analytics │
|
||||
│ │
|
||||
│ ── flex spacer ── │ ← pushes footer to bottom
|
||||
├─────────────────────────┤ ← glass-border divider
|
||||
│ 📖 User Guides │
|
||||
│ 💬 Feedback │
|
||||
│ ⚙ Account │
|
||||
│ ◁ Collapse │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
No brand/logo section at top (unchanged from current). Dashboard sits below the divider with no group label — it's visually separated from the activity zone by the divider and from the "Resolve" label by standard spacing (same as current "first nav item before first group" pattern).
|
||||
|
||||
## What Changes
|
||||
|
||||
### Removed
|
||||
- **Pinned Flows section** — the entire `PinnedFlowsSection` component, `pinnedFlowsStore`, and `pinnedFlows` API module. MSP engineers' work is too dynamic for static pinning; the activity feed replaces this.
|
||||
- **Single "AI Assistant" nav item** — replaced by two distinct items
|
||||
|
||||
### Added
|
||||
|
||||
#### 1. Daily Stats Bar (top of sidebar)
|
||||
Three compact counters in a horizontal row (`display: flex; gap: 2px`), each counter is a flex-1 cell.
|
||||
|
||||
| Stat | Color | Source |
|
||||
|------|-------|--------|
|
||||
| Resolved | `#34d399` (emerald) | Count of sessions completed today with `outcome = 'resolved'` |
|
||||
| Active | `#22d3ee` (cyan) | Count of sessions where `completed_at IS NULL` |
|
||||
| In Session | `#8891a0` (muted) | Sum of time spent in active sessions today (new metric — snapshot, acceptable to be stale) |
|
||||
|
||||
**Dimensions:** `padding: 8px 12px 4px` on the container. Each stat cell: `padding: 6px 4px`, `border-radius: 6px`, `background: rgba(255,255,255,0.02)`. Value: `JetBrains Mono, 14px, font-weight: 600`. Label: `JetBrains Mono, 7px, uppercase, letter-spacing: 0.1em, color: #3d4350`.
|
||||
|
||||
Stats reset daily. "In Session" is a snapshot value computed server-side — it does not live-update as a timer.
|
||||
|
||||
#### 2. Activity Feed (below stats bar)
|
||||
Two sub-sections separated by a subtle divider (`1px solid rgba(255,255,255,0.03)`):
|
||||
|
||||
**Active sessions (max 5):**
|
||||
- Green pulsing dot — currently active session
|
||||
- Amber dot (`#f59e0b`, design system amber) — paused/idle session (started but not interacted with recently)
|
||||
- Flow name (truncated, `13px`, `color: #e2e8f0`)
|
||||
- ConnectWise ticket number in blue (`#60a5fa`, `JetBrains Mono, 9px`) — only shown if the user's team has a PSA connection AND the session is linked to a ticket
|
||||
- If more than 5 active sessions, show a "View all in Sessions →" link styled as `text-muted-foreground, 11px, hover:text-foreground`
|
||||
|
||||
**Green pulsing dot animation:**
|
||||
```css
|
||||
.active-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: #34d399;
|
||||
box-shadow: 0 0 6px rgba(52,211,153,0.5);
|
||||
animation: pulse-dot 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); }
|
||||
50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); }
|
||||
}
|
||||
```
|
||||
|
||||
**Recent completions:**
|
||||
- Small muted dot (`4px`, `#3d4350`)
|
||||
- Flow name in muted text (`11.5px`, `color: #6b7280`)
|
||||
- Relative timestamp (`JetBrains Mono, 9px`, `color: #5a6170`)
|
||||
- Show last 3 completed sessions from today
|
||||
|
||||
The activity section has an "Activity" header: `JetBrains Mono, 9px, uppercase, letter-spacing: 0.12em, color: #5a6170`, with a small emerald clock SVG icon (10px).
|
||||
|
||||
**Activity feed height:** The zone scrolls with the rest of the sidebar — no independent scroll container. The overall sidebar already has `handleSidebarWheel` for scroll forwarding. With max 5 active + 3 recent, the zone is ~280px max which is acceptable.
|
||||
|
||||
**Refresh:** Data fetches on mount (same as current). No polling. Navigating back to a page that renders the sidebar triggers a re-mount/re-fetch. A future enhancement could add polling or WebSocket updates but that's out of scope.
|
||||
|
||||
#### 3. Nav Grouping (Concept A3)
|
||||
Dashboard stands alone at the top (no group label, no special treatment — just a regular `NavItem` above the first group label).
|
||||
|
||||
**Resolve** — what engineers do when working tickets:
|
||||
- Sessions (Clock, emerald `#34d399`, badge: active count)
|
||||
- All Flows (Network, violet `#a78bfa`, badge: total count, with type sub-items)
|
||||
- FlowPilot (Brain, fuchsia `#e879f9`) — NEW: in-session AI copilot
|
||||
- Script Library (Code2, teal `#2dd4bf`)
|
||||
|
||||
**Build** — authoring and improving flows:
|
||||
- Flow Editor (Wrench, amber `#f59e0b`)
|
||||
- Flow Assist (WandSparkles, pink `#f472b6`) — NEW: conversational flow builder AI
|
||||
- Step Library (Library, orange `#fb923c`)
|
||||
- KB Accelerator (Lightbulb, rose `#fb7185`)
|
||||
|
||||
**Insights** — reviewing results:
|
||||
- Exports (FileOutput, blue `#60a5fa`)
|
||||
- Analytics (BarChart3, sky `#38bdf8`)
|
||||
|
||||
Group labels use `JetBrains Mono`, `0.5625rem`, uppercase, `letter-spacing: 0.12em`, `color: #5a6170`, `padding: 14px 12px 5px` (first group: `padding-top: 8px`).
|
||||
|
||||
#### 4. AI Assistant Split
|
||||
|
||||
| Nav Item | Group | Icon | Color | Purpose | Route |
|
||||
|----------|-------|------|-------|---------|-------|
|
||||
| FlowPilot | Resolve | Brain | `#e879f9` (fuchsia) | In-session AI copilot — suggests next steps, explains errors, provides ticket context during active troubleshooting | `/assistant` (existing route, rename label in UI only) |
|
||||
| Flow Assist | Build | WandSparkles | `#f472b6` (pink) | Conversational flow builder — standalone page with a chat interface for generating flows from natural language. Renders a full-page chat UI (similar to `AssistantChatPage`) that creates flows via the AI chat service with `flow_type` selection. NOT a modal wrapper — it's a dedicated page. | `/flow-assist` (new route + new page component `FlowAssistPage`) |
|
||||
|
||||
Both names are already in use internally (FlowPilot in the copilot panel, Flow Assist in the editor AI panel). This just promotes them to top-level navigation.
|
||||
|
||||
**Note:** `Wand2` is an alias for `WandSparkles` in lucide-react@0.563.0. Use `WandSparkles` as the canonical import.
|
||||
|
||||
### Footer (unchanged structure)
|
||||
- User Guides (BookOpen, lime `#a3e635`)
|
||||
- Feedback (MessageSquareText, indigo `#818cf8`)
|
||||
- Account (Settings, no color)
|
||||
- Collapse (PanelLeftClose, no color)
|
||||
|
||||
### Collapsed Sidebar
|
||||
When collapsed, the stats bar and activity feed are hidden. Icon-only nav items are shown in a flat list (no group labels). The collapsed view includes all 13 nav items including the two new AI items:
|
||||
|
||||
- Dashboard (LayoutGrid, cyan)
|
||||
- Sessions (Clock, emerald)
|
||||
- All Flows (Network, violet)
|
||||
- FlowPilot (Brain, fuchsia)
|
||||
- Script Library (Code2, teal)
|
||||
- Flow Editor (Wrench, amber)
|
||||
- Flow Assist (WandSparkles, pink)
|
||||
- Step Library (Library, orange)
|
||||
- KB Accelerator (Lightbulb, rose)
|
||||
- Exports (FileOutput, blue)
|
||||
- Analytics (BarChart3, sky)
|
||||
- User Guides (BookOpen, lime)
|
||||
- Feedback (MessageSquareText, indigo)
|
||||
|
||||
Account and Collapse remain in the footer section of the collapsed view (same as current).
|
||||
|
||||
## Data Requirements
|
||||
|
||||
### New Backend Endpoint
|
||||
|
||||
**`GET /api/v1/sessions/sidebar-stats`** — lightweight endpoint for sidebar data, returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"resolved_today": 7,
|
||||
"active_count": 2,
|
||||
"total_session_minutes_today": 134,
|
||||
"active_sessions": [
|
||||
{
|
||||
"session_id": "uuid",
|
||||
"tree_name": "O365 Mail Flow",
|
||||
"tree_id": "uuid",
|
||||
"tree_type": "troubleshooting",
|
||||
"started_at": "2026-03-15T14:30:00Z",
|
||||
"ticket_number": "#48291"
|
||||
}
|
||||
],
|
||||
"recent_completions": [
|
||||
{
|
||||
"session_id": "uuid",
|
||||
"tree_name": "DNS Resolution",
|
||||
"tree_id": "uuid",
|
||||
"tree_type": "troubleshooting",
|
||||
"completed_at": "2026-03-15T12:15:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `tz_offset` (integer, required): Client's UTC offset in minutes (e.g., `-300` for EST). Used to compute "today" boundaries. Frontend sends `new Date().getTimezoneOffset()`.
|
||||
|
||||
**Backend logic:**
|
||||
- `resolved_today`: `SELECT COUNT(*) FROM sessions WHERE user_id = :uid AND completed_at >= :today_start AND outcome = 'resolved'`
|
||||
- `active_count`: `SELECT COUNT(*) FROM sessions WHERE user_id = :uid AND completed_at IS NULL`
|
||||
- `total_session_minutes_today`: Sum of `EXTRACT(EPOCH FROM (COALESCE(completed_at, now()) - started_at)) / 60` for all sessions where `started_at >= :today_start`
|
||||
- `active_sessions`: Up to 5, ordered by `started_at DESC`, with joined tree name/type
|
||||
- `recent_completions`: Up to 3 completed today, ordered by `completed_at DESC`
|
||||
- `ticket_number`: Populated from PSA connection context if available (depends on CW integration state — nullable)
|
||||
- `today_start`: Computed as midnight in the client's timezone, converted to UTC using `tz_offset`
|
||||
|
||||
**Sessions spanning midnight:** A session started yesterday that is still active today counts toward `total_session_minutes_today` for its full duration (from `started_at`, not from midnight). It also appears in `active_sessions`. This matches user intuition — "I've been on this for 3 hours" regardless of when midnight fell.
|
||||
|
||||
### Frontend Components
|
||||
|
||||
**New components:**
|
||||
- `SidebarStatsBar` — three-stat row component
|
||||
- `SidebarActivityFeed` — active sessions + recents feed with "Activity" header
|
||||
- `ActivityItem` — single activity row (dot + name + ticket/timestamp)
|
||||
- `FlowAssistPage` — standalone page for conversational flow building
|
||||
|
||||
**Modified components:**
|
||||
- `Sidebar.tsx` — major restructure: remove PinnedFlowsSection, add stats bar + activity feed, reorganize nav into groups with labels, add FlowPilot + Flow Assist items, update collapsed view icon set
|
||||
- `router.tsx` — add `/flow-assist` route
|
||||
- `NavItem.tsx` — no changes needed (already supports `iconColor` prop)
|
||||
|
||||
**Removed:**
|
||||
- `PinnedFlowsSection.tsx`
|
||||
- `pinnedFlowsStore.ts`
|
||||
- `api/pinnedFlows.ts` (if it exists)
|
||||
- Related pinning UI in flow cards/pages (pin buttons, pin actions)
|
||||
|
||||
## Visual Reference
|
||||
|
||||
Mockups created during brainstorming are in `.superpowers/brainstorm/` (HTML files, open in browser). The approved composite is `a3-full-sidebar.html` showing the "With AI Split" variant.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- ConnectWise ticket deep-linking (future — just display the number for now)
|
||||
- Team activity feed (showing what other engineers are doing — potential future enhancement)
|
||||
- Removing the pinned flows backend API/database tables (just remove frontend usage; backend cleanup is separate)
|
||||
- Changes to the analytics pages themselves
|
||||
- Mobile/responsive sidebar behavior (current behavior unchanged)
|
||||
- Polling/WebSocket for live activity updates (fetch on mount only)
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **No active sessions:** Stats bar shows "0" for Active and "0m" for In Session. Activity section shows only recents (or "No activity today" placeholder if no recents either).
|
||||
- **No CW integration:** Ticket numbers simply don't appear. Activity items show flow name only.
|
||||
- **Many active sessions:** Activity feed caps at 5 active sessions with a "View all in Sessions →" link below.
|
||||
- **Midnight rollover:** Stats reset based on client timezone offset passed to the API. Frontend sends `tz_offset` on each sidebar fetch.
|
||||
- **Sessions spanning midnight:** Count full duration from `started_at`, not partial duration from midnight.
|
||||
- **New user / no sessions ever:** Stats bar shows all zeros. Activity section shows an empty state: "No activity today".
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Pulsing green dot: add `aria-label="Active session"` to the dot element
|
||||
- Amber dot: `aria-label="Paused session"`
|
||||
- Stats bar values: wrap in elements with `aria-label` combining value + label (e.g., "7 resolved today")
|
||||
- Activity items: render as `<button>` elements (clickable to navigate to session) with descriptive `title` attributes
|
||||
@@ -11,7 +11,6 @@ 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 pinnedFlowsApi } from './pinnedFlows'
|
||||
export { default as analyticsApi } from './analytics'
|
||||
export { targetListsApi } from './targetLists'
|
||||
export { maintenanceSchedulesApi, batchLaunchApi } from './maintenanceSchedules'
|
||||
@@ -23,3 +22,4 @@ export { flowTransferApi } from './flowTransfer'
|
||||
export { kbAcceleratorApi } from './kbAccelerator'
|
||||
export { scriptsApi } from './scripts'
|
||||
export { integrationsApi, sessionPsaApi } from './integrations'
|
||||
export { sidebarApi } from './sidebar'
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
export interface PinnedFlow {
|
||||
id: string
|
||||
tree_id: string
|
||||
tree_name: string
|
||||
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
|
||||
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<PinnedFlowsResponse> => {
|
||||
const { data } = await apiClient.get('/trees/pinned')
|
||||
return data
|
||||
},
|
||||
|
||||
unpin: async (treeId: string): Promise<void> => {
|
||||
await apiClient.delete(`/trees/${treeId}/pin`)
|
||||
},
|
||||
|
||||
pin: async (treeId: string): Promise<void> => {
|
||||
await apiClient.post(`/trees/${treeId}/pin`)
|
||||
},
|
||||
}
|
||||
|
||||
export default pinnedFlowsApi
|
||||
@@ -31,6 +31,7 @@ export const sessionsApi = {
|
||||
|
||||
async create(data: SessionCreate): Promise<Session> {
|
||||
const response = await apiClient.post<Session>('/sessions', data)
|
||||
window.dispatchEvent(new Event('session-changed'))
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -41,6 +42,7 @@ export const sessionsApi = {
|
||||
|
||||
async complete(id: string, data: SessionComplete): Promise<Session> {
|
||||
const response = await apiClient.post<Session>(`/sessions/${id}/complete`, data)
|
||||
window.dispatchEvent(new Event('session-changed'))
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
45
frontend/src/api/sidebar.ts
Normal file
45
frontend/src/api/sidebar.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import apiClient from './client'
|
||||
|
||||
export interface SidebarActiveSession {
|
||||
session_id: string
|
||||
tree_name: string
|
||||
tree_id: string
|
||||
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
|
||||
started_at: string
|
||||
ticket_number: string | null
|
||||
psa_ticket_id: string | null
|
||||
}
|
||||
|
||||
export interface SidebarRecentSession {
|
||||
session_id: string
|
||||
tree_name: string
|
||||
tree_id: string
|
||||
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
|
||||
completed_at: string
|
||||
}
|
||||
|
||||
export interface SidebarTreeCounts {
|
||||
total: number
|
||||
troubleshooting: number
|
||||
procedural: number
|
||||
maintenance: number
|
||||
}
|
||||
|
||||
export interface SidebarStatsResponse {
|
||||
resolved_today: number
|
||||
active_count: number
|
||||
total_session_minutes_today: number
|
||||
tree_counts: SidebarTreeCounts
|
||||
active_sessions: SidebarActiveSession[]
|
||||
recent_completions: SidebarRecentSession[]
|
||||
}
|
||||
|
||||
export const sidebarApi = {
|
||||
getStats: async (): Promise<SidebarStatsResponse> => {
|
||||
const tzOffset = new Date().getTimezoneOffset()
|
||||
const response = await apiClient.get<SidebarStatsResponse>(
|
||||
`/sessions/sidebar-stats?tz_offset=${tzOffset}`
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
@@ -15,12 +15,13 @@ interface NavItemProps {
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
badge?: number | 'dot'
|
||||
iconColor?: string
|
||||
matchPaths?: string[]
|
||||
collapsed?: boolean
|
||||
children?: NavSubItem[]
|
||||
}
|
||||
|
||||
export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed, children }: NavItemProps) {
|
||||
export function NavItem({ href, icon: Icon, label, badge, iconColor, matchPaths, collapsed, children }: NavItemProps) {
|
||||
const location = useLocation()
|
||||
const fullPath = location.pathname + location.search
|
||||
const isActive = matchPaths
|
||||
@@ -49,7 +50,7 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
|
||||
)}
|
||||
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} />
|
||||
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} style={iconColor ? { color: iconColor } : undefined} />
|
||||
{badge !== undefined && badge !== 0 && badge !== 'dot' && (
|
||||
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[0.5rem] font-bold text-primary-foreground">
|
||||
{badge}
|
||||
@@ -78,7 +79,7 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed,
|
||||
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
|
||||
)}
|
||||
|
||||
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} />
|
||||
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} style={iconColor ? { color: iconColor } : undefined} />
|
||||
<span className="truncate">{label}</span>
|
||||
|
||||
{/* Badge */}
|
||||
|
||||
@@ -1,50 +1,57 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText, BotMessageSquare, BookOpen, Sparkles, Terminal } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import {
|
||||
LayoutGrid, Network, Wrench, Clock, FileOutput, BarChart3,
|
||||
Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText,
|
||||
BookOpen, Lightbulb, Code2, Library, Brain, WandSparkles,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
|
||||
import { PinnedFlowsSection } from '@/components/sidebar/PinnedFlowsSection'
|
||||
import { sidebarApi } from '@/api'
|
||||
import type { SidebarStatsResponse } from '@/api/sidebar'
|
||||
import { SidebarStatsBar } from '@/components/sidebar/SidebarStatsBar'
|
||||
import { SidebarActivityFeed } from '@/components/sidebar/SidebarActivityFeed'
|
||||
import { NavItem } from './NavItem'
|
||||
import { sessionsApi, treesApi } from '@/api'
|
||||
|
||||
// Semantic icon colors — each nav item gets a unique color for visual landmarks
|
||||
const NAV_COLORS = {
|
||||
dashboard: '#22d3ee', // cyan-400
|
||||
flows: '#a78bfa', // violet-400
|
||||
editor: '#f59e0b', // amber-500
|
||||
sessions: '#34d399', // emerald-400
|
||||
exports: '#60a5fa', // blue-400
|
||||
flowPilot: '#e879f9', // fuchsia-400
|
||||
flowAssist:'#f472b6', // pink-400
|
||||
stepLib: '#fb923c', // orange-400
|
||||
scripts: '#2dd4bf', // teal-400
|
||||
kb: '#fb7185', // rose-400
|
||||
analytics: '#38bdf8', // sky-400
|
||||
guides: '#a3e635', // lime-400
|
||||
feedback: '#818cf8', // indigo-400
|
||||
} as const
|
||||
|
||||
export function Sidebar() {
|
||||
const sidebarCollapsed = useUserPreferencesStore(s => s.sidebarCollapsed)
|
||||
const toggleSidebar = useUserPreferencesStore(s => s.toggleSidebar)
|
||||
const location = useLocation()
|
||||
|
||||
const pinnedItems = usePinnedFlowsStore((s) => s.items)
|
||||
const loadPinned = usePinnedFlowsStore((s) => s.load)
|
||||
const unpinFlow = usePinnedFlowsStore((s) => s.unpin)
|
||||
const [stats, setStats] = useState<SidebarStatsResponse | null>(null)
|
||||
|
||||
const [activeSessionCount, setActiveSessionCount] = useState(0)
|
||||
const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0, maintenance: 0 })
|
||||
|
||||
// Load pinned flows on mount
|
||||
useEffect(() => {
|
||||
loadPinned()
|
||||
}, [loadPinned])
|
||||
|
||||
// Fetch sidebar data on mount
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [activeSessions, allTrees] = await Promise.all([
|
||||
sessionsApi.list({ completed: false, size: 50 }).catch(() => []),
|
||||
treesApi.list({ sort_by: 'name' }).catch(() => []),
|
||||
])
|
||||
setActiveSessionCount(activeSessions.length)
|
||||
|
||||
const total = allTrees.length
|
||||
const troubleshooting = allTrees.filter(t => t.tree_type === 'troubleshooting').length
|
||||
const procedural = allTrees.filter(t => t.tree_type === 'procedural').length
|
||||
const maintenance = allTrees.filter(t => t.tree_type === 'maintenance').length
|
||||
setTreeCounts({ total, troubleshooting, procedural, maintenance })
|
||||
} catch {
|
||||
// Silently handle errors
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
const refreshStats = useCallback(() => {
|
||||
sidebarApi.getStats().then(setStats).catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Fetch sidebar stats — refreshes on navigation
|
||||
useEffect(() => {
|
||||
refreshStats()
|
||||
}, [location.pathname, refreshStats])
|
||||
|
||||
// Refresh when sessions are created or completed
|
||||
useEffect(() => {
|
||||
window.addEventListener('session-changed', refreshStats)
|
||||
return () => window.removeEventListener('session-changed', refreshStats)
|
||||
}, [refreshStats])
|
||||
|
||||
const handleSidebarWheel = (e: React.WheelEvent<HTMLElement>) => {
|
||||
const sidebar = e.currentTarget
|
||||
const canSidebarScroll = sidebar.scrollHeight > sidebar.clientHeight
|
||||
@@ -76,50 +83,81 @@ export function Sidebar() {
|
||||
<>
|
||||
{/* Collapsed: icon-only nav */}
|
||||
<div className="flex flex-col items-center px-1.5 py-3 space-y-1">
|
||||
<NavItem href="/" icon={LayoutGrid} label="Dashboard" collapsed />
|
||||
<NavItem href="/trees" icon={Box} label="All Flows" matchPaths={['/trees', '/flows']} collapsed />
|
||||
<NavItem href="/my-trees" icon={PenLine} label="Flow Editor" collapsed />
|
||||
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={activeSessionCount || undefined} collapsed />
|
||||
<NavItem href="/shares" icon={FileText} label="Exports" collapsed />
|
||||
<NavItem href="/assistant" icon={BotMessageSquare} label="AI Assistant" collapsed />
|
||||
<NavItem href="/step-library" icon={Bookmark} label="Step Library" collapsed />
|
||||
<NavItem href="/scripts" icon={Terminal} label="Script Library" collapsed />
|
||||
<NavItem href="/kb-accelerator" icon={Sparkles} label="KB Accelerator" collapsed />
|
||||
<NavItem href="/analytics" icon={BarChart3} label="Analytics" collapsed />
|
||||
<NavItem href="/guides" icon={BookOpen} label="User Guides" collapsed />
|
||||
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" collapsed />
|
||||
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} collapsed />
|
||||
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} collapsed />
|
||||
<NavItem href="/trees" icon={Network} label="All Flows" matchPaths={['/trees', '/flows']} iconColor={NAV_COLORS.flows} collapsed />
|
||||
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} collapsed />
|
||||
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} collapsed />
|
||||
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} collapsed />
|
||||
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} collapsed />
|
||||
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} collapsed />
|
||||
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} collapsed />
|
||||
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} collapsed />
|
||||
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} collapsed />
|
||||
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} collapsed />
|
||||
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} collapsed />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Pinned Flows */}
|
||||
<PinnedFlowsSection flows={pinnedItems} onUnpin={unpinFlow} />
|
||||
{/* Stats Bar */}
|
||||
<SidebarStatsBar
|
||||
resolved={stats?.resolved_today ?? 0}
|
||||
active={stats?.active_count ?? 0}
|
||||
completedMinutes={stats?.total_session_minutes_today ?? 0}
|
||||
activeSessionStartTimes={stats?.active_sessions.map(s => s.started_at) ?? []}
|
||||
/>
|
||||
|
||||
{/* Activity Feed */}
|
||||
<SidebarActivityFeed
|
||||
activeSessions={stats?.active_sessions ?? []}
|
||||
recentCompletions={stats?.recent_completions ?? []}
|
||||
totalActive={stats?.active_count ?? 0}
|
||||
/>
|
||||
|
||||
<div style={{ borderBottom: '1px solid var(--glass-border)' }} />
|
||||
|
||||
{/* Primary Navigation */}
|
||||
{/* Navigation */}
|
||||
<div className="px-3 py-2 space-y-0.5">
|
||||
<NavItem href="/" icon={LayoutGrid} label="Dashboard" />
|
||||
{/* Dashboard (standalone) */}
|
||||
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} />
|
||||
|
||||
{/* Resolve */}
|
||||
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
||||
Resolve
|
||||
</div>
|
||||
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} />
|
||||
<NavItem
|
||||
href="/trees"
|
||||
icon={Box}
|
||||
icon={Network}
|
||||
label="All Flows"
|
||||
badge={treeCounts.total || undefined}
|
||||
badge={stats?.tree_counts.total || undefined}
|
||||
iconColor={NAV_COLORS.flows}
|
||||
matchPaths={['/trees', '/flows']}
|
||||
children={[
|
||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: treeCounts.troubleshooting || undefined },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: treeCounts.procedural || undefined },
|
||||
{ href: '/trees?type=maintenance', label: 'Maintenance', count: treeCounts.maintenance || undefined },
|
||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
{ href: '/trees?type=maintenance', label: 'Maintenance', count: stats?.tree_counts.maintenance || undefined },
|
||||
]}
|
||||
/>
|
||||
<NavItem href="/my-trees" icon={PenLine} label="Flow Editor" />
|
||||
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={activeSessionCount || undefined} />
|
||||
<NavItem href="/shares" icon={FileText} label="Exports" />
|
||||
<NavItem href="/assistant" icon={BotMessageSquare} label="AI Assistant" />
|
||||
<NavItem href="/step-library" icon={Bookmark} label="Step Library" />
|
||||
<NavItem href="/scripts" icon={Terminal} label="Script Library" />
|
||||
<NavItem href="/kb-accelerator" icon={Sparkles} label="KB Accelerator" />
|
||||
<NavItem href="/analytics" icon={BarChart3} label="Analytics" />
|
||||
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} />
|
||||
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} />
|
||||
|
||||
{/* Build */}
|
||||
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
||||
Build
|
||||
</div>
|
||||
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} />
|
||||
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} />
|
||||
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} />
|
||||
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} />
|
||||
|
||||
{/* Insights */}
|
||||
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
|
||||
Insights
|
||||
</div>
|
||||
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} />
|
||||
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -137,8 +175,8 @@ export function Sidebar() {
|
||||
>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<NavItem href="/guides" icon={BookOpen} label="User Guides" />
|
||||
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" />
|
||||
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} />
|
||||
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} />
|
||||
<NavItem href="/account" icon={Settings} label="Account" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Star, Download, ClipboardList } from 'lucide-react'
|
||||
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Download, ClipboardList } from 'lucide-react'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import { TagBadges } from '@/components/common/TagBadges'
|
||||
import { StaggerList } from '@/components/common/StaggerList'
|
||||
@@ -16,9 +16,6 @@ interface TreeGridViewProps {
|
||||
onDeleteTree: (tree: TreeListItem) => void
|
||||
onForkTree?: (treeId: string) => void
|
||||
onExportTree?: (treeId: string) => void
|
||||
pinnedTreeIds?: Set<string>
|
||||
onTogglePin?: (treeId: string) => void
|
||||
pinLoadingTreeIds?: Set<string>
|
||||
}
|
||||
|
||||
export function TreeGridView({
|
||||
@@ -29,9 +26,6 @@ export function TreeGridView({
|
||||
onDeleteTree,
|
||||
onForkTree,
|
||||
onExportTree,
|
||||
pinnedTreeIds,
|
||||
onTogglePin,
|
||||
pinLoadingTreeIds,
|
||||
}: TreeGridViewProps) {
|
||||
const { canEditTree, canDeleteTree } = usePermissions()
|
||||
|
||||
@@ -64,26 +58,6 @@ export function TreeGridView({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{onTogglePin && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onTogglePin(tree.id)
|
||||
}}
|
||||
disabled={pinLoadingTreeIds?.has(tree.id)}
|
||||
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
className={cn(
|
||||
'rounded-md p-1 transition-colors',
|
||||
pinnedTreeIds?.has(tree.id)
|
||||
? 'text-amber-400 hover:text-amber-300'
|
||||
: 'text-muted-foreground/40 hover:text-amber-400',
|
||||
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<Star size={14} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
)}
|
||||
{tree.is_public ? (
|
||||
<span title="Public tree">
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench, Star, Download, ClipboardList } from 'lucide-react'
|
||||
import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench, Download, ClipboardList } from 'lucide-react'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import { TagBadges } from '@/components/common/TagBadges'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -15,9 +15,6 @@ interface TreeListViewProps {
|
||||
onDeleteTree: (tree: TreeListItem) => void
|
||||
onForkTree?: (treeId: string) => void
|
||||
onExportTree?: (treeId: string) => void
|
||||
pinnedTreeIds?: Set<string>
|
||||
onTogglePin?: (treeId: string) => void
|
||||
pinLoadingTreeIds?: Set<string>
|
||||
}
|
||||
|
||||
export function TreeListView({
|
||||
@@ -28,9 +25,6 @@ export function TreeListView({
|
||||
onDeleteTree,
|
||||
onForkTree,
|
||||
onExportTree,
|
||||
pinnedTreeIds,
|
||||
onTogglePin,
|
||||
pinLoadingTreeIds,
|
||||
}: TreeListViewProps) {
|
||||
const { canEditTree } = usePermissions()
|
||||
|
||||
@@ -99,26 +93,6 @@ export function TreeListView({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{onTogglePin && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onTogglePin(tree.id)
|
||||
}}
|
||||
disabled={pinLoadingTreeIds?.has(tree.id)}
|
||||
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
className={cn(
|
||||
'shrink-0 rounded-md p-1 transition-colors',
|
||||
pinnedTreeIds?.has(tree.id)
|
||||
? 'text-amber-400 hover:text-amber-300'
|
||||
: 'text-muted-foreground/40 hover:text-amber-400',
|
||||
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<Star size={16} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
)}
|
||||
{onExportTree && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Wrench, Star, Download, ClipboardList } from 'lucide-react'
|
||||
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Wrench, Download, ClipboardList } from 'lucide-react'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import { TagBadges } from '@/components/common/TagBadges'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -17,9 +17,6 @@ interface TreeTableViewProps {
|
||||
onSortChange?: (sortBy: string) => void
|
||||
onForkTree?: (treeId: string) => void
|
||||
onExportTree?: (treeId: string) => void
|
||||
pinnedTreeIds?: Set<string>
|
||||
onTogglePin?: (treeId: string) => void
|
||||
pinLoadingTreeIds?: Set<string>
|
||||
}
|
||||
|
||||
type SortColumn = 'name' | 'category' | 'version' | 'usage' | 'updated'
|
||||
@@ -33,9 +30,6 @@ export function TreeTableView({
|
||||
onSortChange,
|
||||
onForkTree,
|
||||
onExportTree,
|
||||
pinnedTreeIds,
|
||||
onTogglePin,
|
||||
pinLoadingTreeIds,
|
||||
}: TreeTableViewProps) {
|
||||
const { canEditTree } = usePermissions()
|
||||
const [sortColumn, setSortColumn] = useState<SortColumn | null>(null)
|
||||
@@ -83,11 +77,6 @@ export function TreeTableView({
|
||||
<table className="w-full">
|
||||
<thead className="bg-accent/50 sticky top-0 z-10">
|
||||
<tr className="border-b border-border">
|
||||
{onTogglePin && (
|
||||
<th className="w-10 px-2 py-3 text-center">
|
||||
<Star size={14} className="inline text-muted-foreground" />
|
||||
</th>
|
||||
)}
|
||||
<th
|
||||
className="px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
onClick={() => handleSort('name')}
|
||||
@@ -147,28 +136,6 @@ export function TreeTableView({
|
||||
<tbody className="bg-transparent">
|
||||
{trees.map((tree) => (
|
||||
<tr key={tree.id} className="border-b border-border last:border-0 hover:bg-accent/50">
|
||||
{onTogglePin && (
|
||||
<td className="w-10 px-2 py-3 text-center">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onTogglePin(tree.id)
|
||||
}}
|
||||
disabled={pinLoadingTreeIds?.has(tree.id)}
|
||||
aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
className={cn(
|
||||
'rounded-md p-1 transition-colors',
|
||||
pinnedTreeIds?.has(tree.id)
|
||||
? 'text-amber-400 hover:text-amber-300'
|
||||
: 'text-muted-foreground/40 hover:text-amber-400',
|
||||
pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<Star size={14} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground truncate max-w-[200px]">
|
||||
|
||||
105
frontend/src/components/sidebar/ActivityItem.tsx
Normal file
105
frontend/src/components/sidebar/ActivityItem.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { getTreeNavigatePath } from '@/lib/routing'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ActivityItemProps {
|
||||
sessionId: string
|
||||
treeName: string
|
||||
treeId: string
|
||||
treeType: 'troubleshooting' | 'procedural' | 'maintenance'
|
||||
status: 'active' | 'paused' | 'recent'
|
||||
ticketNumber?: string | null
|
||||
timestamp?: string | null
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateString: string): string {
|
||||
const now = Date.now()
|
||||
const then = new Date(dateString).getTime()
|
||||
const diffMinutes = Math.floor((now - then) / 60000)
|
||||
|
||||
if (diffMinutes < 1) return 'just now'
|
||||
if (diffMinutes < 60) return `${diffMinutes}m ago`
|
||||
const diffHours = Math.floor(diffMinutes / 60)
|
||||
if (diffHours < 24) return `${diffHours}h ago`
|
||||
return 'yesterday'
|
||||
}
|
||||
|
||||
export function ActivityItem({
|
||||
sessionId,
|
||||
treeName,
|
||||
treeId,
|
||||
treeType,
|
||||
status,
|
||||
ticketNumber,
|
||||
timestamp,
|
||||
}: ActivityItemProps) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(getTreeNavigatePath(treeId, treeType), {
|
||||
state: { sessionId },
|
||||
})
|
||||
}
|
||||
|
||||
const isRecent = status === 'recent'
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors',
|
||||
'hover:bg-[rgba(255,255,255,0.03)]',
|
||||
isRecent ? 'text-[#6b7280] text-[0.72rem]' : 'text-[#e2e8f0] text-[0.8rem]'
|
||||
)}
|
||||
title={`${treeName}${ticketNumber ? ` (${ticketNumber})` : ''} — click to resume`}
|
||||
aria-label={
|
||||
status === 'active'
|
||||
? `Active session: ${treeName}`
|
||||
: status === 'paused'
|
||||
? `Paused session: ${treeName}`
|
||||
: `Recent session: ${treeName}`
|
||||
}
|
||||
>
|
||||
{/* Status dot */}
|
||||
{status === 'active' && (
|
||||
<span
|
||||
className="h-[7px] w-[7px] shrink-0 rounded-full"
|
||||
style={{
|
||||
background: '#34d399',
|
||||
boxShadow: '0 0 6px rgba(52,211,153,0.5)',
|
||||
animation: 'pulse-dot 2s ease-in-out infinite',
|
||||
}}
|
||||
aria-label="Active session"
|
||||
/>
|
||||
)}
|
||||
{status === 'paused' && (
|
||||
<span
|
||||
className="h-[7px] w-[7px] shrink-0 rounded-full"
|
||||
style={{
|
||||
background: '#f59e0b',
|
||||
boxShadow: '0 0 4px rgba(245,158,11,0.3)',
|
||||
}}
|
||||
aria-label="Paused session"
|
||||
/>
|
||||
)}
|
||||
{status === 'recent' && (
|
||||
<span className="h-1 w-1 shrink-0 rounded-full bg-[#3d4350]" />
|
||||
)}
|
||||
|
||||
{/* Flow name */}
|
||||
<span className="flex-1 truncate">{treeName}</span>
|
||||
|
||||
{/* Ticket number or timestamp */}
|
||||
{ticketNumber && !isRecent && (
|
||||
<span className="shrink-0 font-label text-[0.5625rem] text-[#60a5fa]">
|
||||
{ticketNumber}
|
||||
</span>
|
||||
)}
|
||||
{isRecent && timestamp && (
|
||||
<span className="shrink-0 font-label text-[0.5625rem] text-[#5a6170]">
|
||||
{formatRelativeTime(timestamp)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ChevronDown, ChevronRight, Pin } from 'lucide-react'
|
||||
import { getTreeNavigatePath } from '@/lib/routing'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { PinnedFlow } from '@/api/pinnedFlows'
|
||||
|
||||
interface PinnedFlowsSectionProps {
|
||||
flows: PinnedFlow[]
|
||||
onUnpin: (treeId: string) => void
|
||||
}
|
||||
|
||||
const TRUNCATE_COUNT = 5
|
||||
|
||||
export function PinnedFlowsSection({ flows, onUnpin }: PinnedFlowsSectionProps) {
|
||||
const navigate = useNavigate()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
|
||||
const handleToggleCollapse = () => {
|
||||
if (collapsed) {
|
||||
setShowAll(false) // Reset to truncated on re-expand
|
||||
}
|
||||
setCollapsed(!collapsed)
|
||||
}
|
||||
|
||||
const visibleFlows = showAll ? flows : flows.slice(0, TRUNCATE_COUNT)
|
||||
const hasMore = flows.length > TRUNCATE_COUNT
|
||||
|
||||
const handleFlowClick = (flow: PinnedFlow) => {
|
||||
setShowAll(false) // Collapse back to 5 on navigation
|
||||
navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2">
|
||||
<button
|
||||
onClick={handleToggleCollapse}
|
||||
className="flex w-full items-center gap-1 px-3 mb-1 font-heading text-[0.6875rem] font-bold uppercase tracking-[0.04em] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{collapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
|
||||
Pinned
|
||||
{flows.length > 0 && (
|
||||
<span className="ml-auto text-[0.625rem] font-normal">{flows.length}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="overflow-hidden transition-[max-height] duration-250 ease-out"
|
||||
style={{
|
||||
maxHeight: collapsed ? 0 : showAll
|
||||
? `${flows.length * 36 + 40}px`
|
||||
: `${Math.min(flows.length, TRUNCATE_COUNT) * 36 + 40}px`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
{flows.length === 0 ? (
|
||||
<p className="px-3 py-2 text-xs text-muted-foreground">
|
||||
Pin your most-used flows here
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{visibleFlows.map(flow => (
|
||||
<button
|
||||
key={flow.tree_id}
|
||||
onClick={() => handleFlowClick(flow)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
onUnpin(flow.tree_id)
|
||||
}}
|
||||
className={cn(
|
||||
'group flex w-full items-center gap-2.5 rounded-lg px-3 py-1.5 text-[0.8125rem] font-medium transition-colors',
|
||||
'text-muted-foreground hover:bg-[var(--sidebar-hover)] hover:text-foreground'
|
||||
)}
|
||||
title={`${flow.tree_name} (right-click to unpin)`}
|
||||
>
|
||||
<span className="text-sm shrink-0">
|
||||
{flow.tree_type === 'procedural' ? '📋' : flow.tree_type === 'maintenance' ? '🛠️' : '🔧'}
|
||||
</span>
|
||||
<span className="truncate flex-1 text-left">{flow.tree_name}</span>
|
||||
<Pin size={12} className="shrink-0 opacity-0 group-hover:opacity-40 transition-opacity" />
|
||||
</button>
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
className="w-full px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors text-left"
|
||||
>
|
||||
{showAll ? 'Show less' : `Show more (${flows.length})`}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
80
frontend/src/components/sidebar/SidebarActivityFeed.tsx
Normal file
80
frontend/src/components/sidebar/SidebarActivityFeed.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Clock } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ActivityItem } from './ActivityItem'
|
||||
import type { SidebarActiveSession, SidebarRecentSession } from '@/api/sidebar'
|
||||
|
||||
interface SidebarActivityFeedProps {
|
||||
activeSessions: SidebarActiveSession[]
|
||||
recentCompletions: SidebarRecentSession[]
|
||||
totalActive: number
|
||||
}
|
||||
|
||||
export function SidebarActivityFeed({
|
||||
activeSessions,
|
||||
recentCompletions,
|
||||
totalActive,
|
||||
}: SidebarActivityFeedProps) {
|
||||
const navigate = useNavigate()
|
||||
const hasActivity = activeSessions.length > 0 || recentCompletions.length > 0
|
||||
|
||||
return (
|
||||
<div className="px-3 pb-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 mb-0.5">
|
||||
<Clock size={10} style={{ color: '#34d399' }} />
|
||||
<span className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170]">
|
||||
Activity
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!hasActivity ? (
|
||||
<p className="px-2.5 py-2 text-xs text-muted-foreground">
|
||||
No activity today
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{/* Active sessions */}
|
||||
{activeSessions.map((session) => (
|
||||
<ActivityItem
|
||||
key={session.session_id}
|
||||
sessionId={session.session_id}
|
||||
treeName={session.tree_name}
|
||||
treeId={session.tree_id}
|
||||
treeType={session.tree_type}
|
||||
status="active"
|
||||
ticketNumber={session.ticket_number || session.psa_ticket_id}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Overflow link */}
|
||||
{totalActive > 5 && (
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="w-full px-2.5 py-1 text-left text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
View all in Sessions →
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Divider between active and recent */}
|
||||
{activeSessions.length > 0 && recentCompletions.length > 0 && (
|
||||
<div className="mx-2.5 my-1" style={{ height: '1px', background: 'rgba(255,255,255,0.03)' }} />
|
||||
)}
|
||||
|
||||
{/* Recent completions */}
|
||||
{recentCompletions.map((session) => (
|
||||
<ActivityItem
|
||||
key={session.session_id}
|
||||
sessionId={session.session_id}
|
||||
treeName={session.tree_name}
|
||||
treeId={session.tree_id}
|
||||
treeType={session.tree_type}
|
||||
status="recent"
|
||||
timestamp={session.completed_at}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
frontend/src/components/sidebar/SidebarStatsBar.tsx
Normal file
93
frontend/src/components/sidebar/SidebarStatsBar.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface SidebarStatsBarProps {
|
||||
resolved: number
|
||||
active: number
|
||||
/** Minutes from completed sessions today (server-computed) */
|
||||
completedMinutes: number
|
||||
/** Start times of currently active sessions (ISO strings) */
|
||||
activeSessionStartTimes: string[]
|
||||
}
|
||||
|
||||
function formatDuration(totalSeconds: number): string {
|
||||
if (totalSeconds < 60) return `${totalSeconds}s`
|
||||
const totalMinutes = Math.floor(totalSeconds / 60)
|
||||
if (totalMinutes < 60) return `${totalMinutes}m`
|
||||
const h = Math.floor(totalMinutes / 60)
|
||||
const m = totalMinutes % 60
|
||||
return m > 0 ? `${h}h ${m}m` : `${h}h`
|
||||
}
|
||||
|
||||
function parseUTCTimestamp(st: string): number {
|
||||
// Backend returns naive UTC timestamps without 'Z' suffix.
|
||||
// JS Date() treats bare strings as local time, so append Z to force UTC.
|
||||
return new Date(st.endsWith('Z') ? st : st + 'Z').getTime()
|
||||
}
|
||||
|
||||
function calcActiveSeconds(startTimes: string[]): number {
|
||||
const now = Date.now()
|
||||
return startTimes.reduce((sum, st) => {
|
||||
const elapsed = Math.floor((now - parseUTCTimestamp(st)) / 1000)
|
||||
return sum + Math.max(0, elapsed)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function SidebarStatsBar({ resolved, active, completedMinutes, activeSessionStartTimes }: SidebarStatsBarProps) {
|
||||
const [liveSeconds, setLiveSeconds] = useState(() => calcActiveSeconds(activeSessionStartTimes))
|
||||
|
||||
// Tick every second to keep the timer in sync with the session timer
|
||||
useEffect(() => {
|
||||
setLiveSeconds(calcActiveSeconds(activeSessionStartTimes))
|
||||
if (activeSessionStartTimes.length === 0) return
|
||||
const interval = setInterval(() => {
|
||||
setLiveSeconds(calcActiveSeconds(activeSessionStartTimes))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [activeSessionStartTimes])
|
||||
|
||||
const totalSeconds = (completedMinutes * 60) + liveSeconds
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-0.5 px-3 pt-2 pb-1"
|
||||
role="group"
|
||||
aria-label="Today's stats"
|
||||
>
|
||||
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
|
||||
<div
|
||||
className="font-label text-sm font-semibold leading-none"
|
||||
style={{ color: '#34d399' }}
|
||||
aria-label={`${resolved} resolved today`}
|
||||
>
|
||||
{resolved}
|
||||
</div>
|
||||
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
|
||||
Resolved
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
|
||||
<div
|
||||
className="font-label text-sm font-semibold leading-none"
|
||||
style={{ color: '#22d3ee' }}
|
||||
aria-label={`${active} active sessions`}
|
||||
>
|
||||
{active}
|
||||
</div>
|
||||
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
|
||||
Active
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
|
||||
<div
|
||||
className="font-label text-sm font-semibold leading-none text-muted-foreground"
|
||||
aria-label={`${formatDuration(totalSeconds)} total session time today`}
|
||||
>
|
||||
{formatDuration(totalSeconds)}
|
||||
</div>
|
||||
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
|
||||
Total Time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -153,6 +153,11 @@
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); }
|
||||
50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); }
|
||||
}
|
||||
|
||||
@keyframes stagger-fade-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
|
||||
30
frontend/src/pages/FlowAssistPage.tsx
Normal file
30
frontend/src/pages/FlowAssistPage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { WandSparkles } from 'lucide-react'
|
||||
|
||||
export default function FlowAssistPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<WandSparkles size={24} style={{ color: '#f472b6' }} />
|
||||
Flow Assist
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Build flows from natural language — describe what you need and Flow Assist will generate the decision tree or procedural steps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static p-8 text-center">
|
||||
<WandSparkles size={40} className="mx-auto mb-4 text-muted-foreground" />
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground mb-2">
|
||||
Coming Soon
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
||||
Flow Assist will be available here as a dedicated conversational flow builder.
|
||||
In the meantime, use the AI panel in the Flow Editor to generate flows.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react'
|
||||
import { Search, Loader2, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
@@ -9,7 +9,6 @@ import type { Session } from '@/types/session'
|
||||
import { getTreeNavigatePath } from '@/lib/routing'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { usePaginationParams } from '@/hooks/usePaginationParams'
|
||||
import { useCachedQuota } from '@/hooks/useCachedQuota'
|
||||
@@ -66,7 +65,7 @@ export function QuickStartPage() {
|
||||
const [allFlowsCeiling, setAllFlowsCeiling] = useState(false)
|
||||
|
||||
// Favorites state
|
||||
const [showAllFavorites, setShowAllFavorites] = useState(false)
|
||||
|
||||
|
||||
// AI Builder
|
||||
const { aiEnabled } = useCachedQuota()
|
||||
@@ -81,19 +80,6 @@ export function QuickStartPage() {
|
||||
const [forkReason, setForkReason] = useState('')
|
||||
const [isForking, setIsForking] = useState(false)
|
||||
|
||||
// Pin store
|
||||
const pinnedItems = usePinnedFlowsStore((s) => s.items)
|
||||
const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading)
|
||||
const loadPinned = usePinnedFlowsStore((s) => s.load)
|
||||
const isMutatingByTreeId = usePinnedFlowsStore((s) => s.isMutatingByTreeId)
|
||||
const togglePin = usePinnedFlowsStore((s) => s.toggle)
|
||||
|
||||
const pinnedTreeIds = useMemo(() => new Set(pinnedItems.map((f) => f.tree_id)), [pinnedItems])
|
||||
const pinLoadingTreeIds = useMemo(
|
||||
() => new Set(Object.entries(isMutatingByTreeId).filter(([, v]) => v).map(([k]) => k)),
|
||||
[isMutatingByTreeId]
|
||||
)
|
||||
|
||||
// Preferences
|
||||
const { dashboardMyFlowsView, setDashboardMyFlowsView } = useUserPreferencesStore()
|
||||
|
||||
@@ -103,8 +89,7 @@ export function QuickStartPage() {
|
||||
allowedPageSizes: [10, 25, 50, 'all'],
|
||||
})
|
||||
|
||||
// Load pinned flows
|
||||
useEffect(() => { loadPinned() }, [loadPinned])
|
||||
|
||||
|
||||
// Load sessions on mount
|
||||
useEffect(() => {
|
||||
@@ -241,11 +226,6 @@ export function QuickStartPage() {
|
||||
|
||||
// recentSessionItems removed — replaced by RecentActivity component
|
||||
|
||||
// Favorites display
|
||||
const MAX_VISIBLE_FAVORITES = 8
|
||||
const visibleFavorites = showAllFavorites ? pinnedItems : pinnedItems.slice(0, MAX_VISIBLE_FAVORITES)
|
||||
const hasMoreFavorites = pinnedItems.length > MAX_VISIBLE_FAVORITES
|
||||
|
||||
// Handlers
|
||||
const handleStartSession = (treeId: string, treeType?: string) => {
|
||||
navigate(getTreeNavigatePath(treeId, treeType))
|
||||
@@ -319,7 +299,6 @@ export function QuickStartPage() {
|
||||
{ label: 'Active Flows', value: myFlows.length, gradient: true, glow: true },
|
||||
{ label: 'This Week', value: todaySessions },
|
||||
{ label: 'Open Sessions', value: openSessions },
|
||||
{ label: 'Favorites', value: pinnedItems.length },
|
||||
].map((stat, i) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
@@ -387,63 +366,6 @@ export function QuickStartPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Favorites Section */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">
|
||||
Favorites
|
||||
{pinnedItems.length > 0 && (
|
||||
<span className="ml-2 text-sm font-normal text-muted-foreground">({pinnedItems.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
{hasMoreFavorites && (
|
||||
<button
|
||||
onClick={() => setShowAllFavorites(!showAllFavorites)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{showAllFavorites ? 'Show less' : 'View all favorites'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{pinnedIsLoading ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-20 rounded-xl bg-card border border-border animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : pinnedItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-4">
|
||||
Star a flow to pin it here for quick access.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||
{visibleFavorites.map((flow) => (
|
||||
<button
|
||||
key={flow.tree_id}
|
||||
onClick={() => navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type))}
|
||||
className="group relative flex items-center gap-3 rounded-xl bg-card border border-border p-4 text-left transition-colors hover:border-border/80 hover:bg-accent/50"
|
||||
>
|
||||
<span className="text-lg shrink-0">
|
||||
{flow.tree_type === 'procedural' ? '📋' : flow.tree_type === 'maintenance' ? '🛠️' : '🔧'}
|
||||
</span>
|
||||
<span className="truncate text-sm font-medium text-foreground">{flow.tree_name}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
togglePin(flow.tree_id)
|
||||
}}
|
||||
aria-label="Remove from favorites"
|
||||
className="absolute top-2 right-2 rounded-md p-1 text-amber-400 opacity-0 group-hover:opacity-100 hover:text-amber-300 transition-all"
|
||||
>
|
||||
<Star size={14} fill="currentColor" />
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* My Flows Section — tabbed */}
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-1 border-b border-border">
|
||||
@@ -513,9 +435,6 @@ export function QuickStartPage() {
|
||||
onTagClick={handleTagClick}
|
||||
onFolderCreated={handleFolderCreated}
|
||||
onDeleteTree={handleDeleteTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
/>
|
||||
)}
|
||||
{dashboardMyFlowsView === 'list' && (
|
||||
@@ -525,9 +444,6 @@ export function QuickStartPage() {
|
||||
onTagClick={handleTagClick}
|
||||
onFolderCreated={handleFolderCreated}
|
||||
onDeleteTree={handleDeleteTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
/>
|
||||
)}
|
||||
{dashboardMyFlowsView === 'table' && (
|
||||
@@ -537,9 +453,6 @@ export function QuickStartPage() {
|
||||
onTagClick={handleTagClick}
|
||||
onFolderCreated={handleFolderCreated}
|
||||
onDeleteTree={handleDeleteTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SessionHistoryPage() {
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [trees, setTrees] = useState<TreeListItem[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('all')
|
||||
const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('active')
|
||||
|
||||
// Close session popover state
|
||||
const [closingSessionId, setClosingSessionId] = useState<string | null>(null)
|
||||
@@ -227,7 +227,7 @@ export function SessionHistoryPage() {
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="mb-6 flex gap-2 border-b border-border">
|
||||
{(['all', 'active', 'completed', 'prepared'] as const).map((tab) => (
|
||||
{(['active', 'prepared', 'completed', 'all'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setFilter(tab)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
@@ -24,7 +24,6 @@ import { cn, safeGetItem } from '@/lib/utils'
|
||||
import { getSessionResumePath, getTreeNavigatePath } from '@/lib/routing'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
|
||||
import { useCachedQuota } from '@/hooks/useCachedQuota'
|
||||
import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
@@ -96,17 +95,6 @@ export function TreeLibraryPage() {
|
||||
|
||||
const { aiEnabled } = useCachedQuota()
|
||||
|
||||
// Pin store
|
||||
const pinnedItems = usePinnedFlowsStore((s) => s.items)
|
||||
const isMutatingByTreeId = usePinnedFlowsStore((s) => s.isMutatingByTreeId)
|
||||
const pinnedTreeIds = useMemo(() => new Set(pinnedItems.map((f) => f.tree_id)), [pinnedItems])
|
||||
const pinLoadingTreeIds = useMemo(
|
||||
() => new Set(Object.entries(isMutatingByTreeId).filter(([, v]) => v).map(([k]) => k)),
|
||||
[isMutatingByTreeId]
|
||||
)
|
||||
const togglePin = usePinnedFlowsStore((s) => s.toggle)
|
||||
const loadPinned = usePinnedFlowsStore((s) => s.load)
|
||||
|
||||
// Repeat Last Session
|
||||
const lastSessionData = (() => {
|
||||
const raw = safeGetItem('last-session')
|
||||
@@ -140,9 +128,6 @@ export function TreeLibraryPage() {
|
||||
.catch((err) => console.error('Failed to load incomplete sessions:', err))
|
||||
}, [])
|
||||
|
||||
// Load pinned flows
|
||||
useEffect(() => { loadPinned() }, [loadPinned])
|
||||
|
||||
const dismissSession = (sessionId: string) => {
|
||||
const next = new Set(dismissedSessionIds)
|
||||
next.add(sessionId)
|
||||
@@ -534,9 +519,6 @@ export function TreeLibraryPage() {
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
onExportTree={handleExportTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
/>
|
||||
)}
|
||||
{treeLibraryView === 'list' && (
|
||||
@@ -552,9 +534,6 @@ export function TreeLibraryPage() {
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
onExportTree={handleExportTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
/>
|
||||
)}
|
||||
{treeLibraryView === 'table' && (
|
||||
@@ -575,9 +554,6 @@ export function TreeLibraryPage() {
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
onExportTree={handleExportTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -44,6 +44,7 @@ const StepLibraryPage = lazy(() => import('@/pages/StepLibraryPage'))
|
||||
const ScriptLibraryPage = lazy(() => import('@/pages/ScriptLibraryPage'))
|
||||
const ScriptManagePage = lazy(() => import('@/pages/ScriptManagePage'))
|
||||
const AssistantChatPage = lazy(() => import('@/pages/AssistantChatPage'))
|
||||
const FlowAssistPage = lazy(() => import('@/pages/FlowAssistPage'))
|
||||
const KBAcceleratorPage = lazy(() => import('@/pages/KBAcceleratorPage'))
|
||||
const GuidesHubPage = lazy(() => import('@/pages/GuidesHubPage'))
|
||||
const GuideDetailPage = lazy(() => import('@/pages/GuideDetailPage'))
|
||||
@@ -167,6 +168,7 @@ export const router = sentryCreateBrowserRouter([
|
||||
{ path: 'scripts/manage', element: page(ScriptManagePage) },
|
||||
{ path: 'kb-accelerator', element: page(KBAcceleratorPage) },
|
||||
{ path: 'assistant', element: page(AssistantChatPage) },
|
||||
{ path: 'flow-assist', element: page(FlowAssistPage) },
|
||||
{ path: 'guides', element: page(GuidesHubPage) },
|
||||
{ path: 'guides/:slug', element: page(GuideDetailPage) },
|
||||
// Admin routes
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { pinnedFlowsApi } from '@/api/pinnedFlows'
|
||||
import type { PinnedFlow } from '@/api/pinnedFlows'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
interface PinnedFlowsState {
|
||||
items: PinnedFlow[]
|
||||
isLoaded: boolean
|
||||
isLoading: boolean
|
||||
isMutatingByTreeId: Record<string, boolean>
|
||||
error: string | null
|
||||
|
||||
load: (force?: boolean) => Promise<void>
|
||||
pin: (treeId: string) => Promise<void>
|
||||
unpin: (treeId: string) => Promise<void>
|
||||
toggle: (treeId: string) => void
|
||||
isPinned: (treeId: string) => boolean
|
||||
}
|
||||
|
||||
export const usePinnedFlowsStore = create<PinnedFlowsState>()((set, get) => ({
|
||||
items: [],
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
isMutatingByTreeId: {},
|
||||
error: null,
|
||||
|
||||
load: async (force = false) => {
|
||||
const state = get()
|
||||
if (state.isLoaded && !force) return
|
||||
if (state.isLoading) return
|
||||
|
||||
set({ isLoading: true, error: null })
|
||||
try {
|
||||
const data = await pinnedFlowsApi.list()
|
||||
set({ items: data.items, isLoaded: true, isLoading: false })
|
||||
} catch {
|
||||
set({ error: 'Failed to load pinned flows', isLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
pin: async (treeId: string) => {
|
||||
const state = get()
|
||||
if (state.isMutatingByTreeId[treeId]) return
|
||||
|
||||
set({ isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true } })
|
||||
|
||||
try {
|
||||
await pinnedFlowsApi.pin(treeId)
|
||||
const data = await pinnedFlowsApi.list()
|
||||
set((s) => ({
|
||||
items: data.items,
|
||||
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
||||
}))
|
||||
} catch (err: unknown) {
|
||||
const status = (err as { response?: { status?: number } })?.response?.status
|
||||
if (status === 409) {
|
||||
toast.error('Maximum of 15 favorites reached. Unpin a flow to add a new one.')
|
||||
} else {
|
||||
toast.error('Failed to pin flow')
|
||||
}
|
||||
set((s) => ({
|
||||
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
unpin: async (treeId: string) => {
|
||||
const state = get()
|
||||
if (state.isMutatingByTreeId[treeId]) return
|
||||
|
||||
const prevItems = state.items
|
||||
set({
|
||||
items: state.items.filter((f) => f.tree_id !== treeId),
|
||||
isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true },
|
||||
})
|
||||
|
||||
try {
|
||||
await pinnedFlowsApi.unpin(treeId)
|
||||
set((s) => ({
|
||||
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
||||
}))
|
||||
} catch {
|
||||
toast.error('Failed to unpin flow')
|
||||
set((s) => ({
|
||||
items: prevItems,
|
||||
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
toggle: (treeId: string) => {
|
||||
const state = get()
|
||||
if (state.isPinned(treeId)) {
|
||||
state.unpin(treeId)
|
||||
} else {
|
||||
state.pin(treeId)
|
||||
}
|
||||
},
|
||||
|
||||
isPinned: (treeId: string) => {
|
||||
return get().items.some((f) => f.tree_id === treeId)
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user