wiki-formant 0.50.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.
- package/README.md +84 -23
- package/bin/_cli.mjs +50 -0
- package/bin/analytics-digest.mjs +4 -23
- package/bin/check-pages.mjs +6 -20
- package/bin/fts-ddl.mjs +4 -19
- package/bin/indexnow.mjs +4 -6
- package/bin/maintenance-log.mjs +6 -31
- package/bin/mark-verified.mjs +5 -30
- package/bin/passkey-invite.mjs +9 -19
- package/bin/prisma-models.mjs +40 -5
- package/dist/base.css +25 -0
- package/dist/block-editor.d.ts +203 -0
- package/dist/block-editor.js +218 -0
- package/dist/block-views.js +2 -3
- package/dist/blocks.d.ts +67 -0
- package/dist/crawlers.d.ts +2 -2
- package/dist/db.d.ts +25 -0
- package/dist/db.js +32 -1
- package/dist/editor.d.ts +61 -0
- package/dist/editor.js +29 -1
- package/dist/headers.d.ts +27 -0
- package/dist/headers.js +32 -1
- package/dist/headings.d.ts +10 -0
- package/dist/headings.js +12 -0
- package/dist/history.js +1 -1
- package/dist/http.d.ts +7 -2
- package/dist/http.js +7 -2
- package/dist/indexnow.d.ts +7 -0
- package/dist/indexnow.js +22 -3
- package/dist/maintenance.js +2 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +3 -4
- package/dist/sanitize.js +1 -1
- package/dist/stats.d.ts +28 -0
- package/dist/stats.js +18 -1
- package/dist/tiptap.js +2 -4
- package/package.json +18 -83
package/bin/prisma-models.mjs
CHANGED
|
@@ -16,21 +16,56 @@
|
|
|
16
16
|
// npx prisma-models --check # exit 1 if either is missing or differs
|
|
17
17
|
// npx prisma-models analytics # only the ones named
|
|
18
18
|
// npx prisma-models --dir db/schema # another folder
|
|
19
|
+
//
|
|
20
|
+
// An app that keeps one schema file and hand-writes the models into it, with a
|
|
21
|
+
// relation the shipped ones cannot carry, checks them in place instead.
|
|
22
|
+
// `--tenant` lets each model's `siteId` drop its default and gain a relation:
|
|
23
|
+
//
|
|
24
|
+
// npx prisma-models analytics --check --in prisma/schema.prisma --tenant
|
|
19
25
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
20
26
|
import { join } from 'node:path';
|
|
27
|
+
import { arg } from './_cli.mjs';
|
|
21
28
|
|
|
22
29
|
const MODELS = ['analytics', 'passkey'];
|
|
23
30
|
const args = process.argv.slice(2);
|
|
24
31
|
const check = args.includes('--check');
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
32
|
+
const dir = arg('--dir') ?? 'prisma/schema';
|
|
33
|
+
const into = arg('--in');
|
|
34
|
+
const tenant = args.includes('--tenant');
|
|
35
|
+
const named = args.filter((a, i) => !a.startsWith('--') && !['--dir', '--in'].includes(args[i - 1]));
|
|
28
36
|
const unknown = named.filter(n => !MODELS.includes(n));
|
|
29
37
|
|
|
30
|
-
if (unknown.length || !dir) {
|
|
31
|
-
console.error(`usage: prisma-models [${MODELS.join('|')}…] [--dir <folder>] [--check]`);
|
|
38
|
+
if (unknown.length || !dir || (into !== undefined && !check)) {
|
|
39
|
+
console.error(`usage: prisma-models [${MODELS.join('|')}…] [--dir <folder>] [--check [--in <schema file> [--tenant]]]`);
|
|
32
40
|
process.exit(2);
|
|
33
41
|
}
|
|
42
|
+
|
|
43
|
+
// A model as `name → its lines`, comments, blank lines and spacing dropped.
|
|
44
|
+
const models = text =>
|
|
45
|
+
new Map(
|
|
46
|
+
[...text.matchAll(/^model\s+(\w+)\s*\{[ \t]*$([\s\S]*?)^\}/gm)].map(([, name, body]) => [
|
|
47
|
+
name,
|
|
48
|
+
body
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map(l => l.replace(/\/\/.*/, '').trim().replace(/\s+/g, ' '))
|
|
51
|
+
.filter(l => l && !(tenant && /^\w+ \w+ @relation\(fields: \[siteId\]/.test(l)))
|
|
52
|
+
.map(l => (tenant ? l.replace(/^siteId String @default\(""\)$/, 'siteId String') : l))
|
|
53
|
+
.join('\n'),
|
|
54
|
+
]),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (into !== undefined) {
|
|
58
|
+
const local = models(readFileSync(into, 'utf8'));
|
|
59
|
+
let drift = 0;
|
|
60
|
+
for (const name of named.length ? named : MODELS) {
|
|
61
|
+
for (const [model, body] of models(readFileSync(new URL(`../prisma/${name}.prisma`, import.meta.url), 'utf8'))) {
|
|
62
|
+
if (local.get(model) === body) continue;
|
|
63
|
+
drift++;
|
|
64
|
+
console.error(`${into}: model ${model} ${local.has(model) ? 'differs from' : 'is missing from'} wiki-formant's prisma/${name}.prisma`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.exit(drift ? 1 : 0);
|
|
68
|
+
}
|
|
34
69
|
if (!existsSync(dir)) {
|
|
35
70
|
console.error(`prisma-models: no ${dir}/ here. Move prisma/schema.prisma into it and point prisma.config.ts's schema at the folder.`);
|
|
36
71
|
process.exit(2);
|
package/dist/base.css
CHANGED
|
@@ -40,6 +40,31 @@
|
|
|
40
40
|
.columns-layout > .column-view { align-self: stretch; }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* ---- layout primitives ------------------------------------------------------
|
|
44
|
+
A column, a row and a row with its ends pushed apart. radix-wiki and caper
|
|
45
|
+
wrote these with the same gaps, and acuiq2 the column; a site that meant
|
|
46
|
+
something else by a name renamed its own. The gaps are the ones all of them
|
|
47
|
+
used, so they stay here rather than going back to every stylesheet. */
|
|
48
|
+
.stack, .stack-xs, .stack-sm, .stack-lg { display: flex; flex-direction: column; gap: 1rem; }
|
|
49
|
+
.stack-xs { gap: 0.25rem; }
|
|
50
|
+
.stack-sm { gap: 0.5rem; }
|
|
51
|
+
.stack-lg { gap: 1.5rem; }
|
|
52
|
+
.row, .row-xs, .row-md, .row-lg { display: flex; align-items: center; gap: 0.5rem; }
|
|
53
|
+
.row-xs { gap: 0.25rem; }
|
|
54
|
+
.row-md { gap: 0.75rem; }
|
|
55
|
+
.row-lg { gap: 1rem; }
|
|
56
|
+
.spread { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
57
|
+
.badge { display: inline-flex; align-items: center; border-radius: 9999px; }
|
|
58
|
+
|
|
59
|
+
/* A page whose first block is an infobox, one column with the infobox on top.
|
|
60
|
+
Where it splits into prose and a rail is the site's: a viewport width on one,
|
|
61
|
+
its column's width on another. Both items need `min-width: 0`, or the one
|
|
62
|
+
shared track sizes to the widest item's min-content and the other inherits
|
|
63
|
+
the overflow. */
|
|
64
|
+
.page-with-infobox { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "infobox" "content"; }
|
|
65
|
+
.page-with-infobox > .page-main-content { grid-area: content; min-width: 0; }
|
|
66
|
+
.page-with-infobox > .infobox { grid-area: infobox; min-width: 0; }
|
|
67
|
+
|
|
43
68
|
/* ---- the block views and the infobox ---------------------------------------
|
|
44
69
|
The structure three or four sites had each restated around this markup;
|
|
45
70
|
padding, gaps, radii and colour stay theirs. A stats block is two columns,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { type ComponentType, type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { type BannerBlock, type CodeTabsBlock, type ColumnsBlock, type InfoboxBlock, type LinkGridBlock, type ReferencesBlock, type StatsBlock } from './blocks.js';
|
|
3
|
+
export interface BlockEditorClassNames {
|
|
4
|
+
/** A page's block list. */
|
|
5
|
+
list?: string;
|
|
6
|
+
/** A column's or an infobox's. */
|
|
7
|
+
nestedList?: string;
|
|
8
|
+
/** The line an empty list shows. */
|
|
9
|
+
empty?: string;
|
|
10
|
+
/** A block. It carries `data-selected` while it is the one being edited, and `data-nested` inside a container. */
|
|
11
|
+
card?: string;
|
|
12
|
+
cardHead?: string;
|
|
13
|
+
cardLabel?: string;
|
|
14
|
+
cardActions?: string;
|
|
15
|
+
/** Every icon button: a card's actions, add-below, remove. */
|
|
16
|
+
action?: string;
|
|
17
|
+
/** The Add block button at the foot of a list. */
|
|
18
|
+
addBlock?: string;
|
|
19
|
+
/** The default `Add` button: Add tab, Add link. */
|
|
20
|
+
add?: string;
|
|
21
|
+
/** The default `Menu`'s wrapper and panel. */
|
|
22
|
+
menuAnchor?: string;
|
|
23
|
+
menu?: string;
|
|
24
|
+
menuItem?: string;
|
|
25
|
+
menuText?: string;
|
|
26
|
+
menuHint?: string;
|
|
27
|
+
/** A vertical group of fields. */
|
|
28
|
+
stack?: string;
|
|
29
|
+
/** A row of inputs ending in a remove button. */
|
|
30
|
+
row?: string;
|
|
31
|
+
/** A group inside a block: a link group, a column. */
|
|
32
|
+
nest?: string;
|
|
33
|
+
/** What holds the columns of a columns block. */
|
|
34
|
+
columns?: string;
|
|
35
|
+
/** The default `Field`'s label. */
|
|
36
|
+
field?: string;
|
|
37
|
+
input?: string;
|
|
38
|
+
/** Added to a code tab's language select. */
|
|
39
|
+
lang?: string;
|
|
40
|
+
/** Added to a code tab's textarea. */
|
|
41
|
+
code?: string;
|
|
42
|
+
/** Added to a stat's figure input. */
|
|
43
|
+
figure?: string;
|
|
44
|
+
/** Added to a count input. */
|
|
45
|
+
number?: string;
|
|
46
|
+
/** Row numbers, hints and notes. */
|
|
47
|
+
muted?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface BlockEditorIcons {
|
|
50
|
+
add: ReactNode;
|
|
51
|
+
up: ReactNode;
|
|
52
|
+
down: ReactNode;
|
|
53
|
+
duplicate: ReactNode;
|
|
54
|
+
remove: ReactNode;
|
|
55
|
+
}
|
|
56
|
+
export interface FieldProps {
|
|
57
|
+
label: string;
|
|
58
|
+
hint?: string;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}
|
|
61
|
+
/** One of a few values: a toggle group, a chip row or a select. */
|
|
62
|
+
export interface ChoiceProps {
|
|
63
|
+
label: string;
|
|
64
|
+
value: string;
|
|
65
|
+
options: ReadonlyArray<{
|
|
66
|
+
value: string;
|
|
67
|
+
label: string;
|
|
68
|
+
}>;
|
|
69
|
+
onChange: (value: string) => void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The add-block menu around its trigger. `trigger` is the button the editor
|
|
73
|
+
* drew; a popover takes it as its anchor, an inline panel renders it first.
|
|
74
|
+
*/
|
|
75
|
+
export interface MenuProps {
|
|
76
|
+
open: boolean;
|
|
77
|
+
onOpenChange: (open: boolean) => void;
|
|
78
|
+
trigger: ReactElement;
|
|
79
|
+
/** The icon in a block's head that adds under it, rather than the list's foot button. */
|
|
80
|
+
below: boolean;
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
}
|
|
83
|
+
export interface AddProps {
|
|
84
|
+
onPress: () => void;
|
|
85
|
+
children: ReactNode;
|
|
86
|
+
}
|
|
87
|
+
export interface BlockEditorKit {
|
|
88
|
+
classNames: BlockEditorClassNames;
|
|
89
|
+
icons: BlockEditorIcons;
|
|
90
|
+
Field?: ComponentType<FieldProps>;
|
|
91
|
+
Choice?: ComponentType<ChoiceProps>;
|
|
92
|
+
Menu?: ComponentType<MenuProps>;
|
|
93
|
+
Add?: ComponentType<AddProps>;
|
|
94
|
+
}
|
|
95
|
+
export type EditableBlock = {
|
|
96
|
+
id: string;
|
|
97
|
+
type: string;
|
|
98
|
+
};
|
|
99
|
+
export interface BlockMeta {
|
|
100
|
+
label: string;
|
|
101
|
+
icon?: ReactNode;
|
|
102
|
+
/** The line under the label in the add menu. */
|
|
103
|
+
hint?: string;
|
|
104
|
+
}
|
|
105
|
+
export interface BlockSchema<B extends EditableBlock> {
|
|
106
|
+
/** Undefined for a type this build does not know: it is shown by its raw type and kept as it is. */
|
|
107
|
+
meta: (type: B['type']) => BlockMeta | undefined;
|
|
108
|
+
create: (type: B['type']) => B;
|
|
109
|
+
/** A copy with fresh ids at every level. */
|
|
110
|
+
duplicate: (block: B) => B;
|
|
111
|
+
/**
|
|
112
|
+
* A block's fields. The wiki's own `switch`, so it stays exhaustive over its
|
|
113
|
+
* union. Null for a type with no fields here: the card says it is kept as is.
|
|
114
|
+
*/
|
|
115
|
+
renderBody: (block: B, onUpdate: (next: B) => void) => ReactNode;
|
|
116
|
+
/** What a page's add menus offer, in their order. */
|
|
117
|
+
types: readonly B['type'][];
|
|
118
|
+
/** What a column's and an infobox's offer: never a container. */
|
|
119
|
+
nestedTypes: readonly B['type'][];
|
|
120
|
+
/** Types a list holds at most one of, such as an infobox an article lifts into its rail. */
|
|
121
|
+
single?: readonly B['type'][];
|
|
122
|
+
}
|
|
123
|
+
export interface BlockEditorProps<B extends EditableBlock> {
|
|
124
|
+
blocks: B[];
|
|
125
|
+
onChange: (blocks: B[]) => void;
|
|
126
|
+
schema: BlockSchema<B>;
|
|
127
|
+
kit: BlockEditorKit;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A page's blocks, edited: each in a card with its add-below, move, duplicate
|
|
131
|
+
* and delete controls, and an add menu at the foot. Columns and infoboxes hold
|
|
132
|
+
* the same list, offering `nestedTypes`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function BlockEditor<B extends EditableBlock>({ blocks, onChange, schema, kit }: BlockEditorProps<B>): import("react").JSX.Element;
|
|
135
|
+
/** A labelled field in the kit's style, for a wiki's own block editors as well as these. */
|
|
136
|
+
export declare function Field(props: FieldProps): import("react").JSX.Element;
|
|
137
|
+
/** One of a few values in the kit's style; a `<select>` when the kit has none. */
|
|
138
|
+
export declare function Choice(props: ChoiceProps): import("react").JSX.Element;
|
|
139
|
+
/** A count clamped to 1…`MAX_BLOCK_ROWS`, the cap the write path enforces. */
|
|
140
|
+
export declare function LimitField({ label, value, onChange }: {
|
|
141
|
+
label?: string;
|
|
142
|
+
value: number | undefined;
|
|
143
|
+
onChange: (n: number) => void;
|
|
144
|
+
}): import("react").JSX.Element;
|
|
145
|
+
/**
|
|
146
|
+
* `code` is source text, never HTML: every view escapes it. A stored language
|
|
147
|
+
* outside `langs` stays selectable rather than reading as the first option and
|
|
148
|
+
* being overwritten on the next save.
|
|
149
|
+
*/
|
|
150
|
+
export declare function CodeTabsFields({ block, onUpdate, langs, defaultLang, }: {
|
|
151
|
+
block: CodeTabsBlock;
|
|
152
|
+
onUpdate: (block: CodeTabsBlock) => void;
|
|
153
|
+
langs?: readonly string[];
|
|
154
|
+
defaultLang?: string;
|
|
155
|
+
}): import("react").JSX.Element;
|
|
156
|
+
export declare function StatsFields({ block, onUpdate, hint }: {
|
|
157
|
+
block: StatsBlock;
|
|
158
|
+
onUpdate: (block: StatsBlock) => void;
|
|
159
|
+
hint?: string;
|
|
160
|
+
}): import("react").JSX.Element;
|
|
161
|
+
/** `title` is the heading a references block shows when it stores none, and `noun` what one row is called. */
|
|
162
|
+
export declare function ReferencesFields({ block, onUpdate, title, noun, hint, }: {
|
|
163
|
+
block: ReferencesBlock;
|
|
164
|
+
onUpdate: (block: ReferencesBlock) => void;
|
|
165
|
+
title?: string;
|
|
166
|
+
noun?: string;
|
|
167
|
+
hint?: string;
|
|
168
|
+
}): import("react").JSX.Element;
|
|
169
|
+
export declare function LinkGridFields({ block, onUpdate }: {
|
|
170
|
+
block: LinkGridBlock;
|
|
171
|
+
onUpdate: (block: LinkGridBlock) => void;
|
|
172
|
+
}): import("react").JSX.Element;
|
|
173
|
+
export declare function BannerFields({ block, onUpdate }: {
|
|
174
|
+
block: BannerBlock;
|
|
175
|
+
onUpdate: (block: BannerBlock) => void;
|
|
176
|
+
}): import("react").JSX.Element;
|
|
177
|
+
/** A page a page list can pick. `detail` is shown after the title, such as its section. */
|
|
178
|
+
export interface PageOption {
|
|
179
|
+
id: string;
|
|
180
|
+
title: string;
|
|
181
|
+
detail?: string;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The picked pages in their order, each movable and removable. With `pages`
|
|
185
|
+
* the rest are offered in a picker; without, an id is typed in.
|
|
186
|
+
*/
|
|
187
|
+
export declare function PageListFields<B extends {
|
|
188
|
+
pageIds: string[];
|
|
189
|
+
}>({ block, onUpdate, pages }: {
|
|
190
|
+
block: B;
|
|
191
|
+
onUpdate: (block: B) => void;
|
|
192
|
+
pages?: readonly PageOption[];
|
|
193
|
+
}): import("react").JSX.Element;
|
|
194
|
+
/** Two to four columns, each a block list offering the schema's `nestedTypes`. */
|
|
195
|
+
export declare function ColumnsFields<A extends EditableBlock>({ block, onUpdate }: {
|
|
196
|
+
block: ColumnsBlock<A>;
|
|
197
|
+
onUpdate: (block: ColumnsBlock<A>) => void;
|
|
198
|
+
}): import("react").JSX.Element;
|
|
199
|
+
/** An infobox's blocks, offering the schema's `nestedTypes`. */
|
|
200
|
+
export declare function InfoboxFields<A extends EditableBlock>({ block, onUpdate }: {
|
|
201
|
+
block: InfoboxBlock<A>;
|
|
202
|
+
onUpdate: (block: InfoboxBlock<A>) => void;
|
|
203
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// block-editor.tsx — the block list an author edits, and the fields of the core
|
|
4
|
+
// block types.
|
|
5
|
+
//
|
|
6
|
+
// Three wikis had written this, 500 to 700 lines each, and the copies had
|
|
7
|
+
// drifted the way copies do: one dropped a stored code language that was not
|
|
8
|
+
// in its list, one let a typed count run past the row cap, one named every
|
|
9
|
+
// remove button "Remove", one never highlighted the block being edited. The
|
|
10
|
+
// behaviour lives here once. The look does not: each wiki draws its inputs,
|
|
11
|
+
// toggles and menus with its own design system, so those arrive as a KIT —
|
|
12
|
+
// class names, icons, and four optional components — and the markup here is
|
|
13
|
+
// bare semantic HTML wearing whatever the kit says.
|
|
14
|
+
//
|
|
15
|
+
// The dispatch stays with the wiki. `renderBody` is a `switch` over its own
|
|
16
|
+
// closed union, as S4 of the workspace's STRUCTURE.md requires, so a new block
|
|
17
|
+
// type is a compile error there; its cases hand the core types to the field
|
|
18
|
+
// editors below and draw its native types themselves.
|
|
19
|
+
//
|
|
20
|
+
// The kit and the schema reach every nested list through context, which is
|
|
21
|
+
// what lets a column or an infobox hold a list without being handed the whole
|
|
22
|
+
// configuration. Keep both stable — a module constant or a `useMemo` — or every
|
|
23
|
+
// card re-renders on every keystroke.
|
|
24
|
+
import { createContext, memo, useCallback, useContext, useMemo, useState, } from 'react';
|
|
25
|
+
import { BlockActions, useBlockOperations } from './react.js';
|
|
26
|
+
import { CODE_LANGS, DEFAULT_LANG, } from './blocks.js';
|
|
27
|
+
import { BANNER_VARIANTS } from './text.js';
|
|
28
|
+
import { MAX_BLOCK_ROWS } from './validation.js';
|
|
29
|
+
import { cx } from './html.js';
|
|
30
|
+
const Context = createContext(null);
|
|
31
|
+
function useEditorContext() {
|
|
32
|
+
const ctx = useContext(Context);
|
|
33
|
+
if (!ctx)
|
|
34
|
+
throw new Error('A block field editor must render inside <BlockEditor>.');
|
|
35
|
+
return ctx;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A page's blocks, edited: each in a card with its add-below, move, duplicate
|
|
39
|
+
* and delete controls, and an add menu at the foot. Columns and infoboxes hold
|
|
40
|
+
* the same list, offering `nestedTypes`.
|
|
41
|
+
*/
|
|
42
|
+
export function BlockEditor({ blocks, onChange, schema, kit }) {
|
|
43
|
+
const value = useMemo(() => ({ kit, schema: schema }), [kit, schema]);
|
|
44
|
+
return (_jsx(Context.Provider, { value: value, children: _jsx(BlockList, { blocks: blocks, onChange: onChange }) }));
|
|
45
|
+
}
|
|
46
|
+
function BlockList({ blocks, onChange, nested = false }) {
|
|
47
|
+
const { kit, schema } = useEditorContext();
|
|
48
|
+
const c = kit.classNames;
|
|
49
|
+
const ops = useBlockOperations(blocks, onChange, schema);
|
|
50
|
+
const base = nested ? schema.nestedTypes : schema.types;
|
|
51
|
+
// A string, so the offer is recomputed — and every card re-rendered — only
|
|
52
|
+
// when a single-use type comes or goes, not on every keystroke.
|
|
53
|
+
const held = (schema.single ?? []).filter(t => blocks.some(b => b.type === t)).join(' ');
|
|
54
|
+
const offered = useMemo(() => base.filter(t => !held.split(' ').includes(t)), [base, held]);
|
|
55
|
+
const insertNew = useCallback((type) => ops.insert(schema.create(type)), [ops.insert, schema]);
|
|
56
|
+
return (_jsxs("div", { className: nested ? c.nestedList : c.list, children: [blocks.length === 0 && _jsx("p", { className: c.empty, children: nested ? 'Empty.' : 'Nothing here yet. Add the first block.' }), blocks.map((block, i) => (_jsx(BlockCard, { block: block, index: i, total: blocks.length, selected: ops.selectedIndex === i, nested: nested, offered: offered, update: ops.update, move: ops.move, duplicate: ops.duplicate, remove: ops.remove, insert: ops.insert, select: ops.setSelectedIndex }, block.id))), _jsx(InsertMenu, { types: offered, onPick: insertNew })] }));
|
|
57
|
+
}
|
|
58
|
+
// Memoised on the list's ref-stable callbacks, so a keystroke in one block does
|
|
59
|
+
// not re-render every other block's editor.
|
|
60
|
+
const BlockCard = memo(function BlockCard({ block, index, total, selected, nested, offered, update, move, duplicate, remove, insert, select }) {
|
|
61
|
+
const { kit, schema } = useEditorContext();
|
|
62
|
+
const c = kit.classNames;
|
|
63
|
+
const onUpdate = useCallback((next) => update(index, next), [update, index]);
|
|
64
|
+
const onSelect = useCallback(() => select(index), [select, index]);
|
|
65
|
+
const meta = schema.meta(block.type);
|
|
66
|
+
const label = meta?.label ?? block.type;
|
|
67
|
+
const body = meta ? schema.renderBody(block, onUpdate) : null;
|
|
68
|
+
return (_jsxs("section", { className: c.card, "data-selected": selected || undefined, "data-nested": nested || undefined, onFocusCapture: onSelect, onClick: onSelect, children: [_jsxs("div", { className: c.cardHead, children: [_jsxs("span", { className: c.cardLabel, children: [meta?.icon, label] }), _jsxs("span", { className: c.cardActions, children: [_jsx(InsertMenu, { below: true, types: offered, onPick: type => insert(schema.create(type), index + 1) }), _jsx(BlockActions, { index: index, total: total, ops: { move, duplicate, remove }, icons: kit.icons, className: c.cardActions, buttonClassName: c.action, blockLabel: label })] })] }), body ?? _jsx("p", { className: c.muted, children: "Kept as it is \u2013 this editor has no fields for it." })] }));
|
|
69
|
+
});
|
|
70
|
+
function InsertMenu({ types, onPick, below = false }) {
|
|
71
|
+
const { kit, schema } = useEditorContext();
|
|
72
|
+
const c = kit.classNames;
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
const Menu = kit.Menu ?? InlineMenu;
|
|
75
|
+
const toggle = () => setOpen(o => !o);
|
|
76
|
+
const trigger = below ? (_jsx("button", { type: "button", className: c.action, title: "Add a block below", "aria-label": "Add a block below", "aria-expanded": open, onClick: toggle, children: kit.icons.add })) : (_jsxs("button", { type: "button", className: c.addBlock, "aria-expanded": open, onClick: toggle, children: [kit.icons.add, "Add block"] }));
|
|
77
|
+
return (_jsx(Menu, { open: open, onOpenChange: setOpen, trigger: trigger, below: below, children: types.map(type => {
|
|
78
|
+
const meta = schema.meta(type);
|
|
79
|
+
return (_jsxs("button", { type: "button", className: c.menuItem, onClick: e => {
|
|
80
|
+
// Inside a card, a pick is not also a click on the card.
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
onPick(type);
|
|
83
|
+
setOpen(false);
|
|
84
|
+
}, children: [meta?.icon, _jsxs("span", { className: c.menuText, children: [_jsx("span", { children: meta?.label ?? type }), meta?.hint && _jsx("span", { className: c.menuHint, children: meta.hint })] })] }, type));
|
|
85
|
+
}) }));
|
|
86
|
+
}
|
|
87
|
+
function InlineMenu({ open, trigger, children }) {
|
|
88
|
+
const c = useEditorContext().kit.classNames;
|
|
89
|
+
return (_jsxs("span", { className: c.menuAnchor, children: [trigger, open && _jsx("span", { className: c.menu, children: children })] }));
|
|
90
|
+
}
|
|
91
|
+
// ---- the kit's parts, resolved ----------------------------------------------
|
|
92
|
+
/** A labelled field in the kit's style, for a wiki's own block editors as well as these. */
|
|
93
|
+
export function Field(props) {
|
|
94
|
+
const { Field: Custom, classNames: c } = useEditorContext().kit;
|
|
95
|
+
if (Custom)
|
|
96
|
+
return _jsx(Custom, { ...props });
|
|
97
|
+
return (_jsxs("label", { className: c.field, children: [_jsx("span", { children: props.label }), props.children, props.hint && _jsx("small", { className: c.muted, children: props.hint })] }));
|
|
98
|
+
}
|
|
99
|
+
/** One of a few values in the kit's style; a `<select>` when the kit has none. */
|
|
100
|
+
export function Choice(props) {
|
|
101
|
+
const { Choice: Custom, classNames: c } = useEditorContext().kit;
|
|
102
|
+
if (Custom)
|
|
103
|
+
return _jsx(Custom, { ...props });
|
|
104
|
+
return (_jsx(Field, { label: props.label, children: _jsx("select", { className: c.input, value: props.value, onChange: e => props.onChange(e.target.value), children: props.options.map(o => (_jsx("option", { value: o.value, children: o.label }, o.value))) }) }));
|
|
105
|
+
}
|
|
106
|
+
function Add({ onPress, children }) {
|
|
107
|
+
const { Add: Custom, classNames: c, icons } = useEditorContext().kit;
|
|
108
|
+
if (Custom)
|
|
109
|
+
return _jsx(Custom, { onPress: onPress, children: children });
|
|
110
|
+
return (_jsxs("button", { type: "button", className: c.add, onClick: onPress, children: [icons.add, children] }));
|
|
111
|
+
}
|
|
112
|
+
function Remove({ label, onPress }) {
|
|
113
|
+
const { classNames: c, icons } = useEditorContext().kit;
|
|
114
|
+
return (_jsx("button", { type: "button", className: c.action, title: label, "aria-label": label, onClick: onPress, children: icons.remove }));
|
|
115
|
+
}
|
|
116
|
+
/** A text input. `optional` stores an emptied field as absent rather than `''`. */
|
|
117
|
+
function TextInput({ label, value, onChange, placeholder = label, optional = false, type = 'text', className, }) {
|
|
118
|
+
const c = useEditorContext().kit.classNames;
|
|
119
|
+
return (_jsx("input", { type: type, className: cx(c.input, className), "aria-label": label, placeholder: placeholder, value: value ?? '', onChange: e => onChange(optional ? e.target.value.trim() ? e.target.value : undefined : e.target.value) }));
|
|
120
|
+
}
|
|
121
|
+
const replaceAt = (items, i, item) => items.map((x, j) => (j === i ? item : x));
|
|
122
|
+
const removeAt = (items, i) => items.filter((_, j) => j !== i);
|
|
123
|
+
const newId = () => crypto.randomUUID();
|
|
124
|
+
// ---- the core blocks' fields -------------------------------------------------
|
|
125
|
+
/** A count clamped to 1…`MAX_BLOCK_ROWS`, the cap the write path enforces. */
|
|
126
|
+
export function LimitField({ label = 'How many', value, onChange }) {
|
|
127
|
+
const c = useEditorContext().kit.classNames;
|
|
128
|
+
return (_jsx(Field, { label: label, children: _jsx("input", { type: "number", min: 1, max: MAX_BLOCK_ROWS, className: cx(c.input, c.number), value: value ?? '', onChange: e => onChange(Math.min(MAX_BLOCK_ROWS, Math.max(1, Math.floor(Number(e.target.value)) || 1))) }) }));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* `code` is source text, never HTML: every view escapes it. A stored language
|
|
132
|
+
* outside `langs` stays selectable rather than reading as the first option and
|
|
133
|
+
* being overwritten on the next save.
|
|
134
|
+
*/
|
|
135
|
+
export function CodeTabsFields({ block, onUpdate, langs = CODE_LANGS, defaultLang = DEFAULT_LANG, }) {
|
|
136
|
+
const c = useEditorContext().kit.classNames;
|
|
137
|
+
const tabs = block.tabs;
|
|
138
|
+
const set = (next) => onUpdate({ ...block, tabs: next });
|
|
139
|
+
return (_jsxs("div", { className: c.stack, children: [tabs.map((tab, i) => (_jsxs("div", { className: c.stack, children: [_jsxs("div", { className: c.row, children: [_jsx(TextInput, { label: "Tab label", value: tab.label, onChange: label => set(replaceAt(tabs, i, { ...tab, label: label ?? '' })) }), _jsx("select", { "aria-label": "Language", className: cx(c.input, c.lang), value: tab.language, onChange: e => set(replaceAt(tabs, i, { ...tab, language: e.target.value })), children: [...new Set([tab.language, ...langs])].map(l => (_jsx("option", { value: l, children: l }, l))) }), _jsx(Remove, { label: `Remove tab ${tab.label || i + 1}`, onPress: () => set(removeAt(tabs, i)) })] }), _jsx("textarea", { "aria-label": "Code", placeholder: "Code", rows: 6, spellCheck: false, className: cx(c.input, c.code), value: tab.code, onChange: e => set(replaceAt(tabs, i, { ...tab, code: e.target.value })) })] }, i))), _jsx(Add, { onPress: () => set([...tabs, { label: '', language: defaultLang, code: '' }]), children: "Add tab" })] }));
|
|
140
|
+
}
|
|
141
|
+
export function StatsFields({ block, onUpdate, hint }) {
|
|
142
|
+
const c = useEditorContext().kit.classNames;
|
|
143
|
+
const items = block.items;
|
|
144
|
+
const set = (next) => onUpdate({ ...block, items: next });
|
|
145
|
+
return (_jsxs("div", { className: c.stack, children: [_jsx(Choice, { label: "Per row", value: String(block.columns), options: ['2', '3', '4'].map(n => ({ value: n, label: n })), onChange: n => onUpdate({ ...block, columns: Number(n) }) }), items.map((item, i) => (_jsxs("div", { className: c.row, children: [_jsx(TextInput, { label: "Figure", className: c.figure, value: item.value, onChange: value => set(replaceAt(items, i, { ...item, value: value ?? '' })) }), _jsx(TextInput, { label: "What it counts", value: item.label, onChange: label => set(replaceAt(items, i, { ...item, label: label ?? '' })) }), _jsx(Remove, { label: `Remove figure ${i + 1}`, onPress: () => set(removeAt(items, i)) })] }, item.id))), _jsx(Add, { onPress: () => set([...items, { id: newId(), value: '', label: '' }]), children: "Add figure" }), hint && _jsx("small", { className: c.muted, children: hint })] }));
|
|
146
|
+
}
|
|
147
|
+
/** `title` is the heading a references block shows when it stores none, and `noun` what one row is called. */
|
|
148
|
+
export function ReferencesFields({ block, onUpdate, title = 'References', noun = 'reference', hint, }) {
|
|
149
|
+
const c = useEditorContext().kit.classNames;
|
|
150
|
+
const items = block.items;
|
|
151
|
+
const set = (next) => onUpdate({ ...block, items: next });
|
|
152
|
+
return (_jsxs("div", { className: c.stack, children: [_jsx(Field, { label: "Heading", children: _jsx(TextInput, { label: "Heading", placeholder: title, optional: true, value: block.title, onChange: t => onUpdate({ ...block, title: t }) }) }), items.map((item, i) => (_jsxs("div", { className: c.row, children: [_jsxs("span", { className: c.muted, children: [i + 1, "."] }), _jsx(TextInput, { label: "Title, author, publisher", value: item.text, onChange: text => set(replaceAt(items, i, { ...item, text: text ?? '' })) }), _jsx(TextInput, { label: "Address", type: "url", placeholder: "https://", optional: true, value: item.url, onChange: url => set(replaceAt(items, i, { ...item, url })) }), _jsx(Remove, { label: `Remove ${noun} ${i + 1}`, onPress: () => set(removeAt(items, i)) })] }, item.id))), _jsxs(Add, { onPress: () => set([...items, { id: newId(), text: '' }]), children: ["Add ", noun] }), hint && _jsx("small", { className: c.muted, children: hint })] }));
|
|
153
|
+
}
|
|
154
|
+
export function LinkGridFields({ block, onUpdate }) {
|
|
155
|
+
const c = useEditorContext().kit.classNames;
|
|
156
|
+
const groups = block.groups;
|
|
157
|
+
return (_jsxs("div", { className: c.stack, children: [_jsx(Field, { label: "Intro (optional)", children: _jsx(TextInput, { label: "Intro", placeholder: "A paragraph before the groups", optional: true, value: block.intro, onChange: intro => onUpdate({ ...block, intro }) }) }), groups.map((group, gi) => {
|
|
158
|
+
const setGroup = (next) => onUpdate({ ...block, groups: replaceAt(groups, gi, next) });
|
|
159
|
+
return (_jsxs("div", { className: cx(c.nest, c.stack), children: [_jsxs("div", { className: c.row, children: [_jsx(TextInput, { label: "Heading", value: group.heading, onChange: heading => setGroup({ ...group, heading: heading ?? '' }) }), _jsx(Remove, { label: `Remove group ${group.heading || gi + 1}`, onPress: () => onUpdate({ ...block, groups: removeAt(groups, gi) }) })] }), _jsx(TextInput, { label: "Description", placeholder: "Description (optional)", optional: true, value: group.description ?? undefined, onChange: description => setGroup({ ...group, description }) }), group.links.map((link, li) => (_jsxs("div", { className: c.row, children: [_jsx(TextInput, { label: "Label", value: link.label, onChange: label => setGroup({ ...group, links: replaceAt(group.links, li, { ...link, label: label ?? '' }) }) }), _jsx(TextInput, { label: "Address", placeholder: "/path or https://", value: link.href, onChange: href => setGroup({ ...group, links: replaceAt(group.links, li, { ...link, href: href ?? '' }) }) }), _jsx(Remove, { label: `Remove link ${link.label || li + 1}`, onPress: () => setGroup({ ...group, links: removeAt(group.links, li) }) })] }, li))), _jsx(Add, { onPress: () => setGroup({ ...group, links: [...group.links, { label: '', href: '' }] }), children: "Add link" })] }, group.id));
|
|
160
|
+
}), _jsx(Add, { onPress: () => onUpdate({ ...block, groups: [...groups, { id: newId(), heading: '', links: [] }] }), children: "Add group" })] }));
|
|
161
|
+
}
|
|
162
|
+
export function BannerFields({ block, onUpdate }) {
|
|
163
|
+
const c = useEditorContext().kit.classNames;
|
|
164
|
+
return (_jsxs("div", { className: c.stack, children: [_jsx(Choice, { label: "Notice", value: block.variant, options: BANNER_VARIANTS, onChange: v => onUpdate({ ...block, variant: v }) }), _jsx(Field, { label: "Custom message (optional)", children: _jsx(TextInput, { label: "Custom message", placeholder: "Empty for the standard notice", optional: true, value: block.text, onChange: text => onUpdate({ ...block, text }) }) })] }));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The picked pages in their order, each movable and removable. With `pages`
|
|
168
|
+
* the rest are offered in a picker; without, an id is typed in.
|
|
169
|
+
*/
|
|
170
|
+
export function PageListFields({ block, onUpdate, pages }) {
|
|
171
|
+
const { kit } = useEditorContext();
|
|
172
|
+
const c = kit.classNames;
|
|
173
|
+
const [typed, setTyped] = useState('');
|
|
174
|
+
const ids = block.pageIds;
|
|
175
|
+
const set = (pageIds) => onUpdate({ ...block, pageIds });
|
|
176
|
+
const byId = new Map(pages?.map(p => [p.id, p]));
|
|
177
|
+
const full = ids.length >= MAX_BLOCK_ROWS;
|
|
178
|
+
const add = (id) => {
|
|
179
|
+
if (id && !ids.includes(id) && !full)
|
|
180
|
+
set([...ids, id]);
|
|
181
|
+
};
|
|
182
|
+
const unpicked = pages?.filter(p => !ids.includes(p.id)) ?? [];
|
|
183
|
+
const ops = {
|
|
184
|
+
move: (from, to) => {
|
|
185
|
+
const next = removeAt(ids, from);
|
|
186
|
+
next.splice(to, 0, ids[from]);
|
|
187
|
+
set(next);
|
|
188
|
+
},
|
|
189
|
+
duplicate: () => { },
|
|
190
|
+
remove: (i) => set(removeAt(ids, i)),
|
|
191
|
+
};
|
|
192
|
+
return (_jsxs("div", { className: c.stack, children: [ids.map((id, i) => {
|
|
193
|
+
const page = byId.get(id);
|
|
194
|
+
return (_jsxs("div", { className: c.row, children: [_jsxs("span", { className: c.muted, children: [i + 1, "."] }), _jsxs("span", { children: [pages ? (page?.title ?? `No such page: ${id}`) : id, page?.detail && _jsxs("span", { className: c.muted, children: [" \u00B7 ", page.detail] })] }), _jsx(BlockActions, { index: i, total: ids.length, ops: ops, icons: { up: kit.icons.up, down: kit.icons.down, remove: kit.icons.remove }, className: c.cardActions, buttonClassName: c.action, blockLabel: "page" })] }, i));
|
|
195
|
+
}), pages && pages.length === 0 && _jsx("p", { className: c.muted, children: "This wiki has no pages to pick yet." }), !full && pages && unpicked.length > 0 && (_jsx(Field, { label: "Add a page", children: _jsxs("select", { className: c.input, value: "", onChange: e => add(e.target.value), children: [_jsx("option", { value: "", children: "Choose\u2026" }), unpicked.map(p => (_jsx("option", { value: p.id, children: p.detail ? `${p.title} · ${p.detail}` : p.title }, p.id)))] }) })), !full && !pages && (_jsxs("div", { className: c.row, children: [_jsx("input", { type: "text", className: c.input, "aria-label": "Page id", placeholder: "Page id", value: typed, onChange: e => setTyped(e.target.value), onKeyDown: e => {
|
|
196
|
+
if (e.key !== 'Enter')
|
|
197
|
+
return;
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
add(typed.trim());
|
|
200
|
+
setTyped('');
|
|
201
|
+
} }), _jsx(Add, { onPress: () => {
|
|
202
|
+
add(typed.trim());
|
|
203
|
+
setTyped('');
|
|
204
|
+
}, children: "Add page" })] }))] }));
|
|
205
|
+
}
|
|
206
|
+
const GAPS = ['sm', 'md', 'lg'].map(v => ({ value: v, label: v }));
|
|
207
|
+
const ALIGNS = ['start', 'center', 'end', 'stretch'].map(v => ({ value: v, label: v }));
|
|
208
|
+
const MAX_COLUMNS = 4;
|
|
209
|
+
/** Two to four columns, each a block list offering the schema's `nestedTypes`. */
|
|
210
|
+
export function ColumnsFields({ block, onUpdate }) {
|
|
211
|
+
const c = useEditorContext().kit.classNames;
|
|
212
|
+
const columns = block.columns;
|
|
213
|
+
return (_jsxs("div", { className: c.stack, children: [_jsxs("div", { className: c.row, children: [_jsx(Choice, { label: "Gap", value: block.gap ?? 'md', options: GAPS, onChange: gap => onUpdate({ ...block, gap: gap }) }), _jsx(Choice, { label: "Align", value: block.align ?? 'start', options: ALIGNS, onChange: align => onUpdate({ ...block, align: align }) })] }), _jsx("div", { className: c.columns, children: columns.map((column, ci) => (_jsxs("div", { className: cx(c.nest, c.stack), children: [_jsxs("div", { className: c.cardHead, children: [_jsxs("span", { className: c.cardLabel, children: ["Column ", ci + 1] }), columns.length > 1 && _jsx(Remove, { label: `Remove column ${ci + 1}`, onPress: () => onUpdate({ ...block, columns: removeAt(columns, ci) }) })] }), _jsx(BlockList, { nested: true, blocks: column.blocks, onChange: blocks => onUpdate({ ...block, columns: replaceAt(columns, ci, { ...column, blocks: blocks }) }) })] }, column.id))) }), columns.length < MAX_COLUMNS && _jsx(Add, { onPress: () => onUpdate({ ...block, columns: [...columns, { id: newId(), blocks: [] }] }), children: "Add column" })] }));
|
|
214
|
+
}
|
|
215
|
+
/** An infobox's blocks, offering the schema's `nestedTypes`. */
|
|
216
|
+
export function InfoboxFields({ block, onUpdate }) {
|
|
217
|
+
return _jsx(BlockList, { nested: true, blocks: block.blocks, onChange: blocks => onUpdate({ ...block, blocks: blocks }) });
|
|
218
|
+
}
|
package/dist/block-views.js
CHANGED
|
@@ -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
|
|
6
|
-
// peer, so a consumer that only wants the taxonomy or the MCP transport
|
|
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
|
package/dist/blocks.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RelativeTimeStyle } from './freshness.js';
|
|
2
2
|
import type { BlockGroup } from './revisions.js';
|
|
3
|
+
import { type BannerVariant } from './text.js';
|
|
3
4
|
/**
|
|
4
5
|
* The languages a code block or code tab offers, and the one a new one opens
|
|
5
6
|
* on. Two wikis carried this list character for character; a wiki with its own
|
|
@@ -50,6 +51,72 @@ export interface LinkGridGroup {
|
|
|
50
51
|
description?: string | null;
|
|
51
52
|
links: readonly LinkGridLink[];
|
|
52
53
|
}
|
|
54
|
+
export type EditableCodeTab = CodeTab & {
|
|
55
|
+
language: string;
|
|
56
|
+
};
|
|
57
|
+
export type EditableStatItem = StatItem & {
|
|
58
|
+
id: string;
|
|
59
|
+
value: string;
|
|
60
|
+
};
|
|
61
|
+
export type EditableReferenceItem = ReferenceItem & {
|
|
62
|
+
id: string;
|
|
63
|
+
url?: string;
|
|
64
|
+
};
|
|
65
|
+
export type EditableLinkGridGroup = LinkGridGroup & {
|
|
66
|
+
id: string;
|
|
67
|
+
links: LinkGridLink[];
|
|
68
|
+
};
|
|
69
|
+
export interface ContentBlock {
|
|
70
|
+
id: string;
|
|
71
|
+
type: 'content';
|
|
72
|
+
text: string;
|
|
73
|
+
}
|
|
74
|
+
export interface CodeTabsBlock {
|
|
75
|
+
id: string;
|
|
76
|
+
type: 'codeTabs';
|
|
77
|
+
tabs: EditableCodeTab[];
|
|
78
|
+
}
|
|
79
|
+
export interface StatsBlock {
|
|
80
|
+
id: string;
|
|
81
|
+
type: 'stats';
|
|
82
|
+
items: EditableStatItem[];
|
|
83
|
+
columns: 2 | 3 | 4;
|
|
84
|
+
}
|
|
85
|
+
export interface LinkGridBlock {
|
|
86
|
+
id: string;
|
|
87
|
+
type: 'linkGrid';
|
|
88
|
+
intro?: string;
|
|
89
|
+
groups: EditableLinkGridGroup[];
|
|
90
|
+
}
|
|
91
|
+
export interface ReferencesBlock {
|
|
92
|
+
id: string;
|
|
93
|
+
type: 'references';
|
|
94
|
+
title?: string;
|
|
95
|
+
items: EditableReferenceItem[];
|
|
96
|
+
}
|
|
97
|
+
/** A Wikipedia-style notice. `text` replaces the variant's standard message. */
|
|
98
|
+
export interface BannerBlock {
|
|
99
|
+
id: string;
|
|
100
|
+
type: 'banner';
|
|
101
|
+
variant: BannerVariant;
|
|
102
|
+
text?: string;
|
|
103
|
+
}
|
|
104
|
+
/** `A` is the wiki's atomic union: a container never holds another container. */
|
|
105
|
+
export interface ColumnsBlock<A> {
|
|
106
|
+
id: string;
|
|
107
|
+
type: 'columns';
|
|
108
|
+
columns: {
|
|
109
|
+
id: string;
|
|
110
|
+
blocks: A[];
|
|
111
|
+
}[];
|
|
112
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
113
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
114
|
+
}
|
|
115
|
+
export interface InfoboxBlock<A> {
|
|
116
|
+
id: string;
|
|
117
|
+
type: 'infobox';
|
|
118
|
+
blocks: A[];
|
|
119
|
+
}
|
|
53
120
|
export interface BlockTreeOptions<B> {
|
|
54
121
|
/** Render one leaf block. Return `''` to drop it from the document. */
|
|
55
122
|
atomic: (block: B) => string;
|
package/dist/crawlers.d.ts
CHANGED
|
@@ -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
|
|
18
|
-
*
|
|
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
|
+
};
|