@plasius/sharedcomponents
v1.2.3
Published
Base React UI components for shared navigation, menus, profile shells, and legal/contact surfaces.
Readme
@plasius/sharedcomponents
Base React UI package for cross-application navigation and shared legal/contact UI.
Scope
This package is intentionally a base component layer:
- No auth coupling
- No profile store coupling
- No environment/service coupling
- No router coupling in core components
If a product needs auth/profile behavior, wire it via callbacks/props from the host app.
Included Components
Header: configurable nav with optional profile slot and mobile context menuFooter: configurable legal/footer links with mobile context menuStarRating: controlled, touch-sized one-to-five native radiogroupConstrainedRichTextEditor: lazy, allowlisted transient rich-text editorContactDetails: reusable legal contact block with configurable detailsContextMenu: generic context menu surfaceActionMenu: controlled touch-first overflow menu with an anchored popover and phone-sheet presentationReviewSheet: controlled modal review surface with responsive side-sheet and phone presentationsUserProfile: optional generic avatar/menu shell driven by callbacksConfirmationDialog: reusable confirmation dialog with optional typed challenge flow for destructive actionsStatusPanel: reusable status/alert surface for loading, empty, warning, and retryable error statesCollectionViewport: native scroll region with continuous append, pull-down refresh and accessible button alternativesuseProgressiveItems: reveal an already sorted/filtered collection in batches while keeping a selected row mounted- Built-in interaction analytics forwarding through
@plasius/analytics - Package-owned default display text resolved through
@plasius/translations
Install
npm install @plasius/sharedcomponentsModule formats
This package publishes dual ESM and CJS artifacts.
When CJS output is emitted under dist-cjs/*.js with type: module, dist-cjs/package.json is generated with { "type": "commonjs" } to ensure Node require(...) compatibility.
Continuous collections
Wrap a stable table/list in CollectionViewport, provide a region label, and
pass localized labels for refresh/load-more/loading/refreshing/pull/release/end/
failed/refreshBlocked. onLoadMore(signal) appends the next available page when
the user reaches or scrolls further at the bottom; hasMore=false ends loading.
onRefresh(signal) handles pull-and-release at the top and the Refresh button.
Both callbacks may return promises. The component serializes gestures and
buttons, reports rejected operations without exposing their error contents,
and aborts its signal on unmount or resetKey changes. Hosts must honour the
signal or use query-generation checks, and own request timeouts and data state.
Use disabled during commits, and refreshDisabled while a refresh could discard
an unsaved draft. Refresh should retain the old rows on failure. The component
does not mutate data, fetch endpoints, or evaluate authority/rollout policy.
Its native scrollbar measures the actual loaded content, including expanding
editors. The viewport opts out of layout-driven scroll anchoring; a native scroll
event following a completed wheel/touch append cannot load the same extent twice.
Further deliberate wheel/touch input, button activation or returning to the bottom
can still request another page. Native scrolling, keyboard controls and inline
editors remain available. Scrollbar visibility follows browser/OS preferences. Viewport height is
bounded to 65% of the dynamic viewport (maximum 48rem); a host class can override
layout styles. No synthetic unknown-total scroll range or automatic background
page-draining loop is used.
For complete-array endpoints, call useProgressiveItems(sortedFilteredItems,
{ resetKey, batchSize: 50, keepVisibleIndex }) and render its items. Pass its
hasMore and loadMore to the viewport. Query changes reset the visible batch;
keepVisibleIndex ensures an existing selected editor remains mounted.
Usage
import { useState } from "react";
import {
ContactDetails,
Footer,
Header,
SharedComponentsBrandingProvider,
UserProfile,
type SharedComponentsMetadataInput,
} from "@plasius/sharedcomponents";
const navHeaderItems = [
{ name: "Hexagons", url: "/hexagons" },
{ name: "About", url: "/about" },
];
const navFooterItems = [
{ name: "Privacy", url: "/privacy" },
{ name: "Terms", url: "/terms-of-service" },
];
const sharedMetadata: SharedComponentsMetadataInput = {
organizationName: "Example Organization",
website: "https://example.com",
websiteLabel: "example.com",
contactEmail: "[email protected]",
contactTeamName: "Legal Team",
contactAddressLines: ["123 Example Street", "Sample City", "Sample Region", "00000"],
analytics: {
endpoint: "https://analytics.example.com/collect",
source: "@plasius/sharedcomponents",
context: {
tenant: "example-tenant",
environment: "production",
},
},
};
<SharedComponentsBrandingProvider metadata={sharedMetadata}>
<Header
items={navHeaderItems}
brand={<img src="/brand-logo.svg" alt="Example Organization Logo" />}
profileSlot={
<UserProfile
user={{ firstName: "Ada", lastName: "Lovelace" }}
onOpenSettings={() => console.info("settings")}
onLogout={() => console.info("logout")}
onLogin={(provider) => console.info("login", provider)}
/>
}
/>
<Footer items={navFooterItems} />
<ContactDetails />
</SharedComponentsBrandingProvider>;Header, Footer, and ContactDetails require a branding metadata reference.
Provide it once with SharedComponentsBrandingProvider (recommended), or per component using the metadata prop.
Touch-first action and review surfaces
ActionMenu and ReviewSheet are controlled presentation components. The host
owns open state, authorization, draft state, validation, and persistence. At
widths above 40rem, ActionMenu is anchored to its trigger and ReviewSheet
is a modal right-side overlay. At 40rem and below, both adapt to full-width
touch sheets. The review surface contains keyboard focus, blocks background
interaction, and identifies itself as modal at every presentation width.
import {
ActionMenu,
ReviewSheet,
type ReviewSheetCloseReason,
} from "@plasius/sharedcomponents";
const [actionsOpen, setActionsOpen] = useState(false);
const [reviewOpen, setReviewOpen] = useState(false);
function closeReview(_reason: ReviewSheetCloseReason) {
setReviewOpen(false);
}
<ActionMenu
open={actionsOpen}
label="User actions"
triggerLabel="Open user actions"
trigger={<span aria-hidden="true">•••</span>}
items={[
{
id: "review",
label: "Review change",
onSelect: () => setReviewOpen(true),
},
{
id: "remove",
label: "Remove avatar",
tone: "danger",
onSelect: () => setReviewOpen(true),
},
]}
onOpenChange={setActionsOpen}
/>;
<ReviewSheet
open={reviewOpen}
title="Review user change"
description="Check the before and after values before committing."
closeLabel="Close review"
onClose={closeReview}
footer={<button type="button">Commit change</button>}
>
<dl>{/* caller-owned review details */}</dl>
</ReviewSheet>;Both components provide 44×44 CSS-pixel minimum touch targets, Escape and
outside-pointer dismissal, safe-area padding, reduced-motion handling,
high-contrast focus indicators, and caller-translatable accessible labels.
ActionMenu implements wrapping arrow, Home, and End navigation and returns
focus to its trigger. ReviewSheet reports why close was requested and returns
focus for explicit close, Escape, and outside dismissal.
See Touch-first action surfaces for the
complete API and host responsibilities. The coordinate-based ContextMenu
accepts either label or labelledBy so callers can name the menu. Its Tab
dismissal runs after the browser's native focus action, avoiding focus loss
when the popup is removed. It preserves the active enabled command across
structurally equivalent rerenders and moves to the next enabled command, then
the preceding command, when the active command becomes unavailable. Header,
Footer, and UserProfile menus expose their popup relationship and return focus
to their opener on Escape. The Footer trigger also treats one real pointer
activation as one close operation instead of an outside dismissal followed by
an immediate reopen.
Privacy-safe feedback controls
Footer accepts explicit links and host-owned actions. Actions render as
44×44 native buttons on desktop and as disabled-aware mobile menu commands:
import { FOOTER_FEEDBACK_ACTION_ID } from "@plasius/sharedcomponents";
const feedbackItems = [
{
kind: "link" as const,
id: "privacy",
name: "Privacy",
url: "/privacy",
},
{
kind: "action" as const,
id: FOOTER_FEEDBACK_ACTION_ID,
name: "Rate us or report a bug",
icon: <span aria-hidden="true">★</span>,
onSelect: () => setFeedbackOpen(true),
},
];
<Footer items={feedbackItems} />;Import FOOTER_FEEDBACK_ACTION_ID from @plasius/sharedcomponents rather than
repeating the identifier in host code. The identifier is only a stable
host/component contract: feedback actions emit no package-owned analytics,
create no analytics session, and use no browser analytics queue. Hosts must
also keep feedback form state and content out of any telemetry they add around
onSelect. This exclusion covers the complete mobile path: when an enabled
item has the reserved feedback identity, opening or closing the shared footer
menu also emits no package telemetry. The exact reserved identity remains
private if a host accidentally supplies it on a link, while unrelated footer
links and menus—including menus where feedback is disabled—retain their
existing analytics behavior. Feedback eligibility is captured for each mobile
menu open. If an enabled feedback item appears while an ordinary tracked menu
is already open, the package keeps that command non-invokable, closes the menu,
restores trigger focus, and requires a fresh telemetry-free open.
A menu opened with feedback available remains telemetry-free through dismissal
even if the host revokes feedback before it closes.
StarRating exposes exactly five caller-translated native radio options with
Arrow/Home/End keyboard behavior and visible shape, border, and text state.
ConstrainedRichTextEditor is dynamically loaded and emits only the transient
feedback AST: paragraphs or bullet items, depths 0–4, and bold/italic/underline
text leaves. Its exact 4,000-Unicode-code-point budget includes inter-block
newlines and is bounded again at 8,000 UTF-16 code units.
import {
ConstrainedRichTextEditor,
StarRating,
type FeedbackRichTextDocument,
type StarRatingValue,
} from "@plasius/sharedcomponents";
const [rating, setRating] = useState<StarRatingValue | null>(null);
const [narrative, setNarrative] =
useState<FeedbackRichTextDocument | null>(null);
<StarRating
label="Overall satisfaction"
labels={["Very poor", "Poor", "Fair", "Good", "Excellent"]}
value={rating}
onChange={setRating}
required
/>;
<ConstrainedRichTextEditor
labels={{
editor: "Tell us more",
toolbar: "Text formatting",
bold: "Bold",
italic: "Italic",
underline: "Underline",
bullets: "Bulleted list",
indent: "Increase indent",
outdent: "Decrease indent",
loading: "Loading editor",
}}
placeholder="Optional details"
value={narrative}
onChange={setNarrative}
/>;The editor uses no innerHTML, dangerouslySetInnerHTML, or execCommand.
Paste is plain text only; links, images, attachments, code, mentions, embedded
metadata, HTML/link syntax, arbitrary formatting, and drops are not
represented. The AST and
extractFeedbackRichText output remain sensitive live-browser data: hosts must
redact, validate, and encrypt before sending, and must never log, persist,
cache, or attach them to analytics.
Cancelable edits are admitted through a native beforeinput listener. Any
unintercepted input outside an active validated composition is rolled back to
the canonical model without emitting onChange. Mutation commands require a
freshly mapped browser selection; cached ranges are never used as a fallback.
Canonical recovery keeps the textbox element mounted, does not report an
internal blur, and restores focus only when focus has not genuinely left.
Caret-only empty paragraphs/list items remain private to the mounted editor.
They count toward the limits but are never emitted; onChange receives only a
canonical AST whose blocks and text leaves are non-empty.
Runtime model helpers are intentionally isolated from the root entry so the editor model, editing state, and full stylesheet do not join the application shell. Import model helpers only inside the lazy feedback flow:
import {
extractFeedbackRichText,
normaliseFeedbackRichTextDocument,
} from "@plasius/sharedcomponents/feedback-rich-text-model";See Privacy-safe feedback primitives and ADR-0005 for the complete host boundary and schema-compatibility contract.
Release gate: the feedback editor now consumes the
@plasius/schema ^1.4.0 Unicode-profile helper through its lazy model path.
Schema 1.4.0 is published through its protected workflow, and the candidate
lock and package gates have been reproduced from a clean registry-only install.
This package is released only through protected cd.yml; release-metadata
checkout does not persist the workflow GITHUB_TOKEN, leaving the narrowly
scoped release-preparation GitHub App token as the sole branch-mutation
credential. A successful merge command is only an accepted request: preparation
continues only after GitHub reports MERGED, and fails on closed, unreadable,
invalid, or timed-out PR state. Workflow policy tests also compile the embedded version/pre-release
parser and require archive-member checks to drain their input, so malformed
release hand-offs fail closed without false negatives from shell pipefail.
Downloaded tarballs are published as explicit local package specs; if a failed
attempt has already sealed an immutable tag to an older commit, preparation
advances to a fresh version rather than moving that tag.
Translations
Package-owned labels, default action names, accessibility labels, and fallback helper text are exposed as en-GB dictionaries and resolved through @plasius/translations.
Components keep English fallback defaults when a host has not loaded the package dictionary, while host applications can load or override the same keys through the shared translator.
import { getTranslator } from "@plasius/translations";
import { sharedComponentsTranslations } from "@plasius/sharedcomponents";
const i18n = getTranslator();
for (const [language, dictionary] of Object.entries(sharedComponentsTranslations)) {
i18n.loadTranslations(language, dictionary);
}Interaction Analytics
When metadata.analytics.endpoint is configured, sharedcomponents automatically tracks user interactions for:
- Header nav, brand click, and mobile menu flows
- Footer contact/nav clicks and mobile menu flows
- Contact details email/website clicks
- User profile avatar/menu command interactions (when branding metadata is available)
This keeps analytics endpoint configuration in one white-label metadata object.
Suitability Checklist
Use @plasius/sharedcomponents as your base package when your component:
- is reusable across products
- can be configured only through props/callbacks
- does not import product/domain stores
- does not require backend/service SDKs directly
Do not add components here if they need app-specific business logic or service wiring.
Development
npm install
npm run typecheck
npm run build
npm test
npm run test:coverageGovernance & ADRs
- Security policy: SECURITY.md
- Code of conduct: CODE_OF_CONDUCT.md
- ADRs: docs/adrs
- Base package review: docs/base-package-review.md
- Legal docs: legal
License
MIT
Release integrity
CI keeps the administrative contributor registry outside Git and npm package
artifacts using exact, case-normalised path checks. External fork heads are
excluded from CI triggers. Every CI job runs only for repository-owned pushes
on explicit [self-hosted, Linux, X64] labels in Public CI - Quarantined;
there is no hosted CI fallback or configurable runner selector. The monthly
audit uses the same group and is restricted to main. Release preparation and
publication use a two-run exact-main protocol on GitHub-hosted Node.js 24.18.0
LTS. A read-only job seals the package tarball and SBOM before a dependency-free
production job publishes that exact artifact through npm OIDC with provenance;
there is no npm write-token fallback. Before enabling CD, independently verify
the npm trusted publisher binding and protected-branch-only production
environment. Disable cd.yml to stop release promotion; never restore the
administrative path or token publication.
Protect and freeze each reviewed implementation or release-metadata branch before temporarily admitting its exact workflow/ref to the quarantined group. Required push checks remain mandatory on the PR. Remove temporary admissions after merge; retain only approved main CI and main-only audit refs. See ADR-0008 for trust boundaries and verification.