wiki-formant 0.86.1 → 0.86.2

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/README.md CHANGED
@@ -321,13 +321,6 @@ export async function GET(request: Request) {
321
321
 
322
322
  `descriptorResponse` and `descriptorHandler` serve a JSON descriptor (an agent card, a manifest, an OpenAPI document) with its own ETag and `Access-Control-Allow-Origin: *`, since every descriptor is public and browser agents read them; six of nine routes had passed the header by hand.
323
323
 
324
- `wellKnownHandler({ agentCard, mcpManifest, openapi, registryAuth? })` is the whole `/.well-known/[doc]` route: the card at `agent.json` and `agent-card.json`, the manifest at `mcp.json`, the spec at `openapi.json` and the registry's proof at `mcp-registry-auth`, each through `descriptorResponse`, and a 404 for any other name. A rewrite puts `/openapi.json` on it. `registryAuth: null` serves no proof, for a tenant of a host whose namespace is the host's.
325
-
326
- ```ts
327
- // src/app/.well-known/[doc]/route.ts
328
- export const GET = wellKnownHandler({ agentCard: AGENT_CARD, mcpManifest: MCP_MANIFEST, openapi: SPEC });
329
- ```
330
-
331
324
  `corpusValidatorsFrom(seed, stamps)` is that pair from an aggregate: the newest of `stamps` (the epoch when every one is null), appended to `seed` for the tag. `corpusRoute(validators, build)` is the whole handler, and skips the build on a 304.
332
325
 
333
326
  ```ts
@@ -376,7 +369,7 @@ Four configs had restated these by hand on the grounds that a config could not i
376
369
  export default withWikiDefaults<NextConfig>({ async redirects() { /* … */ } }, { csp });
377
370
  ```
378
371
 
379
- `contentSecurityPolicy` starts from the policy all four origins shared line for line, and each directive passed replaces its default rather than appending, so the call site reads as the whole list. An empty list drops a directive. A second argument, `{ nonce, inlineScripts }`, makes `script-src` strict where there is a nonce: `'self'`, the nonce and `scriptHash` of each inline script, with no `'unsafe-inline'`. The hash admits a static boot script without the layout reading the nonce, and moves with the script when a bump changes it.
372
+ `contentSecurityPolicy` starts from the policy all four origins shared line for line, and each directive passed replaces its default rather than appending, so the call site reads as the whole list. An empty list drops a directive.
380
373
 
381
374
  The default `frame-src` is `DEFAULT_IFRAME_HOSTS`, the same list the sanitiser's `iframeHosts` defaults to. The sanitiser's comment had asked every config to keep the two matched by hand, and one did not: its policy lacked the bare `youtube.com` and `youtube-nocookie.com` its sanitiser kept, so those embeds survived cleaning and then rendered blank. A site with its own host list passes it to the sanitiser and states the same hosts in its `frame-src`.
382
375
 
@@ -756,21 +749,19 @@ const core = coreAtomicValidator({ pageId: id => /^\d{1,9}$/.test(id) });
756
749
 
757
750
  `coreAtomicToMarkdown` joins a resolved page's site-relative `href` to `siteUrl`, because a twin is read off-site; a wiki whose lists hold its own page rows keeps those two cases. `coreAtomicValidator` caps `recentPages.limit` and `pageIds` at `MAX_BLOCK_ROWS`, a whole number of rows, because both end in a database `take:`; `blockLimit` is the same bound on the read side.
758
751
 
759
- `createBlockValidator` returns `blockIssues` beside the boolean validators: the same walk, reporting where each failure is.
752
+ `defineBlocks` (in `wiki-formant/validation`) is the whole model in one declaration: the shape, the types, `createBlock`, `duplicateBlock`, `validateBlocks` and `blockIssues`, from a record of defaults and the wiki's own leaf checks. A check for a core type narrows the package's: `list: (b, core) => core(b) && isSection(b.under)`. `BlockTree<Atomic>` is the union over the two containers.
760
753
 
761
754
  ```ts
762
- const issues = blockIssues(body.content);
763
- if (issues.length) return badRequest(`Invalid content: ${describeBlockIssues(issues)}`);
764
- // Invalid content: [2].columns[0].blocks[1]: malformed linkGrid block
755
+ const model = defineBlocks<Block>({ defaults: { ...coreBlockDefaults(), corpusStats: () => ({ type: 'corpusStats', view: 'sourceTiers' }) }, checks: { corpusStats: b => isCorpusView(b.view) } });
756
+ export const { shape: BLOCK_SHAPE, createBlock, duplicateBlock, validateBlocks, blockIssues } = model;
765
757
  ```
766
758
 
767
- `defineBlocks<Block>({ defaults, checks?, pageId? })` is a wiki's whole block model from its type set. `defaults` is what inserting each type makes, and its keys are the types the wiki knows; `checks` is the leaf check for each native type, and for a core type the wiki narrows, which gets the core check as its second argument. It returns `types`, `atomicTypes` (all but the two containers), `shape`, `createBlock`, `duplicateBlock`, `validateBlocks` and `blockIssues`. `BlockTree<AtomicBlock>` from `wiki-formant/blocks` is the union it models.
759
+ `createBlockValidator` returns `blockIssues` beside the boolean validators: the same walk, reporting where each failure is.
768
760
 
769
761
  ```ts
770
- export const { shape: BLOCK_SHAPE, createBlock, duplicateBlock, validateBlocks, blockIssues } = defineBlocks<Block>({
771
- defaults: { ...coreBlockDefaults({ stats }), corpusStats: () => ({ type: 'corpusStats', view: 'sourceTiers' }) },
772
- checks: { corpusStats: b => isCorpusView(b.view), list: (b, core) => core(b) && isSection(b.under) },
773
- });
762
+ const issues = blockIssues(body.content);
763
+ if (issues.length) return badRequest(`Invalid content: ${describeBlockIssues(issues)}`);
764
+ // Invalid content: [2].columns[0].blocks[1]: malformed linkGrid block
774
765
  ```
775
766
 
776
767
  A `codeTabs` tab's `code` is source text in every wiki here, and every view treats it that way: `CodeTabsView` escapes it into a `<pre><code>`, the sanitiser leaves it alone, the markdown twin fences it verbatim. A wiki that escapes and highlights on the server passes `highlighted` to the view. The package used to assume editors stored highlighted markup; none ever had, and every twin printed `Vec<u8>` as `Vec`.
@@ -976,8 +967,10 @@ its readers asked for. The queries that return zero rows are the valuable ones
976
967
  `wiki-formant/passkey` is the door to a site's own admin pages: a passkey and
977
968
  nothing else, no account, password or email. `createPasskeyGate({ sql, rpName })`
978
969
  returns the whole of it. `gate.route` is the sign-in route, `gate.signOut` the
979
- sign-out route, `gate.signedIn(cookieValue)` is the check a page makes, `gate.allows(request)` the same check over a write route's request, and
980
- `gate.cookie` is the cookie's name. The two tables (`Passkey`, `PasskeyToken`) ship as Prisma
970
+ sign-out route, `gate.signedIn(cookieValue)` is the check a page makes,
971
+ `gate.allows(request)` the same check for a write route's own request, and
972
+ `gate.cookie` is the cookie's name. An editor with one writer needs no
973
+ other door: acuiq2's wiki editor opens on the passkey that opens its stats. The two tables (`Passkey`, `PasskeyToken`) ship as Prisma
981
974
  models in `prisma/passkey.prisma`, and every token is kept only as its hash.
982
975
 
983
976
  There is no sign-up. Every passkey, the first among them, is saved from a
@@ -1108,26 +1101,15 @@ the site's stylesheet gives them and the box a look.
1108
1101
 
1109
1102
  ## Base stylesheet
1110
1103
 
1111
- `wiki-formant/base.css` is first the layout the package's markup does not work without: columns that stack until there is room, stored tab panels that show one at a time, a copy button pinned to its block's corner and visible on focus and on touch, a table's scroll box, the stats block's grid, the code-tab strip, the banner's row and the infobox's wrapping, the breadcrumb row, the `aria-sort` arrow as a mask over `currentColor`, the stats page's grid and bars, and the chart's height and pinned buttons. It also carries the shape and motion of the primitives every site re-implemented and forked — `.spinner` (a `currentColor` ring sized by `--spinner-size`), `.skeleton`'s pulse, `.empty-state`, the wrap rule for long tokens in inline `code` — and the reduced-motion guard none of them had. That part has no colour and no scale, so a design system's own rules override it at equal specificity. The one set of tokens it declares is the page frame every site lays out by: `--content-width` (72rem, gutters inside), `--sidebar-width` (18rem) and `--page-pad` (16 / 24 / 32px at base / 640 / 1024). They sit in the components layer, so a site never restates them in `@theme`, which would lose; an element that must differ sets its own.
1104
+ `wiki-formant/base.css` is the layout the package's markup does not work without, and nothing else: columns that stack until there is room, stored tab panels that show one at a time, a copy button pinned to its block's corner and visible on focus and on touch, a table's scroll box, the stats block's grid, the code-tab strip, the banner's row and the infobox's wrapping, the breadcrumb row, the `aria-sort` arrow as a mask over `currentColor`, the stats page's grid and bars, and the chart's height and pinned buttons. It also carries the shape and motion of the primitives every site re-implemented and forked — `.spinner` (a `currentColor` ring sized by `--spinner-size`), `.skeleton`'s pulse, `.empty-state`, the wrap rule for long tokens in inline `code` — and the reduced-motion guard none of them had. No colour and no scale, so a design system's own rules override it at equal specificity. The one set of tokens it declares is the page frame every site lays out by: `--content-width` (72rem, gutters inside), `--sidebar-width` (18rem) and `--page-pad` (16 / 24 / 32px at base / 640 / 1024). They sit in the components layer, so a site never restates them in `@theme`, which would lose; an element that must differ sets its own.
1112
1105
 
1113
1106
  ```css
1114
1107
  @import "wiki-formant/base.css" layer(components);
1115
1108
  ```
1116
1109
 
1117
- No view emits a Tailwind utility any more — `ColumnsView` states its gap and alignment as data attributes, and an inactive code tab is `hidden` — so a site no longer needs its Tailwind to scan this package for the markup to work.
1118
-
1119
- After the structure comes the look: every component the package's markup names (heading anchors, code tabs and the copy button, notices, references, the infobox, stats cards and deltas, link grids, see-also cards, section cards, the link preview, the rail's table of contents, the work queues, revision diffs, sortable headers, charts) drawn from twelve tokens a site maps once, unlayered, from its own palette. Four sites had each drawn the same components in their own tokens. A site's own rule after the import still wins.
1110
+ Since 0.86 it also draws the package's components — the infobox, section and stat cards, see-also and page-ref lists, the hover card, the maintenance queues, the chart's frame and buttons, code tabs, the editorial banner, references, heading anchors, link grids, revision diffs and the rail's contents — on eleven `--ui-*` tokens (`--ui-surface`, `--ui-surface-2`, `--ui-border`, `--ui-radius`, `--ui-pad`, `--ui-small`, `--ui-muted`, `--ui-accent`, `--ui-on-accent`, `--ui-up`, `--ui-down`) that a site maps from its own palette in `:root`. Four stylesheets had restated sixty of those classes with four token vocabularies; now a site states the rule only where it differs. The split of a page beside its infobox stays the site's, as does the rail's surface.
1120
1111
 
