shell: collapsible nav groups + mobile-friendly settings
- Reorganize sidenav into collapsible groups (Tenancy, Data, Integrations, Communications, Observability, AI & Search) with Overview/Settings pinned at top/bottom. Group open/close persists in localStorage; the group containing the active route auto-opens. Icon-only collapsed rail flattens to a single icon column. Sub-items inside groups drop their per-item icons and indent under the header. - Fix mobile sheet scroll — the nav couldn't reach items past viewport height. SheetContent is now flex-col h-svh, header shrink-0, nav flex-1 min-h-0 overflow-y-auto. - Settings page mobile fixes: section nav wraps instead of horizontal scroll, top padding clears the floating actions pill, LLM config card header and rows stack on narrow widths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
|
||||||
const SIDEBAR_KEY = "crema.shell.sidebar"
|
const SIDEBAR_KEY = "crema.shell.sidebar"
|
||||||
import { NavLink, useNavigate } from "react-router"
|
const NAV_GROUPS_KEY = "crema.shell.nav-groups"
|
||||||
|
import { NavLink, useLocation, useNavigate } from "react-router"
|
||||||
import {
|
import {
|
||||||
Bell,
|
Bell,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
Megaphone,
|
Megaphone,
|
||||||
AlertOctagon,
|
AlertOctagon,
|
||||||
SearchCode,
|
SearchCode,
|
||||||
|
ChevronRight,
|
||||||
// CREMA:NAV-ICONS
|
// CREMA:NAV-ICONS
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
@@ -97,28 +99,102 @@ type NavItem = {
|
|||||||
end?: boolean
|
end?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
type NavGroup = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
items: NavItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinned items render flat at the top of the rail, above any groups.
|
||||||
|
const pinnedTop: NavItem[] = [
|
||||||
{ to: "/", icon: LayoutDashboard, label: "Overview", end: true },
|
{ to: "/", icon: LayoutDashboard, label: "Overview", end: true },
|
||||||
{ to: "/monitoring", icon: Gauge, label: "Monitoring" },
|
]
|
||||||
|
|
||||||
|
// Pinned items render flat at the bottom of the rail, below all groups.
|
||||||
|
const pinnedBottom: NavItem[] = [
|
||||||
|
{ to: "/settings", icon: Settings, label: "Settings" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const navGroups: NavGroup[] = [
|
||||||
|
{
|
||||||
|
key: "tenancy",
|
||||||
|
label: "Tenancy",
|
||||||
|
items: [
|
||||||
{ to: "/tenants", icon: Building2, label: "Tenants" },
|
{ to: "/tenants", icon: Building2, label: "Tenants" },
|
||||||
|
{ to: "/memberships", icon: UserCheck, label: "Memberships" },
|
||||||
|
{ to: "/users", icon: UsersIcon, label: "Users" },
|
||||||
|
{ to: "/sso", icon: ShieldCheck, label: "SSO" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "data",
|
||||||
|
label: "Data",
|
||||||
|
items: [
|
||||||
{ to: "/storage", icon: HardDrive, label: "Storage" },
|
{ to: "/storage", icon: HardDrive, label: "Storage" },
|
||||||
{ to: "/buckets", icon: Boxes, label: "Buckets" },
|
{ to: "/buckets", icon: Boxes, label: "Buckets" },
|
||||||
{ to: "/users", icon: UsersIcon, label: "Users" },
|
|
||||||
{ to: "/memberships", icon: UserCheck, label: "Memberships" },
|
|
||||||
{ to: "/sso", icon: ShieldCheck, label: "SSO" },
|
|
||||||
{ to: "/secrets", icon: KeyRound, label: "Secrets" },
|
{ to: "/secrets", icon: KeyRound, label: "Secrets" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "integrations",
|
||||||
|
label: "Integrations",
|
||||||
|
items: [
|
||||||
{ to: "/webhooks", icon: WebhookIcon, label: "Webhooks" },
|
{ to: "/webhooks", icon: WebhookIcon, label: "Webhooks" },
|
||||||
{ to: "/scheduled-tasks", icon: CalendarClock, label: "Scheduled" },
|
{ to: "/scheduled-tasks", icon: CalendarClock, label: "Scheduled" },
|
||||||
{ to: "/networking", icon: Network, label: "Networking" },
|
{ to: "/networking", icon: Network, label: "Networking" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "comms",
|
||||||
|
label: "Communications",
|
||||||
|
items: [
|
||||||
{ to: "/announcements", icon: Megaphone, label: "Announcements" },
|
{ to: "/announcements", icon: Megaphone, label: "Announcements" },
|
||||||
{ to: "/status-page", icon: AlertOctagon, label: "Status page" },
|
{ to: "/status-page", icon: AlertOctagon, label: "Status page" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "observability",
|
||||||
|
label: "Observability",
|
||||||
|
items: [
|
||||||
|
{ to: "/monitoring", icon: Gauge, label: "Monitoring" },
|
||||||
{ to: "/activity", icon: Activity, label: "Audit log" },
|
{ to: "/activity", icon: Activity, label: "Audit log" },
|
||||||
{ to: "/search", icon: SearchCode, label: "Search" },
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ai",
|
||||||
|
label: "AI & Search",
|
||||||
|
items: [
|
||||||
{ to: "/ai", icon: Bot, label: "AI" },
|
{ to: "/ai", icon: Bot, label: "AI" },
|
||||||
{ to: "/settings", icon: Settings, label: "Settings" },
|
{ to: "/search", icon: SearchCode, label: "Search" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Items appended by `crema add <lib>` land here. Rendered ungrouped at
|
||||||
|
// the bottom of the groups, above the pinned footer.
|
||||||
|
const extraNavItems: NavItem[] = [
|
||||||
// CREMA:NAV-ITEMS
|
// CREMA:NAV-ITEMS
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Flat list — used by the icon-only collapsed rail, where group headers
|
||||||
|
// don't render and items appear as a single column of icons.
|
||||||
|
const allNavItems: NavItem[] = [
|
||||||
|
...pinnedTop,
|
||||||
|
...navGroups.flatMap((g) => g.items),
|
||||||
|
...extraNavItems,
|
||||||
|
...pinnedBottom,
|
||||||
|
]
|
||||||
|
|
||||||
|
function readNavGroupState(): Record<string, boolean> {
|
||||||
|
if (typeof window === "undefined") return {}
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(NAV_GROUPS_KEY)
|
||||||
|
return raw ? (JSON.parse(raw) as Record<string, boolean>) : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AppShellProps = {
|
type AppShellProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
brand?: Brand
|
brand?: Brand
|
||||||
@@ -174,6 +250,35 @@ export function AppShell({
|
|||||||
const [scriptsOpen, setScriptsOpen] = useState(false)
|
const [scriptsOpen, setScriptsOpen] = useState(false)
|
||||||
useScriptsHotkey(() => setScriptsOpen(true))
|
useScriptsHotkey(() => setScriptsOpen(true))
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
const activeGroupKey = useMemo(
|
||||||
|
() =>
|
||||||
|
navGroups.find((g) => g.items.some((it) => location.pathname.startsWith(it.to)))
|
||||||
|
?.key ?? null,
|
||||||
|
[location.pathname],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>(() =>
|
||||||
|
readNavGroupState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-open the group that owns the current route on first mount or
|
||||||
|
// navigation, but never auto-close — the user's explicit toggles win.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeGroupKey) return
|
||||||
|
setOpenGroups((prev) =>
|
||||||
|
prev[activeGroupKey] ? prev : { ...prev, [activeGroupKey]: true },
|
||||||
|
)
|
||||||
|
}, [activeGroupKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
localStorage.setItem(NAV_GROUPS_KEY, JSON.stringify(openGroups))
|
||||||
|
}, [openGroups])
|
||||||
|
|
||||||
|
const toggleGroup = (key: string) =>
|
||||||
|
setOpenGroups((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||||
|
|
||||||
if (!session) return null
|
if (!session) return null
|
||||||
const BrandIcon = brand.icon
|
const BrandIcon = brand.icon
|
||||||
|
|
||||||
@@ -218,31 +323,65 @@ export function AppShell({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2">
|
<nav className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto p-2">
|
||||||
{navItems.map((item) => {
|
{expanded ? (
|
||||||
const Icon = item.icon
|
<>
|
||||||
|
{pinnedTop.map((item) => (
|
||||||
|
<NavRow key={item.label} item={item} expanded />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{navGroups.map((group) => {
|
||||||
|
const isOpen = !!openGroups[group.key]
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<div key={group.key} className="mt-1.5 flex flex-col gap-0.5">
|
||||||
key={item.label}
|
<button
|
||||||
to={item.to}
|
type="button"
|
||||||
end={item.end}
|
data-action={`nav-group-${group.key}`}
|
||||||
title={expanded ? undefined : item.label}
|
onClick={() => toggleGroup(group.key)}
|
||||||
data-action={`nav-${item.label.toLowerCase()}`}
|
aria-expanded={isOpen}
|
||||||
className={({ isActive }) =>
|
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"
|
||||||
[
|
|
||||||
"flex items-center gap-3 rounded-lg px-3 py-2 font-medium transition-colors duration-fast ease-standard",
|
|
||||||
expanded ? "justify-start" : "justify-center",
|
|
||||||
isActive
|
|
||||||
? "bg-primary/10 text-primary"
|
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
||||||
].join(" ")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Icon className="size-5 shrink-0" />
|
<ChevronRight
|
||||||
{expanded && <span className="truncate">{item.label}</span>}
|
className={[
|
||||||
</NavLink>
|
"size-3 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">
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<NavRow key={item.label} item={item} expanded inGroup />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{extraNavItems.length > 0 ? (
|
||||||
|
<div className="mt-1.5 flex flex-col gap-0.5">
|
||||||
|
{extraNavItems.map((item) => (
|
||||||
|
<NavRow key={item.label} item={item} expanded />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-0.5 pt-2">
|
||||||
|
{pinnedBottom.map((item) => (
|
||||||
|
<NavRow key={item.label} item={item} expanded />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Icon-only rail: flat list, no group headers.
|
||||||
|
<>
|
||||||
|
{allNavItems.map((item) => (
|
||||||
|
<NavRow key={item.label} item={item} expanded={false} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="shrink-0 border-t p-2">
|
<div className="shrink-0 border-t p-2">
|
||||||
@@ -276,8 +415,11 @@ export function AppShell({
|
|||||||
>
|
>
|
||||||
<Menu className="size-5" />
|
<Menu className="size-5" />
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="w-72 p-0">
|
<SheetContent
|
||||||
<SheetHeader className="border-b">
|
side="left"
|
||||||
|
className="flex h-svh w-72 flex-col p-0"
|
||||||
|
>
|
||||||
|
<SheetHeader className="shrink-0 border-b">
|
||||||
<SheetTitle className="flex items-center gap-2">
|
<SheetTitle className="flex items-center gap-2">
|
||||||
<div className="flex size-7 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
<div className="flex size-7 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||||
<BrandIcon className="size-4" />
|
<BrandIcon className="size-4" />
|
||||||
@@ -285,30 +427,79 @@ export function AppShell({
|
|||||||
{brand.name}
|
{brand.name}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<nav className="flex flex-col gap-1 p-2">
|
<nav className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto p-2">
|
||||||
{navItems.map((item) => {
|
{pinnedTop.map((item) => (
|
||||||
const Icon = item.icon
|
<NavRow
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
key={item.label}
|
key={item.label}
|
||||||
to={item.to}
|
item={item}
|
||||||
end={item.end}
|
expanded
|
||||||
onClick={() => setMobileOpen(false)}
|
mobile
|
||||||
data-action={`nav-mobile-${item.label.toLowerCase()}`}
|
onNavigate={() => setMobileOpen(false)}
|
||||||
className={({ isActive }) =>
|
/>
|
||||||
[
|
))}
|
||||||
"flex items-center gap-3 rounded-lg px-3 py-2 font-medium transition-colors",
|
|
||||||
isActive
|
{navGroups.map((group) => {
|
||||||
? "bg-primary/10 text-primary"
|
const isOpen = !!openGroups[group.key]
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
return (
|
||||||
].join(" ")
|
<div key={group.key} className="mt-1.5 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"
|
||||||
>
|
>
|
||||||
<Icon className="size-5 shrink-0" />
|
<ChevronRight
|
||||||
<span>{item.label}</span>
|
className={[
|
||||||
</NavLink>
|
"size-3 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">
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<NavRow
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
expanded
|
||||||
|
mobile
|
||||||
|
inGroup
|
||||||
|
onNavigate={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{extraNavItems.length > 0 ? (
|
||||||
|
<div className="mt-1.5 flex flex-col gap-0.5">
|
||||||
|
{extraNavItems.map((item) => (
|
||||||
|
<NavRow
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
expanded
|
||||||
|
mobile
|
||||||
|
onNavigate={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-col gap-0.5 pt-2">
|
||||||
|
{pinnedBottom.map((item) => (
|
||||||
|
<NavRow
|
||||||
|
key={item.label}
|
||||||
|
item={item}
|
||||||
|
expanded
|
||||||
|
mobile
|
||||||
|
onNavigate={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@@ -404,6 +595,55 @@ export function AppShell({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NavRow({
|
||||||
|
item,
|
||||||
|
expanded,
|
||||||
|
mobile = false,
|
||||||
|
inGroup = false,
|
||||||
|
onNavigate,
|
||||||
|
}: {
|
||||||
|
item: NavItem
|
||||||
|
expanded: boolean
|
||||||
|
mobile?: boolean
|
||||||
|
/** True when rendered inside a collapsible group — hides the per-item
|
||||||
|
* icon and indents the label so it aligns under the group header. */
|
||||||
|
inGroup?: boolean
|
||||||
|
onNavigate?: () => void
|
||||||
|
}) {
|
||||||
|
const Icon = item.icon
|
||||||
|
const prefix = mobile ? "nav-mobile-" : "nav-"
|
||||||
|
// Icons are hidden inside groups in the expanded rail. The collapsed
|
||||||
|
// icon-only rail (expanded=false) always shows icons regardless.
|
||||||
|
const showIcon = !inGroup || !expanded
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={item.to}
|
||||||
|
end={item.end}
|
||||||
|
title={expanded ? undefined : item.label}
|
||||||
|
onClick={onNavigate}
|
||||||
|
data-action={`${prefix}${item.label.toLowerCase()}`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
[
|
||||||
|
"flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors duration-fast ease-standard",
|
||||||
|
expanded
|
||||||
|
? inGroup
|
||||||
|
? // Indent the label by chevron(12) + gap(8) = 20px so it
|
||||||
|
// visually aligns under the group header label.
|
||||||
|
"justify-start pl-[1.625rem] pr-3"
|
||||||
|
: "justify-start px-3"
|
||||||
|
: "justify-center px-3",
|
||||||
|
isActive
|
||||||
|
? "bg-primary/10 text-primary"
|
||||||
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||||
|
].join(" ")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{showIcon ? <Icon className="size-5 shrink-0" /> : null}
|
||||||
|
{expanded ? <span className="truncate">{item.label}</span> : null}
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function NotificationDispatcher() {
|
function NotificationDispatcher() {
|
||||||
// Hidden bridge so the action bus can create real notifications:
|
// Hidden bridge so the action bus can create real notifications:
|
||||||
// fill notify-title "Hello"
|
// fill notify-title "Hello"
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export function LlmConfigurationsPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-start justify-between gap-4">
|
<CardHeader className="flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-start">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle>LLM configurations</CardTitle>
|
<CardTitle>LLM configurations</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -235,7 +235,7 @@ export function LlmConfigurationsPanel() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{usage ? (
|
{usage ? (
|
||||||
<div className="flex shrink-0 flex-col items-end rounded-md border bg-muted/40 px-3 py-2 text-right">
|
<div className="flex shrink-0 flex-col items-start rounded-md border bg-muted/40 px-3 py-2 text-left sm:items-end sm:text-right">
|
||||||
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||||
Spend (30d)
|
Spend (30d)
|
||||||
</span>
|
</span>
|
||||||
@@ -248,7 +248,7 @@ export function LlmConfigurationsPanel() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex shrink-0 gap-2">
|
<div className="flex shrink-0 flex-wrap gap-2">
|
||||||
{hasLocalSettings && configs.length === 0 ? (
|
{hasLocalSettings && configs.length === 0 ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -368,7 +368,7 @@ function ConfigRow({
|
|||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center justify-between gap-3 px-1 py-2.5">
|
<li className="flex flex-col items-stretch justify-between gap-3 px-1 py-2.5 sm:flex-row sm:items-center">
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -421,7 +421,7 @@ function ConfigRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-3">
|
<div className="flex shrink-0 items-center justify-end gap-3 pl-7 sm:pl-0">
|
||||||
{spend && spend.cost_cents > 0 ? (
|
{spend && spend.cost_cents > 0 ? (
|
||||||
<div className="flex flex-col items-end text-right">
|
<div className="flex flex-col items-end text-right">
|
||||||
<span className="font-mono text-sm tabular-nums">
|
<span className="font-mono text-sm tabular-nums">
|
||||||
|
|||||||
@@ -171,10 +171,10 @@ export default function SettingsRoute() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<div className="grid gap-6 md:grid-cols-[14rem_1fr]">
|
<div className="grid gap-6 pt-10 md:grid-cols-[14rem_1fr] md:pt-0">
|
||||||
<nav
|
<nav
|
||||||
aria-label="Settings sections"
|
aria-label="Settings sections"
|
||||||
className="flex flex-row gap-1 overflow-x-auto md:flex-col md:gap-0.5"
|
className="flex flex-row flex-wrap gap-1 md:flex-col md:flex-nowrap md:gap-0.5"
|
||||||
>
|
>
|
||||||
{sections.map((s) => {
|
{sections.map((s) => {
|
||||||
const Icon = s.icon
|
const Icon = s.icon
|
||||||
|
|||||||
Reference in New Issue
Block a user