Active projects
Three need a decision this week.
Static, pinned, icon-rail, and inset sidebar configurations plus topbar, all using one navigation tree and an accessible mobile drawer.
app/components/nitro_kit/app_shell.rbNitroKit::AppShell.new(id:, layout: :sidebar, collapsible: false, sidebar: :expanded, sidebar_toggle_label:, skip_link_label:, open_navigation_label:, close_navigation_label:, navigation_dialog_label:) { |shell| shell.navigation { ... }; shell.main { ... } }Compare static, pinned, icon-rail, and application-owned inset sidebars. Topbar uses the same navigation tree without sidebar pinning; every layout becomes the mobile drawer at narrow widths.
collapsible: false. Navigation stays expanded with no pin button or hover peek; the mobile drawer still works.
Sidebar
A caller-owned dashboard composed from ordinary Nitro components.
Three need a decision this week.
All production checks are passing.
Both have an assigned responder.
test/dummy/app/components/gallery/components/app_shell_page.rbrender NitroKit::AppShell.new(
id: "gallery-app-shell-static",
layout: :sidebar,
collapsible: false,
navigation_dialog_label: "Workspace navigation",
data: { gallery_shell_preview: :sidebar }
) do |shell|
shell.brand { strong { "Northstar" } }
shell.navigation do
render_shell_navigation(id: "gallery-app-shell-static", current: :overview, dense: false, long: false)
end
shell.topbar { render NitroKit::Button.new("Search", href: "#search", size: :sm, icon: :search) }
shell.main { render_workspace_main(layout: :sidebar, long: false) }
endcollapsible: true, sidebar: :collapsed. Icons and the brand mark stay aligned; hover or focus to peek without shifting content, or click to pin open.
Peek at navigation without moving this canvas, or pin it to reserve layout space.
test/dummy/app/components/gallery/components/app_shell_page.rbrender NitroKit::AppShell.new(
id: "gallery-app-shell-rail",
collapsible: true,
sidebar: :collapsed,
data: { gallery_shell_preview: :sidebar }
) do |shell|
shell.brand(icon: :star) { strong { "Northstar" } }
shell.navigation do
render NitroKit::AppNavigation.new(label: "Rail workspace") do |navigation|
navigation.body do
navigation.section(label: "Workspace") do
navigation.item("Overview", href: "#overview", icon: :house, current: true)
navigation.item("Projects", href: "#projects", icon: :folder)
navigation.item("People", href: "#people", icon: :users)
end
navigation.spacer
navigation.item("Help", href: "#help", icon: :circle_help)
navigation.item("Settings", href: "#settings", icon: :settings)
end
end
end
shell.topbar { render NitroKit::Button.new("New project", icon: :plus, variant: :primary) }
shell.main do
div(data: { gallery: "app-shell-main" }) do
render NitroKit::PageHeader.new(
title: "Projects",
description: "Peek at navigation without moving this canvas, or pin it to reserve layout space.",
level: 4
)
end
end
endApplication-owned inset_workspace.css joins the toolbar and content into one rounded canvas. Use data-ui hooks, not an inset component option; navigation and the mobile drawer remain Nitro-owned.
One content gutter, an independently scrolling canvas, and a quiet navigation rail.
test/dummy/app/components/gallery/components/app_shell_page.rbrender NitroKit::AppShell.new(
id: "gallery-app-shell-inset",
layout: :sidebar,
collapsible: false,
data: { ui: "inset-workspace", gallery_shell_preview: :sidebar }
) do |shell|
shell.brand { strong { "Northstar" } }
shell.navigation do
render NitroKit::AppNavigation.new(label: "Northstar workspace") do |navigation|
navigation.body do
navigation.section(label: "Workspace") do
navigation.item("Overview", href: "#overview", icon: :house, current: true)
navigation.item("Projects", href: "#projects", icon: :folder, badge: 12)
navigation.item("People", href: "#people", icon: :users)
end
navigation.spacer
navigation.item("Settings", href: "#settings", icon: :settings)
end
navigation.footer { render NitroKit::Button.new("Help", href: "#help", size: :sm, icon: :circle_help) }
end
end
shell.topbar do
render NitroKit::Toolbar.new do |toolbar|
toolbar.leading { h4 { "Workspace overview" } }
toolbar.trailing { render NitroKit::Button.new("New project", href: "#new-project", variant: :primary) }
end
end
shell.main do
render NitroKit::Flex.new(dir: :col, gap: 6, align: :stretch, data: { ui: "workspace-content" }) do
p { "One content gutter, an independently scrolling canvas, and a quiet navigation rail." }
render NitroKit::StatGrid.new do |stats|
stats.stat(key: :projects, label: "Active projects", value: "12", detail: "Three need a decision")
stats.stat(key: :deployments, label: "Deployments", value: "4", detail: "All checks passing")
stats.stat(key: :incidents, label: "Open incidents", value: "2", detail: "Both assigned")
end
end
end
endCombine the application-owned inset canvas with collapsible: true. Start pinned open, unpin to an icon rail, then hover or focus to peek without moving the inset toolbar or content.
Pin to reserve navigation space, or peek over the canvas without reflowing this content.
test/dummy/app/components/gallery/components/app_shell_page.rbrender NitroKit::AppShell.new(
id: "gallery-app-shell-inset-collapsible",
layout: :sidebar,
collapsible: true,
sidebar: :expanded,
data: { ui: "inset-workspace", gallery_shell_preview: :sidebar }
) do |shell|
shell.brand(icon: :star) { strong { "Northstar" } }
shell.navigation do
render NitroKit::AppNavigation.new(label: "Northstar workspace") do |navigation|
navigation.body do
navigation.section(label: "Workspace") do
navigation.item("Overview", href: "#overview", icon: :house, current: true)
navigation.item("Projects", href: "#projects", icon: :folder, badge: 12)
navigation.item("People", href: "#people", icon: :users)
end
navigation.spacer
navigation.item("Settings", href: "#settings", icon: :settings)
end
navigation.footer { render NitroKit::Button.new("Help", href: "#help", size: :sm, icon: :circle_help) }
end
end
shell.topbar do
render NitroKit::Toolbar.new do |toolbar|
toolbar.leading { h4 { "Workspace overview" } }
toolbar.trailing { render NitroKit::Button.new("New project", href: "#new-project", variant: :primary) }
end
end
shell.main do
render NitroKit::Flex.new(dir: :col, gap: 6, align: :stretch, data: { ui: "workspace-content" }) do
p { "Pin to reserve navigation space, or peek over the canvas without reflowing this content." }
render NitroKit::StatGrid.new do |stats|
stats.stat(key: :projects, label: "Active projects", value: "12", detail: "Three need a decision")
stats.stat(key: :deployments, label: "Deployments", value: "4", detail: "All checks passing")
stats.stat(key: :incidents, label: "Open incidents", value: "2", detail: "Both assigned")
end
end
end
endBrand, navigation, and account actions share the desktop header before the same tree becomes a narrow drawer.
Topbar
A caller-owned dashboard composed from ordinary Nitro components.
Three need a decision this week.
All production checks are passing.
Both have an assigned responder.
test/dummy/app/components/gallery/components/app_shell_page.rbrender_workspace_shell(id: "gallery-app-shell-topbar", layout: :topbar, current: :overview, collapsible: false)Optional regions disappear cleanly while long destinations, nested content, and narrow disclosure keep the same contract.
Brand and topbar are optional; the required navigation and main regions still form a complete application frame.
Sidebar
A caller-owned dashboard composed from ordinary Nitro components.
Three need a decision this week.
All production checks are passing.
Both have an assigned responder.
test/dummy/app/components/gallery/components/app_shell_page.rbrender_workspace_shell(
id: "gallery-app-shell-minimal",
layout: :sidebar,
current: :overview,
collapsible: true,
brand: false,
actions: false
)Long brand, route, and content copy shrink inside the owned columns without a layout option or utility class.
Sidebar
Coordinate analytical engine capacity, operational handoffs, and incident readiness across every research and production region.
Three need a decision this week.
All production checks are passing.
Both have an assigned responder.
test/dummy/app/components/gallery/components/app_shell_page.rbrender_workspace_shell(
id: "gallery-app-shell-long",
layout: :sidebar,
current: :capacity,
collapsible: true,
long: true,
dense: true
)Both layouts with Gallery::Hostile's long name as brand, every hostile navigation label with badges, a long-labelled topbar action, and a main region of unbroken headings and hostile member cards.
This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text. This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text. This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text. This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text.
maximiliana.wolfeschlegelsteinhausenbergerdorff+billing.notifications.eu-west-2026@finance.department.enterprise-holdings-international.example
ünïcödé.tëst+ß@exämple-bücher.test
用户@例子.广告
wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege@wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege.example
maximiliana.wolfeschlegelsteinhausenbergerdorff+billing.notifications.eu-west-2026@finance.department.enterprise-holdings-international.example
ünïcödé.tëst+ß@exämple-bücher.test
用户@例子.广告
test/dummy/app/components/gallery/components/app_shell_page.rbNitroKit::AppShell::LAYOUTS.each do |layout|
sample(layout.to_s.humanize, slug: layout.to_s) do
render NitroKit::AppShell.new(
id: "gallery-app-shell-hostile-#{layout}",
layout:,
collapsible: true,
navigation_dialog_label: Gallery::Hostile::LONG_LABEL,
data: { gallery_shell_preview: layout }
) do |shell|
shell.brand { strong { Gallery::Hostile::LONG_NAME } }
shell.navigation do
render NitroKit::AppNavigation.new(
id: "gallery-app-shell-hostile-#{layout}-navigation",
label: Gallery::Hostile::LONG_LABEL
) do |navigation|
navigation.header { strong { Gallery::Hostile::LONG_WORD } }
navigation.body do
navigation.section(label: Gallery::Hostile::LONG_LABEL) do
Gallery::Hostile::NAVIGATION_LABELS.each_with_index do |label, index|
navigation.item(
label,
href: "#{Gallery::Hostile::LONG_URL}#destination-#{index}",
icon: :folder,
badge: Gallery::Hostile::BADGE_LABELS[index],
current: index.zero?
)
end
end
navigation.spacer
navigation.divider
navigation.item(Gallery::Hostile::LONG_WORD, href: Gallery::Hostile::LONG_URL, icon: :settings)
end
navigation.footer { p { Gallery::Hostile::LONG_EMAIL } }
end
end
shell.topbar do
render NitroKit::Button.new(
Gallery::Hostile::LONG_LABEL,
id: "gallery-app-shell-hostile-#{layout}-search",
href: Gallery::Hostile::LONG_URL,
size: :sm,
icon: :search
)
end
shell.main do
div(data: { gallery: "app-shell-main" }) do
render NitroKit::Container.new(size: :lg) do
render NitroKit::Flex.new(dir: :col, gap: 6, align: :stretch) do
render NitroKit::PageHeader.new(
title: Gallery::Hostile::LONG_WORD,
description: Gallery::Hostile::LONG_PARAGRAPH,
level: 4
)
render NitroKit::Grid.new(cols: "1 sm:2 lg:3", gap: 3) do
Gallery::Hostile.members(9).each do |member|
render NitroKit::Card.new(id: "gallery-app-shell-hostile-#{layout}-#{member.id}") do |card|
card.title(member.name, level: 5)
card.description(member.email)
end
end
end
end
end
end
end
end
end
endConstructor options, rendered root, closed vocabularies, and compound boundary for this component, exactly as shipped.
docs/component_contracts.md · NitroKit::AppShell
id:; layout: :sidebar; collapsible: false; sidebar: :expanded (expanded collapsed); configurable skip/open/close/dialog/sidebar-toggle labels defaulting to nitro_kit.app_shell.*div[data-nk=app-shell][data-layout]; layouts sidebar topbar; drawer states open closed; data-enhanced is written by the Stimulus controller and reservednavigation and one main; optional brand(icon: nil) and topbar are unique. Regions are declared inside the render block only. The default sidebar stays expanded with no toggle or peek. collapsible: true opts into a Button-backed pin toggle with aria-pressed and fine-pointer hover/focus overlay peek without changing the rail's layout width. Brand's optional validated Lucide icon stays visible on the rail; full brand content peeks instead of being cropped. Labels retain accessible names and vertical positions; supply item icons for the rail. sidebar: sets initial pin state; sidebar: :collapsed requires collapsible: true. These options do not affect topbar or the narrow native drawer. Pin state is not persisted across page loads. One navigation tree moves between its neutral desktop wrapper and a native modal dialog at narrow widths. IDs are fragment-safe; product policy remains caller-owned.Application conventions this component belongs to. Each summary is the leading section of its pattern document.
docs/patterns/application_foundation.md · docs/patterns/inset_workspace.md · docs/patterns/crud_resource.md
User, Team, and Membership; roles belong to memberships, and tenant-owned records load through Current.team.AppShell for the authenticated product and one application-owned content gutter inside shell.main.Toolbar; keep destinations in AppNavigation.Toast::FlashMessages region for server feedback.Instructions for coding agents, repeated on every component page so one fetched page has enough context. Humans can usually skip this section.
test/dummy/app/components/gallery/agent_rules.rb
render NitroKit::Button.new("Save", variant: :primary).ArgumentError; this component's accepted options are in the contract above.html: for HTML, aria: for ARIA, and data: for application data. class and style are forbidden, including inside html:.NitroKit::Component::RESERVED_DATA_ATTRIBUTES: data-nk, data-slot, data-variant, data-size, data-nk-escape, data-enhanced, data-state, data-disabled, data-required, data-orientation, data-presentation, data-placement, data-layout, data-side, data-field-type, data-dir, data-gap, data-align, data-justify, data-wrap, data-cols, data-mode, data-key. Do not pass them through data:. data-action, data-controller are additive and compose with Nitro behavior.desperately_need_a_class:. It requires a non-blank String and marks the exception with data-nk-escape="class".data-nk; owned parts emit component-qualified data-slot values such as field-control or card-title. Select on those attributes, never on classes.--nk-* custom properties in an application stylesheet. Variables beginning with --_nk-* are private component mechanics.NitroKit::Flex and NitroKit::Grid for layout. Parents own external placement and available width; components own their intrinsic geometry.data-state when styling or behavior also needs it.