1121
- ```css
1122
- :root {
1123
- --ui-surface: var(--color-surface-1); --ui-muted: var(--color-text-muted);
1124
- --ui-surface-2: var(--color-surface-2); --ui-accent: var(--color-accent);
1125
- --ui-border: var(--color-border); --ui-on-accent: var(--color-surface-0);
1126
- --ui-radius: var(--radius); --ui-up: var(--color-success);
1127
- --ui-pad: 1rem; --ui-down: var(--color-error);
1128
- --ui-small: var(--text-small); --ui-shadow: var(--shadow);
1129
- }
1130
- ```
1112
+ No view emits a Tailwind utility any more — `ColumnsView` states its gap and alignment as data attributes, and an inactive code tab is `hidden` — so a site no longer needs its Tailwind to scan this package for the markup to work.
1131
1113
 
1132
1114
  ## API
1133
1115
 
@@ -1170,12 +1152,6 @@ and overwrites any inbound copy — and the leftmost `x-forwarded-for` only wher
1170
1152
  `limitClient(prefix, headers, opts)` is the per-address gate, `rateLimit` over
1171
1153
  `clientKey`, which a dozen call sites had written out.
1172
1154
 
1173
- `wiki-formant/scripts` is what a wiki's direct-database scripts share: `withClient(fn, { tag })` (a pg client that rolls back and fails the run on a throw, and invalidates the cache `tag` through the Vercel CLI when it wrote), `storeFor(client)` and `readPage` over the app's own store, `writeRevision(client, page, blocks, opts)` (one revision, refused if the page moved since it was read), `upsertWikiPage(client, page)` (a revision only when something changed), the block builders `uid`, `content`, `banner` and `infobox`, and `assertLinkShapes`, which throws on the mis-shaped link a script would otherwise store unseen. Its `ScriptClient` is the slice of a pg client they take; Prisma fits it with `$queryRawUnsafe`.
1174
-
1175
- `wiki-formant/link-check`'s `auditLinks(pages, { livePaths, probe?, internal? })` is a checker's whole walk: every link on every page probed once, internals checked against `livePaths` (a page's `/history` and `/edit` resolve with it, and a query rides on its path), metadata facts included, embeds collected for the caller to probe. `printLinkAudit(audit, { scope, pagesScanned })` prints what broke.
1176
-
1177
- `wiki-formant/knip` is the dead-code config every app spreads into its own `knip.ts`: `export default { ...base, ignoreBinaries: [...base.ignoreBinaries, 'ffmpeg'] }`.
1178
-
1179
1155
  ## A bin
1180
1156
 
1181
1157
  `check-classes` fails a build on a `className` token that resolves to nothing, and counts the inverse fault — 3+ Tailwind utilities inline, which is a component class that was never named. Both sets are derived from the emitted stylesheets rather than listed, so nothing has to be maintained per project. Run it from a repo root after a build:
@@ -1213,6 +1189,20 @@ An app that keeps one schema file and writes the models into it by hand, because
1213
1189
  npx prisma-models analytics --check --in prisma/schema.prisma --tenant # miow
1214
1190
  ```
1215
1191
 
1192
+ `wiki-formant/scripts` is what a seed or sweep script shares with the next one: `withClient` (a `pg` client on `DATABASE_URL` that rolls back on a throw and expires the wiki's cache tag after a write), `readPage` and `writeRevision` (one revision over a page the script read, refused if the page moved on), `upsertWikiPage`, the block builders `content`, `banner` and `infobox`, and `assertLinkShapes`, which refuses a link or reference the views would silently drop. radix-wiki and caper had each written it in a different language.
1193
+
1194
+ ```ts
1195
+ import { withClient, readPage, writeRevision } from 'wiki-formant/scripts';
1196
+ await withClient(async client => {
1197
+ const page = await readPage(client, 'contents/tech', 'radix-engine');
1198
+ await writeRevision(client, page!, blocks, { change: 'minor', message: 'Why, with the source.' });
1199
+ }, { tag: 'wiki' });
1200
+ ```
1201
+
1202
+ `wiki-formant/link-check`'s `auditLinks` is the walk every dead-link checker makes — each link once, with the pages that carry it, probed with `probeExternal` or the checker's own — and `printLinkAudit` its first lines; what a checker keeps is its live paths, its special cases and the rest of its report.
1203
+
1204
+ `wiki-formant/knip` is the knip configuration the apps share, spread into each one's `knip.ts` with what is its own.
1205
+
1216
1206
  `fts-ddl <table>` rebuilds a table's `search_tsv` generated column and its GIN index from the expression `wiki-formant/search` queries, over `DIRECT_URL` (else `DATABASE_URL` on 5432). Run it before declaring the column in `schema.prisma`; the bin's header says why. A PascalCase Prisma table goes in quoted, as SQL names it.
1217
1207
 
1218
1208
  ```sh
package/bin/_cli.mjs CHANGED
@@ -38,13 +38,4 @@ export async function connect({ direct = false, ...options } = {}) {
38
38
  return client;
39
39
  }
40
40
 
41
- /** `vercel cache invalidate --tag`, reported either way. */
42
- export async function invalidate(tag) {
43
- const { execFileSync } = await import('node:child_process');
44
- try {
45
- execFileSync('vercel', ['cache', 'invalidate', '--tag', tag, '--yes'], { stdio: 'ignore' });
46
- console.log(`cache: ${tag} tag invalidated`);
47
- } catch (e) {
48
- console.warn(`cache: ${tag} tag NOT invalidated (${e.message}); run \`vercel cache invalidate --tag ${tag}\``);
49
- }
50
- }
41
+ export { invalidate } from '../dist/scripts.js';
package/dist/base.css CHANGED
@@ -1,8 +1,7 @@
1
1
  /* wiki-formant/base.css — the layout the package's markup does not work without.
2
2
 
3
- Structure first: the layout the markup needs, no colour and no type scale.
4
- Then, at the end, the look: every component drawn from twelve `--ui-*`
5
- tokens the importing stylesheet maps from its own palette. That stylesheet
3
+ Structure only: no colour, no type scale, no design token. Every value that
4
+ is a look rather than a mechanism is left to the importing stylesheet, which
6
5
  loads this first and overrides anything here with an equally specific rule.
7
6
 
8
7
  @import "wiki-formant/base.css" layer(components);
@@ -176,6 +175,157 @@ html[data-sidebar='closed'] .wiki-sidebar { width: 0; }
176
175
  .ref-backlink,
177
176
  .reference-link { text-decoration: none; }
178
177
 
178
+ /* ---- the look of the package's components ----------------------------------
179
+ The panels, lists, cards and rails the package's markup produces, drawn once
180
+ on eight tokens a site maps from its own palette in `:root`. Four
181
+ stylesheets had restated sixty of these classes with four vocabularies
182
+ (`--corner`, `--radius`, `--radius-wing`; `--space`, `--spacing-md`,
183
+ `--space-3`) and four bodies. A site that must differ overrides the rule;
184
+ one that sets no token gets a readable default off `currentColor`.
185
+
186
+ :root {
187
+ --ui-surface: var(--color-surface-1); a card's background
188
+ --ui-surface-2: var(--color-surface-2); a raised or hovered one
189
+ --ui-border: var(--color-border);
190
+ --ui-radius: var(--radius);
191
+ --ui-pad: 1rem; a panel's padding
192
+ --ui-small: var(--text-small); the small type size
193
+ --ui-muted: var(--color-text-muted);
194
+ --ui-accent: var(--color-accent);
195
+ --ui-on-accent: var(--color-bg); text on the accent
196
+ --ui-up: var(--color-success); a gain, an addition
197
+ --ui-down: var(--color-error); a loss, a removal, a warning
198
+ } */
199
+ :root {
200
+ --ui-surface: color-mix(in srgb, currentColor 6%, transparent);
201
+ --ui-surface-2: color-mix(in srgb, currentColor 12%, transparent);
202
+ --ui-border: color-mix(in srgb, currentColor 20%, transparent);
203
+ --ui-radius: 0.5rem;
204
+ --ui-pad: 1rem;
205
+ --ui-small: 0.875rem;
206
+ --ui-muted: color-mix(in srgb, currentColor 70%, transparent);
207
+ --ui-accent: currentColor;
208
+ --ui-on-accent: Canvas;
209
+ --ui-up: currentColor;
210
+ --ui-down: currentColor;
211
+ }
212
+
213
+ /* A panel: the infobox, a section card, a stat card, a chart. */
214
+ .infobox, .section-card, .chart {
215
+ background: var(--ui-surface);
216
+ border: 1px solid var(--ui-border);
217
+ border-radius: var(--ui-radius);
218
+ }
219
+ .infobox { padding: var(--ui-pad); }
220
+ .infobox table { width: 100%; table-layout: fixed; border-collapse: collapse; }
221
+ .infobox :is(th, td) { padding: 0.375rem 0.5rem; border: 0; border-bottom: 1px solid var(--ui-border); background: none; text-align: left; vertical-align: top; }
222
+ /* Labels are short and values are the reading: 42% still fits "Published:". */
223
+ .infobox th { width: 42%; font-weight: 600; color: var(--ui-muted); }
224
+ .infobox tr:last-child :is(th, td) { border-bottom: 0; }
225
+ .infobox-facts { margin: 0; }
226
+ .infobox-series { align-items: center; gap: 0.125rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--ui-border); text-align: center; font-size: var(--ui-small); color: var(--ui-muted); }
227
+ .page-with-infobox { gap: 1.5rem; }
228
+
229
+ .section-card { display: flex; flex-direction: column; gap: 0.5rem; padding: var(--ui-pad); transition: border-color 150ms; }
230
+ .section-card:hover { border-color: var(--ui-accent); }
231
+
232
+ .page-refs { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; overflow: hidden; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
233
+ .page-refs > li + li { border-top: 1px solid var(--ui-border); }
234
+
235
+ .see-also-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0.75rem; margin: 0; padding: 0; list-style: none; }
236
+ .see-also-item { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; padding: 0.75rem; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); color: inherit; text-decoration: none; transition: border-color 150ms; }
237
+ .see-also-item:hover { border-color: var(--ui-accent); }
238
+ .see-also-title { font-weight: 600; }
239
+ .see-also-detail { font-size: var(--ui-small); color: var(--ui-muted); }
240
+
241
+ .link-preview-card { position: fixed; z-index: 50; width: 20rem; max-width: calc(100vw - 1rem); overflow: hidden; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); box-shadow: var(--ui-shadow, 0 8px 24px -4px rgb(0 0 0 / 0.25)); }
242
+ .link-preview-body { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.75rem; }
243
+ .link-preview-title { font-weight: 600; }
244
+ .link-preview-excerpt { display: -webkit-box; margin: 0; overflow: hidden; font-size: var(--ui-small); color: var(--ui-muted); -webkit-line-clamp: 4; -webkit-box-orient: vertical; }
245
+
246
+ .maintenance-queue { display: flex; flex-direction: column; gap: 0.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ui-border); }
247
+ .maintenance-count { margin-left: 0.5rem; font-size: var(--ui-small); font-weight: 400; font-variant-numeric: tabular-nums; color: var(--ui-muted); }
248
+ .maintenance-list { display: grid; gap: 0.375rem; margin: 0; padding: 0; list-style: none; }
249
+ .maintenance-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; }
250
+ .maintenance-item > a { color: var(--ui-accent); }
251
+ .maintenance-description, .maintenance-detail { font-size: var(--ui-small); color: var(--ui-muted); }
252
+
253
+ .stat-grid { gap: 0.75rem; }
254
+ .stat-card { gap: 0.25rem; padding: 0.75rem; background: var(--ui-surface); border-radius: var(--ui-radius); }
255
+ .stat-value { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
256
+ .stat-label { font-size: var(--ui-small); color: var(--ui-muted); }
257
+ .stats-delta { font-size: 0.6em; font-weight: 600; }
258
+ .stats-delta[data-better='true'] { color: var(--ui-up); }
259
+ .stats-delta[data-better='false'] { color: var(--ui-down); }
260
+
261
+ .chart-canvas { color: var(--ui-accent); font-size: 0.6875rem; --chart-axis: var(--ui-muted); --chart-up: var(--ui-up); --chart-down: var(--ui-down); }
262
+ .chart-status { font-size: var(--ui-small); color: var(--ui-muted); }
263
+ .chart-controls :is(button, a) { border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) / 2); font-size: 0.625rem; opacity: 1; transition: background-color 150ms, color 150ms; }
264
+ .chart-controls :is(button, a):hover { background: var(--ui-surface-2); }
265
+ .chart-controls :is(button, a):is([aria-pressed='true'], [aria-current]) { background: var(--ui-accent); border-color: var(--ui-accent); color: var(--ui-on-accent); }
266
+
267
+ .code-tabs { margin-block: 1rem; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
268
+ .code-tabs-list { overflow-x: auto; border-bottom: 1px solid var(--ui-border); background: var(--ui-surface); }
269
+ .code-tabs-btn { padding: 0.5rem 1rem; border: 0; border-bottom: 2px solid transparent; background: transparent; font: inherit; font-size: var(--ui-small); color: var(--ui-muted); transition: color 150ms; }
270
+ .code-tabs-btn:hover { color: inherit; }
271
+ .code-tabs-btn-active { border-bottom-color: var(--ui-accent); color: var(--ui-accent); }
272
+ .code-tabs pre { margin: 0; border: 0; border-radius: 0; }
273
+ .code-copy-btn { padding: 0.375rem; border-radius: calc(var(--ui-radius) / 2); background: var(--ui-surface-2); color: var(--ui-muted); transition: opacity 120ms ease, color 120ms ease; }
274
+ .code-copy-btn:hover { color: inherit; }
275
+
276
+ .editorial-banner { align-items: flex-start; gap: 0.75rem; margin-block: 0.75rem; padding: 0.75rem; background: var(--ui-surface); border: 1px solid var(--ui-border); border-left-width: 3px; border-radius: var(--ui-radius); font-size: var(--ui-small); }
277
+ .editorial-banner-body { flex: 1; min-width: 0; margin: 0; }
278
+ .editorial-banner-unsourced, .editorial-banner-outdated { border-color: color-mix(in srgb, var(--ui-down) 40%, transparent); background: color-mix(in srgb, var(--ui-down) 10%, transparent); }
279
+ .editorial-banner-unsourced .editorial-banner-icon, .editorial-banner-outdated .editorial-banner-icon { color: var(--ui-down); }
280
+
281
+ .references-block { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--ui-border); }
282
+ .references-block h2 { margin: 0 0 0.5rem; }
283
+ .references-list { margin: 0; padding-left: 1.5rem; list-style: decimal; font-size: var(--ui-small); color: var(--ui-muted); }
284
+ .reference-item { margin-bottom: 0.25rem; padding-left: 0.25rem; scroll-margin-top: calc(var(--rail-top, 0px) + 1rem); }
285
+ .reference-item a, .ref-backlink { color: var(--ui-accent); }
286
+ .reference-item:target { background: color-mix(in srgb, var(--ui-accent) 15%, transparent); border-radius: calc(var(--ui-radius) / 2); }
287
+
288
+ :is(h2, h3, h4, h5, h6):has(> .heading-anchor) { position: relative; }
289
+ .heading-anchor { position: absolute; top: 0; left: -1.25rem; font-weight: 400; color: var(--ui-muted); opacity: 0; transition: opacity 150ms; }
290
+ .heading-anchor::before { content: '#'; }
291
+ .heading-anchor:hover { color: var(--ui-accent); }
292
+ :is(h2, h3, h4, h5, h6):hover > .heading-anchor, .heading-anchor:focus-visible { opacity: 1; }
293
+
294
+ .facet-summary { margin: 0; font-size: var(--ui-small); color: var(--ui-muted); }
295
+
296
+ .link-grid { display: flex; flex-direction: column; gap: 1rem; margin-block: 1rem; }
297
+ .link-grid-group-description { margin: 0 0 0.75rem; font-size: var(--ui-small); color: var(--ui-muted); }
298
+ .link-grid-group-description p { margin: 0; }
299
+ .link-grid-pills { display: flex; flex-wrap: wrap; gap: 0.5rem; }
300
+ .link-grid-pills > a { padding: 0.375rem 0.75rem; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); font-size: var(--ui-small); color: inherit; text-decoration: none; transition: background 150ms, border-color 150ms, color 150ms; }
301
+ .link-grid-pills > a:hover { background: var(--ui-surface-2); border-color: var(--ui-accent); color: var(--ui-accent); }
302
+
303
+ .revision-change-action { font-weight: 600; }
304
+ .revision-change[data-action='added'] .revision-change-action { color: var(--ui-up); }
305
+ .revision-change[data-action='removed'] .revision-change-action { color: var(--ui-down); }
306
+ .revision-change[data-action='moved'] .revision-change-action { color: var(--ui-accent); }
307
+ .revision-diff { line-height: 1.6; color: var(--ui-muted); }
308
+ .revision-diff del { color: var(--ui-down); }
309
+ .revision-diff ins { padding-inline: 0.125rem; border-radius: 2px; background: color-mix(in srgb, var(--ui-up) 10%, transparent); color: var(--ui-up); text-decoration: none; }
310
+
311
+ .sort-header { gap: 0.25rem; transition: color 150ms; }
312
+ .sort-header:hover, :is([aria-sort='ascending'], [aria-sort='descending']) > .sort-header { color: inherit; }
313
+
314
+ .column-view { gap: 1rem; }
315
+
316
+ .link { color: var(--ui-accent); text-decoration: none; cursor: pointer; }
317
+ .link:hover { text-decoration: underline; }
318
+
319
+ /* The rail's contents: a label over a group of entries, and the article's contents with the section being read lit. */
320
+ .wiki-sidebar { --rail-border: 1px solid var(--ui-border); }
321
+ .wiki-sidebar__scroll { gap: 1.5rem; padding: 1rem var(--page-pad) 3rem; }
322
+ .wiki-rail__group, .wiki-rail__toc-list { display: flex; flex-direction: column; gap: 1px; }
323
+ .wiki-rail__label { padding: 0 0.5rem 0.25rem; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ui-muted); }
324
+ .wiki-rail__toc-btn, .wiki-rail__toc-item { display: flex; align-items: center; gap: 0.25rem; width: 100%; padding: 0.25rem 0.5rem; border: 0; background: none; font: inherit; color: var(--ui-muted); text-align: left; cursor: pointer; }
325
+ .wiki-rail__toc-item { border-inline-start: 2px solid var(--ui-border); font-size: var(--ui-small); text-decoration: none; transition: color 150ms, border-color 150ms; }
326
+ .wiki-rail__toc-item:hover, .wiki-rail__toc-item--active { border-inline-start-color: var(--ui-accent); color: var(--ui-accent); }
327
+ .wiki-rail__toc-item--active { font-weight: 600; }
328
+
179
329
  /* ---- stored tab groups (activateTabGroups) --------------------------------
180
330
  Only the active panel shows. Before the script runs, the first tab's content
181
331
  stands in for the group rather than every tab at once. */
