feat(ux): adopt shared Empty primitive across all empty states

- one Empty/EmptyHeader/EmptyMedia/EmptyTitle/EmptyDescription
  component replaces ~18 ad-hoc empty states
- projects gains a real empty state (was four bare columns) with a
  create CTA wired to the existing handler
- governance/ops/customize/comms/comments empty states unified;
  existing CTAs and conditions preserved (presentational only)
This commit is contained in:
Moheeb Zara
2026-05-15 22:12:52 -07:00
parent 92ba6fb390
commit f885ac73d3
19 changed files with 297 additions and 93 deletions
+70 -45
View File
@@ -2,7 +2,7 @@
import { useState, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { Plus, Search, Lock, Pin, Eye, EyeOff, Pencil, Trash2, X, ChevronDown, Users2 } from 'lucide-react'
import { Plus, Search, Lock, Pin, Eye, EyeOff, Pencil, Trash2, X, ChevronDown, Users2, FileText } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { createKbEntry, updateKbEntry, deleteKbEntry } from '@/lib/actions'
@@ -12,6 +12,7 @@ import { createClient } from '@/lib/supabase/client'
import { toast } from 'sonner'
import type { Tables, TablesInsert } from '@/types/database'
import { PageTitle } from '@/components/ui/page-title'
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyContent } from '@/components/ui/empty'
type KbEntry = Tables<'knowledge_base'>
type AreaLead = Tables<'area_leads'>
@@ -715,19 +716,24 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
</div>
)}
{filteredKb.length === 0 && (
<div className="bg-card rounded border border-dashed border-border p-12 text-center">
<p className="font-sans text-sm text-muted-foreground">
{search ? `No results for "${search}"` : 'No knowledge base entries yet'}
</p>
<Empty className="bg-card border border-dashed border-border">
<EmptyHeader>
<EmptyMedia variant="icon">{search ? <Search /> : <FileText />}</EmptyMedia>
<EmptyTitle>
{search ? `No results for "${search}"` : 'No knowledge base entries yet'}
</EmptyTitle>
</EmptyHeader>
{!search && (
<button
onClick={() => { setEditingKb(null); setShowKbModal(true) }}
className="font-mono text-xs text-primary mt-2 block mx-auto hover:underline"
>
+ Add first entry
</button>
<EmptyContent>
<button
onClick={() => { setEditingKb(null); setShowKbModal(true) }}
className="font-mono text-xs text-primary hover:underline"
>
+ Add first entry
</button>
</EmptyContent>
)}
</div>
</Empty>
)}
</div>
)}
@@ -750,19 +756,24 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
))}
</div>
) : (
<div className="bg-card rounded border border-dashed border-border p-12 text-center">
<p className="font-sans text-sm text-muted-foreground">
{search ? `No results for "${search}"` : 'No process entries yet'}
</p>
<Empty className="bg-card border border-dashed border-border">
<EmptyHeader>
<EmptyMedia variant="icon">{search ? <Search /> : <FileText />}</EmptyMedia>
<EmptyTitle>
{search ? `No results for "${search}"` : 'No process entries yet'}
</EmptyTitle>
</EmptyHeader>
{!search && (
<button
onClick={() => { setEditingKb(null); setShowKbModal(true) }}
className="font-mono text-xs text-primary mt-2 block mx-auto hover:underline"
>
+ Add first process
</button>
<EmptyContent>
<button
onClick={() => { setEditingKb(null); setShowKbModal(true) }}
className="font-mono text-xs text-primary hover:underline"
>
+ Add first process
</button>
</EmptyContent>
)}
</div>
</Empty>
)}
</div>
)}
@@ -771,10 +782,12 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
{activeTab === 'secrets' && (
<div>
{!canSeeSecrets ? (
<div className="bg-card rounded border border-dashed border-border p-12 text-center">
<Lock className="w-8 h-8 text-muted-foreground mx-auto mb-3" />
<p className="font-sans text-sm text-muted-foreground">Admin or board access required to view secrets</p>
</div>
<Empty className="bg-card border border-dashed border-border">
<EmptyHeader>
<EmptyMedia variant="icon"><Lock /></EmptyMedia>
<EmptyTitle>Admin or board access required to view secrets</EmptyTitle>
</EmptyHeader>
</Empty>
) : filteredSecrets.length > 0 ? (
<div className="bg-card rounded border border-border divide-y divide-border">
{filteredSecrets.map(s => (
@@ -789,17 +802,24 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
))}
</div>
) : (
<div className="bg-card rounded border border-dashed border-border p-12 text-center">
<Lock className="w-8 h-8 text-muted-foreground mx-auto mb-3" />
<p className="font-sans text-sm text-muted-foreground">
{search ? `No results for "${search}"` : 'No secrets stored yet'}
</p>
<Empty className="bg-card border border-dashed border-border">
<EmptyHeader>
<EmptyMedia variant="icon"><Lock /></EmptyMedia>
<EmptyTitle>
{search ? `No results for "${search}"` : 'No secrets stored yet'}
</EmptyTitle>
</EmptyHeader>
{!search && (
<button onClick={() => setShowSecretModal(true)} className="font-mono text-xs text-primary mt-2 block mx-auto hover:underline">
+ Add first secret
</button>
<EmptyContent>
<button
onClick={() => setShowSecretModal(true)}
className="font-mono text-xs text-primary hover:underline"
>
+ Add first secret
</button>
</EmptyContent>
)}
</div>
</Empty>
)}
</div>
)}
@@ -841,15 +861,20 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
))}
</div>
) : (
<div className="bg-card rounded border border-dashed border-border p-12 text-center">
<p className="font-sans text-sm text-muted-foreground">No area leads assigned yet</p>
<button
onClick={() => { setEditingLead(null); setShowLeadModal(true) }}
className="font-mono text-xs text-primary mt-2 block mx-auto hover:underline"
>
+ Assign first area lead
</button>
</div>
<Empty className="bg-card border border-dashed border-border">
<EmptyHeader>
<EmptyMedia variant="icon"><Users2 /></EmptyMedia>
<EmptyTitle>No area leads assigned yet</EmptyTitle>
</EmptyHeader>
<EmptyContent>
<button
onClick={() => { setEditingLead(null); setShowLeadModal(true) }}
className="font-mono text-xs text-primary hover:underline"
>
+ Assign first area lead
</button>
</EmptyContent>
</Empty>
)}
</div>
)}