@super-ic/product-shell
v0.1.1
Published
Provider-neutral React application shell primitives: app frame, rails, page and state regions, top bar, segmented search pill, and settings switch row.
Readme
@super-ic/product-shell
Provider-neutral React application shell primitives for SuperIC product surfaces: the app frame, navigation rails, page and state regions, the top bar, the segmented search pill, and the settings switch row.
Every component in this package renders from fixture props alone. Nothing here opens a network connection, reads a cookie, initialises an SDK, or decides authorisation.
Install
npm install @super-ic/product-shell @super-ic/foundation react react-domreact, react-dom and @super-ic/foundation are peer dependencies. The declared React
range is >=19.0.0. Installing this package on its own emits a peer warning naming
@super-ic/foundation; that warning is the intended signal, not a defect.
Required setup
The components style themselves from CSS custom properties that only @super-ic/foundation
defines. Import the token sheet once, at the root of the consuming application, before any
component from this package renders:
import "@super-ic/foundation/tokens.css";
import "@super-ic/product-shell/member-shell.css";Without that import every surface falls back to the browser default colours: the frame has no background, the state regions have no surface, and the switch has no track.
The 19 custom properties this package reads are --accent, --background, --border,
--control-border, --control-elevation, --control-elevation-surface,
--control-highlight, --elev-card, --elev-pill, --foreground, --muted,
--muted-foreground, --primary, --primary-foreground, --radius, --radius-card,
--ring, --surface and --surface-2.
No Tailwind @source line is needed. Existing primitives carry scoped inline CSS. The
MemberShell recipe uses the public ordinary CSS entry shown above, scoped by data-slot
attributes and available in the installed package.
Usage
import "@super-ic/foundation/tokens.css";
import "@super-ic/product-shell/member-shell.css";
import {
AppFrame,
AppRail,
AppTopBar,
PageRegion,
StateRegion,
} from "@super-ic/product-shell";
export function Screen() {
return (
<AppFrame
header={<AppTopBar brand="Example" modes={[{ id: "browse", label: "Browse" }]} activeModeId="browse" />}
rail={<AppRail items={[{ id: "home", label: "Home", href: "/", active: true }]} />}
>
<PageRegion title="Home" description="What is available near you today.">
<StateRegion
state="empty"
title="Nothing here yet"
description="New items appear as neighbours post them."
action={{ label: "Refresh", onAction: () => location.reload() }}
/>
</PageRegion>
</AppFrame>
);
}For a composed member application, use one navigation array and select the current item by
id. MemberShell accepts zero through five unique destinations. A non-empty navigation
requires a matching currentNavigationId, or explicit null for a secondary route
such as Account. An omitted ID still rejects a populated navigation. A secondary
route keeps all primary links available without falsely marking Home current; its
utility link may carry aria-current="page". Empty navigation omits its landmarks.
The frame defaults to 100dvh. Within a parent that has an explicitly resolved
height, set --member-shell-block-size: 100% to fill the available region. A review
banner belongs in the parent layout and must not push navigation below the viewport.
import "@super-ic/foundation/tokens.css";
import "@super-ic/product-shell/member-shell.css";
import { MemberShell } from "@super-ic/product-shell";
<MemberShell
brand="Fixture Share"
navigation={[{ id: "home", label: "Home", href: "/home" }]}
currentNavigationId="home"
pageTitle="Member home"
>
{content}
</MemberShell>;The product adapter owns route transitions, route-heading focus and scroll restoration. An
optional onNavigation callback receives the unmodified React anchor event and item, so an
SPA adapter can preserve modifier clicks and other browser link behavior.
Exports
| Export | Kind | Notes |
| --- | --- | --- |
| MemberShell | recipe | Container-responsive 100dvh member shell with one main, one h1, skip link and zero-to-five destination invariant. Import @super-ic/product-shell/member-shell.css. |
| AppFrame | component | Grid shell. Stamps data-superic-provider="none" and injects the frame stylesheet. |
| AppRail | component | Desktop navigation <nav>; sets aria-current="page" on the active item. |
| MobileBottomNavigation | component | Small-viewport counterpart to AppRail. |
| PageRegion | component | The <main> landmark, its heading, and its description. |
| StateRegion | component | Blocking and recovery states, each with a live region. |
| AppTopBar | component | Brand plus a single-select mode group using aria-pressed. |
| SegmentedSearchPill | component | role="search" form with per-segment labels and one submit control. |
| SettingsSwitchRow | component | role="switch" row wired to its label and help text. |
| StateRegionStates | const | waiting, empty, error, offline, permission, recovery, stale, success, plus matrix aliases loading, initial-empty, validation-error, permission-denied, removed, retryable-error, conflict, capability-disabled, safe-stop, support-entry. |
| ProductMemberRoles, ProductMemberCapabilities | const | Display-only role and capability vocabularies. |
| assertProductMemberDisplayProjection | function | Throws unless authority === "display-only". |
| createProviderNeutralFixture, assertProviderNeutralFixture | function | Throws unless mode === "fixture" and provider === "none". |
| PRODUCT_SHELL_CONTRACT_VERSION | const | Tracks this package's own version. |
Contracts worth knowing before you build on this
ProductMember is a projection, not an authority. It carries role labels and capability
labels for rendering only. assertProductMemberDisplayProjection throws if you hand it a
member whose authority is anything other than "display-only". Never branch a permission
decision on it; ask your own server.
Fixtures may not reach a provider. assertProviderNeutralFixture throws unless the
fixture declares mode: "fixture" and provider: "none". AppFrame, AppTopBar,
SegmentedSearchPill and SettingsSwitchRow all stamp data-superic-provider="none" on
their root so a consumer test can assert the same thing from the DOM.
PageRegion labels itself. When you pass title and no labelledBy, the heading id is
generated per instance with useId, so two PageRegions on one page never collide on a
duplicate id. Pass labelledBy when the heading lives outside the region and you own its
id; PageRegion then uses your id verbatim and does not generate one.
StateRegion picks its own ARIA role. error and permission render
role="alert" with aria-live="assertive"; the other six render role="status" with
aria-live="polite". Do not override this from the outside. The mapping is the contract.
AppTopBar allows exactly one pressed mode. activeModeId selects it, and only that one
button gets aria-pressed="true".
SegmentedSearchPill shows a segment's value or its placeholder, never both. A
segment with a value renders the value as text; a segment without one renders the
placeholder.
Versioning
PRODUCT_SHELL_CONTRACT_VERSION is the package version, not a separate contract line. Assert
against it if you need to fail closed on a shell upgrade.
