feat: sidebar redesign — activity feed, grouped nav, AI split (#107)
* docs: add 5 sidebar icon color concepts for UX review Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): add semantic icon colors and updated icons to sidebar nav Swap generic icons for more descriptive alternatives (Network, Wrench, FileOutput, Library, Code2, Lightbulb) and assign each nav item a unique semantic color for instant visual landmarks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ui): default Sessions page to Active tab, reorder tabs Active sessions are what engineers care about most. Tab order is now Active, Prepared, Completed, All. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar grouping and AI naming concept mockups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign context and decision summary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign spec and implementation plan Design spec covers: activity zone with daily stats + session feed, nav grouping (Resolve/Build/Insights), AI split (FlowPilot + Flow Assist), pinned flows removal. Implementation plan has 5 chunks, 12 tasks, 39 steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar stats Pydantic schemas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add failing tests for sidebar stats endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar stats endpoint with daily stats and activity feed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar API client, stats bar, activity feed components New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem. New API client for sidebar stats endpoint. Pulse-dot CSS animation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: restructure sidebar with stats bar, activity feed, and grouped nav Dashboard-first layout with Resolve/Build/Insights groups. AI split: FlowPilot (Resolve) + Flow Assist (Build). Stats bar: Resolved/Active/In Session daily counters. Activity feed: active sessions with CW ticket #, recent completions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons) Removed: PinnedFlowsSection component, pinnedFlowsStore, pinnedFlows API client. Cleaned: pin buttons from TreeGridView, TreeListView, TreeTableView. Cleaned: favorites section from QuickStartPage, pin props from TreeLibraryPage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add FlowAssistPage placeholder and /flow-assist route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: real-time sidebar stats via session-changed events Sidebar now refreshes stats when sessions are created or completed, not just on page navigation. Uses window event bus pattern (same as folder-changed events in codebase). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: live-ticking In Session timer using active session start times SidebarStatsBar now computes active session elapsed time client-side from started_at timestamps, ticking every 60s. Backend only returns completed session minutes to avoid double-counting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: sidebar In Session timer ticks every second and shows seconds Timer now uses 1s interval (not 60s) and displays seconds when under a minute so it matches the session timer in the flow UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: trigger PR environment redeploy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * debug: add console.log to SidebarStatsBar for timer investigation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: parse sidebar timestamps as UTC (append Z suffix) Backend returns naive UTC timestamps without timezone indicator. JS Date() treats bare ISO strings as local time, causing the timer to compute negative elapsed time (future timestamps). Appending 'Z' forces UTC parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename 'In Session' to 'Total Time' for clarity Makes it clear the timer is an aggregate of all sessions today, not just the current one. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #107.
This commit is contained in:
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