feat(ux): shared PageTitle/SectionTitle primitive, adopt app-wide
- new components/ui/page-title.tsx (PageHeader/PageTitle/SectionTitle) - every page heading routed through PageTitle (one source of truth for the page-title style; identical render) - members restricted-directory screen now uses the standard PageHeader bar instead of an off-style mono heading
This commit is contained in:
@@ -7,6 +7,7 @@ import { createClient } from '@/lib/supabase/client'
|
||||
import { Hash, Users2, Send, ChevronLeft, Plus } from 'lucide-react'
|
||||
import { createChannel } from '@/lib/actions'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Channel = Tables<'comms_channels'>
|
||||
type Message = Tables<'comms_messages'>
|
||||
@@ -184,9 +185,9 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="text-white font-sans text-lg font-semibold">
|
||||
<PageTitle>
|
||||
{!showChannelList && selectedChannel ? `#${selectedChannel.name}` : 'Comms'}
|
||||
</h1>
|
||||
</PageTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="text-sidebar-foreground/70 hover:text-sidebar-foreground text-sm">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
||||
import { Plus, X, Pencil } from 'lucide-react'
|
||||
import { createContact, updateContact, deleteContact } from '@/lib/actions'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Contact = Tables<'contacts'>
|
||||
|
||||
@@ -164,7 +165,7 @@ export function ContactsClient({ contacts: initialContacts }: { contacts: Contac
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Contacts</h1>
|
||||
<PageTitle>Contacts</PageTitle>
|
||||
<button
|
||||
onClick={() => { resetForm(); setShowCreate(true) }}
|
||||
className="flex items-center gap-1.5 bg-primary text-white text-xs font-sans px-3 py-1.5 rounded hover:bg-primary/90 transition"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
function IcoUsers({ className }: { className?: string }) {
|
||||
return <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
||||
@@ -97,7 +98,7 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Dashboard</h1>
|
||||
<PageTitle>Dashboard</PageTitle>
|
||||
<Link href="/tasks" className="flex items-center gap-1.5 bg-primary text-white text-xs font-sans px-3 py-1.5 rounded hover:bg-primary/90 transition">
|
||||
<IcoPlus className="w-3.5 h-3.5" />
|
||||
<span className="hidden sm:inline">Quick Task</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { FinancialVisibility, Payment, Space } from '@/lib/types'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -176,7 +177,7 @@ function PageShell({ title, children }: { title: string; children: React.ReactNo
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">{title}</h1>
|
||||
<PageTitle>{title}</PageTitle>
|
||||
</div>
|
||||
<div className="p-4 md:p-6 space-y-6 max-w-4xl">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import ImportClient from './import-client'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export default async function ImportPage() {
|
||||
const supabase = await createClient()
|
||||
@@ -22,7 +23,7 @@ export default async function ImportPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Import & Sync</h1>
|
||||
<PageTitle>Import & Sync</PageTitle>
|
||||
<p className="font-mono text-xs text-sidebar-foreground/50">CSV import · member & payment data</p>
|
||||
</div>
|
||||
<ImportClient spaceId={member!.space_id} role={member!.role} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { IncidentForm } from './incident-form'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -27,7 +28,7 @@ export default async function NewIncidentPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">File a report</h1>
|
||||
<PageTitle>File a report</PageTitle>
|
||||
</div>
|
||||
<div className="p-4 md:p-6 max-w-2xl">
|
||||
<IncidentForm members={(members ?? []) as Array<{ id: string; display_name: string | null }>} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Incident, IncidentStatus } from '@/lib/types'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -44,7 +45,7 @@ export default async function IncidentsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Incidents</h1>
|
||||
<PageTitle>Incidents</PageTitle>
|
||||
<Link
|
||||
href="/incidents/new"
|
||||
className="bg-primary text-white text-xs font-sans px-3 py-1.5 rounded hover:bg-primary/90 transition"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { addMember, updateMember, approveMember, removeMember, assignAreaLead }
|
||||
import { toast } from 'sonner'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Member = Tables<'space_members'>
|
||||
type AreaLeadRole = { id: string; area_name: string; lead_id: string | null }
|
||||
@@ -194,7 +195,7 @@ export function MembersClient({ members: initialMembers, currentRole, areaLeadRo
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Members</h1>
|
||||
<PageTitle>Members</PageTitle>
|
||||
<span className="font-mono text-xs text-white/50">{total} total</span>
|
||||
</div>
|
||||
{isAdmin(currentRole) && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { PageHeader, PageTitle } from '@/components/ui/page-title'
|
||||
import { MembersClient } from './members-client'
|
||||
|
||||
// member_directory_visibility values come from space settings:
|
||||
@@ -38,14 +39,18 @@ export default async function MembersPage() {
|
||||
|
||||
if (!allowed) {
|
||||
return (
|
||||
<div className="p-8 max-w-2xl">
|
||||
<h1 className="font-mono text-sm tracking-widest uppercase text-muted-foreground mb-3">Members</h1>
|
||||
<div className="bg-card border border-border rounded p-6">
|
||||
<p className="font-sans text-sm text-foreground mb-2">The member directory is restricted in this space.</p>
|
||||
<p className="font-sans text-sm text-muted-foreground">
|
||||
A space admin has set member directory visibility to <code className="font-mono text-xs px-1 py-0.5 bg-muted rounded">{visibility}</code>.
|
||||
Contact an admin if you need access.
|
||||
</p>
|
||||
<div>
|
||||
<PageHeader>
|
||||
<PageTitle>Members</PageTitle>
|
||||
</PageHeader>
|
||||
<div className="p-8 max-w-2xl">
|
||||
<div className="bg-card border border-border rounded p-6">
|
||||
<p className="font-sans text-sm text-foreground mb-2">The member directory is restricted in this space.</p>
|
||||
<p className="font-sans text-sm text-muted-foreground">
|
||||
A space admin has set member directory visibility to <code className="font-mono text-xs px-1 py-0.5 bg-muted rounded">{visibility}</code>.
|
||||
Contact an admin if you need access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ArrowLeft, Pin, Lock } from 'lucide-react'
|
||||
import { createKbEntry, updateKbEntry } from '@/lib/actions'
|
||||
import { toast } from 'sonner'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
interface Props {
|
||||
entry?: Tables<'knowledge_base'>
|
||||
@@ -50,7 +51,7 @@ export default function KbEntryEditor({ entry, member }: Props) {
|
||||
<Link href="/ops" className="text-sidebar-foreground/70 hover:text-sidebar-foreground transition">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Link>
|
||||
<h1 className="text-white font-sans text-lg font-semibold">{isEdit ? 'Edit Entry' : 'New KB Entry'}</h1>
|
||||
<PageTitle>{isEdit ? 'Edit Entry' : 'New KB Entry'}</PageTitle>
|
||||
</div>
|
||||
|
||||
<div className="p-6 max-w-3xl">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { OpsAclEditor } from '@/components/ops/ops-acl-editor'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { toast } from 'sonner'
|
||||
import type { Tables, TablesInsert } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type KbEntry = Tables<'knowledge_base'>
|
||||
type AreaLead = Tables<'area_leads'>
|
||||
@@ -614,7 +615,7 @@ export function OpsClient({ member, spaceId, kbEntries: initial, areaLeads: init
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Ops & Facilities</h1>
|
||||
<PageTitle>Ops & Facilities</PageTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{(activeTab === 'kb' || activeTab === 'processes') && (
|
||||
<button
|
||||
|
||||
@@ -5,6 +5,7 @@ import { X, RefreshCcw } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { logCashPayment, linkPaymentToMember } from '@/lib/actions'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Payment = Tables<'payments'> & {
|
||||
space_members?: { display_name: string } | null
|
||||
@@ -116,7 +117,7 @@ export function PaymentsClient({ payments: initialPayments, members, integration
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Payments</h1>
|
||||
<PageTitle>Payments</PageTitle>
|
||||
{unlinkedCount > 0 && (
|
||||
<span className="font-mono text-xs text-orange-400">{unlinkedCount} unlinked</span>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NewPolicyForm } from './new-policy-form'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -21,7 +22,7 @@ export default async function NewPolicyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">New policy</h1>
|
||||
<PageTitle>New policy</PageTitle>
|
||||
</div>
|
||||
<div className="p-4 md:p-6 max-w-2xl">
|
||||
<NewPolicyForm />
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Policy, PolicyStatus } from '@/lib/types'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -49,7 +50,7 @@ export default async function PoliciesPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Policies</h1>
|
||||
<PageTitle>Policies</PageTitle>
|
||||
{isAdminOrBoard && (
|
||||
<Link
|
||||
href="/policies/new"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { ProfileForm } from './profile-form'
|
||||
import { AffiliationsForm } from './affiliations-form'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -39,7 +40,7 @@ export default async function ProfilePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">My profile</h1>
|
||||
<PageTitle>My profile</PageTitle>
|
||||
</div>
|
||||
|
||||
<div className="p-4 md:p-6 space-y-6 max-w-3xl">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { createProject, updateProjectStatus, deleteProject } from '@/lib/actions'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Project = Tables<'projects'>
|
||||
|
||||
@@ -65,7 +66,7 @@ export function ProjectsClient({
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Projects</h1>
|
||||
<PageTitle>Projects</PageTitle>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="flex items-center gap-1.5 bg-primary text-white text-xs font-sans px-3 py-1.5 rounded hover:bg-primary/90 transition"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NewProposalForm } from './new-proposal-form'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -20,7 +21,7 @@ export default async function NewProposalPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">New proposal</h1>
|
||||
<PageTitle>New proposal</PageTitle>
|
||||
</div>
|
||||
<div className="p-4 md:p-6 max-w-2xl">
|
||||
<NewProposalForm />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Proposal } from '@/lib/types'
|
||||
import { ProposalStatusBadge } from './proposal-badges'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -34,7 +35,7 @@ export default async function ProposalsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Proposals</h1>
|
||||
<PageTitle>Proposals</PageTitle>
|
||||
<Link
|
||||
href="/proposals/new"
|
||||
className="bg-primary text-white text-xs font-sans px-3 py-1.5 rounded hover:bg-primary/90 transition"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -58,7 +59,7 @@ export default async function RecruitmentPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Recruitment</h1>
|
||||
<PageTitle>Recruitment</PageTitle>
|
||||
</div>
|
||||
|
||||
<div className="p-4 md:p-6 max-w-4xl space-y-6">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { toast } from 'sonner'
|
||||
import { financialVisibilities, directoryVisibilities } from '@/lib/validations'
|
||||
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Space = Tables<'spaces'>
|
||||
type Integration = Tables<'integrations'>
|
||||
@@ -213,7 +214,7 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Settings & Admin</h1>
|
||||
<PageTitle>Settings & Admin</PageTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useTransition } from 'react'
|
||||
import { Plus, X, CheckCircle2 } from 'lucide-react'
|
||||
import { createTask, claimTask, completeTask, deleteTask } from '@/lib/actions'
|
||||
import type { Tables } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
|
||||
type Task = Tables<'tasks'>
|
||||
|
||||
@@ -113,7 +114,7 @@ export function TasksClient({ tasks: initialTasks, members, currentUserId, space
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-white font-sans text-lg font-semibold">Tasks & Chores</h1>
|
||||
<PageTitle>Tasks & Chores</PageTitle>
|
||||
<span className="font-mono text-xs text-white/50">{tasks.filter(t => !isDone(t)).length} open</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user