wiki-formant 0.47.1 → 0.49.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.
- package/README.md +95 -26
- package/bin/maintenance-log.mjs +198 -0
- package/bin/mark-verified.mjs +75 -0
- package/bin/prisma-models.mjs +53 -0
- package/dist/analytics.d.ts +0 -25
- package/dist/analytics.js +10 -49
- package/dist/base.css +23 -2
- package/dist/blocks.d.ts +7 -6
- package/dist/blocks.js +11 -3
- package/dist/chart.d.ts +17 -8
- package/dist/chart.js +53 -6
- package/dist/combobox.d.ts +0 -4
- package/dist/combobox.js +2 -0
- package/dist/crawlers.d.ts +0 -24
- package/dist/crawlers.js +9 -3
- package/dist/dom.d.ts +0 -5
- package/dist/dom.js +2 -0
- package/dist/editor.d.ts +0 -43
- package/dist/editor.js +7 -1
- package/dist/feed.d.ts +0 -14
- package/dist/feed.js +4 -0
- package/dist/freshness.d.ts +0 -7
- package/dist/freshness.js +3 -0
- package/dist/headers.d.ts +0 -8
- package/dist/headers.js +3 -1
- package/dist/headings.d.ts +0 -8
- package/dist/headings.js +2 -0
- package/dist/history.d.ts +0 -6
- package/dist/history.js +2 -0
- package/dist/http.d.ts +0 -11
- package/dist/http.js +5 -0
- package/dist/license.d.ts +0 -8
- package/dist/license.js +2 -0
- package/dist/link-check.d.ts +0 -21
- package/dist/link-check.js +6 -2
- package/dist/links.d.ts +0 -5
- package/dist/links.js +2 -0
- package/dist/maintenance.d.ts +61 -0
- package/dist/maintenance.js +137 -0
- package/dist/maps.d.ts +0 -6
- package/dist/maps.js +2 -0
- package/dist/markdown.d.ts +0 -2
- package/dist/markdown.js +1 -1
- package/dist/mcp.d.ts +0 -14
- package/dist/mcp.js +1 -1
- package/dist/metadata.d.ts +14 -11
- package/dist/metadata.js +33 -0
- package/dist/pagination.d.ts +0 -5
- package/dist/pagination.js +0 -4
- package/dist/passkey.js +2 -15
- package/dist/rate-limit.d.ts +5 -16
- package/dist/rate-limit.js +10 -1
- package/dist/react-server.d.ts +0 -12
- package/dist/react-server.js +2 -0
- package/dist/revisions.d.ts +0 -7
- package/dist/revisions.js +2 -0
- package/dist/rola.d.ts +74 -0
- package/dist/rola.js +97 -0
- package/dist/search.d.ts +0 -12
- package/dist/search.js +4 -0
- package/dist/sidebar.d.ts +0 -16
- package/dist/sidebar.js +2 -0
- package/dist/stats.d.ts +0 -4
- package/dist/stats.js +2 -2
- package/dist/taxonomy.d.ts +0 -4
- package/dist/taxonomy.js +2 -2
- package/dist/text.d.ts +16 -22
- package/dist/text.js +39 -5
- package/dist/tiptap.d.ts +5 -4
- package/dist/tiptap.js +2 -1
- package/dist/upload.d.ts +31 -0
- package/dist/upload.js +91 -0
- package/dist/validation.d.ts +0 -6
- package/dist/validation.js +3 -3
- package/dist/well-known.d.ts +0 -20
- package/dist/well-known.js +3 -0
- package/package.json +37 -8
- package/prisma/analytics.prisma +57 -0
- package/prisma/passkey.prisma +21 -0
package/dist/analytics.d.ts
CHANGED
|
@@ -4,15 +4,6 @@ import { type HeaderReader, type RateLimitOptions } from './rate-limit.js';
|
|
|
4
4
|
* Prisma: `(query, ...values) => prisma.$queryRawUnsafe(query, ...values)`.
|
|
5
5
|
*/
|
|
6
6
|
export type Sql = (query: string, ...values: unknown[]) => Promise<unknown>;
|
|
7
|
-
/** iPadOS reports itself as a Mac, so its views count as a computer's. */
|
|
8
|
-
export declare const deviceOf: (ua: string) => "phone" | "tablet" | "computer";
|
|
9
|
-
/**
|
|
10
|
-
* Where a visit came from: its `utm_source` when the link carried one, else the
|
|
11
|
-
* referring host, and null for a referrer that is this site or none.
|
|
12
|
-
*/
|
|
13
|
-
export declare function sourceOf(referrer: unknown, utm: unknown, host: string): string | null;
|
|
14
|
-
/** A browser path as stored: decoded, one leading slash, no trailing one. Null when it will not decode. */
|
|
15
|
-
export declare function normalisePath(path: string): string | null;
|
|
16
7
|
export interface ViewInput {
|
|
17
8
|
/** The beacon request's headers: the address, browser, host and country come from them. */
|
|
18
9
|
headers: HeaderReader;
|
|
@@ -22,8 +13,6 @@ export interface ViewInput {
|
|
|
22
13
|
utm?: unknown;
|
|
23
14
|
site?: string;
|
|
24
15
|
}
|
|
25
|
-
/** Count one page view, unless a bot sent it. Never rejects. */
|
|
26
|
-
export declare function recordView(sql: Sql, { headers, path, referrer, utm, site }: ViewInput): Promise<void>;
|
|
27
16
|
export interface EventInput {
|
|
28
17
|
name: string;
|
|
29
18
|
props?: Record<string, string | number>;
|
|
@@ -170,20 +159,6 @@ export interface SearchQueryInput {
|
|
|
170
159
|
/** Which search box, when a site has more than one. */
|
|
171
160
|
surface?: string;
|
|
172
161
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Props for a "Search Query" event – the human-side twin of `mcpCallProps`.
|
|
175
|
-
*
|
|
176
|
-
* A wiki instruments its agent surface and then counts every question an agent
|
|
177
|
-
* asks while recording none of the questions a person asks. The queries that
|
|
178
|
-
* return nothing are the valuable ones: they name a gap in the corpus in the
|
|
179
|
-
* reader's own words, which is otherwise only ever guessed at.
|
|
180
|
-
*
|
|
181
|
-
* Returns `null` for a query with no content, so an empty field cannot fire an
|
|
182
|
-
* event. The text is untrusted, so it is collapsed, lowercased for aggregation
|
|
183
|
-
* and truncated – the same 64-character bound search itself applies, which
|
|
184
|
-
* keeps the prop and the query that produced it the same string.
|
|
185
|
-
*/
|
|
186
|
-
export declare function searchQueryProps({ query, results, surface, }: SearchQueryInput): Record<string, string> | null;
|
|
187
162
|
export interface TrackerOptions {
|
|
188
163
|
sql: Sql;
|
|
189
164
|
/** `after` in Next: the write runs once the response has gone. */
|
package/dist/analytics.js
CHANGED
|
@@ -14,52 +14,9 @@
|
|
|
14
14
|
//
|
|
15
15
|
// The package owns the SQL; the app owns the connection. `Sql` is one function
|
|
16
16
|
// that runs a parameterised statement, which Prisma, `pg` and postgres.js can
|
|
17
|
-
// all supply in a line. The
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// model View {
|
|
21
|
-
// id BigInt @id @default(autoincrement())
|
|
22
|
-
// siteId String @default("")
|
|
23
|
-
// path String
|
|
24
|
-
// source String?
|
|
25
|
-
// country String?
|
|
26
|
-
// device String
|
|
27
|
-
// visitor String
|
|
28
|
-
// createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
29
|
-
// @@index([siteId, createdAt])
|
|
30
|
-
// }
|
|
31
|
-
//
|
|
32
|
-
// model Event {
|
|
33
|
-
// id BigInt @id @default(autoincrement())
|
|
34
|
-
// siteId String @default("")
|
|
35
|
-
// name String
|
|
36
|
-
// path String?
|
|
37
|
-
// props Json @default("{}")
|
|
38
|
-
// visitor String?
|
|
39
|
-
// createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
40
|
-
// @@index([siteId, name, createdAt])
|
|
41
|
-
// }
|
|
42
|
-
//
|
|
43
|
-
// model Salt {
|
|
44
|
-
// day String @id
|
|
45
|
-
// value String
|
|
46
|
-
// }
|
|
47
|
-
//
|
|
48
|
-
// model ViewDay {
|
|
49
|
-
// siteId String @default("")
|
|
50
|
-
// day DateTime @db.Date
|
|
51
|
-
// visitors Int
|
|
52
|
-
// pageviews Int
|
|
53
|
-
// visits Int
|
|
54
|
-
// bounces Int
|
|
55
|
-
// seconds Int
|
|
56
|
-
// pages Json @default("{}")
|
|
57
|
-
// entryPages Json @default("{}")
|
|
58
|
-
// sources Json @default("{}")
|
|
59
|
-
// countries Json @default("{}")
|
|
60
|
-
// devices Json @default("{}")
|
|
61
|
-
// @@id([siteId, day])
|
|
62
|
-
// }
|
|
17
|
+
// all supply in a line. The four tables it needs are Prisma models shipped in
|
|
18
|
+
// `prisma/analytics.prisma`, which the `prisma-models` bin copies into the
|
|
19
|
+
// app's schema folder, so `prisma db push` keeps them.
|
|
63
20
|
//
|
|
64
21
|
// `ViewDay` holds days a site counted somewhere else before it counted its
|
|
65
22
|
// own, one row a day with each breakdown as `{ value: visitors }`. Nothing
|
|
@@ -80,12 +37,14 @@ const KEEP_DAYS = 400;
|
|
|
80
37
|
const DAY_MS = 86_400_000;
|
|
81
38
|
/** A script that runs a page's JavaScript and says what it is. */
|
|
82
39
|
const BOT = /bot|crawl|spider|slurp|headless|lighthouse|prerender|preview/i;
|
|
83
|
-
/** iPadOS reports itself as a Mac, so its views count as a computer's. */
|
|
40
|
+
/** iPadOS reports itself as a Mac, so its views count as a computer's. @internal */
|
|
84
41
|
export const deviceOf = (ua) => /iPad|Tablet|Android(?!.*Mobile)/i.test(ua) ? 'tablet' : /Mobi|iPhone|Android/i.test(ua) ? 'phone' : 'computer';
|
|
85
42
|
const bareHost = (host) => host.toLowerCase().replace(/:\d+$/, '').replace(/^www\./, '');
|
|
86
43
|
/**
|
|
87
44
|
* Where a visit came from: its `utm_source` when the link carried one, else the
|
|
88
45
|
* referring host, and null for a referrer that is this site or none.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
89
48
|
*/
|
|
90
49
|
export function sourceOf(referrer, utm, host) {
|
|
91
50
|
if (typeof utm === 'string' && utm.trim())
|
|
@@ -100,7 +59,7 @@ export function sourceOf(referrer, utm, host) {
|
|
|
100
59
|
return null;
|
|
101
60
|
}
|
|
102
61
|
}
|
|
103
|
-
/** A browser path as stored: decoded, one leading slash, no trailing one. Null when it will not decode. */
|
|
62
|
+
/** A browser path as stored: decoded, one leading slash, no trailing one. Null when it will not decode. @internal */
|
|
104
63
|
export function normalisePath(path) {
|
|
105
64
|
try {
|
|
106
65
|
const p = decodeURIComponent(path.split(/[?#]/)[0].slice(0, 500));
|
|
@@ -136,7 +95,7 @@ async function visitorOf(sql, site, headers) {
|
|
|
136
95
|
}
|
|
137
96
|
/** Analytics must not be able to fail a response, so a failed write is logged and dropped. */
|
|
138
97
|
const quietly = (task) => task().then(() => undefined, (e) => console.error(`analytics: ${e.message}`));
|
|
139
|
-
/** Count one page view, unless a bot sent it. Never rejects. */
|
|
98
|
+
/** Count one page view, unless a bot sent it. Never rejects. @internal */
|
|
140
99
|
export function recordView(sql, { headers, path, referrer, utm, site = '' }) {
|
|
141
100
|
const ua = headers.get('user-agent') ?? '';
|
|
142
101
|
const page = normalisePath(path);
|
|
@@ -340,6 +299,8 @@ export function mcpCallProps(request, body, server) {
|
|
|
340
299
|
* event. The text is untrusted, so it is collapsed, lowercased for aggregation
|
|
341
300
|
* and truncated – the same 64-character bound search itself applies, which
|
|
342
301
|
* keeps the prop and the query that produced it the same string.
|
|
302
|
+
*
|
|
303
|
+
* @internal
|
|
343
304
|
*/
|
|
344
305
|
export function searchQueryProps({ query, results, surface, }) {
|
|
345
306
|
const q = (query ?? '').replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 64);
|
package/dist/base.css
CHANGED
|
@@ -40,6 +40,27 @@
|
|
|
40
40
|
.columns-layout > .column-view { align-self: stretch; }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* ---- the block views and the infobox ---------------------------------------
|
|
44
|
+
The structure three or four sites had each restated around this markup;
|
|
45
|
+
padding, gaps, radii and colour stay theirs. A stats block is two columns,
|
|
46
|
+
three or four from 40rem; a code-tab strip is a row of buttons; a banner sets
|
|
47
|
+
its icon beside its text; a long address in an infobox wraps. */
|
|
48
|
+
.stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
49
|
+
@media (min-width: 40rem) {
|
|
50
|
+
.stat-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
51
|
+
.stat-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
52
|
+
}
|
|
53
|
+
.stat-card { display: flex; flex-direction: column; }
|
|
54
|
+
.code-tabs { overflow: hidden; }
|
|
55
|
+
.code-tabs-list { display: flex; }
|
|
56
|
+
.code-tabs-btn { cursor: pointer; }
|
|
57
|
+
.editorial-banner { display: flex; }
|
|
58
|
+
.infobox { overflow-wrap: anywhere; }
|
|
59
|
+
.infobox-series { display: flex; flex-direction: column; }
|
|
60
|
+
.heading-anchor,
|
|
61
|
+
.ref-backlink,
|
|
62
|
+
.reference-link { text-decoration: none; }
|
|
63
|
+
|
|
43
64
|
/* ---- stored tab groups (activateTabGroups) --------------------------------
|
|
44
65
|
Only the active panel shows. Before the script runs, the first tab's content
|
|
45
66
|
stands in for the group rather than every tab at once. */
|
|
@@ -174,7 +195,7 @@ pre:hover > .code-copy-btn,
|
|
|
174
195
|
`TimeChart`: the canvas, a loading or empty state over it, and the range and
|
|
175
196
|
step buttons pinned to its top-left corner. A site gives the buttons and the
|
|
176
197
|
box their look; pressed is all this marks. */
|
|
177
|
-
.chart { position: relative; margin: 0; }
|
|
198
|
+
.chart { position: relative; margin: 0; overflow: hidden; }
|
|
178
199
|
/* Its own stacking context: the library's canvases stack themselves, and the buttons go over them. */
|
|
179
200
|
.chart-canvas { position: relative; z-index: 0; height: var(--chart-height, 15rem); }
|
|
180
201
|
/* The chart lays itself out in a <table>; a site's table styles are not for it. */
|
|
@@ -192,7 +213,7 @@ pre:hover > .code-copy-btn,
|
|
|
192
213
|
.chart-controls > div { display: flex; gap: 2px; }
|
|
193
214
|
/* A rule between groups: ranges, then steps, then the candles switch. */
|
|
194
215
|
.chart-controls > * + * { border-inline-start: 1px solid color-mix(in srgb, currentColor 25%, transparent); padding-inline-start: 0.5rem; }
|
|
195
|
-
.chart-controls :is(button, a) { font: inherit; color: inherit; text-decoration: none; background: none; cursor: pointer; opacity: 0.6; }
|
|
216
|
+
.chart-controls :is(button, a) { padding: 0.125rem 0.375rem; font: inherit; color: inherit; text-decoration: none; background: none; cursor: pointer; opacity: 0.6; }
|
|
196
217
|
/* An icon sits on the text's line, so an icon button is as tall as a labelled one. */
|
|
197
218
|
.chart-icon { display: inline-block; width: 1em; height: 1em; vertical-align: -0.125em; }
|
|
198
219
|
/* A button is pressed; a link (a stats page's window) is the page shown. */
|
package/dist/blocks.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type RelativeTimeStyle } from './freshness.js';
|
|
2
2
|
import type { BlockGroup } from './revisions.js';
|
|
3
|
+
/**
|
|
4
|
+
* The languages a code block or code tab offers, and the one a new one opens
|
|
5
|
+
* on. Two wikis carried this list character for character; a wiki with its own
|
|
6
|
+
* passes it to `createCodeBlock` and its tab editor.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CODE_LANGS: readonly ["javascript", "typescript", "css", "json", "bash", "python", "rust", "sql", "html", "xml", "jsx", "tsx", "markdown", "yaml", "toml"];
|
|
9
|
+
export declare const DEFAULT_LANG = "rust";
|
|
3
10
|
export interface CodeTab {
|
|
4
11
|
label: string;
|
|
5
12
|
language?: string;
|
|
@@ -137,14 +144,8 @@ export declare function someBlock<B>(blocks: readonly B[], predicate: (block: B)
|
|
|
137
144
|
* contains a fence cannot close this one early.
|
|
138
145
|
*/
|
|
139
146
|
export declare function codeTabsToMarkdown(tabs: readonly CodeTab[]): string;
|
|
140
|
-
/** A maintenance notice as a blockquote. `label` is the caller's display string. */
|
|
141
|
-
export declare function bannerToMarkdown(label: string, text?: string | null): string;
|
|
142
|
-
/** A numbered citation list, or `''` when there is nothing to cite. */
|
|
143
|
-
export declare function referencesToMarkdown(items: readonly ReferenceItem[], title?: string): string;
|
|
144
147
|
/** Metric cards as a bullet list, or `''` when empty. */
|
|
145
148
|
export declare function statsToMarkdown(items: readonly StatItem[]): string;
|
|
146
|
-
/** Grouped links under their headings, with an optional lead paragraph. */
|
|
147
|
-
export declare function linkGridToMarkdown(groups: readonly LinkGridGroup[], intro?: string | null): string;
|
|
148
149
|
/** A flat bullet list of links — resolved page lists, feed items, link rails. */
|
|
149
150
|
export declare function linkList(items: readonly LinkGridLink[]): string;
|
|
150
151
|
/**
|
package/dist/blocks.js
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
import { htmlToMarkdown, inlineToMarkdown } from './markdown.js';
|
|
14
14
|
import { relativeTime } from './freshness.js';
|
|
15
15
|
import { BANNER_LABELS, bannerToText, codeTabsToText, linkGridToText, pageListToText, referencesToText, statsToText, stripHtml, } from './text.js';
|
|
16
|
+
// ---- the shapes the standard block types carry ------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* The languages a code block or code tab offers, and the one a new one opens
|
|
19
|
+
* on. Two wikis carried this list character for character; a wiki with its own
|
|
20
|
+
* passes it to `createCodeBlock` and its tab editor.
|
|
21
|
+
*/
|
|
22
|
+
export const CODE_LANGS = ['javascript', 'typescript', 'css', 'json', 'bash', 'python', 'rust', 'sql', 'html', 'xml', 'jsx', 'tsx', 'markdown', 'yaml', 'toml'];
|
|
23
|
+
export const DEFAULT_LANG = 'rust';
|
|
16
24
|
export function pageRef(page, now, style = 'long') {
|
|
17
25
|
return {
|
|
18
26
|
title: page.title,
|
|
@@ -163,11 +171,11 @@ export function codeTabsToMarkdown(tabs) {
|
|
|
163
171
|
})
|
|
164
172
|
.join('\n\n');
|
|
165
173
|
}
|
|
166
|
-
/** A maintenance notice as a blockquote. `label` is the caller's display string. */
|
|
174
|
+
/** A maintenance notice as a blockquote. `label` is the caller's display string. @internal */
|
|
167
175
|
export function bannerToMarkdown(label, text) {
|
|
168
176
|
return `> **[${label}]**${text ? ` ${inlineToMarkdown(text)}` : ''}`;
|
|
169
177
|
}
|
|
170
|
-
/** A numbered citation list, or `''` when there is nothing to cite. */
|
|
178
|
+
/** A numbered citation list, or `''` when there is nothing to cite. @internal */
|
|
171
179
|
export function referencesToMarkdown(items, title = 'References') {
|
|
172
180
|
if (!items.length)
|
|
173
181
|
return '';
|
|
@@ -180,7 +188,7 @@ export function statsToMarkdown(items) {
|
|
|
180
188
|
return '';
|
|
181
189
|
return items.map(s => `- **${s.value}${s.suffix ?? ''}** — ${s.label}`).join('\n');
|
|
182
190
|
}
|
|
183
|
-
/** Grouped links under their headings, with an optional lead paragraph. */
|
|
191
|
+
/** Grouped links under their headings, with an optional lead paragraph. @internal */
|
|
184
192
|
export function linkGridToMarkdown(groups, intro) {
|
|
185
193
|
const lead = intro ? `${inlineToMarkdown(intro)}\n\n` : '';
|
|
186
194
|
const rendered = groups.map(g => [
|
package/dist/chart.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export type ChartResolution = 'hour' | '4h' | 'day';
|
|
|
18
18
|
export type ChartStep = 'day' | 'week' | 'month';
|
|
19
19
|
/** Points by resolution, from `from` (Unix seconds, 0 for all time), oldest first. */
|
|
20
20
|
export type ChartLoader = (resolution: ChartResolution, from: number) => Promise<readonly ChartPoint[]>;
|
|
21
|
+
/** Something that happened at a time, in Unix seconds, drawn on the point that holds it. */
|
|
22
|
+
export type ChartMarker = {
|
|
23
|
+
time: number;
|
|
24
|
+
label: string;
|
|
25
|
+
};
|
|
26
|
+
/** The range and step a reader has picked, for a page that keeps them in its URL. */
|
|
27
|
+
export type ChartView = {
|
|
28
|
+
range: ChartRange;
|
|
29
|
+
step: ChartStep | null;
|
|
30
|
+
};
|
|
21
31
|
/** Every range, shortest first. A chart fed daily points offers the ones from 30D up. */
|
|
22
32
|
export declare const CHART_RANGES: readonly ChartRange[];
|
|
23
33
|
/** A step is offered once it makes four points, and the default is the finest that makes 200 or fewer. */
|
|
@@ -25,13 +35,6 @@ export declare function chartSteps(span: number): {
|
|
|
25
35
|
steps: ChartStep[];
|
|
26
36
|
fallback: ChartStep;
|
|
27
37
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Points, oldest first, one per step. A price keeps the last value in each
|
|
30
|
-
* step, as a candle does its close: the first open, the highest high, the
|
|
31
|
-
* lowest low. A count takes the mean of its days, so the week in progress does
|
|
32
|
-
* not read as a collapse. Volume is summed.
|
|
33
|
-
*/
|
|
34
|
-
export declare function bucketChart(points: readonly ChartPoint[], step: ChartStep, aggregate: 'last' | 'mean'): ChartPoint[];
|
|
35
38
|
/** Compact from a thousand up, four significant figures below. */
|
|
36
39
|
export declare const formatChartValue: (n: number) => string;
|
|
37
40
|
export interface TimeChartProps {
|
|
@@ -58,6 +61,12 @@ export interface TimeChartProps {
|
|
|
58
61
|
* window links, which load another digest rather than slice this one.
|
|
59
62
|
*/
|
|
60
63
|
controls?: ReactNode;
|
|
64
|
+
/** Events drawn on the points that hold them. */
|
|
65
|
+
markers?: readonly ChartMarker[];
|
|
66
|
+
/** The step shown first, where the range can make it. */
|
|
67
|
+
step?: ChartStep;
|
|
68
|
+
/** Called with the range and step on mount and on every change. */
|
|
69
|
+
onView?: (view: ChartView) => void;
|
|
61
70
|
className?: string;
|
|
62
71
|
}
|
|
63
72
|
/**
|
|
@@ -65,4 +74,4 @@ export interface TimeChartProps {
|
|
|
65
74
|
* `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
|
|
66
75
|
* and the box their look; the line and fill are the canvas box's `color`.
|
|
67
76
|
*/
|
|
68
|
-
export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, controls, className, }: TimeChartProps): import("react").JSX.Element;
|
|
77
|
+
export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, controls, markers, step: initialStep, onView, className, }: TimeChartProps): import("react").JSX.Element;
|
package/dist/chart.js
CHANGED
|
@@ -8,7 +8,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// loaded on mount, so the page paints without it. Every colour is the canvas
|
|
9
9
|
// box's own `color`, its axis `--chart-axis`, and a candle `--chart-up` or
|
|
10
10
|
// `--chart-down`, each if set. A count's line is curved; a price's is straight,
|
|
11
|
-
// since a curve between two closes draws prices nobody paid.
|
|
11
|
+
// since a curve between two closes draws prices nobody paid. Events are markers
|
|
12
|
+
// on the point they fall in, so a spike carries the reason for it.
|
|
12
13
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
14
|
const DAY = 86_400;
|
|
14
15
|
const RANGE = {
|
|
@@ -49,6 +50,8 @@ function bucketStart(time, step) {
|
|
|
49
50
|
* step, as a candle does its close: the first open, the highest high, the
|
|
50
51
|
* lowest low. A count takes the mean of its days, so the week in progress does
|
|
51
52
|
* not read as a collapse. Volume is summed.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
52
55
|
*/
|
|
53
56
|
export function bucketChart(points, step, aggregate) {
|
|
54
57
|
const out = [];
|
|
@@ -74,6 +77,31 @@ export function bucketChart(points, step, aggregate) {
|
|
|
74
77
|
}
|
|
75
78
|
return out;
|
|
76
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Markers on the points drawn: each on the last point at or before it, so one
|
|
82
|
+
* mid-week sits on its week. Markers on one point share it, labels joined; one
|
|
83
|
+
* before the first point or after the last point's step is left off.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export function placeMarkers(markers, shown, step) {
|
|
88
|
+
const first = shown[0];
|
|
89
|
+
const last = shown.at(-1);
|
|
90
|
+
if (!first || !last)
|
|
91
|
+
return [];
|
|
92
|
+
const end = last.time + (step ? STEP[step].span : DAY);
|
|
93
|
+
const placed = new Map();
|
|
94
|
+
for (const m of [...markers].sort((a, b) => a.time - b.time)) {
|
|
95
|
+
if (m.time < first.time || m.time >= end)
|
|
96
|
+
continue;
|
|
97
|
+
let i = shown.length - 1;
|
|
98
|
+
while (i > 0 && shown[i].time > m.time)
|
|
99
|
+
i--;
|
|
100
|
+
const time = shown[i].time;
|
|
101
|
+
placed.set(time, [...(placed.get(time) ?? []), m.label]);
|
|
102
|
+
}
|
|
103
|
+
return [...placed].map(([time, labels]) => ({ time, label: labels.join(' · ') }));
|
|
104
|
+
}
|
|
77
105
|
const isCandle = (p) => p.open !== undefined && p.high !== undefined && p.low !== undefined;
|
|
78
106
|
const compact = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 });
|
|
79
107
|
const precise = new Intl.NumberFormat('en', { maximumSignificantDigits: 4 });
|
|
@@ -102,14 +130,16 @@ const fromZeroScale = (base) => {
|
|
|
102
130
|
* `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
|
|
103
131
|
* and the box their look; the line and fill are the canvas box's `color`.
|
|
104
132
|
*/
|
|
105
|
-
export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, candles: initialCandles = false, height, controls, className, }) {
|
|
133
|
+
export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, candles: initialCandles = false, height, controls, markers, step: initialStep, onView, className, }) {
|
|
106
134
|
const box = useRef(null);
|
|
107
135
|
const chart = useRef(null);
|
|
108
136
|
const formatRef = useRef(format);
|
|
109
137
|
formatRef.current = format;
|
|
138
|
+
const onViewRef = useRef(onView);
|
|
139
|
+
onViewRef.current = onView;
|
|
110
140
|
const [ready, setReady] = useState(false);
|
|
111
141
|
const [range, setRange] = useState(initial);
|
|
112
|
-
const [step, setStep] = useState(null);
|
|
142
|
+
const [step, setStep] = useState(initialStep ?? null);
|
|
113
143
|
const [candles, setCandles] = useState(initialCandles);
|
|
114
144
|
const [loaded, setLoaded] = useState(null);
|
|
115
145
|
const resolution = RANGE[range].resolution;
|
|
@@ -118,7 +148,7 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
118
148
|
if (!el)
|
|
119
149
|
return;
|
|
120
150
|
let disposed = false;
|
|
121
|
-
import('lightweight-charts').then(({ createChart, AreaSeries, CandlestickSeries, HistogramSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
|
|
151
|
+
import('lightweight-charts').then(({ createChart, createSeriesMarkers, AreaSeries, CandlestickSeries, HistogramSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
|
|
122
152
|
if (disposed)
|
|
123
153
|
return;
|
|
124
154
|
const style = getComputedStyle(el);
|
|
@@ -177,7 +207,11 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
177
207
|
api.panes()[1]?.setStretchFactor(1);
|
|
178
208
|
return volume;
|
|
179
209
|
};
|
|
180
|
-
chart.current = {
|
|
210
|
+
chart.current = {
|
|
211
|
+
api, area, candle, volume: null, addVolume, up, down, ink,
|
|
212
|
+
areaMarkers: createSeriesMarkers(area, []),
|
|
213
|
+
candleMarkers: createSeriesMarkers(candle, []),
|
|
214
|
+
};
|
|
181
215
|
setReady(true);
|
|
182
216
|
});
|
|
183
217
|
return () => {
|
|
@@ -259,8 +293,21 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
259
293
|
const rising = p.value >= (p.open ?? shown[i - 1]?.value ?? p.value);
|
|
260
294
|
return { time: time(p), value: p.volume ?? 0, color: (rising ? c.up : c.down).replace(/[\d.]+\)$/, '0.5)') };
|
|
261
295
|
}));
|
|
296
|
+
const drawn = placeMarkers(markers ?? [], shown, view?.step ?? null).map(m => ({
|
|
297
|
+
time: time(m),
|
|
298
|
+
position: 'aboveBar',
|
|
299
|
+
shape: 'arrowDown',
|
|
300
|
+
color: c.ink,
|
|
301
|
+
text: m.label,
|
|
302
|
+
}));
|
|
303
|
+
c.areaMarkers.setMarkers(asCandles ? [] : drawn);
|
|
304
|
+
c.candleMarkers.setMarkers(asCandles ? drawn : []);
|
|
262
305
|
c.api.timeScale().fitContent();
|
|
263
|
-
}, [ready, source, view, candles]);
|
|
306
|
+
}, [ready, source, view, candles, markers]);
|
|
307
|
+
const shownStep = view?.step ?? null;
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
onViewRef.current?.({ range, step: shownStep });
|
|
310
|
+
}, [range, shownStep]);
|
|
264
311
|
// The steps always show where a range is drawn by day, so the buttons do not jump about; one
|
|
265
312
|
// the range cannot make (any, under 24H and 7D) is disabled.
|
|
266
313
|
const daily = ranges.some(r => RANGE[r].resolution === 'day');
|
package/dist/combobox.d.ts
CHANGED
|
@@ -43,10 +43,6 @@ export interface ComboboxAria {
|
|
|
43
43
|
listProps: ComboboxListProps;
|
|
44
44
|
optionProps: (index: number) => ComboboxOptionProps;
|
|
45
45
|
}
|
|
46
|
-
/** The id of one option row. Exported because a caller that scrolls the
|
|
47
|
-
* highlighted row into view has to find it by the same id the input points at. */
|
|
48
|
-
export declare function optionId(baseId: string, listKey: string | undefined, index: number): string;
|
|
49
|
-
export declare function listId(baseId: string, listKey?: string): string;
|
|
50
46
|
/**
|
|
51
47
|
* Every attribute the three parts of a combobox need, derived from what the
|
|
52
48
|
* typeahead already knows.
|
package/dist/combobox.js
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
// Enter does.
|
|
13
13
|
/** The id of one option row. Exported because a caller that scrolls the
|
|
14
14
|
* highlighted row into view has to find it by the same id the input points at. */
|
|
15
|
+
/** @internal */
|
|
15
16
|
export function optionId(baseId, listKey, index) {
|
|
16
17
|
return `${listId(baseId, listKey)}-opt-${index}`;
|
|
17
18
|
}
|
|
19
|
+
/** @internal */
|
|
18
20
|
export function listId(baseId, listKey) {
|
|
19
21
|
return listKey ? `${baseId}-${listKey}` : `${baseId}-list`;
|
|
20
22
|
}
|
package/dist/crawlers.d.ts
CHANGED
|
@@ -12,15 +12,6 @@ export interface AiCrawler {
|
|
|
12
12
|
*/
|
|
13
13
|
fetches: boolean;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Order is significant: `detectAiBot` returns the first token the user agent
|
|
17
|
-
* contains, so a token that is a substring of another must come after it.
|
|
18
|
-
*/
|
|
19
|
-
export declare const AI_CRAWLERS: readonly AiCrawler[];
|
|
20
|
-
/** The event label for a user agent, or null when it is not a known crawler. */
|
|
21
|
-
export declare function detectAiBot(userAgent: string | null | undefined): string | null;
|
|
22
|
-
/** Every token that needs its own robots.txt group — which is all of them. */
|
|
23
|
-
export declare function aiCrawlerTokens(): string[];
|
|
24
15
|
/**
|
|
25
16
|
* Structurally what Next's `MetadataRoute.Robots['rules']` wants, without
|
|
26
17
|
* importing Next: this package has no runtime dependencies and is not going to
|
|
@@ -31,21 +22,6 @@ export interface RobotsGroup {
|
|
|
31
22
|
allow?: string | string[];
|
|
32
23
|
disallow?: string | string[];
|
|
33
24
|
}
|
|
34
|
-
/**
|
|
35
|
-
* The agent surface an S10 origin serves, allowed in every group. All three
|
|
36
|
-
* `robots.ts` files listed these by hand, and a path missing from one list is
|
|
37
|
-
* an endpoint the origin advertises and then closes to the callers it named.
|
|
38
|
-
*/
|
|
39
|
-
export declare const AGENT_SURFACE_PATHS: readonly string[];
|
|
40
|
-
/**
|
|
41
|
-
* The React Server Components payload a `<Link>` prefetches. A crawler that
|
|
42
|
-
* renders a page runs those prefetches too — 43% of Googlebot's requests on
|
|
43
|
-
* one wiki, against 10% for HTML — and nothing that reads robots.txt can use
|
|
44
|
-
* one. Every group gets it.
|
|
45
|
-
*/
|
|
46
|
-
export declare const RSC_DISALLOW: readonly string[];
|
|
47
|
-
/** The search engines that get the HTML site and none of its twins. */
|
|
48
|
-
export declare const SEARCH_ENGINES: readonly string[];
|
|
49
25
|
/**
|
|
50
26
|
* Every group robots.txt needs: the wildcard, one per AI crawler, one per
|
|
51
27
|
* search engine.
|
package/dist/crawlers.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Order is significant: `detectAiBot` returns the first token the user agent
|
|
16
16
|
* contains, so a token that is a substring of another must come after it.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
17
19
|
*/
|
|
18
20
|
export const AI_CRAWLERS = [
|
|
19
21
|
{ token: 'GPTBot', label: 'GPTBot', fetches: true },
|
|
@@ -39,7 +41,7 @@ export const AI_CRAWLERS = [
|
|
|
39
41
|
{ token: 'DuckAssistBot', label: 'DuckAssistBot', fetches: true },
|
|
40
42
|
{ token: 'Applebot-Extended', label: 'AppleExtended', fetches: false },
|
|
41
43
|
];
|
|
42
|
-
/** The event label for a user agent, or null when it is not a known crawler. */
|
|
44
|
+
/** The event label for a user agent, or null when it is not a known crawler. @internal */
|
|
43
45
|
export function detectAiBot(userAgent) {
|
|
44
46
|
if (!userAgent)
|
|
45
47
|
return null;
|
|
@@ -49,7 +51,7 @@ export function detectAiBot(userAgent) {
|
|
|
49
51
|
}
|
|
50
52
|
return null;
|
|
51
53
|
}
|
|
52
|
-
/** Every token that needs its own robots.txt group — which is all of them. */
|
|
54
|
+
/** Every token that needs its own robots.txt group — which is all of them. @internal */
|
|
53
55
|
export function aiCrawlerTokens() {
|
|
54
56
|
return AI_CRAWLERS.map(c => c.token);
|
|
55
57
|
}
|
|
@@ -57,6 +59,8 @@ export function aiCrawlerTokens() {
|
|
|
57
59
|
* The agent surface an S10 origin serves, allowed in every group. All three
|
|
58
60
|
* `robots.ts` files listed these by hand, and a path missing from one list is
|
|
59
61
|
* an endpoint the origin advertises and then closes to the callers it named.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
60
64
|
*/
|
|
61
65
|
export const AGENT_SURFACE_PATHS = [
|
|
62
66
|
'/api/mcp',
|
|
@@ -71,9 +75,11 @@ export const AGENT_SURFACE_PATHS = [
|
|
|
71
75
|
* renders a page runs those prefetches too — 43% of Googlebot's requests on
|
|
72
76
|
* one wiki, against 10% for HTML — and nothing that reads robots.txt can use
|
|
73
77
|
* one. Every group gets it.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
74
80
|
*/
|
|
75
81
|
export const RSC_DISALLOW = ['/*?_rsc=', '/*&_rsc='];
|
|
76
|
-
/** The search engines that get the HTML site and none of its twins. */
|
|
82
|
+
/** The search engines that get the HTML site and none of its twins. @internal */
|
|
77
83
|
export const SEARCH_ENGINES = ['Googlebot', 'Bingbot'];
|
|
78
84
|
/**
|
|
79
85
|
* Every group robots.txt needs: the wildcard, one per AI crawler, one per
|
package/dist/dom.d.ts
CHANGED
|
@@ -17,11 +17,6 @@ export interface CopyButtonOptions {
|
|
|
17
17
|
* Returns the number of buttons added, so a caller can skip work when zero.
|
|
18
18
|
*/
|
|
19
19
|
export declare function addCopyButtons(root: ParentNode, options?: CopyButtonOptions): number;
|
|
20
|
-
/**
|
|
21
|
-
* The one place this origin is written down. It is both the embed host and the
|
|
22
|
-
* allow-list the resize listener checks.
|
|
23
|
-
*/
|
|
24
|
-
export declare const TWITTER_ORIGIN = "https://platform.twitter.com";
|
|
25
20
|
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
|
26
21
|
export declare const tweetEmbedSrc: (tweetId: string | number) => string;
|
|
27
22
|
/**
|
package/dist/dom.js
CHANGED
|
@@ -56,6 +56,8 @@ export function addCopyButtons(root, options = {}) {
|
|
|
56
56
|
/**
|
|
57
57
|
* The one place this origin is written down. It is both the embed host and the
|
|
58
58
|
* allow-list the resize listener checks.
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
59
61
|
*/
|
|
60
62
|
export const TWITTER_ORIGIN = 'https://platform.twitter.com';
|
|
61
63
|
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
package/dist/editor.d.ts
CHANGED
|
@@ -15,19 +15,6 @@ 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
|
-
/**
|
|
19
|
-
* Tiptap's Link, storing an href and the `link` class and nothing else.
|
|
20
|
-
*
|
|
21
|
-
* Stock Link writes `target="_blank" rel="noopener noreferrer nofollow"` onto
|
|
22
|
-
* every link, internal ones included, and the schema then reads both back from
|
|
23
|
-
* stored HTML, so a page edited once kept them for good. Nofollow on a site's
|
|
24
|
-
* own links throws away its link equity, and a new tab for in-site navigation
|
|
25
|
-
* is a bug. Whether a link is external depends on the host it is served from,
|
|
26
|
-
* which the editor cannot know, so the render path decides: `normaliseLinks`
|
|
27
|
-
* from `wiki-formant/links`. Dropping both attributes from the mark also cleans
|
|
28
|
-
* a legacy link the next time its page is saved.
|
|
29
|
-
*/
|
|
30
|
-
export declare const wikiLink: import("@tiptap/core").Mark<import("@tiptap/extension-link").LinkOptions, any>;
|
|
31
18
|
export interface WikiEditorExtensionOptions {
|
|
32
19
|
/** Empty-document prompt. */
|
|
33
20
|
placeholder?: string;
|
|
@@ -41,20 +28,6 @@ export interface WikiEditorExtensionOptions {
|
|
|
41
28
|
*/
|
|
42
29
|
nodes?: readonly AnyExtension[];
|
|
43
30
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Turn a pasted URL into the richest node that fits it, falling back to a bare
|
|
46
|
-
* iframe. Order matters: a YouTube URL is also a valid iframe source, so the
|
|
47
|
-
* specific cases have to be tried before the general one.
|
|
48
|
-
*
|
|
49
|
-
* A shortened map link inserts IMMEDIATELY with `about:blank` and swaps its
|
|
50
|
-
* `src` once the redirect resolves. Pasting must not block on a network hop,
|
|
51
|
-
* and the node has to already exist for the reader to see anything happen. The
|
|
52
|
-
* swap re-finds the node by its `url` attribute rather than caching a position,
|
|
53
|
-
* because every keystroke between the paste and the resolve moves it.
|
|
54
|
-
*/
|
|
55
|
-
export declare function insertEmbed(editor: Editor, url: string, { resolveMapUrl }: {
|
|
56
|
-
resolveMapUrl: (url: string) => Promise<string | null>;
|
|
57
|
-
}): void;
|
|
58
31
|
/** The table controls, shown only while the selection is inside a table. */
|
|
59
32
|
export declare const TABLE_ACTIONS: ReadonlyArray<[command: string, label: string, danger?: boolean]>;
|
|
60
33
|
/** `editor.isActive` as a toolbar button's `active` descriptor expresses it. */
|
|
@@ -71,13 +44,6 @@ export interface ToolbarAction {
|
|
|
71
44
|
run?: (editor: Editor) => void;
|
|
72
45
|
withUrl?: (editor: Editor, url: string) => void;
|
|
73
46
|
}
|
|
74
|
-
/**
|
|
75
|
-
* The formatting commands, labelled. Three toolbars ran these identically and
|
|
76
|
-
* two titled their buttons with the internal key — a screen reader said
|
|
77
|
-
* "codeBlock", and inline code and code block shared one icon and one name.
|
|
78
|
-
* Upload and embed stay with the caller: they are the parts that differ.
|
|
79
|
-
*/
|
|
80
|
-
export declare const TOOLBAR_ACTIONS: readonly ToolbarAction[];
|
|
81
47
|
/** The actions a toolbar shows, in the order it shows them. */
|
|
82
48
|
export declare function toolbarActions(keys: readonly ToolbarKey[]): ToolbarAction[];
|
|
83
49
|
/**
|
|
@@ -92,15 +58,6 @@ export declare function ToolbarButton({ label, pressed, onPress, className, chil
|
|
|
92
58
|
children: ReactNode;
|
|
93
59
|
}): import("react").JSX.Element;
|
|
94
60
|
export type UrlPromptKind = 'link' | 'embed';
|
|
95
|
-
/**
|
|
96
|
-
* What a link or embed field does with its address. A link sets or replaces
|
|
97
|
-
* the link the cursor is in, across the whole link rather than the collapsed
|
|
98
|
-
* selection, and an emptied address takes it off while keeping the text. An
|
|
99
|
-
* embed with no address does nothing.
|
|
100
|
-
*/
|
|
101
|
-
export declare function applyUrlPrompt(editor: Editor, kind: UrlPromptKind, url: string, { resolveMapUrl }: {
|
|
102
|
-
resolveMapUrl: (url: string) => Promise<string | null>;
|
|
103
|
-
}): void;
|
|
104
61
|
export interface UrlPromptState {
|
|
105
62
|
/** The open field, or null. */
|
|
106
63
|
kind: UrlPromptKind | null;
|