@blueforge-studio/essentials-kit

Shared React components every app needs: sheet/dialog (consent, PWA install and legal subpaths follow). React-only peers, props-only copy, CSS-variable theming

Informations sur le paquet

Latest Version0.4.1
LicenseMIT
Keywordsblueforge, react, sheet, dialog, consent, pwa
Repositorygit+https://github.com/blueforge-studio/blueforge-org.git
Author-
Homepagehttps://github.com/blueforge-studio/blueforge-org#readme
Bugs[object Object]

Versions

VersionPubliée
0.4.12026-10-16
0.4.02026-10-16
0.3.02026-10-16
0.2.02026-10-16
0.1.02026-10-16

Chaîne d'approvisionnement par version

VersionSigSBOMAtt
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0

Cliquez sur une version pour voir ses détails.

Chaîne d'approvisionnement

Provenance

Publié par
web:5
Quand
2026-10-16T08:49:13Z
sha256 (calculé par le registre)
3f964cb1fadc20ed975281be4d28e70b0862cf61c9eab1a57da2cb42fd93a423

Observé par le registre au moment de la publication.

Déclaré par la compilation

L'éditeur n'a rien déclaré sur sa compilation.

Pièces jointes

Signature
aucune
SBOM
aucune

Dépendances

Chargement du graphe de dépendances...

README

@blueforge-studio/essentials-kit

Shared React components every app needs, with React-only peers (react, react-dom >= 19): works in Vite and Next. Copy comes in as props, colours as CSS variables, so apps (and later the marketing CMS) can customise freely. Coming from lq-core PR #165? See MIGRATING-FROM-LQ-CORE.md. Design: docs/superpowers/specs/2026-10-16-essentials-kit-design.md.

Subpaths: /sheet, /pwa, /legal, /consent.

Install

pnpm add @blueforge-studio/essentials-kit
import '@blueforge-studio/essentials-kit/styles.css';
import { Sheet } from '@blueforge-studio/essentials-kit/sheet';

Sheet

Modal bottom sheet on narrow screens, centred dialog on wide ones. Focus trap, Escape to close, focus restored to the opener, page scroll locked (reference-counted, so stacked sheets are safe), role="dialog" + aria-modal + labelled by title. Rendered in a portal on document.body.

<Sheet open={open} onClose={() => setOpen(false)} title="Cookie preferences" closeLabel="Close" dismissOnBackdrop={false}>
  ...
</Sheet>
PropDefaultNotes
open, onClose, title, childrenrequiredtitle is the accessible name and the visible heading
placementautoauto (bottom < 640px, centred above) / bottom / center
dismissOnBackdroptrueset false for decisions that must be explicit
dismissOnEscapetrue
initialFocusReffirst focusable
closeLabel"Close"null renders no close button; translate by passing a string
classNameextra class on the panel

Theming: --ek-bg, --ek-fg, --ek-border, --ek-radius, --ek-sheet-width, --ek-z, --ek-backdrop, --ek-ring; falls back to shadcn tokens (--card, --card-foreground, --border, --ring), then to built-in colours.

Not included: animation beyond a short rise (respects prefers-reduced-motion), drag-to-dismiss, nested focus scopes.

InstallPrompt (/pwa)

import { InstallPrompt } from '@blueforge-studio/essentials-kit/pwa';

<InstallPrompt appName="LearnQuest" hold={!cookieNoticeAnswered} storageKey="lq_install" labels={{ maybeLater: 'Senere' }} />
  • Chromium/Android: a small banner that captures beforeinstallprompt and triggers the browser's own prompt.
  • iOS Safari (which never fires that event): a sheet with the Share -> Add to Home Screen steps and a pointer at the Share button (bottom on iPhone, top-right on iPad).
  • iOS in-app browsers / other browsers: a sheet telling the user to open Safari, with a copy-link button.
  • Nothing shows when the app is already installed (standalone, navigator.standalone, Capacitor), on desktop, or while hold is true.
  • Dismissals snooze 7, 30, then 90 days, then stop. "Don't ask again" and a completed install are permanent. Set storageKey per app if several share an origin.
PropDefaultNotes
appNamerequiredsubstituted for {app} in the text
labelsEnglishevery string; tokens {app}, {where} (iPhone/iPad), {browser}
iosDelayMs3000keeps the iOS sheet off the first paint
holdfalsee.g. until a cookie notice is answered
storageKey, onEvent`onEvent('captured'
env, isMobiledetectedoverrides for tests and previews

Building blocks are exported too: useInstallPrompt, IosInstallSheet, SharePointer, detectInstallPlatform, classifyIosBrowser, readDismissState, recordDismissal, isSnoozed. Extracted from LearnQuest's lq-core (PR #165). Known limitation: the Messages/Mail link preview sends a stock Safari UA and looks like Safari. The iOS sheet is modal (it uses Sheet), unlike LearnQuest's non-modal card.

LegalDocument and Markdown (/legal)

import { LegalDocument } from '@blueforge-studio/essentials-kit/legal';

<LegalDocument source={privacyMarkdown} entity={{ releaseStage: 'live', company: { name: 'Acme ApS' } }} renderLink={RouterLink} />

Renders one legal document from Markdown you supply (a file, the CMS, anywhere). {{company.name}}-style placeholders are filled from entity; unknown ones stay visible and are listed in an alert, so a missing value is never silently blank. A provisional notice shows unless entity.releaseStage === 'live' (override with provisional; reword with provisionalNotice / unresolvedLabel). No page chrome: the app owns its header, footer and routing.

Markdown is a small renderer (headings, paragraphs, lists, tables, blockquotes, rules, bold, italic, code, links). It builds React elements and never injects raw HTML; link targets are limited to http(s), mailto and in-app paths, so a javascript: URL becomes plain text. Pass renderLink to use your router's Link (internal is true for in-app paths). Style it with --ek-* variables (.ek-md). Not supported: nested lists beyond continuation lines, images, footnotes, inline HTML. What the wording must say is the app's and counsel's call.

ConsentManager and createConsentStore (/consent)

import { createConsentStore, ConsentManager } from '@blueforge-studio/essentials-kit/consent';

// Once, at module level. `version` is required: bumping it is a legal decision that forces a re-ask.
export const consent = createConsentStore({ key: 'myapp_consent', version: 1, categories: ['analytics'] });

<ConsentManager
  store={consent}
  categories={[{ id: 'analytics', title: 'Analytics', description: 'Anonymous usage statistics. Off unless you allow it.' }]}
  policyHref="/cookies"
  labels={{ bannerText: '...' }}
/>

if (consent.has('analytics')) startAnalytics();          // undecided === no
consent.subscribe(() => { /* start/stop on change; also fires for other tabs */ });
<button onClick={() => consent.requestPreferences()}>Manage cookies</button>
  • Strictly-necessary storage is shown as "always on" and is not a category. Every optional category is off until opted in.
  • The banner has "Choose", "Essential only" and "Allow all"; the last two are styled identically so declining is as easy as accepting. Closing the dialog without saving records no decision.
  • The record stores { version, decidedAt, choices, content? }. Pass content: { id, version } (e.g. the CMS item id and version of the cookie text the user saw) so a decision can be audited later.
  • A choice made under another version is ignored, so people are asked again. If storage is blocked the choice still applies for the page view.
  • All strings are labels; pass translations (or CMS text). renderPolicyLink takes your router's Link.
  • Framework-free core: createConsentStore works without React; useConsent(store) is the React hook.

The package does storage and gating. Whether a banner is required, what the wording must say and when a re-ask is legally required are the app's and counsel's call. It does not load or block any scripts itself.