@@ -489,144 +639,3 @@ pre:hover > .code-copy-btn,
489
639
  max-width: 100%;
490
640
  min-height: 200px;
491
641
  }
492
-
493
- /* ==== the look ================================================================
494
- What the components above look like, drawn from twelve tokens a site maps
495
- once from its own palette, unlayered so the mapping wins:
496
-
497
- --ui-surface a card's ground --ui-muted secondary text
498
- --ui-surface-2 a strip, a label cell --ui-accent links, the current item
499
- --ui-border every rule and outline --ui-on-accent text on the accent
500
- --ui-radius every corner --ui-up a good change, an insertion
501
- --ui-pad a card's padding --ui-down a bad change, a deletion
502
- --ui-small the small type size --ui-shadow a floating card's shadow
503
-
504
- Four sites had each drawn these same components in their own tokens, the
505
- same rules restated four ways, and the one that forgot a rule shipped it
506
- unstyled. A site still overrides any of it with its own rule after this. */
507
-
508
- /* Permalink beside a heading: the glyph comes from here, so a table of contents
509
- reading the heading's text never picks it up. */
510
- :is(h2, h3, h4, h5, h6):has(> .heading-anchor) { position: relative; }
511
- .heading-anchor { position: absolute; top: 0; left: -1.25rem; font-weight: 400; color: var(--ui-muted); opacity: 0; transition: opacity 0.15s; }
512
- .heading-anchor::before { content: '#'; }
513
- .heading-anchor:hover { color: var(--ui-accent); }
514
- :is(h2, h3, h4, h5, h6):hover > .heading-anchor,
515
- .heading-anchor:focus-visible { opacity: 1; }
516
- .link { color: var(--ui-accent); text-decoration: none; cursor: pointer; }
517
- .link:hover { text-decoration: underline; }
518
-
519
- /* Code tabs and the copy button. */
520
- .code-tabs { margin-block: 1rem; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
521
- .code-tabs-list { overflow-x: auto; background: var(--ui-surface-2); border-bottom: 1px solid var(--ui-border); }
522
- .code-tabs-btn { padding: 0.5rem 1rem; font: inherit; font-size: var(--ui-small); color: var(--ui-muted); background: transparent; border: 0; border-bottom: 2px solid transparent; transition: color 0.15s; }
523
- .code-tabs-btn:hover { color: inherit; }
524
- /* The box clips, so the focus ring goes inside the button. */
525
- .code-tabs-btn:focus-visible { outline-offset: -2px; }
526
- .code-tabs-btn-active { color: var(--ui-accent); border-bottom-color: var(--ui-accent); }
527
- .code-tabs pre { margin: 0; border: 0; border-radius: 0; }
528
- .code-copy-btn { padding: 0.375rem; color: var(--ui-muted); background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); z-index: 1; }
529
- .code-copy-btn:hover { color: var(--ui-accent); }
530
-
531
- /* Notices. A notice is a claim about the page, so it reads as an aside. */
532
- .editorial-banner { align-items: flex-start; gap: 0.75rem; margin-block: 0.75rem; padding: 0.75rem; font-size: var(--ui-small); background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
533
- .editorial-banner-icon { flex-shrink: 0; margin-top: 0.125rem; }
534
- .editorial-banner-body { flex: 1; min-width: 0; margin: 0; }
535
- :is(.editorial-banner-unsourced, .editorial-banner-outdated, .editorial-banner-promotional, .editorial-banner-coi) { border-color: color-mix(in srgb, var(--ui-down) 35%, transparent); background: color-mix(in srgb, var(--ui-down) 8%, var(--ui-surface)); }
536
- :is(.editorial-banner-unsourced, .editorial-banner-outdated, .editorial-banner-promotional, .editorial-banner-coi) .editorial-banner-icon { color: var(--ui-down); }
537
-
538
- /* References. */
539
- .references-block { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--ui-border); }
540
- .references-block h2 { margin: 0 0 0.5rem; }
541
- .references-list { margin: 0; padding-left: 1.5rem; list-style: decimal; font-size: var(--ui-small); color: var(--ui-muted); }
542
- .reference-item { margin-block: 0.25rem; padding-left: 0.25rem; scroll-margin-top: 5rem; }
543
- .reference-item:target { background: color-mix(in srgb, var(--ui-accent) 12%, transparent); border-radius: var(--ui-radius); }
544
- :is(.reference-item a, .ref-backlink, .reference-link) { color: var(--ui-accent); }
545
- :is(.reference-item a, .ref-backlink, .reference-link):hover { text-decoration: underline; }
546
-
547
- /* The infobox and its facts. Labels are short and values are the reading, so
548
- the label column takes 42%, the floor that still fits "Published:". */
549
- .page-with-infobox { gap: 1.5rem; }
550
- .infobox { padding: var(--ui-pad); background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
551
- .infobox table { table-layout: fixed; width: 100%; border-collapse: collapse; }
552
- .infobox :is(th, td) { padding: 0.375rem 0.5rem; font-size: var(--ui-small); text-align: left; vertical-align: top; border: 0; border-bottom: 1px solid var(--ui-border); }
553
- .infobox th { width: 42%; font-weight: 600; color: var(--ui-muted); }
554
- .infobox tr:last-child > :is(th, td) { border-bottom: 0; }
555
- .infobox-facts { margin: 0; }
556
- .infobox-series { align-items: center; gap: 0.125rem; margin-bottom: 0.75rem; padding-bottom: 0.75rem; text-align: center; font-size: var(--ui-small); color: var(--ui-muted); border-bottom: 1px solid var(--ui-border); }
557
-
558
- /* Stats blocks and the figures' change from the window before. */
559
- .stat-grid { gap: 0.75rem; }
560
- .stat-card { gap: 0.25rem; padding: var(--ui-pad); background: var(--ui-surface); border-radius: var(--ui-radius); }
561
- .stat-value { font-weight: 700; line-height: 1.15; }
562
- .stat-label { font-size: var(--ui-small); color: var(--ui-muted); }
563
- .stats-delta { font-size: 0.6em; font-weight: 600; }
564
- .stats-delta[data-better='true'] { color: var(--ui-up); }
565
- .stats-delta[data-better='false'] { color: var(--ui-down); }
566
-
567
- /* Columns, link grids, related pages. */
568
- .column-view { gap: 1rem; }
569
- .link-grid { display: flex; flex-direction: column; gap: 1rem; margin-block: 1rem; }
570
- .link-grid-group-description { margin: 0 0 0.75rem; color: var(--ui-muted); }
571
- .link-grid-group-description p { margin: 0; }
572
- .link-grid-pills { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: 0.5rem; }
573
- .link-grid-pills > a { display: block; padding: 0.625rem 0.875rem; font-size: var(--ui-small); text-align: center; text-decoration: none; color: inherit; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); transition: background 0.15s, border-color 0.15s, color 0.15s; }
574
- .link-grid-pills > a:hover { color: var(--ui-accent); background: var(--ui-surface-2); border-color: var(--ui-accent); }
575
- .see-also-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0.75rem; margin: 0; padding: 0; list-style: none; }
576
- .see-also-item { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; padding: 0.75rem; color: inherit; text-decoration: none; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); transition: border-color 0.15s; }
577
- .see-also-item:hover { border-color: var(--ui-accent); }
578
- .see-also-title { font-weight: 600; }
579
- .see-also-item:hover .see-also-title { color: var(--ui-accent); }
580
- .see-also-detail { font-size: var(--ui-small); color: var(--ui-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
581
- .section-card { display: flex; flex-direction: column; gap: 0.5rem; padding: var(--ui-pad); background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); transition: border-color 0.15s; }
582
- /* A page list as rows (PageRefsView): one panel, a rule between rows. radix-wiki dropped its own rule for this on 0.86 and the package drew none. */
583
- .page-refs { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; overflow: hidden; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
584
- .page-refs > li + li { border-top: 1px solid var(--ui-border); }
585
- .section-card:hover { border-color: var(--ui-accent); }
586
- .facet-summary { margin: 0; font-size: var(--ui-small); color: var(--ui-muted); }
587
-
588
- /* The hover card over a link to another page (useLinkPreview). */
589
- .link-preview-card { position: fixed; z-index: 50; width: 20rem; max-width: calc(100vw - 1rem); overflow: hidden; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); box-shadow: var(--ui-shadow); }
590
- .link-preview-body { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.75rem; }
591
- .link-preview-title { font-weight: 600; }
592
- .link-preview-excerpt { margin: 0; font-size: var(--ui-small); color: var(--ui-muted); display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
593
-
594
- /* The rail's frame and its table of contents. */
595
- .wiki-sidebar { --rail-border: 1px solid var(--ui-border); }
596
- .wiki-rail__group, .wiki-rail__toc-list { display: flex; flex-direction: column; gap: 1px; }
597
- .wiki-rail__label { padding: 0 0.5rem 0.25rem; font-size: var(--ui-small); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ui-muted); }
598
- :is(.wiki-rail__toc-btn, .wiki-rail__toc-item) { display: flex; align-items: center; gap: 0.25rem; width: 100%; font: inherit; text-align: left; color: var(--ui-muted); background: none; border: 0; cursor: pointer; }
599
- .wiki-rail__toc-btn { padding: 0 0.5rem 0.25rem; }
600
- .wiki-rail__toc-item { padding: 0.25rem 0.75rem; font-size: var(--ui-small); line-height: 1.4; text-decoration: none; border-inline-start: 2px solid var(--ui-border); transition: color 0.15s, border-color 0.15s; }
601
- .wiki-rail__toc-item:hover, .wiki-rail__toc-item--active { color: var(--ui-accent); border-inline-start-color: var(--ui-accent); }
602
- .wiki-rail__toc-item--active { font-weight: 600; }
603
-
604
- /* Work queues (MaintenanceQueues). */
605
- .maintenance-queue { display: flex; flex-direction: column; gap: 0.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ui-border); }
606
- .maintenance-count { margin-left: 0.5rem; font-weight: 400; font-variant-numeric: tabular-nums; color: var(--ui-muted); }
607
- .maintenance-description, .maintenance-detail { color: var(--ui-muted); }
608
- .maintenance-detail { font-size: var(--ui-small); }
609
- .maintenance-list { display: grid; gap: 0.375rem; margin: 0; padding: 0; list-style: none; }
610
- .maintenance-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; }
611
- .maintenance-item > a { color: var(--ui-accent); }
612
-
613
- /* A revision's changes and its inline diff. */
614
- .revision-change-action { font-weight: 600; color: var(--ui-muted); }
615
- .revision-change[data-action='added'] .revision-change-action { color: var(--ui-up); }
616
- .revision-change[data-action='removed'] .revision-change-action { color: var(--ui-down); }
617
- .revision-diff { line-height: 1.6; color: var(--ui-muted); }
618
- .revision-diff del { color: var(--ui-down); }
619
- .revision-diff ins { color: var(--ui-up); text-decoration: none; background: color-mix(in srgb, var(--ui-up) 12%, transparent); border-radius: 2px; }
620
-
621
- /* Sortable headers: muted until hovered or sorted by. */
622
- .sort-header { color: var(--ui-muted); transition: color 0.15s; }
623
- .sort-header:hover,
624
- :is([aria-sort='ascending'], [aria-sort='descending']) > .sort-header { color: inherit; }
625
-
626
- /* Charts. */
627
- .chart { background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }
628
- .chart-canvas { font-size: 0.6875rem; color: var(--ui-accent); --chart-axis: var(--ui-muted); --chart-up: var(--ui-up); --chart-down: var(--ui-down); }
629
- .chart-status { font-size: var(--ui-small); color: var(--ui-muted); }
630
- .chart-controls :is(button, a) { font-size: 0.6875rem; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); opacity: 1; }
631
- .chart-controls :is(button, a):hover { background: var(--ui-surface-2); }
632
- .chart-controls :is(button, a):is([aria-pressed='true'], [aria-current]) { color: var(--ui-on-accent); background: var(--ui-accent); border-color: var(--ui-accent); }
package/dist/blocks.d.ts CHANGED
@@ -126,8 +126,6 @@ export interface InfoboxBlock<A> {
126
126
  type: 'infobox';
127
127
  blocks: A[];
128
128
  }
