SessionRail

import { SessionRail, RailBar, RailNotice, StatusDot } from '@hanzo/ui/chat'

The account, and its menu: who, the organization as a choice, the account's places, and signing out. */ function account(org: string, setOrg: (o: string) => void): RailAccount { return { name: 'Dave', sub: org, email: 'dave@acme.test', groups: [ { label: 'Organization', items: ['acme', 'zoo'].map((o) => ({ id: o, label: o, active: o === org, onPress: () => setOrg(o) })), }, [ { id: 'settings', label: 'Settings', icon: <Settings size={16} />, onPress: () => {} }, { id: 'usage', label: 'Usage', icon: <Gauge size={16} />, onPress: () => {} }, { id: 'help', label: 'Get help', icon: <LifeBuoy size={16} />, onPress: () => {} }, ], ], onSignOut: () => {}, signOutLabel: 'Log out', } } /** The rail

The name and a search at the head, New, the surface's places, the recents with their status, a notice, and the account with its menu. Collapse is an explicit toggle the host keeps.

export function Default() {
const [active, setActive] = useState<string | null>(null)
const [collapsed, setCollapsed] = useState(false)
const [org, setOrg] = useState('acme')
const [slack, setSlack] = useState(true)
return (
<XStack height={560}>
<SessionRail brand="Hanzo Build" onSearch={() => {}} searchLabel="Search runs" onNew={() => setActive(null)} fresh={active === null} links={[ { id: 'artifacts', label: 'Artifacts', icon: <Box size={16} /> }, { id: 'customize', label: 'Customize', icon: <Wrench size={16} /> }, ]} more={[{ id: 'projects', label: 'Projects', icon: <Box size={16} /> }]} recents={RECENTS} active={active} onOpen={setActive} onSort={() => {}} notice={ slack ? ( <RailNotice icon={<Slack size={16} />} title="Try Hanzo in Slack" action="Set up" onAction={() => {}} onDismiss={() => setSlack(false)} /> ) : undefined } account={account(org, setOrg)} collapsed={collapsed} onCollapse={setCollapsed} label="Runs" />
</XStack>
)
}

On a phone

Narrow the window below md: the column goes, the bar over the pane opens the same rail as a drawer (menu, name, search), and anything chosen in the drawer closes it.

Hanzo Build
export function Phone() {
const [open, setOpen] = useState(false)
const [active, setActive] = useState<string | null>(null)
return (
<YStack width={390} height={560} borderWidth={1} borderColor="$borderColor">
<RailBar onMenu={() => setOpen(true)} menuLabel="Open runs" brand="Hanzo Build" onBrand={() => setActive(null)} onSearch={() => {}} searchLabel="Search runs" />
<SessionRail brand="Hanzo Build" onSearch={() => {}} onNew={() => setActive(null)} fresh={active === null} recents={RECENTS} active={active} onOpen={setActive} account={{ name: 'Sign in', onPress: () => {} }} open={open} onOpenChange={setOpen} label="Runs" />
</YStack>
)
}

A notice

An offer, its one action, and a way to put it away. The host remembers the dismissal.

Try Hanzo in Slack
Set up
export function Notice() {
return (
<YStack width={256}>
<RailNotice icon={<Slack size={16} />} title="Try Hanzo in Slack" action="Set up" onAction={() => {}} onDismiss={() => {}} />
</YStack>
)
}

Types

export type SessionStatus = 'running' | 'paused' | 'done' | 'stopped' | 'error' | 'idle'
export interface RailSession {
id: string
title: string
status?: SessionStatus
}
export interface RailLink {
id: string
label: string
icon: ReactNode
onPress?: () => void
/** This place is the current view. */
active?: boolean
}
export interface RailAccount {
/** The first line — the person's name, or their email. */
name: string
/** The second line, muted — the organization they act in. */
sub?: string
/** Avatar slot. Falls back to the first letter of `name`. */
avatar?: ReactNode
/** Heads the menu, under the name, when it is not the name itself. */
email?: string
/**
* The menu's rows, in groups with a rule between. A group whose every row
* carries `active` is a choice — a named group, the chosen row checked —
* which is how an organization switcher is drawn.
*/
groups?: UserMenuGroup[]
/** Sign out, at the foot of the menu. */
onSignOut?: () => void
signOutLabel?: string
/**
* With no menu (no `groups`, no `onSignOut`), the row is this one action —
* a signed-out rail's "Sign in".
*/
onPress?: () => void
}
export interface SessionRailProps extends Col {
/**
* The head's first line — the surface's name. A string is drawn as the
* wordmark; a node brings its own (and its own accessible name).
*/
brand?: ReactNode
/** Pressing the brand. Starts a new session (`onNew`) unless given. */
onBrand?: () => void
/** Search. The head draws a search box; the collapsed rail an icon. */
onSearch?: () => void
/** The search control's accessible name — "Search runs". */
searchLabel?: string
/** Starts a new session. */
onNew?: () => void
newLabel?: string
/** The fresh pane IS the current view, so New draws as the active row. */
fresh?: boolean
/** The surface's own places, under New. */
links?: RailLink[]
/** Behind the "More" disclosure. None, and there is no More row. */
more?: RailLink[]
moreLabel?: string
/** The sessions, newest first. */
recents: RailSession[]
recentsLabel?: string
/** The open session's id. */
active?: string | null
onOpen: (id: string) => void
/** The sort/filter control beside the Recents label. None, no control. */
onSort?: () => void
sortLabel?: string
/** Shown in place of the list when there are no recents. */
empty?: ReactNode
/** Drawn above the account row — a `RailNotice`. */
notice?: ReactNode
/** Who is signed in, and their menu. */
account?: RailAccount
/** Collapsed to the icon rail. The host owns and persists it. */
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
/** The rail's top row when collapsed — a surface's mark. It is the expand control. */
mark?: ReactNode
/** Below `md`: the drawer is open. */
open?: boolean
onOpenChange?: (open: boolean) => void
/** The landmark's name. */
label?: string
/** Expanded width, px. */
width?: number
}
export interface RailNoticeProps extends Row {
/** The notice's mark, ~16px. */
icon?: ReactNode
/** What is on offer, one line. */
title: string
/** The action's words — "Set up". */
action?: string
onAction?: () => void
/** Dismissing it. The host remembers that it was. */
onDismiss?: () => void
dismissLabel?: string
}
export interface RailBarProps extends Row {
/** Opens the rail's drawer. */
onMenu: () => void
/** The menu button's accessible name — "Open runs". */
menuLabel?: string
/** The surface's name, as the rail's head draws it. */
brand?: ReactNode
/** Pressing the brand. */
onBrand?: () => void
/** Search, as the rail's head offers it. */
onSearch?: () => void
searchLabel?: string
}

Exports

SessionRailRailBarRailNoticeStatusDottype RailAccounttype RailBarPropstype RailLinktype RailNoticePropstype RailSessiontype SessionRailPropstype SessionStatus