shell+ai: pristine-style nav groups, mobile fixes for /ai

Sidenav (app-shell.tsx):
- Each NavGroup now carries an icon (Building2 / Database / Plug /
  MessageSquare / Eye / Sparkles) rendered on the LEFT of the group
  header, with the chevron moved to the RIGHT. Header typography
  switched to caption + uppercase + tracking-wider muted, matching
  pristine-ui's main-branch app-shell. Same change applied to the
  mobile sheet's group headers.

/ai mobile fixes (ai.tsx):
- Composer container honors iOS safe-area inset
  (pb-[max(0.75rem,env(safe-area-inset-bottom))]) so the input clears
  the home indicator and stays above the soft keyboard.
- Composer toolbar wraps on narrow viewports (flex-wrap + gap-y-1)
  so the agent / model / reasoning / voice chips don't clip.
- Empty-state card uses px-4 sm:px-8 instead of hard px-8.
- MessageRow's 56px turn-number gutter collapses below sm: prose
  flows full-width on phone, two-column layout returns at sm+.

/ai desktop centering:
- Console wrapper opts out of AppShell's [&>*:first-child]:lg:pr-72
  (the page-header clearance for the floating top-right pill) via
  lg:!pr-0. The /ai surface has no top-right page-header controls,
  so the inherited padding was shifting the chat column ~144px left
  of the visible viewport center.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-05 19:08:36 +10:00
parent a286b9cdce
commit a74550d73f
2 changed files with 38 additions and 23 deletions

View File

