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:
@@ -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">
|
||||
|
||||
@@ -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">› </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"
|
||||
|
||||
Reference in New Issue
Block a user