129
- /** A wiki's whole block union from its atomic one: the leaves and the two containers holding them. */
130
- export type BlockTree<A> = A | InfoboxBlock<A> | ColumnsBlock<A>;
131
129
  /** Where a list's pages come from: those in a section, the newest, or ones picked by hand. */
132
130
  export declare const LIST_SOURCES: readonly ["under", "newest", "chosen"];
133
131
  export type ListSource = (typeof LIST_SOURCES)[number];
@@ -406,3 +404,8 @@ export interface CoreBlockDefaults {
406
404
  * `{ ...coreBlockDefaults({ stats }), daoTimeline: () => … }`.
407
405
  */
408
406
  export declare function coreBlockDefaults(opts?: CoreBlockDefaultsOptions): CoreBlockDefaults;
407
+ /** A wiki's whole tree: its leaves, and the two containers over them. */
408
+ export type BlockTree<A extends {
409
+ id: string;
410
+ type: string;
411
+ }> = A | InfoboxBlock<A> | ColumnsBlock<A>;
package/dist/headers.d.ts CHANGED
@@ -11,18 +11,14 @@ export interface HeaderEntry {
11
11
  export declare const DEFAULT_IFRAME_HOSTS: readonly string[];
12
12
  /** CSP directive names this builder writes, in the order it writes them. */
13
13
  export type CspDirective = 'default-src' | 'base-uri' | 'object-src' | 'frame-ancestors' | 'form-action' | 'script-src' | 'style-src' | 'font-src' | 'img-src' | 'media-src' | 'connect-src' | 'frame-src' | 'worker-src' | 'manifest-src';
14
- /** A `script-src` source admitting exactly this inline script body. */
14
+ /** A `'sha256-…'` source for one inline script's body, so a nonced policy still runs it. */
15
15
  export declare const scriptHash: (body: string) => string;
16
16
  /**
17
17
  * The policy string: `BASE_CSP` with each directive in `directives` replacing
18
- * its default. An empty list drops the directive.
19
- *
20
- * A `nonce` swaps `script-src` for the strict form, `'self'`, the nonce and a
21
- * hash of each of `inlineScripts`, with no `'unsafe-inline'`. The hashes admit
22
- * a static boot script without the layout reading the nonce, which would make
23
- * every route render per request; hashing here means a package bump that
24
- * changes the script moves its hash too, where a hand-copied one went stale
25
- * and blanked every nonce route. Without a nonce the baseline stands.
18
+ * its default. An empty list drops the directive. With a `nonce`, `script-src`
19
+ * is that nonce and the hashes of `inlineScripts` instead of `'unsafe-inline'`:
20
+ * the per-request policy an origin sets in its proxy, which Next reads from
21
+ * the request's `content-security-policy` header to nonce its own scripts.
26
22
  */
27
23
  export declare function contentSecurityPolicy(directives?: Partial<Record<CspDirective, readonly string[]>>, { nonce, inlineScripts }?: {
28
24
  nonce?: string | null;
package/dist/headers.js CHANGED
@@ -11,9 +11,8 @@
11
11
  // `youtube-nocookie.com` its sanitiser kept, so those embeds survived cleaning
12
12
  // and then rendered blank.
13
13
  //
14
- // One import, node:crypto for a script's hash, so a config pays for nothing
15
- // else. `withWikiDefaults` is here for the same reason: next.config.ts is its
16
- // only caller.
14
+ // Zero imports, so a config pays for nothing it does not use. `withWikiDefaults`
15
+ // is here for the same reason: next.config.ts is its only caller.
17
16
  import { createHash } from 'node:crypto';
18
17
  /**
19
18
  * The embed hosts the editor's iframe, YouTube, tweet and map nodes produce:
@@ -52,22 +51,20 @@ export const BASE_CSP = {
52
51
  'connect-src': ["'self'"],
53
52
  'frame-src': frameSources(),
54
53
  };
55
- /** A `script-src` source admitting exactly this inline script body. */
54
+ /** A `'sha256-…'` source for one inline script's body, so a nonced policy still runs it. */
56
55
  export const scriptHash = (body) => `'sha256-${createHash('sha256').update(body, 'utf8').digest('base64')}'`;
57
56
  /**
58
57
  * The policy string: `BASE_CSP` with each directive in `directives` replacing
59
- * its default. An empty list drops the directive.
60
- *
61
- * A `nonce` swaps `script-src` for the strict form, `'self'`, the nonce and a
62
- * hash of each of `inlineScripts`, with no `'unsafe-inline'`. The hashes admit
63
- * a static boot script without the layout reading the nonce, which would make
64
- * every route render per request; hashing here means a package bump that
65
- * changes the script moves its hash too, where a hand-copied one went stale
66
- * and blanked every nonce route. Without a nonce the baseline stands.
58
+ * its default. An empty list drops the directive. With a `nonce`, `script-src`
59
+ * is that nonce and the hashes of `inlineScripts` instead of `'unsafe-inline'`:
60
+ * the per-request policy an origin sets in its proxy, which Next reads from
61
+ * the request's `content-security-policy` header to nonce its own scripts.
67
62
  */
68
- export function contentSecurityPolicy(directives = {}, { nonce = null, inlineScripts = [] } = {}) {
69
- const strict = nonce ? { 'script-src': ["'self'", `'nonce-${nonce}'`, ...inlineScripts.map(scriptHash)] } : {};
70
- return Object.entries({ ...BASE_CSP, ...directives, ...strict })
63
+ export function contentSecurityPolicy(directives = {}, { nonce, inlineScripts = [] } = {}) {
64
+ const scripts = nonce
65
+ ? { 'script-src': ["'self'", `'nonce-${nonce}'`, ...inlineScripts.map(scriptHash)] }
66
+ : {};
67
+ return Object.entries({ ...BASE_CSP, ...directives, ...scripts })
71
68
  .filter(([, sources]) => sources?.length)
72
69
  .map(([name, sources]) => `${name} ${sources.join(' ')}`)
73
70
  .join('; ');
package/dist/knip.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- declare const _default: {
1
+ declare const config: {
2
+ $schema: string;
2
3
  entry: string[];
3
4
  project: string[];
4
- ignore: string[];
5
5
  ignoreDependencies: string[];
6
6
  ignoreBinaries: string[];
7
7
  };
8
- export default _default;
8
+ export default config;