@@ -33,7 +33,11 @@ import {
Megaphone,
AlertOctagon,
SearchCode,
ChevronRight,
ChevronDown,
Database,
Plug,
MessageSquare,
Eye,
// CREMA:NAV-ICONS
} from "lucide-react"
@@ -102,6 +106,7 @@ type NavItem = {
type NavGroup = {
key: string
label: string
icon: React.ComponentType<{ className?: string }>
items: NavItem[]
}
@@ -119,6 +124,7 @@ const navGroups: NavGroup[] = [
{
key: "tenancy",
label: "Tenancy",
icon: Building2,
items: [
{ to: "/tenants", icon: Building2, label: "Tenants" },
{ to: "/memberships", icon: UserCheck, label: "Memberships" },
@@ -129,6 +135,7 @@ const navGroups: NavGroup[] = [
{
key: "data",
label: "Data",
icon: Database,
items: [
{ to: "/storage", icon: HardDrive, label: "Storage" },
{ to: "/buckets", icon: Boxes, label: "Buckets" },
@@ -138,6 +145,7 @@ const navGroups: NavGroup[] = [
{
key: "integrations",
label: "Integrations",
icon: Plug,
items: [
{ to: "/webhooks", icon: WebhookIcon, label: "Webhooks" },
{ to: "/scheduled-tasks", icon: CalendarClock, label: "Scheduled" },
@@ -147,6 +155,7 @@ const navGroups: NavGroup[] = [
{
key: "comms",
label: "Communications",
icon: MessageSquare,
items: [
{ to: "/announcements", icon: Megaphone, label: "Announcements" },
{ to: "/status-page", icon: AlertOctagon, label: "Status page" },
@@ -155,6 +164,7 @@ const navGroups: NavGroup[] = [
{
key: "observability",
label: "Observability",
icon: Eye,
items: [
{ to: "/monitoring", icon: Gauge, label: "Monitoring" },
{ to: "/activity", icon: Activity, label: "Audit log" },
@@ -163,6 +173,7 @@ const navGroups: NavGroup[] = [
{
key: "ai",
label: "AI & Search",
icon: Sparkles,
items: [
{ to: "/ai", icon: Bot, label: "AI" },
{ to: "/search", icon: SearchCode, label: "Search" },
@@ -332,22 +343,24 @@ export function AppShell({
{navGroups.map((group) => {
const isOpen = !!openGroups[group.key]
const GroupIcon = group.icon
return (
<div key={group.key} className="mt-1.5 flex flex-col gap-0.5">
<div key={group.key} className="mt-3 flex flex-col gap-0.5">
<button
type="button"
data-action={`nav-group-${group.key}`}
onClick={() => toggleGroup(group.key)}
aria-expanded={isOpen}
className="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground/80 transition-colors hover:text-foreground"
className="flex items-center gap-2 rounded-md px-3 py-1.5 text-left text-caption font-semibold uppercase tracking-wider text-muted-foreground/70 transition-colors duration-fast ease-standard hover:text-foreground"
>
<ChevronRight
<GroupIcon className="size-3.5 shrink-0" />
<span className="flex-1 truncate">{group.label}</span>
<ChevronDown
className={[
"size-3 shrink-0 transition-transform duration-fast ease-standard",
isOpen ? "rotate-90" : "",
"size-3.5 shrink-0 transition-transform duration-fast ease-standard",
isOpen ? "" : "-rotate-90",
].join(" ")}
/>
<span className="truncate">{group.label}</span>
</button>
{isOpen ? (
<div className="flex flex-col gap-0.5">
@@ -440,22 +453,24 @@ export function AppShell({
{navGroups.map((group) => {
const isOpen = !!openGroups[group.key]
const GroupIcon = group.icon
return (
<div key={group.key} className="mt-1.5 flex flex-col gap-0.5">
<div key={group.key} className="mt-3 flex flex-col gap-0.5">
<button
type="button"
data-action={`nav-mobile-group-${group.key}`}
onClick={() => toggleGroup(group.key)}
aria-expanded={isOpen}
className="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground/80 transition-colors hover:text-foreground"
className="flex items-center gap-2 rounded-md px-3 py-1.5 text-left text-caption font-semibold uppercase tracking-wider text-muted-foreground/70 transition-colors duration-fast ease-standard hover:text-foreground"
>
<ChevronRight
<GroupIcon className="size-3.5 shrink-0" />
<span className="flex-1 truncate">{group.label}</span>
<ChevronDown
className={[
"size-3 shrink-0 transition-transform duration-fast ease-standard",
isOpen ? "rotate-90" : "",
"size-3.5 shrink-0 transition-transform duration-fast ease-standard",
isOpen ? "" : "-rotate-90",
].join(" ")}
/>
<span className="truncate">{group.label}</span>
</button>
{isOpen ? (
<div className="flex flex-col gap-0.5">

View File

@@ -630,7 +630,7 @@ export default function AIRoute() {
* toggle still works for them). */}
<div
data-theme="console"
className="-m-6 flex h-full min-h-0 flex-col bg-[var(--console-ink)] text-[var(--console-text)]"
className="-m-6 flex h-full min-h-0 flex-col bg-[var(--console-ink)] text-[var(--console-text)] lg:!pr-0"
>
<ChatSurface
models={availableModels}
@@ -1222,7 +1222,7 @@ function ChatSurface({
{/* Empty state — flight-recorder card with staggered reveal */}
<div
aria-hidden={!isEmpty}
className="pointer-events-none absolute inset-x-0 top-[10%] px-8 transition-opacity duration-300"
className="pointer-events-none absolute inset-x-0 top-[10%] px-4 transition-opacity duration-300 sm:px-8"
style={{ opacity: isEmpty ? 1 : 0 }}
>
<div className="mx-auto flex max-w-3xl flex-col gap-4">
@@ -1363,7 +1363,7 @@ function ChatSurface({
* center when empty, then springs to sticky-bottom on the first message. */}
<div
ref={composerRef}
className="sticky bottom-0 z-20 px-4 pb-3 pt-3 sm:px-6"
className="sticky bottom-0 z-20 px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-6"
style={{
transform: isEmpty
? "translateY(calc(-50dvh + 50% + 4rem))"
@@ -1630,8 +1630,8 @@ function MessageRow({
// row hangs from a left gutter showing the turn number.
if (role === "user") {
return (
<div className="grid grid-cols-[3.5rem_1fr] gap-x-3 self-stretch">
<div className="flex flex-col items-end pt-[3px]">
<div className="grid grid-cols-1 gap-x-3 self-stretch sm:grid-cols-[3.5rem_1fr]">
<div className="hidden flex-col items-end pt-[3px] sm:flex">
<span className="console-turn-num">
T{(turnNum ?? 0).toString().padStart(2, "0")}
</span>
@@ -1641,7 +1641,7 @@ function MessageRow({
</span>
) : null}
</div>
<div className="border-l border-[var(--console-rule-soft)] pl-4">
<div className="sm:border-l sm:border-[var(--console-rule-soft)] sm:pl-4">
<div className="console-op-line whitespace-pre-wrap">
<span className="console-op-prompt">&nbsp;</span>
{content}
@@ -1655,8 +1655,8 @@ function MessageRow({
// there's no prose (just tool calls), suppress the row entirely.
if (!content.trim()) return null
return (
<div className="grid grid-cols-[3.5rem_1fr] gap-x-3 self-stretch">
<div className="flex flex-col items-end pt-[2px]">
<div className="grid grid-cols-1 gap-x-3 self-stretch sm:grid-cols-[3.5rem_1fr]">
<div className="hidden flex-col items-end pt-[2px] sm:flex">
<span className="console-turn-num text-[var(--console-cyan)]">
T{(turnNum ?? 0).toString().padStart(2, "0")}
</span>
@@ -1664,7 +1664,7 @@ function MessageRow({
{agentName?.slice(0, 6).toLowerCase() ?? "atlas"}
</span>
</div>
<div className="border-l border-[var(--console-cyan-deep)]/40 pl-4">
<div className="sm:border-l sm:border-[var(--console-cyan-deep)]/40 sm:pl-4">
<div className="console-agent-prose">
<MessageBody content={content} toolCalls={toolCalls} />
</div>
@@ -1781,7 +1781,7 @@ function Composer({
className="min-h-[3.5rem] w-full resize-none bg-transparent outline-none"
/>
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1">
<div className="flex items-center gap-1">
<button
type="button"