Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
# @suaveplan/react-packer - React 19 companion for @suaveplan/packer-browser — usePackedResource, usePackedImage, usePackedAudio, usePackedVideo, usePackedFont hooks plus PackedResourceProvider context. Consumes a real PackedResourceManager; React is a peer dep so non-React consumers never pay the cost.
React 19 companion for `@suaveplan/packer-browser`. Ships
`usePackedResource`, `usePackedImage`, `usePackedAudio`,
`usePackedVideo`, and `usePackedFont` hooks plus a
`PackedResourceProvider` context, so applications can consume packed
asset archives from React components without `@suaveplan/packer-browser`
having to ship React as a runtime dep.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical error-code registry and concrete error class for the React-tier packer companion. Every failure m…
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical error-code registry and concrete error class for the React-tier packer companion. Every failure m…
- [use-packed-audio](./src/hooks/use-packed-audio/use-packed-audio.md) — `usePackedAudio` is the audio analogue of `usePackedImage`. It binds a packed audio asset URL to a React component's li…
- [use-packed-audio](./src/hooks/use-packed-audio/use-packed-audio.md) — `usePackedAudio` is the audio analogue of `usePackedImage`. It binds a packed audio asset URL to a React component's li…
- [use-packed-font](./src/hooks/use-packed-font/use-packed-font.md) — `usePackedFont` loads a packed font asset and registers it with the document so subsequent rules referencing the suppli…
- [use-packed-font](./src/hooks/use-packed-font/use-packed-font.md) — `usePackedFont` loads a packed font asset and registers it with the document so subsequent rules referencing the suppli…
- [use-packed-image](./src/hooks/use-packed-image/use-packed-image.md) — `usePackedImage` is a thin wrapper around `usePackedResource` that pre-fills sensible defaults for image assets: cachin…
- [use-packed-image](./src/hooks/use-packed-image/use-packed-image.md) — `usePackedImage` is a thin wrapper around `usePackedResource` that pre-fills sensible defaults for image assets: cachin…
- [use-packed-resource](./src/hooks/use-packed-resource/use-packed-resource.md) — `usePackedResource` is the foundation hook of `@suaveplan/react-packer`. It binds a packed-archive virtual URL (e.g. `/…
- [use-packed-resource](./src/hooks/use-packed-resource/use-packed-resource.md) — `usePackedResource` is the foundation hook of `@suaveplan/react-packer`. It binds a packed-archive virtual URL (e.g. `/…
- [use-packed-video](./src/hooks/use-packed-video/use-packed-video.md) — `usePackedVideo` binds a packed video asset URL to a React component's lifecycle. Unlike the image and audio variants,…
- [use-packed-video](./src/hooks/use-packed-video/use-packed-video.md) — `usePackedVideo` binds a packed video asset URL to a React component's lifecycle. Unlike the image and audio variants,…
- [packed-resource-provider](./src/provider/packed-resource-provider/packed-resource-provider.md) — `PackedResourceProvider` lets a React tree explicitly inject a `PackedResourceManager` from `@suaveplan/packer-browser`…
- [packed-resource-provider](./src/provider/packed-resource-provider/packed-resource-provider.md) — `PackedResourceProvider` lets a React tree explicitly inject a `PackedResourceManager` from `@suaveplan/packer-browser`…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/react-packer @suaveplan/packer-browser
```
`react` and `react-dom` (≥ 19.0.0) are peer dependencies — the package
does not bundle them.
## Quick start
```tsx
import { createPackedResourceManager } from "@suaveplan/packer-browser";
import { PackedResourceProvider, usePackedImage } from "@suaveplan/react-packer";
const manager = createPackedResourceManager();
function App() {
return (
<PackedResourceProvider value={{ manager }}>
<Logo />
</PackedResourceProvider>
);
}
function Logo() {
const { url, loading, error } = usePackedImage("/assets/logo.packed");
if (loading) return <div className="logo logo--loading" />;
if (error !== null) return <div className="logo logo--error" />;
return url !== null ? <img alt="logo" src={url} /> : null;
}
```
If you do not mount a `PackedResourceProvider`, hooks fall back to the
singleton at `@suaveplan/packer-browser/singleton`. Adopters that prefer
zero-config get a working manager for free; adopters that want explicit
lifecycle (tests, SSR, multi-tenant apps) mount a provider and skip the
singleton entirely.
## Features
- Five hooks: `usePackedResource` (generic), `usePackedImage`,
`usePackedAudio`, `usePackedVideo`, `usePackedFont`.
- React 19 context provider for explicit `PackedResourceManager` and
`MessageResolver` injection.
- `AbortSignal` honored throughout — pre-aborted signals short-circuit,
unmount cancels the in-flight load, and changing the packed URL
aborts the previous one.
- Stable error registry — `REACT_PACKER_CODES` + `ReactPackerError`
branded subclass over `@suaveplan/error`.
- `cleanup` releases the cached entry on unmount so the manager's LRU
can evict.
## Modules
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md)
- [`src/types/react-packer.types.md`](./src/types/react-packer.types.md)
- [`src/provider/packed-resource-provider/packed-resource-provider.md`](./src/provider/packed-resource-provider/packed-resource-provider.md)
- [`src/hooks/use-packed-resource/use-packed-resource.md`](./src/hooks/use-packed-resource/use-packed-resource.md)
- [`src/hooks/use-packed-image/use-packed-image.md`](./src/hooks/use-packed-image/use-packed-image.md)
- [`src/hooks/use-packed-audio/use-packed-audio.md`](./src/hooks/use-packed-audio/use-packed-audio.md)
- [`src/hooks/use-packed-video/use-packed-video.md`](./src/hooks/use-packed-video/use-packed-video.md)
- [`src/hooks/use-packed-font/use-packed-font.md`](./src/hooks/use-packed-font/use-packed-font.md)

View File

@@ -0,0 +1,94 @@
# `error-codes` — stable error registry for `@suaveplan/react-packer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the canonical error-code registry and concrete error class for
the React-tier packer companion. Every failure mode surfaced through
`usePackedResource` and its variants funnels through `ReactPackerError`,
which extends `@suaveplan/error`'s `SuaveplanError` so consumers get
cause-chain traversal, OTel span-event tagging, and i18n message
resolution for free.
This module is the single source of truth for `REACT_PACKER_*` codes —
no other file may invent new ones. Hooks and the provider import the
exported constants and class to raise typed errors; tests assert on the
code constants rather than string-comparing message keys.
## Features
- `REACT_PACKER_CODES` — frozen registry of three codes: `LOAD_FAILED`,
`FONT_LOAD_FAILED`, `ABORTED`.
- `ReactPackerError` — `SuaveplanError` subclass with a unique brand
`Symbol` so cross-realm `instanceof` failures still permit safe
discrimination via `ReactPackerError.is(value)`.
- `cause` preservation — when a hook wraps a `PackerBrowserError` or a
`DOMException` from `document.fonts.load`, the original error rides on
`.cause` for downstream debugging and OTel attribute capture.
- Stable shape — adding new codes requires a new OpenSpec change so the
public registry never drifts mid-version.
## Usage: Basic
```ts
import { REACT_PACKER_CODES, ReactPackerError } from "@suaveplan/react-packer";
try {
await loadSomething();
} catch (cause) {
throw new ReactPackerError(
REACT_PACKER_CODES.LOAD_FAILED,
"react-packer.load-failed",
{ cause }
);
}
```
## Usage: Advanced
```ts
import { REACT_PACKER_CODES, ReactPackerError } from "@suaveplan/react-packer";
function onError(value: unknown): string {
if (ReactPackerError.is(value)) {
switch (value.code) {
case REACT_PACKER_CODES.LOAD_FAILED:
return "Could not unpack the resource.";
case REACT_PACKER_CODES.FONT_LOAD_FAILED:
return "Font failed to load; falling back to system font.";
case REACT_PACKER_CODES.ABORTED:
return "Cancelled.";
default:
return "Unexpected error.";
}
}
return "Unknown error.";
}
```
## API Reference
- `REACT_PACKER_CODES` — `Readonly<{ LOAD_FAILED, FONT_LOAD_FAILED, ABORTED }>`
registered via `@suaveplan/error`'s `defineCodes("REACT_PACKER", ...)`.
- `ReactPackerErrorCode` — union of the values of `REACT_PACKER_CODES`.
- `ReactPackerError(code, messageKey, options?)` — constructor accepts
the registered code, an i18n message key, and the standard
`SuaveplanErrorOptions` (including `cause` and `params`).
- `ReactPackerError.is(value: unknown): value is ReactPackerError` —
returns `true` only when `value` is a real `ReactPackerError` carrying
the brand symbol.
## Implementation Notes
- The brand `Symbol` is module-local, so passing a forged object with
the same shape will not satisfy `is(...)`. This is intentional —
duck-typed substitutes break OTel reporting and cause chains.
- `SuaveplanError` already pins `.code` and `.message` (resolved from
the messageKey) as read-only fields; subclasses do not need to
redeclare them.
- The brand assignment in the constructor is what makes `super(code, ...)`
safe — TypeScript's definite-assignment analysis is satisfied by the
`public readonly [BRAND]: true` field plus the constructor write.

View File

@@ -0,0 +1,81 @@
# `usePackedAudio` — audio-tuned packed-resource hook
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`usePackedAudio` is the audio analogue of `usePackedImage`. It binds a
packed audio asset URL to a React component's lifecycle, defaults to
caching the underlying bytes (audio cues are typically re-triggered),
and narrows the manager's MIME allow-list to `audio/`-prefixed types so
misconfigured archives serving non-audio bytes fail fast at the loader
rather than at `<audio>` decode time.
The return shape matches `UsePackedResourceReturn` so callers swap
between variant hooks without touching downstream rendering code.
## Features
- `cache: true` default; opt out per call.
- `allowedMimePrefixes: ["audio/"]` default.
- Forwards `verifyKey`, `verifyChecksum`, and `signal` to the underlying
generic hook.
- Identical return shape to `usePackedResource`.
## Usage: Basic
```tsx
import { usePackedAudio } from "@suaveplan/react-packer";
function CueButton({ cue }: { cue: string | null }) {
const { url, loading } = usePackedAudio(cue);
if (loading || url === null) return <button disabled>…</button>;
return (
<button type="button" onClick={() => new Audio(url).play()}>
Play
</button>
);
}
```
## Usage: Advanced
```tsx
import { usePackedAudio } from "@suaveplan/react-packer";
function SignedCue({
cue,
verifyKey,
}: {
cue: string;
verifyKey: Uint8Array;
}) {
const { url, error, reload } = usePackedAudio(cue, {
verifyChecksum: true,
verifyKey,
allowedMimePrefixes: ["audio/ogg", "audio/webm"],
});
if (error !== null) return <button onClick={reload}>Retry</button>;
return url !== null ? <audio controls src={url} /> : null;
}
```
## API Reference
- `usePackedAudio(packedUrl, options?): UsePackedResourceReturn`
- Defaults differ from `usePackedResource`:
- `cache` defaults to `true`.
- `allowedMimePrefixes` defaults to `["audio/"]`.
- All other options pass through.
## Implementation Notes
- Explicit caller options always win. Passing `{ cache: false }`
disables caching for that hook instance; passing
`{ allowedMimePrefixes: [] }` opts out of MIME enforcement entirely.
- The default MIME prefix is checked against the response's
`Content-Type` by the loader; the hook itself does not parse MIME.
- Audio decoding remains the consumer's responsibility — the hook only
produces the Blob URL.

View File

@@ -0,0 +1,122 @@
# `usePackedFont` — font-tuned packed-resource hook
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`usePackedFont` loads a packed font asset and registers it with the
document so subsequent rules referencing the supplied `font-family`
render with the correct face. The hook builds on `usePackedResource`
for the load lifecycle and adds:
1. `@font-face` rule injection — a `<style id="packed-font-<family>">`
element is appended to `document.head` once the Blob URL is ready.
2. `document.fonts.load("1em <family>")` waiting — the hook exposes a
`fontLoaded` flag that flips to `true` once the browser reports the
face as loaded, so consumers can opt into FOIT/FOUT strategies.
3. Cleanup — the injected style element is removed when the component
unmounts or the font family changes.
## Features
- `cache: true` default with a font-friendly MIME allow-list covering
`font/`, `application/font*`, and `application/octet-stream`.
- Idempotent style injection keyed by `packed-font-<family>` — multiple
components mounting the same family share the rule.
- `fontLoaded` flag wired to `document.fonts.load`, with a safe
fallback when the document or the font loading API is unavailable
(SSR, Node tests).
- Failure to load a font surfaces as
`ReactPackerError(FONT_LOAD_FAILED)`; the underlying byte load still
exposes its own errors through `result.error` when relevant.
- Strict TS: no `any` casts, no implicit `document` access — every
global read goes through a typed adapter so non-DOM environments
return clean defaults.
## Usage: Basic
```tsx
import { usePackedFont } from "@suaveplan/react-packer";
function Heading({ src }: { src: string }) {
const { fontLoaded } = usePackedFont(src, "BodyText");
return (
<h1
style={{
fontFamily: fontLoaded ? "BodyText" : "system-ui",
}}
>
Hello
</h1>
);
}
```
## Usage: Advanced
```tsx
import { usePackedFont } from "@suaveplan/react-packer";
function SignedHeading({
src,
family,
key,
}: {
src: string;
family: string;
key: Uint8Array;
}) {
const { fontLoaded, error, reload } = usePackedFont(src, family, {
verifyChecksum: true,
verifyKey: key,
});
if (error !== null)
return (
<button onClick={reload}>Retry ({error.code})</button>
);
return (
<h1
style={{
fontFamily: fontLoaded ? family : "system-ui",
opacity: fontLoaded ? 1 : 0.6,
transition: "opacity 120ms ease",
}}
>
Hello
</h1>
);
}
```
## API Reference
- `usePackedFont(packedUrl, fontFamily?, options?): UsePackedFontReturn`
- `packedUrl: string | null | undefined` — the packed asset URL.
- `fontFamily?: string` — the family name to use in the injected rule.
When omitted, no `@font-face` rule is injected and `fontLoaded`
remains `false`, but the underlying Blob URL is still produced.
- `options: UsePackedResourceOptions` — forwarded to `usePackedResource`
with font-tuned defaults overlaid (`cache: true`, font-friendly MIME
allow-list).
- Returns `UsePackedFontReturn` — `UsePackedResourceReturn` plus
`fontLoaded: boolean`.
## Implementation Notes
- The style element is keyed by `packed-font-<family>`. Mounting two
hooks with the same family results in a single style element shared
across them; the second mount rewrites `textContent` with the same
rule, which is idempotent at the CSSOM level.
- `document.fonts.load(...)` failures (browser-specific, e.g. corrupt
font bytes) become `ReactPackerError(FONT_LOAD_FAILED)` with the
original rejection on `.cause` and surface on `result.error`,
shadowing any prior byte-load error from the base hook.
- In environments without `document` (SSR, Node tests without a DOM)
the hook still works at the byte level — `fontLoaded` simply stays
`false`.
- The hook deliberately does not call `document.fonts.add(face)` —
injecting `@font-face` via stylesheet is what the browser will pick
up anyway, and avoiding the `FontFace` constructor keeps the hook
portable to environments where that constructor is absent.

View File

@@ -0,0 +1,79 @@
# `usePackedImage` — image-tuned packed-resource hook
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`usePackedImage` is a thin wrapper around `usePackedResource` that
pre-fills sensible defaults for image assets: caching is enabled (images
tend to be re-used across renders) and the manager's MIME allow-list is
narrowed to `image/`-prefixed types so a misconfigured archive serving
HTML or JavaScript under a virtual image URL fails fast at the loader
boundary rather than the `<img>` tag.
The hook returns the same `UsePackedResourceReturn` shape as the generic
hook, so it is drop-in for any component that already consumes
`usePackedResource`.
## Features
- `cache: true` default — caller can opt out with `{ cache: false }`.
- `allowedMimePrefixes: ["image/"]` default — caller can broaden or
narrow.
- Forwards `verifyKey`, `verifyChecksum`, and `signal` untouched.
- Identical return shape to `usePackedResource` for trivial swapping.
## Usage: Basic
```tsx
import { usePackedImage } from "@suaveplan/react-packer";
function Avatar({ src }: { src: string | null }) {
const { url, loading } = usePackedImage(src);
if (loading) return <div className="avatar avatar--loading" />;
return url !== null ? <img alt="" className="avatar" src={url} /> : null;
}
```
## Usage: Advanced
```tsx
import { usePackedImage } from "@suaveplan/react-packer";
function SignedAvatar({
src,
key,
}: {
src: string;
key: Uint8Array;
}) {
const { url, error } = usePackedImage(src, {
verifyChecksum: true,
verifyKey: key,
allowedMimePrefixes: ["image/avif", "image/webp"],
});
if (error !== null) return <span>{error.code}</span>;
return url !== null ? <img alt="" src={url} /> : null;
}
```
## API Reference
- `usePackedImage(packedUrl, options?): UsePackedResourceReturn`
- Behaviour is identical to `usePackedResource` except defaults differ:
- `cache` defaults to `true`.
- `allowedMimePrefixes` defaults to `["image/"]`.
- All other options pass through unchanged.
## Implementation Notes
- Defaults apply only when the caller omits the key — explicit `false`
or an empty array opt out of caching / MIME enforcement respectively.
- The MIME allow-list is honoured by the manager's loader, not by the
hook itself; non-image archives surface as
`ReactPackerError(LOAD_FAILED)` with the underlying
`PackerBrowserError(UNSUPPORTED_RESOURCE_TYPE)` on `.cause`.
- The constant `IMAGE_MIME_PREFIXES` is module-local and frozen via
`ReadonlyArray` typing so consumers cannot mutate the default.

View File

@@ -0,0 +1,121 @@
# `usePackedResource` — generic React hook for `@suaveplan/packer-browser`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`usePackedResource` is the foundation hook of `@suaveplan/react-packer`.
It binds a packed-archive virtual URL (e.g. `/packed/logo.bundle.asset`)
to a React component's lifecycle, loading the underlying bytes through
the active `PackedResourceManager`, exposing the resulting Blob URL on
success, and releasing the cached entry on unmount.
The hook is intentionally low-level — variant hooks
(`usePackedImage`, `usePackedAudio`, `usePackedVideo`, `usePackedFont`)
delegate to it with media-tuned defaults. Consumers can also use the
generic hook directly when they need full control over caching,
checksum verification, signature keys, or MIME allow-lists.
## Features
- Lifecycle-correct: `idle` → `loading` → `success` / `error`, with
every state transition guarded by a `mounted` ref so unmounted
components never `setState`.
- `AbortSignal` propagation: the hook owns an internal `AbortController`,
merges any caller-supplied signal into it, and aborts on unmount,
`packedUrl` change, or `reload()`.
- Cache awareness: `manager.release(url, verifyKey)` runs in cleanup so
the cache can evict entries no React component still references.
- Error normalisation: every thrown value is wrapped as
`ReactPackerError` with a stable code (`LOAD_FAILED` or `ABORTED`)
and the original on `.cause`.
- Pluggable: resolves its `PackedResourceManager` via
`usePackedResourceManager()`, so the same hook works against a
provider-supplied manager (tests, multi-tenant apps) or the singleton
(zero-config consumers).
- Reload: the returned `reload()` thunk increments an internal counter
that re-runs the effect against a fresh `AbortController`.
## Usage: Basic
```tsx
import { usePackedResource } from "@suaveplan/react-packer";
function Asset({ src }: { src: string }) {
const { url, loading, error } = usePackedResource(src);
if (loading) return <span>Loading…</span>;
if (error !== null) return <span>{error.message}</span>;
return url !== null ? <img alt="" src={url} /> : null;
}
```
## Usage: Advanced
```tsx
import { usePackedResource } from "@suaveplan/react-packer";
function VerifiedAsset({
src,
verifyKey,
signal,
}: {
src: string | null;
verifyKey: Uint8Array;
signal: AbortSignal;
}) {
const { url, status, error, reload } = usePackedResource(src, {
cache: false,
verifyChecksum: true,
verifyKey,
signal,
allowedMimePrefixes: ["application/octet-stream"],
});
if (status === "idle") return <button onClick={reload}>Load</button>;
if (status === "loading") return <span>Verifying…</span>;
if (status === "error") {
return (
<>
<span>{error?.code}</span>
<button onClick={reload}>Retry</button>
</>
);
}
return url !== null ? <a href={url}>Open</a> : null;
}
```
## API Reference
- `usePackedResource(packedUrl, options?): UsePackedResourceReturn`
- `packedUrl: string | null | undefined` — the packed asset URL. `null`
or `undefined` keeps the hook in the `idle` state and does not invoke
the manager.
- `options: UsePackedResourceOptions`
- `cache?: boolean` — opt out of the manager cache for this load.
- `verifyChecksum?: boolean` — gate the unpack on checksum match.
- `verifyKey?: Uint8Array` — Ed25519 verify key for signed archives.
- `signal?: AbortSignal` — caller-supplied cancellation signal.
- `allowedMimePrefixes?: ReadonlyArray<string>` — MIME allow-list.
- Returns `{ url, loading, error, status, reload }`.
## Implementation Notes
- The mounted ref pattern is the same one used elsewhere
(`drive-react-sharing/use-sharing`); we avoid the strict-mode double
invoke trap by running the cleanup callback unconditionally.
- The `optionsRef` indirection keeps the `useEffect` dependency list
stable: only `packedUrl`, `verifyKey`, the external `signal`, the
`reloadCounter`, the active `manager`, and the active `resolver`
participate in change detection. Inline option-object literals from
callers therefore do not retrigger the effect.
- `error.code` is `REACT_PACKER_ABORTED` whenever the underlying
rejection is an `AbortError` *or* a `PackerBrowserError` whose code
is `PACKER_BROWSER_ABORTED` — both reach the catch arm. Otherwise the
failure surfaces as `LOAD_FAILED` with the original error on
`.cause`.
- `manager.release` is called on every cleanup even when `cache: false`
was supplied; the manager treats unknown keys as no-ops, so the call
is safe and lets the cache rebalance refcounts on remount.

View File

@@ -0,0 +1,77 @@
# `usePackedVideo` — video-tuned packed-resource hook
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`usePackedVideo` binds a packed video asset URL to a React component's
lifecycle. Unlike the image and audio variants, video defaults to
`cache: false` because video assets are typically large enough that
caching them in a Blob LRU dominates the cache budget and starves other
asset classes; the underlying manager will still de-duplicate concurrent
loads of the same URL via its in-flight Promise map.
The hook narrows the MIME allow-list to `video/`-prefixed types so
misconfigured archives fail fast at the loader rather than at
`<video>` decode time.
## Features
- `cache: false` default — video is opted out of the LRU by default.
- `allowedMimePrefixes: ["video/"]` default.
- Forwards `verifyKey`, `verifyChecksum`, and `signal`.
- Caller can re-enable caching with `{ cache: true }` when an explicit
budget warrants it (e.g. a short looping background video).
- Identical return shape to `usePackedResource`.
## Usage: Basic
```tsx
import { usePackedVideo } from "@suaveplan/react-packer";
function Trailer({ src }: { src: string | null }) {
const { url, loading } = usePackedVideo(src);
if (loading) return <div className="trailer trailer--loading" />;
return url !== null ? <video controls src={url} /> : null;
}
```
## Usage: Advanced
```tsx
import { usePackedVideo } from "@suaveplan/react-packer";
function LoopingBackground({ src }: { src: string }) {
// Loops are short — opting back into caching for this one component.
const { url } = usePackedVideo(src, {
cache: true,
allowedMimePrefixes: ["video/webm"],
});
return url !== null ? (
<video autoPlay loop muted playsInline src={url} />
) : null;
}
```
## API Reference
- `usePackedVideo(packedUrl, options?): UsePackedResourceReturn`
- Defaults differ from `usePackedResource`:
- `cache` defaults to `false`.
- `allowedMimePrefixes` defaults to `["video/"]`.
- All other options pass through.
## Implementation Notes
- `cache: false` means the manager will load the bytes and create a
Blob URL, but will not insert the entry into its LRU. The Blob URL
remains valid until `release()` runs in the hook's cleanup; the
underlying Blob is then eligible for GC.
- Consumers who play many videos sequentially should consider the
default — caching one then evicting it on the next load is the usual
pattern; the manager's release semantics make that explicit and
predictable.
- The MIME allow-list does not constrain the codec, only the wrapper.
If you need codec gating, do it in the consumer with a probe.

View File

@@ -0,0 +1,105 @@
# `PackedResourceProvider` — React context for `PackedResourceManager` DI
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PackedResourceProvider` lets a React tree explicitly inject a
`PackedResourceManager` from `@suaveplan/packer-browser` so every
`usePackedResource*` hook in its subtree consumes that manager instead
of the module-level singleton. This makes lifecycle and configuration
explicit: tests construct a fresh manager with an injected `fetch`,
storybook stories vary cache size and signature-verification, SSR
boundaries swap in a no-op manager, and applications that ship multiple
panes each with their own asset budgets isolate their caches.
The provider is optional. Hooks rendered outside any provider transparently
fall back to `packedResourceManager` from
`@suaveplan/packer-browser/singleton`, preserving the upstream
zero-config experience for consumers that don't need DI.
## Features
- React 19 context with a `null` default and a stable
`useContext`-driven resolution path.
- Memoised value object so child consumers don't re-render when the
parent re-renders without changing manager identity or resolver.
- Companion hook `usePackedResourceManager()` that resolves the active
manager — context wins, singleton fallback when no provider exists.
- Optional `messageResolver` field on the context value plumbs an
`@suaveplan/types` `MessageResolver` through to every
`ReactPackerError` raised by hooks in the subtree.
- Nesting: child providers shadow parent providers normally.
## Usage: Basic
```tsx
import { createPackedResourceManager } from "@suaveplan/packer-browser";
import { PackedResourceProvider, usePackedImage } from "@suaveplan/react-packer";
const manager = createPackedResourceManager();
function App() {
return (
<PackedResourceProvider value={{ manager }}>
<Logo />
</PackedResourceProvider>
);
}
function Logo() {
const { url, loading } = usePackedImage("/assets/logo.packed");
if (loading) return null;
return url !== null ? <img src={url} alt="" /> : null;
}
```
## Usage: Advanced
```tsx
import { createPackedResourceManager } from "@suaveplan/packer-browser";
import { PackedResourceProvider } from "@suaveplan/react-packer";
import type { MessageResolver } from "@suaveplan/types";
const resolver: MessageResolver = (key, params) =>
`[${key}]${JSON.stringify(params ?? {})}`;
const manager = createPackedResourceManager({
maxCacheSize: 50,
fetch: async (url) => new Response(/* ... */),
});
export function Tree({ children }: { children: React.ReactNode }) {
return (
<PackedResourceProvider value={{ manager, messageResolver: resolver }}>
{children}
</PackedResourceProvider>
);
}
```
## API Reference
- `PackedResourceProvider({ value, children })` — React component.
`value` is `{ manager: PackedResourceManager; messageResolver?: MessageResolver }`.
- `usePackedResourceManager(): PackedResourceManager` — resolves from
context or falls back to the singleton.
- `usePackedMessageResolver(): MessageResolver | undefined` — returns
the provider-supplied resolver, or `undefined` outside a provider.
## Implementation Notes
- The fallback singleton is imported statically from
`@suaveplan/packer-browser/singleton`. Importing this package will
therefore eagerly construct the singleton; teams that want maximum
tree-shaking should always mount a provider and skip the singleton
module entirely (which they get for free by configuring their bundler
to externalise unused subpaths).
- The provider value is wrapped in `useMemo` keyed on the `manager` and
`messageResolver` references, so passing a new `value={{ manager }}`
literal on every render is fine — descendants do not see a change.
- Hooks call `useContext(PackedResourceContext)` once per render and
never call into the singleton when a provider is mounted, so test
isolation is exact.

View File

@@ -0,0 +1,85 @@
# `react-packer.types` — shared types for `@suaveplan/react-packer`
## Purpose
This module defines the small set of TypeScript interfaces that the
React-tier packer package exposes on its public surface. Keeping the
types in a dedicated module — rather than co-located with each
implementation file — makes it easy to import the shapes without
pulling React into a non-React context (e.g. a type-only re-export to a
shared declarations file in an application).
These types are deliberately structural: any object satisfying the
shape works, so consumers can substitute their own equivalents at the
test boundary without coupling to suaveplan classes.
## Features
- `PackedResourceStatus` — the four-state lifecycle string union
(`"idle" | "loading" | "success" | "error"`).
- `UsePackedResourceOptions` — the per-call option bag accepted by
every hook in the package.
- `UsePackedResourceReturn` — the common return shape exposing `url`,
`loading`, `error`, `status`, `reload`.
- `UsePackedFontReturn` — `UsePackedResourceReturn` extended with
`fontLoaded: boolean`.
- `PackedResourceProviderValue` — the context value supplied by the
`PackedResourceProvider` (manager + optional resolver).
- Re-exports `PackedResourceLoadCallOptions` and
`PackedResourceManager` from `@suaveplan/packer-browser` so consumers
can type their own DI bootstrap code without dual-importing.
## Usage: Basic
```ts
import type {
UsePackedResourceOptions,
UsePackedResourceReturn,
} from "@suaveplan/react-packer";
function withDefaults(opts: UsePackedResourceOptions): UsePackedResourceOptions {
return { cache: true, ...opts };
}
function reportStatus(result: UsePackedResourceReturn): string {
return result.status === "loading" ? "Loading…" : (result.url ?? "");
}
```
## Usage: Advanced
```ts
import type {
PackedResourceProviderValue,
UsePackedFontReturn,
} from "@suaveplan/react-packer";
import { createPackedResourceManager } from "@suaveplan/packer-browser";
const value: PackedResourceProviderValue = {
manager: createPackedResourceManager(),
messageResolver: (key) => key,
};
function isFontReady(result: UsePackedFontReturn): boolean {
return result.status === "success" && result.fontLoaded;
}
```
## API Reference
- `PackedResourceStatus = "idle" | "loading" | "success" | "error"`
- `UsePackedResourceOptions` — `{ cache?, verifyChecksum?, verifyKey?, signal?, allowedMimePrefixes? }`
- `UsePackedResourceReturn` — `{ url, loading, error, status, reload }`
- `UsePackedFontReturn` — `UsePackedResourceReturn & { fontLoaded: boolean }`
- `PackedResourceProviderValue` — `{ manager, messageResolver? }`
- Re-exports: `PackedResourceLoadCallOptions`, `PackedResourceManager`
## Implementation Notes
- Every property is `readonly` on the return interfaces so React
consumers cannot accidentally mutate state outside the hook.
- The status union is closed; adding a new state requires a new
spec change and a downstream major-version bump.
- The provider value's `manager` and `messageResolver` are both
reference-tracked by the provider's `useMemo`, so descendants only
re-render when those identities change.