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,102 @@
# template-registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TemplateRegistry` is a type-safe in-memory store for `EmailTemplate`
values keyed by name. It supports per-name lookup, listing, category /
tag / author search, and lifecycle removal — the surface a typical
email-orchestration service needs to dispatch the right template by
name without coupling to the template implementations themselves.
The registry is intentionally non-persistent: callers needing
cross-process state (DB-backed templates, CDN-hosted JSON, etc.) wire
their own store and `register()` into this registry on boot. Templates
remain plain `EmailTemplate` values; the registry never copies or
freezes them.
## Features
- `register(name, template, metadata?)` with duplicate detection and an
OTel counter (`registry_register_total{outcome}`).
- `get(name)` / `has(name)` / `list()` / `size` accessor.
- `search(criteria)` with name (case-insensitive substring), category
(exact), tag (any-intersection), author (exact) filters — combinable.
- `remove(name)` / `clear()` for lifecycle management.
- Optional `defaultTheme` exposed via a getter, useful as a per-registry
fallback for the renderer.
## Usage: Basic
```ts
import { createRegistry } from "@suaveplan/email-templates/template-registry";
import {
invoiceTemplate,
welcomeTemplate,
} from "@suaveplan/email-templates/templates";
const registry = createRegistry();
registry.register("welcome", welcomeTemplate, {
category: "transactional",
tags: ["onboarding"],
});
registry.register("invoice", invoiceTemplate, { category: "billing" });
const t = registry.get("welcome");
```
## Usage: Advanced
Wire a typed map so `get(name)` returns the exact `EmailTemplate<TProps>`
for that key:
```ts
import { createRegistry } from "@suaveplan/email-templates/template-registry";
import { welcomeTemplate } from "@suaveplan/email-templates/templates";
import type { WelcomeProps } from "@suaveplan/email-templates/templates";
interface MyMap {
welcome: WelcomeProps;
}
const registry = createRegistry<MyMap>();
registry.register("welcome", welcomeTemplate);
const welcomeT = registry.get("welcome"); // type: EmailTemplate<WelcomeProps> | undefined
```
Search by tag intersection and category:
```ts
registry.search({ tags: ["onboarding"], category: "transactional" });
```
## API Reference
| Symbol | Kind | Description |
| ------------------------- | -------- | ---------------------------------------------------- |
| `TemplateRegistry<TMap>` | class | The registry itself; implements `TemplateRegistryApi`. |
| `createRegistry<TMap>` | function | Convenience factory returning a new registry. |
| `RegistryOptions` | type | Constructor options. |
| `TemplateMetadata` | type | `{ category?, tags?, description?, author? }`. |
| `RegistryEntry` | type | Internal entry shape (template + metadata + when). |
| `SearchCriteria` | type | Search input shape. |
| `SearchResult` | type | Search output shape. |
| `TemplateMap` | type | `Record<string, unknown>` upper bound. |
| `TemplateRegistryApi` | type | The interface implemented by `TemplateRegistry`. |
## Implementation Notes
The registry stores its entries in a single `Map<string, RegistryEntry>`
— ordered insertion is preserved for `list()` and `search()`. Duplicate
registration raises `EmailTemplatesError(DUPLICATE_NAME)` deliberately:
silent overwrite is the most common cause of "wrong template ended up in
the inbox" bugs. Consumers needing replacement should `remove(name)`
first.
`search` uses simple JS `Array.some` / `String.includes` — no fuzzy
matching, no scoring. Tag matching is an OR (any tag in the criterion
must appear in the entry's tags); name matching is substring; category
and author are exact. The criteria are combined with logical AND.