ptech-shell-ui
v2.14.0
Published
Composable app shell layout, header, routing feedback, and status-page patterns for Module Federation remotes.
Downloads
340
Maintainers
Readme
ptech-shell-ui
Composable app shell layout, navigation feedback, and status-page presentation for PTECH Module Federation remotes.
This package owns layout patterns — not shell service contracts (see ptech-shell-sdk), not MSAL/router adapters (see ptech-shell-react), and not standalone mocks (see ptech-shell-dev).
Install
npm i ptech-shell-ui ptech-shell-sdk reactThe root entry is React Router runtime-free. It renders breadcrumb anchors and
delegates unmodified app-local clicks through TOKENS.navigation when the host
has registered a navigation service. When TOKENS.breadcrumbContext is present,
both top-bar entries prepend its host-owned portal/tenant/app items and delegate
their unmodified clicks to the host while retaining native href fallbacks.
Consumers that deliberately want a direct
React Router Link integration can also install react-router and import:
import { ShellTopBar } from 'ptech-shell-ui/react-router';Quick start
import { Suspense } from 'react';
import { Routes, Route } from 'react-router';
import {
AppShellLayout,
HeaderProvider,
ShellTopBar,
useHeaderConfig,
NotFoundPage,
useRouteBreadcrumbs,
} from 'ptech-shell-ui';
import { Home } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ROUTE_META } from './config/routeConfig';
import { AppSidebar } from './AppSidebar';
import { UserMenu } from './UserMenu';
function DashboardPage() {
const { t } = useTranslation();
const breadcrumbs = useRouteBreadcrumbs({
routeMeta: ROUTE_META,
translate: t,
homeIcon: Home,
});
useHeaderConfig({
variant: 'breadcrumb',
breadcrumbs,
currentIcon: Home,
currentTitle: t('nav.dashboard'),
});
return <div>...</div>;
}
export function AppShell({ children }: { children?: React.ReactNode }) {
return (
<HeaderProvider>
<AppShellLayout
sidebar={<AppSidebar />}
header={
<ShellTopBar
trailing={<UserMenu />}
classNames={{
root: 'h-12 border-b border-ps-header bg-ps-header backdrop-blur-sm',
breadcrumbLink: 'text-ps-subtle hover:text-ps-aqua transition-colors truncate',
breadcrumbCurrent: 'flex items-center gap-1.5 shrink-0 font-semibold text-ps whitespace-nowrap',
breadcrumbCurrentIcon: 'w-4 h-4 text-ps-aqua',
}}
/>
}
>
<Suspense fallback={null}>
<Routes>
<Route path="dashboard" element={<DashboardPage />} />
</Routes>
</Suspense>
</AppShellLayout>
</HeaderProvider>
);
}Shared status surfaces
StatusPage is the shared, responsive presentation primitive for full-page
navigation and availability feedback. The package also exports semantic wrappers
for recurring shell states:
| Component | Default marker | Intended state |
| --- | --- | --- |
| NotFoundPage | 404 | The owning router cannot match a route |
| AccessDeniedPage | 403 | The signed-in user lacks tenant, app, organization, role, or permission access |
| AuthenticationRequiredPage | 401 | Sign-in or session renewal is required |
| UnexpectedErrorPage | 500 | An error boundary or request failed unexpectedly |
| ServiceUnavailablePage | 503 | An API, dependency, or maintenance window is temporarily unavailable |
| AppUnavailablePage | none | A Module Federation remote did not reach a usable screen |
| OfflinePage | none | Connectivity checks confirm that the client is offline |
| LoadingStatusPage | none | A full-page transition is still preparing consumer-owned content |
The wrappers only select a semantic component name and, where applicable, a
default numeric marker. Consumers still own routing, authorization, MSAL,
connectivity detection, launch deadlines, retry behavior, and support reporting.
Use StatusPage directly for a status that does not match one of the wrappers.
RemoteBoundary catches render and remote-mount failures without imposing a
brand. Supply either a localized fallback node or (error, reset) => ReactNode;
changing resetKey clears a captured failure. useDismissiblePopover standardizes
outside-pointer/Escape dismissal, while useDrawerA11y supplies dialog props,
focus entry/trapping, Escape handling, and focus restoration.
All copy comes from the consumer so applications can use their own i18n layer.
The first direct action child receives primary treatment; subsequent links or
buttons use the secondary style. detail is suitable for a safe pathname,
trace ID, or correlation ID; never pass a raw server error, access token,
credential, secret-bearing URL, or PII.
Route miss
import { Link, Route, Routes, useLocation } from 'react-router';
import { NotFoundPage } from 'ptech-shell-ui';
import { useTranslation } from 'react-i18next';
function RouteNotFound() {
const { pathname } = useLocation();
const { t } = useTranslation();
return (
<NotFoundPage
eyebrow={t('errors.notFound.eyebrow')}
title={t('errors.notFound.title')}
description={t('errors.notFound.description')}
detail={pathname}
actions={
<>
<Link to="/">{t('errors.notFound.goHome')}</Link>
<button type="button" onClick={() => window.history.back()}>
{t('errors.notFound.goBack')}
</button>
</>
}
/>
);
}
export function AppRoutes() {
return (
<Routes>
<Route path="dashboard" element={<DashboardPage />} />
<Route path="*" element={<RouteNotFound />} />
</Routes>
);
}Hosts and remotes should each keep a catch-all route at the boundary they own.
NotFoundPage supplies the presentation without deciding which router owns the
miss.
Access and availability
import { Link } from 'react-router';
import { useTranslation } from 'react-i18next';
import {
AccessDeniedPage,
AppUnavailablePage,
UnexpectedErrorPage,
} from 'ptech-shell-ui';
export function OrganizationAccessDenied() {
const { t } = useTranslation();
return (
<AccessDeniedPage
eyebrow={t('errors.accessDenied.eyebrow')}
title={t('errors.accessDenied.title')}
description={t('errors.accessDenied.description')}
actions={
<Link to="/apps">{t('errors.accessDenied.backToApps')}</Link>
}
/>
);
}
export function RemoteLaunchFailed({
retry,
supportTraceId,
}: {
retry: () => void;
supportTraceId?: string;
}) {
const { t } = useTranslation();
return (
<AppUnavailablePage
title={t('errors.appUnavailable.title')}
description={t('errors.appUnavailable.description')}
detail={supportTraceId}
actions={
<button type="button" onClick={retry}>
{t('errors.actions.retry')}
</button>
}
/>
);
}
export function ErrorBoundaryFallback({ supportTraceId }: { supportTraceId?: string }) {
const { t } = useTranslation();
return (
<UnexpectedErrorPage
title={t('errors.unexpected.title')}
description={t('errors.unexpected.description')}
detail={supportTraceId}
/>
);
}Do not use OfflinePage for every network exception: render it only after a
connectivity check confirms an offline client. A reachable shell with an
unavailable API should use ServiceUnavailablePage. Maintenance is a
service-unavailable variant, not a separate shared page.
All status components use PTECH semantic CSS variables when available and
include standalone fallbacks. Consumers may override custom properties through
style or a wrapper class and may replace the default artwork through
artwork. ptech-status-page is the generic root class; the legacy
ptech-not-found class and --ptech-nf-* custom properties remain present for
backward-compatible NotFoundPage theming.
Standalone bootstrap
Pair with initStandaloneApp from ptech-shell-dev and MSAL from ptech-shell-react:
import { initStandaloneApp } from 'ptech-shell-dev';
import { createMsalUserService } from 'ptech-shell-react';
initStandaloneApp({
appKey: 'my-remote',
apiBase: import.meta.env.PUBLIC_API_URL,
tenant: { tenantId: '...', tenantSlug: 'dev' },
createUserService: () =>
createMsalUserService({
msal: msalInstance,
defaultScopes: ['api://example/access_as_user'],
loginMode: 'redirect',
}),
defaultAuthScopes: ['api://example/access_as_user'],
});Exports
| Export | Purpose |
|--------|---------|
| AppShellLayout | Sidebar + header slots, scrollable content, --app-sidebar-width CSS var |
| HeaderProvider / useHeaderConfig | Page-driven top bar state |
| useHeaderActions | Inject header actions from nested components |
| ShellTopBar | Breadcrumb/overview top bar driven by header context |
| StatusPage | Base responsive, theme-aware status presentation with consumer-owned copy and actions |
| NotFoundPage | Route-miss status surface with a default 404 marker |
| AccessDeniedPage / AuthenticationRequiredPage | Presentational access and sign-in-required status surfaces |
| UnexpectedErrorPage / ServiceUnavailablePage | Presentational unexpected and temporary service failure surfaces |
| AppUnavailablePage / OfflinePage | Presentational remote-launch and confirmed-offline status surfaces |
| useRouteBreadcrumbs | Build parent crumbs from a route meta map |
| useAppPath / useAppPathResolver | Basename-aware hrefs via shell NavigationService |
The optional ./react-router subpath exports a React Router-backed
ShellTopBar with the same public props. Only that subpath imports
react-router; the root entry remains usable without it.
useAppPath, useAppPathResolver, and useCurrentAppPath subscribe to both
the SDK registry and the active navigation service. A component mounted before
TOKENS.navigation is registered therefore re-renders as soon as the host or
standalone bootstrap registers it; no remount is required. During static/server
rendering the hooks use the registry snapshot available for that render and
fall back to app-relative paths when no navigation service exists.
Build
npm run build -w ptech-shell-ui
npm run test -w ptech-shell-uiTests cover static rendering through react-dom/server and late service
registration through React 19 act plus jsdom. react-dom and jsdom are
development-only dependencies; the published UI runtime still relies only on
its existing peers.
