wiki-formant 0.51.0 → 0.52.0

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.
@@ -2,9 +2,8 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  // block-views.tsx — the block leaves the wikis share.
4
4
  //
5
- // Behind its own subpath for the reason `react.tsx` is: React is an OPTIONAL
6
- // peer, so a consumer that only wants the taxonomy or the MCP transport still
7
- // installs a package with no runtime dependencies.
5
+ // Behind its own subpath for the reason `react.tsx` is: React is an optional
6
+ // peer, so a consumer that only wants the taxonomy or the MCP transport needs no React.
8
7
  //
9
8
  // WHY THESE FIVE AND NOT THE SWITCH. The block union is per-repo and staying
10
9
  // that way (see the Block Model note in the workspace CLAUDE.md) — a closed
@@ -14,8 +14,8 @@ export interface AiCrawler {
14
14
  }
15
15
  /**
16
16
  * Structurally what Next's `MetadataRoute.Robots['rules']` wants, without
17
- * importing Next: this package has no runtime dependencies and is not going to
18
- * grow one for a shape that is three optional string fields.
17
+ * importing Next: this package does not depend on Next and is not going to for
18
+ * a shape that is three optional string fields.
19
19
  */
20
20
  export interface RobotsGroup {
21
21
  userAgent: string;
package/dist/db.d.ts CHANGED
@@ -19,3 +19,28 @@ export interface PgPoolConfig {
19
19
  * a page that fans out more than three queries in thirty seconds.
20
20
  */
21
21
  export declare function pgPoolConfig(url: string | undefined): PgPoolConfig;
22
+ /**
23
+ * What every app's prisma.config.ts hands `defineConfig`, from the folder the
24
+ * file sits in: `defineConfig(prismaConfig(import.meta.dirname))`. Four apps
25
+ * carried the file, three of them byte for byte.
26
+ *
27
+ * Read only by the Prisma CLI, never by the app, so the URL prefers DIRECT_URL:
28
+ * every CLI operation that connects (db push, migrate diff, studio) needs a
29
+ * session connection, and Supabase's transaction pooler on 6543, which
30
+ * DATABASE_URL names, hangs on them rather than failing. A placeholder stands
31
+ * in where neither is set, as on a Vercel build: `prisma generate` reads the
32
+ * schema and never connects, and anything that does connect fails loudly.
33
+ *
34
+ * `schema` is a folder of .prisma files (`prisma-models` writes beside the
35
+ * app's own) or a single file. Paths resolve against `root`, so the CLI works
36
+ * from a subdirectory.
37
+ */
38
+ export declare function prismaConfig(root: string, schema?: string): {
39
+ schema: string;
40
+ migrations: {
41
+ path: string;
42
+ };
43
+ datasource: {
44
+ url: string;
45
+ };
46
+ };
package/dist/db.js CHANGED
@@ -1,4 +1,4 @@
1
- // db.ts — the pool settings every app here hands to `pg`.
1
+ // db.ts — the pool settings every app here hands to `pg`, and the config its prisma.config.ts hands the Prisma CLI.
2
2
  //
3
3
  // Four Prisma client modules carried the same block, comment and all. It is a
4
4
  // value, not a client: the app still owns `new pg.Pool(...)`, the adapter and
@@ -25,3 +25,34 @@ export function pgPoolConfig(url) {
25
25
  connectionTimeoutMillis: 30_000,
26
26
  };
27
27
  }
28
+ /**
29
+ * What every app's prisma.config.ts hands `defineConfig`, from the folder the
30
+ * file sits in: `defineConfig(prismaConfig(import.meta.dirname))`. Four apps
31
+ * carried the file, three of them byte for byte.
32
+ *
33
+ * Read only by the Prisma CLI, never by the app, so the URL prefers DIRECT_URL:
34
+ * every CLI operation that connects (db push, migrate diff, studio) needs a
35
+ * session connection, and Supabase's transaction pooler on 6543, which
36
+ * DATABASE_URL names, hangs on them rather than failing. A placeholder stands
37
+ * in where neither is set, as on a Vercel build: `prisma generate` reads the
38
+ * schema and never connects, and anything that does connect fails loudly.
39
+ *
40
+ * `schema` is a folder of .prisma files (`prisma-models` writes beside the
41
+ * app's own) or a single file. Paths resolve against `root`, so the CLI works
42
+ * from a subdirectory.
43
+ */
44
+ export function prismaConfig(root, schema = 'prisma/schema') {
45
+ try {
46
+ process.loadEnvFile(`${root}/.env`);
47
+ }
48
+ catch {
49
+ // A Vercel build has no .env and needs none.
50
+ }
51
+ return {
52
+ schema: `${root}/${schema}`,
53
+ migrations: { path: `${root}/prisma/migrations` },
54
+ datasource: {
55
+ url: process.env.DIRECT_URL ?? process.env.DATABASE_URL ?? 'postgresql://placeholder:placeholder@localhost:5432/placeholder',
56
+ },
57
+ };
58
+ }
package/dist/editor.d.ts CHANGED
@@ -15,6 +15,9 @@ export type { StarterKitOptions } from '@tiptap/starter-kit';
15
15
  export type { LinkOptions } from '@tiptap/extension-link';
16
16
  export type { ImageOptions } from '@tiptap/extension-image';
17
17
  export type { TableOptions } from '@tiptap/extension-table';
18
+ /** The view and the editor's type, so a consumer imports nothing from `@tiptap/*` itself. */
19
+ export { EditorContent } from '@tiptap/react';
20
+ export type { Editor } from '@tiptap/core';
18
21
  export interface WikiEditorExtensionOptions {
19
22
  /** Empty-document prompt. */
20
23
  placeholder?: string;
package/dist/editor.js CHANGED
@@ -33,6 +33,8 @@ import { useEditor } from '@tiptap/react';
33
33
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
34
34
  import { toMapEmbedUrl } from './maps.js';
35
35
  import { cx } from './html.js';
36
+ /** The view and the editor's type, so a consumer imports nothing from `@tiptap/*` itself. */
37
+ export { EditorContent } from '@tiptap/react';
36
38
  // ---- paste scrubbing --------------------------------------------------------
37
39
  /**
38
40
  * Reduce pasted HTML to structure plus the three attributes that carry meaning.
package/dist/headers.d.ts CHANGED
@@ -25,3 +25,30 @@ export declare function contentSecurityPolicy(directives?: Partial<Record<CspDir
25
25
  * sets its policy per request (a nonce) passes none.
26
26
  */
27
27
  export declare function securityHeaders(csp?: string | null): HeaderEntry[];
28
+ /** The parts of a Next config `withWikiDefaults` fills in. */
29
+ interface WikiNextConfig {
30
+ distDir?: string;
31
+ experimental?: object;
32
+ images?: object;
33
+ headers?: () => Promise<unknown[]>;
34
+ }
35
+ /**
36
+ * An app's Next config with the settings every app here wrote out the same way:
37
+ *
38
+ * - `distDir` from `STUDIO_DIST_DIR`, set only by radix-studio's renders, which
39
+ * boot a second dev server beside the running one, and Next allows one per
40
+ * distDir.
41
+ * - `turbopackFileSystemCacheForBuild: false`. Next 16.3 turns Turbopack's
42
+ * build cache on and Vercel restores it from the last deployment; caper
43
+ * shipped a stylesheet two commits stale that way in October 2026.
44
+ * - AVIF before WebP from the image optimiser.
45
+ * - `securityHeaders` on every route, with `csp` enforced in production only:
46
+ * Next's dev runtime is inline and `eval`. An origin that sets its policy
47
+ * per request (a nonce) passes none.
48
+ *
49
+ * Anything the app sets wins, and its own `headers()` follow these.
50
+ */
51
+ export declare function withWikiDefaults<T extends WikiNextConfig>(config: T, { csp }?: {
52
+ csp?: string;
53
+ }): T;
54
+ export {};
package/dist/headers.js CHANGED
@@ -11,7 +11,8 @@
11
11
  // `youtube-nocookie.com` its sanitiser kept, so those embeds survived cleaning
12
12
  // and then rendered blank.
13
13
  //
14
- // Zero imports, so a config pays for nothing it does not use.
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.
15
16
  /**
16
17
  * The embed hosts the editor's iframe, YouTube, tweet and map nodes produce:
17
18
  * the sanitiser's default `iframeHosts` and the default `frame-src`, so an
@@ -78,3 +79,33 @@ export function securityHeaders(csp) {
78
79
  { key: 'X-DNS-Prefetch-Control', value: 'on' },
79
80
  ];
80
81
  }
82
+ /**
83
+ * An app's Next config with the settings every app here wrote out the same way:
84
+ *
85
+ * - `distDir` from `STUDIO_DIST_DIR`, set only by radix-studio's renders, which
86
+ * boot a second dev server beside the running one, and Next allows one per
87
+ * distDir.
88
+ * - `turbopackFileSystemCacheForBuild: false`. Next 16.3 turns Turbopack's
89
+ * build cache on and Vercel restores it from the last deployment; caper
90
+ * shipped a stylesheet two commits stale that way in October 2026.
91
+ * - AVIF before WebP from the image optimiser.
92
+ * - `securityHeaders` on every route, with `csp` enforced in production only:
93
+ * Next's dev runtime is inline and `eval`. An origin that sets its policy
94
+ * per request (a nonce) passes none.
95
+ *
96
+ * Anything the app sets wins, and its own `headers()` follow these.
97
+ */
98
+ export function withWikiDefaults(config, { csp } = {}) {
99
+ return {
100
+ ...(process.env.STUDIO_DIST_DIR ? { distDir: process.env.STUDIO_DIST_DIR } : {}),
101
+ ...config,
102
+ experimental: { turbopackFileSystemCacheForBuild: false, ...config.experimental },
103
+ images: { formats: ['image/avif', 'image/webp'], ...config.images },
104
+ async headers() {
105
+ return [
106
+ { source: '/:path*', headers: securityHeaders(process.env.NODE_ENV === 'production' ? csp : null) },
107
+ ...((await config.headers?.()) ?? []),
108
+ ];
109
+ },
110
+ };
111
+ }
@@ -1,3 +1,4 @@
1
+ import { type NormaliseLinkOptions } from './links.js';
1
2
  /** A heading found in a page's HTML, in document order. */
2
3
  export interface Heading {
3
4
  id: string;
@@ -84,3 +85,12 @@ export interface OutlineIssue {
84
85
  * single `noH1` and nothing else, since every later rule would restate it.
85
86
  */
86
87
  export declare function outlineIssues(html: string): OutlineIssue[];
88
+ /**
89
+ * The pass every app runs over a content block's stored HTML before it renders:
90
+ * heading ids, a scroll box round each table, and, given `links`, every anchor
91
+ * tagged internal or external. Four apps wrote the same chain in three orders.
92
+ * Pass one `used` set (and one `links.citedRefs`) across a page's blocks.
93
+ */
94
+ export declare function articleHtml(html: string, { links, ...headings }?: HeadingIdOptions & {
95
+ links?: NormaliseLinkOptions;
96
+ }): string;
package/dist/headings.js CHANGED
@@ -14,6 +14,8 @@
14
14
  // that with a fresh set each time, so a "Notes" in two blocks shipped two
15
15
  // `id="notes"`. One `used` set per page is what makes the dedupe hold.
16
16
  import { getAttr, stripTags } from './html.js';
17
+ import { scrollTables } from './dom.js';
18
+ import { normaliseLinks } from './links.js';
17
19
  /** The default slug rule: lowercase words joined by hyphens. */
18
20
  export function slugifyHeading(text) {
19
21
  return text
@@ -131,3 +133,13 @@ export function outlineIssues(html) {
131
133
  issues.push({ fault: 'noSubheading', level: 1 });
132
134
  return issues;
133
135
  }
136
+ /**
137
+ * The pass every app runs over a content block's stored HTML before it renders:
138
+ * heading ids, a scroll box round each table, and, given `links`, every anchor
139
+ * tagged internal or external. Four apps wrote the same chain in three orders.
140
+ * Pass one `used` set (and one `links.citedRefs`) across a page's blocks.
141
+ */
142
+ export function articleHtml(html, { links, ...headings } = {}) {
143
+ const out = scrollTables(injectHeadingIds(html, headings));
144
+ return links ? normaliseLinks(out, links) : out;
145
+ }
package/dist/history.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // `revisions.ts` decides which bump a save asks for and stays free of
4
4
  // dependencies, because every write path imports it. This is the reading half:
5
5
  // the same walk, turned into what a reviewer is shown. It needs a text diff, so
6
- // it needs `fast-diff`, an optional peer; hence its own subpath.
6
+ // it needs `fast-diff`; hence its own subpath.
7
7
  //
8
8
  // radix-wiki had it first and shipped both HTML bodies of every modified block
9
9
  // to the browser to diff there: 887 KB of payload for a 13-revision page whose
package/dist/http.d.ts CHANGED
@@ -42,8 +42,12 @@ export declare function textHeaders(etag: string, lastModified: string, maxAge?:
42
42
  *
43
43
  * `lastModified` is optional because not every corpus has a row timestamp to
44
44
  * offer, and a twin that stamps the epoch is worse than one that stamps
45
- * nothing. `extra` carries whatever the mount needs on top — an `X-Robots-Tag`
46
- * where the twin is a second public URL with no canonical of its own.
45
+ * nothing. `extra` carries whatever the mount needs on top.
46
+ *
47
+ * `noindex` is for a twin of an HTML page: a search engine that reads the twin
48
+ * has the page already, and Google indexed `.md` beside the HTML until four
49
+ * routes each set `X-Robots-Tag` by hand. A document with no HTML page, such
50
+ * as AGENTS.md, leaves it off.
47
51
  *
48
52
  * Pass `etag`. The twin is the single most recrawled URL a page has, and a
49
53
  * response with no validator is a full render on every pass forever — the same
@@ -55,6 +59,7 @@ export declare function textHeaders(etag: string, lastModified: string, maxAge?:
55
59
  export declare function markdownHeaders(lastModified?: string | null, opts?: {
56
60
  maxAge?: number;
57
61
  etag?: string;
62
+ noindex?: boolean;
58
63
  extra?: Record<string, string>;
59
64
  }): Record<string, string>;
60
65
  /**
package/dist/http.js CHANGED
@@ -100,8 +100,12 @@ export function textHeaders(etag, lastModified, maxAge = 3600) {
100
100
  *
101
101
  * `lastModified` is optional because not every corpus has a row timestamp to
102
102
  * offer, and a twin that stamps the epoch is worse than one that stamps
103
- * nothing. `extra` carries whatever the mount needs on top — an `X-Robots-Tag`
104
- * where the twin is a second public URL with no canonical of its own.
103
+ * nothing. `extra` carries whatever the mount needs on top.
104
+ *
105
+ * `noindex` is for a twin of an HTML page: a search engine that reads the twin
106
+ * has the page already, and Google indexed `.md` beside the HTML until four
107
+ * routes each set `X-Robots-Tag` by hand. A document with no HTML page, such
108
+ * as AGENTS.md, leaves it off.
105
109
  *
106
110
  * Pass `etag`. The twin is the single most recrawled URL a page has, and a
107
111
  * response with no validator is a full render on every pass forever — the same
@@ -117,6 +121,7 @@ export function markdownHeaders(lastModified, opts = {}) {
117
121
  'Cache-Control': `public, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 24}`,
118
122
  ...(opts.etag ? { ETag: opts.etag } : {}),
119
123
  ...(lastModified ? { 'Last-Modified': lastModified } : {}),
124
+ ...(opts.noindex ? { 'X-Robots-Tag': 'noindex' } : {}),
120
125
  ...opts.extra,
121
126
  };
122
127
  }
@@ -7,6 +7,13 @@ export interface IndexNowEntry {
7
7
  }
8
8
  /** The `<loc>` and `<lastmod>` of every `<url>` in a sitemap. */
9
9
  export declare function sitemapEntries(xml: string): IndexNowEntry[];
10
+ /**
11
+ * Every entry of the sitemap at `url`, following a sitemap index one level down
12
+ * to at most 20 children. The `indexnow` and `check-pages` bins each parsed
13
+ * sitemaps, and only one of them read an index. Throws when `url` itself does
14
+ * not answer 200; a child that fails is skipped.
15
+ */
16
+ export declare function readSitemap(url: string, fetchImpl?: typeof fetch): Promise<IndexNowEntry[]>;
10
17
  /**
11
18
  * The URLs modified at or after `since`. An entry with no `lastModified` is
12
19
  * left out: nothing says it changed, and the protocol asks for changed pages
package/dist/indexnow.js CHANGED
@@ -12,11 +12,10 @@
12
12
  // The key is not a secret. An engine verifies a submission by fetching
13
13
  // `/<key>.txt` from the origin, so the key is public by design, and the file in
14
14
  // each app's `public/` is the only copy.
15
+ import { decodeEntities } from './markdown.js';
15
16
  export const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';
16
17
  /** The protocol's cap on the URLs in one POST. */
17
18
  export const INDEXNOW_BATCH = 10_000;
18
- const XML_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
19
- const unescapeXml = (s) => s.trim().replace(/&(amp|lt|gt|quot|apos);/g, (_, e) => XML_ENTITIES[e]);
20
19
  /** The `<loc>` and `<lastmod>` of every `<url>` in a sitemap. */
21
20
  export function sitemapEntries(xml) {
22
21
  const entries = [];
@@ -25,10 +24,30 @@ export function sitemapEntries(xml) {
25
24
  if (!loc)
26
25
  continue;
27
26
  const lastmod = /<lastmod>([\s\S]*?)<\/lastmod>/.exec(body)?.[1];
28
- entries.push(lastmod ? { url: unescapeXml(loc), lastModified: lastmod.trim() } : { url: unescapeXml(loc) });
27
+ const url = decodeEntities(loc.trim());
28
+ entries.push(lastmod ? { url, lastModified: lastmod.trim() } : { url });
29
29
  }
30
30
  return entries;
31
31
  }
32
+ /**
33
+ * Every entry of the sitemap at `url`, following a sitemap index one level down
34
+ * to at most 20 children. The `indexnow` and `check-pages` bins each parsed
35
+ * sitemaps, and only one of them read an index. Throws when `url` itself does
36
+ * not answer 200; a child that fails is skipped.
37
+ */
38
+ export async function readSitemap(url, fetchImpl = fetch) {
39
+ const res = await fetchImpl(url);
40
+ if (!res.ok)
41
+ throw new Error(`${url} answered ${res.status}`);
42
+ const xml = await res.text();
43
+ if (!/<sitemapindex/i.test(xml))
44
+ return sitemapEntries(xml);
45
+ const children = [...xml.matchAll(/<loc>([\s\S]*?)<\/loc>/g)].slice(0, 20).map(m => decodeEntities(m[1].trim()));
46
+ const entries = [];
47
+ for (const child of children)
48
+ entries.push(...(await readSitemap(child, fetchImpl).catch(() => [])));
49
+ return entries;
50
+ }
32
51
  /**
33
52
  * The URLs modified at or after `since`. An entry with no `lastModified` is
34
53
  * left out: nothing says it changed, and the protocol asks for changed pages
@@ -17,6 +17,7 @@
17
17
  // which is what lets an audit spend no probes on its flag list.
18
18
  //
19
19
  // No subpath exports this: the bins and the tests import it from dist.
20
+ import { escXml } from './feed.js';
20
21
  /** Held in state. */
21
22
  const MAX = { backlog: 60, runs: 30, feedback: 40, sources: 150 };
22
23
  /** Shown on the page. What a reader pays is words, and the entries run long. */
@@ -63,7 +64,7 @@ export function addBacklog(state, items, today) {
63
64
  }
64
65
  return added;
65
66
  }
66
- const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
67
+ const esc = (s) => escXml(String(s ?? ''));
67
68
  const more = (shown, total, what) => total > shown ? `<p><em>${total - shown} older ${what} held in state.</em></p>` : '';
68
69
  const table = (head, rows) => `<table><tbody><tr>${head.map(h => `<td><strong>${h}</strong></td>`).join('')}</tr>` +
69
70
  (rows.join('') || `<tr>${head.map(() => '<td>–</td>').join('')}</tr>`) +
package/dist/react.d.ts CHANGED
@@ -171,7 +171,7 @@ export interface TypeaheadState<T> {
171
171
  * per mount, not module-level, so a stale result cannot outlive the page.
172
172
  *
173
173
  * The debounce is inlined rather than pulled from a package: this one is nine
174
- * lines, and the point of the package is to have no runtime dependencies.
174
+ * lines, and a dependency for nine lines is not worth one.
175
175
  */
176
176
  export declare function useTypeahead<T>({ fetch, onPick, onEscape, delay, minLength, cache, }: TypeaheadOptions<T>): TypeaheadState<T>;
177
177
  export interface LinkPreviewOptions<T> {
package/dist/react.js CHANGED
@@ -3,9 +3,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  // react.tsx — the collapsible wiki sidebar, and the table of contents inside it.
4
4
  //
5
5
  // This is the one part of the package that needs React, so it is the one part
6
- // behind its own subpath: `wiki-formant/react`. React is an OPTIONAL PEER
7
- // dependency, so a consumer that only wants the taxonomy or the MCP transport
8
- // still installs a package with no runtime dependencies at all.
6
+ // behind its own subpath: `wiki-formant/react`. React is an optional peer, so a
7
+ // consumer that only wants the taxonomy or the MCP transport needs no React.
9
8
  //
10
9
  // What is shared here is behaviour, not appearance. Every class name is passed
11
10
  // in, because the three wikis style their rails differently and always will;
@@ -292,7 +291,7 @@ export function useClickOutside(onClose) {
292
291
  * per mount, not module-level, so a stale result cannot outlive the page.
293
292
  *
294
293
  * The debounce is inlined rather than pulled from a package: this one is nine
295
- * lines, and the point of the package is to have no runtime dependencies.
294
+ * lines, and a dependency for nine lines is not worth one.
296
295
  */
297
296
  export function useTypeahead({ fetch, onPick, onEscape, delay = 200, minLength = 1, cache = true, }) {
298
297
  const [query, setQuery] = useState('');
package/dist/sanitize.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // Everything is an allowlist: unknown tags, unknown attributes (every on*
12
12
  // handler among them) and non-http(s) URLs are dropped, not escaped. Run it
13
13
  // server-side, on the render path, so it covers rows written before it existed
14
- // and no sanitiser ships to the client. `sanitize-html` is an optional peer.
14
+ // and no sanitiser ships to the client.
15
15
  import sanitizeHtml from 'sanitize-html';
16
16
  import { DEFAULT_IFRAME_HOSTS } from './headers.js';
17
17
  // Inline SVG, presentational elements only. The infographics pipeline embeds
package/dist/stats.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import type { ReactNode } from 'react';
1
2
  import { type Digest, type StatsFilter } from './analytics.js';
3
+ import type { PasskeyGate } from './passkey.js';
2
4
  export interface StatsProps {
3
5
  /** One window's figures, from `digest` in `wiki-formant/analytics`. */
4
6
  digest: Digest;
@@ -26,3 +28,29 @@ export declare function statsQuery(params: Record<string, string | string[] | un
26
28
  * until the importing stylesheet gives them a colour.
27
29
  */
28
30
  export declare function Stats({ digest: d, days, filter, ranges }: StatsProps): import("react").JSX.Element;
31
+ export interface StatsGateProps {
32
+ gate: PasskeyGate;
33
+ /** The gate's cookie from the request: `(await cookies()).get(gate.cookie)?.value`. */
34
+ session: string | undefined;
35
+ /** `?invite=` from the link, when there is one. */
36
+ invite?: string | undefined;
37
+ /** The site's host, named in the prompt. */
38
+ host: string;
39
+ /** The route that serves `gate.route`. */
40
+ endpoint?: string;
41
+ /** What a signed-in reader sees, rendered only for one. */
42
+ children: () => ReactNode | Promise<ReactNode>;
43
+ classNames?: {
44
+ text?: string;
45
+ actions?: string;
46
+ button?: string;
47
+ error?: string;
48
+ };
49
+ }
50
+ /**
51
+ * A stats page's door: `children` for a signed-in reader, else a line saying
52
+ * what the passkey prompt will do and the button that does it. With an invite
53
+ * it saves a passkey, as it does while the site has none; otherwise it signs
54
+ * in. Three apps wrote this branch and its three sentences.
55
+ */
56
+ export declare function StatsGate({ gate, session, invite, host, endpoint, children, classNames: c }: StatsGateProps): Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").JSX.Element | null | undefined>;
package/dist/stats.js CHANGED
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { STATS_FILTERS } from './analytics.js';
3
+ import { PasskeyButton } from './passkey-button.js';
3
4
  import { StatsFigures } from './stats-figures.js';
4
5
  /** The windows the stats page offers, in days; `Infinity` is all time. */
5
6
  const STATS_RANGES = [1, 7, 30, 90, 365, Infinity];
@@ -74,3 +75,19 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES }) {
74
75
  const windows = (_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx("a", { href: href(n, filter), "aria-current": n === days ? 'page' : undefined, children: windowLabel(n) }, n))) }));
75
76
  return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs("a", { href: href(days, { ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, windows: windows }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => href(days, { ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
76
77
  }
78
+ /**
79
+ * A stats page's door: `children` for a signed-in reader, else a line saying
80
+ * what the passkey prompt will do and the button that does it. With an invite
81
+ * it saves a passkey, as it does while the site has none; otherwise it signs
82
+ * in. Three apps wrote this branch and its three sentences.
83
+ */
84
+ export async function StatsGate({ gate, session, invite, host, endpoint = '/api/passkey', children, classNames: c = {} }) {
85
+ if (!invite && (await gate.signedIn(session)))
86
+ return children();
87
+ const open = !invite && (await gate.open());
88
+ return (_jsxs(_Fragment, { children: [_jsx("p", { className: c.text, children: invite
89
+ ? `Save a passkey on this device to open ${host}'s stats.`
90
+ : open
91
+ ? `No passkey is saved for ${host} yet. Save the first one here; any after that needs an invite.`
92
+ : 'Sign in with a passkey saved for this site.' }), _jsx("div", { className: c.actions, children: _jsx(PasskeyButton, { endpoint: endpoint, invite: invite, create: open, className: c.button, errorClassName: c.error }) })] }));
93
+ }
package/dist/tiptap.js CHANGED
@@ -2,10 +2,8 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // tiptap.tsx — the custom editor nodes the wikis share.
4
4
  //
5
- // Behind its own subpath for the same reason `react.tsx` is: the four
6
- // `@tiptap/*` packages are OPTIONAL peer dependencies, so a consumer that only
7
- // wants the taxonomy or the MCP transport still installs a package with no
8
- // runtime dependencies at all.
5
+ // Behind its own subpath so a module that only wants the taxonomy or the MCP
6
+ // transport never loads the `@tiptap/*` packages.
9
7
  //
10
8
  // What is shared is the node schema and its behaviour, never appearance. Class
11
9
  // names and icons are injected, which is what lets one wiki keep `text-jupiter`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -275,12 +275,11 @@
275
275
  "test": "npm run build && node --test 'test/*.test.js'",
276
276
  "prepare": "git config core.hooksPath .githooks || true"
277
277
  },
278
- "devDependencies": {
279
- "@radixdlt/rola": "^2.1.0",
278
+ "dependencies": {
280
279
  "@simplewebauthn/browser": "^14.0.0",
281
280
  "@simplewebauthn/server": "^14.0.3",
282
- "@tiptap/core": "^2.10.0",
283
- "@tiptap/extension-code-block": "^2.10.0",
281
+ "@tiptap/core": "^2.27.3",
282
+ "@tiptap/extension-code-block": "^2.27.3",
284
283
  "@tiptap/extension-image": "^2.27.3",
285
284
  "@tiptap/extension-link": "^2.27.3",
286
285
  "@tiptap/extension-placeholder": "^2.27.3",
@@ -288,44 +287,29 @@
288
287
  "@tiptap/extension-table-cell": "^2.27.3",
289
288
  "@tiptap/extension-table-header": "^2.27.3",
290
289
  "@tiptap/extension-table-row": "^2.27.3",
291
- "@tiptap/extension-youtube": "^2.10.0",
292
- "@tiptap/pm": "^2.10.0",
293
- "@tiptap/react": "^2.10.0",
290
+ "@tiptap/extension-youtube": "^2.27.3",
291
+ "@tiptap/pm": "^2.27.3",
292
+ "@tiptap/react": "^2.27.3",
294
293
  "@tiptap/starter-kit": "^2.27.3",
294
+ "fast-diff": "^1.3.0",
295
+ "lightweight-charts": "^5.2.1",
296
+ "sanitize-html": "^2.17.7"
297
+ },
298
+ "devDependencies": {
299
+ "@radixdlt/rola": "^2.1.0",
295
300
  "@types/node": "^22.0.0",
296
301
  "@types/react": "^19.0.0",
297
302
  "@types/sanitize-html": "^2.16.1",
298
303
  "@vercel/blob": "^2.8.2",
299
- "fast-diff": "^1.3.0",
300
304
  "jose": "^6.2.12",
301
305
  "jsdom": "^30.1.0",
302
- "lightweight-charts": "^5.2.1",
303
306
  "react": "^19.2.4",
304
- "sanitize-html": "^2.17.7",
305
307
  "sharp": "^0.35.5",
306
308
  "typescript": "^5.9.0"
307
309
  },
308
310
  "peerDependencies": {
309
- "@simplewebauthn/browser": ">=13",
310
- "@simplewebauthn/server": ">=13",
311
- "@tiptap/core": ">=2.10",
312
- "@tiptap/extension-code-block": ">=2.10",
313
- "@tiptap/extension-image": ">=2.10",
314
- "@tiptap/extension-link": ">=2.10",
315
- "@tiptap/extension-placeholder": ">=2.10",
316
- "@tiptap/extension-table": ">=2.10",
317
- "@tiptap/extension-table-cell": ">=2.10",
318
- "@tiptap/extension-table-header": ">=2.10",
319
- "@tiptap/extension-table-row": ">=2.10",
320
- "@tiptap/extension-youtube": ">=2.10",
321
- "@tiptap/pm": ">=2.10",
322
- "@tiptap/react": ">=2.10",
323
- "@tiptap/starter-kit": ">=2.10",
324
- "fast-diff": ">=1.3",
325
- "lightweight-charts": ">=5",
326
311
  "pg": ">=8",
327
312
  "react": ">=18",
328
- "sanitize-html": ">=2",
329
313
  "jose": ">=6",
330
314
  "@radixdlt/rola": ">=2",
331
315
  "sharp": ">=0.33",
@@ -335,63 +319,9 @@
335
319
  "react": {
336
320
  "optional": true
337
321
  },
338
- "@tiptap/core": {
339
- "optional": true
340
- },
341
- "@tiptap/react": {
342
- "optional": true
343
- },
344
- "@tiptap/extension-youtube": {
345
- "optional": true
346
- },
347
- "@tiptap/extension-code-block": {
348
- "optional": true
349
- },
350
- "@tiptap/starter-kit": {
351
- "optional": true
352
- },
353
- "@tiptap/extension-link": {
354
- "optional": true
355
- },
356
- "@tiptap/extension-image": {
357
- "optional": true
358
- },
359
- "@tiptap/extension-table": {
360
- "optional": true
361
- },
362
- "@tiptap/extension-table-row": {
363
- "optional": true
364
- },
365
- "@tiptap/extension-table-cell": {
366
- "optional": true
367
- },
368
- "@tiptap/extension-table-header": {
369
- "optional": true
370
- },
371
- "@tiptap/extension-placeholder": {
372
- "optional": true
373
- },
374
- "sanitize-html": {
375
- "optional": true
376
- },
377
- "@tiptap/pm": {
378
- "optional": true
379
- },
380
- "fast-diff": {
381
- "optional": true
382
- },
383
322
  "pg": {
384
323
  "optional": true
385
324
  },
386
- "@simplewebauthn/server": {
387
- "optional": true
388
- },
389
- "@simplewebauthn/browser": {
390
- "optional": true
391
- },
392
- "lightweight-charts": {
393
- "optional": true
394
- },
395
325
  "jose": {
396
326
  "optional": true
397
327
  },