admin: completeness + UI consistency pass
Arcadia wiring: - home: real Overview dashboard (tenants/users/audit/health probe) replacing the inherited Vibespace welcome tiles; skeleton loaders, refresh button, registers admin context - profile: split into Account (synced via getUser/updateUser of session user) and local Preferences; updateSessionUser keeps the appbar in sync after edits - session: drop unused signIn mock, add updateSessionUser, refresh tests - profile schema: drop redundant Profile.name/email (session is the source of truth) - routes: delete orphaned resources route + lib Auth flows that previously 404'd: - /signup, /login/forgot, /login/reset, /login/2fa wired via @crema/arcadia-auth-ui - shared AuthShell + AuthBrand wrapper Assistant tools (admin-tools.ts): - +10 tools: deactivate_tenant, set_user_status, delete_user, list_memberships, list_roles, revoke_api_key, create_user, update_user, assign_role, remove_role - list_memberships gains user_id filter for "tenants this user belongs to" queries - search_kb / read_chunk: new token resolution (window override → VITE_ARCADIA_SEARCH_TOKEN service token → operator session JWT → "dev"); on 401/403 emit a tailored hint based on which token was used UI consistency: - new PageHeader component - AppShell.title was unrendered — dropped; first-child padding on #main-content keeps the floating actions pill from colliding with header content - removed dead "Sign in required" fallback cards from 14 routes (AppShell already redirects) - stripped p-6 from outer wrappers across 14 routes (was double-padding under AppShell's own p-6) - migrated home + tenants to PageHeader arcadia-search ergonomics: - scripts/mint-search-token.mjs + `npm run mint:search-token` mints HS512 JWT with required tenant_id claim, upserts VITE_ARCADIA_SEARCH_TOKEN into .env.local - README/.env document the new VITE_ARCADIA_SEARCH_URL / VITE_ARCADIA_SEARCH_TOKEN knobs - .env.local now gitignored Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,7 +118,6 @@ const navItems: NavItem[] = [
|
||||
]
|
||||
|
||||
type AppShellProps = {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
brand?: Brand
|
||||
user?: User
|
||||
@@ -131,7 +130,6 @@ type AppShellProps = {
|
||||
}
|
||||
|
||||
export function AppShell({
|
||||
title,
|
||||
children,
|
||||
brand: brandOverride,
|
||||
user: userOverride,
|
||||
@@ -143,14 +141,11 @@ export function AppShell({
|
||||
const session = useSession()
|
||||
const navigate = useNavigate()
|
||||
const brand = brandOverride ?? defaultBrand
|
||||
// Prefer the live session for identity, fall back to the editable profile,
|
||||
// fall back to the stub user.
|
||||
// Prefer the live session for identity, fall back to the stub user.
|
||||
const user = userOverride ?? {
|
||||
name: session?.name || profile.name || defaultUser.name,
|
||||
email: session?.email || profile.email || defaultUser.email,
|
||||
initials: profileInitials(
|
||||
session?.name || profile.name || defaultUser.name,
|
||||
),
|
||||
name: session?.name || defaultUser.name,
|
||||
email: session?.email || defaultUser.email,
|
||||
initials: profileInitials(session?.name || defaultUser.name),
|
||||
}
|
||||
|
||||
// Protected shell: bounce to /login when there's no session.
|
||||
@@ -163,7 +158,9 @@ export function AppShell({
|
||||
navigate(`/login?next=${next}`, { replace: true })
|
||||
}
|
||||
}, [session, navigate])
|
||||
if (!session) return null
|
||||
// All hooks must run unconditionally — keep them above the session
|
||||
// short-circuit so a sign-out doesn't reduce the hook count and trip
|
||||
// React's "rendered fewer hooks than expected" check.
|
||||
const [expanded, setExpanded] = useState<boolean>(() => {
|
||||
if (typeof window === "undefined") return false
|
||||
return localStorage.getItem(SIDEBAR_KEY) === "1"
|
||||
@@ -173,10 +170,11 @@ export function AppShell({
|
||||
}, [expanded])
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [scriptsOpen, setScriptsOpen] = useState(false)
|
||||
const BrandIcon = brand.icon
|
||||
|
||||
useScriptsHotkey(() => setScriptsOpen(true))
|
||||
|
||||
if (!session) return null
|
||||
const BrandIcon = brand.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
data-theme={theme}
|
||||
@@ -389,7 +387,10 @@ export function AppShell({
|
||||
<div
|
||||
id="main-content"
|
||||
tabIndex={-1}
|
||||
className="flex flex-1 flex-col gap-6 p-6 focus:outline-none"
|
||||
// First-child padding clears the fixed top-right floating actions
|
||||
// pill so page headers can put refresh/new buttons in their normal
|
||||
// top-right slot without sliding under the appbar avatar/controls.
|
||||
className="flex flex-1 flex-col gap-6 p-6 focus:outline-none [&>*:first-child]:lg:pr-72"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
36
app/components/layout/page-header.tsx
Normal file
36
app/components/layout/page-header.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { type ReactNode } from "react"
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
/** Inline indicators after the title (badges, status pills). */
|
||||
badges?: ReactNode
|
||||
/** Toolbar rendered below the title row — primary actions go here. */
|
||||
actions?: ReactNode
|
||||
}
|
||||
|
||||
// Right-side space for the appbar's floating actions pill is reserved by
|
||||
// the AppShell's first-child padding rule, not here — keep this layout
|
||||
// concerned only with title/description/actions composition.
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
badges,
|
||||
actions,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<header className="flex flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
{badges}
|
||||
</div>
|
||||
{description ? (
|
||||
<p className="max-w-3xl text-sm text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
{actions ? (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">{actions}</div>
|
||||
) : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user