Primitives
Menu — ParkingOS (rail and panel)
The AppShell's two tiers as standalone menus: `rail` for modules, `panel` for a module's screens. The panel folds away and comes back (⌘B).
Vehicles inside
The page takes the room the panel gives up. Fold it with its heading button, the rail's button, or ⌘B.
"use client";
import { Menu, MenuGroup, MenuItem, Tooltip } from "@waveinno/ui";
import {
BarChart3,
Building2,
CircleDollarSign,
House,
PanelLeftClose,
PanelLeftOpen,
ParkingSquare,
Settings,
Tags,
Users,
UsersRound,
} from "lucide-react";
import * as React from "react";
import { ParkingOSMark } from "./product-marks";
export default function ParkingOSDemo() {
const [mod, setMod] = React.useState("ops");
const [screen, setScreen] = React.useState("inside");
const [panelShut, setPanelShut] = React.useState(false);
return (
<div className="flex items-stretch gap-3">
<Menu variant="rail" value={mod} onValueChange={setMod} heading={<ParkingOSMark className="size-9" />} aria-label="ParkingOS modules">
<MenuGroup label="Work">
<MenuItem value="home" icon={<House />}>
Home
</MenuItem>
<MenuItem value="ops" icon={<ParkingSquare />} count={3}>
Ops
</MenuItem>
<MenuItem value="sites" icon={<Building2 />}>
Sites
</MenuItem>
<MenuItem value="pricing" icon={<Tags />}>
Pricing
</MenuItem>
<MenuItem value="clients" icon={<UsersRound />}>
Clients
</MenuItem>
<MenuItem value="finance" icon={<CircleDollarSign />}>
Finance
</MenuItem>
<MenuItem value="reports" icon={<BarChart3 />}>
Reports
</MenuItem>
</MenuGroup>
<MenuGroup label="Platform">
<MenuItem value="team" icon={<Users />}>
Team
</MenuItem>
<MenuItem value="settings" icon={<Settings />}>
Settings
</MenuItem>
</MenuGroup>
<div className="mt-auto pt-2">
<Tooltip label={panelShut ? "Show panel" : "Hide panel"} shortcut="⌘B" side="right">
<button
type="button"
aria-label={panelShut ? "Show panel" : "Hide panel"}
aria-expanded={!panelShut}
onClick={() => setPanelShut((v) => !v)}
className="mx-auto grid size-10 cursor-pointer place-items-center rounded-ctl text-muted transition-colors hover:bg-hover hover:text-text [&_svg]:size-[18px]"
>
{panelShut ? <PanelLeftOpen /> : <PanelLeftClose />}
</button>
</Tooltip>
</div>
</Menu>
<Menu
variant="panel"
value={screen}
onValueChange={setScreen}
heading="Operations"
collapsible
collapsedMode="hidden"
collapsed={panelShut}
onCollapsedChange={setPanelShut}
collapseShortcut
aria-label="Operations sections"
className="w-[240px]"
>
<MenuGroup label="Live">
<MenuItem value="inside" count={128}>
Vehicles inside
</MenuItem>
<MenuItem value="approvals" count={3}>
Approvals
</MenuItem>
<MenuItem value="review">Review</MenuItem>
<MenuItem value="arrivals">Arrivals</MenuItem>
</MenuGroup>
<MenuGroup label="History">
<MenuItem value="sessions">Sessions</MenuItem>
<MenuItem value="incidents">Incidents</MenuItem>
</MenuGroup>
</Menu>
<div className="hidden min-w-0 flex-1 rounded-card border border-dashed border-border2 p-5 text-[13px] text-muted md:block">
<p className="disp text-[16px] font-semibold capitalize text-text">{screen === "inside" ? "Vehicles inside" : screen}</p>
<p className="mt-1">The page takes the room the panel gives up. Fold it with its heading button, the rail's button, or ⌘B.</p>
</div>
</div>
);
}Imports from ./product-marks are the docs' own (a product's logo). Use your product's mark in their place.
Import#
Everything comes from the package root. The example above uses:
import { Menu, MenuGroup, MenuItem, Tooltip } from "@waveinno/ui";Props#
Read from the source of this version, so they match the code exactly.
<Menu>
src/components/ui/menu.tsx| Prop | Type | Default | Description |
|---|---|---|---|
variant | "soft" | "line" | "raised" | "gradient" | "dark" | "panel" | "rail" | "soft" | |
value | string | — | The chosen item's `value` (controlled). |
defaultValue | string | — | |
onValueChange | (value: string) => void | — | |
heading | React.ReactNode | — | On top of the list: a title, or in the rail, the product mark. |
collapsible | boolean | — | A collapse toggle in the heading row. |
collapsed | boolean | — | |
defaultCollapsed | boolean | false | |
onCollapsedChange | (collapsed: boolean) => void | — | |
collapsedMode | "icons" | "hidden" | "icons" | `icons`: narrow to the icons. `hidden`: fold away entirely (toggle it from outside). |
collapseShortcut | boolean | false | ⌘B / Ctrl+B toggles it. Off by default: AppShell already uses ⌘B for its panel. |
Also takes Omit<React.HTMLAttributes<HTMLElement>, "defaultValue" | "onChange">.
<MenuGroup>
src/components/ui/menu.tsx| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | |
action | React.ReactNode | — | Something small on the label's right: a "+" button, a count. |
className | string | — | |
childrenrequired | React.ReactNode | — |
<MenuItem>
src/components/ui/menu.tsx| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | — | |
description | React.ReactNode | — | Second line, muted: what is behind the item. Puts the icon in a tile. |
badge | React.ReactNode | — | |
count | number | string | — | A number on the right: unread, pending, items inside. |
shortcut | string | — | |
value | string | — | Chosen when it matches Menu's `value`; clicking makes it the value. |
active | boolean | — | Mark it chosen yourself, e.g. from the router's pathname. Overrides `value`. |
danger | boolean | — | |
asChild | boolean | — | Render your own element (a Next.js Link): `<MenuItem asChild><Link href="/x">Label</Link></MenuItem>`. |
Also takes Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "value">.
<Tooltip>
src/components/ui/tooltip.tsx| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | |
shortcut | string | — | |
childrenrequired | React.ReactNode | — | |
side | "top" | "right" | "bottom" | "left" | "bottom" |
Install#
If the project does not have the package yet, add it, then follow the setup for Next.js or Vite once.
$ npm install @waveinno/ui