@plasius/sharedcomponents

v1.2.3

Published

Base React UI components for shared navigation, menus, profile shells, and legal/contact surfaces.

Readme

@plasius/sharedcomponents

npm version Build Status coverage License Code of Conduct Security Policy Changelog

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 menu
  • Footer: configurable legal/footer links with mobile context menu
  • StarRating: controlled, touch-sized one-to-five native radiogroup
  • ConstrainedRichTextEditor: lazy, allowlisted transient rich-text editor
  • ContactDetails: reusable legal contact block with configurable details
  • ContextMenu: generic context menu surface
  • ActionMenu: controlled touch-first overflow menu with an anchored popover and phone-sheet presentation
  • ReviewSheet: controlled modal review surface with responsive side-sheet and phone presentations
  • UserProfile: optional generic avatar/menu shell driven by callbacks
  • ConfirmationDialog: reusable confirmation dialog with optional typed challenge flow for destructive actions
  • StatusPanel: reusable status/alert surface for loading, empty, warning, and retryable error states
  • CollectionViewport: native scroll region with continuous append, pull-down refresh and accessible button alternatives
  • useProgressiveItems: 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/sharedcomponents

Module 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:coverage

Governance & ADRs

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.