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 +29 -39
- package/bin/_cli.mjs +1 -10
- package/dist/base.css +153 -144
- package/dist/blocks.d.ts +5 -2
- package/dist/headers.d.ts +5 -9
- package/dist/headers.js +12 -15
- package/dist/knip.d.ts +3 -3
- package/dist/knip.js +29 -28
- package/dist/link-check.d.ts +27 -26
- package/dist/link-check.js +44 -38
- package/dist/passkey.d.ts +1 -1
- package/dist/passkey.js +12 -8
- package/dist/scripts.d.ts +55 -148
- package/dist/scripts.js +81 -75
- package/dist/validation.d.ts +23 -30
- package/dist/validation.js +23 -17
- package/dist/well-known.d.ts +18 -15
- package/dist/well-known.js +20 -18
- package/package.json +1 -1
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.
|
|
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
|
-
`
|
|
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
|
|
763
|
-
|
|
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
|
-
`
|
|
759
|
+
`createBlockValidator` returns `blockIssues` beside the boolean validators: the same walk, reporting where each failure is.
|
|
768
760
|
|
|
769
761
|
```ts
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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,
|
|
980
|
-
`gate.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
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 `
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
//
|
|
15
|
-
//
|
|
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 `
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
|
69
|
-
const
|
|
70
|
-
|
|
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