@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
Información del paquete
Versiones
| Versión | Publicada |
|---|---|
| 0.4.1 | 2026-10-16 |
| 0.4.0 | 2026-10-16 |
| 0.3.0 | 2026-10-16 |
| 0.2.0 | 2026-10-16 |
| 0.1.0 | 2026-10-16 |
Cadena de suministro por versión
| Versión | Firma | SBOM | Att |
|---|---|---|---|
| 0.4.1 | |||
| 0.4.0 | |||
| 0.3.0 | |||
| 0.2.0 | |||
| 0.1.0 |
Haz clic en una versión para ver sus detalles.
Cadena de suministro
Procedencia
- Publicado por
- web:5
- Cuándo
- 2026-10-16T08:49:13Z
- sha256 (calculado por el registro)
- 3f964cb1fadc20ed975281be4d28e70b0862cf61c9eab1a57da2cb42fd93a423
Observado por el registro en el momento de publicar.
Declarado por la compilación
Quien publicó no declaró nada sobre su compilación.
Adjuntos
- Firma
- ninguno
- SBOM
- ninguno
Dependencias
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>
| Prop | Default | Notes |
|---|---|---|
open, onClose, title, children | required | title is the accessible name and the visible heading |
placement | auto | auto (bottom < 640px, centred above) / bottom / center |
dismissOnBackdrop | true | set false for decisions that must be explicit |
dismissOnEscape | true | |
initialFocusRef | first focusable | |
closeLabel | "Close" | null renders no close button; translate by passing a string |
className | extra 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
beforeinstallpromptand 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 whileholdis true. - Dismissals snooze 7, 30, then 90 days, then stop. "Don't ask again" and a completed install are permanent. Set
storageKeyper app if several share an origin.
| Prop | Default | Notes |
|---|---|---|
appName | required | substituted for {app} in the text |
labels | English | every string; tokens {app}, {where} (iPhone/iPad), {browser} |
iosDelayMs | 3000 | keeps the iOS sheet off the first paint |
hold | false | e.g. until a cookie notice is answered |
storageKey, onEvent | `onEvent('captured' | |
env, isMobile | detected | overrides 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? }. Passcontent: { 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
versionis 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).renderPolicyLinktakes your router's Link. - Framework-free core:
createConsentStoreworks 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.