Overview
These layouts compose the Sidebar component and use Kappa's compact visual style. Namespace, profile, search, navigation, and New project controls work with local example data. No account, router, or backend is connected.
Sidebar component documents the primitive parts, resizing, peeking, loading, and sliding views. This separate Blocks section contains complete compositions.
| Kappa component | Purpose |
|---|---|
| Sidebar | Navigation, resizing, collapsible sections, and the mobile drawer. |
| Dropdown | Namespace, profile, and page-link menus. |
| Command Palette | Quick search and keyboard navigation between pages. |
| Button | Page actions such as New project. |
| Table | Compact project lists in the examples. |
Layouts
Grouped Navigation
A documentation sidebar with text links grouped by section, a version selector, and an editable search field. Desktop collapse hides the sidebar; mobile uses a full-screen drawer.
<script setup lang="ts">
import { computed, ref, useId, watch } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Input } from "@dicehub/kappa/components/input";
import { BookOpen, ChevronsUpDown, Search } from "@lucide/vue";
const props = withDefaults(defineProps<{ standalone?: boolean; collapsibleSections?: boolean }>(), { standalone: true, collapsibleSections: false });
const headingId = useId();
const version = ref("1.0.1");
const query = ref("");
const selected = ref("Create a project");
const groups = [
{ title: "Getting started", items: ["Introduction", "Installation", "Create a project"] },
{ title: "Project workflow", items: ["Import geometry", "Prepare a mesh", "Run a simulation", "Review results"] },
{ title: "Reference", items: ["Configuration", "Keyboard shortcuts", "Troubleshooting"] },
];
const filtered = computed(() => groups.map(group => ({ ...group, items: group.items.filter(item => item.toLowerCase().includes(query.value.trim().toLowerCase())) })).filter(group => group.items.length));
const openSections = ref<Record<string, boolean>>(Object.fromEntries(groups.map(group => [group.title, true])));
watch(query, value => {
if (props.collapsibleSections && value.trim()) {
for (const group of filtered.value) openSections.value[group.title] = true;
}
});
const section = computed(() => groups.find(group => group.items.includes(selected.value))!.title);
const slug = (name: string) => name.toLowerCase().replaceAll(" ", "-");
const positioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
</script>
<template>
<div class="sidebar-grouped-demo" :data-sidebar-block="props.collapsibleSections ? 'collapsible-sections' : 'grouped'" :data-standalone="props.standalone || undefined">
<SidebarLayout label="Documentation navigation" collapsible="offcanvas" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Documentation versions" :positioning="positioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton class="sidebar-grouped-demo__version" :aria-label="`Documentation version: ${version}`">
<template #icon><span class="sidebar-grouped-demo__mark"><BookOpen aria-hidden="true" /></span></template>
<span class="sidebar-grouped-demo__identity"><strong>Documentation</strong><span>v{{ version }}</span></span>
<ChevronsUpDown class="sidebar-grouped-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="version"><Dropdown.Label>Versions</Dropdown.Label>
<Dropdown.RadioItem v-for="item in ['1.0.1', '1.1.0-alpha', '2.0.0-beta']" :key="item" :value="item" close-on-select>v{{ item }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<div class="sidebar-grouped-demo__search"><Search aria-hidden="true" /><Input v-model="query" size="sm" aria-label="Search documentation" placeholder="Search the docs…" type="search" @keydown.esc.stop="query = ''" /></div>
<Sidebar.Group v-for="group in filtered" :key="group.title">
<Sidebar.Collapsible v-if="props.collapsibleSections" v-model:open="openSections[group.title]">
<Sidebar.CollapsibleTrigger class="sidebar-grouped-demo__section-trigger">{{ group.title }}<Sidebar.MenuChevron /></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.Menu><Sidebar.MenuItem v-for="item in group.items" :key="item">
<Sidebar.MenuButton :href="`?page=${slug(item)}`" :active="selected === item" @click.prevent="selected = item; context.setMobileOpen(false)">{{ item }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
<template v-else>
<Sidebar.GroupLabel>{{ group.title }}</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="item in group.items" :key="item">
<Sidebar.MenuButton :href="`?page=${slug(item)}`" :active="selected === item" @click.prevent="selected = item; context.setMobileOpen(false)">{{ item }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
</Sidebar.Group>
<p v-if="!filtered.length" class="sidebar-grouped-demo__empty" role="status">No pages found.</p>
</template>
<template #toolbar>
<span class="sidebar-grouped-demo__divider" aria-hidden="true"></span>
<nav class="sidebar-grouped-demo__breadcrumb" aria-label="Breadcrumb"><span>{{ section }}</span><span aria-hidden="true">/</span><span aria-current="page">{{ selected }}</span></nav>
</template>
<section class="sidebar-grouped-demo__article" :aria-labelledby="headingId">
<span class="sidebar-grouped-demo__eyebrow">{{ section }} · v{{ version }}</span>
<h2 :id="headingId">{{ selected }}</h2>
<p>Keep project data, configuration, and results together in one workspace.</p>
<h3>Before you start</h3>
<p>Choose a workspace and check that you have permission to create projects. Use a clear name so your team can find the project later.</p>
<h3>Next steps</h3>
<ol><li>Create the project in your workspace.</li><li>Import geometry and check its dimensions.</li><li>Save your configuration before you run a simulation.</li></ol>
<p class="sidebar-grouped-demo__note">Example documentation. Search filters the navigation; page and version selections stay in this preview.</p>
</section>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-grouped-demo { inline-size: 100%; block-size: 38rem; min-inline-size: 0; text-align: start; }
.sidebar-grouped-demo[data-standalone] { block-size: 100svh; }
.sidebar-grouped-demo__version.kappa-sidebar__menu-button { min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-grouped-demo__version > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-grouped-demo__version > .kappa-sidebar__menu-label { position: relative; }
.sidebar-grouped-demo__mark { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; color: var(--kappa-accent-contrast); background: var(--kappa-accent-solid); }
.sidebar-grouped-demo__mark svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-grouped-demo__identity { display: flex; flex-direction: column; gap: 0.125rem; padding-inline-end: 1.5rem; }
.sidebar-grouped-demo__identity strong { font-size: 0.875rem; line-height: 1rem; font-weight: 550; }
.sidebar-grouped-demo__identity > span { font-size: 0.75rem; line-height: 0.875rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__search { position: relative; margin-block: 0.25rem 0.5rem; }
.sidebar-grouped-demo__search > svg { position: absolute; inset-inline-start: 0.5rem; inset-block-start: 50%; transform: translateY(-50%); inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); pointer-events: none; }
.sidebar-grouped-demo__search .kappa-input { inline-size: 100%; min-block-size: 2rem; padding-inline-start: 2rem; }
.sidebar-grouped-demo__empty { padding: 0.5rem; color: var(--kappa-subtle); font-size: 0.8125rem; }
.sidebar-grouped-demo__section-trigger.kappa-sidebar__menu-button { justify-content: space-between; min-block-size: 2rem; color: var(--kappa-subtle); font-size: 0.75rem; font-weight: 550; }
.sidebar-grouped-demo__branch + .sidebar-grouped-demo__branch { margin-block-start: 1rem; }
.sidebar-grouped-demo__parent.kappa-sidebar__menu-button { font-weight: 550; }
.sidebar-grouped-demo__dropdown-parent .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; }
.sidebar-grouped-demo__submenu-arrow { inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__submenu-popup { min-inline-size: 13rem; }
.sidebar-grouped-demo__submenu-popup [aria-current="page"] { background: var(--kappa-overlay); font-weight: 550; }
.sidebar-grouped-demo__submenu-popup[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-grouped-demo__parent[data-slot="sidebar-collapsible-trigger"] { justify-content: space-between; }
.sidebar-grouped-demo[data-sidebar-block="collapsible-submenus"] .sidebar-grouped-demo__branch + .sidebar-grouped-demo__branch { margin-block-start: 0.25rem; }
/* Match the floating navigation frame while keeping the content panel unframed. */
.sidebar-grouped-demo[data-sidebar-block="floating-submenus"] .kappa-sidebar-layout:not([data-mobile]) .kappa-sidebar-layout__main { border-block: 1px solid transparent; }
.sidebar-grouped-demo__divider { block-size: 1rem; border-inline-start: 1px solid var(--kappa-line); }
.sidebar-grouped-demo__breadcrumb { display: flex; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-grouped-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-grouped-demo__breadcrumb span:not(:last-child) { color: var(--kappa-subtle); }
.sidebar-grouped-demo__article { max-inline-size: 44rem; padding: 2rem; margin-inline: auto; color: var(--kappa-default); }
.sidebar-grouped-demo__eyebrow { font-size: 0.75rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__article h2 { margin: 0.5rem 0 1rem !important; font-size: 1.5rem !important; line-height: 1.2 !important; letter-spacing: -0.025em; }
.sidebar-grouped-demo__article h3 { margin: 1.5rem 0 0.5rem !important; font-size: 0.875rem !important; }
.sidebar-grouped-demo__article p, .sidebar-grouped-demo__article li { font-size: 0.8125rem; line-height: 1.65; }
.sidebar-grouped-demo__article ol { padding-inline-start: 1.25rem; }
.sidebar-grouped-demo__note { margin-block-start: 2rem !important; padding-block-start: 1rem; border-block-start: 1px solid var(--kappa-line); color: var(--kappa-subtle); font-size: 0.75rem !important; }
@media (max-width: 767px) {
.sidebar-grouped-demo__article { padding: 1.25rem; }
.sidebar-grouped-demo__breadcrumb span:not(:last-child) { display: none; }
.sidebar-grouped-demo__search .kappa-input { min-block-size: 2.75rem; }
}
</style>Collapsible Sections
Section headings expand and collapse their links independently. Search opens matching sections, and section state is retained when switching between desktop and mobile.
<script setup lang="ts">
import { computed, ref, useId, watch } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Input } from "@dicehub/kappa/components/input";
import { BookOpen, ChevronsUpDown, Search } from "@lucide/vue";
const props = withDefaults(defineProps<{ standalone?: boolean; collapsibleSections?: boolean }>(), { standalone: true, collapsibleSections: true });
const headingId = useId();
const version = ref("1.0.1");
const query = ref("");
const selected = ref("Create a project");
const groups = [
{ title: "Getting started", items: ["Introduction", "Installation", "Create a project"] },
{ title: "Project workflow", items: ["Import geometry", "Prepare a mesh", "Run a simulation", "Review results"] },
{ title: "Reference", items: ["Configuration", "Keyboard shortcuts", "Troubleshooting"] },
];
const filtered = computed(() => groups.map(group => ({ ...group, items: group.items.filter(item => item.toLowerCase().includes(query.value.trim().toLowerCase())) })).filter(group => group.items.length));
const openSections = ref<Record<string, boolean>>(Object.fromEntries(groups.map(group => [group.title, true])));
watch(query, value => {
if (props.collapsibleSections && value.trim()) {
for (const group of filtered.value) openSections.value[group.title] = true;
}
});
const section = computed(() => groups.find(group => group.items.includes(selected.value))!.title);
const slug = (name: string) => name.toLowerCase().replaceAll(" ", "-");
const positioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
</script>
<template>
<div class="sidebar-grouped-demo" :data-sidebar-block="props.collapsibleSections ? 'collapsible-sections' : 'grouped'" :data-standalone="props.standalone || undefined">
<SidebarLayout label="Documentation navigation" collapsible="offcanvas" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Documentation versions" :positioning="positioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton class="sidebar-grouped-demo__version" :aria-label="`Documentation version: ${version}`">
<template #icon><span class="sidebar-grouped-demo__mark"><BookOpen aria-hidden="true" /></span></template>
<span class="sidebar-grouped-demo__identity"><strong>Documentation</strong><span>v{{ version }}</span></span>
<ChevronsUpDown class="sidebar-grouped-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="version"><Dropdown.Label>Versions</Dropdown.Label>
<Dropdown.RadioItem v-for="item in ['1.0.1', '1.1.0-alpha', '2.0.0-beta']" :key="item" :value="item" close-on-select>v{{ item }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<div class="sidebar-grouped-demo__search"><Search aria-hidden="true" /><Input v-model="query" size="sm" aria-label="Search documentation" placeholder="Search the docs…" type="search" @keydown.esc.stop="query = ''" /></div>
<Sidebar.Group v-for="group in filtered" :key="group.title">
<Sidebar.Collapsible v-if="props.collapsibleSections" v-model:open="openSections[group.title]">
<Sidebar.CollapsibleTrigger class="sidebar-grouped-demo__section-trigger">{{ group.title }}<Sidebar.MenuChevron /></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.Menu><Sidebar.MenuItem v-for="item in group.items" :key="item">
<Sidebar.MenuButton :href="`?page=${slug(item)}`" :active="selected === item" @click.prevent="selected = item; context.setMobileOpen(false)">{{ item }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
<template v-else>
<Sidebar.GroupLabel>{{ group.title }}</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="item in group.items" :key="item">
<Sidebar.MenuButton :href="`?page=${slug(item)}`" :active="selected === item" @click.prevent="selected = item; context.setMobileOpen(false)">{{ item }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
</Sidebar.Group>
<p v-if="!filtered.length" class="sidebar-grouped-demo__empty" role="status">No pages found.</p>
</template>
<template #toolbar>
<span class="sidebar-grouped-demo__divider" aria-hidden="true"></span>
<nav class="sidebar-grouped-demo__breadcrumb" aria-label="Breadcrumb"><span>{{ section }}</span><span aria-hidden="true">/</span><span aria-current="page">{{ selected }}</span></nav>
</template>
<section class="sidebar-grouped-demo__article" :aria-labelledby="headingId">
<span class="sidebar-grouped-demo__eyebrow">{{ section }} · v{{ version }}</span>
<h2 :id="headingId">{{ selected }}</h2>
<p>Keep project data, configuration, and results together in one workspace.</p>
<h3>Before you start</h3>
<p>Choose a workspace and check that you have permission to create projects. Use a clear name so your team can find the project later.</p>
<h3>Next steps</h3>
<ol><li>Create the project in your workspace.</li><li>Import geometry and check its dimensions.</li><li>Save your configuration before you run a simulation.</li></ol>
<p class="sidebar-grouped-demo__note">Example documentation. Search filters the navigation; page and version selections stay in this preview.</p>
</section>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-grouped-demo { inline-size: 100%; block-size: 38rem; min-inline-size: 0; text-align: start; }
.sidebar-grouped-demo[data-standalone] { block-size: 100svh; }
.sidebar-grouped-demo__version.kappa-sidebar__menu-button { min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-grouped-demo__version > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-grouped-demo__version > .kappa-sidebar__menu-label { position: relative; }
.sidebar-grouped-demo__mark { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; color: var(--kappa-accent-contrast); background: var(--kappa-accent-solid); }
.sidebar-grouped-demo__mark svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-grouped-demo__identity { display: flex; flex-direction: column; gap: 0.125rem; padding-inline-end: 1.5rem; }
.sidebar-grouped-demo__identity strong { font-size: 0.875rem; line-height: 1rem; font-weight: 550; }
.sidebar-grouped-demo__identity > span { font-size: 0.75rem; line-height: 0.875rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__search { position: relative; margin-block: 0.25rem 0.5rem; }
.sidebar-grouped-demo__search > svg { position: absolute; inset-inline-start: 0.5rem; inset-block-start: 50%; transform: translateY(-50%); inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); pointer-events: none; }
.sidebar-grouped-demo__search .kappa-input { inline-size: 100%; min-block-size: 2rem; padding-inline-start: 2rem; }
.sidebar-grouped-demo__empty { padding: 0.5rem; color: var(--kappa-subtle); font-size: 0.8125rem; }
.sidebar-grouped-demo__section-trigger.kappa-sidebar__menu-button { justify-content: space-between; min-block-size: 2rem; color: var(--kappa-subtle); font-size: 0.75rem; font-weight: 550; }
.sidebar-grouped-demo__branch + .sidebar-grouped-demo__branch { margin-block-start: 1rem; }
.sidebar-grouped-demo__parent.kappa-sidebar__menu-button { font-weight: 550; }
.sidebar-grouped-demo__dropdown-parent .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; }
.sidebar-grouped-demo__submenu-arrow { inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__submenu-popup { min-inline-size: 13rem; }
.sidebar-grouped-demo__submenu-popup [aria-current="page"] { background: var(--kappa-overlay); font-weight: 550; }
.sidebar-grouped-demo__submenu-popup[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-grouped-demo__parent[data-slot="sidebar-collapsible-trigger"] { justify-content: space-between; }
.sidebar-grouped-demo[data-sidebar-block="collapsible-submenus"] .sidebar-grouped-demo__branch + .sidebar-grouped-demo__branch { margin-block-start: 0.25rem; }
/* Match the floating navigation frame while keeping the content panel unframed. */
.sidebar-grouped-demo[data-sidebar-block="floating-submenus"] .kappa-sidebar-layout:not([data-mobile]) .kappa-sidebar-layout__main { border-block: 1px solid transparent; }
.sidebar-grouped-demo__divider { block-size: 1rem; border-inline-start: 1px solid var(--kappa-line); }
.sidebar-grouped-demo__breadcrumb { display: flex; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-grouped-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-grouped-demo__breadcrumb span:not(:last-child) { color: var(--kappa-subtle); }
.sidebar-grouped-demo__article { max-inline-size: 44rem; padding: 2rem; margin-inline: auto; color: var(--kappa-default); }
.sidebar-grouped-demo__eyebrow { font-size: 0.75rem; color: var(--kappa-subtle); }
.sidebar-grouped-demo__article h2 { margin: 0.5rem 0 1rem !important; font-size: 1.5rem !important; line-height: 1.2 !important; letter-spacing: -0.025em; }
.sidebar-grouped-demo__article h3 { margin: 1.5rem 0 0.5rem !important; font-size: 0.875rem !important; }
.sidebar-grouped-demo__article p, .sidebar-grouped-demo__article li { font-size: 0.8125rem; line-height: 1.65; }
.sidebar-grouped-demo__article ol { padding-inline-start: 1.25rem; }
.sidebar-grouped-demo__note { margin-block-start: 2rem !important; padding-block-start: 1rem; border-block-start: 1px solid var(--kappa-line); color: var(--kappa-subtle); font-size: 0.75rem !important; }
@media (max-width: 767px) {
.sidebar-grouped-demo__article { padding: 1.25rem; }
.sidebar-grouped-demo__breadcrumb span:not(:last-child) { display: none; }
.sidebar-grouped-demo__search .kappa-input { min-block-size: 2.75rem; }
}
</style>Submenus
Clickable parent pages with always-visible, indented child links. A simple documentation header and off-canvas collapse keep the hierarchy clear.
Floating Submenus
A wider, floating navigation panel with a border, rounded corners, and visible child links. Desktop collapse hides the panel; mobile uses a full-screen drawer.
Collapsible Submenus
Click a parent row to show or hide its indented links. The selected page's group starts open. Search reveals matching links, and open groups are retained on mobile.
Dropdown Submenus
Click a parent row to open its page links in a popup. Search filters the links. Mobile selection closes the full-screen navigation and keeps the selected page.
Collapsible Icon Navigation
Starts expanded with nested link groups, project shortcuts, namespace and profile menus, and Quick search. Collapse to an icon rail; click a group icon to expand its links. Group state and selection are retained on mobile.
<script setup lang="ts">
import { computed, ref, useId } from "vue";
import { SidebarLayout, type SidebarLayoutVariant } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { Button } from "@dicehub/kappa/components/button";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Avatar } from "@dicehub/kappa/components/avatar";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Table } from "@dicehub/kappa/components/table";
import { Building2, ChevronsUpDown, Folder, LayoutDashboard, FileText, Settings, Search, Plus, Box, CircleHelp, MessageSquare, BadgeCheck, Bell, CreditCard, LogOut, Sparkles, UsersRound } from "@lucide/vue";
const props = withDefaults(defineProps<{ variant?: SidebarLayoutVariant; standalone?: boolean; iconNavigation?: boolean; insetNavigation?: boolean }>(), { variant: "workspace", standalone: true, iconNavigation: true, insetNavigation: false });
const richNavigation = computed(() => props.iconNavigation || props.insetNavigation);
const headingId = useId();
const namespace = ref("Engineering");
const profile = ref("ros");
const selected = ref("Overview");
const secondaryLinks = [{ label: "Support", icon: CircleHelp }, { label: "Feedback", icon: MessageSquare }];
const accountPages: Record<string, string> = {
'Upgrade to Pro': 'Review the plans available for your workspace.',
Account: 'Manage your profile and account details.',
Billing: 'Review your subscription, invoices, and payment details.',
Notifications: 'Choose which workspace updates you receive.',
Preferences: 'Set your display and workspace preferences.',
'Log out': 'This preview does not end a real session.',
};
const accountPage = computed(() => accountPages[selected.value]);
const utilityPage = computed(() => Boolean(accountPage.value) || (props.insetNavigation && secondaryLinks.some(link => link.label === selected.value)));
const searchOpen = ref(false);
const query = ref("");
const navigating = ref(false);
const searchFromDrawer = ref(false);
const sectionsOpen = ref(true);
const profiles = [
{ value: "ros", name: "Ros.Space", email: "[email protected]", initials: "RS", avatar: "/avatars/ros-space-astronaut.webp" },
{ value: "jordan", name: "Jordan Lee", email: "[email protected]", initials: "JL", avatar: "/avatars/mei-chen.webp" },
];
const account = computed(() => profiles.find(item => item.value === profile.value)!);
const links = [{ label: "Overview", icon: LayoutDashboard }, { label: "Projects", icon: Folder }, { label: "Reports", icon: FileText }];
const navigationGroups = [
{ label: "Workspace", icon: LayoutDashboard, items: ["Overview", "Reports"] },
{ label: "Mesh", icon: Box, items: ["Geometry", "Refinement", "Simulation"] },
{ label: "Settings", icon: Settings, items: ["General", "Members", "Integrations"] },
];
const openGroups = ref<Record<string, boolean>>({ Workspace: true, Mesh: false, Settings: false });
const searchItems = computed(() => richNavigation.value
? [...navigationGroups.flatMap(group => group.items), ...projects.value.map(project => project.name), ...(props.insetNavigation ? secondaryLinks.map(link => link.label) : [])]
: ["Overview", "Projects", "Reports", "Geometry", "Refinement", "Settings"]);
const projects = ref([
{ name: "Rotor study", code: "MESH-042", state: "Ready" },
{ name: "Channel flow", code: "MESH-041", state: "In review" },
{ name: "Heat exchanger", code: "MESH-039", state: "Draft" },
]);
const namespacePositioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
const desktopProfilePositioning = { placement: "right-end", strategy: "fixed", gutter: 8 } as const;
const mobileProfilePositioning = { placement: "top-start", strategy: "fixed", gutter: 8 } as const;
function navigate(value: string, context: SidebarContextValue) {
selected.value = value;
if (richNavigation.value) {
const group = navigationGroups.find(group => group.items.includes(value));
if (group) openGroups.value[group.label] = true;
}
context.setMobileOpen(false);
}
function search(context: SidebarContextValue) { query.value = ""; navigating.value = false; searchFromDrawer.value = context.isMobile && context.mobileOpen; searchOpen.value = true; }
function createProject() {
const name = `Untitled project ${projects.value.length - 2}`;
projects.value.push({ name, code: `MESH-${String(43 + projects.value.length - 3).padStart(3, "0")}`, state: "Draft" });
selected.value = name;
}
</script>
<template>
<div class="sidebar-block-demo" :data-sidebar-block="props.insetNavigation ? 'inset-navigation' : props.iconNavigation ? 'collapsible-icons' : props.variant" :data-standalone="props.standalone || undefined">
<SidebarLayout :variant="props.variant" :label="props.insetNavigation ? 'Inset application navigation' : props.iconNavigation ? 'Icon application navigation' : `${props.variant} application navigation`" :resizable="!richNavigation && props.variant === 'workspace'" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Namespaces" :positioning="richNavigation && !context.isMobile ? { placement: 'right-start', strategy: 'fixed', gutter: 8 } : namespacePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Namespace: ${namespace}`" :tooltip="`Namespace: ${namespace}`" class="sidebar-block-demo__identity-button sidebar-block-demo__namespace">
<template #icon><span class="sidebar-block-demo__namespace-icon"><Building2 aria-hidden="true" /></span></template>
<span class="sidebar-block-demo__identity"><strong>{{ namespace }}</strong><small>Namespace</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace">
<Dropdown.Label>Switch namespace</Dropdown.Label>
<Dropdown.RadioItem v-for="name in ['Engineering', 'Research', 'Personal']" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Menu class="sidebar-block-demo__search">
<Sidebar.MenuItem><Sidebar.MenuButton :icon="Search" tooltip="Quick search" class="sidebar-block-demo__search-button" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search(context)">Quick search …</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
<template v-if="richNavigation">
<Sidebar.Group>
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="group in navigationGroups" :key="group.label">
<Sidebar.Collapsible v-model:open="openGroups[group.label]">
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton :icon="group.icon" :tooltip="group.label" :active="group.items.includes(selected)" :aria-current="undefined" class="sidebar-block-demo__group-trigger">{{ group.label }}<Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub :aria-label="group.label">
<Sidebar.MenuSubItem v-for="item in group.items" :key="item"><Sidebar.MenuButton :active="selected === item" :href="`?view=${item.toLowerCase()}`" @click.prevent="navigate(item, context)">{{ item }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="project in projects" :key="project.code">
<Sidebar.MenuButton :icon="Folder" :tooltip="project.name" :active="selected === project.name" :href="`?project=${project.code}`" @click.prevent="navigate(project.name, context)">{{ project.name }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
<template v-else>
<Sidebar.Group>
<Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="link in links" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.Collapsible v-model:open="sectionsOpen">
<Sidebar.CollapsibleTrigger as-child><Sidebar.MenuButton :icon="Box" tooltip="Mesh">Mesh <Sidebar.MenuChevron /></Sidebar.MenuButton></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub>
<Sidebar.MenuSubItem v-for="name in ['Geometry', 'Refinement']" :key="name"><Sidebar.MenuButton :active="selected === name" :href="`?view=${name.toLowerCase()}`" @click.prevent="navigate(name, context)">{{ name }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Manage</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="Settings" tooltip="Settings" :active="selected === 'Settings'" href="?view=settings" @click.prevent="navigate('Settings', context)">Settings</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
</template>
<template #footer="context">
<nav v-if="props.insetNavigation" aria-label="Help and feedback" class="sidebar-block-demo__secondary">
<Sidebar.Menu><Sidebar.MenuItem v-for="link in secondaryLinks" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</nav>
<Dropdown.Root aria-label="Profile" :positioning="context.isMobile ? mobileProfilePositioning : desktopProfilePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Profile: ${account.name}`" :tooltip="account.name" class="sidebar-block-demo__identity-button sidebar-block-demo__profile">
<template #icon><Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root></template>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<div class="sidebar-block-demo__profile-summary">
<Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
</div>
<Dropdown.Separator />
<Dropdown.Group><Dropdown.Item value="upgrade" :icon="Sparkles" @select="navigate('Upgrade to Pro', context)">Upgrade to Pro</Dropdown.Item></Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Group>
<Dropdown.Item value="account" :icon="BadgeCheck" @select="navigate('Account', context)">Account</Dropdown.Item>
<Dropdown.Item value="billing" :icon="CreditCard" @select="navigate('Billing', context)">Billing</Dropdown.Item>
<Dropdown.Item value="notifications" :icon="Bell" @select="navigate('Notifications', context)">Notifications</Dropdown.Item>
<Dropdown.Item value="preferences" :icon="Settings" @select="navigate('Preferences', context)">Preferences</Dropdown.Item>
</Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Sub aria-label="Switch profile" :positioning="{ placement: 'right-end', strategy: 'fixed', gutter: 4, fitViewport: true, overflowPadding: 8 }">
<Dropdown.SubTrigger :icon="UsersRound">Switch profile</Dropdown.SubTrigger>
<Dropdown.Context v-slot="submenu"><Dropdown.SubContent :teleport="!context.isMobile" :inert="!submenu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<Dropdown.RadioGroup v-model="profile"><Dropdown.Label>Profiles</Dropdown.Label><Dropdown.RadioItem v-for="item in profiles" :key="item.value" :value="item.value" close-on-select>{{ item.name }}</Dropdown.RadioItem></Dropdown.RadioGroup>
</Dropdown.SubContent></Dropdown.Context>
</Dropdown.Sub>
<Dropdown.Separator />
<Dropdown.Item value="logout" :icon="LogOut" @select="navigate('Log out', context)">Log out</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #toolbar>
<span class="sidebar-block-demo__breadcrumb"><span>{{ namespace }}</span><span aria-hidden="true">/</span><span>{{ selected }}</span></span>
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :restore-focus="!navigating || !searchFromDrawer" aria-label="Search navigation" @select="item => { navigating = true; searchOpen = false; navigate(String(item), context); }">
<CommandPalette.Input placeholder="Search navigation…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<section class="sidebar-block-demo__page" :aria-labelledby="headingId">
<div class="sidebar-block-demo__page-heading">
<div><span class="sidebar-block-demo__eyebrow">{{ namespace }} workspace</span><h2 :id="headingId">{{ selected }}</h2></div>
<Button v-if="!utilityPage" size="sm" variant="secondary" @click="createProject"><Plus aria-hidden="true" />New project</Button>
</div>
<p v-if="utilityPage" class="sidebar-block-demo__description">{{ accountPage || (selected === 'Support' ? 'Find help with your workspace, geometry, and simulation setup.' : 'Share a suggestion or report an issue with your workspace.') }}</p>
<p v-else class="sidebar-block-demo__description">Review geometry, prepare meshes, and track project changes.</p>
<template v-if="!utilityPage">
<div class="sidebar-block-demo__section-heading"><h3>Recent projects</h3><span>{{ projects.length }} projects</span></div>
<Table compact>
<Table.Caption class="sidebar-block-demo__sr-only">Recent projects in {{ namespace }}</Table.Caption>
<Table.Header><Table.Row><Table.Head>Project</Table.Head><Table.Head>Status</Table.Head></Table.Row></Table.Header>
<Table.Body><Table.Row v-for="item in projects" :key="item.code">
<Table.Cell><button type="button" class="sidebar-block-demo__project-link" @click="selected = item.name">{{ item.name }}</button><span class="sidebar-block-demo__project-code">{{ item.code }}</span></Table.Cell>
<Table.Cell><span class="sidebar-block-demo__status" :data-ready="item.state === 'Ready' || undefined">{{ item.state }}</span></Table.Cell>
</Table.Row></Table.Body>
</Table>
</template>
<p v-if="accountPage" class="sidebar-block-demo__note" role="status">Local preview only. No account, payment, notification, or session is changed.</p>
<p v-else-if="utilityPage" class="sidebar-block-demo__note">This is a local navigation preview. Connect this page to your application's {{ selected === 'Support' ? 'help resources' : 'feedback form' }}. No message is sent.</p>
<p v-else class="sidebar-block-demo__note" role="status">{{ projects.length > 3 ? 'New projects added to this example.' : 'Example data. Changes stay in this preview.' }}</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-block-demo { inline-size: 100%; block-size: 33rem; min-inline-size: 0; text-align: start; }
.sidebar-block-demo[data-sidebar-block="collapsible-icons"] { block-size: 38rem; }
.sidebar-block-demo[data-sidebar-block="inset-navigation"] { block-size: 42rem; }
.sidebar-block-demo[data-standalone] { block-size: 100svh; }
.sidebar-block-demo .kappa-sidebar-layout { border: 1px solid var(--kappa-line); border-radius: 0.625rem; overflow: hidden; }
.sidebar-block-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.sidebar-block-demo__identity-button { padding-block: 0.25rem; }
.sidebar-block-demo__identity-button .kappa-sidebar__menu-label { position: relative; padding-inline-end: 1.25rem; }
.sidebar-block-demo__identity { display: flex; flex-direction: column; min-inline-size: 0; gap: 0.125rem; }
.sidebar-block-demo__identity strong { font-size: 0.8125rem; line-height: 1.125rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__identity small { font-size: 0.6875rem; line-height: 1rem; color: var(--kappa-subtle); overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 0.875rem; block-size: 0.875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__namespace-icon { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; background: var(--kappa-accent-solid); color: var(--kappa-accent-contrast); }
.sidebar-block-demo__namespace-icon svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace .kappa-sidebar__menu-label { padding-inline-end: 1.5rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace[data-state="open"] { background: var(--kappa-tint); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button:focus-visible { outline-offset: 2px; }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__namespace { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__avatar.kappa-avatar { --kappa-avatar-size: 2rem; border-radius: 0.5rem; overflow: hidden; }
.sidebar-block-demo__profile > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__profile.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity { gap: 0; }
.sidebar-block-demo__profile .sidebar-block-demo__identity strong, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1.125rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity small, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__profile .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__profile[data-state="open"] { background: var(--kappa-overlay); }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__profile { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__profile-menu.kappa-dropdown__content { inline-size: max(14rem, var(--reference-width, 14rem)); border-radius: 0.5rem; }
.sidebar-block-demo__profile-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.25rem; }
.sidebar-block-demo__profile-summary .sidebar-block-demo__identity { flex: 1; }
.sidebar-block-demo__profile-menu[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-block-demo__secondary { margin-block-end: 0.5rem; }
.kappa-sidebar__footer:has(> .sidebar-block-demo__secondary) { flex-direction: column; align-items: stretch; }
.sidebar-block-demo__search { margin-block-end: 0.5rem; }
.sidebar-block-demo__group-trigger .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button { min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.375rem; background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-default); }
.sidebar-block-demo__search-button > .kappa-sidebar__menu-icon { color: var(--kappa-subtle); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:hover { border-color: var(--kappa-subtle); background: var(--kappa-tint); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:focus-visible { border-color: var(--kappa-focus); outline-offset: 2px; }
.sidebar-block-demo__breadcrumb { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-block-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-block-demo__breadcrumb span:first-child { color: var(--kappa-subtle); }
.sidebar-block-demo__page { padding: 1.5rem; max-inline-size: 62rem; margin-inline: auto; }
.sidebar-block-demo__page-heading { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 1rem; }
.sidebar-block-demo__page-heading h2 { margin: 0.375rem 0 0 !important; color: var(--kappa-default); font-size: 1.25rem !important; line-height: 1.5rem !important; letter-spacing: -0.025em; font-weight: 550; }
.sidebar-block-demo__page-heading button svg { inline-size: 0.875rem; block-size: 0.875rem; }
.sidebar-block-demo__eyebrow { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__description { margin: 0.75rem 0 1.5rem !important; font-size: 0.8125rem !important; line-height: 1.4; color: var(--kappa-subtle); }
.sidebar-block-demo__section-heading { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-block-end: 0.625rem; }
.sidebar-block-demo__section-heading h3 { margin: 0 !important; font-size: 0.8125rem !important; line-height: 1.25rem !important; font-weight: 550; }
.sidebar-block-demo__section-heading > span { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__project-link { display: block; margin: 0; padding: 0; border: 0; background: transparent; color: var(--kappa-default); cursor: pointer; font: inherit; text-align: start; }
.sidebar-block-demo__project-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.sidebar-block-demo__project-link:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 3px; }
.sidebar-block-demo__project-code { display: block; font-size: 0.625rem; line-height: 1rem; color: var(--kappa-subtle); }
.sidebar-block-demo__status { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem; white-space: nowrap; }
.sidebar-block-demo__status::before { content: ""; inline-size: 0.3125rem; block-size: 0.3125rem; border-radius: 50%; background: var(--kappa-subtle); }
.sidebar-block-demo__status[data-ready]::before { background: var(--kappa-success-text); }
.sidebar-block-demo__note { margin: 1rem 0 0 !important; font-size: 0.6875rem !important; color: var(--kappa-subtle); }
.sidebar-block-demo__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 767px) {
.sidebar-block-demo:not([data-standalone]) { block-size: 30rem; }
.sidebar-block-demo__page { padding: 1rem; }
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :first-child,
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :nth-child(2) { display: none; }
}
</style>Inset Navigation
An inset content panel beside nested navigation and project links. Support and Feedback stay above the profile menu while the main links scroll. Includes namespace selection, Quick search, icon collapse, and full-screen mobile navigation.
Inbox Navigation
A fixed folder icon rail beside a searchable message list and reading panel. Filter unread messages, collapse the list, and open messages from a full-screen mobile drawer.
Workspace Pages
A page-based workspace with Favorites, expandable groups, namespace selection, Quick search, and utility links. Desktop collapse hides the sidebar; mobile uses a full-screen drawer. The original Workspace example remains available below.
<script setup lang="ts">
import { computed, nextTick, ref, useId } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Button } from "@dicehub/kappa/components/button";
import { Building2, ChevronsUpDown, Search, House, CalendarDays, Settings, Trash2, CircleHelp, Star } from "@lucide/vue";
const props = withDefaults(defineProps<{ standalone?: boolean }>(), { standalone: true });
const headingId = useId();
type Page = { title: string; emoji: string; summary: string; notes: string[] };
type Group = { name: string; emoji: string; pages: Page[] };
const workspaces: Record<string, Group[]> = {
Engineering: [
{ name: "Projects", emoji: "📐", pages: [
{ title: "Project brief", emoji: "🧭", summary: "A shared starting point for the rotor study. Keep the scope, decisions, and review notes together.", notes: ["Compare the baseline geometry with the revised inlet.", "Use the same boundary conditions for both runs.", "Record mesh quality before the design review."] },
{ title: "Mesh notes", emoji: "🔬", summary: "Mesh preparation and quality checks for the current study.", notes: ["Check the surface normals after import.", "Refine the inlet and the blade tips.", "Keep the baseline mesh for comparison."] },
{ title: "Review checklist", emoji: "✅", summary: "A short checklist before sharing a simulation result.", notes: ["Check units and boundary conditions.", "Attach the convergence plot.", "Link the source geometry and mesh."] },
] },
{ name: "Team", emoji: "👋", pages: [
{ title: "Getting started", emoji: "🌱", summary: "The essentials for working in this namespace.", notes: ["Read the project brief.", "Find the current mesh notes.", "Add frequently used pages to Favorites."] },
{ title: "Meeting notes", emoji: "📝", summary: "Decisions from the latest engineering review.", notes: ["Keep the baseline for the next comparison.", "Review the inlet refinement with the team.", "Update the project brief after approval."] },
{ title: "Team directory", emoji: "👥", summary: "People who maintain the sample workspace.", notes: ["Ros.Space — project coordination.", "Jordan Lee — geometry and mesh review.", "Mei Chen — simulation results."] },
] },
{ name: "Resources", emoji: "📚", pages: [
{ title: "Guidelines", emoji: "📖", summary: "Shared conventions for clear, useful project pages.", notes: ["Give each page one purpose.", "Record decisions with their reasons.", "Link related pages instead of copying their content."] },
{ title: "Release notes", emoji: "🚀", summary: "Recent changes to the sample workflow.", notes: ["The review checklist now includes source links.", "Mesh notes are grouped with the project brief."] },
] },
],
Research: [
{ name: "Studies", emoji: "🔬", pages: [
{ title: "Study plan", emoji: "🧪", summary: "Plan the next comparison before running experiments.", notes: ["Define the hypothesis.", "Choose the comparison cases.", "Record the acceptance criteria."] },
{ title: "Experiment log", emoji: "📝", summary: "A record of the inputs and results for each experiment.", notes: ["Keep one entry per run.", "Include unsuccessful runs and their causes."] },
] },
{ name: "Library", emoji: "📚", pages: [
{ title: "Reading list", emoji: "📖", summary: "Background reading for the current research.", notes: ["Review the internal benchmark notes.", "Summarize useful methods in the study plan."] },
] },
],
Personal: [
{ name: "Notes", emoji: "📝", pages: [
{ title: "Weekly plan", emoji: "🗓️", summary: "A small set of priorities for the week.", notes: ["Review the project brief.", "Prepare questions for the next team meeting."] },
{ title: "Ideas", emoji: "💡", summary: "A place for ideas that need more work.", notes: ["Make review notes easier to find.", "Document one useful shortcut each week."] },
] },
],
};
const namespace = ref("Engineering");
const selections = ref<Record<string, string>>({ Engineering: "Project brief", Research: "Study plan", Personal: "Weekly plan" });
const selected = computed({ get: () => selections.value[namespace.value], set: value => { selections.value[namespace.value] = value; } });
const groups = computed(() => workspaces[namespace.value]);
const pages = computed(() => groups.value.flatMap(group => group.pages));
const currentPage = computed(() => pages.value.find(page => page.title === selected.value));
const currentGroup = computed(() => groups.value.find(group => group.pages.some(page => page.title === selected.value)));
const favorites = ref<Record<string, string[]>>({ Engineering: ["Project brief", "Getting started", "Meeting notes"], Research: ["Study plan"], Personal: ["Weekly plan"] });
const favoritePages = computed(() => favorites.value[namespace.value].map(title => pages.value.find(page => page.title === title)!).filter(Boolean));
const isFavorite = computed(() => favorites.value[namespace.value].includes(selected.value));
const openGroups = ref<Record<string, boolean>>({ "Engineering/Projects": true, "Research/Studies": true, "Personal/Notes": true });
const utilities = [
{ title: "Calendar", icon: CalendarDays, description: "Upcoming reviews in this sample namespace.", notes: ["Tuesday, 10:00 — Project review", "Thursday, 14:00 — Team notes"] },
{ title: "Settings", icon: Settings, description: "Namespace settings preview. No account settings are changed.", notes: ["Access: workspace members", "Default page: workspace home"] },
{ title: "Trash", icon: Trash2, description: "Trash is empty.", notes: ["Deleted pages would appear here. This example does not delete pages."] },
{ title: "Help", icon: CircleHelp, description: "Find your way around the workspace.", notes: ["Use Quick search to find a page by name.", "Expand a group to see its pages.", "Use the star beside a page title to add or remove a Favorite."] },
];
const utility = computed(() => utilities.find(item => item.title === selected.value));
const searchOpen = ref(false);
const query = ref("");
const navigating = ref(false);
const searchFromDrawer = ref(false);
const searchItems = computed(() => ["Home", ...pages.value.map(page => page.title), ...utilities.map(item => item.title)]);
function navigate(title: string, context: SidebarContextValue) {
selected.value = title;
const group = groups.value.find(group => group.pages.some(page => page.title === title));
if (group) openGroups.value[`${namespace.value}/${group.name}`] = true;
context.setMobileOpen(false);
}
function search(context: SidebarContextValue) {
query.value = "";
navigating.value = false;
searchFromDrawer.value = context.isMobile && context.mobileOpen;
searchOpen.value = true;
}
async function selectSearchPage(item: unknown, context: SidebarContextValue) {
navigating.value = true;
searchOpen.value = false;
// Release the nested search layer before closing its navigation drawer.
await nextTick();
navigate(String(item), context);
}
function toggleFavorite() {
if (!currentPage.value) return;
const titles = favorites.value[namespace.value];
favorites.value[namespace.value] = isFavorite.value ? titles.filter(title => title !== selected.value) : [...titles, selected.value];
}
</script>
<template>
<div class="sidebar-pages-demo" data-sidebar-block="workspace-pages" :data-standalone="props.standalone || undefined">
<SidebarLayout label="Workspace pages navigation" navigation-label="Workspace page links" collapsible="offcanvas" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root :positioning="{ placement: context.isMobile ? 'bottom-start' : 'right-start', strategy: 'fixed', gutter: 8 }">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Namespace: ${namespace}`" class="sidebar-pages-demo__namespace">
<template #icon><span class="sidebar-pages-demo__namespace-icon"><Building2 aria-hidden="true" /></span></template>
<span class="sidebar-pages-demo__identity"><strong>{{ namespace }}</strong><small>Workspace pages</small></span>
<ChevronsUpDown class="sidebar-pages-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace"><Dropdown.Label>Switch namespace</Dropdown.Label>
<Dropdown.RadioItem v-for="name in Object.keys(workspaces)" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Menu aria-label="Workspace shortcuts">
<Sidebar.MenuItem><Sidebar.MenuButton :icon="Search" class="sidebar-pages-demo__search" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search(context)">Quick search …</Sidebar.MenuButton></Sidebar.MenuItem>
<Sidebar.MenuItem><Sidebar.MenuButton :icon="House" :active="selected === 'Home'" href="?page=Home" @click.prevent="navigate('Home', context)">Home</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
<Sidebar.Group>
<Sidebar.GroupLabel>Favorites</Sidebar.GroupLabel>
<Sidebar.Menu aria-label="Favorites">
<Sidebar.MenuItem v-for="page in favoritePages" :key="page.title">
<Sidebar.MenuButton :active="selected === page.title" :href="`?page=${encodeURIComponent(page.title)}`" @click.prevent="navigate(page.title, context)"><template #icon>{{ page.emoji }}</template>{{ page.title }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
<p v-if="!favoritePages.length" class="sidebar-pages-demo__empty">No Favorites yet. Open a page and select its star.</p>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
<Sidebar.Menu aria-label="Page groups">
<Sidebar.MenuItem v-for="group in groups" :key="`${namespace}/${group.name}`">
<Sidebar.Collapsible v-model:open="openGroups[`${namespace}/${group.name}`]">
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton class="sidebar-pages-demo__group"><template #icon>{{ group.emoji }}</template>{{ group.name }}<Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub :aria-label="`${group.name} pages`">
<Sidebar.MenuSubItem v-for="page in group.pages" :key="page.title"><Sidebar.MenuButton :active="selected === page.title" :href="`?page=${encodeURIComponent(page.title)}`" @click.prevent="navigate(page.title, context)"><template #icon>{{ page.emoji }}</template>{{ page.title }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
</template>
<template #footer="context">
<Sidebar.Menu aria-label="Utility links" class="sidebar-pages-demo__utilities">
<Sidebar.MenuItem v-for="item in utilities" :key="item.title"><Sidebar.MenuButton :icon="item.icon" :active="selected === item.title" :href="`?page=${item.title}`" @click.prevent="navigate(item.title, context)">{{ item.title }}</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<template #toolbar>
<span class="sidebar-pages-demo__breadcrumb"><span>{{ namespace }}</span><span aria-hidden="true">/</span><span>{{ selected }}</span></span>
<Button v-if="currentPage" size="sm" variant="ghost" shape="square" :icon="Star" class="sidebar-pages-demo__favorite" :aria-label="`${isFavorite ? 'Remove' : 'Add'} ${selected} ${isFavorite ? 'from' : 'to'} Favorites`" :aria-pressed="isFavorite" @click="toggleFavorite" />
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :inert="!searchOpen || undefined" :restore-focus="!navigating || !searchFromDrawer" aria-label="Search workspace pages" @select="item => selectSearchPage(item, context)">
<CommandPalette.Input placeholder="Search workspace pages…" />
<CommandPalette.List><CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results><CommandPalette.Empty>No pages found.</CommandPalette.Empty></CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<section class="sidebar-pages-demo__page" :aria-labelledby="headingId">
<span class="sidebar-pages-demo__page-icon" aria-hidden="true">{{ currentPage?.emoji ?? (selected === 'Home' ? '🏡' : '📋') }}</span>
<p class="sidebar-pages-demo__eyebrow">{{ namespace }}<template v-if="currentGroup"> / {{ currentGroup.name }}</template></p>
<h2 :id="headingId">{{ selected }}</h2>
<p class="sidebar-pages-demo__summary">{{ currentPage?.summary ?? utility?.description ?? 'Your shared pages, project notes, and team resources in one place.' }}</p>
<template v-if="currentPage">
<div class="sidebar-pages-demo__byline"><span>Ros.Space</span><span>Sample document</span></div>
<h3>At a glance</h3>
<ul class="sidebar-pages-demo__notes"><li v-for="note in currentPage.notes" :key="note">{{ note }}</li></ul>
<aside class="sidebar-pages-demo__callout"><Star aria-hidden="true" /><p>Add pages to Favorites with the star in the toolbar. They stay within the selected namespace.</p></aside>
</template>
<ul v-else-if="utility" class="sidebar-pages-demo__notes"><li v-for="note in utility.notes" :key="note">{{ note }}</li></ul>
<template v-else>
<h3>All pages <span class="sidebar-pages-demo__count">{{ pages.length }}</span></h3>
<ul class="sidebar-pages-demo__index"><li v-for="page in pages" :key="page.title"><button type="button" @click="navigate(page.title, context)"><span aria-hidden="true">{{ page.emoji }}</span><span>{{ page.title }}</span></button></li></ul>
</template>
<p class="sidebar-pages-demo__note">Local example data. Page selection, Favorites, and open groups are kept until you reload.</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-pages-demo { inline-size: 100%; block-size: 44rem; min-inline-size: 0; text-align: start; }
.sidebar-pages-demo[data-standalone] { block-size: 100svh; }
.sidebar-pages-demo .kappa-sidebar-layout { overflow: hidden; border: 1px solid var(--kappa-line); border-radius: 0.625rem; }
.sidebar-pages-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.sidebar-pages-demo__namespace.kappa-sidebar__menu-button { block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-pages-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-pages-demo__namespace-icon { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; background: var(--kappa-accent-solid); color: var(--kappa-accent-contrast); }
.sidebar-pages-demo__namespace-icon svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-pages-demo__namespace .kappa-sidebar__menu-label { position: relative; padding-inline-end: 1.5rem; }
.sidebar-pages-demo__identity { display: flex; flex-direction: column; gap: 0.125rem; }
.sidebar-pages-demo__identity strong { overflow: hidden; font-size: 0.875rem; line-height: 1rem; font-weight: 550; text-overflow: ellipsis; }
.sidebar-pages-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; color: var(--kappa-subtle); }
.sidebar-pages-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 1rem; block-size: 1rem; color: var(--kappa-subtle); }
.sidebar-pages-demo__namespace[data-state="open"] { background: var(--kappa-tint); }
.sidebar-pages-demo__search.kappa-sidebar__menu-button { margin-block-end: 0.375rem; min-block-size: 2rem; border: 1px solid var(--kappa-line); background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); }
.sidebar-pages-demo__search.kappa-sidebar__menu-button:hover { border-color: var(--kappa-subtle); background: var(--kappa-tint); }
.sidebar-pages-demo__search.kappa-sidebar__menu-button:focus-visible { border-color: var(--kappa-focus); outline-offset: 2px; }
.sidebar-pages-demo__group .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.sidebar-pages-demo__utilities { inline-size: 100%; }
.sidebar-pages-demo__empty { margin: 0.25rem 0.5rem !important; color: var(--kappa-subtle); font-size: 0.75rem !important; line-height: 1.5 !important; }
.sidebar-pages-demo__breadcrumb { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-pages-demo__breadcrumb span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.sidebar-pages-demo__breadcrumb span:first-child { color: var(--kappa-subtle); }
.sidebar-pages-demo__favorite { flex-shrink: 0; margin-inline-start: auto; }
.sidebar-pages-demo__favorite[aria-pressed="true"] svg { fill: currentColor; }
.sidebar-pages-demo__page { max-inline-size: 46rem; margin-inline: auto; padding: 2.5rem 2rem; color: var(--kappa-default); }
.sidebar-pages-demo__page-icon { display: block; margin-block-end: 1rem; font-size: 2rem; line-height: 1; }
.sidebar-pages-demo__eyebrow { margin: 0 0 0.375rem !important; color: var(--kappa-subtle); font-size: 0.6875rem !important; line-height: 1rem !important; }
.sidebar-pages-demo__page h2 { margin: 0 !important; font-size: 1.5rem !important; line-height: 1.875rem !important; font-weight: 600; letter-spacing: -0.025em; }
.sidebar-pages-demo__summary { margin: 0.75rem 0 1.25rem !important; color: var(--kappa-subtle); font-size: 0.8125rem !important; line-height: 1.6 !important; }
.sidebar-pages-demo__byline { display: flex; gap: 1rem; padding-block-end: 1.25rem; border-block-end: 1px solid var(--kappa-line); color: var(--kappa-subtle); font-size: 0.6875rem; }
.sidebar-pages-demo__byline span:first-child { color: var(--kappa-default); }
.sidebar-pages-demo__page h3 { display: flex; align-items: center; gap: 0.5rem; margin: 1.5rem 0 0.75rem !important; font-size: 0.875rem !important; line-height: 1.25rem !important; font-weight: 550; }
.sidebar-pages-demo__notes { margin: 0 !important; padding-inline-start: 1.25rem !important; font-size: 0.8125rem; line-height: 1.6; list-style: disc; }
.sidebar-pages-demo__notes li { margin-block: 0.5rem; padding-inline-start: 0.25rem; }
.sidebar-pages-demo__notes li::marker { color: var(--kappa-subtle); }
.sidebar-pages-demo__callout { display: flex; gap: 0.75rem; align-items: flex-start; margin-block-start: 1.5rem; padding: 0.875rem; border-radius: 0.375rem; background: var(--kappa-tint); }
.sidebar-pages-demo__callout > svg { flex: 0 0 1rem; inline-size: 1rem; block-size: 1rem; margin-block-start: 0.125rem; color: var(--kappa-subtle); }
.sidebar-pages-demo__callout p { margin: 0 !important; font-size: 0.75rem !important; line-height: 1.5 !important; }
.sidebar-pages-demo__note { margin: 1.5rem 0 0 !important; color: var(--kappa-subtle); font-size: 0.6875rem !important; line-height: 1.5 !important; }
.sidebar-pages-demo__count { color: var(--kappa-subtle); font-size: 0.75rem; font-weight: 400; }
.sidebar-pages-demo__index { margin: 0 !important; padding: 0 !important; list-style: none; }
.sidebar-pages-demo__index li { border-block-end: 1px solid var(--kappa-line); }
.sidebar-pages-demo__index button { display: flex; align-items: center; gap: 0.75rem; inline-size: 100%; padding: 0.625rem 0.25rem; border: 0; border-radius: 0.25rem; background: transparent; color: inherit; font: inherit; font-size: 0.8125rem; text-align: start; cursor: pointer; }
.sidebar-pages-demo__index button:hover { background: var(--kappa-tint); }
.sidebar-pages-demo__index button:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 2px; }
@media (max-width: 767px) {
.sidebar-pages-demo__page { padding: 1.5rem 1rem; }
.sidebar-pages-demo__breadcrumb span:first-child, .sidebar-pages-demo__breadcrumb span:nth-child(2) { display: none; }
}
</style>Minimal Workspace
A compact dicehub-style shell with centered search, a simple namespace selector, a resizable sidebar, and a rounded content panel. Includes local project creation, profile and Help menus, a footer collapse button, and full-screen mobile navigation.
<script setup lang="ts">
import { computed, ref, useId, watch } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { DicehubLogo } from "@dicehub/kappa/components/dicehub-logo";
import { Avatar } from "@dicehub/kappa/components/avatar";
import { Button } from "@dicehub/kappa/components/button";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Input } from "@dicehub/kappa/components/input";
import { Table } from "@dicehub/kappa/components/table";
import { Activity, Building2, ChevronDown, CircleHelp, Clock3, Compass, CreditCard, FileText, Folder, House, LayoutDashboard, LogOut, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, UserRound, UsersRound } from "@lucide/vue";
const props = withDefaults(defineProps<{ standalone?: boolean }>(), { standalone: true });
const headingId = useId();
const namespace = ref("Ros.Space");
const selected = ref("Dashboard");
const filter = ref("");
const query = ref("");
const searchOpen = ref(false);
const namespaces = ["Ros.Space", "Engineering", "Research"];
const globalLinks = ["Dashboard", "Explore", "Templates", "Community"];
const links = [
{ label: "Dashboard", icon: LayoutDashboard },
{ label: "Recently opened", icon: Clock3 },
{ label: "Activities", icon: Activity },
{ label: "User settings", icon: Settings },
];
const descriptions: Record<string, string> = {
Dashboard: "Your projects and recent work, in one place.",
Projects: "Projects in the selected namespace.",
"Recently opened": "Return to a project you opened recently.",
Activities: "Recent updates in this sample namespace.",
"User settings": "Profile and account settings preview. No settings are changed.",
Groups: "Choose a group namespace to see its projects.",
Explore: "Browse the sample projects available in this namespace.",
Templates: "Start with a sample project template.",
Community: "Share project notes and review ideas with your team.",
"Public profile": "Ros.Space · [email protected]",
Billing: "Billing preview. No payment or subscription is changed.",
"Sign out": "Local preview only. Your real session stays signed in.",
Help: "Select a navigation link or use Search or go to… to open a page.",
Documentation: "Use the namespace menu to switch workspaces. The sidebar can collapse to an icon rail.",
Support: "Support preview. No message is sent.",
};
const projects = ref([
{ id: "rotor", name: "Rotor study", namespace: "Ros.Space", status: "Ready", updated: "Today" },
{ id: "channel", name: "Channel flow", namespace: "Ros.Space", status: "In review", updated: "Yesterday" },
{ id: "heat", name: "Heat exchanger", namespace: "Engineering", status: "Draft", updated: "Today" },
{ id: "inlet", name: "Inlet comparison", namespace: "Research", status: "Ready", updated: "Yesterday" },
]);
const namespaceProjects = computed(() => projects.value.filter(project => project.namespace === namespace.value));
const visibleProjects = computed(() => namespaceProjects.value.filter(project => project.name.toLowerCase().includes(filter.value.trim().toLowerCase())));
const currentProject = computed(() => namespaceProjects.value.find(project => project.name === selected.value));
const isProjectList = computed(() => ["Dashboard", "Projects", "Recently opened", "Explore"].includes(selected.value));
const searchItems = computed(() => [...new Set([...globalLinks, ...links.map(link => link.label), "Projects", "Groups", ...namespaceProjects.value.map(project => project.name), "Help"])]);
const menuPosition = { placement: "bottom-end", strategy: "fixed", gutter: 6 } as const;
watch(namespace, () => { selected.value = "Dashboard"; filter.value = ""; });
function navigate(label: string, context: SidebarContextValue) {
selected.value = label;
filter.value = "";
context.setMobileOpen(false);
}
function search() { query.value = ""; searchOpen.value = true; }
function createProject(context: SidebarContextValue, template?: string) {
const number = projects.value.length + 1;
const name = template ? `${template} ${number}` : `Untitled project ${number}`;
projects.value.push({ id: `project-${number}`, name, namespace: namespace.value, status: "Draft", updated: "Just now" });
navigate(name, context);
}
</script>
<template>
<div class="minimal-workspace-demo" data-sidebar-block="minimal-workspace" :data-standalone="props.standalone || undefined">
<SidebarLayout variant="header" label="Minimal workspace navigation" width="250px" :default-width="250" :min-width="200" :max-width="420" collapsed-width="48px" resizable full-screen-on-mobile>
<template #toolbar="context">
<div class="minimal-workspace-demo__header-start">
<a href="?view=dashboard" class="minimal-workspace-demo__brand" aria-label="dicehub home" @click.prevent="navigate('Dashboard', context)"><DicehubLogo aria-hidden="true" /></a>
<nav class="minimal-workspace-demo__global-nav" aria-label="Application pages"><a v-for="label in globalLinks" :key="label" :href="`?view=${encodeURIComponent(label)}`" :aria-current="selected === label ? 'page' : undefined" @click.prevent="navigate(label, context)">{{ label }}</a></nav>
</div>
<Button size="sm" variant="ghost" :icon="Search" class="minimal-workspace-demo__search" aria-label="Search or go to…" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search"><span>Search or go to…</span></Button>
<div class="minimal-workspace-demo__header-end">
<Dropdown.Root :positioning="menuPosition">
<Dropdown.Trigger as-child><Button size="sm" shape="square" variant="ghost" :icon="Compass" aria-label="Browse application pages" class="minimal-workspace-demo__browse" /></Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :inert="!menu.open || undefined"><Dropdown.Group><Dropdown.Label>Application pages</Dropdown.Label><Dropdown.Item v-for="label in globalLinks" :key="label" :value="label" @select="navigate(label, context)">{{ label }}</Dropdown.Item></Dropdown.Group></Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
<Dropdown.Root :positioning="menuPosition">
<Dropdown.Trigger as-child><Button size="sm" variant="ghost" class="minimal-workspace-demo__profile" aria-label="Profile: Ros.Space"><Avatar.Root aria-hidden="true"><Avatar.Image src="/avatars/ros-space-astronaut.webp" alt="" /><Avatar.Fallback>RS</Avatar.Fallback></Avatar.Root><ChevronDown aria-hidden="true" /></Button></Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :inert="!menu.open || undefined" class="minimal-workspace-demo__profile-menu">
<div class="minimal-workspace-demo__identity"><strong>Ros.Space</strong><span>[email protected]</span></div>
<Dropdown.Separator />
<Dropdown.Item value="profile" :icon="UserRound" @select="navigate('Public profile', context)">Public profile</Dropdown.Item>
<Dropdown.Item value="settings" :icon="Settings" @select="navigate('User settings', context)">User settings</Dropdown.Item>
<Dropdown.Item value="billing" :icon="CreditCard" @select="navigate('Billing', context)">Billing</Dropdown.Item>
<Dropdown.Separator />
<Dropdown.Item value="sign-out" :icon="LogOut" @select="navigate('Sign out', context)">Sign out</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</div>
</template>
<template #header="context">
<Dropdown.Root :positioning="{ placement: context.iconCollapsed ? 'right-start' : 'bottom-start', strategy: 'fixed', gutter: 6 }">
<Dropdown.Trigger as-child><Sidebar.MenuButton class="minimal-workspace-demo__namespace" :aria-label="`Namespace: ${namespace}`" :tooltip="`Namespace: ${namespace}`">
<template #icon><Avatar.Root v-if="namespace === 'Ros.Space'" aria-hidden="true"><Avatar.Image src="/avatars/ros-space-astronaut.webp" alt="" /><Avatar.Fallback>RS</Avatar.Fallback></Avatar.Root><Building2 v-else aria-hidden="true" /></template>
<span>{{ namespace }}</span><ChevronDown aria-hidden="true" />
</Sidebar.MenuButton></Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace"><Dropdown.Label>Namespaces</Dropdown.Label><Dropdown.RadioItem v-for="name in namespaces" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem></Dropdown.RadioGroup>
<Dropdown.Separator />
<Dropdown.Item value="groups" :icon="UsersRound" @select="navigate('Groups', context)">Show all groups</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Group>
<Sidebar.GroupLabel>{{ namespace === 'Ros.Space' ? 'User' : 'Group' }}</Sidebar.GroupLabel>
<Sidebar.Menu aria-label="Workspace pages"><Sidebar.MenuItem v-for="link in links" :key="link.label"><Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${encodeURIComponent(link.label)}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group class="minimal-workspace-demo__projects-nav">
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton :icon="Folder" tooltip="Projects" :active="selected === 'Projects'" href="?view=projects" @click.prevent="navigate('Projects', context)">Projects</Sidebar.MenuButton>
<Sidebar.MenuSub v-if="!context.iconCollapsed" aria-label="Project shortcuts"><Sidebar.MenuSubItem v-for="project in namespaceProjects" :key="project.id"><Sidebar.MenuButton :href="`?project=${project.id}`" :active="selected === project.name" @click.prevent="navigate(project.name, context)">{{ project.name }}</Sidebar.MenuButton></Sidebar.MenuSubItem></Sidebar.MenuSub>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group><Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="UsersRound" tooltip="Groups" :active="selected === 'Groups'" href="?view=groups" @click.prevent="navigate('Groups', context)">Groups</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu></Sidebar.Group>
</template>
<template #footer="context">
<Dropdown.Root :positioning="{ placement: context.iconCollapsed ? 'right-end' : 'top-start', strategy: 'fixed', gutter: 6 }">
<Dropdown.Trigger as-child><Sidebar.MenuButton :icon="CircleHelp" tooltip="Help" class="minimal-workspace-demo__help">Help</Sidebar.MenuButton></Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined"><Dropdown.Group><Dropdown.Label>Help and resources</Dropdown.Label><Dropdown.Item v-for="label in ['Help', 'Documentation', 'Support']" :key="label" :value="label" @select="navigate(label, context)">{{ label }}</Dropdown.Item></Dropdown.Group></Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
<Sidebar.Trigger v-if="!context.isMobile" as-child><Sidebar.MenuButton class="minimal-workspace-demo__collapse" :icon="context.open ? PanelLeftClose : PanelLeftOpen" :tooltip="context.open ? 'Collapse sidebar' : 'Expand sidebar'">{{ context.open ? 'Collapse sidebar' : 'Expand sidebar' }}</Sidebar.MenuButton></Sidebar.Trigger>
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :inert="!searchOpen || undefined" aria-label="Search minimal workspace" @select="item => { searchOpen = false; navigate(String(item), context); }">
<CommandPalette.Input placeholder="Search pages and projects…" />
<CommandPalette.List><CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results><CommandPalette.Empty>No results found.</CommandPalette.Empty></CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<nav class="minimal-workspace-demo__breadcrumb" aria-label="Workspace breadcrumb"><a href="?view=dashboard" aria-label="Workspace home" @click.prevent="navigate('Dashboard', context)"><House aria-hidden="true" /></a><a href="?view=projects" @click.prevent="navigate('Projects', context)">{{ namespace }}</a><span aria-hidden="true">/</span><span aria-current="page">{{ selected }}</span></nav>
<section class="minimal-workspace-demo__page" :aria-labelledby="headingId">
<div class="minimal-workspace-demo__page-heading"><div><h2 :id="headingId">{{ selected }}</h2><p>{{ currentProject ? 'Project overview in this local workspace.' : descriptions[selected] }}</p></div><Button v-if="isProjectList" size="sm" :icon="Plus" @click="createProject(context)">New project</Button></div>
<div v-if="isProjectList" class="minimal-workspace-demo__project-panel">
<div class="minimal-workspace-demo__panel-heading"><h3>Projects</h3><span>{{ namespaceProjects.length }}</span></div>
<div class="minimal-workspace-demo__filters"><div class="minimal-workspace-demo__filter"><Search aria-hidden="true" /><Input v-model="filter" type="search" size="sm" aria-label="Search projects" placeholder="Search by name" @keydown.esc="filter = ''" /></div><span>{{ visibleProjects.length }} shown</span></div>
<Table compact><Table.Caption class="minimal-workspace-demo__sr-only">Projects in {{ namespace }}</Table.Caption><Table.Header><Table.Row><Table.Head>Project</Table.Head><Table.Head>Status</Table.Head><Table.Head>Updated</Table.Head></Table.Row></Table.Header><Table.Body>
<Table.Row v-for="project in visibleProjects" :key="project.id"><Table.Cell><button type="button" class="minimal-workspace-demo__project-link" @click="navigate(project.name, context)"><span class="minimal-workspace-demo__project-icon" aria-hidden="true">{{ project.name[0] }}</span><span><small>{{ namespace }} /</small><strong>{{ project.name }}</strong></span></button></Table.Cell><Table.Cell><span class="minimal-workspace-demo__status" :data-ready="project.status === 'Ready' || undefined">{{ project.status }}</span></Table.Cell><Table.Cell>{{ project.updated }}</Table.Cell></Table.Row>
<Table.Row v-if="!visibleProjects.length"><Table.Cell colspan="3" class="minimal-workspace-demo__empty">No projects found.</Table.Cell></Table.Row>
</Table.Body></Table>
</div>
<dl v-else-if="currentProject" class="minimal-workspace-demo__details"><div><dt>Namespace</dt><dd>{{ namespace }}</dd></div><div><dt>Status</dt><dd>{{ currentProject.status }}</dd></div><div><dt>Updated</dt><dd>{{ currentProject.updated }}</dd></div></dl>
<ul v-else-if="selected === 'Activities'" class="minimal-workspace-demo__list"><li v-for="project in namespaceProjects" :key="project.id"><span>{{ project.name }} — {{ project.status }}</span><small>{{ project.updated }}</small></li></ul>
<ul v-else-if="selected === 'Groups'" class="minimal-workspace-demo__list"><li v-for="name in namespaces.slice(1)" :key="name"><Button variant="ghost" size="sm" :icon="Building2" @click="namespace = name">{{ name }}</Button><small>Group namespace</small></li></ul>
<ul v-else-if="selected === 'Templates'" class="minimal-workspace-demo__list"><li v-for="name in ['Flow study', 'Thermal study']" :key="name"><span>{{ name }}</span><Button size="sm" variant="outline" @click="createProject(context, name)">Use {{ name }}</Button></li></ul>
<p v-else-if="selected === 'Community'" class="minimal-workspace-demo__note"><FileText aria-hidden="true" />No shared notes yet.</p>
<p class="minimal-workspace-demo__note">Example data only. Changes stay in this preview until reload.</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.minimal-workspace-demo { container: minimal-workspace / inline-size; inline-size: 100%; block-size: 36rem; min-inline-size: 0; text-align: start; }
.minimal-workspace-demo[data-standalone] { block-size: 100svh; }
.minimal-workspace-demo .kappa-sidebar-layout { background: var(--kappa-tint); border: 1px solid var(--kappa-line); border-radius: 0.625rem; overflow: hidden; }
.minimal-workspace-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.minimal-workspace-demo .kappa-sidebar-layout .kappa-sidebar-layout__site-header { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 0.75rem; min-block-size: 3rem; padding: 0.375rem 0.75rem; border: 0; background: var(--kappa-tint); }
/* The built-in trigger opens mobile navigation; desktop uses the footer trigger. */
.minimal-workspace-demo .kappa-sidebar-layout:not([data-mobile]) .kappa-sidebar-layout__site-header > .kappa-sidebar__trigger { display: none; }
.minimal-workspace-demo .kappa-sidebar-layout__site-header > .kappa-sidebar__trigger { grid-area: 1 / 1; justify-self: start; z-index: 1; }
.minimal-workspace-demo__header-start, .minimal-workspace-demo__header-end { display: flex; align-items: center; gap: 0.75rem; min-inline-size: 0; }
.minimal-workspace-demo__header-start { grid-area: 1 / 1; }
.minimal-workspace-demo__header-end { grid-area: 1 / 3; justify-self: end; }
.minimal-workspace-demo .kappa-sidebar-layout[data-mobile] .minimal-workspace-demo__header-start { padding-inline-start: 2.5rem; }
.minimal-workspace-demo .kappa-sidebar-layout__body { padding-block-end: 0.5rem; padding-inline-end: 0.5rem; gap: 0.5rem; }
.minimal-workspace-demo .kappa-sidebar-layout__main { border-radius: 0.5rem; overflow: hidden; }
.minimal-workspace-demo .kappa-sidebar-layout__navigation { border: 0; background: var(--kappa-tint); }
.minimal-workspace-demo .kappa-sidebar__resize-handle::after { opacity: 0; }
.minimal-workspace-demo .kappa-sidebar__resize-handle:is(:hover, :focus-visible, [data-dragging]):not([data-disabled])::after { opacity: 1; }
.minimal-workspace-demo .kappa-sidebar-layout__navigation > .kappa-sidebar__header { min-block-size: 2.875rem; padding: 0.375rem 0.5rem; }
.minimal-workspace-demo .kappa-sidebar-layout__navigation > .kappa-sidebar__footer { flex-direction: column; align-items: stretch; gap: 0; padding-block: 0.375rem; }
.minimal-workspace-demo .kappa-sidebar-layout__navigation .kappa-sidebar__menu-button { min-block-size: 1.875rem; padding-block: 0.25rem; font-size: 0.8125rem; }
.minimal-workspace-demo .kappa-sidebar-layout__navigation .kappa-sidebar__menu-button[data-active] { background: var(--kappa-overlay); font-weight: 550; }
.minimal-workspace-demo .kappa-sidebar-layout__navigation .kappa-sidebar__menu-icon { color: var(--kappa-subtle); }
.minimal-workspace-demo .kappa-sidebar-layout__navigation .kappa-sidebar__menu-button[data-active] > .kappa-sidebar__menu-icon { color: var(--kappa-accent-text); }
.minimal-workspace-demo .kappa-sidebar-layout__navigation .kappa-sidebar__menu-sub { border: 0; margin-inline-start: 1.375rem; padding-inline-start: 0; }
.minimal-workspace-demo__brand { flex: 0 0 auto; display: block; inline-size: 5.75rem; border-radius: 0.25rem; }
.minimal-workspace-demo__brand .kappa-dicehub-logo { color: var(--kappa-default); }
.minimal-workspace-demo__global-nav { display: flex; gap: 0.25rem; }
.minimal-workspace-demo__global-nav a { padding: 0.375rem 0.5rem; border-radius: 0.25rem; color: var(--kappa-subtle); font-size: 0.75rem; text-decoration: none; white-space: nowrap; }
.minimal-workspace-demo__global-nav a:hover, .minimal-workspace-demo__global-nav a[aria-current] { color: var(--kappa-default); background: var(--kappa-overlay); }
.minimal-workspace-demo__brand:focus-visible, .minimal-workspace-demo__global-nav a:focus-visible, .minimal-workspace-demo__breadcrumb a:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 2px; }
.minimal-workspace-demo__browse.kappa-button { display: none; }
.minimal-workspace-demo__search.kappa-button { grid-area: 1 / 2; inline-size: 14rem; min-inline-size: 0; justify-content: flex-start; border: 1px solid var(--kappa-line); background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-subtle); }
.minimal-workspace-demo__search .kappa-button__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.minimal-workspace-demo__profile.kappa-button { padding: 0.125rem 0.25rem; flex: 0 0 auto; gap: 0.375rem; }
.minimal-workspace-demo__profile .kappa-button__label { display: flex; align-items: center; gap: 0.375rem; }
.minimal-workspace-demo__profile .kappa-avatar { --kappa-avatar-size: 1.5rem; }
.minimal-workspace-demo__profile svg { inline-size: 0.75rem; block-size: 0.75rem; color: var(--kappa-subtle); }
.minimal-workspace-demo__profile-menu.kappa-dropdown__content { min-inline-size: 14rem; }
.minimal-workspace-demo__identity { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.625rem 0.5rem; font-size: 0.8125rem; }
.minimal-workspace-demo__identity strong { font-weight: 550; }
.minimal-workspace-demo__identity span { color: var(--kappa-subtle); font-size: 0.75rem; }
.minimal-workspace-demo__namespace .kappa-avatar { --kappa-avatar-size: 1.25rem; }
.minimal-workspace-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 1.25rem; block-size: 1.25rem; }
.minimal-workspace-demo__namespace .kappa-sidebar__menu-label { display: flex; align-items: center; gap: 0.5rem; }
.minimal-workspace-demo__namespace .kappa-sidebar__menu-label > span { overflow: hidden; text-overflow: ellipsis; }
.minimal-workspace-demo__namespace .kappa-sidebar__menu-label > svg { inline-size: 0.75rem; block-size: 0.75rem; flex: none; color: var(--kappa-subtle); }
.minimal-workspace-demo__help, .minimal-workspace-demo__collapse { color: var(--kappa-subtle); }
.minimal-workspace-demo__collapse.kappa-sidebar__menu-button { min-inline-size: 0; padding-inline: 0.5rem; text-align: start; }
.minimal-workspace-demo__breadcrumb { display: flex; gap: 0.375rem; align-items: center; min-block-size: 2.875rem; padding: 0.5rem 1.25rem; font-size: 0.6875rem; }
.minimal-workspace-demo__breadcrumb > :last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.minimal-workspace-demo__breadcrumb a { flex-shrink: 0; color: var(--kappa-accent-text); text-decoration: none; border-radius: 0.125rem; }
.minimal-workspace-demo__breadcrumb svg { inline-size: 0.75rem; block-size: 0.75rem; }
.minimal-workspace-demo__page { max-inline-size: 72rem; margin-inline: auto; padding: 1.25rem 1.5rem 2rem; }
.minimal-workspace-demo__page-heading { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-block-end: 1.5rem; }
.minimal-workspace-demo__page-heading h2 { margin: 0 !important; font-size: 1.125rem !important; line-height: 1.5rem !important; font-weight: 550; }
.minimal-workspace-demo__page-heading p { margin: 0.375rem 0 0 !important; color: var(--kappa-subtle); font-size: 0.8125rem !important; line-height: 1.5 !important; }
.minimal-workspace-demo__project-panel { overflow: hidden; border: 1px solid var(--kappa-line); border-radius: 0.375rem; }
.minimal-workspace-demo__panel-heading { display: flex; align-items: center; gap: 0.5rem; padding: 0.625rem 0.75rem; border-block-end: 1px solid var(--kappa-line); background: color-mix(in srgb, var(--kappa-tint) 40%, var(--kappa-control)); }
.minimal-workspace-demo__panel-heading h3 { margin: 0 !important; font-size: 0.8125rem !important; line-height: 1.125rem !important; font-weight: 550; }
.minimal-workspace-demo__panel-heading > span { color: var(--kappa-subtle); font-size: 0.6875rem; }
.minimal-workspace-demo__filters { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.5rem 0.75rem; border-block-end: 1px solid var(--kappa-line); }
.minimal-workspace-demo__filter { position: relative; inline-size: min(14rem, 100%); min-inline-size: 0; }
.minimal-workspace-demo__filter > svg { position: absolute; inset-inline-start: 0.625rem; inset-block-start: 50%; transform: translateY(-50%); inline-size: 0.875rem; block-size: 0.875rem; color: var(--kappa-subtle); pointer-events: none; }
.minimal-workspace-demo__filter .kappa-input { padding-inline-start: 1.875rem; }
.minimal-workspace-demo__filters > span { flex-shrink: 0; color: var(--kappa-subtle); font-size: 0.6875rem; }
.minimal-workspace-demo__project-panel .kappa-table { border: 0; }
.minimal-workspace-demo__project-panel .kappa-table__cell { padding: 0.625rem 0.75rem; font-size: 0.75rem; }
.minimal-workspace-demo__project-panel .kappa-table__head { padding-inline: 0.75rem; }
.minimal-workspace-demo__project-link { display: flex; align-items: center; gap: 0.625rem; padding: 0; border: 0; background: transparent; color: var(--kappa-default); font: inherit; text-align: start; cursor: pointer; }
.minimal-workspace-demo__project-link small { display: block; margin-block-end: 0.125rem; color: var(--kappa-subtle); font-size: 0.6875rem; }
.minimal-workspace-demo__project-link strong { font-weight: 550; }
.minimal-workspace-demo__project-link:hover strong { text-decoration: underline; text-underline-offset: 0.2em; }
.minimal-workspace-demo__project-link:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 3px; border-radius: 0.125rem; }
.minimal-workspace-demo__project-icon { display: grid; place-items: center; flex: 0 0 2rem; block-size: 2rem; border-radius: 0.25rem; background: var(--kappa-tint); color: var(--kappa-subtle); font-size: 0.875rem; }
.minimal-workspace-demo__status { display: inline-flex; align-items: center; gap: 0.375rem; white-space: nowrap; color: var(--kappa-subtle); font-size: 0.6875rem; }
.minimal-workspace-demo__status::before { content: ""; inline-size: 0.3125rem; block-size: 0.3125rem; border-radius: 50%; background: var(--kappa-subtle); }
.minimal-workspace-demo__status[data-ready]::before { background: var(--kappa-success-text); }
.minimal-workspace-demo__empty { padding: 1.5rem !important; text-align: center; color: var(--kappa-subtle); }
.minimal-workspace-demo__details { margin: 0; border-block-start: 1px solid var(--kappa-line); font-size: 0.8125rem; }
.minimal-workspace-demo__details > div { display: grid; grid-template-columns: 7rem 1fr; padding: 0.75rem 0; border-block-end: 1px solid var(--kappa-line); }
.minimal-workspace-demo__details dt { color: var(--kappa-subtle); }
.minimal-workspace-demo__details dd { margin: 0; }
.minimal-workspace-demo__list { margin: 0 !important; padding: 0 !important; list-style: none; font-size: 0.8125rem; }
.minimal-workspace-demo__list li { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-block: 0.75rem; border-block-end: 1px solid var(--kappa-line); }
.minimal-workspace-demo__list small { color: var(--kappa-subtle); font-size: 0.6875rem; }
.minimal-workspace-demo__note { display: flex; gap: 0.5rem; margin: 1.25rem 0 0 !important; color: var(--kappa-subtle); font-size: 0.6875rem !important; line-height: 1.5 !important; }
.minimal-workspace-demo__note svg { inline-size: 0.875rem; block-size: 0.875rem; flex: none; }
.minimal-workspace-demo__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@container minimal-workspace (max-width: 68rem) {
.minimal-workspace-demo__global-nav { display: none; }
.minimal-workspace-demo__browse.kappa-button { display: inline-flex; }
.minimal-workspace-demo__page { padding-inline: 1rem; }
}
@container minimal-workspace (max-width: 40rem) {
.minimal-workspace-demo__search.kappa-button { inline-size: 2rem; padding-inline: 0.5rem; }
.minimal-workspace-demo__search .kappa-button__label { display: none; }
}
@container minimal-workspace (max-width: 26rem) {
.minimal-workspace-demo .kappa-sidebar-layout .kappa-sidebar-layout__site-header { gap: 0.5rem; padding-inline: 0.5rem; }
.minimal-workspace-demo__header-end { gap: 0.5rem; }
.minimal-workspace-demo__profile .kappa-button__label > svg { display: none; }
.minimal-workspace-demo__brand { inline-size: 5.25rem; }
.minimal-workspace-demo__project-panel .kappa-table__cell { padding-inline: 0.5rem; }
.minimal-workspace-demo__project-icon { display: none; }
.minimal-workspace-demo__page-heading { gap: 0.75rem; }
}
@media (max-width: 767px) {
.minimal-workspace-demo .kappa-sidebar-layout__body { padding: 0; }
.minimal-workspace-demo .kappa-sidebar-layout__main { border-radius: 0; }
.minimal-workspace-demo__namespace.kappa-sidebar__menu-button { min-block-size: 2.75rem; }
}
</style>Workspace
Grouped navigation, namespace and profile menus, Quick search, and a resizable edge.
<script setup lang="ts">
import { computed, ref, useId } from "vue";
import { SidebarLayout, type SidebarLayoutVariant } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { Button } from "@dicehub/kappa/components/button";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Avatar } from "@dicehub/kappa/components/avatar";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Table } from "@dicehub/kappa/components/table";
import { Building2, ChevronsUpDown, Folder, LayoutDashboard, FileText, Settings, Search, Plus, Box, CircleHelp, MessageSquare, BadgeCheck, Bell, CreditCard, LogOut, Sparkles, UsersRound } from "@lucide/vue";
const props = withDefaults(defineProps<{ variant?: SidebarLayoutVariant; standalone?: boolean; iconNavigation?: boolean; insetNavigation?: boolean }>(), { variant: "workspace", standalone: true, iconNavigation: false, insetNavigation: false });
const richNavigation = computed(() => props.iconNavigation || props.insetNavigation);
const headingId = useId();
const namespace = ref("Engineering");
const profile = ref("ros");
const selected = ref("Overview");
const secondaryLinks = [{ label: "Support", icon: CircleHelp }, { label: "Feedback", icon: MessageSquare }];
const accountPages: Record<string, string> = {
'Upgrade to Pro': 'Review the plans available for your workspace.',
Account: 'Manage your profile and account details.',
Billing: 'Review your subscription, invoices, and payment details.',
Notifications: 'Choose which workspace updates you receive.',
Preferences: 'Set your display and workspace preferences.',
'Log out': 'This preview does not end a real session.',
};
const accountPage = computed(() => accountPages[selected.value]);
const utilityPage = computed(() => Boolean(accountPage.value) || (props.insetNavigation && secondaryLinks.some(link => link.label === selected.value)));
const searchOpen = ref(false);
const query = ref("");
const navigating = ref(false);
const searchFromDrawer = ref(false);
const sectionsOpen = ref(true);
const profiles = [
{ value: "ros", name: "Ros.Space", email: "[email protected]", initials: "RS", avatar: "/avatars/ros-space-astronaut.webp" },
{ value: "jordan", name: "Jordan Lee", email: "[email protected]", initials: "JL", avatar: "/avatars/mei-chen.webp" },
];
const account = computed(() => profiles.find(item => item.value === profile.value)!);
const links = [{ label: "Overview", icon: LayoutDashboard }, { label: "Projects", icon: Folder }, { label: "Reports", icon: FileText }];
const navigationGroups = [
{ label: "Workspace", icon: LayoutDashboard, items: ["Overview", "Reports"] },
{ label: "Mesh", icon: Box, items: ["Geometry", "Refinement", "Simulation"] },
{ label: "Settings", icon: Settings, items: ["General", "Members", "Integrations"] },
];
const openGroups = ref<Record<string, boolean>>({ Workspace: true, Mesh: false, Settings: false });
const searchItems = computed(() => richNavigation.value
? [...navigationGroups.flatMap(group => group.items), ...projects.value.map(project => project.name), ...(props.insetNavigation ? secondaryLinks.map(link => link.label) : [])]
: ["Overview", "Projects", "Reports", "Geometry", "Refinement", "Settings"]);
const projects = ref([
{ name: "Rotor study", code: "MESH-042", state: "Ready" },
{ name: "Channel flow", code: "MESH-041", state: "In review" },
{ name: "Heat exchanger", code: "MESH-039", state: "Draft" },
]);
const namespacePositioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
const desktopProfilePositioning = { placement: "right-end", strategy: "fixed", gutter: 8 } as const;
const mobileProfilePositioning = { placement: "top-start", strategy: "fixed", gutter: 8 } as const;
function navigate(value: string, context: SidebarContextValue) {
selected.value = value;
if (richNavigation.value) {
const group = navigationGroups.find(group => group.items.includes(value));
if (group) openGroups.value[group.label] = true;
}
context.setMobileOpen(false);
}
function search(context: SidebarContextValue) { query.value = ""; navigating.value = false; searchFromDrawer.value = context.isMobile && context.mobileOpen; searchOpen.value = true; }
function createProject() {
const name = `Untitled project ${projects.value.length - 2}`;
projects.value.push({ name, code: `MESH-${String(43 + projects.value.length - 3).padStart(3, "0")}`, state: "Draft" });
selected.value = name;
}
</script>
<template>
<div class="sidebar-block-demo" :data-sidebar-block="props.insetNavigation ? 'inset-navigation' : props.iconNavigation ? 'collapsible-icons' : props.variant" :data-standalone="props.standalone || undefined">
<SidebarLayout :variant="props.variant" :label="props.insetNavigation ? 'Inset application navigation' : props.iconNavigation ? 'Icon application navigation' : `${props.variant} application navigation`" :resizable="!richNavigation && props.variant === 'workspace'" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Namespaces" :positioning="richNavigation && !context.isMobile ? { placement: 'right-start', strategy: 'fixed', gutter: 8 } : namespacePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Namespace: ${namespace}`" :tooltip="`Namespace: ${namespace}`" class="sidebar-block-demo__identity-button sidebar-block-demo__namespace">
<template #icon><span class="sidebar-block-demo__namespace-icon"><Building2 aria-hidden="true" /></span></template>
<span class="sidebar-block-demo__identity"><strong>{{ namespace }}</strong><small>Namespace</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace">
<Dropdown.Label>Switch namespace</Dropdown.Label>
<Dropdown.RadioItem v-for="name in ['Engineering', 'Research', 'Personal']" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Menu class="sidebar-block-demo__search">
<Sidebar.MenuItem><Sidebar.MenuButton :icon="Search" tooltip="Quick search" class="sidebar-block-demo__search-button" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search(context)">Quick search …</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
<template v-if="richNavigation">
<Sidebar.Group>
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="group in navigationGroups" :key="group.label">
<Sidebar.Collapsible v-model:open="openGroups[group.label]">
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton :icon="group.icon" :tooltip="group.label" :active="group.items.includes(selected)" :aria-current="undefined" class="sidebar-block-demo__group-trigger">{{ group.label }}<Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub :aria-label="group.label">
<Sidebar.MenuSubItem v-for="item in group.items" :key="item"><Sidebar.MenuButton :active="selected === item" :href="`?view=${item.toLowerCase()}`" @click.prevent="navigate(item, context)">{{ item }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="project in projects" :key="project.code">
<Sidebar.MenuButton :icon="Folder" :tooltip="project.name" :active="selected === project.name" :href="`?project=${project.code}`" @click.prevent="navigate(project.name, context)">{{ project.name }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
<template v-else>
<Sidebar.Group>
<Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="link in links" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.Collapsible v-model:open="sectionsOpen">
<Sidebar.CollapsibleTrigger as-child><Sidebar.MenuButton :icon="Box" tooltip="Mesh">Mesh <Sidebar.MenuChevron /></Sidebar.MenuButton></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub>
<Sidebar.MenuSubItem v-for="name in ['Geometry', 'Refinement']" :key="name"><Sidebar.MenuButton :active="selected === name" :href="`?view=${name.toLowerCase()}`" @click.prevent="navigate(name, context)">{{ name }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Manage</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="Settings" tooltip="Settings" :active="selected === 'Settings'" href="?view=settings" @click.prevent="navigate('Settings', context)">Settings</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
</template>
<template #footer="context">
<nav v-if="props.insetNavigation" aria-label="Help and feedback" class="sidebar-block-demo__secondary">
<Sidebar.Menu><Sidebar.MenuItem v-for="link in secondaryLinks" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</nav>
<Dropdown.Root aria-label="Profile" :positioning="context.isMobile ? mobileProfilePositioning : desktopProfilePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Profile: ${account.name}`" :tooltip="account.name" class="sidebar-block-demo__identity-button sidebar-block-demo__profile">
<template #icon><Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root></template>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<div class="sidebar-block-demo__profile-summary">
<Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
</div>
<Dropdown.Separator />
<Dropdown.Group><Dropdown.Item value="upgrade" :icon="Sparkles" @select="navigate('Upgrade to Pro', context)">Upgrade to Pro</Dropdown.Item></Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Group>
<Dropdown.Item value="account" :icon="BadgeCheck" @select="navigate('Account', context)">Account</Dropdown.Item>
<Dropdown.Item value="billing" :icon="CreditCard" @select="navigate('Billing', context)">Billing</Dropdown.Item>
<Dropdown.Item value="notifications" :icon="Bell" @select="navigate('Notifications', context)">Notifications</Dropdown.Item>
<Dropdown.Item value="preferences" :icon="Settings" @select="navigate('Preferences', context)">Preferences</Dropdown.Item>
</Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Sub aria-label="Switch profile" :positioning="{ placement: 'right-end', strategy: 'fixed', gutter: 4, fitViewport: true, overflowPadding: 8 }">
<Dropdown.SubTrigger :icon="UsersRound">Switch profile</Dropdown.SubTrigger>
<Dropdown.Context v-slot="submenu"><Dropdown.SubContent :teleport="!context.isMobile" :inert="!submenu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<Dropdown.RadioGroup v-model="profile"><Dropdown.Label>Profiles</Dropdown.Label><Dropdown.RadioItem v-for="item in profiles" :key="item.value" :value="item.value" close-on-select>{{ item.name }}</Dropdown.RadioItem></Dropdown.RadioGroup>
</Dropdown.SubContent></Dropdown.Context>
</Dropdown.Sub>
<Dropdown.Separator />
<Dropdown.Item value="logout" :icon="LogOut" @select="navigate('Log out', context)">Log out</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #toolbar>
<span class="sidebar-block-demo__breadcrumb"><span>{{ namespace }}</span><span aria-hidden="true">/</span><span>{{ selected }}</span></span>
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :restore-focus="!navigating || !searchFromDrawer" aria-label="Search navigation" @select="item => { navigating = true; searchOpen = false; navigate(String(item), context); }">
<CommandPalette.Input placeholder="Search navigation…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<section class="sidebar-block-demo__page" :aria-labelledby="headingId">
<div class="sidebar-block-demo__page-heading">
<div><span class="sidebar-block-demo__eyebrow">{{ namespace }} workspace</span><h2 :id="headingId">{{ selected }}</h2></div>
<Button v-if="!utilityPage" size="sm" variant="secondary" @click="createProject"><Plus aria-hidden="true" />New project</Button>
</div>
<p v-if="utilityPage" class="sidebar-block-demo__description">{{ accountPage || (selected === 'Support' ? 'Find help with your workspace, geometry, and simulation setup.' : 'Share a suggestion or report an issue with your workspace.') }}</p>
<p v-else class="sidebar-block-demo__description">Review geometry, prepare meshes, and track project changes.</p>
<template v-if="!utilityPage">
<div class="sidebar-block-demo__section-heading"><h3>Recent projects</h3><span>{{ projects.length }} projects</span></div>
<Table compact>
<Table.Caption class="sidebar-block-demo__sr-only">Recent projects in {{ namespace }}</Table.Caption>
<Table.Header><Table.Row><Table.Head>Project</Table.Head><Table.Head>Status</Table.Head></Table.Row></Table.Header>
<Table.Body><Table.Row v-for="item in projects" :key="item.code">
<Table.Cell><button type="button" class="sidebar-block-demo__project-link" @click="selected = item.name">{{ item.name }}</button><span class="sidebar-block-demo__project-code">{{ item.code }}</span></Table.Cell>
<Table.Cell><span class="sidebar-block-demo__status" :data-ready="item.state === 'Ready' || undefined">{{ item.state }}</span></Table.Cell>
</Table.Row></Table.Body>
</Table>
</template>
<p v-if="accountPage" class="sidebar-block-demo__note" role="status">Local preview only. No account, payment, notification, or session is changed.</p>
<p v-else-if="utilityPage" class="sidebar-block-demo__note">This is a local navigation preview. Connect this page to your application's {{ selected === 'Support' ? 'help resources' : 'feedback form' }}. No message is sent.</p>
<p v-else class="sidebar-block-demo__note" role="status">{{ projects.length > 3 ? 'New projects added to this example.' : 'Example data. Changes stay in this preview.' }}</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-block-demo { inline-size: 100%; block-size: 33rem; min-inline-size: 0; text-align: start; }
.sidebar-block-demo[data-sidebar-block="collapsible-icons"] { block-size: 38rem; }
.sidebar-block-demo[data-sidebar-block="inset-navigation"] { block-size: 42rem; }
.sidebar-block-demo[data-standalone] { block-size: 100svh; }
.sidebar-block-demo .kappa-sidebar-layout { border: 1px solid var(--kappa-line); border-radius: 0.625rem; overflow: hidden; }
.sidebar-block-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.sidebar-block-demo__identity-button { padding-block: 0.25rem; }
.sidebar-block-demo__identity-button .kappa-sidebar__menu-label { position: relative; padding-inline-end: 1.25rem; }
.sidebar-block-demo__identity { display: flex; flex-direction: column; min-inline-size: 0; gap: 0.125rem; }
.sidebar-block-demo__identity strong { font-size: 0.8125rem; line-height: 1.125rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__identity small { font-size: 0.6875rem; line-height: 1rem; color: var(--kappa-subtle); overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 0.875rem; block-size: 0.875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__namespace-icon { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; background: var(--kappa-accent-solid); color: var(--kappa-accent-contrast); }
.sidebar-block-demo__namespace-icon svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace .kappa-sidebar__menu-label { padding-inline-end: 1.5rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace[data-state="open"] { background: var(--kappa-tint); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button:focus-visible { outline-offset: 2px; }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__namespace { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__avatar.kappa-avatar { --kappa-avatar-size: 2rem; border-radius: 0.5rem; overflow: hidden; }
.sidebar-block-demo__profile > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__profile.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity { gap: 0; }
.sidebar-block-demo__profile .sidebar-block-demo__identity strong, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1.125rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity small, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__profile .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__profile[data-state="open"] { background: var(--kappa-overlay); }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__profile { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__profile-menu.kappa-dropdown__content { inline-size: max(14rem, var(--reference-width, 14rem)); border-radius: 0.5rem; }
.sidebar-block-demo__profile-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.25rem; }
.sidebar-block-demo__profile-summary .sidebar-block-demo__identity { flex: 1; }
.sidebar-block-demo__profile-menu[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-block-demo__secondary { margin-block-end: 0.5rem; }
.kappa-sidebar__footer:has(> .sidebar-block-demo__secondary) { flex-direction: column; align-items: stretch; }
.sidebar-block-demo__search { margin-block-end: 0.5rem; }
.sidebar-block-demo__group-trigger .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button { min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.375rem; background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-default); }
.sidebar-block-demo__search-button > .kappa-sidebar__menu-icon { color: var(--kappa-subtle); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:hover { border-color: var(--kappa-subtle); background: var(--kappa-tint); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:focus-visible { border-color: var(--kappa-focus); outline-offset: 2px; }
.sidebar-block-demo__breadcrumb { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-block-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-block-demo__breadcrumb span:first-child { color: var(--kappa-subtle); }
.sidebar-block-demo__page { padding: 1.5rem; max-inline-size: 62rem; margin-inline: auto; }
.sidebar-block-demo__page-heading { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 1rem; }
.sidebar-block-demo__page-heading h2 { margin: 0.375rem 0 0 !important; color: var(--kappa-default); font-size: 1.25rem !important; line-height: 1.5rem !important; letter-spacing: -0.025em; font-weight: 550; }
.sidebar-block-demo__page-heading button svg { inline-size: 0.875rem; block-size: 0.875rem; }
.sidebar-block-demo__eyebrow { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__description { margin: 0.75rem 0 1.5rem !important; font-size: 0.8125rem !important; line-height: 1.4; color: var(--kappa-subtle); }
.sidebar-block-demo__section-heading { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-block-end: 0.625rem; }
.sidebar-block-demo__section-heading h3 { margin: 0 !important; font-size: 0.8125rem !important; line-height: 1.25rem !important; font-weight: 550; }
.sidebar-block-demo__section-heading > span { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__project-link { display: block; margin: 0; padding: 0; border: 0; background: transparent; color: var(--kappa-default); cursor: pointer; font: inherit; text-align: start; }
.sidebar-block-demo__project-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.sidebar-block-demo__project-link:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 3px; }
.sidebar-block-demo__project-code { display: block; font-size: 0.625rem; line-height: 1rem; color: var(--kappa-subtle); }
.sidebar-block-demo__status { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem; white-space: nowrap; }
.sidebar-block-demo__status::before { content: ""; inline-size: 0.3125rem; block-size: 0.3125rem; border-radius: 50%; background: var(--kappa-subtle); }
.sidebar-block-demo__status[data-ready]::before { background: var(--kappa-success-text); }
.sidebar-block-demo__note { margin: 1rem 0 0 !important; font-size: 0.6875rem !important; color: var(--kappa-subtle); }
.sidebar-block-demo__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 767px) {
.sidebar-block-demo:not([data-standalone]) { block-size: 30rem; }
.sidebar-block-demo__page { padding: 1rem; }
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :first-child,
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :nth-child(2) { display: none; }
}
</style>Icon Rail
Starts with a compact icon rail. Expand it for labels and nested navigation.
<script setup lang="ts">
import { computed, ref, useId } from "vue";
import { SidebarLayout, type SidebarLayoutVariant } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { Button } from "@dicehub/kappa/components/button";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Avatar } from "@dicehub/kappa/components/avatar";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Table } from "@dicehub/kappa/components/table";
import { Building2, ChevronsUpDown, Folder, LayoutDashboard, FileText, Settings, Search, Plus, Box, CircleHelp, MessageSquare, BadgeCheck, Bell, CreditCard, LogOut, Sparkles, UsersRound } from "@lucide/vue";
const props = withDefaults(defineProps<{ variant?: SidebarLayoutVariant; standalone?: boolean; iconNavigation?: boolean; insetNavigation?: boolean }>(), { variant: "rail", standalone: true, iconNavigation: false, insetNavigation: false });
const richNavigation = computed(() => props.iconNavigation || props.insetNavigation);
const headingId = useId();
const namespace = ref("Engineering");
const profile = ref("ros");
const selected = ref("Overview");
const secondaryLinks = [{ label: "Support", icon: CircleHelp }, { label: "Feedback", icon: MessageSquare }];
const accountPages: Record<string, string> = {
'Upgrade to Pro': 'Review the plans available for your workspace.',
Account: 'Manage your profile and account details.',
Billing: 'Review your subscription, invoices, and payment details.',
Notifications: 'Choose which workspace updates you receive.',
Preferences: 'Set your display and workspace preferences.',
'Log out': 'This preview does not end a real session.',
};
const accountPage = computed(() => accountPages[selected.value]);
const utilityPage = computed(() => Boolean(accountPage.value) || (props.insetNavigation && secondaryLinks.some(link => link.label === selected.value)));
const searchOpen = ref(false);
const query = ref("");
const navigating = ref(false);
const searchFromDrawer = ref(false);
const sectionsOpen = ref(true);
const profiles = [
{ value: "ros", name: "Ros.Space", email: "[email protected]", initials: "RS", avatar: "/avatars/ros-space-astronaut.webp" },
{ value: "jordan", name: "Jordan Lee", email: "[email protected]", initials: "JL", avatar: "/avatars/mei-chen.webp" },
];
const account = computed(() => profiles.find(item => item.value === profile.value)!);
const links = [{ label: "Overview", icon: LayoutDashboard }, { label: "Projects", icon: Folder }, { label: "Reports", icon: FileText }];
const navigationGroups = [
{ label: "Workspace", icon: LayoutDashboard, items: ["Overview", "Reports"] },
{ label: "Mesh", icon: Box, items: ["Geometry", "Refinement", "Simulation"] },
{ label: "Settings", icon: Settings, items: ["General", "Members", "Integrations"] },
];
const openGroups = ref<Record<string, boolean>>({ Workspace: true, Mesh: false, Settings: false });
const searchItems = computed(() => richNavigation.value
? [...navigationGroups.flatMap(group => group.items), ...projects.value.map(project => project.name), ...(props.insetNavigation ? secondaryLinks.map(link => link.label) : [])]
: ["Overview", "Projects", "Reports", "Geometry", "Refinement", "Settings"]);
const projects = ref([
{ name: "Rotor study", code: "MESH-042", state: "Ready" },
{ name: "Channel flow", code: "MESH-041", state: "In review" },
{ name: "Heat exchanger", code: "MESH-039", state: "Draft" },
]);
const namespacePositioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
const desktopProfilePositioning = { placement: "right-end", strategy: "fixed", gutter: 8 } as const;
const mobileProfilePositioning = { placement: "top-start", strategy: "fixed", gutter: 8 } as const;
function navigate(value: string, context: SidebarContextValue) {
selected.value = value;
if (richNavigation.value) {
const group = navigationGroups.find(group => group.items.includes(value));
if (group) openGroups.value[group.label] = true;
}
context.setMobileOpen(false);
}
function search(context: SidebarContextValue) { query.value = ""; navigating.value = false; searchFromDrawer.value = context.isMobile && context.mobileOpen; searchOpen.value = true; }
function createProject() {
const name = `Untitled project ${projects.value.length - 2}`;
projects.value.push({ name, code: `MESH-${String(43 + projects.value.length - 3).padStart(3, "0")}`, state: "Draft" });
selected.value = name;
}
</script>
<template>
<div class="sidebar-block-demo" :data-sidebar-block="props.insetNavigation ? 'inset-navigation' : props.iconNavigation ? 'collapsible-icons' : props.variant" :data-standalone="props.standalone || undefined">
<SidebarLayout :variant="props.variant" :label="props.insetNavigation ? 'Inset application navigation' : props.iconNavigation ? 'Icon application navigation' : `${props.variant} application navigation`" :resizable="!richNavigation && props.variant === 'workspace'" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Namespaces" :positioning="richNavigation && !context.isMobile ? { placement: 'right-start', strategy: 'fixed', gutter: 8 } : namespacePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Namespace: ${namespace}`" :tooltip="`Namespace: ${namespace}`" class="sidebar-block-demo__identity-button sidebar-block-demo__namespace">
<template #icon><span class="sidebar-block-demo__namespace-icon"><Building2 aria-hidden="true" /></span></template>
<span class="sidebar-block-demo__identity"><strong>{{ namespace }}</strong><small>Namespace</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace">
<Dropdown.Label>Switch namespace</Dropdown.Label>
<Dropdown.RadioItem v-for="name in ['Engineering', 'Research', 'Personal']" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Menu class="sidebar-block-demo__search">
<Sidebar.MenuItem><Sidebar.MenuButton :icon="Search" tooltip="Quick search" class="sidebar-block-demo__search-button" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search(context)">Quick search …</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
<template v-if="richNavigation">
<Sidebar.Group>
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="group in navigationGroups" :key="group.label">
<Sidebar.Collapsible v-model:open="openGroups[group.label]">
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton :icon="group.icon" :tooltip="group.label" :active="group.items.includes(selected)" :aria-current="undefined" class="sidebar-block-demo__group-trigger">{{ group.label }}<Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub :aria-label="group.label">
<Sidebar.MenuSubItem v-for="item in group.items" :key="item"><Sidebar.MenuButton :active="selected === item" :href="`?view=${item.toLowerCase()}`" @click.prevent="navigate(item, context)">{{ item }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="project in projects" :key="project.code">
<Sidebar.MenuButton :icon="Folder" :tooltip="project.name" :active="selected === project.name" :href="`?project=${project.code}`" @click.prevent="navigate(project.name, context)">{{ project.name }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
<template v-else>
<Sidebar.Group>
<Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="link in links" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.Collapsible v-model:open="sectionsOpen">
<Sidebar.CollapsibleTrigger as-child><Sidebar.MenuButton :icon="Box" tooltip="Mesh">Mesh <Sidebar.MenuChevron /></Sidebar.MenuButton></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub>
<Sidebar.MenuSubItem v-for="name in ['Geometry', 'Refinement']" :key="name"><Sidebar.MenuButton :active="selected === name" :href="`?view=${name.toLowerCase()}`" @click.prevent="navigate(name, context)">{{ name }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Manage</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="Settings" tooltip="Settings" :active="selected === 'Settings'" href="?view=settings" @click.prevent="navigate('Settings', context)">Settings</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
</template>
<template #footer="context">
<nav v-if="props.insetNavigation" aria-label="Help and feedback" class="sidebar-block-demo__secondary">
<Sidebar.Menu><Sidebar.MenuItem v-for="link in secondaryLinks" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</nav>
<Dropdown.Root aria-label="Profile" :positioning="context.isMobile ? mobileProfilePositioning : desktopProfilePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Profile: ${account.name}`" :tooltip="account.name" class="sidebar-block-demo__identity-button sidebar-block-demo__profile">
<template #icon><Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root></template>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<div class="sidebar-block-demo__profile-summary">
<Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
</div>
<Dropdown.Separator />
<Dropdown.Group><Dropdown.Item value="upgrade" :icon="Sparkles" @select="navigate('Upgrade to Pro', context)">Upgrade to Pro</Dropdown.Item></Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Group>
<Dropdown.Item value="account" :icon="BadgeCheck" @select="navigate('Account', context)">Account</Dropdown.Item>
<Dropdown.Item value="billing" :icon="CreditCard" @select="navigate('Billing', context)">Billing</Dropdown.Item>
<Dropdown.Item value="notifications" :icon="Bell" @select="navigate('Notifications', context)">Notifications</Dropdown.Item>
<Dropdown.Item value="preferences" :icon="Settings" @select="navigate('Preferences', context)">Preferences</Dropdown.Item>
</Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Sub aria-label="Switch profile" :positioning="{ placement: 'right-end', strategy: 'fixed', gutter: 4, fitViewport: true, overflowPadding: 8 }">
<Dropdown.SubTrigger :icon="UsersRound">Switch profile</Dropdown.SubTrigger>
<Dropdown.Context v-slot="submenu"><Dropdown.SubContent :teleport="!context.isMobile" :inert="!submenu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<Dropdown.RadioGroup v-model="profile"><Dropdown.Label>Profiles</Dropdown.Label><Dropdown.RadioItem v-for="item in profiles" :key="item.value" :value="item.value" close-on-select>{{ item.name }}</Dropdown.RadioItem></Dropdown.RadioGroup>
</Dropdown.SubContent></Dropdown.Context>
</Dropdown.Sub>
<Dropdown.Separator />
<Dropdown.Item value="logout" :icon="LogOut" @select="navigate('Log out', context)">Log out</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #toolbar>
<span class="sidebar-block-demo__breadcrumb"><span>{{ namespace }}</span><span aria-hidden="true">/</span><span>{{ selected }}</span></span>
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :restore-focus="!navigating || !searchFromDrawer" aria-label="Search navigation" @select="item => { navigating = true; searchOpen = false; navigate(String(item), context); }">
<CommandPalette.Input placeholder="Search navigation…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<section class="sidebar-block-demo__page" :aria-labelledby="headingId">
<div class="sidebar-block-demo__page-heading">
<div><span class="sidebar-block-demo__eyebrow">{{ namespace }} workspace</span><h2 :id="headingId">{{ selected }}</h2></div>
<Button v-if="!utilityPage" size="sm" variant="secondary" @click="createProject"><Plus aria-hidden="true" />New project</Button>
</div>
<p v-if="utilityPage" class="sidebar-block-demo__description">{{ accountPage || (selected === 'Support' ? 'Find help with your workspace, geometry, and simulation setup.' : 'Share a suggestion or report an issue with your workspace.') }}</p>
<p v-else class="sidebar-block-demo__description">Review geometry, prepare meshes, and track project changes.</p>
<template v-if="!utilityPage">
<div class="sidebar-block-demo__section-heading"><h3>Recent projects</h3><span>{{ projects.length }} projects</span></div>
<Table compact>
<Table.Caption class="sidebar-block-demo__sr-only">Recent projects in {{ namespace }}</Table.Caption>
<Table.Header><Table.Row><Table.Head>Project</Table.Head><Table.Head>Status</Table.Head></Table.Row></Table.Header>
<Table.Body><Table.Row v-for="item in projects" :key="item.code">
<Table.Cell><button type="button" class="sidebar-block-demo__project-link" @click="selected = item.name">{{ item.name }}</button><span class="sidebar-block-demo__project-code">{{ item.code }}</span></Table.Cell>
<Table.Cell><span class="sidebar-block-demo__status" :data-ready="item.state === 'Ready' || undefined">{{ item.state }}</span></Table.Cell>
</Table.Row></Table.Body>
</Table>
</template>
<p v-if="accountPage" class="sidebar-block-demo__note" role="status">Local preview only. No account, payment, notification, or session is changed.</p>
<p v-else-if="utilityPage" class="sidebar-block-demo__note">This is a local navigation preview. Connect this page to your application's {{ selected === 'Support' ? 'help resources' : 'feedback form' }}. No message is sent.</p>
<p v-else class="sidebar-block-demo__note" role="status">{{ projects.length > 3 ? 'New projects added to this example.' : 'Example data. Changes stay in this preview.' }}</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-block-demo { inline-size: 100%; block-size: 33rem; min-inline-size: 0; text-align: start; }
.sidebar-block-demo[data-sidebar-block="collapsible-icons"] { block-size: 38rem; }
.sidebar-block-demo[data-sidebar-block="inset-navigation"] { block-size: 42rem; }
.sidebar-block-demo[data-standalone] { block-size: 100svh; }
.sidebar-block-demo .kappa-sidebar-layout { border: 1px solid var(--kappa-line); border-radius: 0.625rem; overflow: hidden; }
.sidebar-block-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.sidebar-block-demo__identity-button { padding-block: 0.25rem; }
.sidebar-block-demo__identity-button .kappa-sidebar__menu-label { position: relative; padding-inline-end: 1.25rem; }
.sidebar-block-demo__identity { display: flex; flex-direction: column; min-inline-size: 0; gap: 0.125rem; }
.sidebar-block-demo__identity strong { font-size: 0.8125rem; line-height: 1.125rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__identity small { font-size: 0.6875rem; line-height: 1rem; color: var(--kappa-subtle); overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 0.875rem; block-size: 0.875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__namespace-icon { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; background: var(--kappa-accent-solid); color: var(--kappa-accent-contrast); }
.sidebar-block-demo__namespace-icon svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace .kappa-sidebar__menu-label { padding-inline-end: 1.5rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace[data-state="open"] { background: var(--kappa-tint); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button:focus-visible { outline-offset: 2px; }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__namespace { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__avatar.kappa-avatar { --kappa-avatar-size: 2rem; border-radius: 0.5rem; overflow: hidden; }
.sidebar-block-demo__profile > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__profile.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity { gap: 0; }
.sidebar-block-demo__profile .sidebar-block-demo__identity strong, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1.125rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity small, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__profile .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__profile[data-state="open"] { background: var(--kappa-overlay); }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__profile { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__profile-menu.kappa-dropdown__content { inline-size: max(14rem, var(--reference-width, 14rem)); border-radius: 0.5rem; }
.sidebar-block-demo__profile-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.25rem; }
.sidebar-block-demo__profile-summary .sidebar-block-demo__identity { flex: 1; }
.sidebar-block-demo__profile-menu[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-block-demo__secondary { margin-block-end: 0.5rem; }
.kappa-sidebar__footer:has(> .sidebar-block-demo__secondary) { flex-direction: column; align-items: stretch; }
.sidebar-block-demo__search { margin-block-end: 0.5rem; }
.sidebar-block-demo__group-trigger .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button { min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.375rem; background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-default); }
.sidebar-block-demo__search-button > .kappa-sidebar__menu-icon { color: var(--kappa-subtle); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:hover { border-color: var(--kappa-subtle); background: var(--kappa-tint); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:focus-visible { border-color: var(--kappa-focus); outline-offset: 2px; }
.sidebar-block-demo__breadcrumb { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-block-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-block-demo__breadcrumb span:first-child { color: var(--kappa-subtle); }
.sidebar-block-demo__page { padding: 1.5rem; max-inline-size: 62rem; margin-inline: auto; }
.sidebar-block-demo__page-heading { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 1rem; }
.sidebar-block-demo__page-heading h2 { margin: 0.375rem 0 0 !important; color: var(--kappa-default); font-size: 1.25rem !important; line-height: 1.5rem !important; letter-spacing: -0.025em; font-weight: 550; }
.sidebar-block-demo__page-heading button svg { inline-size: 0.875rem; block-size: 0.875rem; }
.sidebar-block-demo__eyebrow { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__description { margin: 0.75rem 0 1.5rem !important; font-size: 0.8125rem !important; line-height: 1.4; color: var(--kappa-subtle); }
.sidebar-block-demo__section-heading { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-block-end: 0.625rem; }
.sidebar-block-demo__section-heading h3 { margin: 0 !important; font-size: 0.8125rem !important; line-height: 1.25rem !important; font-weight: 550; }
.sidebar-block-demo__section-heading > span { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__project-link { display: block; margin: 0; padding: 0; border: 0; background: transparent; color: var(--kappa-default); cursor: pointer; font: inherit; text-align: start; }
.sidebar-block-demo__project-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.sidebar-block-demo__project-link:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 3px; }
.sidebar-block-demo__project-code { display: block; font-size: 0.625rem; line-height: 1rem; color: var(--kappa-subtle); }
.sidebar-block-demo__status { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem; white-space: nowrap; }
.sidebar-block-demo__status::before { content: ""; inline-size: 0.3125rem; block-size: 0.3125rem; border-radius: 50%; background: var(--kappa-subtle); }
.sidebar-block-demo__status[data-ready]::before { background: var(--kappa-success-text); }
.sidebar-block-demo__note { margin: 1rem 0 0 !important; font-size: 0.6875rem !important; color: var(--kappa-subtle); }
.sidebar-block-demo__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 767px) {
.sidebar-block-demo:not([data-standalone]) { block-size: 30rem; }
.sidebar-block-demo__page { padding: 1rem; }
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :first-child,
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :nth-child(2) { display: none; }
}
</style>Inset Content
A quiet navigation surface beside a bordered, inset content panel.
<script setup lang="ts">
import { computed, ref, useId } from "vue";
import { SidebarLayout, type SidebarLayoutVariant } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { Button } from "@dicehub/kappa/components/button";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Avatar } from "@dicehub/kappa/components/avatar";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Table } from "@dicehub/kappa/components/table";
import { Building2, ChevronsUpDown, Folder, LayoutDashboard, FileText, Settings, Search, Plus, Box, CircleHelp, MessageSquare, BadgeCheck, Bell, CreditCard, LogOut, Sparkles, UsersRound } from "@lucide/vue";
const props = withDefaults(defineProps<{ variant?: SidebarLayoutVariant; standalone?: boolean; iconNavigation?: boolean; insetNavigation?: boolean }>(), { variant: "inset", standalone: true, iconNavigation: false, insetNavigation: false });
const richNavigation = computed(() => props.iconNavigation || props.insetNavigation);
const headingId = useId();
const namespace = ref("Engineering");
const profile = ref("ros");
const selected = ref("Overview");
const secondaryLinks = [{ label: "Support", icon: CircleHelp }, { label: "Feedback", icon: MessageSquare }];
const accountPages: Record<string, string> = {
'Upgrade to Pro': 'Review the plans available for your workspace.',
Account: 'Manage your profile and account details.',
Billing: 'Review your subscription, invoices, and payment details.',
Notifications: 'Choose which workspace updates you receive.',
Preferences: 'Set your display and workspace preferences.',
'Log out': 'This preview does not end a real session.',
};
const accountPage = computed(() => accountPages[selected.value]);
const utilityPage = computed(() => Boolean(accountPage.value) || (props.insetNavigation && secondaryLinks.some(link => link.label === selected.value)));
const searchOpen = ref(false);
const query = ref("");
const navigating = ref(false);
const searchFromDrawer = ref(false);
const sectionsOpen = ref(true);
const profiles = [
{ value: "ros", name: "Ros.Space", email: "[email protected]", initials: "RS", avatar: "/avatars/ros-space-astronaut.webp" },
{ value: "jordan", name: "Jordan Lee", email: "[email protected]", initials: "JL", avatar: "/avatars/mei-chen.webp" },
];
const account = computed(() => profiles.find(item => item.value === profile.value)!);
const links = [{ label: "Overview", icon: LayoutDashboard }, { label: "Projects", icon: Folder }, { label: "Reports", icon: FileText }];
const navigationGroups = [
{ label: "Workspace", icon: LayoutDashboard, items: ["Overview", "Reports"] },
{ label: "Mesh", icon: Box, items: ["Geometry", "Refinement", "Simulation"] },
{ label: "Settings", icon: Settings, items: ["General", "Members", "Integrations"] },
];
const openGroups = ref<Record<string, boolean>>({ Workspace: true, Mesh: false, Settings: false });
const searchItems = computed(() => richNavigation.value
? [...navigationGroups.flatMap(group => group.items), ...projects.value.map(project => project.name), ...(props.insetNavigation ? secondaryLinks.map(link => link.label) : [])]
: ["Overview", "Projects", "Reports", "Geometry", "Refinement", "Settings"]);
const projects = ref([
{ name: "Rotor study", code: "MESH-042", state: "Ready" },
{ name: "Channel flow", code: "MESH-041", state: "In review" },
{ name: "Heat exchanger", code: "MESH-039", state: "Draft" },
]);
const namespacePositioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
const desktopProfilePositioning = { placement: "right-end", strategy: "fixed", gutter: 8 } as const;
const mobileProfilePositioning = { placement: "top-start", strategy: "fixed", gutter: 8 } as const;
function navigate(value: string, context: SidebarContextValue) {
selected.value = value;
if (richNavigation.value) {
const group = navigationGroups.find(group => group.items.includes(value));
if (group) openGroups.value[group.label] = true;
}
context.setMobileOpen(false);
}
function search(context: SidebarContextValue) { query.value = ""; navigating.value = false; searchFromDrawer.value = context.isMobile && context.mobileOpen; searchOpen.value = true; }
function createProject() {
const name = `Untitled project ${projects.value.length - 2}`;
projects.value.push({ name, code: `MESH-${String(43 + projects.value.length - 3).padStart(3, "0")}`, state: "Draft" });
selected.value = name;
}
</script>
<template>
<div class="sidebar-block-demo" :data-sidebar-block="props.insetNavigation ? 'inset-navigation' : props.iconNavigation ? 'collapsible-icons' : props.variant" :data-standalone="props.standalone || undefined">
<SidebarLayout :variant="props.variant" :label="props.insetNavigation ? 'Inset application navigation' : props.iconNavigation ? 'Icon application navigation' : `${props.variant} application navigation`" :resizable="!richNavigation && props.variant === 'workspace'" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Namespaces" :positioning="richNavigation && !context.isMobile ? { placement: 'right-start', strategy: 'fixed', gutter: 8 } : namespacePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Namespace: ${namespace}`" :tooltip="`Namespace: ${namespace}`" class="sidebar-block-demo__identity-button sidebar-block-demo__namespace">
<template #icon><span class="sidebar-block-demo__namespace-icon"><Building2 aria-hidden="true" /></span></template>
<span class="sidebar-block-demo__identity"><strong>{{ namespace }}</strong><small>Namespace</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace">
<Dropdown.Label>Switch namespace</Dropdown.Label>
<Dropdown.RadioItem v-for="name in ['Engineering', 'Research', 'Personal']" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Menu class="sidebar-block-demo__search">
<Sidebar.MenuItem><Sidebar.MenuButton :icon="Search" tooltip="Quick search" class="sidebar-block-demo__search-button" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search(context)">Quick search …</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
<template v-if="richNavigation">
<Sidebar.Group>
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="group in navigationGroups" :key="group.label">
<Sidebar.Collapsible v-model:open="openGroups[group.label]">
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton :icon="group.icon" :tooltip="group.label" :active="group.items.includes(selected)" :aria-current="undefined" class="sidebar-block-demo__group-trigger">{{ group.label }}<Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub :aria-label="group.label">
<Sidebar.MenuSubItem v-for="item in group.items" :key="item"><Sidebar.MenuButton :active="selected === item" :href="`?view=${item.toLowerCase()}`" @click.prevent="navigate(item, context)">{{ item }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="project in projects" :key="project.code">
<Sidebar.MenuButton :icon="Folder" :tooltip="project.name" :active="selected === project.name" :href="`?project=${project.code}`" @click.prevent="navigate(project.name, context)">{{ project.name }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
<template v-else>
<Sidebar.Group>
<Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="link in links" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.Collapsible v-model:open="sectionsOpen">
<Sidebar.CollapsibleTrigger as-child><Sidebar.MenuButton :icon="Box" tooltip="Mesh">Mesh <Sidebar.MenuChevron /></Sidebar.MenuButton></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub>
<Sidebar.MenuSubItem v-for="name in ['Geometry', 'Refinement']" :key="name"><Sidebar.MenuButton :active="selected === name" :href="`?view=${name.toLowerCase()}`" @click.prevent="navigate(name, context)">{{ name }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Manage</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="Settings" tooltip="Settings" :active="selected === 'Settings'" href="?view=settings" @click.prevent="navigate('Settings', context)">Settings</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
</template>
<template #footer="context">
<nav v-if="props.insetNavigation" aria-label="Help and feedback" class="sidebar-block-demo__secondary">
<Sidebar.Menu><Sidebar.MenuItem v-for="link in secondaryLinks" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</nav>
<Dropdown.Root aria-label="Profile" :positioning="context.isMobile ? mobileProfilePositioning : desktopProfilePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Profile: ${account.name}`" :tooltip="account.name" class="sidebar-block-demo__identity-button sidebar-block-demo__profile">
<template #icon><Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root></template>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<div class="sidebar-block-demo__profile-summary">
<Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
</div>
<Dropdown.Separator />
<Dropdown.Group><Dropdown.Item value="upgrade" :icon="Sparkles" @select="navigate('Upgrade to Pro', context)">Upgrade to Pro</Dropdown.Item></Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Group>
<Dropdown.Item value="account" :icon="BadgeCheck" @select="navigate('Account', context)">Account</Dropdown.Item>
<Dropdown.Item value="billing" :icon="CreditCard" @select="navigate('Billing', context)">Billing</Dropdown.Item>
<Dropdown.Item value="notifications" :icon="Bell" @select="navigate('Notifications', context)">Notifications</Dropdown.Item>
<Dropdown.Item value="preferences" :icon="Settings" @select="navigate('Preferences', context)">Preferences</Dropdown.Item>
</Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Sub aria-label="Switch profile" :positioning="{ placement: 'right-end', strategy: 'fixed', gutter: 4, fitViewport: true, overflowPadding: 8 }">
<Dropdown.SubTrigger :icon="UsersRound">Switch profile</Dropdown.SubTrigger>
<Dropdown.Context v-slot="submenu"><Dropdown.SubContent :teleport="!context.isMobile" :inert="!submenu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<Dropdown.RadioGroup v-model="profile"><Dropdown.Label>Profiles</Dropdown.Label><Dropdown.RadioItem v-for="item in profiles" :key="item.value" :value="item.value" close-on-select>{{ item.name }}</Dropdown.RadioItem></Dropdown.RadioGroup>
</Dropdown.SubContent></Dropdown.Context>
</Dropdown.Sub>
<Dropdown.Separator />
<Dropdown.Item value="logout" :icon="LogOut" @select="navigate('Log out', context)">Log out</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #toolbar>
<span class="sidebar-block-demo__breadcrumb"><span>{{ namespace }}</span><span aria-hidden="true">/</span><span>{{ selected }}</span></span>
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :restore-focus="!navigating || !searchFromDrawer" aria-label="Search navigation" @select="item => { navigating = true; searchOpen = false; navigate(String(item), context); }">
<CommandPalette.Input placeholder="Search navigation…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<section class="sidebar-block-demo__page" :aria-labelledby="headingId">
<div class="sidebar-block-demo__page-heading">
<div><span class="sidebar-block-demo__eyebrow">{{ namespace }} workspace</span><h2 :id="headingId">{{ selected }}</h2></div>
<Button v-if="!utilityPage" size="sm" variant="secondary" @click="createProject"><Plus aria-hidden="true" />New project</Button>
</div>
<p v-if="utilityPage" class="sidebar-block-demo__description">{{ accountPage || (selected === 'Support' ? 'Find help with your workspace, geometry, and simulation setup.' : 'Share a suggestion or report an issue with your workspace.') }}</p>
<p v-else class="sidebar-block-demo__description">Review geometry, prepare meshes, and track project changes.</p>
<template v-if="!utilityPage">
<div class="sidebar-block-demo__section-heading"><h3>Recent projects</h3><span>{{ projects.length }} projects</span></div>
<Table compact>
<Table.Caption class="sidebar-block-demo__sr-only">Recent projects in {{ namespace }}</Table.Caption>
<Table.Header><Table.Row><Table.Head>Project</Table.Head><Table.Head>Status</Table.Head></Table.Row></Table.Header>
<Table.Body><Table.Row v-for="item in projects" :key="item.code">
<Table.Cell><button type="button" class="sidebar-block-demo__project-link" @click="selected = item.name">{{ item.name }}</button><span class="sidebar-block-demo__project-code">{{ item.code }}</span></Table.Cell>
<Table.Cell><span class="sidebar-block-demo__status" :data-ready="item.state === 'Ready' || undefined">{{ item.state }}</span></Table.Cell>
</Table.Row></Table.Body>
</Table>
</template>
<p v-if="accountPage" class="sidebar-block-demo__note" role="status">Local preview only. No account, payment, notification, or session is changed.</p>
<p v-else-if="utilityPage" class="sidebar-block-demo__note">This is a local navigation preview. Connect this page to your application's {{ selected === 'Support' ? 'help resources' : 'feedback form' }}. No message is sent.</p>
<p v-else class="sidebar-block-demo__note" role="status">{{ projects.length > 3 ? 'New projects added to this example.' : 'Example data. Changes stay in this preview.' }}</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-block-demo { inline-size: 100%; block-size: 33rem; min-inline-size: 0; text-align: start; }
.sidebar-block-demo[data-sidebar-block="collapsible-icons"] { block-size: 38rem; }
.sidebar-block-demo[data-sidebar-block="inset-navigation"] { block-size: 42rem; }
.sidebar-block-demo[data-standalone] { block-size: 100svh; }
.sidebar-block-demo .kappa-sidebar-layout { border: 1px solid var(--kappa-line); border-radius: 0.625rem; overflow: hidden; }
.sidebar-block-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.sidebar-block-demo__identity-button { padding-block: 0.25rem; }
.sidebar-block-demo__identity-button .kappa-sidebar__menu-label { position: relative; padding-inline-end: 1.25rem; }
.sidebar-block-demo__identity { display: flex; flex-direction: column; min-inline-size: 0; gap: 0.125rem; }
.sidebar-block-demo__identity strong { font-size: 0.8125rem; line-height: 1.125rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__identity small { font-size: 0.6875rem; line-height: 1rem; color: var(--kappa-subtle); overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 0.875rem; block-size: 0.875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__namespace-icon { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; background: var(--kappa-accent-solid); color: var(--kappa-accent-contrast); }
.sidebar-block-demo__namespace-icon svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace .kappa-sidebar__menu-label { padding-inline-end: 1.5rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace[data-state="open"] { background: var(--kappa-tint); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button:focus-visible { outline-offset: 2px; }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__namespace { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__avatar.kappa-avatar { --kappa-avatar-size: 2rem; border-radius: 0.5rem; overflow: hidden; }
.sidebar-block-demo__profile > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__profile.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity { gap: 0; }
.sidebar-block-demo__profile .sidebar-block-demo__identity strong, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1.125rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity small, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__profile .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__profile[data-state="open"] { background: var(--kappa-overlay); }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__profile { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__profile-menu.kappa-dropdown__content { inline-size: max(14rem, var(--reference-width, 14rem)); border-radius: 0.5rem; }
.sidebar-block-demo__profile-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.25rem; }
.sidebar-block-demo__profile-summary .sidebar-block-demo__identity { flex: 1; }
.sidebar-block-demo__profile-menu[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-block-demo__secondary { margin-block-end: 0.5rem; }
.kappa-sidebar__footer:has(> .sidebar-block-demo__secondary) { flex-direction: column; align-items: stretch; }
.sidebar-block-demo__search { margin-block-end: 0.5rem; }
.sidebar-block-demo__group-trigger .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button { min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.375rem; background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-default); }
.sidebar-block-demo__search-button > .kappa-sidebar__menu-icon { color: var(--kappa-subtle); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:hover { border-color: var(--kappa-subtle); background: var(--kappa-tint); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:focus-visible { border-color: var(--kappa-focus); outline-offset: 2px; }
.sidebar-block-demo__breadcrumb { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-block-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-block-demo__breadcrumb span:first-child { color: var(--kappa-subtle); }
.sidebar-block-demo__page { padding: 1.5rem; max-inline-size: 62rem; margin-inline: auto; }
.sidebar-block-demo__page-heading { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 1rem; }
.sidebar-block-demo__page-heading h2 { margin: 0.375rem 0 0 !important; color: var(--kappa-default); font-size: 1.25rem !important; line-height: 1.5rem !important; letter-spacing: -0.025em; font-weight: 550; }
.sidebar-block-demo__page-heading button svg { inline-size: 0.875rem; block-size: 0.875rem; }
.sidebar-block-demo__eyebrow { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__description { margin: 0.75rem 0 1.5rem !important; font-size: 0.8125rem !important; line-height: 1.4; color: var(--kappa-subtle); }
.sidebar-block-demo__section-heading { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-block-end: 0.625rem; }
.sidebar-block-demo__section-heading h3 { margin: 0 !important; font-size: 0.8125rem !important; line-height: 1.25rem !important; font-weight: 550; }
.sidebar-block-demo__section-heading > span { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__project-link { display: block; margin: 0; padding: 0; border: 0; background: transparent; color: var(--kappa-default); cursor: pointer; font: inherit; text-align: start; }
.sidebar-block-demo__project-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.sidebar-block-demo__project-link:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 3px; }
.sidebar-block-demo__project-code { display: block; font-size: 0.625rem; line-height: 1rem; color: var(--kappa-subtle); }
.sidebar-block-demo__status { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem; white-space: nowrap; }
.sidebar-block-demo__status::before { content: ""; inline-size: 0.3125rem; block-size: 0.3125rem; border-radius: 50%; background: var(--kappa-subtle); }
.sidebar-block-demo__status[data-ready]::before { background: var(--kappa-success-text); }
.sidebar-block-demo__note { margin: 1rem 0 0 !important; font-size: 0.6875rem !important; color: var(--kappa-subtle); }
.sidebar-block-demo__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 767px) {
.sidebar-block-demo:not([data-standalone]) { block-size: 30rem; }
.sidebar-block-demo__page { padding: 1rem; }
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :first-child,
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :nth-child(2) { display: none; }
}
</style>Floating Sidebar
A separate navigation panel with a small outer gap and rounded edges.
<script setup lang="ts">
import { computed, ref, useId } from "vue";
import { SidebarLayout, type SidebarLayoutVariant } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar, type SidebarContextValue } from "@dicehub/kappa/components/sidebar";
import { Button } from "@dicehub/kappa/components/button";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { Avatar } from "@dicehub/kappa/components/avatar";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Table } from "@dicehub/kappa/components/table";
import { Building2, ChevronsUpDown, Folder, LayoutDashboard, FileText, Settings, Search, Plus, Box, CircleHelp, MessageSquare, BadgeCheck, Bell, CreditCard, LogOut, Sparkles, UsersRound } from "@lucide/vue";
const props = withDefaults(defineProps<{ variant?: SidebarLayoutVariant; standalone?: boolean; iconNavigation?: boolean; insetNavigation?: boolean }>(), { variant: "floating", standalone: true, iconNavigation: false, insetNavigation: false });
const richNavigation = computed(() => props.iconNavigation || props.insetNavigation);
const headingId = useId();
const namespace = ref("Engineering");
const profile = ref("ros");
const selected = ref("Overview");
const secondaryLinks = [{ label: "Support", icon: CircleHelp }, { label: "Feedback", icon: MessageSquare }];
const accountPages: Record<string, string> = {
'Upgrade to Pro': 'Review the plans available for your workspace.',
Account: 'Manage your profile and account details.',
Billing: 'Review your subscription, invoices, and payment details.',
Notifications: 'Choose which workspace updates you receive.',
Preferences: 'Set your display and workspace preferences.',
'Log out': 'This preview does not end a real session.',
};
const accountPage = computed(() => accountPages[selected.value]);
const utilityPage = computed(() => Boolean(accountPage.value) || (props.insetNavigation && secondaryLinks.some(link => link.label === selected.value)));
const searchOpen = ref(false);
const query = ref("");
const navigating = ref(false);
const searchFromDrawer = ref(false);
const sectionsOpen = ref(true);
const profiles = [
{ value: "ros", name: "Ros.Space", email: "[email protected]", initials: "RS", avatar: "/avatars/ros-space-astronaut.webp" },
{ value: "jordan", name: "Jordan Lee", email: "[email protected]", initials: "JL", avatar: "/avatars/mei-chen.webp" },
];
const account = computed(() => profiles.find(item => item.value === profile.value)!);
const links = [{ label: "Overview", icon: LayoutDashboard }, { label: "Projects", icon: Folder }, { label: "Reports", icon: FileText }];
const navigationGroups = [
{ label: "Workspace", icon: LayoutDashboard, items: ["Overview", "Reports"] },
{ label: "Mesh", icon: Box, items: ["Geometry", "Refinement", "Simulation"] },
{ label: "Settings", icon: Settings, items: ["General", "Members", "Integrations"] },
];
const openGroups = ref<Record<string, boolean>>({ Workspace: true, Mesh: false, Settings: false });
const searchItems = computed(() => richNavigation.value
? [...navigationGroups.flatMap(group => group.items), ...projects.value.map(project => project.name), ...(props.insetNavigation ? secondaryLinks.map(link => link.label) : [])]
: ["Overview", "Projects", "Reports", "Geometry", "Refinement", "Settings"]);
const projects = ref([
{ name: "Rotor study", code: "MESH-042", state: "Ready" },
{ name: "Channel flow", code: "MESH-041", state: "In review" },
{ name: "Heat exchanger", code: "MESH-039", state: "Draft" },
]);
const namespacePositioning = { placement: "bottom-start", strategy: "fixed", gutter: 6 } as const;
const desktopProfilePositioning = { placement: "right-end", strategy: "fixed", gutter: 8 } as const;
const mobileProfilePositioning = { placement: "top-start", strategy: "fixed", gutter: 8 } as const;
function navigate(value: string, context: SidebarContextValue) {
selected.value = value;
if (richNavigation.value) {
const group = navigationGroups.find(group => group.items.includes(value));
if (group) openGroups.value[group.label] = true;
}
context.setMobileOpen(false);
}
function search(context: SidebarContextValue) { query.value = ""; navigating.value = false; searchFromDrawer.value = context.isMobile && context.mobileOpen; searchOpen.value = true; }
function createProject() {
const name = `Untitled project ${projects.value.length - 2}`;
projects.value.push({ name, code: `MESH-${String(43 + projects.value.length - 3).padStart(3, "0")}`, state: "Draft" });
selected.value = name;
}
</script>
<template>
<div class="sidebar-block-demo" :data-sidebar-block="props.insetNavigation ? 'inset-navigation' : props.iconNavigation ? 'collapsible-icons' : props.variant" :data-standalone="props.standalone || undefined">
<SidebarLayout :variant="props.variant" :label="props.insetNavigation ? 'Inset application navigation' : props.iconNavigation ? 'Icon application navigation' : `${props.variant} application navigation`" :resizable="!richNavigation && props.variant === 'workspace'" full-screen-on-mobile>
<template #header="context">
<Dropdown.Root aria-label="Namespaces" :positioning="richNavigation && !context.isMobile ? { placement: 'right-start', strategy: 'fixed', gutter: 8 } : namespacePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Namespace: ${namespace}`" :tooltip="`Namespace: ${namespace}`" class="sidebar-block-demo__identity-button sidebar-block-demo__namespace">
<template #icon><span class="sidebar-block-demo__namespace-icon"><Building2 aria-hidden="true" /></span></template>
<span class="sidebar-block-demo__identity"><strong>{{ namespace }}</strong><small>Namespace</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined">
<Dropdown.RadioGroup v-model="namespace">
<Dropdown.Label>Switch namespace</Dropdown.Label>
<Dropdown.RadioItem v-for="name in ['Engineering', 'Research', 'Personal']" :key="name" :value="name" close-on-select>{{ name }}</Dropdown.RadioItem>
</Dropdown.RadioGroup>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #navigation="context">
<Sidebar.Menu class="sidebar-block-demo__search">
<Sidebar.MenuItem><Sidebar.MenuButton :icon="Search" tooltip="Quick search" class="sidebar-block-demo__search-button" aria-haspopup="dialog" :aria-expanded="searchOpen" @click="search(context)">Quick search …</Sidebar.MenuButton></Sidebar.MenuItem>
</Sidebar.Menu>
<template v-if="richNavigation">
<Sidebar.Group>
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="group in navigationGroups" :key="group.label">
<Sidebar.Collapsible v-model:open="openGroups[group.label]">
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton :icon="group.icon" :tooltip="group.label" :active="group.items.includes(selected)" :aria-current="undefined" class="sidebar-block-demo__group-trigger">{{ group.label }}<Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub :aria-label="group.label">
<Sidebar.MenuSubItem v-for="item in group.items" :key="item"><Sidebar.MenuButton :active="selected === item" :href="`?view=${item.toLowerCase()}`" @click.prevent="navigate(item, context)">{{ item }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem v-for="project in projects" :key="project.code">
<Sidebar.MenuButton :icon="Folder" :tooltip="project.name" :active="selected === project.name" :href="`?project=${project.code}`" @click.prevent="navigate(project.name, context)">{{ project.name }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
<template v-else>
<Sidebar.Group>
<Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="link in links" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.Collapsible v-model:open="sectionsOpen">
<Sidebar.CollapsibleTrigger as-child><Sidebar.MenuButton :icon="Box" tooltip="Mesh">Mesh <Sidebar.MenuChevron /></Sidebar.MenuButton></Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent><Sidebar.MenuSub>
<Sidebar.MenuSubItem v-for="name in ['Geometry', 'Refinement']" :key="name"><Sidebar.MenuButton :active="selected === name" :href="`?view=${name.toLowerCase()}`" @click.prevent="navigate(name, context)">{{ name }}</Sidebar.MenuButton></Sidebar.MenuSubItem>
</Sidebar.MenuSub></Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Manage</Sidebar.GroupLabel>
<Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="Settings" tooltip="Settings" :active="selected === 'Settings'" href="?view=settings" @click.prevent="navigate('Settings', context)">Settings</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Group>
</template>
</template>
<template #footer="context">
<nav v-if="props.insetNavigation" aria-label="Help and feedback" class="sidebar-block-demo__secondary">
<Sidebar.Menu><Sidebar.MenuItem v-for="link in secondaryLinks" :key="link.label">
<Sidebar.MenuButton :icon="link.icon" :tooltip="link.label" :active="selected === link.label" :href="`?view=${link.label.toLowerCase()}`" @click.prevent="navigate(link.label, context)">{{ link.label }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</nav>
<Dropdown.Root aria-label="Profile" :positioning="context.isMobile ? mobileProfilePositioning : desktopProfilePositioning">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :aria-label="`Profile: ${account.name}`" :tooltip="account.name" class="sidebar-block-demo__identity-button sidebar-block-demo__profile">
<template #icon><Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root></template>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
<ChevronsUpDown class="sidebar-block-demo__chevrons" aria-hidden="true" />
</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Context v-slot="menu"><Dropdown.Content :teleport="!context.isMobile" :inert="!menu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<div class="sidebar-block-demo__profile-summary">
<Avatar.Root class="sidebar-block-demo__avatar" aria-hidden="true"><Avatar.Image :src="account.avatar" alt="" /><Avatar.Fallback>{{ account.initials }}</Avatar.Fallback></Avatar.Root>
<span class="sidebar-block-demo__identity"><strong>{{ account.name }}</strong><small>{{ account.email }}</small></span>
</div>
<Dropdown.Separator />
<Dropdown.Group><Dropdown.Item value="upgrade" :icon="Sparkles" @select="navigate('Upgrade to Pro', context)">Upgrade to Pro</Dropdown.Item></Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Group>
<Dropdown.Item value="account" :icon="BadgeCheck" @select="navigate('Account', context)">Account</Dropdown.Item>
<Dropdown.Item value="billing" :icon="CreditCard" @select="navigate('Billing', context)">Billing</Dropdown.Item>
<Dropdown.Item value="notifications" :icon="Bell" @select="navigate('Notifications', context)">Notifications</Dropdown.Item>
<Dropdown.Item value="preferences" :icon="Settings" @select="navigate('Preferences', context)">Preferences</Dropdown.Item>
</Dropdown.Group>
<Dropdown.Separator />
<Dropdown.Sub aria-label="Switch profile" :positioning="{ placement: 'right-end', strategy: 'fixed', gutter: 4, fitViewport: true, overflowPadding: 8 }">
<Dropdown.SubTrigger :icon="UsersRound">Switch profile</Dropdown.SubTrigger>
<Dropdown.Context v-slot="submenu"><Dropdown.SubContent :teleport="!context.isMobile" :inert="!submenu.open || undefined" class="sidebar-block-demo__profile-menu" :data-mobile="context.isMobile || undefined">
<Dropdown.RadioGroup v-model="profile"><Dropdown.Label>Profiles</Dropdown.Label><Dropdown.RadioItem v-for="item in profiles" :key="item.value" :value="item.value" close-on-select>{{ item.name }}</Dropdown.RadioItem></Dropdown.RadioGroup>
</Dropdown.SubContent></Dropdown.Context>
</Dropdown.Sub>
<Dropdown.Separator />
<Dropdown.Item value="logout" :icon="LogOut" @select="navigate('Log out', context)">Log out</Dropdown.Item>
</Dropdown.Content></Dropdown.Context>
</Dropdown.Root>
</template>
<template #toolbar>
<span class="sidebar-block-demo__breadcrumb"><span>{{ namespace }}</span><span aria-hidden="true">/</span><span>{{ selected }}</span></span>
</template>
<template #default="context">
<CommandPalette.Root v-model:open="searchOpen" v-model:value="query" :items="searchItems" :restore-focus="!navigating || !searchFromDrawer" aria-label="Search navigation" @select="item => { navigating = true; searchOpen = false; navigate(String(item), context); }">
<CommandPalette.Input placeholder="Search navigation…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }"><CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item></CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer><span>↑↓ Navigate</span><span>Enter Open</span></CommandPalette.Footer>
</CommandPalette.Root>
<section class="sidebar-block-demo__page" :aria-labelledby="headingId">
<div class="sidebar-block-demo__page-heading">
<div><span class="sidebar-block-demo__eyebrow">{{ namespace }} workspace</span><h2 :id="headingId">{{ selected }}</h2></div>
<Button v-if="!utilityPage" size="sm" variant="secondary" @click="createProject"><Plus aria-hidden="true" />New project</Button>
</div>
<p v-if="utilityPage" class="sidebar-block-demo__description">{{ accountPage || (selected === 'Support' ? 'Find help with your workspace, geometry, and simulation setup.' : 'Share a suggestion or report an issue with your workspace.') }}</p>
<p v-else class="sidebar-block-demo__description">Review geometry, prepare meshes, and track project changes.</p>
<template v-if="!utilityPage">
<div class="sidebar-block-demo__section-heading"><h3>Recent projects</h3><span>{{ projects.length }} projects</span></div>
<Table compact>
<Table.Caption class="sidebar-block-demo__sr-only">Recent projects in {{ namespace }}</Table.Caption>
<Table.Header><Table.Row><Table.Head>Project</Table.Head><Table.Head>Status</Table.Head></Table.Row></Table.Header>
<Table.Body><Table.Row v-for="item in projects" :key="item.code">
<Table.Cell><button type="button" class="sidebar-block-demo__project-link" @click="selected = item.name">{{ item.name }}</button><span class="sidebar-block-demo__project-code">{{ item.code }}</span></Table.Cell>
<Table.Cell><span class="sidebar-block-demo__status" :data-ready="item.state === 'Ready' || undefined">{{ item.state }}</span></Table.Cell>
</Table.Row></Table.Body>
</Table>
</template>
<p v-if="accountPage" class="sidebar-block-demo__note" role="status">Local preview only. No account, payment, notification, or session is changed.</p>
<p v-else-if="utilityPage" class="sidebar-block-demo__note">This is a local navigation preview. Connect this page to your application's {{ selected === 'Support' ? 'help resources' : 'feedback form' }}. No message is sent.</p>
<p v-else class="sidebar-block-demo__note" role="status">{{ projects.length > 3 ? 'New projects added to this example.' : 'Example data. Changes stay in this preview.' }}</p>
</section>
</template>
</SidebarLayout>
</div>
</template>
<style>
.sidebar-block-demo { inline-size: 100%; block-size: 33rem; min-inline-size: 0; text-align: start; }
.sidebar-block-demo[data-sidebar-block="collapsible-icons"] { block-size: 38rem; }
.sidebar-block-demo[data-sidebar-block="inset-navigation"] { block-size: 42rem; }
.sidebar-block-demo[data-standalone] { block-size: 100svh; }
.sidebar-block-demo .kappa-sidebar-layout { border: 1px solid var(--kappa-line); border-radius: 0.625rem; overflow: hidden; }
.sidebar-block-demo[data-standalone] .kappa-sidebar-layout { border: 0; border-radius: 0; }
.sidebar-block-demo__identity-button { padding-block: 0.25rem; }
.sidebar-block-demo__identity-button .kappa-sidebar__menu-label { position: relative; padding-inline-end: 1.25rem; }
.sidebar-block-demo__identity { display: flex; flex-direction: column; min-inline-size: 0; gap: 0.125rem; }
.sidebar-block-demo__identity strong { font-size: 0.8125rem; line-height: 1.125rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__identity small { font-size: 0.6875rem; line-height: 1rem; color: var(--kappa-subtle); overflow: hidden; text-overflow: ellipsis; }
.sidebar-block-demo__chevrons { position: absolute; inset-inline-end: 0; inset-block-start: 50%; transform: translateY(-50%); inline-size: 0.875rem; block-size: 0.875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__namespace > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__namespace-icon { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; border-radius: 0.5rem; background: var(--kappa-accent-solid); color: var(--kappa-accent-contrast); }
.sidebar-block-demo__namespace-icon svg { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace .kappa-sidebar__menu-label { padding-inline-end: 1.5rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__namespace .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__namespace[data-state="open"] { background: var(--kappa-tint); }
.sidebar-block-demo__namespace.kappa-sidebar__menu-button:focus-visible { outline-offset: 2px; }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__namespace { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__avatar.kappa-avatar { --kappa-avatar-size: 2rem; border-radius: 0.5rem; overflow: hidden; }
.sidebar-block-demo__profile > .kappa-sidebar__menu-icon { inline-size: 2rem; block-size: 2rem; }
.sidebar-block-demo__profile.kappa-sidebar__menu-button { block-size: 3rem; min-block-size: 3rem; padding: 0.5rem; gap: 0.5rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity { gap: 0; }
.sidebar-block-demo__profile .sidebar-block-demo__identity strong, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity strong { font-size: 0.875rem; line-height: 1.125rem; }
.sidebar-block-demo__profile .sidebar-block-demo__identity small, .sidebar-block-demo__profile-summary .sidebar-block-demo__identity small { font-size: 0.75rem; line-height: 0.875rem; }
.sidebar-block-demo__profile .sidebar-block-demo__chevrons { inline-size: 1rem; block-size: 1rem; }
.sidebar-block-demo__profile[data-state="open"] { background: var(--kappa-overlay); }
.kappa-sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar-block-demo__profile { block-size: 2rem; min-block-size: 2rem; padding: 0; }
.sidebar-block-demo__profile-menu.kappa-dropdown__content { inline-size: max(14rem, var(--reference-width, 14rem)); border-radius: 0.5rem; }
.sidebar-block-demo__profile-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.25rem; }
.sidebar-block-demo__profile-summary .sidebar-block-demo__identity { flex: 1; }
.sidebar-block-demo__profile-menu[data-mobile] .kappa-dropdown__item { min-block-size: 2.75rem; }
.sidebar-block-demo__secondary { margin-block-end: 0.5rem; }
.kappa-sidebar__footer:has(> .sidebar-block-demo__secondary) { flex-direction: column; align-items: stretch; }
.sidebar-block-demo__search { margin-block-end: 0.5rem; }
.sidebar-block-demo__group-trigger .kappa-sidebar__menu-label { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button { min-block-size: 2rem; border: 1px solid var(--kappa-line); border-radius: 0.375rem; background: color-mix(in srgb, var(--kappa-control) 25%, var(--kappa-tint)); color: var(--kappa-default); }
.sidebar-block-demo__search-button > .kappa-sidebar__menu-icon { color: var(--kappa-subtle); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:hover { border-color: var(--kappa-subtle); background: var(--kappa-tint); }
.sidebar-block-demo__search-button.kappa-sidebar__menu-button:focus-visible { border-color: var(--kappa-focus); outline-offset: 2px; }
.sidebar-block-demo__breadcrumb { display: flex; align-items: center; gap: 0.5rem; min-inline-size: 0; font-size: 0.75rem; }
.sidebar-block-demo__breadcrumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-block-demo__breadcrumb span:first-child { color: var(--kappa-subtle); }
.sidebar-block-demo__page { padding: 1.5rem; max-inline-size: 62rem; margin-inline: auto; }
.sidebar-block-demo__page-heading { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 1rem; }
.sidebar-block-demo__page-heading h2 { margin: 0.375rem 0 0 !important; color: var(--kappa-default); font-size: 1.25rem !important; line-height: 1.5rem !important; letter-spacing: -0.025em; font-weight: 550; }
.sidebar-block-demo__page-heading button svg { inline-size: 0.875rem; block-size: 0.875rem; }
.sidebar-block-demo__eyebrow { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__description { margin: 0.75rem 0 1.5rem !important; font-size: 0.8125rem !important; line-height: 1.4; color: var(--kappa-subtle); }
.sidebar-block-demo__section-heading { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-block-end: 0.625rem; }
.sidebar-block-demo__section-heading h3 { margin: 0 !important; font-size: 0.8125rem !important; line-height: 1.25rem !important; font-weight: 550; }
.sidebar-block-demo__section-heading > span { font-size: 0.6875rem; color: var(--kappa-subtle); }
.sidebar-block-demo__project-link { display: block; margin: 0; padding: 0; border: 0; background: transparent; color: var(--kappa-default); cursor: pointer; font: inherit; text-align: start; }
.sidebar-block-demo__project-link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.sidebar-block-demo__project-link:focus-visible { outline: 2px solid var(--kappa-focus); outline-offset: 3px; }
.sidebar-block-demo__project-code { display: block; font-size: 0.625rem; line-height: 1rem; color: var(--kappa-subtle); }
.sidebar-block-demo__status { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem; white-space: nowrap; }
.sidebar-block-demo__status::before { content: ""; inline-size: 0.3125rem; block-size: 0.3125rem; border-radius: 50%; background: var(--kappa-subtle); }
.sidebar-block-demo__status[data-ready]::before { background: var(--kappa-success-text); }
.sidebar-block-demo__note { margin: 1rem 0 0 !important; font-size: 0.6875rem !important; color: var(--kappa-subtle); }
.sidebar-block-demo__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 767px) {
.sidebar-block-demo:not([data-standalone]) { block-size: 30rem; }
.sidebar-block-demo__page { padding: 1rem; }
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :first-child,
.sidebar-block-demo__site-name + .sidebar-block-demo__breadcrumb > :nth-child(2) { display: none; }
}
</style>Installation and Usage
Application Shell uses the published SidebarLayout block. Import it from @dicehub/kappa or @dicehub/kappa/blocks/sidebar-layout. Icons in the complete examples use the docs site's existing Lucide package; the block itself has no icon dependency.
View Code provides a complete Vue file with the example styles. Copy it into your application, replace the synthetic data, and connect navigation to your router. For a smaller composition, use the named slots directly:
<script setup>
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Folder } from "@lucide/vue";
</script>
<template>
<SidebarLayout variant="workspace" label="Project navigation" style="height: 100svh">
<template #header>
<Sidebar.MenuButton :icon="Folder" tooltip="Workspace">Workspace</Sidebar.MenuButton>
</template>
<template #navigation="{ setMobileOpen }">
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton :icon="Folder" href="/projects" tooltip="Projects"
@click="setMobileOpen(false)">Projects</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<template #toolbar>Projects</template>
<main style="padding: 1.5rem">Your application content.</main>
</SidebarLayout>
</template>Give the block a height. The built-in toggle and mobile Close remain available in every layout. Do not add another Provider or Root inside its slots. SidebarLayout does not create a main landmark; use one in the default slot for an application page.
API and Composition
All Sidebar.Provider props and events are supported, including controlled open/mobile state, width, resize events, compact mode, logical placement, and custom mobile breakpoints. Attributes, styles, and DOM listeners go to the Provider layout element.
| Prop | Default | Purpose |
|---|---|---|
variant | workspace | workspace, rail, inset, floating, split, or header. |
defaultOpen | False for rail; true otherwise | Initial desktop state. Changing variant does not reset the current preference. |
width / defaultWidth | 352px for split; 260px otherwise | Fixed CSS width or initial numeric resize width. Explicit values take priority. |
label | Application navigation | Navigation landmark and mobile dialog name. |
navigationLabel / secondaryLabel | Navigation links / Secondary navigation | Names for the independently scrollable columns. |
fullScreenOnMobile | false | Full-viewport mobile navigation. Enabled in all gallery examples. |
triggerProps / closeLabel / resizeLabel | Sidebar labels | Translate the built-in controls. Trigger props also support disabled state. |
Slots: header, navigation, footer, toolbar, secondary, and the default page content. Each receives the Sidebar context. The secondary slot is used only by split. The toolbar is above the full layout for header, and above page content for other variants.
Header, Content, Footer, the toggle, Close, and optional ResizeHandle are composed by the block. Put Menu, Group, links, selectors, or other controls inside the matching slot. Use icons on top-level entries for icon-rail collapse. Grouped Navigation uses text-only links and collapsible="offcanvas" instead.
Use v-model:open, v-model:mobile-open, and v-model:resize-width as with Sidebar.Provider. Keep selection, form values, and controlled section state outside navigation slots: mobile transitions remount the Sidebar content.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { DirectionProvider } from "@dicehub/kappa/components/direction-provider";
import { Button } from "@dicehub/kappa/components/button";
import { Folder } from "@lucide/vue";
const open = ref(true);
const width = ref(352);
const locked = ref(false);
const end = ref(false);
const rtl = ref(false);
</script>
<template>
<div class="sidebar-layout-state" data-sidebar-layout-state>
<div class="sidebar-layout-state__controls">
<Button size="sm" variant="secondary" :aria-pressed="locked" @click="locked = !locked">{{ locked ? 'Unlock changes' : 'Lock changes' }}</Button>
<Button size="sm" variant="secondary" :aria-pressed="end" @click="end = !end">End placement</Button>
<Button size="sm" variant="secondary" :aria-pressed="rtl" @click="rtl = !rtl">RTL</Button>
</div>
<component :is="rtl ? DirectionProvider : 'div'" :locale="rtl ? 'ar' : undefined">
<SidebarLayout
variant="split" :open="open" :resize-width="width" resizable
:dir="rtl ? 'rtl' : 'ltr'" :side="end ? 'end' : 'start'"
:min-width="220" :max-width="440" label="Controlled navigation"
:trigger-props="{ expandLabel: 'Expand navigation', collapseLabel: 'Collapse navigation', openLabel: 'Open navigation' }"
close-label="Dismiss navigation" resize-label="Resize navigation"
class="sidebar-layout-state__layout" data-example-attribute="forwarded"
@update:open="value => { if (!locked) open = value; }"
@update:resize-width="value => { if (!locked) width = value; }"
>
<template #header><Sidebar.MenuButton :icon="Folder" tooltip="Workspace">Workspace</Sidebar.MenuButton></template>
<template #navigation><Sidebar.Menu><Sidebar.MenuItem><Sidebar.MenuButton :icon="Folder" tooltip="Projects">Projects</Sidebar.MenuButton></Sidebar.MenuItem></Sidebar.Menu></template>
<template #secondary><Button size="sm" variant="secondary" :disabled="locked" @click="open = false">Close project list</Button></template>
<template #toolbar>Controlled layout</template>
<template #default><output class="sidebar-layout-state__output" aria-live="polite">{{ open ? 'Expanded' : 'Collapsed' }} · {{ Math.round(width) }}px</output></template>
</SidebarLayout>
</component>
</div>
</template>
<style>
.sidebar-layout-state { inline-size: 100%; min-inline-size: 0; }
.sidebar-layout-state__controls { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-block-end: 0.75rem; }
.sidebar-layout-state__layout { block-size: 17rem; border: 1px solid var(--kappa-line); border-radius: 0.5rem; overflow: hidden; }
.sidebar-layout-state__output { display: block; padding: 1rem; font-size: 0.8125rem; }
</style>This controlled example can reject open and resize requests. Close project list tests focus return from the hidden second column; End placement and RTL show logical positioning.
Grouped Navigation composes Kappa Sidebar, Dropdown, and Input. Its search field filters section links, Escape clears the query, and selecting a page updates the local heading and breadcrumb. The version selector retains its selection across mobile transitions. This is example-level state, not a new SidebarLayout variant.
Collapsible Sections adds Sidebar.Collapsible, CollapsibleTrigger, and CollapsibleContent to each group. All sections start open. Enter and Space toggle the focused heading. Section state is held outside the navigation slot so mobile transitions preserve it. Typing a search opens matching sections; clearing search keeps the current section state.
Submenus uses Sidebar.MenuSub and MenuSubItem for nested lists. Parent links open section overviews, not disclosure controls. Child links remain visible. Native Tab and Enter navigation is preserved; selecting a page closes the mobile drawer and keeps the selection when the viewport changes.
Floating Submenus uses the same navigation with SidebarLayout's floating variant and a 19rem desktop width. The panel has an outer gap and rounded frame. Mobile retains the standard full-screen drawer without the desktop frame.
Collapsible Submenus wraps each nested list in Sidebar.Collapsible. Parent rows are buttons, not links; click, Enter, or Space toggles the list without changing the selected page. Only the selected page's group starts open. Search opens matching groups; clearing search keeps their current state. Open groups and selection survive mobile transitions.
Dropdown Submenus composes Kappa Dropdown with Sidebar.MenuButton. Parent buttons open page links in a popup; they do not navigate. Arrow keys move between menu items, Enter selects a page, and Escape closes the popup and returns focus to its parent. Search filters each popup. Mobile popups stay inside the full-screen drawer; selection closes both layers and returns focus to the sidebar toggle.
This local preview uses Dropdown's navigate callback to dispatch a cancelable native click. The link's @click.prevent then prevents keyboard and pointer selection from leaving the example. Replace this demo handling with your application's navigation when you copy the source.
Collapsible Icon Navigation starts expanded and uses SidebarLayout with icon collapse. Each parent button toggles an independent Sidebar.Collapsible. Clicking a group icon in the rail expands the sidebar and opens that group. Hidden child links are not keyboard targets. Quick search opens the selected page's group, and namespace, profile, group state, and page selection are held outside the slots to survive mobile transitions. This is a demo composition, not a new SidebarLayout variant. The original Icon Rail example is unchanged.
Inset Navigation uses the inset variant with nested link groups and project shortcuts. Its Help and feedback navigation sits in the footer above the profile menu, outside the scrolling primary links. Secondary links open local preview pages, are included in Quick search, and close the mobile drawer after selection. The existing Inset Content example remains unchanged. This composition does not use the secondary slot, which is reserved for the split layout.
Namespace/profile popups use :teleport="!isMobile" and fixed positioning. The namespace trigger uses a 32px icon tile and a 48px row. In the workspace layouts, Quick search is a bordered button that opens Kappa CommandPalette, not an editable input. The examples close mobile navigation after selection; your application must decide when routing has succeeded.
Application Shell profile triggers use a 48px row, a 32px rounded Avatar, and two lines of identity text. Their menus repeat the identity above grouped plan, account, billing, notification, and logout actions. Switch profile opens a submenu; Preferences remains available. All account actions open local previews and do not change real accounts or sessions. The sample portraits use the docs site's local avatar assets; replace their paths when copying this example. Quick search uses a subtle token-based surface instead of a solid white background.
Inbox Navigation composes Sidebar parts directly for a fixed 64px folder rail, a collapsible message list, and a reading panel. It does not add or change a SidebarLayout variant. Folder selection opens the list and clears its filters. Search matches the sender, subject, and body; Unread filters the sample data. Filtering selects the first visible message when necessary. Opening a message does not mark it as read. Empty folders and filters have explicit empty states.
On mobile, the folder rail and message list share a full-screen drawer. Selecting a message closes the drawer and returns focus to its opener. Folder, query, unread filter, and selected message survive viewport changes. Tab and Enter operate the folder and message buttons. The Ros.Space profile uses the same local avatar and grouped account actions; this compact single-account example does not include profile switching. No mail or account action contacts a server.
Workspace Pages is a separate composition of SidebarLayout, Sidebar.Collapsible, Dropdown, CommandPalette, and Button. It does not replace the original Workspace example or add a SidebarLayout variant. Namespace selection changes the available pages. Each namespace keeps its own selected page, Favorites, and open groups until reload. The toolbar star adds or removes the current page from Favorites; removing its last Favorite shows an empty state.
Workspace group buttons only expand or collapse their child links. Selecting a page from Favorites, Home, or Quick search opens its group. Mobile selection closes the full-screen drawer and returns focus to the sidebar toggle. Search matches page names in the active namespace; Escape closes search and returns to its opener. Closed search content is inert during its exit animation. Search selection releases the nested dialog on the next Vue update before closing the drawer, so the two focus traps do not compete. Calendar, Settings, Trash, and Help open local previews. There is no editor, account integration, or server persistence.
The examples also set closed popup content to inert through Dropdown.Context. This prevents delayed menu focus from returning to a closed popup during its exit animation. Selection, dismissal, and trigger focus remain controlled by Kappa Dropdown.
Minimal Workspace is a separate dicehub-style composition of SidebarLayout's existing header variant. Its shared top bar is 48px high. Equal-width header sides center Search or go to across the full example, independent of sidebar width. The desktop sidebar starts at 250px, resizes from 200px to 420px, and collapses to a 48px icon rail. The resize line is hidden at rest and appears on hover, keyboard focus, or drag; its hit area stays active. Drag the separator or use its arrow keys; the last expanded width survives collapse and mobile transitions.
The desktop collapse button sits below Help and uses Sidebar.Trigger with Sidebar.MenuButton. The built-in top-bar trigger is hidden only on desktop; it remains the mobile drawer opener. A simple namespace row and project links sit beside a rounded content panel. The example does not include account warnings, credits, or onboarding cards.
Its Search or go to button opens CommandPalette from the top bar. Search by name filters the project table; Escape clears that field. New project and Use template add local sample projects. Namespace selection changes the project list and resets the page to Dashboard. Profile, Help, and other navigation actions open local previews; Sign out does not end a session. State lasts until reload, and no account, router, or backend is connected.
Container queries replace the wide top navigation with a Browse application pages menu in narrow previews and reduce search to a centered icon on small screens. On mobile, navigation uses Kappa's full-screen drawer; selecting a link or Help action closes it and returns focus to the single top-bar opener. The desktop footer toggle and resize handle are absent on mobile. Desktop collapse retains the namespace and navigation icons while hiding project shortcuts. All other examples remain unchanged.
Accessibility and Styling
Keyboard navigation, visible focus, mobile focus trapping, Escape, and resize controls come from the existing Kappa components. Split navigation hides and disables its secondary column during icon collapse, then stacks the columns on mobile. The top header stays outside the scrolling content.
Navigation headers and page toolbars have a 64px minimum height. Floating and inset surfaces are block-only styles. Mobile uses the normal Sidebar drawer surface. Light and dark modes use the same semantic tokens; reduced-motion settings are preserved.
Optional dimensions: --kappa-sidebar-layout-gap (0.5rem), --kappa-sidebar-layout-radius (0.625rem), and --kappa-sidebar-layout-primary-width (9.5rem for split's first column). Namespace and profile data remain example-level compositions, not new application-specific controls.