feat: UI design system - sidebar layout, workspace system, and shell redesign (#77)

* feat: add workspace system and sidebar layout (UI design system Phase A+B)

Backend: Workspace model, migration (036), schemas, CRUD API endpoints.
Adds workspace_id to trees and categories, seeds 4 default workspaces
per account, auto-assigns existing trees by tree_type.

Frontend: Complete AppLayout rewrite from top-nav to CSS Grid shell
with persistent sidebar + topbar. New components: WorkspaceSwitcher,
NavItem, CategoryList, TagCloud, TopBar, Sidebar. Dashboard components:
QuickStats, FiltersBar, SectionGroup, TreeListItem, SessionsPanel.
WorkspaceStore with localStorage persistence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add command palette search, dashboard rewrite, and shell height fixes (Phase C)

- Add ⌘K command palette with debounced search across flows and sessions
- Rewrite QuickStartPage as dashboard with stats, filters, sessions panel
- Fix h-[calc(100vh-4rem)] → h-full across all pages for CSS Grid shell
- Add active session count badge to sidebar Sessions nav item

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add sidebar collapse, category/tag filtering, and workspace CRUD (Phase D)

- Sidebar collapse/expand toggle with icon-only rail mode (persisted)
- Sidebar category/tag clicks navigate to /trees with URL params
- TreeLibraryPage syncs filters from URL search params bidirectionally
- Workspace create modal with icon picker and auto-slug generation
- TopBar logo adapts to collapsed sidebar state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Quick Launch modal with actions and recent flows

- Zap button opens Quick Launch with create/navigate shortcuts
- Shows recent flows for quick session start
- Keyboard navigation support (arrows + enter)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add activity notifications panel with session feed

- Bell icon shows dot indicator for recent activity
- Dropdown panel shows recent sessions with status icons
- Links to session detail and sessions list page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: remove workspace system, add pinned flows and label renames

Replace workspace system with pinned flows API (pin/unpin/list/reorder).
Rename user-facing labels: Tree→Flow, Procedure→Project. Add sidebar
nav sub-items for flow type filtering. Remove 11 workspace files,
add migrations 037-038, clean all workspace references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapsed sidebar layout scaling and toggle button size

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate auth pages to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate TreeLibraryPage to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate session pages to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate TreeEditorPage to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate TreeNavigationPage to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate session sharing components to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove workspace dropdown animation (dead code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate common components to new design system

Migrate 15 components from monochrome glass-card design to purple gradient
accent design system tokens (bg-card, border-border, text-foreground,
bg-gradient-brand, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate procedural and step library components to new design system

Migrate 10 components from monochrome glass-card design to purple gradient
accent design system tokens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate admin pages and components to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate remaining pages to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate remaining components to new design system

Migrates 38 files: tree-editor forms, session modals, step library,
common components, library views, tree preview, and misc UI to use
design tokens (bg-card, border-border, text-foreground, bg-accent,
bg-gradient-brand) replacing old monochrome patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: keep brand text visible on sidebar collapse, hide sub-items until hover

- TopBar: always show "ResolutionFlow" text regardless of sidebar state
- NavItem: sub-items (Troubleshooting, Projects) hidden by default,
  revealed on hover or when a child route is active

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #77.
This commit is contained in:
chihlasm
2026-02-15 22:45:19 -05:00
committed by GitHub
parent ef829f06a4
commit fa709faa60
138 changed files with 5011 additions and 3796 deletions

View File

@@ -130,7 +130,7 @@ export function AccountSettingsPage() {
if (isLoading) {
return (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -154,23 +154,23 @@ export function AccountSettingsPage() {
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-8">
<div className="flex items-center gap-3">
<Building2 className="h-8 w-8 text-white/50" />
<h1 className="text-2xl font-bold text-white sm:text-3xl">Account Settings</h1>
<Building2 className="h-8 w-8 text-muted-foreground" />
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Account Settings</h1>
</div>
<p className="mt-2 text-white/40">
<p className="mt-2 text-muted-foreground">
Manage your account, subscription, and team
</p>
</div>
<div className="max-w-3xl space-y-6">
{/* Account Info Section */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Account Information</h2>
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground">Account Information</h2>
<div className="mt-4 space-y-4">
{/* Account Name */}
<div>
<label className="block text-sm font-medium text-white">
<label className="block text-sm font-medium text-foreground">
Account Name
</label>
{isEditingName ? (
@@ -180,9 +180,9 @@ export function AccountSettingsPage() {
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className={cn(
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
'flex-1 rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
autoFocus
onKeyDown={(e) => {
@@ -197,8 +197,8 @@ export function AccountSettingsPage() {
onClick={handleSaveName}
disabled={isSavingName}
className={cn(
'rounded-md bg-white p-2 text-black',
'hover:bg-white/90 disabled:opacity-50'
'rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 p-2',
'hover:opacity-90 disabled:opacity-50'
)}
>
{isSavingName ? (
@@ -212,18 +212,18 @@ export function AccountSettingsPage() {
setEditedName(account?.name ?? '')
setIsEditingName(false)
}}
className="rounded-md border border-white/10 p-2 text-white/40 hover:bg-white/10"
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<div className="mt-1 flex items-center gap-2">
<span className="text-sm text-white">{account?.name}</span>
<span className="text-sm text-foreground">{account?.name}</span>
{isAccountOwner && (
<button
onClick={() => setIsEditingName(true)}
className="text-xs text-white hover:underline"
className="text-xs text-foreground hover:underline"
>
Edit
</button>
@@ -234,10 +234,10 @@ export function AccountSettingsPage() {
{/* Display Code */}
<div>
<label className="block text-sm font-medium text-white">
<label className="block text-sm font-medium text-foreground">
Display Code
</label>
<p className="mt-1 text-sm font-mono text-white/40">
<p className="mt-1 text-sm font-mono text-muted-foreground">
{account?.display_code}
</p>
</div>
@@ -245,8 +245,8 @@ export function AccountSettingsPage() {
</div>
{/* Subscription Section */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Subscription</h2>
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground">Subscription</h2>
<div className="mt-4 space-y-4">
{/* Plan & Status */}
@@ -254,9 +254,9 @@ export function AccountSettingsPage() {
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
plan === 'free' && 'bg-white/10 text-white/70',
plan === 'pro' && 'bg-white/10 text-white',
plan === 'team' && 'bg-white/10 text-white'
plan === 'free' && 'bg-accent text-muted-foreground',
plan === 'pro' && 'bg-accent text-foreground',
plan === 'team' && 'bg-accent text-foreground'
)}
>
<Crown className="h-3.5 w-3.5" />
@@ -270,7 +270,7 @@ export function AccountSettingsPage() {
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-400',
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-400',
sub.status === 'canceled' && 'bg-red-400/10 text-red-400',
sub.status === 'orphaned' && 'bg-white/10 text-white/40'
sub.status === 'orphaned' && 'bg-accent text-muted-foreground'
)}
>
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
@@ -279,7 +279,7 @@ export function AccountSettingsPage() {
</div>
{sub?.current_period_end && (
<p className="text-sm text-white/40">
<p className="text-sm text-muted-foreground">
Current period ends: {new Date(sub.current_period_end).toLocaleDateString()}
</p>
)}
@@ -322,32 +322,32 @@ export function AccountSettingsPage() {
{/* Team Members Section (owners only) */}
{isAccountOwner && (
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Team Members</h2>
<Users className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Team Members</h2>
</div>
{members.length === 0 ? (
<p className="mt-4 text-sm text-white/40">No team members yet.</p>
<p className="mt-4 text-sm text-muted-foreground">No team members yet.</p>
) : (
<div className="mt-4 divide-y divide-white/[0.06]">
<div className="mt-4 divide-y divide-border">
{members.map((member) => (
<div
key={member.id}
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
>
<div>
<p className="text-sm font-medium text-white">{member.name}</p>
<p className="text-xs text-white/40">{member.email}</p>
<p className="text-sm font-medium text-foreground">{member.name}</p>
<p className="text-xs text-muted-foreground">{member.email}</p>
</div>
<div className="flex items-center gap-3">
<span
className={cn(
'rounded-full px-2.5 py-0.5 text-xs font-medium',
member.account_role === 'owner' && 'bg-white/10 text-white',
member.account_role === 'engineer' && 'bg-white/10 text-white/70',
member.account_role === 'viewer' && 'bg-white/10 text-white/40'
member.account_role === 'owner' && 'bg-accent text-foreground',
member.account_role === 'engineer' && 'bg-accent text-muted-foreground',
member.account_role === 'viewer' && 'bg-accent text-muted-foreground'
)}
>
{member.account_role}
@@ -360,7 +360,7 @@ export function AccountSettingsPage() {
{member.account_role !== 'owner' && (
<button
onClick={() => handleRemoveMember(member.id)}
className="text-white/40 hover:text-red-400"
className="text-muted-foreground hover:text-red-400"
title="Remove member"
>
<X className="h-4 w-4" />
@@ -376,10 +376,10 @@ export function AccountSettingsPage() {
{/* Invite Member Section (owners only) */}
{isAccountOwner && (
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Mail className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Invite Member</h2>
<Mail className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Invite Member</h2>
</div>
<form onSubmit={handleInvite} className="mt-4 space-y-3">
@@ -391,17 +391,17 @@ export function AccountSettingsPage() {
onChange={(e) => setInviteEmail(e.target.value)}
required
className={cn(
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
'flex-1 rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className={cn(
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
'rounded-md border border-border bg-card px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="engineer">Engineer</option>
@@ -411,8 +411,8 @@ export function AccountSettingsPage() {
type="submit"
disabled={isInviting || !inviteEmail.trim()}
className={cn(
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
'rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium',
'hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{isInviting ? (
@@ -437,8 +437,8 @@ export function AccountSettingsPage() {
{/* Pending Invites */}
{invites.length > 0 && (
<div className="mt-6">
<h3 className="text-sm font-medium text-white">Pending Invites</h3>
<div className="mt-2 divide-y divide-white/[0.06]">
<h3 className="text-sm font-medium text-foreground">Pending Invites</h3>
<div className="mt-2 divide-y divide-border">
{invites
.filter((inv) => !inv.used_at)
.map((invite) => (
@@ -447,21 +447,21 @@ export function AccountSettingsPage() {
className="flex items-center justify-between py-2"
>
<div>
<p className="text-sm text-white">{invite.email}</p>
<p className="text-xs text-white/40">
<p className="text-sm text-foreground">{invite.email}</p>
<p className="text-xs text-muted-foreground">
{invite.expires_at
? `Expires ${new Date(invite.expires_at).toLocaleDateString()}`
: 'No expiration'}
</p>
</div>
<div className="flex items-center gap-2">
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs text-muted-foreground">
{invite.role}
</span>
<button
onClick={() => handleResendInvite(invite.id)}
disabled={resendingId === invite.id}
className="text-white/40 hover:text-white disabled:opacity-50"
className="text-muted-foreground hover:text-foreground disabled:opacity-50"
title="Resend invite"
>
{resendingId === invite.id ? (
@@ -483,34 +483,34 @@ export function AccountSettingsPage() {
{isAccountOwner && (
<Link
to="/account/categories"
className="glass-card rounded-2xl p-4 sm:p-6 flex items-center justify-between group hover:border-white/20 transition-all"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<FolderTree className="h-5 w-5 text-white/50" />
<FolderTree className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-white">Team Categories</h2>
<p className="text-sm text-white/40">Manage tree categories for your team</p>
<h2 className="text-lg font-semibold text-foreground">Team Categories</h2>
<p className="text-sm text-muted-foreground">Manage tree categories for your team</p>
</div>
</div>
<span className="text-white/30 group-hover:text-white transition-colors">&rarr;</span>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
)}
{/* Preferences Section */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Preferences</h2>
<Settings className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Preferences</h2>
</div>
<div className="mt-4">
<label
htmlFor="export-format"
className="block text-sm font-medium text-white"
className="block text-sm font-medium text-foreground"
>
Default Export Format
</label>
<p className="text-sm text-white/40">
<p className="text-sm text-muted-foreground">
This format will be pre-selected when exporting sessions
</p>
<select
@@ -521,9 +521,9 @@ export function AccountSettingsPage() {
toast.success('Preference saved')
}}
className={cn(
'mt-2 block w-full max-w-xs rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-sm text-white',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
'mt-2 block w-full max-w-xs rounded-xl border border-border bg-card px-3 py-2',
'text-sm text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="markdown">Markdown (.md)</option>
@@ -554,24 +554,24 @@ function UsageStat({
return (
<div className="glass-stat rounded-md p-3">
<p className="text-xs font-medium text-white/40">{label}</p>
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<p
className={cn(
'mt-1 text-lg font-semibold',
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-white'
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-foreground'
)}
>
{current}
<span className="text-sm font-normal text-white/40">
<span className="text-sm font-normal text-muted-foreground">
{' '}/ {isUnlimited ? 'Unlimited' : max}
</span>
</p>
{!isUnlimited && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/10">
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-accent">
<div
className={cn(
'h-full rounded-full transition-all',
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-white'
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
)}
style={{ width: `${percentage}%` }}
/>