A phone's g1t is flat, as a computer's is. The page fills the screen above the bottom bar with no inset, no rounded corners and no frame around it, and the bar runs flat along the bottom edge under a rule, on the rail's colour, instead of floating as a rounded card. Each tab's name is centred under its icon (they sat to the left of their cells, so Home and Chat drifted off their icons), and a long one gives way rather than crowding its neighbours; the tab you are in is marked as the rail marks the app you are in, its icon on the sidebar's accent, instead of a line on the bar's top edge. More stands flat on the bar, and the account sheet and More's tiles and rows take the sidebar's corners.
The sidebar a phone opens is most of the screen wide, up to 22rem, instead of the computer's 15rem, so project names read whole and the pins sit at the row's end. A group's button, such as Projects' new project, shares its label's row, so it stays level with the words at any text size instead of slipping under them; and the sidebar's rules stay inside its edges (their width lost to the separator's own full width, so they ran past it). The phone app's bar follows the site's: flat on the rail's colour, the tab you are in on the rail's accent. The workspaces guide says how the phone's bar and sidebar look.
- Claude-Session:
- https://claude.ai/code/session_01MQE64a4WNN14F8GELCiuc9
7 files+97−670/7 viewed
| 753 | 753 | ||
| 754 | 754 | | | What it does | | |
| 755 | 755 | | --- | --- | | |
| 756 | − | | **The bar along the bottom** | **Home**, **Chat**, **Notifications**, **Agents** and **Code** (for members with Code access), each with what is unread, and **More**. It steps aside while the keyboard is up and inside a conversation. | | |
| 756 | + | | **The bar along the bottom** | **Home**, **Chat**, **Notifications**, **Agents** and **Code** (for members with Code access), each with what is unread, and **More**. It runs flat along the bottom edge, as the rail stands beside the page on a computer, and the page fills the screen above it. The app you are in is marked as on the rail. It steps aside while the keyboard is up and inside a conversation. | | |
| 757 | 757 | | **More** | A panel above the bar with **All apps**, the [Marketplace](/guides/marketplace/), your pinned apps, **Artifacts**, **People**, **Workspace**, and **You and help**: your status, profile and settings, [appearance](#appearance), the documentation, support, status, the keyboard's shortcuts and signing out. | | |
| 758 | − | | **The sidebar button**, at the left of the page's header | Opens the sidebar of the app you are in from the left: the same lists and links as on a computer. Tap outside it, or open a page, and it closes. | | |
| 758 | + | | **The sidebar button**, at the left of the page's header | Opens the sidebar of the app you are in from the left, most of the screen wide: the same lists and links as on a computer. Tap outside it, or open a page, and it closes. | | |
| 759 | 759 | | **The tab you are already on** | Tap it again to open that app's sidebar too. | | |
| 760 | 760 | | **The workspace's name** in the page's header | Switches workspace, or opens your profile or settings. | | |
| 761 | 761 |
| 1 | 1 | import * as Notifications from "expo-notifications"; | |
| 2 | 2 | import { Tabs, router, useGlobalSearchParams } from "expo-router"; | |
| 3 | 3 | import { Bell, Bot, Code2, House, Menu, MessagesSquare } from "lucide-react-native"; | |
| 4 | − | import { useEffect, useRef } from "react"; | |
| 4 | + | import type { LucideIcon } from "lucide-react-native"; | |
| 5 | + | import { type ComponentType, useEffect, useRef } from "react"; | |
| 6 | + | import { StyleSheet, View } from "react-native"; | |
| 5 | 7 | ||
| 6 | 8 | import { Banner } from "@/components/chat/banner"; | |
| 7 | 9 | import { FeedProvider, useFeed } from "@/lib/feed"; | |
| ⋯ | |||
| 44 | 46 | initialRouteName="chat" | |
| 45 | 47 | screenOptions={{ | |
| 46 | 48 | headerShown: false, | |
| 47 | − | tabBarActiveTintColor: colors.accent, | |
| 48 | − | tabBarInactiveTintColor: colors.muted, | |
| 49 | − | tabBarStyle: { backgroundColor: colors.surface, borderTopColor: colors.line }, | |
| 50 | − | tabBarLabelStyle: { fontSize: 11, fontWeight: "600" }, | |
| 49 | + | // The site's phone bar: flat along the bottom on the rail's colour, the | |
| 50 | + | // app you are in marked as the rail marks it (apps/web components/mobile.tsx). | |
| 51 | + | tabBarActiveTintColor: colors.fg, | |
| 52 | + | tabBarInactiveTintColor: colors.faint, | |
| 53 | + | tabBarStyle: { backgroundColor: colors.rail, borderTopColor: colors.line, borderTopWidth: StyleSheet.hairlineWidth, elevation: 0 }, | |
| 54 | + | tabBarLabelStyle: { fontSize: 11, fontWeight: "500" }, | |
| 55 | + | tabBarIconStyle: { width: 44, height: 28 }, | |
| 51 | 56 | }} | |
| 52 | 57 | > | |
| 53 | 58 | <Tabs.Screen name="index" options={{ href: null }} /> | |
| 54 | − | <Tabs.Screen name="home" options={{ title: "Home", tabBarIcon: ({ color, size }) => <House color={color} size={size} /> }} /> | |
| 59 | + | <Tabs.Screen name="home" options={{ title: "Home", tabBarIcon: (props) => <BarIcon icon={House} {...props} /> }} /> | |
| 55 | 60 | <Tabs.Screen | |
| 56 | 61 | name="chat" | |
| 57 | 62 | options={{ | |
| 58 | 63 | title: "Chat", | |
| 59 | − | tabBarIcon: ({ color, size }) => <MessagesSquare color={color} size={size} />, | |
| 64 | + | tabBarIcon: (props) => <BarIcon icon={MessagesSquare} {...props} />, | |
| 60 | 65 | tabBarBadge: badge, | |
| 61 | 66 | tabBarBadgeStyle: { backgroundColor: mentions > 0 ? colors.accent : colors.lineStrong, color: mentions > 0 ? colors.onAccent : colors.fg, fontSize: 11 }, | |
| 62 | 67 | }} | |
| 63 | 68 | /> | |
| 64 | − | <Tabs.Screen name="notifications" options={{ title: "Notifications", tabBarIcon: ({ color, size }) => <Bell color={color} size={size} /> }} /> | |
| 65 | − | <Tabs.Screen name="agents" options={{ title: "Agents", tabBarIcon: ({ color, size }) => <Bot color={color} size={size} /> }} /> | |
| 66 | − | <Tabs.Screen name="code" options={{ title: "Code", href: code ? undefined : null, tabBarIcon: ({ color, size }) => <Code2 color={color} size={size} /> }} /> | |
| 69 | + | <Tabs.Screen name="notifications" options={{ title: "Notifications", tabBarIcon: (props) => <BarIcon icon={Bell} {...props} /> }} /> | |
| 70 | + | <Tabs.Screen name="agents" options={{ title: "Agents", tabBarIcon: (props) => <BarIcon icon={Bot} {...props} /> }} /> | |
| 71 | + | <Tabs.Screen name="code" options={{ title: "Code", href: code ? undefined : null, tabBarIcon: (props) => <BarIcon icon={Code2} {...props} /> }} /> | |
| 67 | 72 | <Tabs.Screen name="customize" options={{ href: null }} /> | |
| 68 | − | <Tabs.Screen name="more" options={{ title: "More", tabBarIcon: ({ color, size }) => <Menu color={color} size={size} /> }} /> | |
| 73 | + | <Tabs.Screen name="more" options={{ title: "More", tabBarIcon: (props) => <BarIcon icon={Menu} {...props} /> }} /> | |
| 69 | 74 | </Tabs> | |
| 70 | 75 | ); | |
| 71 | 76 | } | |
| 72 | 77 | ||
| 78 | + | /** A tab's icon: on the rail's accent in a 10px-rounded square when it is the tab you are in. */ | |
| 79 | + | function BarIcon({ icon: Icon, focused }: { icon: LucideIcon | ComponentType<{ color: string; size: number }>; focused: boolean }) { | |
| 80 | + | const colors = useColors(); | |
| 81 | + | return ( | |
| 82 | + | <View style={{ width: 44, height: 28, borderRadius: 10, alignItems: "center", justifyContent: "center", backgroundColor: focused ? colors.railAccent : "transparent" }}> | |
| 83 | + | <Icon color={focused ? colors.fg : colors.muted} size={20} /> | |
| 84 | + | </View> | |
| 85 | + | ); | |
| 86 | + | } | |
| 87 | + | ||
| 73 | 88 | /** | |
| 74 | 89 | * Registers the phone for notifications once signed in (asking the first | |
| 75 | 90 | * time), and opens what a tapped notification is about: the thread or | |
| 21 | 21 | accentLow: string; | |
| 22 | 22 | /** Text on an accent-filled button. */ | |
| 23 | 23 | onAccent: string; | |
| 24 | + | /** The rail and the bottom bar (apps/web app.css `--color-rail`). */ | |
| 25 | + | rail: string; | |
| 26 | + | /** What marks the app you are in on the rail (`--color-sidebar-accent`). */ | |
| 27 | + | railAccent: string; | |
| 24 | 28 | success: string; | |
| 25 | 29 | successLow: string; | |
| 26 | 30 | info: string; | |
| ⋯ | |||
| 43 | 47 | accentDim: "#8577e0", | |
| 44 | 48 | accentLow: "#221e38", | |
| 45 | 49 | onAccent: "#0f0f11", | |
| 50 | + | rail: "#050507", | |
| 51 | + | railAccent: "#1c1c20", | |
| 46 | 52 | success: "#86efc4", | |
| 47 | 53 | successLow: "#10302a", | |
| 48 | 54 | info: "#8ab4ff", | |
| ⋯ | |||
| 65 | 71 | accentDim: "#9584dc", | |
| 66 | 72 | accentLow: "#ece8fb", | |
| 67 | 73 | onAccent: "#ffffff", | |
| 74 | + | rail: "#ffffff", | |
| 75 | + | railAccent: "#e1e1de", | |
| 68 | 76 | success: "#0d7a52", | |
| 69 | 77 | successLow: "#e2f4ec", | |
| 70 | 78 | info: "#2160cc", | |
| 353 | 353 | ||
| 354 | 354 | /* | |
| 355 | 355 | * The app's frame (components/shell.tsx), set on <body data-frame="app">: | |
| 356 | − | * the page fills the window beside the rail and the sidebar; on a phone | |
| 357 | − | * it is a rounded panel inset above the bottom bar. What sticks to the top | |
| 358 | − | * or the bottom of a page reads these: | |
| 359 | − | * --frame-top the gap above the page (none on a computer) | |
| 356 | + | * the page fills the window beside the rail and the sidebar, and on a | |
| 357 | + | * phone it fills the window above the bottom bar, flat, as on a computer. | |
| 358 | + | * What sticks to the top or the bottom of a page reads these: | |
| 359 | + | * --frame-top what is above the page: a phone's status bar, where it overlaps | |
| 360 | 360 | * --topbar-h that gap plus the top bar: where sticky things sit | |
| 361 | − | * --tabbar-h what the bottom takes: on a phone, the gap under the panel and the bar | |
| 361 | + | * --tabbar-h what the bottom takes: on a phone, the bar and the home indicator under it | |
| 362 | 362 | * --page-h the height left for a page that fills the window (Chat) | |
| 363 | 363 | * The public frame keeps a plain header 3.5rem tall and nothing at the bottom. | |
| 364 | 364 | */ | |
| ⋯ | |||
| 374 | 374 | } | |
| 375 | 375 | @media (max-width: 767px) { | |
| 376 | 376 | body[data-frame="app"] { | |
| 377 | − | --frame-top: max(0.375rem, env(safe-area-inset-top)); | |
| 378 | − | /* The bottom bar: 64px tall, 8px off the edge, 6px under the panel. */ | |
| 379 | − | --tabbar-h: calc(4.875rem + env(safe-area-inset-bottom)); | |
| 377 | + | --frame-top: env(safe-area-inset-top); | |
| 378 | + | /* The bottom bar: 56px tall along the bottom edge, and the home indicator's room under it. */ | |
| 379 | + | --tabbar-h: calc(3.5rem + env(safe-area-inset-bottom)); | |
| 380 | 380 | } | |
| 381 | 381 | } | |
| 382 | 382 | ||
| 120 | 120 | type Tab = { key: ModeKey; label: string; badge?: { count: number; loud: boolean } }; | |
| 121 | 121 | ||
| 122 | 122 | /** | |
| 123 | − | * The bottom bar of a phone, floating 8px from the edges: Home, Chat, | |
| 123 | + | * The bottom bar of a phone, flat along the bottom edge under a rule, as | |
| 124 | + | * the rail stands flat beside the page on a computer: Home, Chat, | |
| 124 | 125 | * Notifications, Agents and Code (for a member with Code access), each | |
| 125 | − | * with what is unread, and More. It steps aside while the keyboard is up | |
| 126 | + | * with what is unread, and More. The tab you are in has the rail's own | |
| 127 | + | * mark: its icon on the sidebar's accent. It steps aside while the keyboard is up | |
| 126 | 128 | * and inside a conversation. The tab you are in, tapped again, opens its | |
| 127 | 129 | * mode's menu (its sidebar). | |
| 128 | 130 | */ | |
| ⋯ | |||
| 160 | 162 | <> | |
| 161 | 163 | <nav | |
| 162 | 164 | aria-label="Apps" | |
| 163 | − | className="fixed right-2 bottom-[calc(0.5rem+env(safe-area-inset-bottom))] left-2 z-40 h-16 rounded-2xl border border-line bg-rail px-1 shadow-[0_1px_0_rgba(255,255,255,0.03)_inset,0_8px_24px_rgba(0,0,0,0.35)] md:hidden in-data-[keyboard=open]:hidden" | |
| 165 | + | className="fixed inset-x-0 bottom-0 z-40 h-(--tabbar-h) border-t border-line bg-rail pb-[env(safe-area-inset-bottom)] md:hidden in-data-[keyboard=open]:hidden" | |
| 164 | 166 | > | |
| 165 | 167 | <ul className="grid h-full" style={{ gridTemplateColumns: `repeat(${tabs.length + 1}, minmax(0, 1fr))` }}> | |
| 166 | 168 | {tabs.map((tab) => { | |
| ⋯ | |||
| 183 | 185 | }} | |
| 184 | 186 | className={BAR_ITEM} | |
| 185 | 187 | > | |
| 186 | − | {current && <TopBar />} | |
| 187 | 188 | <BarIcon current={current}> | |
| 188 | 189 | {appIcon(tab.key as BuiltinApp, 20)} | |
| 189 | 190 | {tab.badge && <CountBadge count={count} loud={tab.badge.loud} />} | |
| 190 | 191 | </BarIcon> | |
| 191 | − | <span className={`w-full truncate text-[0.625rem] leading-tight font-medium max-[380px]:text-[0.5625rem] max-[380px]:tracking-[-0.02em] ${current ? "text-fg" : "text-faint"}`}>{tab.label}</span> | |
| 192 | + | <BarLabel current={current}>{tab.label}</BarLabel> | |
| 192 | 193 | </Link> | |
| 193 | 194 | </li> | |
| 194 | 195 | ); | |
| 195 | 196 | })} | |
| 196 | 197 | <li className="min-w-0"> | |
| 197 | 198 | <button type="button" aria-expanded={more} aria-haspopup="dialog" onClick={() => setMore(true)} className={BAR_ITEM}> | |
| 198 | − | {(more || !inTabs) && <TopBar />} | |
| 199 | 199 | <BarIcon current={more || !inTabs}> | |
| 200 | 200 | <Ellipsis size={20} /> | |
| 201 | 201 | </BarIcon> | |
| 202 | − | <span className={`w-full truncate text-[0.625rem] leading-tight font-medium ${more || !inTabs ? "text-fg" : "text-faint"}`}>More</span> | |
| 202 | + | <BarLabel current={more || !inTabs}>More</BarLabel> | |
| 203 | 203 | </button> | |
| 204 | 204 | </li> | |
| 205 | 205 | </ul> | |
| ⋯ | |||
| 209 | 209 | ); | |
| 210 | 210 | } | |
| 211 | 211 | ||
| 212 | − | const BAR_ITEM = "relative flex h-full w-full flex-col items-center justify-center gap-0.5 rounded-xl outline-none transition-colors focus-visible:ring-2 focus-visible:ring-accent active:bg-raised/60"; | |
| 212 | + | const BAR_ITEM = "group relative flex h-full w-full flex-col items-center justify-center gap-1 outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset"; | |
| 213 | 213 | ||
| 214 | − | /** The bar on the bottom bar's top edge, over the current tab. */ | |
| 215 | − | function TopBar() { | |
| 216 | − | return <span aria-hidden="true" className="absolute -top-px left-1/2 h-[3px] w-[22px] -translate-x-1/2 rounded-b-[3px] bg-fg" />; | |
| 214 | + | /** A tab's icon, marked as the rail marks the app you are in (ui/sidebar.tsx, `size: icon`). */ | |
| 215 | + | function BarIcon({ current, children }: { current: boolean; children: ReactNode }) { | |
| 216 | + | return ( | |
| 217 | + | <span | |
| 218 | + | className={`relative flex h-7 w-11 items-center justify-center rounded-[10px] transition-colors group-active:bg-sidebar-accent/60 ${current ? "bg-sidebar-accent text-fg" : "text-muted"}`} | |
| 219 | + | > | |
| 220 | + | {children} | |
| 221 | + | </span> | |
| 222 | + | ); | |
| 217 | 223 | } | |
| 218 | 224 | ||
| 219 | − | function BarIcon({ current, children }: { current: boolean; children: ReactNode }) { | |
| 225 | + | /** A tab's name, centred under its icon; a long one gives way rather than crowding its neighbours. */ | |
| 226 | + | function BarLabel({ current, children }: { current: boolean; children: ReactNode }) { | |
| 220 | 227 | return ( | |
| 221 | − | <span className={`relative flex size-8 items-center justify-center rounded-[10px] transition-colors ${current ? "bg-raised text-fg shadow-[inset_0_0_0_1px_var(--color-line-strong)]" : "text-muted"}`}> | |
| 228 | + | <span className={`block w-full truncate px-0.5 text-center text-[0.6875rem] leading-none font-medium max-[380px]:text-[0.625rem] ${current ? "text-fg" : "text-faint"}`}> | |
| 222 | 229 | {children} | |
| 223 | 230 | </span> | |
| 224 | 231 | ); | |
| ⋯ | |||
| 228 | 235 | function MoreTile({ to, icon, label, onClick, disabled, note }: { to?: string; icon: ReactNode; label: string; onClick?: () => void; disabled?: boolean; note?: string }) { | |
| 229 | 236 | const inner = ( | |
| 230 | 237 | <> | |
| 231 | − | <span className={`flex size-11 items-center justify-center rounded-xl ${disabled ? "bg-raised/60 text-faint" : "bg-raised text-fg"}`}>{icon}</span> | |
| 238 | + | <span className={`flex size-11 items-center justify-center rounded-[10px] ${disabled ? "bg-raised/60 text-faint" : "bg-raised text-fg"}`}>{icon}</span> | |
| 232 | 239 | <span className={`line-clamp-2 w-full text-xs leading-tight max-[380px]:text-[0.6875rem] ${disabled ? "text-faint" : "text-fg-soft"}`}>{label}</span> | |
| 233 | 240 | {note && <span className="-mt-0.5 rounded-full bg-line px-1.5 text-[0.625rem] font-medium text-muted">{note}</span>} | |
| 234 | 241 | </> | |
| 235 | 242 | ); | |
| 236 | − | const className = "flex min-w-0 flex-col items-center gap-1.5 rounded-xl px-1 py-2.5 text-center outline-none focus-visible:ring-2 focus-visible:ring-accent active:bg-raised/60"; | |
| 243 | + | const className = "flex min-w-0 flex-col items-center gap-1.5 rounded-md px-1 py-2.5 text-center outline-none focus-visible:ring-2 focus-visible:ring-accent active:bg-raised/60"; | |
| 237 | 244 | if (to) { | |
| 238 | 245 | return ( | |
| 239 | 246 | <Link to={to} className={className}> | |
| ⋯ | |||
| 249 | 256 | } | |
| 250 | 257 | ||
| 251 | 258 | /** | |
| 252 | − | * More, from the bottom bar: a rounded panel just above it, with g1t's | |
| 259 | + | * More, from the bottom bar: a flat panel standing on it, with g1t's | |
| 253 | 260 | * mark at its top and a grid of big tiles: all apps, the Marketplace, | |
| 254 | 261 | * your pinned apps, Artifacts, People, Workspace, and "You and | |
| 255 | 262 | * help", which opens your account's sheet. | |
| ⋯ | |||
| 266 | 273 | <Primitive.Content | |
| 267 | 274 | aria-describedby={undefined} | |
| 268 | 275 | onOpenAutoFocus={(event) => event.preventDefault()} | |
| 269 | − | className="fixed right-2 bottom-[calc(4.875rem+env(safe-area-inset-bottom))] left-2 z-50 max-h-[calc(100dvh-7rem)] overflow-y-auto rounded-[18px] border border-line-strong bg-rail px-3 pt-2 pb-3.5 text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-pop-in data-[state=closed]:animate-pop-out md:hidden" | |
| 276 | + | className="fixed inset-x-0 bottom-(--tabbar-h) z-50 max-h-[calc(100dvh-var(--tabbar-h)-3.5rem)] overflow-y-auto border-t border-line bg-rail px-3 pt-2 pb-3.5 text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-pop-in data-[state=closed]:animate-pop-out md:hidden" | |
| 270 | 277 | > | |
| 271 | 278 | <div aria-hidden="true" className="mx-auto mt-0.5 mb-2 h-1 w-9 rounded-full bg-line-strong" /> | |
| 272 | 279 | <div className="mb-2 flex items-center gap-2 px-1"> | |
| ⋯ | |||
| 342 | 349 | aria-describedby={undefined} | |
| 343 | 350 | onOpenAutoFocus={(event) => event.preventDefault()} | |
| 344 | 351 | style={{ transform: drag ? `translateY(${drag}px)` : undefined, transition: from.current == null ? "transform 0.2s ease-out" : "none" }} | |
| 345 | − | className="fixed inset-x-0 bottom-0 z-50 flex max-h-[88dvh] flex-col rounded-t-2xl border-t border-line-strong bg-surface pb-[env(safe-area-inset-bottom)] text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-[sheet-in-bottom_0.24s_cubic-bezier(0.16,1,0.3,1)]" | |
| 352 | + | className="fixed inset-x-0 bottom-0 z-50 flex max-h-[88dvh] flex-col border-t border-line bg-surface pb-[env(safe-area-inset-bottom)] text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-[sheet-in-bottom_0.24s_cubic-bezier(0.16,1,0.3,1)]" | |
| 346 | 353 | > | |
| 347 | 354 | <div {...handlers} className="flex shrink-0 cursor-grab touch-none justify-center pt-2.5 pb-2" aria-hidden="true"> | |
| 348 | 355 | <span className="h-1.5 w-10 rounded-full bg-line-strong" /> | |
| ⋯ | |||
| 357 | 364 | ||
| 358 | 365 | /** A row in a bottom sheet: 48px tall, an icon, words, and what sits at its end. */ | |
| 359 | 366 | export function SheetRow({ to, href, icon, children, end, onClick }: { to?: string; href?: string; icon: ReactNode; children: ReactNode; end?: ReactNode; onClick?: () => void }) { | |
| 360 | − | const className = "flex h-12 w-full items-center gap-3 rounded-xl px-3 text-left text-[0.9375rem] text-fg-soft transition-colors active:bg-raised"; | |
| 367 | + | const className = "flex h-12 w-full items-center gap-3 rounded-md px-3 text-left text-[0.9375rem] text-fg-soft transition-colors active:bg-raised"; | |
| 361 | 368 | const inner = ( | |
| 362 | 369 | <> | |
| 363 | 370 | <span className="flex w-6 shrink-0 justify-center text-muted [&_svg]:size-5">{icon}</span> | |
| 288 | 288 | children, | |
| 289 | 289 | }: { | |
| 290 | 290 | title: string; | |
| 291 | − | /** A small button at the label's end: what the group makes (a `SidebarGroupAction`). */ | |
| 291 | + | /** A small button at the label's end, in its row: what the group makes (a `SidebarGroupAction` with `relative top-auto right-auto`). */ | |
| 292 | 292 | action?: ReactNode; | |
| 293 | 293 | className?: string; | |
| 294 | 294 | children: ReactNode; | |
| 295 | 295 | }) { | |
| 296 | 296 | return ( | |
| 297 | 297 | <SidebarGroup className={className}> | |
| 298 | − | <SidebarGroupLabel asChild> | |
| 299 | − | <h2>{title}</h2> | |
| 300 | − | </SidebarGroupLabel> | |
| 301 | − | {action} | |
| 298 | + | <div className="flex items-center justify-between gap-2 pr-1"> | |
| 299 | + | <SidebarGroupLabel asChild> | |
| 300 | + | <h2>{title}</h2> | |
| 301 | + | </SidebarGroupLabel> | |
| 302 | + | {action} | |
| 303 | + | </div> | |
| 302 | 304 | <SidebarGroupContent> | |
| 303 | 305 | <SidebarMenu>{children}</SidebarMenu> | |
| 304 | 306 | </SidebarGroupContent> | |
| ⋯ | |||
| 688 | 690 | ); | |
| 689 | 691 | return ( | |
| 690 | 692 | <SidebarGroup> | |
| 691 | − | <SidebarGroupLabel asChild> | |
| 692 | − | <h2>Projects</h2> | |
| 693 | − | </SidebarGroupLabel> | |
| 694 | − | <Hint label="New project"> | |
| 695 | − | <SidebarGroupAction asChild> | |
| 696 | − | <Link to="/new" aria-label="New project"> | |
| 697 | − | <Plus /> | |
| 698 | − | </Link> | |
| 699 | − | </SidebarGroupAction> | |
| 700 | − | </Hint> | |
| 693 | + | {/* The label and its button share a row, so the button stays level with the words at any text size. */} | |
| 694 | + | <div className="flex items-center justify-between gap-2 pr-1"> | |
| 695 | + | <SidebarGroupLabel asChild> | |
| 696 | + | <h2>Projects</h2> | |
| 697 | + | </SidebarGroupLabel> | |
| 698 | + | <Hint label="New project"> | |
| 699 | + | <SidebarGroupAction asChild className="relative top-auto right-auto"> | |
| 700 | + | <Link to="/new" aria-label="New project"> | |
| 701 | + | <Plus /> | |
| 702 | + | </Link> | |
| 703 | + | </SidebarGroupAction> | |
| 704 | + | </Hint> | |
| 705 | + | </div> | |
| 701 | 706 | <SidebarGroupContent> | |
| 702 | 707 | {/* Only there once something is pinned: each row below has its own pin. */} | |
| 703 | 708 | {pinned.length > 0 && ( | |
| ⋯ | |||
| 1941 | 1946 | * filling the rest of the window, with its top bar across the top and a | |
| 1942 | 1947 | * line under it: the sidebar's toggle, where you are, search, Ask g1t and | |
| 1943 | 1948 | * Create new. On a phone the rail is a bar along the bottom and the page | |
| 1944 | − | * a rounded panel above it. Only for someone signed in; without a workspace yet, there | |
| 1949 | + | * fills the window above it, flat as well. Only for someone signed in; without a workspace yet, there | |
| 1945 | 1950 | * is no rail. The rail and the sidebar are drawn from the sidebar kit | |
| 1946 | 1951 | * (components/ui/sidebar.tsx): one provider holds whether the sidebar is | |
| 1947 | 1952 | * open on a computer and whether the drawer is. | |
| ⋯ | |||
| 2089 | 2094 | </SidebarMobile> | |
| 2090 | 2095 | )} | |
| 2091 | 2096 | ||
| 2092 | − | <div className={`min-h-dvh pt-(--frame-top) pb-(--tabbar-h) max-md:px-1.5 ${pad}`}> | |
| 2097 | + | <div className={`min-h-dvh pt-(--frame-top) pb-(--tabbar-h) ${pad}`}> | |
| 2093 | 2098 | {/* The top bar always has its line, so a page's own header (data-page-head: a conversation's, a project's, | |
| 2094 | 2099 | a sticky one under an open artifact) sits under it as its own band. */} | |
| 2095 | − | <div className="flex min-h-[calc(100dvh-var(--frame-top)-var(--tabbar-h))] min-w-0 flex-col bg-bg max-md:rounded-[14px]"> | |
| 2100 | + | <div className="flex min-h-[calc(100dvh-var(--frame-top)-var(--tabbar-h))] min-w-0 flex-col bg-bg"> | |
| 2096 | 2101 | <header | |
| 2097 | − | className={`sticky top-(--frame-top) z-30 flex h-14 items-center gap-1 border-b border-line bg-bg pr-2 pl-2 max-md:rounded-t-[14px] sm:pr-3 ${inline ? "lg:pl-3.5" : ""} ${conversation ? "max-md:hidden" : ""}`} | |
| 2102 | + | className={`sticky top-(--frame-top) z-30 flex h-14 items-center gap-1 border-b border-line bg-bg pr-2 pl-2 sm:pr-3 ${inline ? "lg:pl-3.5" : ""} ${conversation ? "max-md:hidden" : ""}`} | |
| 2098 | 2103 | > | |
| 2099 | 2104 | {panel && ( | |
| 2100 | 2105 | <SidebarTrigger | |
| ⋯ | |||
| 2142 | 2147 | </main> | |
| 2143 | 2148 | </div> | |
| 2144 | 2149 | </div> | |
| 2145 | − | {/* On a phone the page is a rounded panel above the bottom bar: a frame over it, with the background beyond, | |
| 2146 | − | so what scrolls stays inside. A phone's conversation, full screen over everything, goes without. */} | |
| 2147 | − | <div | |
| 2148 | − | aria-hidden="true" | |
| 2149 | − | className={`pointer-events-none fixed top-(--frame-top) right-1.5 bottom-(--tabbar-h) left-1.5 z-[35] rounded-[14px] shadow-[0_0_0_1px_var(--color-line),0_0_0_100vmax_var(--color-shell)] md:hidden ${conversation ? "max-md:hidden" : ""}`} | |
| 2150 | − | /> | |
| 2151 | 2150 | {ws && <BottomBar user={user} workspace={ws} pins={shell.pins ?? []} unread={unread} onReselect={panel ? () => setDrawer(true) : undefined} />} | |
| 2152 | 2151 | <CommandPalette open={palette} onOpenChange={setPalette} commands={commands} repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null} /> | |
| 2153 | 2152 | </SidebarProvider> | |
| 160 | 160 | event.preventDefault(); | |
| 161 | 161 | (event.currentTarget as HTMLElement | null)?.focus(); | |
| 162 | 162 | }} | |
| 163 | − | className={cn("group/sidebar w-[min(var(--sidebar-width),86vw)] border-sidebar-border bg-sidebar px-0 text-fg", className)} | |
| 163 | + | // A phone's drawer takes most of the screen: names read whole and rows reach a thumb, with the page still showing beside it. | |
| 164 | + | className={cn("group/sidebar w-[min(22rem,86vw)] border-sidebar-border bg-sidebar px-0 text-fg", className)} | |
| 164 | 165 | {...props} | |
| 165 | 166 | > | |
| 166 | 167 | <SheetTitle className="sr-only">{title}</SheetTitle> | |
| ⋯ | |||
| 235 | 236 | } | |
| 236 | 237 | ||
| 237 | 238 | export function SidebarSeparator({ className, ...props }: ComponentProps<typeof Separator>) { | |
| 238 | − | return <Separator data-slot="sidebar-separator" className={cn("mx-2 my-2 w-auto bg-sidebar-border", className)} {...props} />; | |
| 239 | + | return <Separator data-slot="sidebar-separator" className={cn("mx-2 my-2 bg-sidebar-border data-[orientation=horizontal]:w-auto", className)} {...props} />; | |
| 239 | 240 | } | |
| 240 | 241 | ||
| 241 | 242 | /** What scrolls: the groups, between the header and the footer. */ | |