@ismail-elkorchi/ui-shell
v0.1.2
Published
Token-driven Light DOM shell components for application layouts built with Lit.
Downloads
306
Maintainers
Readme
@ismail-elkorchi/ui-shell
Token-driven shell components (activity bar, sidebars, status bar, and an optional frame layout) for Light DOM composition. They depend on @ismail-elkorchi/ui-primitives for controls, expose slots + parts, and read all visual values from @ismail-elkorchi/ui-tokens CSS variables.
Layout layer
- Regions: left rail (
activity-bar), primary sidebar, main content, optional secondary sidebar, and status bar. uik-shell-layoutstitches the regions together and tags them withdata-regionattributes to keep the layout contract visible in the DOM.- Shell components expose only UI surface/state; business logic should live in the host app.
- Contract: Shell components use
ui-primitivesstrictly via their public API (attributes/props). Visual styling comes from--uik-*custom properties (no framework utility classes).
Landmarks & labels (Accessibility contract)
uik-shell-layoutrenders arole="region"container; override its label viaaria-labeloraria-labelledbyon the host.uik-shell-activity-barrenders an<aside>landmark and forwardsaria-label/aria-labelledbyto the internal nav rail; default label is "Activity bar".uik-shell-sidebaranduik-shell-secondary-sidebarrender<aside>landmarks; default labels come from theheadingor fall back to "Sidebar"/"Secondary sidebar".uik-shell-status-barusesrole="status"witharia-live="polite"for status messages.- Provide a semantic
<main>element in themain-contentslot and label additional landmarks as needed in host markup.
Focus + roving focus
- Shell navigation surfaces delegate roving focus to primitives (
uik-nav-rail,uik-tree-view) and do not add competing keyboard handlers. - Follow the Focus + Roving Focus contract in
@ismail-elkorchi/ui-primitiveswhen composing activity bars or navigation trees.
Overlay close semantics
- Overlay-like shells emit close events with
detail.reasonaligned to primitives:escape | outside | programmatic | toggle. uik-shell-secondary-sidebarcaptures the previously focused element on open and restores focus on close (unless afocus-return-targetis provided).
Using the components
import { html } from "lit";
import "@ismail-elkorchi/ui-primitives/register";
import "@ismail-elkorchi/ui-shell/register";
import type { UikShellActivityBarItem } from "@ismail-elkorchi/ui-shell/activity-bar";
const activityItems: UikShellActivityBarItem[] = [
{
id: "explorer",
label: "Explorer",
icon: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z",
},
{
id: "search",
label: "Search",
icon: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
},
];
html`
<uik-shell-layout ?isSecondarySidebarVisible=${true}>
<uik-shell-activity-bar
slot="activity-bar"
.items=${activityItems}
.activeId=${"explorer"}
@activity-bar-select=${(e: CustomEvent<{ id: string }>) =>
console.log(e.detail.id)}
>
</uik-shell-activity-bar>
<uik-shell-sidebar slot="primary-sidebar" heading="Explorer">
<uik-button slot="actions" variant="ghost" size="icon">…</uik-button>
<div style="font-size: var(--uik-typography-font-size-2);">
<!-- put your tree view or navigation here -->
</div>
</uik-shell-sidebar>
<main
slot="main-content"
style="flex: 1 1 auto; min-height: var(--uik-space-0);"
>
Your editor or subviews
</main>
<uik-shell-secondary-sidebar
slot="secondary-sidebar"
.isOpen=${true}
heading="AI Assistant"
@secondary-sidebar-close=${() => console.log("close secondary")}
>
<p
style="
font-size: var(--uik-typography-font-size-2);
color: oklch(var(--uik-text-muted));
"
>
Auxiliary tools live here.
</p>
</uik-shell-secondary-sidebar>
<uik-shell-status-bar
slot="status-bar"
message="Ready"
tone="info"
meta="3 files selected"
></uik-shell-status-bar>
</uik-shell-layout>
`;Component notes
uik-shell-layout: named slotsactivity-bar,primary-sidebar,main-content,secondary-sidebar,status-bar.uik-shell-activity-bar: accepts.items(id/label/icon/path) and emitsactivity-bar-select; optionalfooterslot; delegates roving focus touik-nav-rail(setaria-labelif you need a custom name).uik-shell-sidebar:slot="actions"for header actions, default slot for body, optionalslot="footer";isBodyPadded/isBodyScrollabletoggle spacing + scroll.uik-shell-secondary-sidebar: controlled via.isOpen; optionalfocus-return-target(selector or element) to restore focus on close; Escape and the close button emitsecondary-sidebar-close(detail.reasonisescape | toggle).uik-shell-status-bar:.message+.tonecolorize the left side;metastring (outline badge) orslot="meta"for custom content; optionalslot="actions".- Use
@ismail-elkorchi/ui-primitives/uik-navor@ismail-elkorchi/ui-primitives/uik-tree-viewfor sidebar navigation content.
Custom properties
- Activity bar:
--uik-component-shell-activity-bar-bg,--uik-component-shell-activity-bar-fg,--uik-component-shell-activity-bar-width,--uik-component-shell-activity-bar-item-size,--uik-component-shell-activity-bar-item-icon-size,--uik-component-shell-activity-bar-item-indicator-bg,--uik-component-shell-activity-bar-item-indicator-radius,--uik-component-shell-activity-bar-item-indicator-width. - Sidebar:
--uik-component-shell-sidebar-bg,--uik-component-shell-sidebar-fg,--uik-component-shell-sidebar-width. - Secondary sidebar:
--uik-component-shell-secondary-sidebar-bg,--uik-component-shell-secondary-sidebar-width. - Status bar:
--uik-component-shell-status-bar-bg,--uik-component-shell-status-bar-fg,--uik-component-shell-status-bar-height. - Shared:
--uik-component-shell-divider-color,--uik-component-shell-scrollbar-track,--uik-component-shell-scrollbar-thumb.
Tokens & theming
Load tokens once and set theme/density attributes on a shared container (often :root):
@import "@ismail-elkorchi/ui-tokens/index.css";<html data-uik-theme="light" data-uik-density="comfortable">
...
</html>Routing store
A tiny EventTarget-based router lives in @ismail-elkorchi/ui-shell/router. It is framework-light, keeps state in memory (no history), and is meant for desktop flows that only need named views and optional subviews.
import {
createUikShellRouter,
UIK_SHELL_NAVIGATION_EVENT,
type UikShellNavigationDetail,
} from "@ismail-elkorchi/ui-shell/router";
const routes = [
{
id: "explorer",
label: "Explorer",
subviews: ["code", "prompt", "apply"],
defaultSubview: "code",
},
{ id: "search", label: "Search" },
{ id: "settings", label: "Settings" },
] as const;
export const shellRouter = createUikShellRouter({
routes,
initialView: "explorer",
initialSubview: "code",
});
// React to navigation anywhere in the app
const unsubscribe = shellRouter.subscribe(({ view, subview }) => {
console.log("Current location", view, subview);
});
// Wire Lit components through events
html`
<uik-shell-activity-bar
.items=${routes.map((r) => ({ id: r.id, label: r.label ?? r.id }))}
.activeId=${shellRouter.current.view}
@activity-bar-select=${(e: CustomEvent<{ id: string }>) =>
shellRouter.navigate(e.detail.id)}
>
</uik-shell-activity-bar>
<editor-area
.activeSubview=${shellRouter.current.subview ?? "code"}
@subview-change=${(e: CustomEvent<{ subview: string }>) =>
shellRouter.navigate(shellRouter.current.view, e.detail.subview)}
>
</editor-area>
`;
// Listen to the low-level navigation event if you prefer EventTarget
window.addEventListener(UIK_SHELL_NAVIGATION_EVENT, (event: Event) => {
const detail = (event as CustomEvent<UikShellNavigationDetail>).detail;
console.log(detail.from, detail.to, detail.route);
});- Routes are simple
{id, label?, subviews?, defaultSubview?}objects. navigate(view, subview?)resolves subviews per route (keeping the last used subview for that route).subscribeimmediately fires with the current location and returns an unsubscribe function.
