@vielzeug/codex 26.10.4 → 26.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +84 -48
- package/data/llms-full.txt +2386 -1275
- package/data/llms.txt +3 -2
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +76 -0
- package/data/packages/arsenal.json +18 -4
- package/data/packages/courier.json +3 -3
- package/data/packages/prism.json +57 -54
- package/data/packages/refine.json +2 -2
- package/data/packages/tandem.json +1 -1
- package/data/refine.json +3763 -3594
- package/data/search.json +40 -19
- package/package.json +2 -2
- package/skills/vielzeug/SKILL.md +2 -1
package/data/llms-full.txt
CHANGED
|
@@ -1,6 +1,1301 @@
|
|
|
1
1
|
# Vielzeug: Full Documentation
|
|
2
2
|
|
|
3
|
-
> Complete documentation for
|
|
3
|
+
> Complete documentation for 42 packages. Version: 26.10.6
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## @vielzeug/archive
|
|
8
|
+
|
|
9
|
+
**Category:** utilities
|
|
10
|
+
|
|
11
|
+
### Overview
|
|
12
|
+
|
|
13
|
+
## Why Archive?
|
|
14
|
+
|
|
15
|
+
You can share content entries with ranked browsing, filters, likes and account
|
|
16
|
+
erasure on Cloudflare Workers + D1. `defineArchiveCollection` supplies your JSON
|
|
17
|
+
payload schema and indexing rules; the Worker supplies sessions, guards,
|
|
18
|
+
idempotent publishing, cursors and rate limits.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Before: each deployment repeats validation, indexing and route plumbing.
|
|
22
|
+
|
|
23
|
+
// After: the deployment is one collection definition plus one factory call.
|
|
24
|
+
import { defineArchiveCollection } from '@vielzeug/archive';
|
|
25
|
+
import { createArchiveWorker } from '@vielzeug/archive/worker';
|
|
26
|
+
import { s } from '@vielzeug/spell';
|
|
27
|
+
|
|
28
|
+
const notes = defineArchiveCollection({
|
|
29
|
+
name: 'notes', contentVersion: 1,
|
|
30
|
+
payloadSchema: s.object({ title: s.string().min(1).max(128) }),
|
|
31
|
+
index: ({ title }) => ({ title, filter: null, tags: [] }),
|
|
32
|
+
});
|
|
33
|
+
export default createArchiveWorker([notes]);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Feature | Archive | Hand-rolled Worker | BaaS (Firebase/Supabase) |
|
|
37
|
+
| ------------------------------------ | -------------------------------------------------------- | ------------------------------------ | ------------------------------------- |
|
|
38
|
+
| Bundle size (core) | | Depends on deployment code | Depends on selected SDK |
|
|
39
|
+
| Zero dependencies | No — Spell and jose | Depends on implementation | No — client SDK |
|
|
40
|
+
| Content knowledge in the core | none — one collection seam | baked in | rules everywhere |
|
|
41
|
+
| Canonical fresh-database D1 schema | in the package | Deployment-owned | Not a D1 backend |
|
|
42
|
+
| Idempotent publish w/ tombstones | built-in | rebuild it | rebuild it |
|
|
43
|
+
| GDPR erasure / export endpoints | built-in | rebuild it | partial |
|
|
44
|
+
| Strict DTO re-validation on read | every entry | optional | optional |
|
|
45
|
+
|
|
46
|
+
**Use Archive when** an app needs a shareable, ranked, liked community space for
|
|
47
|
+
content entries on Cloudflare Workers + D1, and can express its content as one strict
|
|
48
|
+
payload schema.
|
|
49
|
+
**Consider alternatives when** you need rich queries beyond filter/tag/search, live
|
|
50
|
+
collaboration, or a backend you do not control — Archive is deliberately one narrow,
|
|
51
|
+
hardened surface.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```sh [pnpm]
|
|
56
|
+
pnpm add @vielzeug/archive @vielzeug/spell
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```sh [npm]
|
|
60
|
+
npm install @vielzeug/archive @vielzeug/spell
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```sh [yarn]
|
|
64
|
+
yarn add @vielzeug/archive @vielzeug/spell
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
A deployment is one collection definition plus one factory call. The definition
|
|
70
|
+
brings the payload schema and what to index; everything else — the emitted-entry
|
|
71
|
+
envelope, the content hash, the wire contract, the routes — is composed for you.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { defineArchiveCollection } from '@vielzeug/archive';
|
|
75
|
+
import { createArchiveWorker } from '@vielzeug/archive/worker';
|
|
76
|
+
import { s } from '@vielzeug/spell';
|
|
77
|
+
|
|
78
|
+
// The collection's payload: one strict schema is the whole content contract.
|
|
79
|
+
const payloadSchema = s.object({
|
|
80
|
+
cuisine: s.enum(['italian', 'japanese']),
|
|
81
|
+
ingredients: s.array(s.string().trim().min(1).max(64)).min(1).max(20),
|
|
82
|
+
name: s.string().trim().min(1).max(32),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const recipesCollection = defineArchiveCollection({
|
|
86
|
+
canonicalize: (payload) => ({ ...payload, ingredients: [...payload.ingredients].sort() }),
|
|
87
|
+
contentVersion: 1,
|
|
88
|
+
filters: ['italian', 'japanese'],
|
|
89
|
+
index: (payload) => ({
|
|
90
|
+
filter: payload.cuisine,
|
|
91
|
+
tags: [payload.ingredients.length ({ title, filter: null, tags: [] }),
|
|
92
|
+
});
|
|
93
|
+
const response = await fetch('/api/v1/collections/notes/entries?sort=liked');
|
|
94
|
+
if (!response.ok) throw new Error('Archive unavailable');
|
|
95
|
+
const parsed = notes.contract.page.parse(await response.json());
|
|
96
|
+
console.log(parsed.entries);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Features
|
|
100
|
+
|
|
101
|
+
- `defineArchiveCollection` — the one content seam: payload schema, canonicalization, filter/tag/title derivation, union-typed vocabularies
|
|
102
|
+
- `createArchiveWorker` — the whole routes/auth/limits/SQL stack from registered collections
|
|
103
|
+
- `collection.contract` — entry, page, publish/update input schemas shared by client and Worker
|
|
104
|
+
- `ARCHIVE_LIMITS` + `archiveHealthSchema` — the application bounds, advertised by health and validated on both sides
|
|
105
|
+
- `archiveAuthHealthSchema` + `AUTH_SCHEMA_VERSION` — typed issuer readiness checked against the issuer DDL stamp
|
|
106
|
+
- `archiveErrorResponseSchema` — the one error envelope, code-typed across the wire
|
|
107
|
+
- `createAuthWorker` — scanner-safe confirmation, live session-version enforcement, durable budgets, metadata export and coordinated erasure
|
|
108
|
+
- `AUTH_ISSUER_SERVICE` — required uncached issuer check after local catalog JWT verification
|
|
109
|
+
- `ArchiveDerivationError` — distinguishes faulty collection code from invalid submitted content
|
|
110
|
+
- Moderation — reader reports, the operator soft-hide and the report queue, keyed on a verified `roles` claim
|
|
111
|
+
- `worker.tap()` / `issuer.tap()` — typed sanitized runtime events; neither core logs by default
|
|
112
|
+
- `migrations/` — canonical fresh-database DDL with the storage stamp health reads back; no upgrade framework
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [Usage Guide](./usage.md) — stand up a collection, deploy the Worker, wire the client.
|
|
117
|
+
- [API Reference](./api.md) — every export, all three entry points, the full route table.
|
|
118
|
+
- [Examples](./examples.md) — recipes from first deployment to GDPR erasure.
|
|
119
|
+
- [Release Status](./migration.md) — unreleased package and fresh-database scope.
|
|
120
|
+
|
|
121
|
+
## See Also
|
|
122
|
+
|
|
123
|
+
- [Spell](/spell/) — every payload schema here is a spell schema; the core composes envelopes from it.
|
|
124
|
+
- [Courier](/courier/) — the HTTP client shape an app's frontend mirrors to consume these routes.
|
|
125
|
+
- [Rune](/rune/) — the structured logger a deployment points `worker.tap()` events at.
|
|
126
|
+
|
|
127
|
+
### API Reference
|
|
128
|
+
|
|
129
|
+
## API Overview
|
|
130
|
+
|
|
131
|
+
| Symbol | Purpose | Execution mode | Common gotcha |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| `defineArchiveCollection` | Compose validation, indexing, identity and wire schemas | Sync | Invalid content derives to `null`; callback faults throw |
|
|
134
|
+
| `createArchiveWorker` | Build the catalog Worker | Sync | Collection names must be unique |
|
|
135
|
+
| `createAuthWorker` | Build the passwordless issuer | Sync | `AUTH_ARCHIVE` is required |
|
|
136
|
+
| `worker.tap`, `issuer.tap` | Observe sanitized runtime outcomes | Sync | Neither core logs by default; observer exceptions are swallowed |
|
|
137
|
+
| `ArchiveDerivationError` | Identify a collection implementation/runtime fault | Sync | Faults become sanitized 500s, not content-validation 422s |
|
|
138
|
+
| `ARCHIVE_LIMITS` | Shared application bounds | Sync | Page bounds do not cap total exports |
|
|
139
|
+
| `archiveErrorResponseSchema`, `archiveAuthHealthSchema` | Validate error and issuer-health responses | Sync | Unknown error codes and health states reject |
|
|
140
|
+
| `ArchiveError`, `ArchiveDefinitionError` | Public base and invalid-definition errors | Sync | Thrown locally, not transmitted as error objects |
|
|
141
|
+
| `sessionCookieValue` | Compose shared session-cookie attributes | Sync | Deployments must choose `secure` from their environment |
|
|
142
|
+
| `isWellFormedText` | Check control characters and lone surrogates | Sync | Does not trim or impose a length bound |
|
|
143
|
+
|
|
144
|
+
## Package Entry Point
|
|
145
|
+
|
|
146
|
+
| Import | Purpose |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `@vielzeug/archive` | Browser-safe collections, constants, schemas and errors |
|
|
149
|
+
| `@vielzeug/archive/worker` | Catalog factory, issuer-session service binding and event types |
|
|
150
|
+
| `@vielzeug/archive/auth` | Issuer factory, operator lifetime and binding types |
|
|
151
|
+
|
|
152
|
+
## Collections
|
|
153
|
+
|
|
154
|
+
### `defineArchiveCollection`
|
|
155
|
+
|
|
156
|
+
Creates a frozen collection and its contract; malformed names, versions or vocabularies throw `ArchiveDefinitionError`.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
function defineArchiveCollection(definition: ArchiveCollectionDefinition): ArchiveCollection;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
| Parameter | Contract |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `name` | Lowercase `[a-z0-9-]{1,32}` |
|
|
165
|
+
| `contentVersion` | Positive safe integer |
|
|
166
|
+
| `payloadSchema` | Spell schema whose output is a stable JSON object |
|
|
167
|
+
| `filters`, `tags` | Optional readonly arrays; omitted means empty vocabulary |
|
|
168
|
+
| `canonicalize` | Optional pure, idempotent, schema-valid normalization |
|
|
169
|
+
| `index` | Derive `filter`, `tags` and searchable `title` without mutating the payload |
|
|
170
|
+
|
|
171
|
+
**Returns:** `ArchiveCollection`.
|
|
172
|
+
|
|
173
|
+
**Example:** define and derive one collection without Worker bindings.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { defineArchiveCollection } from '@vielzeug/archive';
|
|
177
|
+
import { s } from '@vielzeug/spell';
|
|
178
|
+
|
|
179
|
+
const recipes = defineArchiveCollection({
|
|
180
|
+
name: 'recipes',
|
|
181
|
+
contentVersion: 1,
|
|
182
|
+
payloadSchema: s.object({ name: s.string().min(1).max(32) }),
|
|
183
|
+
index: (payload) => ({ filter: null, tags: [], title: payload.name }),
|
|
184
|
+
});
|
|
185
|
+
const derived = await recipes.derive({ name: 'Rice' });
|
|
186
|
+
if (derived === null) throw new Error('Invalid recipe');
|
|
187
|
+
console.log(derived.contentHash);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Method / property | Result |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `derive(raw)` | `Promise` |
|
|
193
|
+
| `contract.entry` | Public entry schema |
|
|
194
|
+
| `contract.ownedEntry`, `ownedEntries` | Owner view with `clientKey` and `hidden` |
|
|
195
|
+
| `contract.page` | Entries and forward/backward cursors |
|
|
196
|
+
| `contract.publishInput`, `updateInput` | Publish and revision-guarded update request schemas |
|
|
197
|
+
| `filters`, `tags` | Frozen vocabulary snapshots |
|
|
198
|
+
|
|
199
|
+
`derive` returns `null` for invalid JSON objects, schema-invalid input, invalid titles, too many tags and oversized canonical payloads. Unexpected schema, callback or hashing failures throw `ArchiveDerivationError`. Non-idempotent or schema-invalid canonicalization, unstable schema transformations, index mutation and out-of-vocabulary projections are also derivation faults. Routes map `null` to 422 `invalid-payload` and faults to a sanitized 500 `internal`.
|
|
200
|
+
Schema output that is lossy or no longer JSON is a derivation fault, not invalid
|
|
201
|
+
submitted content. Invalid index-result shapes also throw.
|
|
202
|
+
|
|
203
|
+
Const generics preserve the content-version literal and infer filter/tag unions from vocabulary arrays without `as const`. `NoInfer` prevents `index` from widening those unions; omitted arrays infer `never`, permitting only `filter: null` and empty tags.
|
|
204
|
+
|
|
205
|
+
Objects serialize with recursively sorted keys; arrays preserve order unless your canonicalizer changes it. SHA-256 hashes the core-owned `{contentVersion, payload}` envelope. Response schemas require stored payloads to remain stable under parsing; a response-validation fault has stage `response`.
|
|
206
|
+
|
|
207
|
+
`filter`, tags and searchable title are persisted projections, not computed during reads. Changing `index` or its vocabulary does not update stored rows. The active definition and persisted dataset must remain consistent; Archive has no projection-update or historical-schema API.
|
|
208
|
+
|
|
209
|
+
## Worker and Maintenance
|
|
210
|
+
|
|
211
|
+
### `createArchiveWorker`
|
|
212
|
+
|
|
213
|
+
Returns the catalog request/scheduled handler and its observation side channel.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
function createArchiveWorker(
|
|
217
|
+
collections: readonly ArchiveCollection[],
|
|
218
|
+
): ArchiveWorkerHandler;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Returns:** `ArchiveWorkerHandler`; duplicate names throw `ArchiveDefinitionError` at registration.
|
|
222
|
+
|
|
223
|
+
**Example:** export a deployable Worker using the package's structural bindings.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { defineArchiveCollection } from '@vielzeug/archive';
|
|
227
|
+
import { createArchiveWorker, type ArchiveWorkerEnv } from '@vielzeug/archive/worker';
|
|
228
|
+
import { s } from '@vielzeug/spell';
|
|
229
|
+
|
|
230
|
+
const notes = defineArchiveCollection({
|
|
231
|
+
name: 'notes', contentVersion: 1,
|
|
232
|
+
payloadSchema: s.object({ title: s.string().min(1).max(128) }),
|
|
233
|
+
index: ({ title }) => ({ title, filter: null, tags: [] }),
|
|
234
|
+
});
|
|
235
|
+
export default createArchiveWorker([notes]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
| Method | Contract |
|
|
239
|
+
| --- | --- |
|
|
240
|
+
| `fetch(request, env, ctx)` | Returns `Promise` |
|
|
241
|
+
| `scheduled(controller, env, ctx)` | Sweeps expired retry keys and revocation tombstones; emits failures and rethrows |
|
|
242
|
+
| `tap(handler, { signal? }?)` | Returns unsubscribe; a pre-aborted signal registers nothing |
|
|
243
|
+
|
|
244
|
+
`AUTH_ISSUER_SERVICE` is a required `fetch`-shaped service binding to the issuer.
|
|
245
|
+
The catalog verifies the signed JWT locally, then asks the issuer to validate its
|
|
246
|
+
current account session version. It does not cache successful issuer verdicts.
|
|
247
|
+
If the service exchange fails or exceeds its fixed 5000 ms timeout (body decoding
|
|
248
|
+
included), the catalog returns `503 auth-unavailable`; the timeout does not cancel
|
|
249
|
+
work already started by the issuer.
|
|
250
|
+
|
|
251
|
+
Only anonymous entry lists can use the 30-second edge cache. Any `Cookie` or `Authorization` header disables caching, even if invalid or empty. Detail, metadata, private and error responses are `no-store`. Public reads do not verify sessions.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### `worker.tap`
|
|
256
|
+
|
|
257
|
+
Observes typed request, scheduled and cache-failure events without affecting request behavior.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
tap(handler: (event: ArchiveEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Returns:** an unsubscribe function; abort detaches the observer. Diagnostics contain categories and trusted configuration field names, never raw errors, SQL, subjects, aliases, tokens or URLs.
|
|
264
|
+
|
|
265
|
+
**Example:** observe a Worker and detach explicitly.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { createArchiveWorker, type ArchiveWorkerEnv } from '@vielzeug/archive/worker';
|
|
269
|
+
|
|
270
|
+
const worker = createArchiveWorker([]);
|
|
271
|
+
const stop = worker.tap((event) => console.log(JSON.stringify(event)));
|
|
272
|
+
stop();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Identity Issuer
|
|
278
|
+
|
|
279
|
+
### `createAuthWorker`
|
|
280
|
+
|
|
281
|
+
Returns the passwordless email issuer with account export and coordinated erasure.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
function createAuthWorker(): AuthWorkerHandler;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Returns:** `AuthWorkerHandler`.
|
|
288
|
+
|
|
289
|
+
**Example:** export an issuer configured through deployment bindings.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { createAuthWorker, type AuthWorkerEnv } from '@vielzeug/archive/auth';
|
|
293
|
+
|
|
294
|
+
export default createAuthWorker();
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| Method | Contract |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| `fetch(request, env, ctx?)` | Returns `Promise` |
|
|
300
|
+
| `scheduled(controller, env, ctx?)` | Removes expired links, closed send windows and unverified unreferenced accounts |
|
|
301
|
+
| `tap(handler, { signal? }?)` | Observes typed `AuthEvent`; returns unsubscribe; pre-aborted signals are inert |
|
|
302
|
+
|
|
303
|
+
Magic links expire after 15 minutes and work once. The durable per-email budget allows three sends per hour; consumption, expiration and send failure do not refund it. Ordinary sessions last at most 30 days. Operator sessions use `OPERATOR_SESSION_MAX_AGE_SECONDS` (900 seconds). `OPERATOR_EMAILS` controls newly minted role claims.
|
|
304
|
+
|
|
305
|
+
The issuer-global admission budget is durable D1 state in `auth_sign_in_budget`:
|
|
306
|
+
20 requests per 60-second window before account creation or email delivery.
|
|
307
|
+
This supplements, rather than replaces, the durable per-email budget. No external
|
|
308
|
+
rate-limiter binding is required.
|
|
309
|
+
Admission spends are not refunded after later database or email failures.
|
|
310
|
+
Request emails are trimmed and lowercased before validation. `SENDER_ADDRESS`
|
|
311
|
+
must already be canonical; `OPERATOR_EMAILS` entries are trimmed and lowercased
|
|
312
|
+
before validation.
|
|
313
|
+
|
|
314
|
+
Sign-in requests create pending account rows with `lastSignInAt: null`. Successful
|
|
315
|
+
redemption atomically claims the one-use link and stamps the account's sign-in
|
|
316
|
+
time. The sweep deletes expired links and closed per-user send windows, then
|
|
317
|
+
unverified accounts with neither remaining links nor send-budget references.
|
|
318
|
+
Verified accounts are retained until explicit account deletion.
|
|
319
|
+
|
|
320
|
+
`@vielzeug/archive/auth` exports `OPERATOR_SESSION_MAX_AGE_SECONDS = 900` alongside
|
|
321
|
+
the factory and issuer binding types.
|
|
322
|
+
|
|
323
|
+
`AUTH_JWT_PREVIOUS_PUBLIC_JWKS` optionally publishes retired public RS256 keys and verifies sessions minted before rotation; private members reject configuration. The package has no demo sign-in routes.
|
|
324
|
+
|
|
325
|
+
Opening a magic link performs a safe GET that displays a click-confirmation page,
|
|
326
|
+
not redemption. Only a marked POST from an exact allowed origin or
|
|
327
|
+
`PUBLIC_ORIGIN` consumes `{ token }` and returns 204 with the session cookie. The
|
|
328
|
+
confirmation page navigates to `/signed-in` after success; the POST is not a
|
|
329
|
+
redirect. Confirmation responses are `no-store` with no-referrer and restrictive
|
|
330
|
+
CSP; the page removes the token from the address bar and loads no external assets.
|
|
331
|
+
GET displays confirmation for any syntactically valid token, including a used
|
|
332
|
+
token; only POST establishes whether the link is still redeemable. POST accepts
|
|
333
|
+
exactly `{ token }` and rejects query parameters.
|
|
334
|
+
|
|
335
|
+
The issuer never logs directly. `tap` emits sanitized request and scheduled
|
|
336
|
+
outcomes; observer failures are swallowed, abort detaches the observer and failed
|
|
337
|
+
scheduled work emits then rethrows. `AuthDiagnostic` reports only a category.
|
|
338
|
+
|
|
339
|
+
## HTTP Routes
|
|
340
|
+
|
|
341
|
+
Every route below is mounted under `/api/v1`. Mutation guards require an exact allowed `Origin` and `x-archive-request: 1`. Registered publishers pin `x-archive-publisher` to the identity captured when preparing private requests. The header is a freshness precondition, not authority.
|
|
342
|
+
|
|
343
|
+
| Route | Contract |
|
|
344
|
+
| --- | --- |
|
|
345
|
+
| `GET /health` | Public catalog readiness; 200 `ok` or 503 `degraded` |
|
|
346
|
+
| `POST /sign-out` | Catalog-local sign-out; `{ ok: true }` and expired session cookie, without issuer access |
|
|
347
|
+
| `GET /collections/:collection` | Collection metadata |
|
|
348
|
+
| `GET /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; bidirectional entry page |
|
|
349
|
+
| `POST /collections/:collection/entries` | `{ clientKey, payload }`; 201 create or 200 replay |
|
|
350
|
+
| `GET /collections/:collection/entries/:id` | Public entry; always `no-store` |
|
|
351
|
+
| `PUT /collections/:collection/entries/:id` | Owner-only `{ expectedRevision, payload }` |
|
|
352
|
+
| `DELETE /collections/:collection/entries/:id` | Owner or operator removal; 204 |
|
|
353
|
+
| `PUT` / `DELETE /collections/:collection/entries/:id/like` | Like / unlike visible entries only; guarded write and response; like-state response |
|
|
354
|
+
| `POST /collections/:collection/entries/:id/report` | `{ reason }`; idempotent per entry/reporter; 204 |
|
|
355
|
+
| `PUT` / `DELETE /collections/:collection/entries/:id/hidden` | Operator hide / unhide; 204 |
|
|
356
|
+
| `GET /collections/:collection/reports` | Operator page; `status=unresolved` (default) or `resolved`, `cursor`, `limit` |
|
|
357
|
+
| `PUT /collections/:collection/reports/:id/resolved` | Operator resolves report by its unique report ID; idempotent 204 |
|
|
358
|
+
| `DELETE /publishers/:id` | Operator erases Archive profile; not issuer account |
|
|
359
|
+
| `GET /moderation-log` | Operator forward page; `cursor`, `limit` |
|
|
360
|
+
| `GET` / `POST` / `PUT /me` | Identity, register with optional `{ name }`, rename with required `{ name }` |
|
|
361
|
+
| `DELETE /me` | Profile-only cascade; 204; does not clear cookie |
|
|
362
|
+
| `DELETE /me/account` | Revoke issuer/subject and atomically erase profile; 204; coordinated issuer-erasure seam |
|
|
363
|
+
| `GET /me/publications?collection=…` | Owner entries including hidden state and retry keys |
|
|
364
|
+
| `GET /me/likes` | Forward page `{ ids, cursor }`; `cursor`, `limit` |
|
|
365
|
+
| `GET /me/likes?ids=…` | Subset lookup `{ ids }`; at most `likeStateIds` input IDs |
|
|
366
|
+
| `GET /me/reports` | Own forward report page across collections; `cursor`, `limit` |
|
|
367
|
+
| `GET /auth/health` | Public issuer readiness; typed `AuthHealth`, 200 `ok` or 503 `degraded`; no email or peer probe |
|
|
368
|
+
| `GET /auth/session` | Internal issuer-session check for the catalog service binding; exact `{ ok: true }`, `no-store`, no PII; 401 for an invalid, stale or absent-account session |
|
|
369
|
+
| `POST /auth/sign-out-all` | Requires marker, exact origin, session and captured `ACCOUNT_HEADER`; atomically increments `session_version`, deletes outstanding links, preserves account and catalog data; 200 `{ ok: true }` with an expired cookie only on durable success |
|
|
370
|
+
| `GET /auth/jwks.json` | Active and configured retired public verification keys |
|
|
371
|
+
| `POST /auth/sign-in` | `{ email }` (trimmed and lowercased); 202 `{ ok: true }` |
|
|
372
|
+
| `GET /auth/verify?token=…` | Safe no-store click-confirmation page; no token consumption or session change |
|
|
373
|
+
| `POST /auth/verify` | JSON `{ token }`, marker and exact allowed origin or `PUBLIC_ORIGIN`; consume once; 204 with session cookie |
|
|
374
|
+
| `GET /auth/account` | `{ id, email, createdAt, lastSignInAt }`; no tokens or budget data |
|
|
375
|
+
| `DELETE /auth/account` | Requires `ACCOUNT_HEADER` with the captured issuer account UUID; catalog `/me/account` via `AUTH_ARCHIVE`, require exact 204, then issuer user and cookie clear; 204 |
|
|
376
|
+
|
|
377
|
+
Forward pages are newest first with opaque context-bound keyset cursors. Keep the same query context when following a cursor; end at `cursor: null`. `maxPageSize` bounds each page, not the total number of liked IDs, reports or audit actions you can export. The subset-like query is not a page and cannot combine `ids` with `cursor` or `limit`.
|
|
378
|
+
|
|
379
|
+
Full erasure creates an issuer/subject revocation tombstone for 30 days plus ten
|
|
380
|
+
seconds and deletes the profile in the same transaction. Catalog authentication
|
|
381
|
+
rejects that subject with 401 on ordinary private routes; registration also checks
|
|
382
|
+
revocation in SQL to prevent resurrection races. Only `DELETE /me/account` may
|
|
383
|
+
bypass the issuer session lookup, and only to retry erasure with the original
|
|
384
|
+
signed session of the same principal while its existing revocation tombstone is
|
|
385
|
+
valid; JWT signature and claims are still verified. It is not a general
|
|
386
|
+
stale-session exception: initial erasure validates the issuer session. The
|
|
387
|
+
issuer-facing `DELETE /auth/account` always checks the current session version
|
|
388
|
+
before calling the catalog, so retrying it after completed erasure returns 401
|
|
389
|
+
because the account is absent.
|
|
390
|
+
Direct profile-only `DELETE /me` allows later registration using the same session.
|
|
391
|
+
The sweep removes expired tombstones. Re-enrollment through the reference issuer
|
|
392
|
+
creates a fresh account UUID subject; an external issuer must issue a new subject
|
|
393
|
+
to re-enroll during the revocation window.
|
|
394
|
+
|
|
395
|
+
The issuer stores `auth_users.session_version` as a safe non-negative integer,
|
|
396
|
+
defaulting to `0`. Every session JWT carries the current value as the required
|
|
397
|
+
`sessionVersion` claim. Each ordinary issuer-authenticated route compares that
|
|
398
|
+
claim to the current account row; a mismatch is unauthenticated. `POST
|
|
399
|
+
/api/v1/auth/sign-out-all` checks the captured issuer account UUID against the
|
|
400
|
+
verified subject before atomically incrementing the version and deleting pending
|
|
401
|
+
links. A changed account answers 409 `account-changed`; a failed request does not
|
|
402
|
+
clear the cookie. This revokes all sessions, including the caller's, after a
|
|
403
|
+
successful response. Requests authorized before revocation may finish; there is
|
|
404
|
+
no cross-database transactional or in-flight cancellation guarantee.
|
|
405
|
+
|
|
406
|
+
The catalog requires `AUTH_ISSUER_SERVICE`, a service binding to the issuer. After
|
|
407
|
+
local JWT signature/claims verification, it requests
|
|
408
|
+
`GET /api/v1/auth/session` through that binding on every private request. Only a
|
|
409
|
+
strictly validated 401 `unauthenticated` remains a 401. Malformed, unexpected,
|
|
410
|
+
provider and 5xx responses become 503 `auth-unavailable`; there is no authority
|
|
411
|
+
cache. Both service directions use a fixed 5000 ms timeout that includes body
|
|
412
|
+
decoding. The issuer's `/api/v1/auth/session` is an internal
|
|
413
|
+
service contract, not a frontend session API.
|
|
414
|
+
|
|
415
|
+
Issuer readiness is independent of catalog readiness. `GET
|
|
416
|
+
/api/v1/auth/health` returns `archiveAuthHealthSchema` as `AuthHealth`; its
|
|
417
|
+
read-only probe checks configuration, a signing and verification key proof,
|
|
418
|
+
binding shapes, the singleton `auth_schema_meta` stamp against
|
|
419
|
+
`AUTH_SCHEMA_VERSION`, and readability of expected tables, columns and triggers.
|
|
420
|
+
It does not write, send email or probe the catalog service. Responses are
|
|
421
|
+
sanitized.
|
|
422
|
+
Configuration failure, database failure and schema mismatch return HTTP 503
|
|
423
|
+
with `status: 'degraded'` and the corresponding reason.
|
|
424
|
+
|
|
425
|
+
Effective hide, unhide, operator removal and publisher erasure append audit rows atomically with the state transition. Concurrent idempotent no-ops append nothing. Audit rows contain opaque IDs, not names or content, and have no foreign keys so they outlive erasure. Resolving a report sets `resolvedAt`; it is not one of the four audit action kinds.
|
|
426
|
+
|
|
427
|
+
## Constants and Schemas
|
|
428
|
+
|
|
429
|
+
| Export | Value or parsed output |
|
|
430
|
+
| --- | --- |
|
|
431
|
+
| `ARCHIVE_CONTRACT_VERSION`, `ARCHIVE_SCHEMA_VERSION` | Both debut at `1`; wire and storage versions are independent |
|
|
432
|
+
| `AUTH_SCHEMA_VERSION` | `1`; issuer DDL stamp checked by `/auth/health` |
|
|
433
|
+
| `ARCHIVE_COLLECTION_NAME_PATTERN` | `/^[a-z0-9-]{1,32}$/` |
|
|
434
|
+
| `ARCHIVE_OPERATOR_ROLE` | `'archive-operator'` |
|
|
435
|
+
| `SESSION_COOKIE_NAME`, `SESSION_COOKIE_PATH` | `'archive_session'`, `'/api/v1'` |
|
|
436
|
+
| `SESSION_MAX_AGE_SECONDS` | `2592000` |
|
|
437
|
+
| `OPERATOR_SESSION_MAX_AGE_SECONDS` | `900`; shared root contract, also re-exported by `/auth` |
|
|
438
|
+
| `ACCOUNT_HEADER` | `'X-Archive-Account'`; required issuer-account UUID on coordinated erasure |
|
|
439
|
+
| `PUBLISHER_HEADER`, `REQUEST_MARKER_HEADER` | `'x-archive-publisher'`, `'x-archive-request'` |
|
|
440
|
+
| `ILL_FORMED_TEXT`, `isWellFormedText(value: string): boolean` | Reject control characters and lone UTF-16 surrogates |
|
|
441
|
+
| `uuidSchema`, `isoTimestampSchema`, `hex64Schema` | UUID, ISO timestamp, lowercase SHA-256 hex strings |
|
|
442
|
+
| `archiveAliasSchema` | Trimmed, well-formed alias of at most 24 characters |
|
|
443
|
+
| `archiveAuthorSchema` | `{ id: string; name: string }` |
|
|
444
|
+
| `archiveIdentitySchema` | `ArchiveIdentity` |
|
|
445
|
+
| `archiveAccountSchema` | `ArchiveAccount`; issuer metadata, excluding private fields |
|
|
446
|
+
| `archiveLimitsSchema` | All numeric `ARCHIVE_LIMITS` fields |
|
|
447
|
+
| `archiveHealthSchema` | `ArchiveHealth` |
|
|
448
|
+
| `archiveAuthHealthSchema` | `AuthHealth` |
|
|
449
|
+
| `archiveErrorResponseSchema` | `ArchiveErrorResponse` |
|
|
450
|
+
| `archiveOkSchema` | `{ ok: true }` |
|
|
451
|
+
| `archiveCollectionMetaSchema` | `ArchiveCollectionMeta` |
|
|
452
|
+
| `archiveLikedIdsSchema` | `{ ids: string[] }` subset response |
|
|
453
|
+
| `archiveLikesPageSchema` | `ArchiveLikesPage` |
|
|
454
|
+
| `archiveLikeStateSchema` | `{ id: string; likeCount: number; liked: boolean }` |
|
|
455
|
+
| `archiveReportsSchema`, `archiveOwnReportsSchema` | `ArchiveReports`, `ArchiveOwnReports` |
|
|
456
|
+
| `archiveModerationLogSchema` | `ArchiveModerationLog` |
|
|
457
|
+
| `archiveSortSchema` | `ArchiveSort` |
|
|
458
|
+
| `ARCHIVE_ERROR_CODES` | Values of `ArchiveErrorCode` below |
|
|
459
|
+
| `ARCHIVE_MODERATION_ACTIONS` | `['hide', 'unhide', 'remove', 'erase-publisher']` |
|
|
460
|
+
|
|
461
|
+
### `sessionCookieValue`
|
|
462
|
+
|
|
463
|
+
Composes a session cookie using the shared security attributes.
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
function sessionCookieValue(token: string, options: { maxAgeSeconds: number; secure: boolean }): string;
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
| Parameter | Contract |
|
|
470
|
+
| --- | --- |
|
|
471
|
+
| `token` | Session token, or empty string when clearing |
|
|
472
|
+
| `options.maxAgeSeconds` | Cookie lifetime; `0` clears it |
|
|
473
|
+
| `options.secure` | Include `Secure` for production HTTPS |
|
|
474
|
+
|
|
475
|
+
**Returns:** the `Set-Cookie` value with `HttpOnly`, `SameSite=Strict`, the shared path and optional `Secure`.
|
|
476
|
+
|
|
477
|
+
**Example:** clear the production cookie.
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import { sessionCookieValue } from '@vielzeug/archive';
|
|
481
|
+
|
|
482
|
+
const expiredCookie = sessionCookieValue('', { maxAgeSeconds: 0, secure: true });
|
|
483
|
+
console.log(expiredCookie);
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### `isWellFormedText`
|
|
489
|
+
|
|
490
|
+
Checks text for control characters and lone UTF-16 surrogates.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
function isWellFormedText(value: string): boolean;
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Returns:** `true` when `ILL_FORMED_TEXT` does not match; it does not trim or impose bounds.
|
|
497
|
+
|
|
498
|
+
**Example:** validate text independently of the alias schema.
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
import { isWellFormedText } from '@vielzeug/archive';
|
|
502
|
+
|
|
503
|
+
console.log(isWellFormedText('Rice'), isWellFormedText('\u0000'));
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### `ARCHIVE_LIMITS`
|
|
507
|
+
|
|
508
|
+
`ARCHIVE_LIMITS` is frozen with these values:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
const limits = {
|
|
512
|
+
aliasChars: 24, cursorChars: 1024, idempotencyRetentionMs: 172800000,
|
|
513
|
+
likeStateIds: 50, maxPageSize: 50, maxPublications: 50, pageSize: 10,
|
|
514
|
+
payloadBytes: 8192, publicCacheSeconds: 30, reportReasonChars: 256,
|
|
515
|
+
requestBytes: 12288, searchChars: 64, searchTerms: 6, tagsPerEntry: 16,
|
|
516
|
+
titleChars: 128, vocabChars: 64,
|
|
517
|
+
};
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Types
|
|
521
|
+
|
|
522
|
+
The following definitions use `Schema` from `@vielzeug/spell`. Schema-derived aliases show their complete output shape.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
interface ArchiveIndexing {
|
|
526
|
+
filter: F | null;
|
|
527
|
+
tags: readonly T[];
|
|
528
|
+
title: string;
|
|
529
|
+
}
|
|
530
|
+
interface ArchiveCollectionDefinition {
|
|
531
|
+
canonicalize?(payload: P): P;
|
|
532
|
+
contentVersion: C;
|
|
533
|
+
filters?: readonly F[];
|
|
534
|
+
index(payload: P): ArchiveIndexing, NoInfer>;
|
|
535
|
+
name: string;
|
|
536
|
+
payloadSchema: Schema;
|
|
537
|
+
tags?: readonly T[];
|
|
538
|
+
}
|
|
539
|
+
interface ArchiveDerivation {
|
|
540
|
+
canonicalJson: string;
|
|
541
|
+
contentHash: string;
|
|
542
|
+
contentVersion: number;
|
|
543
|
+
filter: string | null;
|
|
544
|
+
tags: readonly string[];
|
|
545
|
+
title: string;
|
|
546
|
+
}
|
|
547
|
+
interface ArchiveEntry {
|
|
548
|
+
author: { id: string; name: string };
|
|
549
|
+
contentHash: string;
|
|
550
|
+
contentVersion: C;
|
|
551
|
+
id: string;
|
|
552
|
+
likeCount: number;
|
|
553
|
+
payload: P;
|
|
554
|
+
publishedAt: string;
|
|
555
|
+
revision: number;
|
|
556
|
+
schemaVersion: typeof ARCHIVE_CONTRACT_VERSION;
|
|
557
|
+
updatedAt: string;
|
|
558
|
+
}
|
|
559
|
+
interface ArchiveOwnedEntry extends ArchiveEntry {
|
|
560
|
+
clientKey: string;
|
|
561
|
+
hidden: boolean;
|
|
562
|
+
}
|
|
563
|
+
interface ArchiveContract {
|
|
564
|
+
entry: Schema>;
|
|
565
|
+
ownedEntries: Schema[] }>;
|
|
566
|
+
ownedEntry: Schema>;
|
|
567
|
+
page: Schema>>;
|
|
568
|
+
publishInput: Schema;
|
|
569
|
+
updateInput: Schema;
|
|
570
|
+
}
|
|
571
|
+
interface ArchiveCollection {
|
|
572
|
+
readonly contentVersion: C;
|
|
573
|
+
readonly contract: ArchiveContract;
|
|
574
|
+
readonly derive: (raw: unknown) => Promise;
|
|
575
|
+
readonly filters: readonly string[];
|
|
576
|
+
readonly name: string;
|
|
577
|
+
readonly tags: readonly string[];
|
|
578
|
+
}
|
|
579
|
+
interface ArchivePage {
|
|
580
|
+
cursor: string | null;
|
|
581
|
+
entries: readonly T[];
|
|
582
|
+
previous: string | null;
|
|
583
|
+
}
|
|
584
|
+
type ArchiveSort = 'liked' | 'recent';
|
|
585
|
+
type ArchiveDerivationStage = 'schema' | 'canonicalize' | 'index' | 'hash' | 'response';
|
|
586
|
+
type ArchiveIdentity = { id: string; name: string; operator: boolean } | null;
|
|
587
|
+
type ArchiveAccount = {
|
|
588
|
+
id: string; email: string; createdAt: string; lastSignInAt: string | null;
|
|
589
|
+
};
|
|
590
|
+
type ArchiveCollectionMeta = { filters: string[]; name: string; tags: string[] };
|
|
591
|
+
type ArchiveLikesPage = { cursor: string | null; ids: string[] };
|
|
592
|
+
type ArchiveErrorCode =
|
|
593
|
+
| 'account-changed' | 'auth-unavailable' | 'bad-request' | 'conflict' | 'forbidden' | 'gone'
|
|
594
|
+
| 'internal' | 'invalid-payload' | 'likes-disabled' | 'method-not-allowed'
|
|
595
|
+
| 'not-found' | 'publisher-changed' | 'publishing-disabled' | 'quota'
|
|
596
|
+
| 'rate-limited' | 'record-unavailable' | 'timeout' | 'too-large'
|
|
597
|
+
| 'unauthenticated' | 'unavailable' | 'unsupported-media';
|
|
598
|
+
type ArchiveErrorResponse = {
|
|
599
|
+
error: { code: ArchiveErrorCode; message: string; requestId: string };
|
|
600
|
+
};
|
|
601
|
+
type ArchiveHealth = {
|
|
602
|
+
dbSchemaVersion: number | null;
|
|
603
|
+
features: { likes: boolean; publishing: boolean };
|
|
604
|
+
limits: {
|
|
605
|
+
aliasChars: number; cursorChars: number; idempotencyRetentionMs: number;
|
|
606
|
+
likeStateIds: number; maxPageSize: number; maxPublications: number;
|
|
607
|
+
pageSize: number; payloadBytes: number; publicCacheSeconds: number;
|
|
608
|
+
reportReasonChars: number; requestBytes: number; searchChars: number;
|
|
609
|
+
searchTerms: number; tagsPerEntry: number; titleChars: number; vocabChars: number;
|
|
610
|
+
};
|
|
611
|
+
schemaVersion: 1;
|
|
612
|
+
status: 'ok' | 'degraded';
|
|
613
|
+
reason: 'database-unavailable' | 'schema-mismatch' | null;
|
|
614
|
+
};
|
|
615
|
+
type AuthHealth = {
|
|
616
|
+
dbSchemaVersion: number | null;
|
|
617
|
+
schemaVersion: 1;
|
|
618
|
+
status: 'ok' | 'degraded';
|
|
619
|
+
reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null;
|
|
620
|
+
};
|
|
621
|
+
type ArchiveReports = {
|
|
622
|
+
cursor: string | null;
|
|
623
|
+
reports: {
|
|
624
|
+
id: string; createdAt: string; entryId: string; entryTitle: string;
|
|
625
|
+
reason: string; reporter: { id: string; name: string }; resolvedAt: string | null;
|
|
626
|
+
}[];
|
|
627
|
+
};
|
|
628
|
+
type ArchiveOwnReports = {
|
|
629
|
+
cursor: string | null;
|
|
630
|
+
reports: {
|
|
631
|
+
collection: string; createdAt: string; entryId: string; id: string;
|
|
632
|
+
reason: string; resolvedAt: string | null;
|
|
633
|
+
}[];
|
|
634
|
+
};
|
|
635
|
+
type ArchiveModerationAction = 'hide' | 'unhide' | 'remove' | 'erase-publisher';
|
|
636
|
+
type ArchiveModerationLog = {
|
|
637
|
+
cursor: string | null;
|
|
638
|
+
actions: {
|
|
639
|
+
id: string; action: ArchiveModerationAction; collection: string | null;
|
|
640
|
+
createdAt: string; operatorId: string; targetId: string;
|
|
641
|
+
}[];
|
|
642
|
+
};
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Worker subpath types model only the binding methods the catalog calls.
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
interface D1Statement {
|
|
649
|
+
all(): Promise;
|
|
650
|
+
bind(...params: (string | number | null)[]): D1Statement;
|
|
651
|
+
first(): Promise;
|
|
652
|
+
run(): Promise;
|
|
653
|
+
}
|
|
654
|
+
interface D1Like {
|
|
655
|
+
batch(statements: D1Statement[]): Promise;
|
|
656
|
+
prepare(sql: string): D1Statement;
|
|
657
|
+
}
|
|
658
|
+
interface RateLimiterBinding {
|
|
659
|
+
limit(input: { key: string }): Promise;
|
|
660
|
+
}
|
|
661
|
+
interface ArchiveWorkerEnv {
|
|
662
|
+
AUTH_ISSUER_SERVICE: AuthIssuerServiceBinding;
|
|
663
|
+
ALLOWED_ORIGINS: string;
|
|
664
|
+
APP_ENV: string;
|
|
665
|
+
DB: D1Like;
|
|
666
|
+
JWT_AUDIENCE: string;
|
|
667
|
+
JWT_ISSUER: string;
|
|
668
|
+
JWT_JWKS_URL: string;
|
|
669
|
+
LIKES_ENABLED: string;
|
|
670
|
+
PUBLISHING_ENABLED: string;
|
|
671
|
+
RATE_LIMIT_KEY_SECRET: string;
|
|
672
|
+
READ_LIMIT: RateLimiterBinding;
|
|
673
|
+
WRITE_LIMIT: RateLimiterBinding;
|
|
674
|
+
}
|
|
675
|
+
interface AuthIssuerServiceBinding {
|
|
676
|
+
fetch(request: Request): Promise;
|
|
677
|
+
}
|
|
678
|
+
interface ArchiveExecutionContext {
|
|
679
|
+
waitUntil(promise: Promise): void;
|
|
680
|
+
}
|
|
681
|
+
type ArchiveDiagnostic =
|
|
682
|
+
| { readonly kind: 'configuration'; readonly field: string }
|
|
683
|
+
| { readonly kind: 'database' | 'record' | 'schema' | 'internal' | 'cache' | 'collection' };
|
|
684
|
+
interface ArchiveRequestEvent {
|
|
685
|
+
code: ArchiveErrorCode | 'ok';
|
|
686
|
+
diagnostic?: ArchiveDiagnostic;
|
|
687
|
+
ms: number;
|
|
688
|
+
requestId: string;
|
|
689
|
+
route: string;
|
|
690
|
+
status: number;
|
|
691
|
+
type: 'request';
|
|
692
|
+
}
|
|
693
|
+
type ArchiveScheduledEvent =
|
|
694
|
+
| { removed: number; type: 'scheduled'; status: 'ok' }
|
|
695
|
+
| { type: 'scheduled'; status: 'failed'; diagnostic: ArchiveDiagnostic };
|
|
696
|
+
type ArchiveEvent =
|
|
697
|
+
| ArchiveRequestEvent
|
|
698
|
+
| ArchiveScheduledEvent
|
|
699
|
+
| { readonly type: 'cache-error'; readonly diagnostic: ArchiveDiagnostic };
|
|
700
|
+
interface ArchiveWorkerHandler {
|
|
701
|
+
fetch(request: Request, env: E, ctx: ArchiveExecutionContext): Promise;
|
|
702
|
+
scheduled?(controller: unknown, env: E, ctx: ArchiveExecutionContext): void | Promise;
|
|
703
|
+
tap(handler: (event: ArchiveEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Scheduled `removed` counts the combined expired retry-key and revocation rows.
|
|
708
|
+
|
|
709
|
+
Auth subpath types describe the sibling issuer and its required service binding.
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
interface SendEmail {
|
|
713
|
+
send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise;
|
|
714
|
+
}
|
|
715
|
+
interface AuthArchiveBinding {
|
|
716
|
+
fetch(request: Request): Promise;
|
|
717
|
+
}
|
|
718
|
+
interface AuthWorkerEnv {
|
|
719
|
+
ALLOWED_ORIGINS: string;
|
|
720
|
+
APP_ENV: string;
|
|
721
|
+
APP_NAME: string;
|
|
722
|
+
AUDIENCE: string;
|
|
723
|
+
AUTH_ARCHIVE: AuthArchiveBinding;
|
|
724
|
+
AUTH_DB: D1Like;
|
|
725
|
+
AUTH_EMAIL: SendEmail;
|
|
726
|
+
AUTH_ISSUER: string;
|
|
727
|
+
AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;
|
|
728
|
+
AUTH_JWT_PRIVATE_JWK: string;
|
|
729
|
+
OPERATOR_EMAILS: string;
|
|
730
|
+
PUBLIC_ORIGIN: string;
|
|
731
|
+
SENDER_ADDRESS: string;
|
|
732
|
+
}
|
|
733
|
+
interface AuthWorkerHandler {
|
|
734
|
+
fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise;
|
|
735
|
+
scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise;
|
|
736
|
+
tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
|
|
737
|
+
}
|
|
738
|
+
type AuthDiagnostic = {
|
|
739
|
+
readonly kind: 'configuration' | 'database' | 'email' | 'catalog' | 'internal';
|
|
740
|
+
};
|
|
741
|
+
type AuthEvent =
|
|
742
|
+
| {
|
|
743
|
+
readonly type: 'request';
|
|
744
|
+
readonly requestId: string;
|
|
745
|
+
readonly route:
|
|
746
|
+
| '/auth/account'
|
|
747
|
+
| '/auth/health'
|
|
748
|
+
| '/auth/jwks.json'
|
|
749
|
+
| '/auth/session'
|
|
750
|
+
| '/auth/sign-in'
|
|
751
|
+
| '/auth/sign-out-all'
|
|
752
|
+
| '/auth/verify'
|
|
753
|
+
| 'unmatched';
|
|
754
|
+
readonly status: number;
|
|
755
|
+
readonly code: ArchiveErrorCode | 'ok';
|
|
756
|
+
readonly ms: number;
|
|
757
|
+
readonly diagnostic?: AuthDiagnostic;
|
|
758
|
+
}
|
|
759
|
+
| { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }
|
|
760
|
+
| { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Issuer scheduled `removed` counts expired token rows, closed per-user send windows
|
|
764
|
+
and deleted unverified accounts. The singleton admission row is retained and its
|
|
765
|
+
window resets atomically on a later sign-in request.
|
|
766
|
+
|
|
767
|
+
## Errors
|
|
768
|
+
|
|
769
|
+
| Exported class | Trigger | Properties |
|
|
770
|
+
| --- | --- | --- |
|
|
771
|
+
| `ArchiveError` | Base for public package errors | `name`, `message`, optional `cause` |
|
|
772
|
+
| `ArchiveDefinitionError` | Invalid collection definition or duplicate registered collection names | Extends `ArchiveError` |
|
|
773
|
+
| `ArchiveDerivationError` | Unexpected schema/canonicalization/index/hash/response fault or violated collection invariant | Extends `ArchiveError`; readonly `stage: ArchiveDerivationStage`; optional `cause` |
|
|
774
|
+
|
|
775
|
+
`ArchiveError` and `ArchiveDefinitionError` accept `(message: string, opts?: ErrorOptions)`.
|
|
776
|
+
`ArchiveDerivationError` accepts `(stage: ArchiveDerivationStage, message: string, opts?: ErrorOptions)`.
|
|
777
|
+
Its message and cause are local diagnostics, not response content. Worker request
|
|
778
|
+
failures use the wire envelope, not these thrown classes. Internal `ApiError` and
|
|
779
|
+
`AuthOperationError` are not exported.
|
|
780
|
+
|
|
781
|
+
Stored entries that fail the current wire schema return 503 `record-unavailable`.
|
|
782
|
+
Unexpected exceptions in the collection's response-payload callbacks instead
|
|
783
|
+
throw `ArchiveDerivationError` with stage `response` and become sanitized 500s.
|
|
784
|
+
|
|
785
|
+
The complete public error signatures are:
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
class ArchiveError extends Error {
|
|
789
|
+
constructor(message: string, opts?: ErrorOptions);
|
|
790
|
+
}
|
|
791
|
+
class ArchiveDefinitionError extends ArchiveError {}
|
|
792
|
+
class ArchiveDerivationError extends ArchiveError {
|
|
793
|
+
readonly stage: ArchiveDerivationStage;
|
|
794
|
+
constructor(stage: ArchiveDerivationStage, message: string, opts?: ErrorOptions);
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Catalog rate-limit responses use `Retry-After: 60`; configure catalog limiter periods to 60 seconds. Issuer admission/send-budget responses instead carry seconds until their durable window reopens. Health's degraded response is the health DTO, not an error envelope: schema mismatch reports the observed version; database unavailability reports `dbSchemaVersion: null`.
|
|
799
|
+
|
|
800
|
+
### Usage Guide
|
|
801
|
+
|
|
802
|
+
## Basic Usage
|
|
803
|
+
|
|
804
|
+
A deployment is a collection definition plus one factory call. Put the definition in a
|
|
805
|
+
module both sides import — the browser needs the composed schemas, the Worker needs
|
|
806
|
+
the collection itself.
|
|
807
|
+
|
|
808
|
+
```ts
|
|
809
|
+
// recipes-collection.ts — the one content contract, shared by both sides.
|
|
810
|
+
import { defineArchiveCollection } from '@vielzeug/archive';
|
|
811
|
+
import { s } from '@vielzeug/spell';
|
|
812
|
+
|
|
813
|
+
const payloadSchema = s.object({
|
|
814
|
+
cuisine: s.enum(['italian', 'japanese']),
|
|
815
|
+
ingredients: s.array(s.string().trim().min(1).max(64)).min(1).max(20),
|
|
816
|
+
name: s.string().trim().min(1).max(32),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
export const recipesCollection = defineArchiveCollection({
|
|
820
|
+
// Ingredient order is not identity: the canonical form sorts, so reordered
|
|
821
|
+
// content hashes identically and updates as a no-op.
|
|
822
|
+
canonicalize: (payload) => ({ ...payload, ingredients: [...payload.ingredients].sort() }),
|
|
823
|
+
contentVersion: 1,
|
|
824
|
+
filters: ['italian', 'japanese'],
|
|
825
|
+
index: (payload) => ({
|
|
826
|
+
filter: payload.cuisine,
|
|
827
|
+
tags: [payload.ingredients.length console.log(JSON.stringify(event)));
|
|
828
|
+
|
|
829
|
+
export default worker;
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
The browser imports the same collection for its schemas — never the worker subpath:
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
import { recipesCollection } from './recipes-collection';
|
|
836
|
+
import { archiveIdentitySchema, PUBLISHER_HEADER } from '@vielzeug/archive';
|
|
837
|
+
|
|
838
|
+
const response = await fetch('/api/v1/collections/recipes/entries');
|
|
839
|
+
if (!response.ok) throw new Error('Archive unavailable');
|
|
840
|
+
const page = recipesCollection.contract.page.parse(await response.json());
|
|
841
|
+
const meResponse = await fetch('/api/v1/me', { credentials: 'same-origin' });
|
|
842
|
+
if (!meResponse.ok) throw new Error('Identity unavailable');
|
|
843
|
+
const me = archiveIdentitySchema.parse(await meResponse.json());
|
|
844
|
+
if (me === null) throw new Error('Register a publisher first');
|
|
845
|
+
const mineResponse = await fetch('/api/v1/me/publications?collection=recipes', {
|
|
846
|
+
credentials: 'same-origin', headers: { [PUBLISHER_HEADER]: me.id },
|
|
847
|
+
});
|
|
848
|
+
if (!mineResponse.ok) throw new Error('Own publications unavailable');
|
|
849
|
+
const owned = recipesCollection.contract.ownedEntries.parse(await mineResponse.json());
|
|
850
|
+
console.log(page.entries, owned.entries);
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
## The collection contract
|
|
854
|
+
|
|
855
|
+
`defineArchiveCollection` is the only place content knowledge lives. The definition is
|
|
856
|
+
validated at registration — a bad `name`, `contentVersion`, or any vocabulary value
|
|
857
|
+
(well-formed text, 1..`vocabChars` chars) throws there, never mid-request — and every
|
|
858
|
+
ordinary invalid content returns `null` → `422 invalid-payload`. Collection
|
|
859
|
+
implementation faults instead throw `ArchiveDerivationError` → sanitized
|
|
860
|
+
`500 internal`:
|
|
861
|
+
|
|
862
|
+
- **Parse** — accept stable JSON objects only; schema outputs must remain JSON.
|
|
863
|
+
- **Canonicalize** — optional; a result that leaves the payload schema throws, so a
|
|
864
|
+
broken normalizer can never store invalid content.
|
|
865
|
+
- **Index** — the returned `filter` must be declared in `filters`, every `tag` in
|
|
866
|
+
`tags` (duplicates collapse), and the `title` is trimmed and bounded
|
|
867
|
+
(`titleChars`, well-formed text). A derivation outside the declared vocabulary
|
|
868
|
+
would store a row no list query can reach — it throws instead.
|
|
869
|
+
- **Size** — the canonical JSON must fit `payloadBytes`.
|
|
870
|
+
- **Identity** — the content hash is SHA-256 over `{contentVersion, payload}`:
|
|
871
|
+
identical content hashes identically, a real edit always re-identifies, and the
|
|
872
|
+
core owns the envelope so collections cannot drift it.
|
|
873
|
+
|
|
874
|
+
Declare vocabularies with readonly arrays (`['fire', 'ice']`) for automatic
|
|
875
|
+
literal-union inference. Const generics retain the version literal and vocabulary
|
|
876
|
+
unions; `NoInfer` prevents the index callback from widening them. Omitted arrays
|
|
877
|
+
mean empty vocabularies: return `null` for
|
|
878
|
+
the filter and no tags. Object keys are recursively sorted by the core; schema
|
|
879
|
+
transformations must stabilize and `canonicalize` must be pure and idempotent.
|
|
880
|
+
Non-JSON input returns `null`. Unexpected callback, schema or hashing failures,
|
|
881
|
+
unstable transformations, non-idempotent canonicalization and index mutation throw
|
|
882
|
+
`ArchiveDerivationError`; its `stage` identifies the failing phase.
|
|
883
|
+
Lossy or non-JSON schema output and invalid index-result shapes also throw.
|
|
884
|
+
|
|
885
|
+
`contentVersion` participates in content identity. Request handling validates
|
|
886
|
+
only the current collection definition; it does not interpret historical schemas.
|
|
887
|
+
Archive is unreleased and provides no content-upgrade APIs.
|
|
888
|
+
|
|
889
|
+
Filters, tags and searchable titles are persisted data derived at publish/update
|
|
890
|
+
time. Editing `index` or its vocabularies does not recompute existing projections.
|
|
891
|
+
Keep the active definition and dataset consistent. For an unreleased development
|
|
892
|
+
deployment, deliberately reset and reseed a fresh database when you change payload
|
|
893
|
+
or projection semantics; the package does not supply a migration framework.
|
|
894
|
+
Resetting destroys existing data. Perform it only as an explicit, deliberate
|
|
895
|
+
development operation, not automatically during startup or deployment.
|
|
896
|
+
|
|
897
|
+
**Payloads carry decisions, not content.** The payload schema should accept
|
|
898
|
+
user-authored choices expressed as IDs plus user-written text — never the content's
|
|
899
|
+
own copyrighted expression (card text, artwork, rules), never more than the game's
|
|
900
|
+
own identifiers. For games with paid or DLC content this is the whole answer:
|
|
901
|
+
ownership resolves on each viewer's device against their own library, the service
|
|
902
|
+
never learns what anyone owns, and the strict schema is the enforcement seam —
|
|
903
|
+
use a strict schema that rejects unknown fields, so a collection that only models IDs cannot
|
|
904
|
+
accidentally start shipping paid content. For example, published game builds can
|
|
905
|
+
reference cards, equipment and potions by ID plus a user-written name. The client
|
|
906
|
+
renders pieces the viewer does not own as unavailable. Keep the browser-safe
|
|
907
|
+
publication contract in a shared domain module consumed by both the frontend and
|
|
908
|
+
its backend, without importing frontend application code into the Worker.
|
|
909
|
+
|
|
910
|
+
## The Worker and its environment
|
|
911
|
+
|
|
912
|
+
`createArchiveWorker(collections)` returns the full handler: anonymous public reads,
|
|
913
|
+
cookie-authenticated mutations behind exact-origin and request-marker guards, local
|
|
914
|
+
RS256/JWKS verification followed by an uncached issuer session-version check,
|
|
915
|
+
per-IP read limits and per-subject write limits keyed by peppered HMACs, the
|
|
916
|
+
30-second anonymous cache on public entry lists only, operator moderation, and a
|
|
917
|
+
scheduled expiry sweep for retired publish retry keys and revocation tombstones.
|
|
918
|
+
Health sits outside the rate-limit and session pipelines: probes never burn the
|
|
919
|
+
anonymous read budget or call the issuer.
|
|
920
|
+
|
|
921
|
+
The Worker reads its environment fail-closed — every binding must be present and
|
|
922
|
+
well-formed, or the route answers 503 without naming what is missing:
|
|
923
|
+
|
|
924
|
+
| Variable | Meaning |
|
|
925
|
+
| ------------------------ | ---------------------------------------------------------------- |
|
|
926
|
+
| `APP_ENV` | `local` or `production` (transport rules and the pepper policy) |
|
|
927
|
+
| `ALLOWED_ORIGINS` | Exact canonical origins; never wildcards |
|
|
928
|
+
| `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | Your identity provider |
|
|
929
|
+
| `AUTH_ISSUER_SERVICE` | Required issuer service binding for live session-version checks |
|
|
930
|
+
| `LIKES_ENABLED` / `PUBLISHING_ENABLED` | Feature flags |
|
|
931
|
+
| `RATE_LIMIT_KEY_SECRET` | HMAC pepper. Required ≥ 32 chars in production; local may fall back |
|
|
932
|
+
| `DB` / `READ_LIMIT` / `WRITE_LIMIT` | D1 + rate-limit bindings |
|
|
933
|
+
|
|
934
|
+
Both limiter bindings must run 60-second windows (`simple` limiters:
|
|
935
|
+
`period: 60`): the platform binding does not expose its period at runtime, so the
|
|
936
|
+
Worker's 429 `Retry-After` header is fixed at 60 seconds — a longer window would
|
|
937
|
+
make the header lie.
|
|
938
|
+
|
|
939
|
+
Generated Env types (wrangler's `worker-configuration.d.ts`) satisfy the package's
|
|
940
|
+
structural `ArchiveWorkerEnv` — no `@cloudflare/workers-types` dependency, no casts.
|
|
941
|
+
|
|
942
|
+
## The auth issuer
|
|
943
|
+
|
|
944
|
+
`createAuthWorker()` from `@vielzeug/archive/auth` is the reference passwordless
|
|
945
|
+
identity issuer a deployment runs beside the catalog Worker: one POST with an email
|
|
946
|
+
sends a single-use magic link, and explicitly confirming it sets the `archive_session`
|
|
947
|
+
cookie. The catalog verifies the JWT locally, then calls the issuer through
|
|
948
|
+
`AUTH_ISSUER_SERVICE` to check the account's current session version. Requests create
|
|
949
|
+
pending account rows; explicit link redemption atomically stamps the account's
|
|
950
|
+
sign-in time and claims the token. The issuer's D1 schema ships at
|
|
951
|
+
`node_modules/@vielzeug/archive/migrations/auth/`; point the issuer's
|
|
952
|
+
`migrations_dir` there. The catalog's `JWT_ISSUER` and `JWT_JWKS_URL` must identify
|
|
953
|
+
the issuer, and its audience must match the issuer's `AUDIENCE`.
|
|
954
|
+
|
|
955
|
+
| Variable | Meaning |
|
|
956
|
+
| --- | --- |
|
|
957
|
+
| `APP_NAME` | The app's display name: the magic-link email's subject and copy name it |
|
|
958
|
+
| `ALLOWED_ORIGINS` | Exact origins allowed to trigger unsafe routes (the app origin(s)) |
|
|
959
|
+
| `APP_ENV` | `local` or `production` — production requires HTTPS and sets Secure cookies |
|
|
960
|
+
| `AUDIENCE` / `AUTH_ISSUER` | The claims every minted session carries; the catalog verifies the same pair |
|
|
961
|
+
| `AUTH_DB` / `AUTH_EMAIL` | The issuer's D1 and send-email bindings |
|
|
962
|
+
| `AUTH_ARCHIVE` | Required service binding to the catalog Worker; deletion calls its profile-erasure route |
|
|
963
|
+
| `AUTH_JWT_PRIVATE_JWK` | The RS256 private key as a JWK JSON string: a wrangler secret in production |
|
|
964
|
+
| `AUTH_JWT_PREVIOUS_PUBLIC_JWKS` | Optional retired public RS256 JWKS; publish during rotation until old sessions expire |
|
|
965
|
+
| `OPERATOR_EMAILS` | Comma-separated emails whose newly minted sessions carry the operator role |
|
|
966
|
+
| `PUBLIC_ORIGIN` | App origin for links, confirmation POST origin checks and client navigation after sign-in |
|
|
967
|
+
| `SENDER_ADDRESS` | The address magic links come from: a domain onboarded to Cloudflare Email Service |
|
|
968
|
+
|
|
969
|
+
Under `wrangler dev` the send-email binding is simulated — the link prints to the
|
|
970
|
+
issuer's console, so that console is the inbox. Demo-only Alice/Bob routes are
|
|
971
|
+
deployment wrappers, not package APIs.
|
|
972
|
+
|
|
973
|
+
The issuer first admits sign-in requests through its singleton durable
|
|
974
|
+
`auth_sign_in_budget` table: 20 requests per 60 seconds before creating accounts
|
|
975
|
+
or delivering email. No rate-limiter binding is required. It then
|
|
976
|
+
permits three sign-in emails per email in an hourly durable window.
|
|
977
|
+
Later database or delivery failures do not refund global admission.
|
|
978
|
+
Using or expiring a link never refunds sends; failed delivery also spends the
|
|
979
|
+
allowance. Links expire after 15 minutes. Operator sessions expire after 900
|
|
980
|
+
seconds, ordinary sessions after at most 30 days. Retain retired public keys during
|
|
981
|
+
rotation; the active private key signs every new session. Sign-in input trims and
|
|
982
|
+
lowercases email before validation. `SENDER_ADDRESS` must already be canonical;
|
|
983
|
+
`OPERATOR_EMAILS` entries are trimmed and lowercased before validation.
|
|
984
|
+
|
|
985
|
+
The scheduled sweep deletes expired links and closed per-user send windows, then
|
|
986
|
+
pending accounts only when neither a link nor send-budget row still references
|
|
987
|
+
them. Failed delivery deletes the unusable token but retains the spent allowance;
|
|
988
|
+
it does not immediately remove the pending account. Verified accounts stay until
|
|
989
|
+
explicit erasure. The singleton admission row remains and resets its window on a
|
|
990
|
+
later admitted request.
|
|
991
|
+
|
|
992
|
+
Opening `GET /auth/verify?token=…` renders a no-store confirmation page and does
|
|
993
|
+
not consume the link or change the session, so email scanners cannot sign in by
|
|
994
|
+
fetching it. A deliberate click sends `POST /auth/verify` with JSON `{ token }`,
|
|
995
|
+
the request marker and an exact allowed origin or `PUBLIC_ORIGIN`.
|
|
996
|
+
Success is 204 with Set-Cookie; the page then navigates to `/signed-in`. The
|
|
997
|
+
redemption update and account sign-in stamp are trigger-backed and atomic.
|
|
998
|
+
The page removes the token from the address bar, loads no external assets, and
|
|
999
|
+
sets a restrictive CSP and `Referrer-Policy: no-referrer`.
|
|
1000
|
+
|
|
1001
|
+
`GET /auth/account` exports account metadata only. For full erasure, call
|
|
1002
|
+
`DELETE /auth/account` with `ACCOUNT_HEADER` (`X-Archive-Account`) set to the
|
|
1003
|
+
issuer account UUID captured from `GET /auth/account` before confirmation.
|
|
1004
|
+
The issuer rejects missing or invalid UUIDs with 400 and a different signed-in
|
|
1005
|
+
account with 409 `account-changed`, before any erasure. This guard applies even
|
|
1006
|
+
before publisher registration and must stay pinned across retries.
|
|
1007
|
+
The issuer forwards the caller's session and publisher
|
|
1008
|
+
precondition through `AUTH_ARCHIVE` to `DELETE /me/account`, requires exactly 204,
|
|
1009
|
+
then deletes its
|
|
1010
|
+
own user and clears the cookie. Failure preserves the session for retry. Direct
|
|
1011
|
+
`DELETE /me` removes only the Archive profile and leaves the cookie intact.
|
|
1012
|
+
|
|
1013
|
+
Several game frontends can share one issuer and catalog. Keep the cookie
|
|
1014
|
+
host-only on the central API host, use `credentials: 'include'` from same-site
|
|
1015
|
+
HTTPS game subdomains, and allow each exact frontend origin in both Workers.
|
|
1016
|
+
Do not share cookies through a parent-domain `Domain` attribute. The
|
|
1017
|
+
[multi-collection recipe](./examples/multi-collection-deployment.md) describes
|
|
1018
|
+
the topology, session revalidation and platform-wide account boundary.
|
|
1019
|
+
|
|
1020
|
+
`DELETE /me/account` atomically revokes the issuer/subject and cascades profile
|
|
1021
|
+
deletion. Its tombstone lasts 30 days plus ten seconds; old sessions cannot read
|
|
1022
|
+
private data or register again, including a racing registration. The dedicated
|
|
1023
|
+
account-erasure route alone may bypass the issuer lookup, and only to retry with
|
|
1024
|
+
the same principal's original signed session while its existing tombstone is
|
|
1025
|
+
valid; signature and claims are still verified, and initial erasure validates the
|
|
1026
|
+
issuer. The issuer's `DELETE /auth/account` checks the current version before
|
|
1027
|
+
starting erasure; after completed erasure a retry there returns 401. A fresh
|
|
1028
|
+
issuer account gets a new UUID subject. External issuers must use a new subject to
|
|
1029
|
+
re-enroll during the revocation window. Profile-only deletion does not revoke the
|
|
1030
|
+
subject and permits re-registration with existing credentials.
|
|
1031
|
+
|
|
1032
|
+
Each JWT also carries the issuer account's required safe-integer `sessionVersion`.
|
|
1033
|
+
The issuer compares it with `auth_users.session_version` on every ordinary
|
|
1034
|
+
authenticated route. A JWT with an old version is unauthenticated even before its
|
|
1035
|
+
expiry; the catalog asks the issuer on every private request and does not cache a
|
|
1036
|
+
successful verdict. A valid local JWT with an unavailable, malformed, 5xx or
|
|
1037
|
+
timed-out (fixed 5000 ms, including body decoding) issuer response becomes
|
|
1038
|
+
`503 auth-unavailable`, not anonymous identity.
|
|
1039
|
+
|
|
1040
|
+
## The database
|
|
1041
|
+
|
|
1042
|
+
The package ships the canonical catalog migration:
|
|
1043
|
+
`node_modules/@vielzeug/archive/migrations/0001_community_archive.sql` — the whole schema,
|
|
1044
|
+
moderation included, as one designed file. The runtime SQL in the package is
|
|
1045
|
+
written against exactly this schema, and the package's own integration suite runs
|
|
1046
|
+
against the same file. Point wrangler at the directory:
|
|
1047
|
+
|
|
1048
|
+
```jsonc
|
|
1049
|
+
// wrangler.jsonc — the deployment owns its D1 database, not its DDL.
|
|
1050
|
+
{
|
|
1051
|
+
"d1_databases": [
|
|
1052
|
+
{
|
|
1053
|
+
"binding": "DB",
|
|
1054
|
+
"database_name": "my-app",
|
|
1055
|
+
"migrations_dir": "./node_modules/@vielzeug/archive/migrations"
|
|
1056
|
+
}
|
|
1057
|
+
]
|
|
1058
|
+
}
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
The issuer points to
|
|
1062
|
+
`node_modules/@vielzeug/archive/migrations/auth/0001_auth.sql`. Both files are
|
|
1063
|
+
fresh-database initialization definitions, not upgrades. The issuer DDL initializes
|
|
1064
|
+
`auth_users.session_version` to `0` and stamps `auth_schema_meta.applied` as
|
|
1065
|
+
`AUTH_SCHEMA_VERSION` (`1`). Neither file automatically deletes or resets data.
|
|
1066
|
+
While Archive is unreleased, its
|
|
1067
|
+
canonical initial schemas are edited directly. Reset and reseed an unreleased
|
|
1068
|
+
development database deliberately when its schema or collection semantics no
|
|
1069
|
+
longer match; reapplying the initial file does not update an existing dataset.
|
|
1070
|
+
The initialization file also stamps the storage schema
|
|
1071
|
+
(`archive_schema_meta`) at `ARCHIVE_SCHEMA_VERSION`, and health reads it back: a
|
|
1072
|
+
database with a mismatched stamp answers `status: 'degraded'` with the
|
|
1073
|
+
observed `dbSchemaVersion` and HTTP 503 (`reason: 'schema-mismatch'`).
|
|
1074
|
+
An unavailable database produces HTTP 503 with `dbSchemaVersion: null` and
|
|
1075
|
+
`reason: 'database-unavailable'`. The debut storage stamp is `1`. A unit
|
|
1076
|
+
test pins the shipped DDL's `CHECK` bounds to `ARCHIVE_LIMITS`, so the two can never
|
|
1077
|
+
drift silently. The stamp does not detect changes in your collection's payload
|
|
1078
|
+
schema, vocabulary or indexing function; you own that dataset consistency.
|
|
1079
|
+
|
|
1080
|
+
## Routes and wire names
|
|
1081
|
+
|
|
1082
|
+
```
|
|
1083
|
+
GET /api/v1/health
|
|
1084
|
+
POST /api/v1/sign-out
|
|
1085
|
+
GET /api/v1/auth/health
|
|
1086
|
+
GET /api/v1/auth/session (internal issuer check)
|
|
1087
|
+
POST /api/v1/auth/sign-out-all (revoke every session)
|
|
1088
|
+
GET /api/v1/collections/:collection (self-description)
|
|
1089
|
+
GET /api/v1/collections/:collection/entries (author, filter, tag, search, sort, cursor, limit)
|
|
1090
|
+
POST /api/v1/collections/:collection/entries
|
|
1091
|
+
GET /api/v1/collections/:collection/entries/:id
|
|
1092
|
+
PUT /api/v1/collections/:collection/entries/:id (expectedRevision)
|
|
1093
|
+
DELETE /api/v1/collections/:collection/entries/:id
|
|
1094
|
+
PUT /api/v1/collections/:collection/entries/:id/like
|
|
1095
|
+
DELETE /api/v1/collections/:collection/entries/:id/like
|
|
1096
|
+
POST /api/v1/collections/:collection/entries/:id/report (reason)
|
|
1097
|
+
PUT /api/v1/collections/:collection/entries/:id/hidden (operator)
|
|
1098
|
+
DELETE /api/v1/collections/:collection/entries/:id/hidden (operator)
|
|
1099
|
+
GET /api/v1/collections/:collection/reports (operator: status, cursor, limit)
|
|
1100
|
+
PUT /api/v1/collections/:collection/reports/:id/resolved (operator: report ID)
|
|
1101
|
+
DELETE /api/v1/publishers/:id (operator: erase any publisher)
|
|
1102
|
+
GET /api/v1/moderation-log (operator: cursor, limit)
|
|
1103
|
+
GET /api/v1/me POST /me PUT /me DELETE /me
|
|
1104
|
+
GET /api/v1/me/publications?collection=:collection GET /api/v1/me/likes[?ids=…]
|
|
1105
|
+
GET /api/v1/me/reports (cursor, limit)
|
|
1106
|
+
GET /api/v1/auth/account
|
|
1107
|
+
GET /api/v1/auth/jwks.json
|
|
1108
|
+
POST /api/v1/auth/sign-in (email)
|
|
1109
|
+
DELETE /api/v1/auth/account (coordinated full erasure)
|
|
1110
|
+
DELETE /api/v1/me/account (issuer erasure seam; revokes subject)
|
|
1111
|
+
GET /api/v1/auth/verify?token=… (confirmation only)
|
|
1112
|
+
POST /api/v1/auth/verify (guarded {token} redemption)
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
Publishing is bounded by `maxPublications` live entries per `(collection, owner)`.
|
|
1116
|
+
The quota check runs inside the guarded insert.
|
|
1117
|
+
|
|
1118
|
+
`/me/likes` without `ids`, `/me/reports`, operator reports and `/moderation-log`
|
|
1119
|
+
use forward keyset pages: follow `cursor` until `null`, preserving query context.
|
|
1120
|
+
`limit` defaults to `pageSize` and is at most `maxPageSize`; there is no total
|
|
1121
|
+
export bound. `/me/likes?ids=…` remains a subset lookup returning `{ ids }`, not a
|
|
1122
|
+
page, and cannot combine `ids` with `cursor` or `limit`.
|
|
1123
|
+
|
|
1124
|
+
Shared wire names live in the package — `archive_session` cookie,
|
|
1125
|
+
`x-archive-publisher` and `x-archive-request` headers, `ARCHIVE_ERROR_CODES`, and
|
|
1126
|
+
`ARCHIVE_LIMITS` — so a client and the Worker cannot drift. Parse failures with the
|
|
1127
|
+
collection's composed schemas; parse errors with `archiveErrorResponseSchema`, the
|
|
1128
|
+
sign-out body with `archiveOkSchema`, a collection's self-description with
|
|
1129
|
+
`archiveCollectionMetaSchema` and the operator queue with `archiveReportsSchema`.
|
|
1130
|
+
Every entry's `schemaVersion` is the archive contract's version, not the
|
|
1131
|
+
collection's; the collection's own format version lives in its hash envelope and in
|
|
1132
|
+
each entry's `contentVersion`.
|
|
1133
|
+
|
|
1134
|
+
Only anonymous entry lists carry a 30-second edge-cache contract
|
|
1135
|
+
(`publicCacheSeconds`). Detail is always `no-store`. Any Cookie or Authorization
|
|
1136
|
+
header disables caching, regardless of validity; metadata, private and error
|
|
1137
|
+
responses also use `no-store`.
|
|
1138
|
+
|
|
1139
|
+
Implement UI ownership checks in your application by comparing the current
|
|
1140
|
+
publisher ID to the entry's `author.id`; there is no package action-policy helper.
|
|
1141
|
+
Server mutations remain authoritative.
|
|
1142
|
+
|
|
1143
|
+
`POST /api/v1/sign-out` is deliberately local: it clears only this browser's
|
|
1144
|
+
cookie and does not contact the issuer or revoke copied sessions. To revoke every
|
|
1145
|
+
session, post to `/api/v1/auth/sign-out-all` with the mutation marker, an exact
|
|
1146
|
+
allowed `Origin`, and the issuer account UUID captured from
|
|
1147
|
+
`GET /api/v1/auth/account` in `X-Archive-Account`. The issuer returns 409
|
|
1148
|
+
`account-changed` if the active subject differs. On success it atomically increments
|
|
1149
|
+
`session_version`, deletes outstanding links, preserves account and catalog data,
|
|
1150
|
+
and returns `{ ok: true }` with an expired cookie. Requests authorized before
|
|
1151
|
+
revocation may finish; there is no cross-database cancellation guarantee.
|
|
1152
|
+
|
|
1153
|
+
`GET /api/v1/auth/health` is public and read-only. It validates issuer
|
|
1154
|
+
configuration, key signing/verification, binding shapes, the `auth_schema_meta`
|
|
1155
|
+
stamp and expected tables, columns and triggers, returns `AuthHealth` through
|
|
1156
|
+
`archiveAuthHealthSchema`, and answers 503 `degraded` for configuration,
|
|
1157
|
+
database or schema failures. It never writes, sends email or probes the catalog.
|
|
1158
|
+
|
|
1159
|
+
## Moderation
|
|
1160
|
+
|
|
1161
|
+
Operator authority is the session token's optional `roles` claim carrying
|
|
1162
|
+
`ARCHIVE_OPERATOR_ROLE` (`'archive-operator'`) — minted by your issuer, verified with
|
|
1163
|
+
the same signature, audience and issuer checks as any session, and never a separate
|
|
1164
|
+
credential. Moderation routes additionally require a registered publisher row: one
|
|
1165
|
+
accountable identity per action.
|
|
1166
|
+
|
|
1167
|
+
- **Report** — any registered publisher reports a visible entry with a bounded
|
|
1168
|
+
free-text reason (`reportReasonChars`). Idempotent per (entry, reporter): the same
|
|
1169
|
+
report twice is the same 204. Missing or hidden entries answer 404 — there is
|
|
1170
|
+
nothing left to report.
|
|
1171
|
+
- **Hide** — an operator removes an entry from every public surface (list, detail,
|
|
1172
|
+
likes) with a soft-hide: the row, its likes and its retry key stay intact. The
|
|
1173
|
+
owner still sees the entry in `/me/publications` with `hidden: true`, edits and
|
|
1174
|
+
key replay are frozen (403), removal stays allowed, and unhiding restores
|
|
1175
|
+
everything. Already hidden or already visible answer the same 204.
|
|
1176
|
+
- **Queue** — the operator pages newest reports with entry title and reporter
|
|
1177
|
+
identity; `status` is `unresolved` (default) or `resolved`. Each report has its
|
|
1178
|
+
own `id` and nullable `resolvedAt`. Resolve it with
|
|
1179
|
+
`PUT /collections/:collection/reports/:id/resolved`; repeating is a 204 no-op.
|
|
1180
|
+
- **Log** — every effective moderation action (hide, unhide, removal, publisher
|
|
1181
|
+
erasure) appends one accountability row in the same transaction that applies it,
|
|
1182
|
+
and `GET /api/v1/moderation-log` pages the newest actions to
|
|
1183
|
+
operators. Rows carry only ids, the action kind and the time — no names (they
|
|
1184
|
+
change), no content — and no foreign keys: the audit must outlive the erasures it
|
|
1185
|
+
records. Idempotent repeats (hiding an already-hidden entry, erasing an
|
|
1186
|
+
already-erased publisher) answer the same 204 with no new row, including
|
|
1187
|
+
concurrent repeats: audit insertion tests the effective state inside the batch.
|
|
1188
|
+
|
|
1189
|
+
Like and unlike both require a visible entry. Their SQL guards visibility during
|
|
1190
|
+
the mutation, and the response checks it again; hiding or deleting an entry
|
|
1191
|
+
cannot turn a racing unlike into a successful response for unavailable content.
|
|
1192
|
+
|
|
1193
|
+
## Observability
|
|
1194
|
+
|
|
1195
|
+
The core never logs. `worker.tap(handler, { signal? })` emits one `ArchiveEvent` per
|
|
1196
|
+
request — `{ type: 'request', code, ms, requestId, route, status, diagnostic? }` —
|
|
1197
|
+
and successful sweep — `{ type: 'scheduled', status: 'ok', removed }`. Failed
|
|
1198
|
+
sweeps emit `{ type: 'scheduled', status: 'failed', diagnostic }` and rethrow so
|
|
1199
|
+
the runtime sees failure. Cache failures emit `{ type: 'cache-error', diagnostic }`.
|
|
1200
|
+
The Worker swallows observer errors so
|
|
1201
|
+
observability cannot affect the Worker. With no handlers registered there is zero
|
|
1202
|
+
work. Route is a finite class like `/collections/:collection/entries`, never a
|
|
1203
|
+
raw path; no event carries a URL, cookie, subject, alias or token. Diagnostics are
|
|
1204
|
+
typed sanitized categories, not raw exceptions. A pre-aborted tap signal is inert.
|
|
1205
|
+
The sweep's `removed` counts expired retry keys and revocation tombstones together.
|
|
1206
|
+
|
|
1207
|
+
The issuer also exposes `tap()` with `AuthEvent` from `@vielzeug/archive/auth`.
|
|
1208
|
+
It never logs directly. Events contain finite route labels, codes, counts,
|
|
1209
|
+
duration and sanitized `AuthDiagnostic` categories, never emails, tokens or raw
|
|
1210
|
+
exceptions. Scheduled failures emit then rethrow; pre-aborted taps are inert.
|
|
1211
|
+
Issuer sweep counts include expired tokens, closed per-user send windows and
|
|
1212
|
+
removed pending accounts; the singleton admission row is not removed.
|
|
1213
|
+
|
|
1214
|
+
```ts
|
|
1215
|
+
import { createLogger } from '@vielzeug/rune';
|
|
1216
|
+
import { createArchiveWorker } from '@vielzeug/archive/worker';
|
|
1217
|
+
import { recipesCollection } from './recipes-collection';
|
|
1218
|
+
|
|
1219
|
+
const log = createLogger({ namespace: 'recipes' });
|
|
1220
|
+
const worker = createArchiveWorker([recipesCollection]);
|
|
1221
|
+
worker.tap((event) => log.debug(event, `archive:${event.type}`));
|
|
1222
|
+
export default worker;
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
## Testing
|
|
1226
|
+
|
|
1227
|
+
The package proves itself against real workerd + local D1:
|
|
1228
|
+
|
|
1229
|
+
```sh
|
|
1230
|
+
pnpm vitest run packages/archive/src/__tests__/ # fast unit suite
|
|
1231
|
+
pnpm --filter @vielzeug/archive test:integration # real workerd + local D1 + issuer coverage
|
|
1232
|
+
pnpm --filter @vielzeug/archive test:types # strict types over src and the integration suite
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
Prove your own collection the way the reference deployment does: a slim deployment
|
|
1236
|
+
suite that publishes over your real vocabulary and asserts the derivation, plus the
|
|
1237
|
+
package's generic suite for the core behavior. A collection with an `index` that can
|
|
1238
|
+
drift out of vocabulary should unit-test `derive` directly: ordinary invalid input
|
|
1239
|
+
returns `null`, while implementation faults throw `ArchiveDerivationError`.
|
|
1240
|
+
Direct DDL tests exercise the shipped schema rather than a duplicate schema.
|
|
1241
|
+
|
|
1242
|
+
## Framework Integration
|
|
1243
|
+
|
|
1244
|
+
The root entry is plain TypeScript with no framework coupling; Vue, React and Svelte
|
|
1245
|
+
apps all consume the composed schemas the same way. A Vue 3 composable that pages a
|
|
1246
|
+
collection:
|
|
1247
|
+
|
|
1248
|
+
```ts
|
|
1249
|
+
// useRecipes.ts — one collection's typed client in any component tree.
|
|
1250
|
+
import { recipesCollection } from './recipes-collection';
|
|
1251
|
+
import { ref, shallowRef } from 'vue';
|
|
1252
|
+
|
|
1253
|
+
export function useRecipes() {
|
|
1254
|
+
const entries = shallowRef[]>([]);
|
|
1255
|
+
const cursor = ref(null);
|
|
1256
|
+
async function load(sort: 'liked' | 'recent') {
|
|
1257
|
+
const params = new URLSearchParams({ sort });
|
|
1258
|
+
if (cursor.value !== null) params.set('cursor', cursor.value);
|
|
1259
|
+
const response = await fetch(`/api/v1/collections/recipes/entries?${params}`);
|
|
1260
|
+
if (!response.ok) throw new Error('The archive is unavailable.');
|
|
1261
|
+
const page = recipesCollection.contract.page.parse(await response.json());
|
|
1262
|
+
entries.value = [...entries.value, ...page.entries];
|
|
1263
|
+
cursor.value = page.cursor;
|
|
1264
|
+
}
|
|
1265
|
+
return { cursor, entries, load };
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
## Working with Other Vielzeug Libraries
|
|
1270
|
+
|
|
1271
|
+
- [Spell](/spell/) — the payload schema a collection declares; the core composes the
|
|
1272
|
+
envelope schemas from it, so the wire and the types cannot drift.
|
|
1273
|
+
- [Courier](/courier/) — the HTTP client shape an app's frontend mirrors to consume
|
|
1274
|
+
these routes: strict response schemas over one typed port.
|
|
1275
|
+
- [Rune](/rune/) — the structured logger a deployment points `tap()` events at.
|
|
1276
|
+
|
|
1277
|
+
## Best Practices
|
|
1278
|
+
|
|
1279
|
+
- Compose client schemas from `collection.contract`; never hand-write the envelope.
|
|
1280
|
+
- Declare exactly the vocabulary your `index` derives, with union types where you
|
|
1281
|
+
have them; a value outside it rejects loudly.
|
|
1282
|
+
- Keep `canonicalize` pure and schema-true: reordered lists in, canonical form out.
|
|
1283
|
+
- Initialize fresh databases with the shipped DDL and deliberately reset/reseed
|
|
1284
|
+
unreleased development data when collection or storage semantics change.
|
|
1285
|
+
- Treat `ARCHIVE_ERROR_CODES` and `archiveErrorResponseSchema` as one tested contract.
|
|
1286
|
+
- Pin the expected-publisher header from the identity you captured at sign-in, never from storage alone.
|
|
1287
|
+
- Grant the operator role claim only through your issuer, and only to accounts that
|
|
1288
|
+
are also registered publishers.
|
|
1289
|
+
- Wire a sanitized `tap()` sink and register the rate-limit pepper as a production secret.
|
|
1290
|
+
|
|
1291
|
+
### Examples
|
|
1292
|
+
|
|
1293
|
+
- [Stand Up a Content Archive](./examples/stand-up-a-content-archive.md)
|
|
1294
|
+
- [Content Identity and Versions](./examples/content-identity-and-versions.md)
|
|
1295
|
+
- [Multi-Collection Deployment](./examples/multi-collection-deployment.md)
|
|
1296
|
+
- [Moderate Content](./examples/moderate-content.md)
|
|
1297
|
+
- [Client Error Mapping](./examples/client-error-mapping.md)
|
|
1298
|
+
- [GDPR Erasure and Export](./examples/gdpr-erasure-and-export.md)
|
|
4
1299
|
|
|
5
1300
|
---
|
|
6
1301
|
|
|
@@ -119,6 +1414,8 @@ console.log(byRole, health, profile);
|
|
|
119
1414
|
| `allocate` | Lossless proportional distribution | Sync | Available from `/math` |
|
|
120
1415
|
| `isEqual` | Structural equality | Sync | Root export |
|
|
121
1416
|
| `hash` | Deterministic serialization for cache keys | Sync | Available from `/object` |
|
|
1417
|
+
| `compressBytes` / `decompressBytes` | Native byte compression with bounded decompression | Async | Requires native compression streams; decoding requires an output limit |
|
|
1418
|
+
| `pack` / `unpack` | Versioned, compressed, URL-safe payload envelope | Async | Both sides bound the payload with `maxBytes`; compression falls back to raw |
|
|
122
1419
|
|
|
123
1420
|
## Package Entry Points
|
|
124
1421
|
|
|
@@ -128,6 +1425,7 @@ console.log(byRole, health, profile);
|
|
|
128
1425
|
| `@vielzeug/arsenal/array` | Immutable transforms, set operations, sorting, fuzzy search |
|
|
129
1426
|
| `@vielzeug/arsenal/async` | Retry, cancellation, task pool, timing |
|
|
130
1427
|
| `@vielzeug/arsenal/cache` | In-memory cache and memoization |
|
|
1428
|
+
| `@vielzeug/arsenal/encoding` | Base64url and native byte compression |
|
|
131
1429
|
| `@vielzeug/arsenal/function` | Assertions, composition, timing, teardown |
|
|
132
1430
|
| `@vielzeug/arsenal/guards` | Type guards, predicate combinators, equality guards |
|
|
133
1431
|
| `@vielzeug/arsenal/math` | Ranges, aggregation, statistics, interpolation, exact allocation |
|
|
@@ -245,12 +1543,107 @@ base64UrlToBytes(code); // throws on characters outside the alphabet
|
|
|
245
1543
|
utf8Bytes('Hunter äöü'); // 11: byte length of a UTF-8 string
|
|
246
1544
|
```
|
|
247
1545
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
### tryParseJson
|
|
1546
|
+
### compressBytes
|
|
251
1547
|
|
|
252
1548
|
```ts
|
|
253
|
-
|
|
1549
|
+
compressBytes(bytes: Uint8Array, options?: CompressionOptions): Promise
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
Compresses bytes using the native `CompressionStream` API without modifying the input.
|
|
1553
|
+
|
|
1554
|
+
| Option | Type | Default | Meaning |
|
|
1555
|
+
| --- | --- | --- | --- |
|
|
1556
|
+
| `format` | `CompressionFormat` | `'deflate-raw'` | Compression format; the receiver must use the same format |
|
|
1557
|
+
|
|
1558
|
+
**Returns:** Compressed bytes. Rejects with `ArsenalSerializationError` when the format or capability is unavailable, or stream processing fails. Does not fall back to another format.
|
|
1559
|
+
|
|
1560
|
+
**Example:** Encode compressed bytes for a URL.
|
|
1561
|
+
|
|
1562
|
+
```ts
|
|
1563
|
+
import { bytesToBase64Url, compressBytes } from '@vielzeug/arsenal';
|
|
1564
|
+
|
|
1565
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ name: 'Hunters' }));
|
|
1566
|
+
const code = bytesToBase64Url(await compressBytes(bytes));
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1571
|
+
### decompressBytes
|
|
1572
|
+
|
|
1573
|
+
```ts
|
|
1574
|
+
decompressBytes(bytes: Uint8Array, options: DecompressionOptions): Promise
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
Decompresses bytes using the native `DecompressionStream` API, enforcing the output limit while reading chunks.
|
|
1578
|
+
|
|
1579
|
+
| Option | Type | Default | Meaning |
|
|
1580
|
+
| --- | --- | --- | --- |
|
|
1581
|
+
| `format` | `CompressionFormat` | `'deflate-raw'` | Format used by the sender |
|
|
1582
|
+
| `maxOutputBytes` | `number` | Required | Non-negative safe integer; maximum decoded byte length |
|
|
1583
|
+
|
|
1584
|
+
**Returns:** Decompressed bytes. Rejects with `ArsenalSerializationError` for invalid limits, corrupt or truncated data, unavailable formats/capabilities, or output exceeding the limit. A failed stream is canceled and its reader lock released. Bound encoded input separately before calling this function.
|
|
1585
|
+
|
|
1586
|
+
**Example:** Decode text within an application-defined byte limit.
|
|
1587
|
+
|
|
1588
|
+
```ts
|
|
1589
|
+
import { compressBytes, decompressBytes } from '@vielzeug/arsenal';
|
|
1590
|
+
|
|
1591
|
+
const compressed = await compressBytes(new TextEncoder().encode('Hunters'));
|
|
1592
|
+
const bytes = await decompressBytes(compressed, { maxOutputBytes: 8192 });
|
|
1593
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
---
|
|
1597
|
+
|
|
1598
|
+
### pack
|
|
1599
|
+
|
|
1600
|
+
```ts
|
|
1601
|
+
pack(payload: unknown, options: ShareCodeOptions): Promise
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
Serializes `payload` as JSON and encodes it into a compact share code: `prefix` + mode marker + base64url bytes, compressed with raw DEFLATE when the compressed form is shorter and the runtime supports the streams.
|
|
1605
|
+
|
|
1606
|
+
| Option | Type | Meaning |
|
|
1607
|
+
| --- | --- | --- |
|
|
1608
|
+
| `maxBytes` | `number` | Positive safe integer; maximum serialized payload size |
|
|
1609
|
+
| `prefix` | `string` | 1–8 alphanumeric characters identifying the code family and version |
|
|
1610
|
+
|
|
1611
|
+
**Returns:** The shorter of the raw (`prefix` + `r`) and compressed (`prefix` + `d`) representations. Throws `ShareCodeError` (`too-large`, `invalid`) for oversized payloads and misconfigured options.
|
|
1612
|
+
|
|
1613
|
+
**Example:** Encode a shareable build as a code small enough for a URL.
|
|
1614
|
+
|
|
1615
|
+
```ts
|
|
1616
|
+
import { pack } from '@vielzeug/arsenal';
|
|
1617
|
+
|
|
1618
|
+
const code = await pack({ cards: ['slash', 'guard'] }, { maxBytes: 8192, prefix: 'ab1' });
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
---
|
|
1622
|
+
|
|
1623
|
+
### unpack
|
|
1624
|
+
|
|
1625
|
+
```ts
|
|
1626
|
+
unpack(code: string, options: ShareCodeOptions): Promise
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
Decodes a share code produced by `pack` with the same options. Both sides are bounded: input longer than `maxBytes` could carry is refused up front, streamed decompression stops at `maxBytes`, and non-canonical base64url is rejected by round-trip.
|
|
1630
|
+
|
|
1631
|
+
**Returns:** The parsed JSON payload. Throws `ShareCodeError` (`invalid` for foreign or malformed codes, `too-large` for budget overruns, `compression-unavailable` when a compressed code arrives on a runtime without `DecompressionStream`). Underlying compression failures surface as `ArsenalSerializationError`.
|
|
1632
|
+
|
|
1633
|
+
**Example:** Decode untrusted input within the byte budget.
|
|
1634
|
+
|
|
1635
|
+
```ts
|
|
1636
|
+
import { unpack } from '@vielzeug/arsenal';
|
|
1637
|
+
|
|
1638
|
+
const payload = await unpack(code, { maxBytes: 8192, prefix: 'ab1' });
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
## Object
|
|
1642
|
+
|
|
1643
|
+
### tryParseJson
|
|
1644
|
+
|
|
1645
|
+
```ts
|
|
1646
|
+
type JsonParseResult = { ok: true; value: unknown } | { error: SyntaxError; ok: false };
|
|
254
1647
|
|
|
255
1648
|
tryParseJson(text: string): JsonParseResult
|
|
256
1649
|
```
|
|
@@ -327,13 +1720,22 @@ type CacheOptions = {
|
|
|
327
1720
|
now?: () => number;
|
|
328
1721
|
ttlMs?: number;
|
|
329
1722
|
};
|
|
1723
|
+
|
|
1724
|
+
interface CompressionOptions {
|
|
1725
|
+
format?: CompressionFormat;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
interface DecompressionOptions extends CompressionOptions {
|
|
1729
|
+
maxOutputBytes: number;
|
|
1730
|
+
}
|
|
330
1731
|
```
|
|
331
1732
|
|
|
332
1733
|
## Errors
|
|
333
1734
|
|
|
334
1735
|
- `RangeError`: invalid numeric bounds, capacity, concurrency, or retry count.
|
|
335
1736
|
- `TypeError`: invalid value types, unsupported comparison, or required path missing.
|
|
336
|
-
- `ArsenalSerializationError`: memo or hash cannot serialize supplied input.
|
|
1737
|
+
- `ArsenalSerializationError`: memo or hash cannot serialize supplied input; compression fails, is unavailable, receives an invalid output limit, or exceeds that limit. Stream failures retain the original error as `cause`.
|
|
1738
|
+
- `ShareCodeError`: a share code cannot be encoded or decoded; the `reason` field is `invalid`, `too-large`, or `compression-unavailable`.
|
|
337
1739
|
|
|
338
1740
|
### Usage Guide
|
|
339
1741
|
|
|
@@ -500,6 +1902,27 @@ const payload = JSON.parse(base64UrlToText(code));
|
|
|
500
1902
|
|
|
501
1903
|
Decoding throws on characters outside the base64url alphabet, so corrupted input fails loudly instead of decoding to garbage.
|
|
502
1904
|
|
|
1905
|
+
## Compress Share Payloads
|
|
1906
|
+
|
|
1907
|
+
Compress UTF-8 bytes before Base64url encoding. Set a decoded-byte limit before accepting compressed input from another device.
|
|
1908
|
+
|
|
1909
|
+
```ts
|
|
1910
|
+
import { base64UrlToBytes, bytesToBase64Url, compressBytes, decompressBytes } from '@vielzeug/arsenal';
|
|
1911
|
+
|
|
1912
|
+
const payload = JSON.stringify({ name: 'Hunters', notes: 'An expedition through Alborea. '.repeat(20) });
|
|
1913
|
+
const compressed = await compressBytes(new TextEncoder().encode(payload));
|
|
1914
|
+
const code = bytesToBase64Url(compressed);
|
|
1915
|
+
const bytes = await decompressBytes(base64UrlToBytes(code), { maxOutputBytes: 8192 });
|
|
1916
|
+
const restored = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1917
|
+
console.log(restored === payload);
|
|
1918
|
+
```
|
|
1919
|
+
|
|
1920
|
+
Both helpers default to `deflate-raw`. You can explicitly select `gzip` or `deflate`; use the same format at both ends. The helpers reject when native streams do not support the selected format. They do not choose a fallback or define a wire protocol.
|
|
1921
|
+
|
|
1922
|
+
For a complete versioned envelope over these primitives — prefix, compression mode marker, byte budgets on both sides — see [`pack` / `unpack`](./api.md#pack).
|
|
1923
|
+
|
|
1924
|
+
Compression can enlarge small inputs. Compare the final encoded strings, including any format marker, before choosing a representation. Bound input size before decoding Base64url and validate decompressed JSON against your schema. The decompression limit bounds retained output, not native stream working memory.
|
|
1925
|
+
|
|
503
1926
|
## Working with Other Vielzeug Libraries
|
|
504
1927
|
|
|
505
1928
|
Use Spell after `tryParseJson` for typed external data. Use Vault instead of `cache` when data must survive reloads or process restart.
|
|
@@ -533,6 +1956,7 @@ const settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };
|
|
|
533
1956
|
- [Array utilities](./examples/array.md)
|
|
534
1957
|
- [Async utilities](./examples/async.md)
|
|
535
1958
|
- [Cache utilities](./examples/cache.md)
|
|
1959
|
+
- [Encoding utilities](./examples/encoding.md)
|
|
536
1960
|
- [Function utilities](./examples/function.md)
|
|
537
1961
|
- [Guards / Typed predicates](./examples/typed.md)
|
|
538
1962
|
- [Math utilities](./examples/math.md)
|
|
@@ -555,6 +1979,7 @@ const settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };
|
|
|
555
1979
|
- taskPool - Bounded concurrent work (id: `async-queue`)
|
|
556
1980
|
- retry - Retry failed operations (id: `async-retry`)
|
|
557
1981
|
- waitFor - Poll until condition is true or timeout/abort fires (id: `async-waitFor`)
|
|
1982
|
+
- compressBytes - Bounded share payload compression (id: `encoding-compression`)
|
|
558
1983
|
- debounce - Trailing (default) and leading-edge options (id: `function-debounce`)
|
|
559
1984
|
- memo - LRU cache with size tracking and invalidation (id: `function-memo`)
|
|
560
1985
|
- pipe - Left-to-right function composition (id: `function-pipe`)
|
|
@@ -3207,7 +4632,13 @@ Sends one HTTP request. `config.method` defaults to `GET`; pass any method (incl
|
|
|
3207
4632
|
| `timeout` | `number` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity`; overrides default |
|
|
3208
4633
|
| `fetchInit` | `Omit` | Raw fetch options for advanced use |
|
|
3209
4634
|
|
|
3210
|
-
**Returns:** Parsed response body (or `undefined` for empty bodies).
|
|
4635
|
+
**Returns:** Parsed response body (or `undefined` for empty bodies). With
|
|
4636
|
+
`responseType: 'raw'` the resolved value is the `Response` itself; a `204`/`205`
|
|
4637
|
+
answer whose transport carried an unexpected empty body stream is normalized to
|
|
4638
|
+
`body: null` — the stray stream is cancelled and headers, status, statusText,
|
|
4639
|
+
URL, redirect and type metadata are preserved (on the response and its clones).
|
|
4640
|
+
A null-body raw response owns no pending resource, so no consumption is required
|
|
4641
|
+
to release the request.
|
|
3211
4642
|
|
|
3212
4643
|
```ts
|
|
3213
4644
|
const created = await courier.request('/users', {
|
|
@@ -3487,7 +4918,7 @@ Concurrent callers share physical work but own their own signal and timeout. One
|
|
|
3487
4918
|
|
|
3488
4919
|
## Response Parsing
|
|
3489
4920
|
|
|
3490
|
-
Courier parses successful 2xx response bodies automatically based on `content-type`, unless you set `responseType` explicitly (`'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'raw'` for the `Response` itself). Empty JSON bodies return `undefined` instead of throwing. Raw response bodies remain owned by Courier until consumed or cancelled, so `cancelAll()` and `dispose()` still abort them. Raw responses cannot be combined with `schema`. Always consume or cancel the body to release request ownership, and set `timeout: Infinity` for intentionally long-lived streams. Courier preserves URL, redirect, and type metadata on tracked responses and their clones.
|
|
4921
|
+
Courier parses successful 2xx response bodies automatically based on `content-type`, unless you set `responseType` explicitly (`'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'raw'` for the `Response` itself). Empty JSON bodies return `undefined` instead of throwing. Raw response bodies remain owned by Courier until consumed or cancelled, so `cancelAll()` and `dispose()` still abort them. Raw responses cannot be combined with `schema`. Always consume or cancel the body to release request ownership, and set `timeout: Infinity` for intentionally long-lived streams. Courier preserves URL, redirect, and type metadata on tracked responses and their clones. A `204`/`205` raw answer is normalized to `body: null` — a stray empty transport stream is cancelled for you — so a null-body raw response never carries pending body ownership and needs no consumption.
|
|
3491
4922
|
|
|
3492
4923
|
```ts
|
|
3493
4924
|
const buffer = await courier.request('/files/{id}', {
|
|
@@ -3670,6 +5101,7 @@ There is no `use()` method. To change middleware, create a new client. This keep
|
|
|
3670
5101
|
## Examples
|
|
3671
5102
|
|
|
3672
5103
|
- [Authentication](./examples/authentication.md)
|
|
5104
|
+
- [Service Discovery and Custom Fetch](./examples/service-discovery-and-custom-fetch.md)
|
|
3673
5105
|
- [CRUD Operations](./examples/crud-operations.md)
|
|
3674
5106
|
- [Disposal](./examples/disposal.md)
|
|
3675
5107
|
- [Error Handling Patterns](./examples/error-handling-patterns.md)
|
|
@@ -16403,64 +17835,60 @@ The IndexedDB adapter is built on Vault. Use Vault directly for unrelated storag
|
|
|
16403
17835
|
|
|
16404
17836
|
## Why Prism?
|
|
16405
17837
|
|
|
16406
|
-
|
|
17838
|
+
Prism renders charts as SVG in a host element and keeps application state outside the chart. You can replace chart data explicitly, respond to container resizing, and style the output with CSS custom properties.
|
|
16407
17839
|
|
|
16408
17840
|
```ts
|
|
16409
|
-
// Before: Chart.js, imperative setup with a canvas you can't CSS-theme
|
|
16410
|
-
import Chart from 'chart.js/auto';
|
|
16411
|
-
const ctx = document.getElementById('myChart') as HTMLCanvasElement;
|
|
16412
|
-
new Chart(ctx, {
|
|
16413
|
-
type: 'line',
|
|
16414
|
-
data: { labels, datasets: [{ data: values }] },
|
|
16415
|
-
// re-create or mutate the Chart.js instance when data changes
|
|
16416
|
-
});
|
|
16417
|
-
|
|
16418
|
-
// After: Prism, responsive SVG with an explicit update boundary
|
|
16419
17841
|
import { createLineChart } from '@vielzeug/prism';
|
|
16420
17842
|
|
|
16421
|
-
const
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
17843
|
+
const host = document.createElement('div');
|
|
17844
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
17845
|
+
document.body.append(host);
|
|
17846
|
+
|
|
17847
|
+
// Before: replace a hand-drawn SVG each time values change.
|
|
17848
|
+
function drawLine(values: number[]) {
|
|
17849
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
17850
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
17851
|
+
const low = Math.min(...values);
|
|
17852
|
+
const span = Math.max(...values) - low || 1;
|
|
17853
|
+
const points = values.map((value, index) => {
|
|
17854
|
+
const x = (index / Math.max(1, values.length - 1)) * 640;
|
|
17855
|
+
const y = 320 - ((value - low) / span) * 320;
|
|
17856
|
+
return `${index === 0 ? 'M' : 'L'}${x},${y}`;
|
|
17857
|
+
});
|
|
17858
|
+
svg.setAttribute('viewBox', '0 0 640 320');
|
|
17859
|
+
path.setAttribute('d', points.join(' '));
|
|
17860
|
+
path.setAttribute('fill', 'none');
|
|
17861
|
+
path.setAttribute('stroke', 'currentColor');
|
|
17862
|
+
svg.append(path);
|
|
17863
|
+
host.replaceChildren(svg);
|
|
17864
|
+
}
|
|
17865
|
+
drawLine([10, 25, 18]);
|
|
17866
|
+
drawLine([10, 25, 18, 32]);
|
|
17867
|
+
|
|
17868
|
+
// After: keep the chart instance and replace its series data.
|
|
17869
|
+
const chart = createLineChart(host, {
|
|
17870
|
+
a11y: { ariaLabel: 'Revenue by month' },
|
|
17871
|
+
series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }], name: 'Revenue' }],
|
|
16434
17872
|
});
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
{ key: 2, value: 40 },
|
|
16441
|
-
{ key: 3, value: 28 },
|
|
16442
|
-
{ key: 4, value: 65 },
|
|
16443
|
-
],
|
|
16444
|
-
name: 'Users',
|
|
16445
|
-
},
|
|
16446
|
-
]);
|
|
17873
|
+
chart.update([{
|
|
17874
|
+
data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],
|
|
17875
|
+
name: 'Revenue',
|
|
17876
|
+
}]);
|
|
17877
|
+
chart.dispose();
|
|
16447
17878
|
```
|
|
16448
17879
|
|
|
16449
|
-
| Feature
|
|
16450
|
-
|
|
|
16451
|
-
| Bundle size
|
|
16452
|
-
| Zero dependencies
|
|
16453
|
-
|
|
|
16454
|
-
|
|
|
16455
|
-
|
|
|
16456
|
-
|
|
|
16457
|
-
| Accessible SVG | | | | Manual |
|
|
16458
|
-
| Keyboard navigation | | | | Manual |
|
|
16459
|
-
| TypeScript-first | | Partial | | Types available |
|
|
17880
|
+
| Feature | Prism | Hand-written SVG |
|
|
17881
|
+
| --- | --- | --- |
|
|
17882
|
+
| Bundle size | | No chart package |
|
|
17883
|
+
| Zero dependencies | (uses Orbit) | |
|
|
17884
|
+
| Data updates | `ChartHandle.update()` replaces chart data | You implement the update path |
|
|
17885
|
+
| Responsive sizing | `ResizeObserver` recalculates the chart layout | You implement the resize behavior |
|
|
17886
|
+
| Chart interactions | Built-in pointer and keyboard interactions on supported charts | You implement them |
|
|
17887
|
+
| SVG accessibility | Informative charts support an accessible name and keyboard focus | You implement the accessible behavior |
|
|
16460
17888
|
|
|
16461
|
-
**Use Prism when** you need
|
|
17889
|
+
**Use Prism when** you need chart factories, responsive SVG output, and an explicit data-update boundary without adopting a charting framework.
|
|
16462
17890
|
|
|
16463
|
-
**Consider
|
|
17891
|
+
**Consider hand-written SVG when** you need a single specialized graphic and prefer to own its scales, resizing, labels, and interactions directly.
|
|
16464
17892
|
|
|
16465
17893
|
## Installation
|
|
16466
17894
|
|
|
@@ -16478,63 +17906,48 @@ yarn add @vielzeug/prism
|
|
|
16478
17906
|
|
|
16479
17907
|
## Quick Start
|
|
16480
17908
|
|
|
17909
|
+
Create and size a host element before creating the chart. Import `theme.css` once to load Prism's default styles and CSS custom properties.
|
|
17910
|
+
|
|
16481
17911
|
```ts
|
|
16482
17912
|
import { createLineChart } from '@vielzeug/prism';
|
|
16483
17913
|
import '@vielzeug/prism/theme.css';
|
|
16484
17914
|
|
|
16485
|
-
const
|
|
17915
|
+
const host = document.createElement('div');
|
|
17916
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
17917
|
+
document.body.append(host);
|
|
17918
|
+
|
|
17919
|
+
const chart = createLineChart(host, {
|
|
16486
17920
|
a11y: { ariaLabel: 'Revenue by month' },
|
|
16487
|
-
series: [
|
|
16488
|
-
{
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
{ key: 1, value: 10 },
|
|
16492
|
-
{ key: 2, value: 25 },
|
|
16493
|
-
{ key: 3, value: 18 },
|
|
16494
|
-
{ key: 4, value: 32 },
|
|
16495
|
-
],
|
|
16496
|
-
name: 'Revenue',
|
|
16497
|
-
},
|
|
16498
|
-
],
|
|
16499
|
-
xAxis: { position: 'bottom' },
|
|
16500
|
-
yAxis: { position: 'left', grid: true },
|
|
17921
|
+
series: [{
|
|
17922
|
+
data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }],
|
|
17923
|
+
name: 'Revenue',
|
|
17924
|
+
}],
|
|
16501
17925
|
tooltip: true,
|
|
16502
|
-
crosshair: true,
|
|
16503
|
-
onHover: (event) => console.log(event?.datum),
|
|
16504
17926
|
});
|
|
16505
17927
|
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }, { key: 5, value: 28 }],
|
|
16511
|
-
name: 'Revenue',
|
|
16512
|
-
},
|
|
16513
|
-
]);
|
|
17928
|
+
chart.update([{
|
|
17929
|
+
data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],
|
|
17930
|
+
name: 'Revenue',
|
|
17931
|
+
}]);
|
|
16514
17932
|
|
|
16515
|
-
// Cleanup when done
|
|
16516
17933
|
chart.dispose();
|
|
16517
17934
|
```
|
|
16518
17935
|
|
|
16519
17936
|
## Features
|
|
16520
17937
|
|
|
16521
|
-
- **`createLineChart(
|
|
16522
|
-
- **`createBarChart(
|
|
16523
|
-
- **`
|
|
16524
|
-
- **`createPieChart(
|
|
16525
|
-
- **`createRadarChart(
|
|
16526
|
-
- **`createSparkline(
|
|
16527
|
-
- **`
|
|
16528
|
-
- **`ChartHandle.
|
|
16529
|
-
- **`
|
|
16530
|
-
-
|
|
16531
|
-
- **
|
|
16532
|
-
- **
|
|
16533
|
-
- **
|
|
16534
|
-
- **CSS custom properties**: full theme control via `--prism-*` tokens, with dark mode built in
|
|
16535
|
-
- **Responsive**: auto-resizes via `ResizeObserver`
|
|
16536
|
-
- **`ChartHandle.tap()`**: observe resize and dispose events outside the render path, with zero cost when untapped
|
|
16537
|
-
- **`Symbol.dispose`**: explicit resource management following the TC39 proposal
|
|
17938
|
+
- **`createLineChart()` / `createAreaChart()`**: plot continuous numeric or date-keyed series
|
|
17939
|
+
- **`createBarChart()`**: compare categories with grouped, stacked, or horizontal bars
|
|
17940
|
+
- **`createScatterChart()`**: plot independent numeric `(x, y)` points
|
|
17941
|
+
- **`createPieChart()`**: render pie, donut, or semi-circle slices
|
|
17942
|
+
- **`createRadarChart()`**: compare series across fixed, labelled axes
|
|
17943
|
+
- **`createSparkline()`**: render compact line, area, bar, or stacked values without axes
|
|
17944
|
+
- **`ChartHandle.update(data)`**: replace chart data without handing state ownership to Prism
|
|
17945
|
+
- **`ChartHandle.dispose()` / `Symbol.dispose`**: release the SVG, observer, overlays, and chart-owned animation
|
|
17946
|
+
- **`ChartHandle.tap()`**: observe resize and disposal events
|
|
17947
|
+
- **`setTheme()` / `resetTheme()`**: set supported palette, typography, axis, grid, and tooltip tokens globally or on one element
|
|
17948
|
+
- **CSS custom properties**: style the chart palette, marks, axes, labels, tooltip, legend, and motion
|
|
17949
|
+
- **Resize observation**: recalculate SVG layout when the host element changes size
|
|
17950
|
+
- **Accessible interaction**: labelled interactive charts expose keyboard navigation and polite value announcements; sparklines are pointer-only without value announcements
|
|
16538
17951
|
|
|
16539
17952
|
## Documentation
|
|
16540
17953
|
|
|
@@ -16545,37 +17958,44 @@ chart.dispose();
|
|
|
16545
17958
|
|
|
16546
17959
|
## See Also
|
|
16547
17960
|
|
|
16548
|
-
- [Refine](/refine/):
|
|
16549
|
-
- [Orbit](/orbit/): floating
|
|
17961
|
+
- [Refine](/refine/): web components and theme tokens for building controls and layouts around chart hosts
|
|
17962
|
+
- [Orbit](/orbit/): floating-element positioning used internally by Prism's built-in tooltip
|
|
16550
17963
|
|
|
16551
17964
|
### API Reference
|
|
16552
17965
|
|
|
16553
17966
|
## API Overview
|
|
16554
17967
|
|
|
17968
|
+
All chart factories, scale utilities, theme helpers, and error classes are synchronous. Charts render into an HTML element and return a `ChartHandle`; they do not return a promise.
|
|
17969
|
+
|
|
16555
17970
|
| Symbol | Purpose | Execution mode | Common gotcha |
|
|
16556
17971
|
| --- | --- | --- | --- |
|
|
16557
|
-
| `createLineChart()` | Render
|
|
16558
|
-
| `createAreaChart()` | Render
|
|
16559
|
-
| `
|
|
16560
|
-
| `
|
|
16561
|
-
| `
|
|
16562
|
-
| `
|
|
16563
|
-
| `
|
|
16564
|
-
| `
|
|
16565
|
-
| `
|
|
16566
|
-
| `
|
|
16567
|
-
| `
|
|
16568
|
-
| `
|
|
17972
|
+
| `createLineChart()` | Render one or more continuous line series | Sync | `key` values must be numbers or dates |
|
|
17973
|
+
| `createAreaChart()` | Render continuous series with filled areas | Sync | Fills use a gradient by default |
|
|
17974
|
+
| `createScatterChart()` | Plot independent `(x, y)` points | Sync | `key` is the x-value; the y-domain is fitted to data |
|
|
17975
|
+
| `createBarChart()` | Render categorical bars | Sync | Negative values in stacked variants are clamped to zero |
|
|
17976
|
+
| `createPieChart()` | Render pie, donut, or semi-circle slices | Sync | `variant` is fixed at creation; `update()` replaces slices |
|
|
17977
|
+
| `createRadarChart()` | Compare series across fixed axes | Sync | Requires at least three axes to draw |
|
|
17978
|
+
| `createSparkline()` | Render a compact line, area, bar, or stack | Sync | Has no axes, legend, or keyboard navigation |
|
|
17979
|
+
| `linearScale()` | Create a numeric scale | Sync | `nice` defaults to `true`; set `nice: false` to preserve the domain |
|
|
17980
|
+
| `timeScale()` | Create a date scale | Sync | `nice` defaults to `true` |
|
|
17981
|
+
| `bandScale()` | Create an ordered categorical scale | Sync | Unknown categories map to `0` and warn in development |
|
|
17982
|
+
| `setTheme()` | Set Prism CSS custom properties | Sync | Writes inline styles to `document.documentElement` by default |
|
|
17983
|
+
| `resetTheme()` | Remove theme overrides set by `setTheme()` | Sync | Only clears the selected target |
|
|
17984
|
+
| `seriesColor()` | Resolve a palette CSS value | Sync | Palette positions wrap after eight |
|
|
17985
|
+
| `PrismError` | Base class for Prism errors | Sync | Use `instanceof` to catch any Prism error |
|
|
17986
|
+
| `PrismRenderError` | Report an invalid chart container or failed chart operation | Sync | Also thrown by `update()` after disposal |
|
|
16569
17987
|
|
|
16570
17988
|
## Package Entry Point
|
|
16571
17989
|
|
|
16572
17990
|
| Import | Purpose |
|
|
16573
17991
|
| --- | --- |
|
|
16574
|
-
| `@vielzeug/prism` | Chart factories,
|
|
16575
|
-
| `@vielzeug/prism/theme.css` | Default CSS custom properties
|
|
17992
|
+
| `@vielzeug/prism` | Chart factories, scale and theme functions, error classes, and public types |
|
|
17993
|
+
| `@vielzeug/prism/theme.css` | Default chart styles, CSS custom properties, dark-mode values, and reduced-motion rules |
|
|
16576
17994
|
|
|
16577
17995
|
## Chart Factories
|
|
16578
17996
|
|
|
17997
|
+
Each factory appends an SVG to `container`. A chart with no measurable container size uses a temporary `600 × 300` SVG size and emits a development warning; `ResizeObserver` applies later size changes. Unless `a11y` is informative, the SVG is hidden from the accessibility tree.
|
|
17998
|
+
|
|
16579
17999
|
### `createLineChart()`
|
|
16580
18000
|
|
|
16581
18001
|
```ts
|
|
@@ -16585,28 +18005,32 @@ function createLineChart(
|
|
|
16585
18005
|
): ChartHandle;
|
|
16586
18006
|
```
|
|
16587
18007
|
|
|
16588
|
-
Renders
|
|
18008
|
+
Renders line series and returns a handle whose `update()` argument replaces the complete series array. Axes render by default; the value axis has grid lines by default. Pass `false` to `xAxis` or `yAxis` to suppress that axis.
|
|
18009
|
+
|
|
18010
|
+
Series use linear interpolation by default, omit point markers by default, and use a point radius of 3 when markers are enabled. `yAxis` defaults to `'left'`; crosshair, tooltip, and legend are disabled unless configured.
|
|
16589
18011
|
|
|
16590
18012
|
| Parameter | Type | Description |
|
|
16591
18013
|
| --- | --- | --- |
|
|
16592
|
-
| `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
|
|
16593
|
-
| `config` | `LineChartConfig` |
|
|
18014
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
|
|
18015
|
+
| `config` | `LineChartConfig` | Series, axes, interaction, accessibility, and motion settings |
|
|
16594
18016
|
|
|
16595
18017
|
**Returns:** `ChartHandle`.
|
|
16596
18018
|
|
|
16597
18019
|
```ts
|
|
16598
18020
|
import { createLineChart } from '@vielzeug/prism';
|
|
16599
18021
|
|
|
16600
|
-
const
|
|
18022
|
+
const host = document.createElement('div');
|
|
18023
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
18024
|
+
document.body.append(host);
|
|
18025
|
+
|
|
18026
|
+
const chart = createLineChart(host, {
|
|
16601
18027
|
a11y: { ariaLabel: 'Revenue by month' },
|
|
16602
|
-
series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],
|
|
18028
|
+
series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'Revenue' }],
|
|
16603
18029
|
});
|
|
16604
|
-
|
|
16605
|
-
chart.
|
|
18030
|
+
chart.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }, { key: 3, value: 18 }], name: 'Revenue' }]);
|
|
18031
|
+
chart.dispose();
|
|
16606
18032
|
```
|
|
16607
18033
|
|
|
16608
|
-
---
|
|
16609
|
-
|
|
16610
18034
|
### `createAreaChart()`
|
|
16611
18035
|
|
|
16612
18036
|
```ts
|
|
@@ -16616,24 +18040,67 @@ function createAreaChart(
|
|
|
16616
18040
|
): ChartHandle;
|
|
16617
18041
|
```
|
|
16618
18042
|
|
|
16619
|
-
Renders
|
|
18043
|
+
Renders continuous area series with a gradient fill by default. `update()` replaces the complete series array.
|
|
18044
|
+
|
|
18045
|
+
Area series use linear interpolation by default and show their top line unless `showLine: false` is set. Axes render by default, with grid lines on the value axis.
|
|
16620
18046
|
|
|
16621
18047
|
| Parameter | Type | Description |
|
|
16622
18048
|
| --- | --- | --- |
|
|
16623
|
-
| `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
|
|
16624
|
-
| `config` | `AreaChartConfig` |
|
|
18049
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
|
|
18050
|
+
| `config` | `AreaChartConfig` | Series, axes, interaction, accessibility, and motion settings |
|
|
16625
18051
|
|
|
16626
18052
|
**Returns:** `ChartHandle`.
|
|
16627
18053
|
|
|
16628
18054
|
```ts
|
|
16629
18055
|
import { createAreaChart } from '@vielzeug/prism';
|
|
16630
18056
|
|
|
16631
|
-
const
|
|
16632
|
-
|
|
18057
|
+
const host = document.createElement('div');
|
|
18058
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
18059
|
+
document.body.append(host);
|
|
18060
|
+
|
|
18061
|
+
const chart = createAreaChart(host, {
|
|
18062
|
+
a11y: { ariaLabel: 'Visitors over time' },
|
|
18063
|
+
series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 18 }], name: 'Visitors' }],
|
|
16633
18064
|
});
|
|
18065
|
+
chart.dispose();
|
|
16634
18066
|
```
|
|
16635
18067
|
|
|
16636
|
-
|
|
18068
|
+
### `createScatterChart()`
|
|
18069
|
+
|
|
18070
|
+
```ts
|
|
18071
|
+
function createScatterChart(
|
|
18072
|
+
container: HTMLElement,
|
|
18073
|
+
config: ScatterChartConfig,
|
|
18074
|
+
): ChartHandle;
|
|
18075
|
+
```
|
|
18076
|
+
|
|
18077
|
+
Plots each datum independently: `key` is the numeric or date x-value and `value` is the y-value. Points are not joined. `update()` replaces the complete series array.
|
|
18078
|
+
|
|
18079
|
+
Axes render by default, with grid lines on the value axis. The value scale is fitted to the observed data rather than forced to include zero. Tooltip, crosshair, and legend are disabled unless configured.
|
|
18080
|
+
|
|
18081
|
+
| Parameter | Type | Description |
|
|
18082
|
+
| --- | --- | --- |
|
|
18083
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
|
|
18084
|
+
| `config` | `ScatterChartConfig` | Independent points, axes, interaction, accessibility, and motion settings |
|
|
18085
|
+
|
|
18086
|
+
**Returns:** `ChartHandle`.
|
|
18087
|
+
|
|
18088
|
+
```ts
|
|
18089
|
+
import { createScatterChart } from '@vielzeug/prism';
|
|
18090
|
+
|
|
18091
|
+
const host = document.createElement('div');
|
|
18092
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
18093
|
+
document.body.append(host);
|
|
18094
|
+
|
|
18095
|
+
const chart = createScatterChart(host, {
|
|
18096
|
+
a11y: { ariaLabel: 'Height against weight' },
|
|
18097
|
+
series: [{ data: [{ key: 160, value: 55 }, { key: 175, value: 70 }], name: 'Cohort A' }],
|
|
18098
|
+
tooltip: true,
|
|
18099
|
+
});
|
|
18100
|
+
chart.dispose();
|
|
18101
|
+
```
|
|
18102
|
+
|
|
18103
|
+
Scatter interaction reports the datum and series of the point nearest the pointer in two dimensions, without a `values` array. Keyboard navigation visits points across series in x order. Tooltip `titleFormat` receives the x-value and `valueFormat` the y-value. On mount, points grow from radius zero at their final positions; updates slide existing points and grow new ones.
|
|
16637
18104
|
|
|
16638
18105
|
### `createBarChart()`
|
|
16639
18106
|
|
|
@@ -16644,25 +18111,32 @@ function createBarChart(
|
|
|
16644
18111
|
): ChartHandle;
|
|
16645
18112
|
```
|
|
16646
18113
|
|
|
16647
|
-
Renders grouped, stacked, grouped-horizontal, or stacked-horizontal
|
|
18114
|
+
Renders grouped, stacked, grouped-horizontal, or stacked-horizontal categories. `update()` replaces the complete series array; the variant is fixed at creation. Axes render by default, with grid lines on the value axis.
|
|
18115
|
+
|
|
18116
|
+
The default variant is `grouped`; tooltip and legend are disabled unless configured.
|
|
16648
18117
|
|
|
16649
18118
|
| Parameter | Type | Description |
|
|
16650
18119
|
| --- | --- | --- |
|
|
16651
|
-
| `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
|
|
16652
|
-
| `config` | `BarChartConfig` |
|
|
18120
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
|
|
18121
|
+
| `config` | `BarChartConfig` | Series, layout variant, axes, interaction, accessibility, and motion settings |
|
|
16653
18122
|
|
|
16654
18123
|
**Returns:** `ChartHandle`.
|
|
16655
18124
|
|
|
16656
18125
|
```ts
|
|
16657
18126
|
import { createBarChart } from '@vielzeug/prism';
|
|
16658
18127
|
|
|
16659
|
-
const
|
|
16660
|
-
|
|
16661
|
-
|
|
18128
|
+
const host = document.createElement('div');
|
|
18129
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
18130
|
+
document.body.append(host);
|
|
18131
|
+
|
|
18132
|
+
const chart = createBarChart(host, {
|
|
18133
|
+
a11y: { ariaLabel: 'Tasks by status' },
|
|
18134
|
+
series: [{ data: [{ key: 'Open', value: 12 }, { key: 'Done', value: 8 }], name: 'Tasks' }],
|
|
16662
18135
|
});
|
|
18136
|
+
chart.dispose();
|
|
16663
18137
|
```
|
|
16664
18138
|
|
|
16665
|
-
|
|
18139
|
+
For stacked variants, negative values are treated as zero and produce a development warning.
|
|
16666
18140
|
|
|
16667
18141
|
### `createPieChart()`
|
|
16668
18142
|
|
|
@@ -16673,26 +18147,30 @@ function createPieChart(
|
|
|
16673
18147
|
): ChartHandle;
|
|
16674
18148
|
```
|
|
16675
18149
|
|
|
16676
|
-
Renders pie, donut, or semi-circle slices.
|
|
18150
|
+
Renders pie, donut, or semi-circle slices. The default variant is `pie`; `update()` replaces the slice array. The default inner radius is zero for pies and 55% of the outer radius for donut and semi variants. Pie slices default to no padding or rounded corners; donut and semi slices default to 8px padding and corner radius.
|
|
16677
18151
|
|
|
16678
18152
|
| Parameter | Type | Description |
|
|
16679
18153
|
| --- | --- | --- |
|
|
16680
|
-
| `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
|
|
16681
|
-
| `config` | `PieChartConfig` |
|
|
18154
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
|
|
18155
|
+
| `config` | `PieChartConfig` | Slices, shape, geometry, legend, tooltip, and interaction settings |
|
|
16682
18156
|
|
|
16683
18157
|
**Returns:** `ChartHandle`.
|
|
16684
18158
|
|
|
16685
18159
|
```ts
|
|
16686
18160
|
import { createPieChart } from '@vielzeug/prism';
|
|
16687
18161
|
|
|
16688
|
-
const
|
|
16689
|
-
|
|
18162
|
+
const host = document.createElement('div');
|
|
18163
|
+
host.style.cssText = 'width:320px;height:240px';
|
|
18164
|
+
document.body.append(host);
|
|
18165
|
+
|
|
18166
|
+
const chart = createPieChart(host, {
|
|
18167
|
+
a11y: { ariaLabel: 'Orders by channel' },
|
|
18168
|
+
data: [{ label: 'Direct', value: 72 }, { label: 'Referral', value: 28 }],
|
|
16690
18169
|
variant: 'donut',
|
|
16691
18170
|
});
|
|
18171
|
+
chart.dispose();
|
|
16692
18172
|
```
|
|
16693
18173
|
|
|
16694
|
-
---
|
|
16695
|
-
|
|
16696
18174
|
### `createRadarChart()`
|
|
16697
18175
|
|
|
16698
18176
|
```ts
|
|
@@ -16702,47 +18180,32 @@ function createRadarChart(
|
|
|
16702
18180
|
): ChartHandle;
|
|
16703
18181
|
```
|
|
16704
18182
|
|
|
16705
|
-
Renders one
|
|
18183
|
+
Renders one shape per series across the configured axes. `update()` replaces series data but not axes or other configuration. Fewer than three axes warn in development and draw no chart. Tooltip and legend are disabled unless configured.
|
|
16706
18184
|
|
|
16707
18185
|
| Parameter | Type | Description |
|
|
16708
18186
|
| --- | --- | --- |
|
|
16709
|
-
| `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
|
|
16710
|
-
| `config` | `RadarChartConfig` |
|
|
16711
|
-
|
|
16712
|
-
| Field | Type | Default | Description |
|
|
16713
|
-
| --- | --- | --- | --- |
|
|
16714
|
-
| `axes` | `RadarAxisConfig[]` | N/A | Ordered axes, drawn clockwise; needs at least 3 |
|
|
16715
|
-
| `series` | `RadarSeriesConfig[]` | N/A | Values keyed by `axis.key` |
|
|
16716
|
-
| `domain` | `[number, number]` | `[0, nice max]` | Shared value range; `axis.min`/`axis.max` override it per axis |
|
|
16717
|
-
| `grid` | `boolean \| RadarGridConfig` | `true` | Rings, bands, and level labels; `false` hides the grid |
|
|
16718
|
-
| `fill` | `'gradient' \| 'none' \| 'solid'` | `'solid'` | Shape fill; `gradient` deepens toward the rim |
|
|
16719
|
-
| `curve` | `'linear' \| 'rounded'` | `'linear'` | Straight edges or a smooth closed curve |
|
|
16720
|
-
| `showPoints` | `boolean` | `true` | Vertex dots; a missing value shows a hollow dot at the centre |
|
|
16721
|
-
| `showValues` | `boolean` | `false` | Print each value beside its vertex |
|
|
16722
|
-
| `startAngle` | `number` | `0` | Degrees clockwise from 12 o'clock for the first axis |
|
|
16723
|
-
| `onHover` | `(event: RadarEvent \| null) => void` | N/A | Nearest axis under the pointer or keyboard focus |
|
|
16724
|
-
| `onClick` | `(event: RadarEvent) => void` | N/A | Axis clicked, or activated with Enter/Space |
|
|
18187
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
|
|
18188
|
+
| `config` | `RadarChartConfig` | Fixed axes, series, scale, grid, appearance, and interaction settings |
|
|
16725
18189
|
|
|
16726
18190
|
**Returns:** `ChartHandle`.
|
|
16727
18191
|
|
|
16728
18192
|
```ts
|
|
16729
18193
|
import { createRadarChart } from '@vielzeug/prism';
|
|
16730
18194
|
|
|
16731
|
-
const
|
|
18195
|
+
const host = document.createElement('div');
|
|
18196
|
+
host.style.cssText = 'width:400px;height:320px';
|
|
18197
|
+
document.body.append(host);
|
|
18198
|
+
|
|
18199
|
+
const chart = createRadarChart(host, {
|
|
16732
18200
|
a11y: { ariaLabel: 'Hero stats' },
|
|
16733
|
-
axes: [
|
|
16734
|
-
{ key: 'str', label: 'Strength' },
|
|
16735
|
-
{ key: 'agi', label: 'Agility' },
|
|
16736
|
-
{ key: 'end', label: 'Endurance' },
|
|
16737
|
-
],
|
|
18201
|
+
axes: [{ key: 'str', label: 'Strength' }, { key: 'agi', label: 'Agility' }, { key: 'end', label: 'Endurance' }],
|
|
16738
18202
|
domain: [0, 10],
|
|
16739
18203
|
series: [{ data: [{ key: 'str', value: 8 }, { key: 'agi', value: 6 }, { key: 'end', value: 9 }], name: 'Adam' }],
|
|
16740
18204
|
});
|
|
18205
|
+
chart.dispose();
|
|
16741
18206
|
```
|
|
16742
18207
|
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
---
|
|
18208
|
+
Radar defaults are a solid fill, linear curves, visible points, a four-level polygon grid with bands, no grid labels, and a first-axis angle of zero degrees. Grid labels render only when all axes share a domain.
|
|
16746
18209
|
|
|
16747
18210
|
### `createSparkline()`
|
|
16748
18211
|
|
|
@@ -16753,22 +18216,29 @@ function createSparkline(
|
|
|
16753
18216
|
): ChartHandle;
|
|
16754
18217
|
```
|
|
16755
18218
|
|
|
16756
|
-
Renders
|
|
18219
|
+
Renders compact data without axes, chart margins, or a legend. `update()` replaces the number or stack-segment array. The default variant is `line`; only line and area variants draw the endpoint marker by default.
|
|
16757
18220
|
|
|
16758
18221
|
| Parameter | Type | Description |
|
|
16759
18222
|
| --- | --- | --- |
|
|
16760
|
-
| `container` | `HTMLElement` | Host that receives the SVG |
|
|
16761
|
-
| `config` | `SparklineConfig` |
|
|
18223
|
+
| `container` | `HTMLElement` | Host that receives the chart SVG |
|
|
18224
|
+
| `config` | `SparklineConfig` | Values, variant, appearance, accessibility, and pointer callbacks |
|
|
16762
18225
|
|
|
16763
18226
|
**Returns:** `ChartHandle`.
|
|
16764
18227
|
|
|
16765
18228
|
```ts
|
|
16766
18229
|
import { createSparkline } from '@vielzeug/prism';
|
|
16767
18230
|
|
|
16768
|
-
const
|
|
18231
|
+
const host = document.createElement('div');
|
|
18232
|
+
host.style.cssText = 'width:240px;height:48px';
|
|
18233
|
+
document.body.append(host);
|
|
18234
|
+
|
|
18235
|
+
const chart = createSparkline(host, { data: [2, 5, 3, 8], variant: 'area' });
|
|
16769
18236
|
chart.update([2, 5, 3, 8, 13]);
|
|
18237
|
+
chart.dispose();
|
|
16770
18238
|
```
|
|
16771
18239
|
|
|
18240
|
+
`StackSegment[]` is for `variant: 'stack'`. Pointer callbacks are available for numeric line, area, and bar sparklines; sparklines do not support keyboard navigation.
|
|
18241
|
+
|
|
16772
18242
|
## Scale Factories
|
|
16773
18243
|
|
|
16774
18244
|
### `linearScale()`
|
|
@@ -16777,14 +18247,16 @@ chart.update([2, 5, 3, 8, 13]);
|
|
|
16777
18247
|
function linearScale(config: LinearScaleConfig): Scale;
|
|
16778
18248
|
```
|
|
16779
18249
|
|
|
16780
|
-
|
|
18250
|
+
Maps a numeric domain to a numeric range. `nice` defaults to `true`; `clamp` defaults to `false` and applies to `map()`, not `invert()`.
|
|
18251
|
+
|
|
18252
|
+
| Parameter | Type | Default | Description |
|
|
16781
18253
|
| --- | --- | --- | --- |
|
|
16782
|
-
| `domain` | `[number, number]` |
|
|
16783
|
-
| `range` | `[number, number]` |
|
|
16784
|
-
| `clamp` | `boolean` | `false` | Clamp mapped
|
|
16785
|
-
| `nice` | `boolean` | `true` | Expand the domain to rounded boundaries |
|
|
18254
|
+
| `config.domain` | `[number, number]` | Required | Input extent |
|
|
18255
|
+
| `config.range` | `[number, number]` | Required | Output extent |
|
|
18256
|
+
| `config.clamp` | `boolean` | `false` | Clamp mapped values to the range |
|
|
18257
|
+
| `config.nice` | `boolean` | `true` | Expand the domain to rounded boundaries |
|
|
16786
18258
|
|
|
16787
|
-
**Returns:** `Scale`.
|
|
18259
|
+
**Returns:** `Scale`, with `map`, `invert`, `ticks`, `domain`, and `range`.
|
|
16788
18260
|
|
|
16789
18261
|
```ts
|
|
16790
18262
|
import { linearScale } from '@vielzeug/prism';
|
|
@@ -16793,21 +18265,21 @@ const scale = linearScale({ domain: [0, 100], range: [0, 500] });
|
|
|
16793
18265
|
scale.map(50); // 250
|
|
16794
18266
|
```
|
|
16795
18267
|
|
|
16796
|
-
---
|
|
16797
|
-
|
|
16798
18268
|
### `timeScale()`
|
|
16799
18269
|
|
|
16800
18270
|
```ts
|
|
16801
18271
|
function timeScale(config: TimeScaleConfig): Scale;
|
|
16802
18272
|
```
|
|
16803
18273
|
|
|
16804
|
-
|
|
18274
|
+
Maps date values to a numeric range. `nice` defaults to `true` and rounds the domain to a time interval.
|
|
18275
|
+
|
|
18276
|
+
| Parameter | Type | Default | Description |
|
|
16805
18277
|
| --- | --- | --- | --- |
|
|
16806
|
-
| `domain` | `[Date, Date]` |
|
|
16807
|
-
| `range` | `[number, number]` |
|
|
16808
|
-
| `nice` | `boolean` | `true` | Expand the domain to
|
|
18278
|
+
| `config.domain` | `[Date, Date]` | Required | Input date extent |
|
|
18279
|
+
| `config.range` | `[number, number]` | Required | Output extent |
|
|
18280
|
+
| `config.nice` | `boolean` | `true` | Expand the domain to time-interval boundaries |
|
|
16809
18281
|
|
|
16810
|
-
**Returns:** `Scale`.
|
|
18282
|
+
**Returns:** `Scale`, with `map`, `invert`, `ticks`, `domain`, and `range`.
|
|
16811
18283
|
|
|
16812
18284
|
```ts
|
|
16813
18285
|
import { timeScale } from '@vielzeug/prism';
|
|
@@ -16816,45 +18288,59 @@ const scale = timeScale({
|
|
|
16816
18288
|
domain: [new Date('2026-01-01'), new Date('2026-12-31')],
|
|
16817
18289
|
range: [0, 500],
|
|
16818
18290
|
});
|
|
18291
|
+
scale.map(new Date('2026-07-02'));
|
|
16819
18292
|
```
|
|
16820
18293
|
|
|
16821
|
-
---
|
|
16822
|
-
|
|
16823
18294
|
### `bandScale()`
|
|
16824
18295
|
|
|
16825
18296
|
```ts
|
|
16826
18297
|
function bandScale(config: BandScaleConfig): BandScale;
|
|
16827
18298
|
```
|
|
16828
18299
|
|
|
16829
|
-
|
|
18300
|
+
Maps ordered string categories to the start of each band. `padding` defaults to `0.1`; `paddingOuter` defaults to the same value. Mapping an unknown category returns `0` and warns in development.
|
|
18301
|
+
|
|
18302
|
+
| Parameter | Type | Default | Description |
|
|
16830
18303
|
| --- | --- | --- | --- |
|
|
16831
|
-
| `domain` | `string[]` |
|
|
16832
|
-
| `range` | `[number, number]` |
|
|
16833
|
-
| `padding` | `number` | `0.1` | Inner gap ratio |
|
|
16834
|
-
| `paddingOuter` | `number` | `padding` | Outer gap ratio |
|
|
18304
|
+
| `config.domain` | `string[]` | Required | Ordered categories |
|
|
18305
|
+
| `config.range` | `[number, number]` | Required | Output extent |
|
|
18306
|
+
| `config.padding` | `number` | `0.1` | Inner gap as a ratio of bandwidth |
|
|
18307
|
+
| `config.paddingOuter` | `number` | `padding` | Outer gap as a ratio of bandwidth |
|
|
16835
18308
|
|
|
16836
|
-
**Returns:** `BandScale`.
|
|
18309
|
+
**Returns:** `BandScale`, with `map`, `ticks`, `bandwidth`, `gap`, `domain`, and `range`.
|
|
16837
18310
|
|
|
16838
18311
|
```ts
|
|
16839
18312
|
import { bandScale } from '@vielzeug/prism';
|
|
16840
18313
|
|
|
16841
18314
|
const scale = bandScale({ domain: ['A', 'B'], range: [0, 200] });
|
|
18315
|
+
scale.bandwidth();
|
|
16842
18316
|
```
|
|
16843
18317
|
|
|
16844
18318
|
## Theme Utilities
|
|
16845
18319
|
|
|
18320
|
+
`theme.css` supplies the base CSS values and styles. `setTheme()` writes inline custom properties, which take precedence over stylesheet declarations. For other visual controls, set CSS custom properties directly.
|
|
18321
|
+
|
|
16846
18322
|
### `setTheme()`
|
|
16847
18323
|
|
|
16848
18324
|
```ts
|
|
16849
18325
|
function setTheme(theme: PrismTheme, options?: ThemeScope): void;
|
|
16850
18326
|
```
|
|
16851
18327
|
|
|
16852
|
-
Sets
|
|
18328
|
+
Sets the provided theme values on `options.scope`, or on `document.documentElement` when omitted. Omitted scalar keys remain unchanged. Providing `colors` replaces all eight palette slots, removing unused slots.
|
|
16853
18329
|
|
|
16854
18330
|
| Parameter | Type | Description |
|
|
16855
18331
|
| --- | --- | --- |
|
|
16856
|
-
| `theme` | `PrismTheme` |
|
|
16857
|
-
| `options.scope` | `HTMLElement` |
|
|
18332
|
+
| `theme` | `PrismTheme` | Palette and supported axis, grid, text, font, and tooltip properties |
|
|
18333
|
+
| `options.scope` | `HTMLElement` | Element that scopes the inline custom properties |
|
|
18334
|
+
|
|
18335
|
+
**Returns:** `void`.
|
|
18336
|
+
|
|
18337
|
+
```ts
|
|
18338
|
+
import { setTheme } from '@vielzeug/prism';
|
|
18339
|
+
|
|
18340
|
+
const host = document.createElement('div');
|
|
18341
|
+
document.body.append(host);
|
|
18342
|
+
setTheme({ colors: ['#2563eb', '#16a34a'], textColor: '#111827' }, { scope: host });
|
|
18343
|
+
```
|
|
16858
18344
|
|
|
16859
18345
|
### `resetTheme()`
|
|
16860
18346
|
|
|
@@ -16862,7 +18348,21 @@ Sets Prism theme custom properties as inline styles on `options.scope` when give
|
|
|
16862
18348
|
function resetTheme(options?: ThemeScope): void;
|
|
16863
18349
|
```
|
|
16864
18350
|
|
|
16865
|
-
Removes every custom property `setTheme()`
|
|
18351
|
+
Removes every custom property managed by `setTheme()` from the given scope or the document root. It does not remove CSS rules or change any other custom properties.
|
|
18352
|
+
|
|
18353
|
+
| Parameter | Type | Description |
|
|
18354
|
+
| --- | --- | --- |
|
|
18355
|
+
| `options` | `ThemeScope` | Optional target; defaults to `document.documentElement` |
|
|
18356
|
+
|
|
18357
|
+
**Returns:** `void`.
|
|
18358
|
+
|
|
18359
|
+
```ts
|
|
18360
|
+
import { resetTheme, setTheme } from '@vielzeug/prism';
|
|
18361
|
+
|
|
18362
|
+
const scope = document.createElement('div');
|
|
18363
|
+
setTheme({ textColor: '#111827' }, { scope });
|
|
18364
|
+
resetTheme({ scope });
|
|
18365
|
+
```
|
|
16866
18366
|
|
|
16867
18367
|
### `seriesColor()`
|
|
16868
18368
|
|
|
@@ -16870,1383 +18370,993 @@ Removes every custom property `setTheme()` can set from `options.scope` (default
|
|
|
16870
18370
|
function seriesColor(index: number, override?: string): string;
|
|
16871
18371
|
```
|
|
16872
18372
|
|
|
16873
|
-
Returns `override` when provided; otherwise returns `var(--prism-color-N)`
|
|
18373
|
+
Returns `override` when it is provided; otherwise returns a `var(--prism-color-N)` reference. The palette has eight positions and the index wraps around after the eighth.
|
|
16874
18374
|
|
|
16875
|
-
|
|
18375
|
+
| Parameter | Type | Description |
|
|
18376
|
+
| --- | --- | --- |
|
|
18377
|
+
| `index` | `number` | Zero-based palette position |
|
|
18378
|
+
| `override` | `string` | Optional color returned instead of a CSS variable |
|
|
16876
18379
|
|
|
16877
|
-
|
|
18380
|
+
**Returns:** `string`.
|
|
16878
18381
|
|
|
16879
18382
|
```ts
|
|
16880
|
-
|
|
16881
|
-
readonly disposalSignal: AbortSignal;
|
|
16882
|
-
readonly disposed: boolean;
|
|
16883
|
-
readonly el: SVGSVGElement;
|
|
16884
|
-
tap(handler: (event: PrismEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
|
|
16885
|
-
update(data: TData): void;
|
|
16886
|
-
dispose(): void;
|
|
16887
|
-
[Symbol.dispose](): void;
|
|
16888
|
-
}
|
|
18383
|
+
import { seriesColor } from '@vielzeug/prism';
|
|
16889
18384
|
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
18385
|
+
seriesColor(0); // 'var(--prism-color-1)'
|
|
18386
|
+
seriesColor(1, '#2563eb'); // '#2563eb'
|
|
18387
|
+
```
|
|
16893
18388
|
|
|
16894
|
-
|
|
16895
|
-
top: number;
|
|
16896
|
-
right: number;
|
|
16897
|
-
bottom: number;
|
|
16898
|
-
left: number;
|
|
16899
|
-
}
|
|
18389
|
+
## Chart Handle
|
|
16900
18390
|
|
|
16901
|
-
|
|
16902
|
-
width: number;
|
|
16903
|
-
height: number;
|
|
16904
|
-
margin: ChartMargin;
|
|
16905
|
-
}
|
|
18391
|
+
`ChartHandle` is returned by every chart factory. `update()` synchronously rebuilds the chart from its new data; enabled motion may animate the resulting SVG. `dispose()` is idempotent, removes the chart SVG, observers, tooltip and legend, cancels chart-owned animation, aborts `disposalSignal`, and emits one `dispose` tap event. `update()` after disposal throws `PrismRenderError`.
|
|
16906
18392
|
|
|
16907
|
-
|
|
16908
|
-
| { readonly decorative: true }
|
|
16909
|
-
| { readonly ariaLabel: string; readonly decorative?: false };
|
|
16910
|
-
```
|
|
18393
|
+
`tap()` observes `resize` and `dispose` events outside the render path. It returns an unsubscribe function; an optional abort signal removes the subscription. Observer-handler exceptions are swallowed so they cannot affect chart behavior. A tap added after disposal returns a no-op unsubscribe function.
|
|
16911
18394
|
|
|
16912
|
-
`
|
|
18395
|
+
`[Symbol.dispose]()` delegates to `dispose()`, allowing explicit resource management where supported.
|
|
16913
18396
|
|
|
16914
|
-
|
|
18397
|
+
All chart handles expose the same lifecycle members; interactive cartesian and pie/radar charts use a `.prism-live` status region when no tooltip is enabled. A tooltip is itself a polite status region. Sparklines have neither a tooltip nor a live value announcer.
|
|
16915
18398
|
|
|
16916
|
-
|
|
18399
|
+
| Method or property | Contract |
|
|
18400
|
+
| --- | --- |
|
|
18401
|
+
| `el` | The chart's root `SVGSVGElement` |
|
|
18402
|
+
| `update(data)` | Replace chart data and render; throws `PrismRenderError` after disposal |
|
|
18403
|
+
| `dispose()` | Idempotently release chart-owned DOM, observation, and motion |
|
|
18404
|
+
| `disposed` | Whether the handle has been disposed |
|
|
18405
|
+
| `disposalSignal` | `AbortSignal` aborted during disposal |
|
|
18406
|
+
| `tap(handler, options?)` | Observe resize/dispose events; returns an unsubscribe function |
|
|
18407
|
+
| `[Symbol.dispose]()` | Calls `dispose()` |
|
|
18408
|
+
|
|
18409
|
+
`PrismEvent` reports a changed SVG content size as `{ type: 'resize', width, height }`; the `dispose` event is emitted once before teardown completes.
|
|
18410
|
+
|
|
18411
|
+
## Types
|
|
18412
|
+
|
|
18413
|
+
The following are all types and interfaces exported from `@vielzeug/prism`. Definitions mirror the package entry-point contracts.
|
|
16917
18414
|
|
|
16918
|
-
###
|
|
18415
|
+
### Animation Types
|
|
16919
18416
|
|
|
16920
18417
|
```ts
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
18418
|
+
export type EasingFn = (t: number) => number;
|
|
18419
|
+
export type EasingName = 'back-out' | 'ease-in' | 'ease-in-out' | 'ease-out' | 'expo-out' | 'linear';
|
|
18420
|
+
|
|
18421
|
+
export interface TransitionConfig {
|
|
18422
|
+
/** Milliseconds per animated lane. Defaults to 300 (420 for a line entrance, 400 for radar). */
|
|
18423
|
+
duration?: number;
|
|
18424
|
+
easing?: EasingName | ((t: number) => number);
|
|
18425
|
+
/** `system` skips animation when reduced motion is preferred. */
|
|
18426
|
+
preference?: 'always' | 'never' | 'system';
|
|
18427
|
+
/** Per-bar delay on bar charts, per-entering-point delay (x order) on scatter charts, or per-series delay on radar charts. Bar and scatter charts cap the total added delay at about 400 ms. */
|
|
18428
|
+
stagger?: number;
|
|
16927
18429
|
}
|
|
16928
18430
|
|
|
16929
|
-
type
|
|
18431
|
+
export type TransitionOption = TransitionConfig | boolean;
|
|
18432
|
+
```
|
|
16930
18433
|
|
|
16931
|
-
|
|
16932
|
-
name: string;
|
|
16933
|
-
data: TDatum[];
|
|
16934
|
-
color?: string;
|
|
16935
|
-
id?: string; // stable data-series-id on the series group; defaults to series-
|
|
16936
|
-
}
|
|
18434
|
+
### Theme Types
|
|
16937
18435
|
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
18436
|
+
```ts
|
|
18437
|
+
export interface PrismTheme {
|
|
18438
|
+
axisColor?: string;
|
|
18439
|
+
colors?: string[];
|
|
18440
|
+
fontFamily?: string;
|
|
18441
|
+
gridColor?: string;
|
|
18442
|
+
gridOpacity?: number;
|
|
18443
|
+
textColor?: string;
|
|
18444
|
+
textColorSecondary?: string;
|
|
18445
|
+
tooltipBg?: string;
|
|
18446
|
+
tooltipBorder?: string;
|
|
18447
|
+
tooltipColor?: string;
|
|
16943
18448
|
}
|
|
16944
18449
|
|
|
16945
|
-
interface
|
|
16946
|
-
|
|
16947
|
-
series: Series;
|
|
18450
|
+
export interface ThemeScope {
|
|
18451
|
+
readonly scope?: HTMLElement;
|
|
16948
18452
|
}
|
|
16949
18453
|
```
|
|
16950
18454
|
|
|
16951
|
-
|
|
18455
|
+
`PrismTheme.colors` writes up to eight palette slots. The scalar fields map as follows:
|
|
16952
18456
|
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
18457
|
+
| Field | CSS custom property |
|
|
18458
|
+
| --- | --- |
|
|
18459
|
+
| `axisColor` | `--prism-axis-color` |
|
|
18460
|
+
| `fontFamily` | `--prism-font-family` |
|
|
18461
|
+
| `gridColor` | `--prism-grid-color` |
|
|
18462
|
+
| `gridOpacity` | `--prism-grid-opacity` |
|
|
18463
|
+
| `textColor` | `--prism-text-color` |
|
|
18464
|
+
| `textColorSecondary` | `--prism-text-color-secondary` |
|
|
18465
|
+
| `tooltipBg` | `--prism-tooltip-bg` |
|
|
18466
|
+
| `tooltipBorder` | `--prism-tooltip-border` |
|
|
18467
|
+
| `tooltipColor` | `--prism-tooltip-color` |
|
|
16956
18468
|
|
|
16957
|
-
|
|
16958
|
-
| --- | --- | --- |
|
|
16959
|
-
| Bar | `stroke-dasharray` on the datum's `rect`, plus a 1px series-color stroke so the dash is visible on the fill. | Element `opacity` on the datum's `rect`. |
|
|
16960
|
-
| Line | The segment from this datum to the next (Chart.js-style segments). The last datum's `dash` has no segment and is ignored. | That same segment, plus this datum's own point marker. |
|
|
16961
|
-
| Area | The top line's segment from this datum to the next (last datum ignored). The fill keeps the series-level treatment. | Same segment on the top line. |
|
|
16962
|
-
| Sparkline, pie, radar | Ignored. | Ignored. |
|
|
18469
|
+
### Core and Data Types
|
|
16963
18470
|
|
|
16964
|
-
|
|
18471
|
+
```ts
|
|
18472
|
+
export interface ChartMargin {
|
|
18473
|
+
bottom: number;
|
|
18474
|
+
left: number;
|
|
18475
|
+
right: number;
|
|
18476
|
+
top: number;
|
|
18477
|
+
}
|
|
16965
18478
|
|
|
16966
|
-
|
|
18479
|
+
export interface ChartDimensions {
|
|
18480
|
+
height: number;
|
|
18481
|
+
margin: ChartMargin;
|
|
18482
|
+
width: number;
|
|
18483
|
+
}
|
|
16967
18484
|
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
### Chart configurations
|
|
16973
|
-
|
|
16974
|
-
```ts
|
|
16975
|
-
interface BaseChartConfig {
|
|
16976
|
-
a11y?: ChartA11y;
|
|
16977
|
-
legend?: boolean | LegendConfig;
|
|
16978
|
-
margin?: Partial;
|
|
16979
|
-
onClick?: (event: ChartEvent) => void;
|
|
16980
|
-
onHover?: (event: ChartEvent | null) => void;
|
|
16981
|
-
tooltip?: boolean | TooltipConfig;
|
|
16982
|
-
transition?: TransitionOption; // motion is on by default; false renders synchronously
|
|
16983
|
-
// Omitted renders the axis with defaults (value axis with gridlines); false suppresses it.
|
|
16984
|
-
xAxis?: XAxisConfig | false;
|
|
16985
|
-
yAxis?: YAxisConfig | false;
|
|
16986
|
-
}
|
|
16987
|
-
|
|
16988
|
-
interface LineChartConfig extends BaseChartConfig {
|
|
16989
|
-
crosshair?: boolean | CrosshairConfig;
|
|
16990
|
-
rightYAxis?: Omit;
|
|
16991
|
-
series: LineSeriesConfig[];
|
|
16992
|
-
}
|
|
16993
|
-
|
|
16994
|
-
interface AreaChartConfig extends BaseChartConfig {
|
|
16995
|
-
crosshair?: boolean | CrosshairConfig;
|
|
16996
|
-
series: AreaSeriesConfig[];
|
|
16997
|
-
}
|
|
16998
|
-
|
|
16999
|
-
interface BarChartConfig extends BaseChartConfig {
|
|
17000
|
-
series: BarSeriesConfig[];
|
|
17001
|
-
variant?: BarVariant;
|
|
17002
|
-
}
|
|
17003
|
-
|
|
17004
|
-
interface PieChartConfig
|
|
17005
|
-
extends Omit {
|
|
17006
|
-
data: PieSliceConfig[];
|
|
17007
|
-
variant?: PieVariant;
|
|
17008
|
-
innerRadius?: number;
|
|
17009
|
-
cornerRadius?: number;
|
|
17010
|
-
padPixels?: number;
|
|
17011
|
-
onClick?: (event: PieEvent) => void;
|
|
17012
|
-
onHover?: (event: PieEvent | null) => void;
|
|
17013
|
-
}
|
|
17014
|
-
|
|
17015
|
-
interface RadarChartConfig
|
|
17016
|
-
extends Omit {
|
|
17017
|
-
axes: RadarAxisConfig[];
|
|
17018
|
-
series: RadarSeriesConfig[];
|
|
17019
|
-
curve?: 'linear' | 'rounded';
|
|
17020
|
-
domain?: [number, number];
|
|
17021
|
-
fill?: 'gradient' | 'none' | 'solid';
|
|
17022
|
-
grid?: boolean | RadarGridConfig;
|
|
17023
|
-
showPoints?: boolean;
|
|
17024
|
-
showValues?: boolean;
|
|
17025
|
-
startAngle?: number;
|
|
17026
|
-
onClick?: (event: RadarEvent) => void;
|
|
17027
|
-
onHover?: (event: RadarEvent | null) => void;
|
|
17028
|
-
}
|
|
17029
|
-
|
|
17030
|
-
interface SparklineConfig {
|
|
17031
|
-
data: number[] | StackSegment[];
|
|
17032
|
-
variant?: SparklineVariant;
|
|
17033
|
-
a11y?: ChartA11y;
|
|
17034
|
-
color?: string;
|
|
17035
|
-
cornerRadius?: number;
|
|
17036
|
-
curve?: 'linear' | 'monotone' | 'step';
|
|
17037
|
-
fillOpacity?: number; // overrides --prism-spark-fill-opacity (area variant)
|
|
17038
|
-
onClick?: (event: SparklineEvent) => void;
|
|
17039
|
-
onHover?: (event: SparklineEvent | null) => void;
|
|
17040
|
-
padPixels?: number;
|
|
17041
|
-
showEndPoint?: boolean; // default true: dot on the latest value (line, area)
|
|
17042
|
-
strokeWidth?: number;
|
|
17043
|
-
transition?: TransitionOption; // motion is on by default; false renders synchronously
|
|
17044
|
-
}
|
|
17045
|
-
```
|
|
17046
|
-
|
|
17047
|
-
---
|
|
17048
|
-
|
|
17049
|
-
### Series and slice configurations
|
|
17050
|
-
|
|
17051
|
-
```ts
|
|
17052
|
-
interface LineSeriesConfig extends Series {
|
|
17053
|
-
yAxis?: 'left' | 'right'; // defaults to 'left'
|
|
17054
|
-
curve?: 'linear' | 'monotone' | 'step';
|
|
17055
|
-
pointRadius?: number;
|
|
17056
|
-
showPoints?: boolean;
|
|
17057
|
-
strokeWidth?: number; // overrides --prism-line-width
|
|
17058
|
-
}
|
|
18485
|
+
export type PrismEvent =
|
|
18486
|
+
| { readonly height: number; readonly type: 'resize'; readonly width: number }
|
|
18487
|
+
| { readonly type: 'dispose' };
|
|
17059
18488
|
|
|
17060
|
-
interface
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
18489
|
+
export interface ChartHandle {
|
|
18490
|
+
readonly disposalSignal: AbortSignal;
|
|
18491
|
+
dispose(): void;
|
|
18492
|
+
readonly disposed: boolean;
|
|
18493
|
+
readonly el: SVGSVGElement;
|
|
18494
|
+
tap(handler: (event: PrismEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
|
|
18495
|
+
update(data: TData): void;
|
|
18496
|
+
[Symbol.dispose](): void;
|
|
17065
18497
|
}
|
|
17066
18498
|
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
}
|
|
18499
|
+
export type ChartA11y =
|
|
18500
|
+
| { readonly decorative: true }
|
|
18501
|
+
| { readonly ariaLabel: string; readonly decorative?: false };
|
|
17070
18502
|
|
|
17071
|
-
interface
|
|
18503
|
+
export interface Datum {
|
|
18504
|
+
dash?: string;
|
|
18505
|
+
key: TKey;
|
|
18506
|
+
meta?: Record;
|
|
18507
|
+
opacity?: number;
|
|
17072
18508
|
value: number;
|
|
17073
|
-
color?: string;
|
|
17074
|
-
label?: string;
|
|
17075
18509
|
}
|
|
17076
18510
|
|
|
17077
|
-
|
|
17078
|
-
index: number;
|
|
17079
|
-
originalEvent: Event;
|
|
17080
|
-
slice: PieSliceConfig;
|
|
17081
|
-
}
|
|
18511
|
+
export type ContinuousDatum = Datum;
|
|
17082
18512
|
|
|
17083
|
-
interface
|
|
17084
|
-
|
|
17085
|
-
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
format?: (value: number) => string;
|
|
17089
|
-
}
|
|
17090
|
-
|
|
17091
|
-
type RadarSeriesConfig = Series>;
|
|
17092
|
-
|
|
17093
|
-
interface RadarGridConfig {
|
|
17094
|
-
bands?: boolean; // default true
|
|
17095
|
-
labels?: boolean; // default false; drawn only when every axis shares one domain
|
|
17096
|
-
levels?: number; // default 4
|
|
17097
|
-
shape?: 'circle' | 'polygon'; // default 'polygon'
|
|
18513
|
+
export interface Series {
|
|
18514
|
+
color?: string;
|
|
18515
|
+
data: TDatum[];
|
|
18516
|
+
id?: string;
|
|
18517
|
+
name: string;
|
|
17098
18518
|
}
|
|
17099
18519
|
|
|
17100
|
-
interface
|
|
17101
|
-
|
|
17102
|
-
index: number;
|
|
17103
|
-
values: RadarAxisValue[];
|
|
18520
|
+
export interface ChartEvent {
|
|
18521
|
+
datum: Datum;
|
|
17104
18522
|
originalEvent: Event;
|
|
18523
|
+
series: Series;
|
|
18524
|
+
values?: SeriesValue[];
|
|
17105
18525
|
}
|
|
17106
18526
|
|
|
17107
|
-
interface
|
|
18527
|
+
export interface SeriesValue {
|
|
17108
18528
|
datum: Datum | undefined;
|
|
17109
|
-
series:
|
|
17110
|
-
}
|
|
17111
|
-
|
|
17112
|
-
interface StackSegment {
|
|
17113
|
-
value: number;
|
|
17114
|
-
color?: string;
|
|
17115
|
-
label?: string;
|
|
17116
|
-
}
|
|
17117
|
-
|
|
17118
|
-
interface SparklineEvent {
|
|
17119
|
-
index: number;
|
|
17120
|
-
originalEvent: Event;
|
|
17121
|
-
value: number;
|
|
18529
|
+
series: Series;
|
|
17122
18530
|
}
|
|
17123
18531
|
```
|
|
17124
18532
|
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
---
|
|
17128
|
-
|
|
17129
|
-
### Axis, interaction, and transition configurations
|
|
17130
|
-
|
|
17131
|
-
```ts
|
|
17132
|
-
type HorizontalAxisPosition = 'bottom' | 'top';
|
|
17133
|
-
type VerticalAxisPosition = 'left' | 'right';
|
|
17134
|
-
type AxisPosition = HorizontalAxisPosition | VerticalAxisPosition;
|
|
17135
|
-
|
|
17136
|
-
interface AxisConfig {
|
|
17137
|
-
grid?: boolean | GridConfig;
|
|
17138
|
-
label?: string;
|
|
17139
|
-
position?: TPosition;
|
|
17140
|
-
tickCount?: number;
|
|
17141
|
-
tickFormat?: (value: Date | number | string) => string;
|
|
17142
|
-
}
|
|
17143
|
-
|
|
17144
|
-
type XAxisConfig = AxisConfig;
|
|
17145
|
-
type YAxisConfig = AxisConfig;
|
|
17146
|
-
|
|
17147
|
-
interface GridConfig {
|
|
17148
|
-
color?: string;
|
|
17149
|
-
dash?: string;
|
|
17150
|
-
}
|
|
17151
|
-
|
|
17152
|
-
interface TooltipConfig {
|
|
17153
|
-
offset?: number;
|
|
17154
|
-
render?: (datum: Datum, series: Series) => Node | string;
|
|
17155
|
-
}
|
|
18533
|
+
`Datum.dash` and `Datum.opacity` affect rendered marks only on the chart types that support them. On bars, `dash` adds a 1px series-color stroke so the dash is visible against the fill. `Series.id` becomes a `data-series-id` attribute on a series group; characters outside `[A-Za-z0-9_-]` are replaced with `-`. When omitted, the id is based on the series index.
|
|
17156
18534
|
|
|
17157
|
-
|
|
17158
|
-
|
|
17159
|
-
|
|
17160
|
-
|
|
17161
|
-
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
position?: LegendPosition;
|
|
17165
|
-
}
|
|
17166
|
-
|
|
17167
|
-
type LegendPosition = 'bottom' | 'left' | 'right' | 'top';
|
|
17168
|
-
|
|
17169
|
-
interface TransitionConfig {
|
|
17170
|
-
duration?: number; // ms per lane; default 300
|
|
17171
|
-
easing?: EasingName | EasingFn;
|
|
17172
|
-
preference?: 'always' | 'never' | 'system'; // 'system' (default) skips reduced-motion
|
|
17173
|
-
stagger?: number; // ms per bar (bar) or series (radar); ignored elsewhere
|
|
17174
|
-
}
|
|
17175
|
-
|
|
17176
|
-
// Motion is on by default. `false` renders every update synchronously,
|
|
17177
|
-
// `true` uses the chart defaults, or pass a config to tune them.
|
|
17178
|
-
type TransitionOption = TransitionConfig | boolean;
|
|
17179
|
-
|
|
17180
|
-
type EasingName = 'ease-in' | 'ease-in-out' | 'ease-out' | 'back-out' | 'expo-out' | 'linear';
|
|
17181
|
-
|
|
17182
|
-
type EasingFn = (t: number) => number;
|
|
17183
|
-
```
|
|
18535
|
+
| Chart | `dash` | `opacity` |
|
|
18536
|
+
| --- | --- | --- |
|
|
18537
|
+
| Bar | Strokes that datum's bar | Applies to that bar |
|
|
18538
|
+
| Line | Applies to the segment from this datum to the next | Applies to that segment and its point marker |
|
|
18539
|
+
| Area | Applies to the top-line segment from this datum to the next | Applies to that segment |
|
|
18540
|
+
| Scatter | Ignored | Applies to that point |
|
|
18541
|
+
| Pie, radar, sparkline | Ignored | Ignored |
|
|
17184
18542
|
|
|
17185
|
-
|
|
18543
|
+
For line and area charts, the last datum has no following segment, so its `dash` value is ignored. The chart factories group adjacent segments with the same presentation values into path runs.
|
|
17186
18544
|
|
|
17187
|
-
|
|
18545
|
+
For line, area, and bar charts, `ChartEvent.values` lists each series at the active key in series order. Its `datum` is `undefined` when that series has no matching key. `originalEvent` is a pointer event or a keyboard event for keyboard activation. Scatter events identify only the nearest independent point and omit `values`.
|
|
17188
18546
|
|
|
17189
|
-
### Scale
|
|
18547
|
+
### Scale Types
|
|
17190
18548
|
|
|
17191
18549
|
```ts
|
|
17192
|
-
interface Scale {
|
|
18550
|
+
export interface Scale {
|
|
17193
18551
|
readonly domain: readonly [T, T];
|
|
17194
|
-
readonly range: readonly [number, number];
|
|
17195
|
-
map(value: T): number;
|
|
17196
18552
|
invert(pixel: number): T;
|
|
18553
|
+
map(value: T): number;
|
|
18554
|
+
readonly range: readonly [number, number];
|
|
17197
18555
|
ticks(count?: number): T[];
|
|
17198
18556
|
}
|
|
17199
18557
|
|
|
17200
|
-
interface BandScale {
|
|
18558
|
+
export interface BandScale {
|
|
18559
|
+
bandwidth(): number;
|
|
17201
18560
|
readonly domain: readonly string[];
|
|
17202
|
-
|
|
18561
|
+
gap(): number;
|
|
17203
18562
|
map(value: string): number;
|
|
18563
|
+
readonly range: readonly [number, number];
|
|
17204
18564
|
ticks(count?: number): string[];
|
|
17205
|
-
bandwidth(): number;
|
|
17206
|
-
gap(): number;
|
|
17207
18565
|
}
|
|
17208
18566
|
|
|
17209
|
-
interface LinearScaleConfig {
|
|
17210
|
-
domain: [number, number];
|
|
17211
|
-
range: [number, number];
|
|
18567
|
+
export interface LinearScaleConfig {
|
|
17212
18568
|
clamp?: boolean;
|
|
18569
|
+
domain: [number, number];
|
|
17213
18570
|
nice?: boolean;
|
|
18571
|
+
range: [number, number];
|
|
17214
18572
|
}
|
|
17215
18573
|
|
|
17216
|
-
interface TimeScaleConfig {
|
|
18574
|
+
export interface TimeScaleConfig {
|
|
17217
18575
|
domain: [Date, Date];
|
|
17218
|
-
range: [number, number];
|
|
17219
18576
|
nice?: boolean;
|
|
18577
|
+
range: [number, number];
|
|
17220
18578
|
}
|
|
17221
18579
|
|
|
17222
|
-
interface BandScaleConfig {
|
|
18580
|
+
export interface BandScaleConfig {
|
|
17223
18581
|
domain: string[];
|
|
17224
|
-
range: [number, number];
|
|
17225
18582
|
padding?: number;
|
|
17226
18583
|
paddingOuter?: number;
|
|
18584
|
+
range: [number, number];
|
|
17227
18585
|
}
|
|
17228
18586
|
```
|
|
17229
18587
|
|
|
17230
|
-
|
|
17231
|
-
|
|
17232
|
-
### Remaining unions and theme type
|
|
18588
|
+
### Axis and Interaction Types
|
|
17233
18589
|
|
|
17234
18590
|
```ts
|
|
17235
|
-
type
|
|
17236
|
-
type
|
|
17237
|
-
type
|
|
18591
|
+
export type HorizontalAxisPosition = 'bottom' | 'top';
|
|
18592
|
+
export type VerticalAxisPosition = 'left' | 'right';
|
|
18593
|
+
export type AxisPosition = HorizontalAxisPosition | VerticalAxisPosition;
|
|
17238
18594
|
|
|
17239
|
-
interface
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
fontFamily?: string;
|
|
17243
|
-
gridColor?: string;
|
|
17244
|
-
gridOpacity?: number;
|
|
17245
|
-
textColor?: string; // --prism-text-color
|
|
17246
|
-
textColorSecondary?: string; // --prism-text-color-secondary
|
|
17247
|
-
tooltipBg?: string; // --prism-tooltip-bg
|
|
17248
|
-
tooltipBorder?: string; // --prism-tooltip-border
|
|
17249
|
-
tooltipColor?: string; // --prism-tooltip-color
|
|
18595
|
+
export interface GridConfig {
|
|
18596
|
+
color?: string;
|
|
18597
|
+
dash?: string;
|
|
17250
18598
|
}
|
|
17251
18599
|
|
|
17252
|
-
interface
|
|
17253
|
-
|
|
18600
|
+
export interface AxisConfig {
|
|
18601
|
+
grid?: GridConfig | boolean;
|
|
18602
|
+
label?: string;
|
|
18603
|
+
position?: TPosition;
|
|
18604
|
+
tickCount?: number;
|
|
18605
|
+
tickFormat?: (value: Date | number | string) => string;
|
|
18606
|
+
tickValues?: ReadonlyArray;
|
|
17254
18607
|
}
|
|
17255
|
-
```
|
|
17256
|
-
|
|
17257
|
-
## Errors
|
|
17258
|
-
|
|
17259
|
-
### `PrismError`
|
|
17260
|
-
|
|
17261
|
-
Base class for every Prism-originated error. It supports standard `ErrorOptions` cause chaining.
|
|
17262
|
-
|
|
17263
|
-
### `PrismRenderError`
|
|
17264
18608
|
|
|
17265
|
-
|
|
18609
|
+
export type XAxisConfig = AxisConfig;
|
|
18610
|
+
export type YAxisConfig = AxisConfig;
|
|
17266
18611
|
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
```ts
|
|
17274
|
-
import { createLineChart } from '@vielzeug/prism';
|
|
17275
|
-
import '@vielzeug/prism/theme.css';
|
|
17276
|
-
|
|
17277
|
-
const container = document.querySelector('#chart')!;
|
|
17278
|
-
const chart = createLineChart(container, {
|
|
17279
|
-
series: [
|
|
17280
|
-
{
|
|
17281
|
-
name: 'Revenue',
|
|
17282
|
-
data: [
|
|
17283
|
-
{ key: 1, value: 10 },
|
|
17284
|
-
{ key: 2, value: 16 },
|
|
17285
|
-
],
|
|
17286
|
-
},
|
|
17287
|
-
],
|
|
17288
|
-
});
|
|
17289
|
-
|
|
17290
|
-
chart.dispose();
|
|
17291
|
-
```
|
|
17292
|
-
|
|
17293
|
-
```html
|
|
17294
|
-
|
|
17295
|
-
```
|
|
17296
|
-
|
|
17297
|
-
Prism observes the container size via `ResizeObserver` and re-renders automatically on resize. If the container has zero dimensions at mount time, a `warn` is emitted in development: ensure the container has layout before calling the chart factory.
|
|
17298
|
-
|
|
17299
|
-
## Updating Data
|
|
17300
|
-
|
|
17301
|
-
Chart factories render synchronously and return a typed handle. Call `update()` with the same data shape used by the factory when application state changes.
|
|
17302
|
-
|
|
17303
|
-
```ts
|
|
17304
|
-
import { createLineChart } from '@vielzeug/prism';
|
|
17305
|
-
|
|
17306
|
-
const chart = createLineChart(container, {
|
|
17307
|
-
series: [{ data: [{ key: 1, value: 10 }], name: 'Live' }],
|
|
17308
|
-
});
|
|
17309
|
-
|
|
17310
|
-
chart.update([
|
|
17311
|
-
{
|
|
17312
|
-
data: [
|
|
17313
|
-
{ key: 1, value: 10 },
|
|
17314
|
-
{ key: 2, value: 20 },
|
|
17315
|
-
],
|
|
17316
|
-
name: 'Live',
|
|
17317
|
-
},
|
|
17318
|
-
]);
|
|
17319
|
-
```
|
|
17320
|
-
|
|
17321
|
-
`update()` is state-library neutral. Call it from a framework effect, store subscription, event handler, or request callback. It throws after the chart is disposed instead of silently retaining detached state.
|
|
17322
|
-
|
|
17323
|
-
## Line Charts
|
|
17324
|
-
|
|
17325
|
-
```ts
|
|
17326
|
-
import { createLineChart } from '@vielzeug/prism';
|
|
17327
|
-
|
|
17328
|
-
const chart = createLineChart(container, {
|
|
17329
|
-
series: [
|
|
17330
|
-
{
|
|
17331
|
-
name: 'Revenue',
|
|
17332
|
-
data: [
|
|
17333
|
-
{ key: 1, value: 100 },
|
|
17334
|
-
{ key: 2, value: 150 },
|
|
17335
|
-
{ key: 3, value: 130 },
|
|
17336
|
-
],
|
|
17337
|
-
color: '#3b82f6',
|
|
17338
|
-
curve: 'monotone', // 'linear' | 'monotone' | 'step'
|
|
17339
|
-
strokeWidth: 2,
|
|
17340
|
-
showPoints: true,
|
|
17341
|
-
pointRadius: 4,
|
|
17342
|
-
},
|
|
17343
|
-
],
|
|
17344
|
-
xAxis: { position: 'bottom' },
|
|
17345
|
-
yAxis: { position: 'left', grid: true },
|
|
17346
|
-
tooltip: true,
|
|
17347
|
-
crosshair: true,
|
|
17348
|
-
});
|
|
17349
|
-
```
|
|
17350
|
-
|
|
17351
|
-
### Multiple Series
|
|
17352
|
-
|
|
17353
|
-
```ts
|
|
17354
|
-
const chart = createLineChart(container, {
|
|
17355
|
-
series: [
|
|
17356
|
-
{ name: 'Revenue', data: revenueData, color: '#3b82f6' },
|
|
17357
|
-
{ name: 'Expenses', data: expenseData, color: '#ef4444' },
|
|
17358
|
-
],
|
|
17359
|
-
xAxis: { position: 'bottom' },
|
|
17360
|
-
yAxis: { position: 'left', grid: true },
|
|
17361
|
-
});
|
|
17362
|
-
```
|
|
17363
|
-
|
|
17364
|
-
### Independent Value Axes
|
|
17365
|
-
|
|
17366
|
-
Assign a line series to `yAxis: 'right'` when its values need a different range. Both axes share the same X scale, while tooltips retain the original values.
|
|
17367
|
-
|
|
17368
|
-
```ts
|
|
17369
|
-
const chart = createLineChart(container, {
|
|
17370
|
-
series: [
|
|
17371
|
-
{ name: 'Health', data: [{ key: 0, value: 30 }, { key: 1, value: 20 }] },
|
|
17372
|
-
{ name: 'Damage', yAxis: 'right', data: [{ key: 0, value: 0 }, { key: 1, value: 300 }] },
|
|
17373
|
-
],
|
|
17374
|
-
yAxis: { label: 'Health' },
|
|
17375
|
-
rightYAxis: { label: 'Damage' },
|
|
17376
|
-
tooltip: true,
|
|
17377
|
-
});
|
|
17378
|
-
```
|
|
17379
|
-
|
|
17380
|
-
The primary axis is on the left when right-axis series are present. Grid configuration belongs to the primary axis. Dual-axis curve crossings do not imply equal numeric values. Preserve each marker's axis assignment when calling `update()`.
|
|
17381
|
-
|
|
17382
|
-
### Time-based X Axis
|
|
17383
|
-
|
|
17384
|
-
When data points use `Date` objects for `key`, Prism automatically applies a time scale. The domain spans exactly the first to the last date, so the series fills the plot width:
|
|
17385
|
-
|
|
17386
|
-
```ts
|
|
17387
|
-
const chart = createLineChart(container, {
|
|
17388
|
-
series: [
|
|
17389
|
-
{
|
|
17390
|
-
name: 'Signups',
|
|
17391
|
-
data: [
|
|
17392
|
-
{ key: new Date('2024-01-01'), value: 50 },
|
|
17393
|
-
{ key: new Date('2024-02-01'), value: 80 },
|
|
17394
|
-
{ key: new Date('2024-03-01'), value: 120 },
|
|
17395
|
-
],
|
|
17396
|
-
},
|
|
17397
|
-
],
|
|
17398
|
-
xAxis: { position: 'bottom', tickFormat: (d) => (d as Date).toLocaleDateString() },
|
|
17399
|
-
yAxis: { position: 'left' },
|
|
17400
|
-
});
|
|
17401
|
-
```
|
|
18612
|
+
export interface TooltipConfig {
|
|
18613
|
+
offset?: number;
|
|
18614
|
+
render?: (datum: Datum, series: Series) => Node | string;
|
|
18615
|
+
titleFormat?: (key: Date | number | string) => string;
|
|
18616
|
+
valueFormat?: (value: number, datum: Datum, series: Series) => string;
|
|
18617
|
+
}
|
|
17402
18618
|
|
|
17403
|
-
|
|
18619
|
+
export interface CrosshairConfig {
|
|
18620
|
+
horizontal?: boolean;
|
|
18621
|
+
snap?: boolean;
|
|
18622
|
+
vertical?: boolean;
|
|
18623
|
+
}
|
|
17404
18624
|
|
|
17405
|
-
|
|
17406
|
-
import { createBarChart } from '@vielzeug/prism';
|
|
18625
|
+
export type LegendPosition = 'bottom' | 'left' | 'right' | 'top';
|
|
17407
18626
|
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
name: 'Sales',
|
|
17412
|
-
data: [
|
|
17413
|
-
{ key: 'Q1', value: 200 },
|
|
17414
|
-
{ key: 'Q2', value: 350 },
|
|
17415
|
-
{ key: 'Q3', value: 280 },
|
|
17416
|
-
{ key: 'Q4', value: 400 },
|
|
17417
|
-
],
|
|
17418
|
-
borderRadius: 4,
|
|
17419
|
-
},
|
|
17420
|
-
],
|
|
17421
|
-
xAxis: { position: 'bottom' },
|
|
17422
|
-
yAxis: { position: 'left', grid: true },
|
|
17423
|
-
tooltip: true,
|
|
17424
|
-
});
|
|
18627
|
+
export interface LegendConfig {
|
|
18628
|
+
position?: LegendPosition;
|
|
18629
|
+
}
|
|
17425
18630
|
```
|
|
17426
18631
|
|
|
17427
|
-
|
|
18632
|
+
The crosshair defaults to a vertical line that snaps to the nearest datum; the horizontal line is hidden by default. With `snap: false`, only the guide follows the pointer—tooltip and callback data still refer to the nearest datum.
|
|
17428
18633
|
|
|
17429
|
-
|
|
18634
|
+
`tickValues` renders exactly the given ticks instead of scale-generated ones, and gridlines follow the same list. Values must match the axis's scale kind (numbers on a linear axis, `Date`s on a time axis, category strings on a band axis); an off-scale value warns in development. Without it, ticks come from the scale as before.
|
|
17430
18635
|
|
|
17431
|
-
|
|
17432
|
-
| ---------------------- | -------------------------- |
|
|
17433
|
-
| `'grouped'` | Vertical grouped (default) |
|
|
17434
|
-
| `'stacked'` | Vertical stacked |
|
|
17435
|
-
| `'grouped-horizontal'` | Horizontal grouped |
|
|
17436
|
-
| `'stacked-horizontal'` | Horizontal stacked |
|
|
18636
|
+
Numeric tick labels at a magnitude of 1e5 or above default to compact notation (`123K`, `1.23M`) so long numbers fit the default value-axis margin; pass `tickFormat: String` for raw labels. Supplying a partial `yAxis` (or the value axis of a horizontal bar chart) merges over the default, so `{ tickFormat }` alone keeps the default grid lines.
|
|
17437
18637
|
|
|
17438
|
-
|
|
17439
|
-
const chart = createBarChart(container, {
|
|
17440
|
-
variant: 'stacked',
|
|
17441
|
-
series: [
|
|
17442
|
-
{ name: 'Mobile', data: mobileData, color: '#3b82f6', borderRadius: 0 },
|
|
17443
|
-
{ name: 'Desktop', data: desktopData, color: '#10b981', borderRadius: 0 },
|
|
17444
|
-
],
|
|
17445
|
-
xAxis: { position: 'bottom' },
|
|
17446
|
-
yAxis: { position: 'left', grid: true },
|
|
17447
|
-
tooltip: true,
|
|
17448
|
-
legend: true,
|
|
17449
|
-
});
|
|
17450
|
-
```
|
|
18638
|
+
`titleFormat` and `valueFormat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—and the same formatting is applied to the tooltip's spoken summary. `render` still replaces the whole tooltip body and takes precedence. Tooltip strings are assigned as text, not parsed as HTML. For a radar chart, `tooltip.render` receives a datum whose `meta` contains the active axis, all series values, and the default announcement text.
|
|
17451
18639
|
|
|
17452
|
-
|
|
18640
|
+
### Chart Configuration Types
|
|
17453
18641
|
|
|
17454
18642
|
```ts
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
```ts
|
|
17468
|
-
const chart = createBarChart(container, {
|
|
17469
|
-
series: [
|
|
17470
|
-
{ name: '2023', data: lastYearData, color: '#94a3b8' },
|
|
17471
|
-
{ name: '2024', data: thisYearData, color: '#3b82f6' },
|
|
17472
|
-
],
|
|
17473
|
-
});
|
|
17474
|
-
```
|
|
17475
|
-
|
|
17476
|
-
## Area Charts
|
|
17477
|
-
|
|
17478
|
-
Area fills use a vertical gradient that fades toward the baseline. Set `fill: 'solid'` for a flat fill:
|
|
18643
|
+
export interface BaseChartConfig {
|
|
18644
|
+
a11y?: ChartA11y;
|
|
18645
|
+
legend?: LegendConfig | boolean;
|
|
18646
|
+
margin?: Partial;
|
|
18647
|
+
onClick?: (event: ChartEvent) => void;
|
|
18648
|
+
onHover?: (event: ChartEvent | null) => void;
|
|
18649
|
+
tooltip?: TooltipConfig | boolean;
|
|
18650
|
+
transition?: TransitionOption;
|
|
18651
|
+
xAxis?: XAxisConfig | false;
|
|
18652
|
+
yAxis?: YAxisConfig | false;
|
|
18653
|
+
}
|
|
17479
18654
|
|
|
17480
|
-
|
|
17481
|
-
|
|
18655
|
+
export interface LineSeriesConfig extends Series {
|
|
18656
|
+
curve?: 'linear' | 'monotone' | 'step';
|
|
18657
|
+
pointRadius?: number;
|
|
18658
|
+
showPoints?: boolean;
|
|
18659
|
+
strokeWidth?: number;
|
|
18660
|
+
yAxis?: 'left' | 'right';
|
|
18661
|
+
}
|
|
17482
18662
|
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
curve: 'monotone',
|
|
17489
|
-
fill: 'gradient', // 'gradient' (default) | 'solid'
|
|
17490
|
-
showLine: true,
|
|
17491
|
-
},
|
|
17492
|
-
],
|
|
17493
|
-
xAxis: { position: 'bottom' },
|
|
17494
|
-
yAxis: { position: 'left', grid: true },
|
|
17495
|
-
crosshair: true,
|
|
17496
|
-
});
|
|
17497
|
-
```
|
|
18663
|
+
export interface LineChartConfig extends BaseChartConfig {
|
|
18664
|
+
crosshair?: CrosshairConfig | boolean;
|
|
18665
|
+
rightYAxis?: Omit;
|
|
18666
|
+
series: LineSeriesConfig[];
|
|
18667
|
+
}
|
|
17498
18668
|
|
|
17499
|
-
|
|
18669
|
+
export interface AreaSeriesConfig extends Series {
|
|
18670
|
+
curve?: 'linear' | 'monotone' | 'step';
|
|
18671
|
+
fill?: 'gradient' | 'solid';
|
|
18672
|
+
fillOpacity?: number;
|
|
18673
|
+
showLine?: boolean;
|
|
18674
|
+
}
|
|
17500
18675
|
|
|
17501
|
-
|
|
18676
|
+
export interface AreaChartConfig extends BaseChartConfig {
|
|
18677
|
+
crosshair?: CrosshairConfig | boolean;
|
|
18678
|
+
series: AreaSeriesConfig[];
|
|
18679
|
+
}
|
|
17502
18680
|
|
|
17503
|
-
|
|
17504
|
-
|
|
17505
|
-
|
|
17506
|
-
tooltips, hover, keyboard, and screen-reader behavior never change.
|
|
18681
|
+
export interface ScatterSeriesConfig extends Series {
|
|
18682
|
+
pointRadius?: number;
|
|
18683
|
+
}
|
|
17507
18684
|
|
|
17508
|
-
|
|
18685
|
+
export interface ScatterChartConfig extends BaseChartConfig {
|
|
18686
|
+
crosshair?: CrosshairConfig | boolean;
|
|
18687
|
+
series: ScatterSeriesConfig[];
|
|
18688
|
+
}
|
|
17509
18689
|
|
|
17510
|
-
|
|
17511
|
-
|
|
18690
|
+
export interface BarSeriesConfig extends Series {
|
|
18691
|
+
borderRadius?: number;
|
|
18692
|
+
}
|
|
17512
18693
|
|
|
17513
|
-
|
|
18694
|
+
export type BarVariant = 'grouped' | 'grouped-horizontal' | 'stacked' | 'stacked-horizontal';
|
|
17514
18695
|
|
|
17515
|
-
|
|
17516
|
-
series: [
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
data: values.map((value, i) => ({
|
|
17520
|
-
key: months[i],
|
|
17521
|
-
value,
|
|
17522
|
-
...(types[i] === 'FORECAST' ? { dash: '5 5', opacity: 0.4 } : {}),
|
|
17523
|
-
})),
|
|
17524
|
-
},
|
|
17525
|
-
],
|
|
17526
|
-
});
|
|
17527
|
-
```
|
|
18696
|
+
export interface BarChartConfig extends BaseChartConfig {
|
|
18697
|
+
series: BarSeriesConfig[];
|
|
18698
|
+
variant?: BarVariant;
|
|
18699
|
+
}
|
|
17528
18700
|
|
|
17529
|
-
|
|
17530
|
-
next, so the forecast run renders dashed while the actuals stay solid; its
|
|
17531
|
-
`opacity` fades that segment and the point marker. The last datum's `dash` has
|
|
17532
|
-
no segment to paint and is ignored. On a bar chart, both fields apply to that
|
|
17533
|
-
datum's own bar — a `dash` adds a 1px series-color stroke so the dash reads on
|
|
17534
|
-
the filled rect. On an area chart they style the top line only; the fill keeps
|
|
17535
|
-
its series-level treatment.
|
|
18701
|
+
export type PieVariant = 'donut' | 'pie' | 'semi';
|
|
17536
18702
|
|
|
17537
|
-
|
|
18703
|
+
export interface PieSliceConfig {
|
|
18704
|
+
color?: string;
|
|
18705
|
+
label?: string;
|
|
18706
|
+
value: number;
|
|
18707
|
+
}
|
|
17538
18708
|
|
|
17539
|
-
|
|
17540
|
-
|
|
18709
|
+
export interface PieEvent {
|
|
18710
|
+
index: number;
|
|
18711
|
+
originalEvent: Event;
|
|
18712
|
+
slice: PieSliceConfig;
|
|
18713
|
+
}
|
|
17541
18714
|
|
|
17542
|
-
|
|
17543
|
-
|
|
17544
|
-
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17551
|
-
},
|
|
17552
|
-
],
|
|
17553
|
-
});
|
|
17554
|
-
```
|
|
18715
|
+
export interface PieChartConfig extends Omit {
|
|
18716
|
+
cornerRadius?: number;
|
|
18717
|
+
data: PieSliceConfig[];
|
|
18718
|
+
innerRadius?: number;
|
|
18719
|
+
onClick?: (event: PieEvent) => void;
|
|
18720
|
+
onHover?: (event: PieEvent | null) => void;
|
|
18721
|
+
padPixels?: number;
|
|
18722
|
+
variant?: PieVariant;
|
|
18723
|
+
}
|
|
17555
18724
|
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
18725
|
+
export interface RadarAxisConfig {
|
|
18726
|
+
format?: (value: number) => string;
|
|
18727
|
+
key: string;
|
|
18728
|
+
label: string;
|
|
18729
|
+
max?: number;
|
|
18730
|
+
min?: number;
|
|
18731
|
+
}
|
|
17560
18732
|
|
|
17561
|
-
|
|
18733
|
+
export type RadarSeriesConfig = Series>;
|
|
17562
18734
|
|
|
17563
|
-
|
|
18735
|
+
export interface RadarGridConfig {
|
|
18736
|
+
bands?: boolean;
|
|
18737
|
+
labels?: boolean;
|
|
18738
|
+
levels?: number;
|
|
18739
|
+
shape?: 'circle' | 'polygon';
|
|
18740
|
+
}
|
|
17564
18741
|
|
|
17565
|
-
|
|
17566
|
-
|
|
18742
|
+
export interface RadarAxisValue {
|
|
18743
|
+
datum: Datum | undefined;
|
|
18744
|
+
series: RadarSeriesConfig;
|
|
18745
|
+
}
|
|
17567
18746
|
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
17574
|
-
],
|
|
17575
|
-
variant: 'donut', // 'pie' | 'donut' | 'semi'
|
|
17576
|
-
tooltip: true,
|
|
17577
|
-
transition: { duration: 400, easing: 'ease-out' },
|
|
17578
|
-
});
|
|
17579
|
-
```
|
|
18747
|
+
export interface RadarEvent {
|
|
18748
|
+
axis: RadarAxisConfig;
|
|
18749
|
+
index: number;
|
|
18750
|
+
originalEvent: Event;
|
|
18751
|
+
values: RadarAxisValue[];
|
|
18752
|
+
}
|
|
17580
18753
|
|
|
17581
|
-
|
|
18754
|
+
export interface RadarChartConfig extends Omit {
|
|
18755
|
+
axes: RadarAxisConfig[];
|
|
18756
|
+
curve?: 'linear' | 'rounded';
|
|
18757
|
+
domain?: [number, number];
|
|
18758
|
+
fill?: 'gradient' | 'none' | 'solid';
|
|
18759
|
+
grid?: RadarGridConfig | boolean;
|
|
18760
|
+
onClick?: (event: RadarEvent) => void;
|
|
18761
|
+
onHover?: (event: RadarEvent | null) => void;
|
|
18762
|
+
series: RadarSeriesConfig[];
|
|
18763
|
+
showPoints?: boolean;
|
|
18764
|
+
showValues?: boolean;
|
|
18765
|
+
startAngle?: number;
|
|
18766
|
+
}
|
|
17582
18767
|
|
|
17583
|
-
|
|
|
17584
|
-
| --------- | ------------------------------------------------------- |
|
|
17585
|
-
| `'pie'` | Full circle, no hole |
|
|
17586
|
-
| `'donut'` | Full circle with inner hole (~55% of outer by default) |
|
|
17587
|
-
| `'semi'` | Top-half semicircle with inner hole: useful for gauges |
|
|
18768
|
+
export type SparklineVariant = 'area' | 'bar' | 'line' | 'stack';
|
|
17588
18769
|
|
|
17589
|
-
|
|
18770
|
+
export interface StackSegment {
|
|
18771
|
+
color?: string;
|
|
18772
|
+
label?: string;
|
|
18773
|
+
value: number;
|
|
18774
|
+
}
|
|
17590
18775
|
|
|
17591
|
-
|
|
18776
|
+
export interface SparklineEvent {
|
|
18777
|
+
index: number;
|
|
18778
|
+
originalEvent: Event;
|
|
18779
|
+
value: number;
|
|
18780
|
+
}
|
|
17592
18781
|
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
|
|
18782
|
+
export interface SparklineConfig {
|
|
18783
|
+
a11y?: ChartA11y;
|
|
18784
|
+
color?: string;
|
|
18785
|
+
cornerRadius?: number;
|
|
18786
|
+
curve?: 'linear' | 'monotone' | 'step';
|
|
18787
|
+
data: number[] | StackSegment[];
|
|
18788
|
+
fillOpacity?: number;
|
|
18789
|
+
onClick?: (event: SparklineEvent) => void;
|
|
18790
|
+
onHover?: (event: SparklineEvent | null) => void;
|
|
18791
|
+
padPixels?: number;
|
|
18792
|
+
showEndPoint?: boolean;
|
|
18793
|
+
strokeWidth?: number;
|
|
18794
|
+
transition?: TransitionOption;
|
|
18795
|
+
variant?: SparklineVariant;
|
|
18796
|
+
}
|
|
17599
18797
|
```
|
|
17600
18798
|
|
|
17601
|
-
|
|
18799
|
+
## Errors
|
|
17602
18800
|
|
|
17603
|
-
|
|
18801
|
+
### `PrismError`
|
|
17604
18802
|
|
|
17605
18803
|
```ts
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
Style labels via CSS:
|
|
17610
|
-
|
|
17611
|
-
```css
|
|
17612
|
-
:root {
|
|
17613
|
-
--prism-pie-label-color: #fff;
|
|
17614
|
-
--prism-pie-label-size: 11px;
|
|
18804
|
+
class PrismError extends Error {
|
|
18805
|
+
constructor(message: string, opts?: ErrorOptions);
|
|
17615
18806
|
}
|
|
17616
18807
|
```
|
|
17617
18808
|
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
```ts
|
|
17621
|
-
const chart = createPieChart(container, {
|
|
17622
|
-
data: [
|
|
17623
|
-
{ label: 'A', value: 40 },
|
|
17624
|
-
{ label: 'B', value: 60 },
|
|
17625
|
-
],
|
|
17626
|
-
variant: 'donut',
|
|
17627
|
-
});
|
|
17628
|
-
|
|
17629
|
-
chart.update([
|
|
17630
|
-
{ label: 'A', value: 55 },
|
|
17631
|
-
{ label: 'B', value: 45 },
|
|
17632
|
-
]);
|
|
17633
|
-
```
|
|
18809
|
+
Base class for Prism-originated errors. Its `name` is set to the concrete error class name, and it supports the standard `ErrorOptions` including `cause`.
|
|
17634
18810
|
|
|
17635
|
-
###
|
|
18811
|
+
### `PrismRenderError`
|
|
17636
18812
|
|
|
17637
18813
|
```ts
|
|
17638
|
-
|
|
17639
|
-
data,
|
|
17640
|
-
onHover: (event) => {
|
|
17641
|
-
// event is PieEvent | null (null on mouseleave)
|
|
17642
|
-
if (event) console.log(event.index, event.slice.label, event.slice.value);
|
|
17643
|
-
},
|
|
17644
|
-
onClick: (event) => {
|
|
17645
|
-
console.log('clicked', event.index, event.slice.label);
|
|
17646
|
-
},
|
|
17647
|
-
});
|
|
18814
|
+
class PrismRenderError extends PrismError {}
|
|
17648
18815
|
```
|
|
17649
18816
|
|
|
17650
|
-
|
|
18817
|
+
Thrown when a chart cannot be rendered because its container is not a DOM element, when initial rendering fails (the original error is available as `cause`), or when `update()` is called after disposal. Empty or malformed chart data may instead produce a development warning or an empty rendering, depending on the chart.
|
|
17651
18818
|
|
|
17652
|
-
|
|
18819
|
+
### Usage Guide
|
|
17653
18820
|
|
|
17654
|
-
|
|
18821
|
+
## Basic Usage
|
|
18822
|
+
|
|
18823
|
+
Create a host element with non-zero dimensions, then pass it to a chart factory. This example creates the host in JavaScript so the snippet can run as written.
|
|
17655
18824
|
|
|
17656
18825
|
```ts
|
|
17657
|
-
import {
|
|
18826
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
18827
|
+
import '@vielzeug/prism/theme.css';
|
|
17658
18828
|
|
|
17659
|
-
const
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
{ key: 'int', label: 'Intellect' },
|
|
17663
|
-
{ key: 'end', label: 'Endurance' },
|
|
17664
|
-
{ key: 'spd', label: 'Speed', max: 5 },
|
|
17665
|
-
];
|
|
18829
|
+
const container = document.createElement('div');
|
|
18830
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
18831
|
+
document.body.append(container);
|
|
17666
18832
|
|
|
17667
|
-
const chart =
|
|
17668
|
-
a11y: { ariaLabel: '
|
|
17669
|
-
axes,
|
|
17670
|
-
domain: [0, 10],
|
|
17671
|
-
fill: 'gradient',
|
|
17672
|
-
showValues: true,
|
|
18833
|
+
const chart = createLineChart(container, {
|
|
18834
|
+
a11y: { ariaLabel: 'Revenue by month' },
|
|
17673
18835
|
series: [
|
|
17674
18836
|
{
|
|
17675
|
-
name: '
|
|
18837
|
+
name: 'Revenue',
|
|
17676
18838
|
data: [
|
|
17677
|
-
{ key:
|
|
17678
|
-
{ key:
|
|
17679
|
-
{ key: 'int', value: 4 },
|
|
17680
|
-
{ key: 'end', value: 9 },
|
|
17681
|
-
{ key: 'spd', value: 3 },
|
|
18839
|
+
{ key: 1, value: 10 },
|
|
18840
|
+
{ key: 2, value: 16 },
|
|
17682
18841
|
],
|
|
17683
18842
|
},
|
|
17684
18843
|
],
|
|
18844
|
+
tooltip: true,
|
|
17685
18845
|
});
|
|
18846
|
+
|
|
18847
|
+
chart.dispose();
|
|
17686
18848
|
```
|
|
17687
18849
|
|
|
17688
|
-
|
|
18850
|
+
Each factory appends an SVG to its container and returns a `ChartHandle`. Prism observes the container with `ResizeObserver`; if it has zero dimensions on creation, Prism uses a temporary `600 × 300` size and warns in development. Give the host its layout before creating the chart.
|
|
17689
18851
|
|
|
17690
|
-
|
|
18852
|
+
## Updating Data
|
|
17691
18853
|
|
|
17692
|
-
|
|
18854
|
+
Keep application state outside Prism and pass the next complete data value to `update()`. The update method replaces chart data synchronously; the default transition may animate the resulting SVG.
|
|
17693
18855
|
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
| `fill` | `'solid'`, `'gradient'`, `'none'` | `gradient` for a single hero; `solid` or `none` when overlaying several |
|
|
17697
|
-
| `curve` | `'linear'`, `'rounded'` | Rounded reads softer; linear keeps exact vertices |
|
|
17698
|
-
| `grid.shape` | `'polygon'`, `'circle'` | Polygon matches the axes; circle suits many axes |
|
|
17699
|
-
| `grid.levels` / `grid.bands` | number / boolean | Ring count and alternate shading |
|
|
17700
|
-
| `grid.labels` | boolean | Level values; drawn only when every axis shares one domain |
|
|
17701
|
-
| `showValues` | boolean | Value beside each vertex, formatted by `axis.format` |
|
|
18856
|
+
```ts
|
|
18857
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
17702
18858
|
|
|
17703
|
-
|
|
18859
|
+
const container = document.createElement('div');
|
|
18860
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
18861
|
+
document.body.append(container);
|
|
17704
18862
|
|
|
17705
|
-
|
|
18863
|
+
let series = [{ data: [{ key: 1, value: 10 }], name: 'Live' }];
|
|
18864
|
+
const chart = createLineChart(container, { series });
|
|
17706
18865
|
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
legend: true,
|
|
17711
|
-
series: [adam, eve],
|
|
17712
|
-
tooltip: true, // "Agility: Adam 6, Eve 9"
|
|
17713
|
-
onHover: (event) => event && console.log(event.axis.label, event.values),
|
|
17714
|
-
});
|
|
18866
|
+
series = [{ data: [{ key: 1, value: 10 }, { key: 2, value: 20 }], name: 'Live' }];
|
|
18867
|
+
chart.update(series);
|
|
18868
|
+
chart.dispose();
|
|
17715
18869
|
```
|
|
17716
18870
|
|
|
17717
|
-
|
|
18871
|
+
`update()` accepts the type shown by that factory's handle: a complete series array for line, area, scatter, bar, and radar charts; a slice array for pie charts; and a number or stack-segment array for sparklines. Chart options such as axes, variant, callbacks, and accessibility are set at creation. For example, radar axes and a pie chart's variant do not change through `update()`.
|
|
18872
|
+
|
|
18873
|
+
## Line Charts
|
|
17718
18874
|
|
|
17719
|
-
|
|
18875
|
+
Line and area series use number or `Date` keys. All series in one chart must use the same key kind. Use `curve` to choose interpolation, and enable markers with `showPoints`.
|
|
17720
18876
|
|
|
17721
18877
|
```ts
|
|
17722
|
-
import {
|
|
18878
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
17723
18879
|
|
|
17724
|
-
const
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
18880
|
+
const container = document.createElement('div');
|
|
18881
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
18882
|
+
document.body.append(container);
|
|
18883
|
+
|
|
18884
|
+
const chart = createLineChart(container, {
|
|
18885
|
+
a11y: { ariaLabel: 'Revenue and expenses' },
|
|
18886
|
+
series: [
|
|
18887
|
+
{
|
|
18888
|
+
curve: 'monotone',
|
|
18889
|
+
data: [{ key: 1, value: 100 }, { key: 2, value: 150 }, { key: 3, value: 130 }],
|
|
18890
|
+
name: 'Revenue',
|
|
18891
|
+
showPoints: true,
|
|
18892
|
+
},
|
|
18893
|
+
{
|
|
18894
|
+
color: '#c2410c',
|
|
18895
|
+
data: [{ key: 1, value: 70 }, { key: 2, value: 95 }, { key: 3, value: 110 }],
|
|
18896
|
+
name: 'Expenses',
|
|
18897
|
+
},
|
|
18898
|
+
],
|
|
18899
|
+
tooltip: true,
|
|
18900
|
+
xAxis: { label: 'Month' },
|
|
18901
|
+
yAxis: { grid: true, label: 'Dollars' },
|
|
17730
18902
|
});
|
|
17731
18903
|
|
|
17732
|
-
|
|
18904
|
+
chart.dispose();
|
|
17733
18905
|
```
|
|
17734
18906
|
|
|
17735
|
-
###
|
|
18907
|
+
### Independent Value Axes
|
|
17736
18908
|
|
|
17737
|
-
|
|
17738
|
-
- **`area`**: gradient-filled area + line overlay
|
|
17739
|
-
- **`bar`**: vertical bar for each data point
|
|
17740
|
-
- **`stack`**: horizontal proportional segments; use `StackSegment[]` for `data` with per-segment colors
|
|
18909
|
+
Assign a line series to the right axis when it uses a different value range. Both axes share the x-scale; `rightYAxis` configures the second axis and cannot configure its position or grid.
|
|
17741
18910
|
|
|
17742
|
-
|
|
18911
|
+
```ts
|
|
18912
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
17743
18913
|
|
|
17744
|
-
|
|
18914
|
+
const container = document.createElement('div');
|
|
18915
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
18916
|
+
document.body.append(container);
|
|
17745
18917
|
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
18918
|
+
const chart = createLineChart(container, {
|
|
18919
|
+
a11y: { ariaLabel: 'Health and damage by level' },
|
|
18920
|
+
rightYAxis: { label: 'Damage' },
|
|
18921
|
+
series: [
|
|
18922
|
+
{ data: [{ key: 1, value: 30 }, { key: 2, value: 20 }], name: 'Health' },
|
|
18923
|
+
{ data: [{ key: 1, value: 100 }, { key: 2, value: 300 }], name: 'Damage', yAxis: 'right' },
|
|
18924
|
+
],
|
|
18925
|
+
yAxis: { grid: true, label: 'Health' },
|
|
17750
18926
|
});
|
|
17751
18927
|
|
|
17752
|
-
|
|
18928
|
+
chart.dispose();
|
|
17753
18929
|
```
|
|
17754
18930
|
|
|
17755
|
-
|
|
18931
|
+
When any series uses the right axis, the primary value axis is placed on the left. Grid lines belong to the primary `yAxis`. The independent numeric scales mean that curve crossings do not imply equal values.
|
|
18932
|
+
|
|
18933
|
+
### Time-Based X Axis
|
|
17756
18934
|
|
|
17757
|
-
|
|
18935
|
+
Use `Date` values for every key when plotting time. Line and area charts use a time scale whose domain fits the data instead of rounding outward to tick boundaries.
|
|
17758
18936
|
|
|
17759
18937
|
```ts
|
|
17760
|
-
|
|
17761
|
-
data: [10, 20, 30],
|
|
17762
|
-
onHover: (event) => {
|
|
17763
|
-
// event is SparklineEvent | null (null on mouseleave)
|
|
17764
|
-
if (event) console.log(`Hovering point ${event.index}: ${event.value}`);
|
|
17765
|
-
},
|
|
17766
|
-
onClick: (event) => {
|
|
17767
|
-
console.log(`Clicked point ${event.index}: ${event.value}`);
|
|
17768
|
-
},
|
|
17769
|
-
});
|
|
17770
|
-
```
|
|
18938
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
17771
18939
|
|
|
17772
|
-
|
|
18940
|
+
const container = document.createElement('div');
|
|
18941
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
18942
|
+
document.body.append(container);
|
|
17773
18943
|
|
|
17774
|
-
|
|
18944
|
+
const formatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });
|
|
18945
|
+
const chart = createLineChart(container, {
|
|
18946
|
+
a11y: { ariaLabel: 'Signups over time' },
|
|
18947
|
+
series: [{
|
|
18948
|
+
data: [
|
|
18949
|
+
{ key: new Date('2026-09-01'), value: 50 },
|
|
18950
|
+
{ key: new Date('2026-09-08'), value: 80 },
|
|
18951
|
+
{ key: new Date('2026-09-15'), value: 120 },
|
|
18952
|
+
],
|
|
18953
|
+
name: 'Signups',
|
|
18954
|
+
}],
|
|
18955
|
+
xAxis: { tickFormat: (value) => value instanceof Date ? formatter.format(value) : String(value) },
|
|
18956
|
+
});
|
|
17775
18957
|
|
|
17776
|
-
|
|
18958
|
+
chart.dispose();
|
|
18959
|
+
```
|
|
17777
18960
|
|
|
17778
|
-
|
|
18961
|
+
`tickFormat` formats axis labels only. Tooltip content and accessibility announcements use the underlying keys and values. To render an exact set of ticks instead of scale-generated ones, pass `tickValues`; gridlines follow the same list. Values must match the axis's scale kind, and an off-scale value warns in development:
|
|
17779
18962
|
|
|
17780
18963
|
```ts
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
label: 'Month',
|
|
17787
|
-
grid: true, // or { color: '#ddd', dash: '4 2' }
|
|
17788
|
-
},
|
|
17789
|
-
yAxis: {
|
|
17790
|
-
position: 'left', // 'left' | 'right'
|
|
17791
|
-
grid: { color: '#f0f0f0' },
|
|
17792
|
-
label: 'Revenue ($)',
|
|
17793
|
-
},
|
|
17794
|
-
}
|
|
18964
|
+
// Force a tick on the last month, which nice-stepped ticks can skip.
|
|
18965
|
+
const chart = createLineChart(container, {
|
|
18966
|
+
series: [{ data: [{ key: 0, value: 90 }, { key: 11, value: 128 }], name: 'Costs' }],
|
|
18967
|
+
xAxis: { tickValues: [0, 6, 11] },
|
|
18968
|
+
});
|
|
17795
18969
|
```
|
|
17796
18970
|
|
|
17797
|
-
|
|
18971
|
+
Numeric tick labels at a magnitude of `1e5` or above default to compact notation (`123K`, `1.23M`) so long numbers fit the default value-axis margin; pass `tickFormat: String` for raw labels. Supplying a partial value-axis config merges over its default, so `yAxis: { tickFormat }` alone keeps the default grid lines.
|
|
17798
18972
|
|
|
17799
|
-
##
|
|
18973
|
+
## Bar Charts
|
|
17800
18974
|
|
|
17801
|
-
|
|
18975
|
+
Bar chart keys are string categories. The default `grouped` variant puts series side by side; the horizontal variants put categories on the y-axis and values on the x-axis.
|
|
17802
18976
|
|
|
17803
18977
|
```ts
|
|
17804
|
-
{
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
}
|
|
18978
|
+
import { createBarChart } from '@vielzeug/prism';
|
|
18979
|
+
|
|
18980
|
+
const container = document.createElement('div');
|
|
18981
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
18982
|
+
document.body.append(container);
|
|
18983
|
+
|
|
18984
|
+
const chart = createBarChart(container, {
|
|
18985
|
+
a11y: { ariaLabel: 'Sales by quarter' },
|
|
18986
|
+
series: [{
|
|
18987
|
+
data: [{ key: 'Q1', value: 200 }, { key: 'Q2', value: 350 }, { key: 'Q3', value: 280 }],
|
|
18988
|
+
name: 'Sales',
|
|
18989
|
+
}],
|
|
18990
|
+
tooltip: true,
|
|
18991
|
+
});
|
|
18992
|
+
|
|
18993
|
+
chart.dispose();
|
|
17814
18994
|
```
|
|
17815
18995
|
|
|
17816
|
-
|
|
18996
|
+
Choose `grouped`, `stacked`, `grouped-horizontal`, or `stacked-horizontal` in `variant`. Negative values in a stacked chart are clamped to zero and produce a development warning. Bars in stacked variants remain square; `borderRadius` applies to grouped bars.
|
|
17817
18997
|
|
|
17818
|
-
|
|
18998
|
+
Axes render by default, with grid lines on the value axis. For horizontal variants, `xAxis` is the value axis and `yAxis` is the category axis. Configure `grid` on the value axis.
|
|
17819
18999
|
|
|
17820
|
-
##
|
|
19000
|
+
## Area Charts
|
|
17821
19001
|
|
|
17822
|
-
|
|
19002
|
+
Area series use a gradient fill by default. Set `fill: 'solid'` for a flat fill, or set `fillOpacity` to override the theme opacity for that series.
|
|
17823
19003
|
|
|
17824
19004
|
```ts
|
|
17825
|
-
{
|
|
19005
|
+
import { createAreaChart } from '@vielzeug/prism';
|
|
19006
|
+
|
|
19007
|
+
const container = document.createElement('div');
|
|
19008
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19009
|
+
document.body.append(container);
|
|
19010
|
+
|
|
19011
|
+
const chart = createAreaChart(container, {
|
|
19012
|
+
a11y: { ariaLabel: 'Visitors over time' },
|
|
17826
19013
|
crosshair: true,
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
}
|
|
19014
|
+
series: [{
|
|
19015
|
+
curve: 'monotone',
|
|
19016
|
+
data: [{ key: 1, value: 12 }, { key: 2, value: 18 }, { key: 3, value: 15 }],
|
|
19017
|
+
fill: 'solid',
|
|
19018
|
+
fillOpacity: 0.2,
|
|
19019
|
+
name: 'Visitors',
|
|
19020
|
+
showLine: true,
|
|
19021
|
+
}],
|
|
19022
|
+
});
|
|
19023
|
+
|
|
19024
|
+
chart.dispose();
|
|
17830
19025
|
```
|
|
17831
19026
|
|
|
17832
|
-
##
|
|
19027
|
+
## Scatter Charts
|
|
17833
19028
|
|
|
17834
|
-
|
|
19029
|
+
Scatter charts treat every datum as an independent point: `key` is the numeric or `Date` x-value and `value` is the y-value. The scales fit the observed values rather than forcing the y-domain to include zero. Set `pointRadius` on a series to change its marker size (default `3`).
|
|
17835
19030
|
|
|
17836
19031
|
```ts
|
|
17837
|
-
{
|
|
17838
|
-
legend: true,
|
|
17839
|
-
// or:
|
|
17840
|
-
legend: { position: 'top' }, // 'top' | 'bottom' | 'left' | 'right'
|
|
17841
|
-
}
|
|
17842
|
-
```
|
|
19032
|
+
import { createScatterChart } from '@vielzeug/prism';
|
|
17843
19033
|
|
|
17844
|
-
|
|
19034
|
+
const container = document.createElement('div');
|
|
19035
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19036
|
+
document.body.append(container);
|
|
17845
19037
|
|
|
17846
|
-
|
|
17847
|
-
:
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
19038
|
+
const chart = createScatterChart(container, {
|
|
19039
|
+
a11y: { ariaLabel: 'Height against weight' },
|
|
19040
|
+
series: [{
|
|
19041
|
+
data: [{ key: 160, value: 55 }, { key: 175, value: 70 }, { key: 190, value: 88 }],
|
|
19042
|
+
name: 'Cohort A',
|
|
19043
|
+
pointRadius: 5,
|
|
19044
|
+
}],
|
|
19045
|
+
tooltip: true,
|
|
19046
|
+
});
|
|
19047
|
+
|
|
19048
|
+
chart.dispose();
|
|
17852
19049
|
```
|
|
17853
19050
|
|
|
17854
|
-
|
|
19051
|
+
Scatter callbacks identify the nearest point in two dimensions and do not provide cross-series `values`, because points do not share a category key. Keyboard navigation visits every point across series in x order. With a tooltip, `titleFormat` formats the x-value and `valueFormat` the y-value, in both the visible row and the spoken summary.
|
|
19052
|
+
|
|
19053
|
+
## Forecast and Emphasis Styling
|
|
17855
19054
|
|
|
17856
|
-
|
|
19055
|
+
Add `dash` or `opacity` to individual `Datum` objects when one part of a series needs distinct styling. These fields do not change interaction, tooltip, or accessibility behavior.
|
|
17857
19056
|
|
|
17858
19057
|
```ts
|
|
19058
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
19059
|
+
|
|
19060
|
+
const container = document.createElement('div');
|
|
19061
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19062
|
+
document.body.append(container);
|
|
19063
|
+
|
|
19064
|
+
const values = [10, 13, 15, 18];
|
|
17859
19065
|
const chart = createLineChart(container, {
|
|
17860
|
-
series: [{
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
19066
|
+
series: [{
|
|
19067
|
+
data: values.map((value, index) => ({
|
|
19068
|
+
key: index + 1,
|
|
19069
|
+
value,
|
|
19070
|
+
...(index >= 2 ? { dash: '5 5', opacity: 0.4 } : {}),
|
|
19071
|
+
})),
|
|
19072
|
+
name: 'Forecast',
|
|
19073
|
+
showPoints: true,
|
|
19074
|
+
}],
|
|
17868
19075
|
});
|
|
17869
|
-
```
|
|
17870
19076
|
|
|
17871
|
-
|
|
19077
|
+
chart.dispose();
|
|
19078
|
+
```
|
|
17872
19079
|
|
|
17873
|
-
|
|
17874
|
-
- `series`: the corresponding `Series` config
|
|
17875
|
-
- `values`: every series at the same key (line, area, and bar charts)
|
|
17876
|
-
- `originalEvent`: the raw `MouseEvent` or, for keyboard navigation, `KeyboardEvent`
|
|
19080
|
+
For line and area charts, each datum's `dash` styles the segment from that datum to the next; the final datum has no outgoing segment. Line opacity also fades the datum's marker. Area styling affects the top line, not the fill. Bar opacity and dashes apply to the individual bar; scatter opacity applies to its point and scatter dashes are ignored. Pie, radar, and sparkline ignore these two fields.
|
|
17877
19081
|
|
|
17878
|
-
|
|
19082
|
+
## Pie, Donut, and Semi Charts
|
|
17879
19083
|
|
|
17880
|
-
|
|
19084
|
+
Use `createPieChart()` for all three shapes. `variant` is fixed when the chart is created; `update()` replaces the slice data.
|
|
17881
19085
|
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
radar polygons reshape, and sparklines tween. Motion runs through one
|
|
17885
|
-
`requestAnimationFrame` loop and is skipped when the user prefers reduced motion
|
|
17886
|
-
(`preference: 'system'`, the default).
|
|
19086
|
+
```ts
|
|
19087
|
+
import { createPieChart } from '@vielzeug/prism';
|
|
17887
19088
|
|
|
17888
|
-
|
|
19089
|
+
const container = document.createElement('div');
|
|
19090
|
+
container.style.cssText = 'width:320px;height:240px';
|
|
19091
|
+
document.body.append(container);
|
|
17889
19092
|
|
|
17890
|
-
|
|
17891
|
-
{
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
|
|
17895
|
-
|
|
17896
|
-
|
|
17897
|
-
|
|
17898
|
-
|
|
19093
|
+
const chart = createPieChart(container, {
|
|
19094
|
+
a11y: { ariaLabel: 'Orders by channel' },
|
|
19095
|
+
data: [
|
|
19096
|
+
{ label: 'Direct', value: 42 },
|
|
19097
|
+
{ label: 'Organic', value: 28 },
|
|
19098
|
+
{ label: 'Referral', value: 18 },
|
|
19099
|
+
{ label: 'Social', value: 12 },
|
|
19100
|
+
],
|
|
19101
|
+
legend: true,
|
|
19102
|
+
tooltip: true,
|
|
19103
|
+
variant: 'donut',
|
|
19104
|
+
});
|
|
17899
19105
|
|
|
17900
|
-
|
|
17901
|
-
{ transition: false } // every update renders synchronously
|
|
17902
|
-
{ transition: true } // the defaults, stated explicitly
|
|
19106
|
+
chart.dispose();
|
|
17903
19107
|
```
|
|
17904
19108
|
|
|
17905
|
-
|
|
17906
|
-
`'expo-out'` (a hard decelerate), and `'back-out'` (a small overshoot); pass a
|
|
17907
|
-
`(t: number) => number` for anything else. `stagger` applies to bar-chart bars
|
|
17908
|
-
and radar-chart series and is ignored elsewhere — bar charts cap the total
|
|
17909
|
-
staggered delay near 400 ms so a long category axis still finishes promptly.
|
|
19109
|
+
The `pie` default has no inner radius. `donut` and `semi` default to an inner radius of 55% of the outer radius. `innerRadius` overrides that size in pixels. Slice labels are omitted when they do not fit; keep a tooltip or legend when small slices need identifying.
|
|
17910
19110
|
|
|
17911
|
-
##
|
|
19111
|
+
## Radar Charts
|
|
17912
19112
|
|
|
17913
|
-
|
|
19113
|
+
Define at least three axes when creating a radar chart. Series data uses string keys that match `RadarAxisConfig.key`; `update()` replaces series but keeps axes and other configuration fixed.
|
|
17914
19114
|
|
|
17915
19115
|
```ts
|
|
17916
|
-
import '@vielzeug/prism
|
|
17917
|
-
```
|
|
17918
|
-
|
|
17919
|
-
### Programmatic Theme with `setTheme`
|
|
17920
|
-
|
|
17921
|
-
Call `setTheme` once at app startup to apply custom tokens programmatically:
|
|
19116
|
+
import { createRadarChart } from '@vielzeug/prism';
|
|
17922
19117
|
|
|
17923
|
-
|
|
17924
|
-
|
|
19118
|
+
const container = document.createElement('div');
|
|
19119
|
+
container.style.cssText = 'width:400px;height:320px';
|
|
19120
|
+
document.body.append(container);
|
|
17925
19121
|
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
|
|
17933
|
-
|
|
19122
|
+
const axes = [
|
|
19123
|
+
{ key: 'strength', label: 'Strength' },
|
|
19124
|
+
{ key: 'agility', label: 'Agility' },
|
|
19125
|
+
{ key: 'endurance', label: 'Endurance' },
|
|
19126
|
+
];
|
|
19127
|
+
const chart = createRadarChart(container, {
|
|
19128
|
+
a11y: { ariaLabel: 'Hero stats' },
|
|
19129
|
+
axes,
|
|
19130
|
+
domain: [0, 10],
|
|
19131
|
+
series: [{
|
|
19132
|
+
data: [{ key: 'strength', value: 8 }, { key: 'agility', value: 6 }, { key: 'endurance', value: 9 }],
|
|
19133
|
+
name: 'Adam',
|
|
19134
|
+
}],
|
|
19135
|
+
tooltip: true,
|
|
17934
19136
|
});
|
|
17935
|
-
```
|
|
17936
|
-
|
|
17937
|
-
`setTheme` writes inline custom properties, so it takes precedence over stylesheet defaults. Call `resetTheme()` to clear every custom property `setTheme` can set and restore the default theme: useful for a theme-switcher's "reset" action or test teardown:
|
|
17938
19137
|
|
|
17939
|
-
|
|
17940
|
-
import { resetTheme } from '@vielzeug/prism';
|
|
17941
|
-
|
|
17942
|
-
resetTheme();
|
|
19138
|
+
chart.dispose();
|
|
17943
19139
|
```
|
|
17944
19140
|
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
Pass `scope` to write the tokens onto one element instead of `document.documentElement`. Charts render inside it, so their `var(--prism-*)` lookups resolve against the scoped values without touching `:root` — the hook for apps whose theme lives in a JS palette object (MUI, Chakra, Backstage):
|
|
19141
|
+
The default shared domain starts at zero and uses a nice maximum; set `domain` to compare series on a fixed range. Per-axis `min` and `max` override the chart domain for that axis. Grid rings, bands, curves, fills, vertex dots, and values are configurable; see `RadarChartConfig` and `RadarGridConfig` in the API reference.
|
|
17948
19142
|
|
|
17949
|
-
|
|
17950
|
-
import { useEffect, useRef } from 'react';
|
|
17951
|
-
import { useTheme } from '@mui/material/styles';
|
|
17952
|
-
import { createBarChart, resetTheme, setTheme, type BarSeriesConfig } from '@vielzeug/prism';
|
|
17953
|
-
import '@vielzeug/prism/theme.css';
|
|
19143
|
+
## Sparklines
|
|
17954
19144
|
|
|
17955
|
-
|
|
17956
|
-
const theme = useTheme();
|
|
17957
|
-
const containerRef = useRef(null);
|
|
19145
|
+
Sparklines omit axes, margins, and legends. Choose `line`, `area`, or `bar` for numeric arrays, and `stack` for an array of `StackSegment` objects.
|
|
17958
19146
|
|
|
17959
|
-
|
|
17960
|
-
|
|
19147
|
+
```ts
|
|
19148
|
+
import { createSparkline } from '@vielzeug/prism';
|
|
17961
19149
|
|
|
17962
|
-
|
|
19150
|
+
const container = document.createElement('div');
|
|
19151
|
+
container.style.cssText = 'width:240px;height:48px';
|
|
19152
|
+
document.body.append(container);
|
|
17963
19153
|
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
19154
|
+
const chart = createSparkline(container, {
|
|
19155
|
+
a11y: { ariaLabel: 'Weekly orders' },
|
|
19156
|
+
data: [10, 14, 12, 18, 22],
|
|
19157
|
+
variant: 'area',
|
|
19158
|
+
});
|
|
17969
19159
|
|
|
17970
|
-
|
|
19160
|
+
chart.update([10, 14, 12, 18, 22, 25]);
|
|
19161
|
+
chart.dispose();
|
|
19162
|
+
```
|
|
17971
19163
|
|
|
17972
|
-
|
|
17973
|
-
chart.dispose();
|
|
17974
|
-
resetTheme({ scope });
|
|
17975
|
-
};
|
|
17976
|
-
}, [series, theme]);
|
|
19164
|
+
For a stacked sparkline, each segment can set `value`, `color`, and `label`. Sparklines do not offer keyboard navigation, a tooltip, or a live value announcer. Add a visible value or summary when the trend conveys important information.
|
|
17977
19165
|
|
|
17978
|
-
|
|
17979
|
-
}
|
|
17980
|
-
```
|
|
19166
|
+
## Interaction
|
|
17981
19167
|
|
|
17982
|
-
|
|
19168
|
+
Set `tooltip: true` to show Prism's default tooltip, or pass a `TooltipConfig` to format its content. `titleFormat` and `valueFormat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—without rebuilding its markup, and the same formatting is applied to the tooltip's spoken summary. A `render` callback receives the active `Datum` and `Series` and replaces the whole body, taking precedence over the formatters. Returned strings are inserted as text; return a DOM `Node` for structured content.
|
|
17983
19169
|
|
|
17984
|
-
|
|
19170
|
+
```ts
|
|
19171
|
+
import { createLineChart } from '@vielzeug/prism';
|
|
17985
19172
|
|
|
17986
|
-
|
|
17987
|
-
:
|
|
17988
|
-
|
|
17989
|
-
--prism-color-2: #22c55e;
|
|
17990
|
-
--prism-axis-color: #71717a;
|
|
17991
|
-
--prism-grid-color: #f4f4f5;
|
|
17992
|
-
--prism-text-color: #18181b;
|
|
17993
|
-
--prism-tooltip-bg: #27272a;
|
|
17994
|
-
--prism-font-family: 'Inter', system-ui, sans-serif;
|
|
17995
|
-
}
|
|
17996
|
-
```
|
|
19173
|
+
const container = document.createElement('div');
|
|
19174
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19175
|
+
document.body.append(container);
|
|
17997
19176
|
|
|
17998
|
-
|
|
19177
|
+
const compact = new Intl.NumberFormat(undefined, { notation: 'compact' });
|
|
19178
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
17999
19179
|
|
|
18000
|
-
|
|
19180
|
+
const chart = createLineChart(container, {
|
|
19181
|
+
series: [{ data: [{ key: 11, value: 128000 }], name: 'EC2' }],
|
|
19182
|
+
tooltip: {
|
|
19183
|
+
titleFormat: (key) => months[Number(key)] ?? String(key),
|
|
19184
|
+
valueFormat: (value) => compact.format(value),
|
|
19185
|
+
},
|
|
19186
|
+
});
|
|
18001
19187
|
|
|
18002
|
-
|
|
18003
|
-
.dark-dashboard {
|
|
18004
|
-
--prism-axis-color: #64748b;
|
|
18005
|
-
--prism-grid-color: #334155;
|
|
18006
|
-
--prism-text-color: #e2e8f0;
|
|
18007
|
-
}
|
|
19188
|
+
chart.dispose();
|
|
18008
19189
|
```
|
|
18009
19190
|
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
| Token | Default | Description |
|
|
18013
|
-
| ------------------------- | -------------------------- | ---------------------- |
|
|
18014
|
-
| `--prism-color-{1-8}` | Colorblind-safe palette | Series color palette |
|
|
18015
|
-
| `--prism-bg` | `transparent` | Chart background |
|
|
18016
|
-
| `--prism-axis-color` | `hsl(215deg 16% 47%)` | Axis lines and ticks |
|
|
18017
|
-
| `--prism-grid-color` | `hsl(220deg 10% 75%)` | Grid lines |
|
|
18018
|
-
| `--prism-text-color` | `hsl(215deg 25% 27%)` | Axis labels and text |
|
|
18019
|
-
| `--prism-font-family` | `system-ui` | Chart font |
|
|
18020
|
-
| `--prism-font-size` | `0.75rem` | Label font size |
|
|
18021
|
-
| `--prism-tooltip-bg` | `hsl(222deg 47% 11%)` | Tooltip background |
|
|
18022
|
-
| `--prism-tooltip-color` | `hsl(210deg 40% 98%)` | Tooltip text |
|
|
18023
|
-
| `--prism-tooltip-radius` | `0.375rem` | Tooltip border radius |
|
|
18024
|
-
| `--prism-crosshair-color` | `hsl(215deg 16% 47%)` | Crosshair line |
|
|
18025
|
-
| `--prism-crosshair-dash` | `4 2` | Crosshair dash pattern |
|
|
18026
|
-
|
|
18027
|
-
The palette follows Wong (2011) so adjacent series stay distinguishable under common color-vision deficiencies. Dark mode applies through `prefers-color-scheme`, an `html.dark` class, or `data-prism-theme="dark"` on the chart container or any ancestor — the last is the hook for apps that switch theme in JS without owning ``.
|
|
18028
|
-
|
|
18029
|
-
Radar charts add their own tokens, each defaulting to a shared one:
|
|
18030
|
-
|
|
18031
|
-
| Token | Default | Description |
|
|
18032
|
-
| --- | --- | --- |
|
|
18033
|
-
| `--prism-radar-grid-color` | `--prism-grid-color` | Rings |
|
|
18034
|
-
| `--prism-radar-grid-opacity` | `0.7` | Ring and spoke opacity |
|
|
18035
|
-
| `--prism-radar-band-fill` | `--prism-grid-color` | Alternate ring shading |
|
|
18036
|
-
| `--prism-radar-band-opacity` | `0.14` | Shading opacity |
|
|
18037
|
-
| `--prism-radar-spoke-color` | `--prism-radar-grid-color` | Spokes |
|
|
18038
|
-
| `--prism-radar-fill-opacity` | `--prism-area-opacity` | Solid shape fill |
|
|
18039
|
-
| `--prism-radar-stroke-width` | `--prism-line-width` | Shape outline |
|
|
18040
|
-
| `--prism-radar-point-radius` | `--prism-point-radius` | Vertex dots |
|
|
18041
|
-
| `--prism-radar-point-radius-active` | `--prism-point-radius-hover` | Dots on the active axis |
|
|
18042
|
-
| `--prism-radar-label-color` / `-size` | Secondary text tokens | Axis labels |
|
|
18043
|
-
| `--prism-radar-value-color` / `-size` | Text tokens | Vertex values |
|
|
18044
|
-
| `--prism-radar-dim-opacity` | `--prism-dim-opacity` | Other shapes while one is hovered |
|
|
18045
|
-
|
|
18046
|
-
Shared interaction and chart tokens:
|
|
18047
|
-
|
|
18048
|
-
| Token | Default | Description |
|
|
18049
|
-
| --- | --- | --- |
|
|
18050
|
-
| `--prism-dim-opacity` | `0.35` | Series or slices outside the active one |
|
|
18051
|
-
| `--prism-active-point-radius` | `--prism-point-radius-hover` | Points marked at the active key |
|
|
18052
|
-
| `--prism-point-ring` | `Canvas` | Ring separating active and end points from the line |
|
|
18053
|
-
| `--prism-focus-color` | `--prism-color-1` | Focus outline of a keyboard-focused chart |
|
|
18054
|
-
| `--prism-bar-radius` | `4px` | Bar corner radius (stacked bars stay square) |
|
|
18055
|
-
| `--prism-bar-band-fill` / `-opacity` | Grid colour / `0.18` | Shaded active category |
|
|
18056
|
-
| `--prism-bar-dim-opacity` | `0.55` | Bars outside the active category |
|
|
18057
|
-
| `--prism-area-gradient-start` / `-end` | `0.45` / `0.02` | Gradient fill opacity at the line and the baseline |
|
|
18058
|
-
| `--prism-spark-gradient-start` / `-end` | `0.35` / `0` | Sparkline gradient fill |
|
|
19191
|
+
`onHover` receives an event while a point, category, slice, or radar axis is active, then `null` when the interaction clears. `onClick` receives the selected event. For line, area, and bar charts, `ChartEvent.values` contains each series' datum at that shared key; a missing datum is `undefined`. Scatter events are `ChartEvent` objects without `values`; pie, radar, and sparkline use their chart-specific event types.
|
|
18059
19192
|
|
|
18060
|
-
|
|
19193
|
+
`crosshair` is available on line, area, and scatter charts. By default it shows a vertical guide and snaps the line to the nearest datum. Set `horizontal: true` to add a horizontal guide or `vertical: false` to hide the vertical one. Set `snap: false` to make the guide follow the pointer; tooltip and callback data still come from the nearest datum.
|
|
18061
19194
|
|
|
18062
|
-
|
|
19195
|
+
### Keyboard and Announcements
|
|
18063
19196
|
|
|
18064
|
-
|
|
19197
|
+
Set `a11y: { ariaLabel }` on an informative chart. Prism gives the SVG `role="img"`, an accessible name, and keyboard focus. Arrow keys move among chart data; `Home` and `End` move to the first and last item where supported; `Enter` or `Space` activates an item when an `onClick` callback is configured; `Escape` clears the active selection. Sparklines do not support keyboard navigation.
|
|
18065
19198
|
|
|
18066
|
-
|
|
18067
|
-
import { linearScale, timeScale, bandScale } from '@vielzeug/prism';
|
|
19199
|
+
When `a11y` is omitted, or set to `{ decorative: true }`, the SVG is marked `aria-hidden="true"` and does not enter the keyboard tab order. Only mark charts decorative when their information is available elsewhere. Interactive cartesian and pie/radar charts use the tooltip as a polite live region when enabled; otherwise they announce active values in `.prism-live`. Sparklines have no live value announcements, so provide an equivalent visible value or summary.
|
|
18068
19200
|
|
|
18069
|
-
|
|
18070
|
-
y.map(50); // → 150
|
|
18071
|
-
y.invert(150); // → 50
|
|
18072
|
-
y.ticks(5); // → [0, 20, 40, 60, 80, 100]
|
|
19201
|
+
## Animation
|
|
18073
19202
|
|
|
18074
|
-
|
|
18075
|
-
x.map('B'); // → pixel left edge of band B
|
|
18076
|
-
x.bandwidth(); // → width of each band
|
|
18077
|
-
```
|
|
19203
|
+
Chart motion is enabled by default, with a 300 ms duration per animated lane (radar uses 400 ms). Pass `transition: false` to render updates synchronously, or pass `true` to state the default explicitly.
|
|
18078
19204
|
|
|
18079
|
-
|
|
19205
|
+
```ts
|
|
19206
|
+
import { createBarChart } from '@vielzeug/prism';
|
|
18080
19207
|
|
|
18081
|
-
|
|
19208
|
+
const container = document.createElement('div');
|
|
19209
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19210
|
+
document.body.append(container);
|
|
18082
19211
|
|
|
18083
|
-
|
|
18084
|
-
|
|
19212
|
+
const chart = createBarChart(container, {
|
|
19213
|
+
series: [{ data: [{ key: 'A', value: 4 }, { key: 'B', value: 7 }], name: 'Count' }],
|
|
19214
|
+
transition: { duration: 500, easing: 'ease-out', preference: 'system' },
|
|
19215
|
+
});
|
|
18085
19216
|
|
|
18086
|
-
// When done:
|
|
18087
19217
|
chart.dispose();
|
|
18088
|
-
|
|
18089
|
-
// Or with TC39 explicit resource management:
|
|
18090
|
-
{
|
|
18091
|
-
using chart = createLineChart(container, config);
|
|
18092
|
-
// auto-disposed at block end
|
|
18093
|
-
}
|
|
18094
19218
|
```
|
|
18095
19219
|
|
|
18096
|
-
|
|
18097
|
-
|
|
18098
|
-
- Cancels in-flight transitions
|
|
18099
|
-
- Disconnects the `ResizeObserver`
|
|
18100
|
-
- Removes the SVG element, tooltip, and legend from the DOM
|
|
18101
|
-
- Restores container styles changed for tooltip positioning
|
|
18102
|
-
- Is idempotent: safe to call multiple times
|
|
19220
|
+
Columns grow together from their baseline by default over 300 ms. Lines trace their final curve over 420 ms, uncovering point markers along the way without fading or shifting the series. Line data updates retain the default 300 ms geometry transition; an explicit `duration` overrides both timings.
|
|
18103
19221
|
|
|
18104
|
-
|
|
19222
|
+
`preference: 'system'` (the default) disables JavaScript motion when the user prefers reduced motion. Use `'always'` to ignore that preference or `'never'` to disable motion. `stagger` sets an optional delay between bars, entering scatter points (in x order), or radar series; bar and scatter charts cap the effective delay so many marks do not add unbounded time. Named easing values are `ease-in`, `ease-in-out`, `ease-out`, `back-out`, `expo-out`, and `linear`; you can also provide an easing function.
|
|
18105
19223
|
|
|
18106
|
-
## Responsive
|
|
19224
|
+
## Responsive Layout and Lifecycle
|
|
18107
19225
|
|
|
18108
|
-
|
|
19226
|
+
Size the host with CSS. Prism observes its content box and updates the SVG dimensions and chart layout when it changes. Bar, line, and scatter resize redraws apply immediately rather than animating between layout sizes; entrance animations and explicit data-update transitions remain enabled. The legend is rendered as container chrome outside the SVG and included in resize calculations.
|
|
18109
19227
|
|
|
18110
|
-
|
|
19228
|
+
Call `dispose()` when the view no longer owns the chart. Disposal is idempotent: it removes the SVG, tooltip, legend, and resize observation, stops in-flight chart motion, and aborts `disposalSignal`. `chart[Symbol.dispose]()` calls the same cleanup method.
|
|
18111
19229
|
|
|
18112
|
-
|
|
19230
|
+
Use `tap()` when external code needs resize or dispose notifications rather than observing the chart DOM itself:
|
|
18113
19231
|
|
|
18114
19232
|
```ts
|
|
18115
19233
|
import { createLineChart } from '@vielzeug/prism';
|
|
18116
19234
|
|
|
19235
|
+
const container = document.createElement('div');
|
|
19236
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19237
|
+
document.body.append(container);
|
|
19238
|
+
|
|
18117
19239
|
const chart = createLineChart(container, {
|
|
18118
19240
|
series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],
|
|
18119
19241
|
});
|
|
18120
|
-
const
|
|
18121
|
-
|
|
18122
|
-
(event)
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
);
|
|
19242
|
+
const stopObserving = chart.tap((event) => {
|
|
19243
|
+
if (event.type === 'resize') console.log(event.width, event.height);
|
|
19244
|
+
if (event.type === 'dispose') console.log('chart disposed');
|
|
19245
|
+
});
|
|
19246
|
+
|
|
19247
|
+
stopObserving();
|
|
19248
|
+
chart.dispose();
|
|
18128
19249
|
```
|
|
18129
19250
|
|
|
18130
|
-
`tap()` returns an unsubscribe function
|
|
19251
|
+
`tap()` returns an unsubscribe function and accepts `{ signal }` for automatic unsubscription. A listener exception is swallowed by the observer so it cannot interrupt chart rendering.
|
|
18131
19252
|
|
|
18132
|
-
##
|
|
19253
|
+
## Theming
|
|
18133
19254
|
|
|
18134
|
-
|
|
19255
|
+
Import the stylesheet once to load Prism's structural styles, default tokens, dark-mode rules, and reduced-motion CSS. You can override any CSS custom property on an ancestor of the chart.
|
|
18135
19256
|
|
|
18136
|
-
```
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
19257
|
+
```css
|
|
19258
|
+
.dashboard-chart {
|
|
19259
|
+
--prism-color-1: #2563eb;
|
|
19260
|
+
--prism-color-2: #16a34a;
|
|
19261
|
+
--prism-grid-color: #94a3b8;
|
|
19262
|
+
--prism-line-width: 3;
|
|
19263
|
+
--prism-text-color: #172033;
|
|
19264
|
+
--prism-tooltip-bg: #111827;
|
|
19265
|
+
}
|
|
18141
19266
|
```
|
|
18142
19267
|
|
|
18143
|
-
|
|
19268
|
+
`setTheme()` provides a typed subset of theme properties. A scoped call writes inline values on the provided element; omit `scope` to write them to `document.documentElement`. Inline values override stylesheet tokens until reset.
|
|
18144
19269
|
|
|
18145
19270
|
```ts
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
19271
|
+
import { resetTheme, setTheme } from '@vielzeug/prism';
|
|
19272
|
+
|
|
19273
|
+
const container = document.createElement('div');
|
|
19274
|
+
container.style.cssText = 'width:640px;height:320px';
|
|
19275
|
+
document.body.append(container);
|
|
19276
|
+
|
|
19277
|
+
setTheme({ colors: ['#2563eb', '#16a34a'], fontFamily: 'Inter, sans-serif' }, { scope: container });
|
|
19278
|
+
// Call resetTheme({ scope: container }) when this scoped theme is no longer needed.
|
|
19279
|
+
resetTheme({ scope: container });
|
|
18150
19280
|
```
|
|
18151
19281
|
|
|
18152
|
-
###
|
|
19282
|
+
### Available Tokens
|
|
18153
19283
|
|
|
18154
|
-
|
|
19284
|
+
The stylesheet defines defaults for the following token groups. Pie-label color and size are read with fallback values by the renderer but are not declared in the base token block. Set a token on the chart container or an ancestor to scope an override.
|
|
18155
19285
|
|
|
18156
|
-
|
|
|
18157
|
-
| --- | --- |
|
|
18158
|
-
|
|
|
18159
|
-
|
|
|
18160
|
-
|
|
|
18161
|
-
|
|
|
19286
|
+
| Group | CSS custom properties |
|
|
19287
|
+
| --- | --- |
|
|
19288
|
+
| Palette and surfaces | `--prism-color-1` through `--prism-color-8`, `--prism-bg`, `--prism-border-radius` |
|
|
19289
|
+
| Axes and grid | `--prism-axis-color`, `--prism-axis-width`, `--prism-grid-color`, `--prism-grid-width`, `--prism-grid-dash`, `--prism-grid-opacity` |
|
|
19290
|
+
| Text | `--prism-font-family`, `--prism-font-size`, `--prism-font-size-label`, `--prism-font-size-title`, `--prism-font-weight`, `--prism-font-weight-title`, `--prism-text-color`, `--prism-text-color-secondary` |
|
|
19291
|
+
| Tooltip | `--prism-tooltip-bg`, `--prism-tooltip-color`, `--prism-tooltip-border`, `--prism-tooltip-radius`, `--prism-tooltip-padding`, `--prism-tooltip-shadow`, `--prism-tooltip-font-size` |
|
|
19292
|
+
| Crosshair | `--prism-crosshair-color`, `--prism-crosshair-width`, `--prism-crosshair-dash`, `--prism-crosshair-opacity` |
|
|
19293
|
+
| Points | `--prism-point-radius`, `--prism-point-radius-hover`, `--prism-point-stroke-width`, `--prism-point-stroke` |
|
|
19294
|
+
| Bars | `--prism-bar-radius`, `--prism-bar-hover-opacity`, `--prism-bar-gap`, `--prism-bar-band-fill`, `--prism-bar-band-opacity`, `--prism-bar-dim-opacity` |
|
|
19295
|
+
| Areas and lines | `--prism-area-opacity`, `--prism-area-stroke-width`, `--prism-area-gradient-start`, `--prism-area-gradient-end`, `--prism-line-width`, `--prism-line-width-hover` |
|
|
19296
|
+
| Interaction | `--prism-dim-opacity`, `--prism-active-point-radius`, `--prism-point-ring`, `--prism-focus-color` |
|
|
19297
|
+
| Sparklines | `--prism-spark-fill-opacity`, `--prism-spark-gradient-start`, `--prism-spark-gradient-end` |
|
|
19298
|
+
| Radar | `--prism-radar-grid-color`, `--prism-radar-grid-opacity`, `--prism-radar-band-fill`, `--prism-radar-band-opacity`, `--prism-radar-spoke-color`, `--prism-radar-fill-opacity`, `--prism-radar-stroke-width`, `--prism-radar-point-radius`, `--prism-radar-point-radius-active`, `--prism-radar-label-color`, `--prism-radar-label-size`, `--prism-radar-value-color`, `--prism-radar-value-size`, `--prism-radar-dim-opacity` |
|
|
19299
|
+
| Motion | `--prism-duration-fast`, `--prism-duration`, `--prism-duration-slow`, `--prism-ease`, `--prism-ease-spring`, `--prism-transition`, `--prism-transition-fast` |
|
|
19300
|
+
| Legend and pie labels | `--prism-legend-gap`, `--prism-legend-dot-size`, `--prism-legend-font-size`, `--prism-pie-label-color`, `--prism-pie-label-size` |
|
|
19301
|
+
|
|
19302
|
+
The default palette and structural colors adapt to `prefers-color-scheme: dark`, `html.dark`, or `[data-prism-theme='dark']` on an ancestor. Scoped inline values set with `setTheme()` take precedence in either mode. The stylesheet also provides `prefers-reduced-motion` overrides for CSS transitions.
|
|
18162
19303
|
|
|
18163
|
-
|
|
19304
|
+
## Errors and Diagnostics
|
|
18164
19305
|
|
|
18165
|
-
|
|
19306
|
+
Prism throws `PrismRenderError` when the container is not a DOM element, when initial rendering fails, or when `update()` is called on a disposed chart. A wrapped initial-render error exposes its cause. Use `PrismError` to catch all Prism-originated errors.
|
|
18166
19307
|
|
|
18167
|
-
|
|
19308
|
+
Empty or malformed data is not always an exception. Depending on the chart, Prism may draw nothing or emit a development-only `console.warn`; examples include an unknown `bandScale` category, negative values in stacked bars, and radar axes with fewer than three entries or duplicate keys. Correct the input rather than relying on a warning for validation.
|
|
18168
19309
|
|
|
18169
19310
|
## Framework Integration
|
|
18170
19311
|
|
|
18171
|
-
Prism has no
|
|
19312
|
+
Prism has no React, Vue, or Svelte adapter. In any framework, give Prism a host element after it is mounted, call `update()` when data changes, and call `dispose()` before the host is removed. This plain DOM pattern shows the ownership boundary without depending on a framework-specific lifecycle API:
|
|
18172
19313
|
|
|
18173
19314
|
```ts
|
|
18174
|
-
import { createLineChart, type
|
|
18175
|
-
|
|
18176
|
-
let chart: ChartHandle | undefined;
|
|
18177
|
-
|
|
18178
|
-
export function mountChart(container: HTMLElement, data: ContinuousDatum[]): void {
|
|
18179
|
-
chart = createLineChart(container, { series: [{ data, name: 'Series' }] });
|
|
18180
|
-
}
|
|
19315
|
+
import { createLineChart, type LineSeriesConfig } from '@vielzeug/prism';
|
|
18181
19316
|
|
|
18182
|
-
|
|
18183
|
-
chart
|
|
18184
|
-
}
|
|
19317
|
+
function mountChart(host: HTMLElement, series: LineSeriesConfig[]) {
|
|
19318
|
+
const chart = createLineChart(host, { a11y: { ariaLabel: 'Revenue' }, series });
|
|
18185
19319
|
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
19320
|
+
return {
|
|
19321
|
+
update(next: LineSeriesConfig[]) {
|
|
19322
|
+
chart.update(next);
|
|
19323
|
+
},
|
|
19324
|
+
dispose() {
|
|
19325
|
+
chart.dispose();
|
|
19326
|
+
},
|
|
19327
|
+
};
|
|
18189
19328
|
}
|
|
18190
|
-
```
|
|
18191
|
-
|
|
18192
|
-
## Working with Other Vielzeug Libraries
|
|
18193
|
-
|
|
18194
|
-
### With Ripple
|
|
18195
19329
|
|
|
18196
|
-
|
|
18197
|
-
|
|
18198
|
-
|
|
18199
|
-
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
const data = signal([{ key: 1, value: 10 }]);
|
|
18203
|
-
const toSeries = () => [{ data: data.value, name: 'Series' }];
|
|
18204
|
-
const chart = createLineChart(container, { series: toSeries() });
|
|
18205
|
-
const unsubscribe = data.subscribe(() => chart.update(toSeries()));
|
|
18206
|
-
|
|
18207
|
-
// Cleanup both owners together.
|
|
18208
|
-
unsubscribe();
|
|
18209
|
-
chart.dispose();
|
|
19330
|
+
const host = document.createElement('div');
|
|
19331
|
+
host.style.cssText = 'width:640px;height:320px';
|
|
19332
|
+
document.body.append(host);
|
|
19333
|
+
const view = mountChart(host, [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }]);
|
|
19334
|
+
view.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'Revenue' }]);
|
|
19335
|
+
view.dispose();
|
|
18210
19336
|
```
|
|
18211
19337
|
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
Bind chart data to a Sourcerer remote source so charts update whenever the list refreshes.
|
|
19338
|
+
The application owns its state and host; Prism owns only the SVG, observer, and optional overlay elements it creates.
|
|
18215
19339
|
|
|
18216
|
-
|
|
18217
|
-
import { createBarChart } from '@vielzeug/prism';
|
|
18218
|
-
import { createPageSource } from '@vielzeug/sourcerer';
|
|
19340
|
+
## Working with Other Vielzeug Libraries
|
|
18219
19341
|
|
|
18220
|
-
|
|
18221
|
-
load: async ({ page, pageSize, signal }) => {
|
|
18222
|
-
const result = await api.stats.list({ page, pageSize }, { signal });
|
|
18223
|
-
return { items: result.data, totalItems: result.total };
|
|
18224
|
-
},
|
|
18225
|
-
});
|
|
18226
|
-
const toSeries = (state) => [
|
|
18227
|
-
{
|
|
18228
|
-
data: state.items.map((item) => ({ key: item.label, value: item.count })),
|
|
18229
|
-
name: 'Series',
|
|
18230
|
-
},
|
|
18231
|
-
];
|
|
18232
|
-
const chart = createBarChart(container, { series: toSeries(source.state) });
|
|
18233
|
-
const unsubscribe = source.subscribe((state) => chart.update(toSeries(state)));
|
|
18234
|
-
void source.reload().catch(() => undefined);
|
|
18235
|
-
```
|
|
19342
|
+
Use Refine components for dashboard controls and keep Prism in a sized host element. Map Refine theme tokens to Prism CSS custom properties or call `setTheme()` with a scoped element; Prism does not require a Refine adapter. Prism uses Orbit internally to place tooltips, so chart users do not need to create Orbit floating elements for the built-in tooltip.
|
|
18236
19343
|
|
|
18237
19344
|
## Best Practices
|
|
18238
19345
|
|
|
18239
|
-
-
|
|
18240
|
-
-
|
|
18241
|
-
-
|
|
18242
|
-
-
|
|
18243
|
-
-
|
|
18244
|
-
-
|
|
19346
|
+
- Give the host a non-zero size before creating a chart.
|
|
19347
|
+
- Set `a11y.ariaLabel` when a chart communicates information.
|
|
19348
|
+
- Use `Date` or number keys consistently across each line or area chart.
|
|
19349
|
+
- Pass the complete new data value to `update()`.
|
|
19350
|
+
- Keep chart options fixed after creation; recreate the chart to change them.
|
|
19351
|
+
- Dispose the handle when its host leaves the owning view.
|
|
19352
|
+
- Use `transition: false` for deterministic synchronous rendering.
|
|
19353
|
+
- Prefer CSS custom properties for reusable theme overrides.
|
|
18245
19354
|
|
|
18246
19355
|
### Examples
|
|
18247
19356
|
|
|
18248
19357
|
- [Create a line chart](./examples/line-chart.md)
|
|
18249
19358
|
- [Plot a time series](./examples/time-series.md)
|
|
19359
|
+
- [Plot a scatter chart](./examples/scatter-chart.md)
|
|
18250
19360
|
- [Compare categories with a bar chart](./examples/bar-chart.md)
|
|
18251
19361
|
- [Render compact charts](./examples/compact-charts.md)
|
|
18252
19362
|
- [Compare hero stats with a radar chart](./examples/radar-chart.md)
|
|
@@ -19282,7 +20392,7 @@ Component registration happens through side-effect imports such as `@vielzeug/re
|
|
|
19282
20392
|
|
|
19283
20393
|
**Feedback:** `ore-alert`, `ore-async`, `ore-badge`, `ore-chip`, `ore-cookie-banner`, `ore-password-strength`, `ore-progress`, `ore-skeleton`, `ore-toast`, `ore-typing-indicator`
|
|
19284
20394
|
|
|
19285
|
-
**Inputs:** `ore-button`, `ore-button-group`, `ore-calendar`, `ore-checkbox`, `ore-checkbox-group`, `ore-column`, `ore-combobox`, `ore-counter`, `ore-datagrid`, `ore-date-picker`, `ore-file-input`, `ore-input`, `ore-message-composer`, `ore-number-input`, `ore-otp-input`, `ore-qr-scanner`, `ore-radio`, `ore-radio-group`, `ore-rating`, `ore-select`, `ore-slider`, `ore-switch`, `ore-textarea`, `ore-time-picker`
|
|
20395
|
+
**Inputs:** `ore-button`, `ore-button-group`, `ore-calendar`, `ore-checkbox`, `ore-checkbox-group`, `ore-column`, `ore-combobox`, `ore-counter`, `ore-datagrid`, `ore-date-picker`, `ore-file-input`, `ore-input`, `ore-message-composer`, `ore-number-input`, `ore-otp-input`, `ore-qr-scanner`, `ore-query-bar`, `ore-radio`, `ore-radio-group`, `ore-rating`, `ore-select`, `ore-slider`, `ore-switch`, `ore-textarea`, `ore-time-picker`
|
|
19286
20396
|
|
|
19287
20397
|
**Layout:** `ore-box`, `ore-grid`, `ore-grid-item`, `ore-navbar`, `ore-sidebar`
|
|
19288
20398
|
|
|
@@ -19388,7 +20498,7 @@ import type { OreButtonProps } from '@vielzeug/refine/button';
|
|
|
19388
20498
|
| Content | `avatar`, `avatar-group`, `breadcrumb`, `card`, `carousel`, `chat-message`, `code-window`, `copy-command`, `icon`, `keyboard-key`, `list`, `list-item`, `marquee`, `pagination`, `separator`, `speech-player`, `stats`, `step`, `stepper`, `table`, `text` |
|
|
19389
20499
|
| Disclosure | `accordion`, `accordion-item`, `tabs`, `tab-item`, `tab-panel` |
|
|
19390
20500
|
| Feedback | `alert`, `async`, `badge`, `chip`, `cookie-banner`, `password-strength`, `progress`, `skeleton`, `toast`, `typing-indicator` |
|
|
19391
|
-
| Inputs | `button`, `button-group`, `calendar`, `checkbox`, `checkbox-group`, `combobox`, `counter`, `datagrid`, `date-picker`, `file-input`, `input`, `message-composer`, `number-input`, `otp-input`, `radio`, `radio-group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time-picker` |
|
|
20501
|
+
| Inputs | `button`, `button-group`, `calendar`, `checkbox`, `checkbox-group`, `combobox`, `counter`, `datagrid`, `date-picker`, `file-input`, `input`, `message-composer`, `number-input`, `otp-input`, `query-bar`, `radio`, `radio-group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time-picker` |
|
|
19392
20502
|
| Layout | `box`, `grid`, `grid-item`, `navbar`, `sidebar` |
|
|
19393
20503
|
| Overlays | `command-palette`, `dialog`, `drawer`, `menu`, `navigation-menu`, `popover`, `tooltip` |
|
|
19394
20504
|
|
|
@@ -29322,6 +30432,7 @@ useEffect(() => {
|
|
|
29322
30432
|
## Examples
|
|
29323
30433
|
|
|
29324
30434
|
- [Sync a Vault Store](./examples/sync-a-vault-store.md)
|
|
30435
|
+
- [Make Store Commands Undoable](./examples/make-store-commands-undoable.md)
|
|
29325
30436
|
- [Keep Deletions from Resurrecting](./examples/keep-deletions-from-resurrecting.md)
|
|
29326
30437
|
- [Switch Accounts Safely](./examples/switch-accounts-safely.md)
|
|
29327
30438
|
|