@immediately-run/grove 0.1.1 → 0.1.3
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 +43 -115
- package/llms.txt +6 -2
- package/package.json +20 -8
- package/src/App.tsx +11 -7
- package/src/GroveApp.css +485 -85
- package/src/GroveWiki.tsx +176 -54
- package/src/components/AssetImage.tsx +1 -12
- package/src/components/Backlinks.tsx +2 -2
- package/src/components/Catalogue.test.tsx +157 -0
- package/src/components/ContentTheme.test.tsx +146 -0
- package/src/components/ContentTheme.tsx +111 -0
- package/src/components/DocList.infinite.test.tsx +177 -0
- package/src/components/DocList.tsx +82 -9
- package/src/components/Drawer.tsx +14 -2
- package/src/components/EntryHeader.tsx +24 -15
- package/src/components/EntryImage.test.tsx +210 -0
- package/src/components/EntryImage.tsx +47 -0
- package/src/components/Galleries.test.tsx +98 -0
- package/src/components/GroveAgent.test.tsx +330 -0
- package/src/components/GroveAgent.tsx +269 -149
- package/src/components/GroveNav.test.tsx +229 -0
- package/src/components/GroveNav.tsx +69 -20
- package/src/components/Icon.tsx +0 -1
- package/src/components/InlineProse.tsx +37 -0
- package/src/components/LayoutGallery.tsx +65 -0
- package/src/components/PageView.tsx +19 -4
- package/src/components/Search.test.tsx +113 -0
- package/src/components/Search.tsx +53 -18
- package/src/components/Sidebar.test.tsx +135 -0
- package/src/components/Sidebar.tsx +114 -12
- package/src/components/TableOfContents.test.tsx +27 -16
- package/src/components/ThemeAssets.test.tsx +109 -0
- package/src/components/ThemeAssets.tsx +63 -0
- package/src/components/ThemeGallery.tsx +31 -0
- package/src/components/Timeline.tsx +5 -2
- package/src/components/WikiLink.tsx +2 -2
- package/src/data/catalogue.ts +54 -0
- package/src/data/themeFonts.ts +58 -0
- package/src/data/themes.ts +44 -4
- package/src/devfs.d.ts +5 -4
- package/src/hooks/{useCorpusMetadata.ts → useBundleMetadata.ts} +6 -6
- package/src/hooks/useContentComponents.ts +1 -1
- package/src/hooks/useEditAffordance.ts +99 -0
- package/src/hooks/useOpenWikiBoot.ts +1 -1
- package/src/hooks/useOverlayFocusDismiss.test.tsx +139 -0
- package/src/hooks/useOverlayFocusDismiss.ts +118 -0
- package/src/hooks/useScrollReset.test.tsx +132 -0
- package/src/hooks/useScrollReset.ts +52 -0
- package/src/index.css +9 -2
- package/src/lib/agentPrompt.test.ts +96 -0
- package/src/lib/agentPrompt.ts +86 -0
- package/src/lib/agentTools.test.ts +115 -0
- package/src/lib/agentTools.ts +132 -0
- package/src/lib/agentTranscript.ts +50 -0
- package/src/lib/assetPath.test.ts +36 -0
- package/src/lib/assetPath.ts +42 -0
- package/src/lib/collectionCalls.test.ts +69 -0
- package/src/lib/content.test.ts +10 -2
- package/src/lib/content.ts +7 -0
- package/src/lib/contentRoot.ts +16 -1
- package/src/lib/contentStylesheet.test.ts +80 -0
- package/src/lib/contentStylesheet.ts +103 -0
- package/src/lib/corpusScan.test.ts +37 -3
- package/src/lib/corpusScan.ts +17 -2
- package/src/lib/editTarget.test.ts +108 -0
- package/src/lib/editTarget.ts +93 -0
- package/src/lib/inlineProse.parity.test.ts +52 -0
- package/src/lib/layout.ts +29 -0
- package/src/lib/openWiki.test.ts +46 -5
- package/src/lib/openWiki.ts +18 -3
- package/src/lib/pageVariants.test.tsx +87 -0
- package/src/lib/queries.test.ts +33 -1
- package/src/lib/queries.ts +33 -1
- package/src/lib/reachCard.test.ts +94 -0
- package/src/lib/reachCard.ts +112 -0
- package/src/lib/shell.ts +17 -0
- package/src/lib/starterSweep.test.tsx +160 -0
- package/src/lib/starterSweep.ts +97 -0
- package/src/lib/themeAssets.test.ts +135 -0
- package/src/lib/themeAssets.ts +143 -0
- package/src/lib/themeSelection.test.ts +52 -0
- package/src/lib/themeSelection.ts +59 -0
- package/src/mdxComponents.ts +4 -0
- package/viewer-manifest.schema.json +37 -0
- package/viewer.manifest.json +133 -6
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Engine-resolved theme assets (R3-315; plan 05-content-carried-themes §4).
|
|
2
|
+
//
|
|
3
|
+
// A theme DECLARES its fonts and named assets (`fonts: [{family, src, weight,
|
|
4
|
+
// style}]`, `assets: {<name>: <relPath>}`, resolved relative to the declaring
|
|
5
|
+
// file); the ENGINE reads the bytes off the fs, mints a `blob:` URL for each, and
|
|
6
|
+
// hands the result to CSS — an `@font-face` rule per font (emitted into the
|
|
7
|
+
// engine-owned cascade layer) and a `--asset-<name>` custom property per asset.
|
|
8
|
+
// Content never names a network location at all, which is the point: `blob:` is
|
|
9
|
+
// permitted by `img-src`, `font-src` AND `style-src` in every stance including
|
|
10
|
+
// M3, so one mechanism serves fork, dispatch and library modes — and the wiki
|
|
11
|
+
// renders its faces with the network off, in every stance.
|
|
12
|
+
//
|
|
13
|
+
// The revoke scope is the THEME'S lifetime, not a component's — `useObjectUrl`'s
|
|
14
|
+
// component-scoped revoke does not fit an asset set that outlives any single
|
|
15
|
+
// surface, so this module owns the lifecycle explicitly: `revoke()` releases
|
|
16
|
+
// every minted URL, and the switching surface calls it on teardown.
|
|
17
|
+
//
|
|
18
|
+
// DEGRADE, NEVER BREAK: a missing font file skips that `@font-face` (the token's
|
|
19
|
+
// fallback stack answers); a missing named asset skips its var (the var()'s own
|
|
20
|
+
// fallback answers). Nothing throws and nothing renders a broken glyph.
|
|
21
|
+
//
|
|
22
|
+
// The minted URLs carry no path information (`blob:` + origin), so under dispatch
|
|
23
|
+
// the chroot prefix cannot reach a URL or a `@font-face` src by construction —
|
|
24
|
+
// asserted, not assumed, in the tests.
|
|
25
|
+
|
|
26
|
+
import { resolvePath } from './assetPath';
|
|
27
|
+
|
|
28
|
+
/** One declared webfont. `src` is relative to the declaring file (or `/…` absolute). */
|
|
29
|
+
export interface FontDeclaration {
|
|
30
|
+
family: string;
|
|
31
|
+
src: string;
|
|
32
|
+
/** CSS `font-weight` descriptor, e.g. `'400 700'` or `'700'`. */
|
|
33
|
+
weight?: string;
|
|
34
|
+
/** CSS `font-style` descriptor, e.g. `'normal'` | `'italic'`. */
|
|
35
|
+
style?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The asset declarations a theme (or the engine's own default set) carries. */
|
|
39
|
+
export interface ThemeAssetDeclarations {
|
|
40
|
+
fonts?: FontDeclaration[];
|
|
41
|
+
assets?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The minted result: CSS the engine owns, plus the revoke handle. */
|
|
45
|
+
export interface MintedThemeAssets {
|
|
46
|
+
/** `@font-face` rules with `blob:` srcs — the caller emits these inside the
|
|
47
|
+
* engine cascade layer. Empty when nothing declared or everything degraded. */
|
|
48
|
+
fontFaceCss: string;
|
|
49
|
+
/** A `.grove-root` rule assigning `--asset-<name>: url(blob:…)` per minted
|
|
50
|
+
* asset — empty when nothing declared or everything degraded. */
|
|
51
|
+
assetVarsCss: string;
|
|
52
|
+
/** How many URLs were minted (observable for leak tests). */
|
|
53
|
+
minted: number;
|
|
54
|
+
/** Release every minted URL. Idempotent. */
|
|
55
|
+
revoke(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The bytes-read the minter needs — injected so tests spy and fault-inject. */
|
|
59
|
+
export type AssetReader = (absPath: string) => Promise<Uint8Array<ArrayBuffer> | string>;
|
|
60
|
+
|
|
61
|
+
const FONT_MIME: Record<string, string> = {
|
|
62
|
+
woff2: 'font/woff2',
|
|
63
|
+
woff: 'font/woff',
|
|
64
|
+
ttf: 'font/ttf',
|
|
65
|
+
otf: 'font/otf',
|
|
66
|
+
};
|
|
67
|
+
const IMAGE_MIME: Record<string, string> = {
|
|
68
|
+
png: 'image/png',
|
|
69
|
+
jpg: 'image/jpeg',
|
|
70
|
+
jpeg: 'image/jpeg',
|
|
71
|
+
webp: 'image/webp',
|
|
72
|
+
svg: 'image/svg+xml',
|
|
73
|
+
avif: 'image/avif',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function ext(p: string): string {
|
|
77
|
+
const m = /\.([a-z0-9]+)$/i.exec(p);
|
|
78
|
+
return m ? m[1].toLowerCase() : '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Mint the blob URLs + CSS for a declaration set.
|
|
83
|
+
*
|
|
84
|
+
* @param decls the fonts/assets the declaring file asked for
|
|
85
|
+
* @param basePath the DECLARING FILE's absolute fs path (resolution base)
|
|
86
|
+
* @param read the bytes-read (fs)
|
|
87
|
+
*/
|
|
88
|
+
export async function mintThemeAssets(
|
|
89
|
+
decls: ThemeAssetDeclarations,
|
|
90
|
+
basePath: string,
|
|
91
|
+
read: AssetReader,
|
|
92
|
+
): Promise<MintedThemeAssets> {
|
|
93
|
+
const urls: string[] = [];
|
|
94
|
+
const fontRules: string[] = [];
|
|
95
|
+
const varRules: string[] = [];
|
|
96
|
+
|
|
97
|
+
const mint = async (ref: string): Promise<string | null> => {
|
|
98
|
+
try {
|
|
99
|
+
const abs = resolvePath(basePath, ref);
|
|
100
|
+
const bytes = await read(abs);
|
|
101
|
+
const e = ext(abs);
|
|
102
|
+
const type = FONT_MIME[e] ?? IMAGE_MIME[e] ?? 'application/octet-stream';
|
|
103
|
+
const url = URL.createObjectURL(new Blob([bytes], { type }));
|
|
104
|
+
urls.push(url);
|
|
105
|
+
return url;
|
|
106
|
+
} catch {
|
|
107
|
+
return null; // degrade: the fallback stack / var() fallback answers
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const f of decls.fonts ?? []) {
|
|
112
|
+
if (!f || typeof f.family !== 'string' || typeof f.src !== 'string') continue;
|
|
113
|
+
const url = await mint(f.src);
|
|
114
|
+
if (!url) continue;
|
|
115
|
+
const descriptors = [
|
|
116
|
+
`font-family: ${JSON.stringify(f.family)}`,
|
|
117
|
+
`src: url(${JSON.stringify(url)}) format(${JSON.stringify(ext(f.src) === 'woff2' ? 'woff2' : ext(f.src))})`,
|
|
118
|
+
...(f.weight ? [`font-weight: ${f.weight}`] : []),
|
|
119
|
+
...(f.style ? [`font-style: ${f.style}`] : []),
|
|
120
|
+
'font-display: swap',
|
|
121
|
+
];
|
|
122
|
+
fontRules.push(`@font-face{${descriptors.join(';')}}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const [name, ref] of Object.entries(decls.assets ?? {})) {
|
|
126
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(name) || typeof ref !== 'string') continue;
|
|
127
|
+
const url = await mint(ref);
|
|
128
|
+
if (!url) continue;
|
|
129
|
+
varRules.push(`--asset-${name}: url(${JSON.stringify(url)});`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let revoked = false;
|
|
133
|
+
return {
|
|
134
|
+
fontFaceCss: fontRules.join('\n'),
|
|
135
|
+
assetVarsCss: varRules.length ? `.grove-root{${varRules.join('')}}` : '',
|
|
136
|
+
minted: urls.length,
|
|
137
|
+
revoke() {
|
|
138
|
+
if (revoked) return;
|
|
139
|
+
revoked = true;
|
|
140
|
+
for (const u of urls) URL.revokeObjectURL(u);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The selection precedence (02-theme-contract §4), pinned as a decision table —
|
|
2
|
+
// pure, so the whole cross-product of sources is cheap to assert. R3-308.
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { resolvePalette, resolvePolarity } from './themeSelection';
|
|
5
|
+
import { preferredPolarity, THEMES } from '../data/themes';
|
|
6
|
+
|
|
7
|
+
describe('resolvePalette — reader, else author, else default', () => {
|
|
8
|
+
it('a reader override wins over the author declaration', () => {
|
|
9
|
+
expect(resolvePalette({ reader: 'pixies', author: 'lotr' })).toBe('pixies');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("a reader's explicit 'default' outranks a declaration — it is a choice, not an absence", () => {
|
|
13
|
+
expect(resolvePalette({ reader: 'default', author: 'lotr' })).toBe('default');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('the author declaration is the default a reader falls into', () => {
|
|
17
|
+
expect(resolvePalette({ reader: null, author: 'family' })).toBe('family');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('no reader, no author → default', () => {
|
|
21
|
+
expect(resolvePalette({})).toBe('default');
|
|
22
|
+
expect(resolvePalette({ reader: '', author: null })).toBe('default');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('resolvePolarity — reader, else host, else the palette’s own preference', () => {
|
|
27
|
+
it('a reader override wins over the host', () => {
|
|
28
|
+
expect(resolvePolarity({ reader: 'light', host: 'dark', preferred: 'dark' })).toBe('light');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('the host drives polarity when the reader is silent — palette untouched (theme:read is polarity-only)', () => {
|
|
32
|
+
expect(resolvePolarity({ reader: null, host: 'light', preferred: 'dark' })).toBe('light');
|
|
33
|
+
expect(resolvePolarity({ reader: null, host: 'dark', preferred: 'light' })).toBe('dark');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('no reader, no host → the palette’s preferred polarity', () => {
|
|
37
|
+
expect(resolvePolarity({ reader: null, host: null, preferred: 'light' })).toBe('light');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('an unparseable stored override is silence, not a crash', () => {
|
|
41
|
+
expect(resolvePolarity({ reader: null, host: 'light', preferred: 'dark' })).toBe('light');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('the catalogue carries a preferred polarity for every theme (R3-308)', () => {
|
|
46
|
+
it('every theme declares one, and unknown ids fall back to dark', () => {
|
|
47
|
+
for (const t of THEMES) expect(['light', 'dark']).toContain(t.preferred);
|
|
48
|
+
expect(preferredPolarity('editorial')).toBe('dark');
|
|
49
|
+
expect(preferredPolarity('archive')).toBe('light');
|
|
50
|
+
expect(preferredPolarity('never-heard-of')).toBe('dark');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Theme selection — the ONE resolution of who picks what a reader sees
|
|
2
|
+
// (plans/grove-layouts-and-themes/02-theme-contract.mdx §4, R3-308).
|
|
3
|
+
//
|
|
4
|
+
// Two INDEPENDENT axes, three sources, stated once here so no surface re-derives
|
|
5
|
+
// them (ways_of_working §5: one resolution entry point per concern):
|
|
6
|
+
//
|
|
7
|
+
// palette = reader override, else author declaration, else 'default'
|
|
8
|
+
// polarity = reader override, else host theme, else the theme's own preferred
|
|
9
|
+
//
|
|
10
|
+
// The host has an opinion about POLARITY ONLY — it holds `theme:read` for exactly
|
|
11
|
+
// that and nothing else, so it never appears in the palette chain. A reader's
|
|
12
|
+
// choice outranks everyone and persists; an author's declaration is the default a
|
|
13
|
+
// reader falls into, not a wall.
|
|
14
|
+
//
|
|
15
|
+
// PURE: takes already-read inputs, returns a decision. Where each input comes from
|
|
16
|
+
// (localStorage, home-entry frontmatter, the host channel) is wiring, not policy,
|
|
17
|
+
// and lives in the components.
|
|
18
|
+
|
|
19
|
+
/** Light/dark — the axis that selects WITHIN a palette family. */
|
|
20
|
+
export type Polarity = 'light' | 'dark';
|
|
21
|
+
|
|
22
|
+
/** A palette family id — a `Theme['id']`, or any string a reader's override holds. */
|
|
23
|
+
export type PaletteId = string;
|
|
24
|
+
|
|
25
|
+
export interface PaletteInputs {
|
|
26
|
+
/** The reader's stored override (`grove:theme`), if any. */
|
|
27
|
+
reader?: PaletteId | null;
|
|
28
|
+
/** The author's `theme:` declaration on the home entry, if any. */
|
|
29
|
+
author?: PaletteId | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the palette. An empty/absent override is NOT an override — `''` would
|
|
34
|
+
* otherwise beat a real declaration while meaning nothing. `'default'` as a READER
|
|
35
|
+
* choice is meaningful ("the brand palette, even though this wiki declares another"),
|
|
36
|
+
* so any explicit reader value — `default` included — outranks the author.
|
|
37
|
+
*/
|
|
38
|
+
export function resolvePalette({ reader, author }: PaletteInputs): PaletteId {
|
|
39
|
+
if (reader) return reader;
|
|
40
|
+
if (author) return author;
|
|
41
|
+
return 'default';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PolarityInputs {
|
|
45
|
+
/** The reader's stored override (`grove:appearance`), if any. */
|
|
46
|
+
reader?: Polarity | null;
|
|
47
|
+
/** The host's current theme, when the host has one (standalone: the channel's
|
|
48
|
+
* initial — the host axis simply has no live source there). */
|
|
49
|
+
host?: Polarity | null;
|
|
50
|
+
/** The resolved palette's own preferred polarity. */
|
|
51
|
+
preferred: Polarity;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve the polarity. Reader, then host, then the palette's own preference. */
|
|
55
|
+
export function resolvePolarity({ reader, host, preferred }: PolarityInputs): Polarity {
|
|
56
|
+
if (reader === 'light' || reader === 'dark') return reader;
|
|
57
|
+
if (host === 'light' || host === 'dark') return host;
|
|
58
|
+
return preferred;
|
|
59
|
+
}
|
package/src/mdxComponents.ts
CHANGED
|
@@ -6,6 +6,8 @@ import Lede from './components/Lede';
|
|
|
6
6
|
import Infobox from './components/Infobox';
|
|
7
7
|
import More from './components/More';
|
|
8
8
|
import DocList from './components/DocList';
|
|
9
|
+
import ThemeGallery from './components/ThemeGallery';
|
|
10
|
+
import LayoutGallery from './components/LayoutGallery';
|
|
9
11
|
import TagCloud from './components/TagCloud';
|
|
10
12
|
import TagList from './components/TagList';
|
|
11
13
|
import Directory from './components/Directory';
|
|
@@ -61,6 +63,8 @@ export const GROVE_MDX = {
|
|
|
61
63
|
ChildPages,
|
|
62
64
|
Timeline,
|
|
63
65
|
FamilyTree,
|
|
66
|
+
ThemeGallery,
|
|
67
|
+
LayoutGallery,
|
|
64
68
|
// Layout primitives — a `_layout.mdx` arranges the shell out of these around an
|
|
65
69
|
// <Outlet/>, so the site chrome is content (see src/lib/layout.ts).
|
|
66
70
|
Outlet,
|
|
@@ -57,6 +57,43 @@
|
|
|
57
57
|
"corpusTooling": { "type": "array", "items": { "type": "string" }, "description": "Keys a corpus's own tooling reads. The engine ignores them." },
|
|
58
58
|
"passThrough": { "type": "boolean", "description": "Whether unknown keys are allowed (they are carried, unread). False makes the vocabulary closed." }
|
|
59
59
|
}
|
|
60
|
+
},
|
|
61
|
+
"layouts": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"description": "The layout-starters catalogue (R3-309, bucket A): `_layout.mdx` starters under `content/_layouts/` a contributor or agent copies from. Keyed by starter id (= filename); `layoutRole` is the declared kind the entry and the file must agree on — the field existed in the sample layouts for a year with nothing reading it, and the catalogue is what gives it a job.",
|
|
64
|
+
"additionalProperties": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"required": ["layoutRole", "arranges", "ships", "summary"],
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"properties": {
|
|
69
|
+
"layoutRole": {
|
|
70
|
+
"enum": ["root", "section"],
|
|
71
|
+
"description": "`root` is an outermost shell (selects the nav arrangement); `section` nests under a root and wraps one folder."
|
|
72
|
+
},
|
|
73
|
+
"arranges": { "type": "string", "description": "The chrome the starter arranges around `<Outlet/>`." },
|
|
74
|
+
"ships": { "type": "boolean", "description": "Whether the starter file exists today (kept false for a catalogue entry whose file lands with a later item)." },
|
|
75
|
+
"summary": { "type": "string" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"collections": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"description": "The collection-shapes catalogue (R3-309, bucket C): what a LIST of entries looks like. Behaviour lives here, not in a starter — an `_layout.mdx` cannot hold state, so infinite scroll is an engine component's literal prop, never a file you copy.",
|
|
82
|
+
"additionalProperties": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"required": ["ships", "summary"],
|
|
85
|
+
"additionalProperties": false,
|
|
86
|
+
"properties": {
|
|
87
|
+
"component": { "type": "string", "description": "The engine component the shape rides on (absent for shapes that are not component calls at all — a directory URL is one)." },
|
|
88
|
+
"props": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"description": "The literal attributes a corpus writes to select the shape. Expression props are dropped silently by the interpreter — every documented call must stay literal.",
|
|
91
|
+
"additionalProperties": { "type": "string" }
|
|
92
|
+
},
|
|
93
|
+
"ships": { "type": "boolean", "description": "Whether the shape renders today; a not-yet-shipping entry documents what lands with which item." },
|
|
94
|
+
"summary": { "type": "string" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
60
97
|
}
|
|
61
98
|
}
|
|
62
99
|
}
|
package/viewer.manifest.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"title": "string?",
|
|
37
37
|
"children": "node?"
|
|
38
38
|
},
|
|
39
|
-
"summary": "An aside block with a kind (note / warning /
|
|
39
|
+
"summary": "An aside block with a kind (note / warning / …)."
|
|
40
40
|
},
|
|
41
41
|
"Lede": {
|
|
42
42
|
"tier": "engine",
|
|
@@ -73,9 +73,11 @@
|
|
|
73
73
|
"sanitizing": false,
|
|
74
74
|
"props": {
|
|
75
75
|
"tag": "string?",
|
|
76
|
-
"limit": "number?"
|
|
76
|
+
"limit": "number?",
|
|
77
|
+
"paginate": "'infinite'?",
|
|
78
|
+
"batch": "number?"
|
|
77
79
|
},
|
|
78
|
-
"summary": "A queried list of entries
|
|
80
|
+
"summary": "A queried list of entries — the generic index primitive. paginate=\"infinite\" windows it with a sentinel and a stated end (R3-314); the attribute must be a literal."
|
|
79
81
|
},
|
|
80
82
|
"TagCloud": {
|
|
81
83
|
"tier": "engine",
|
|
@@ -177,7 +179,7 @@
|
|
|
177
179
|
"overridable": true,
|
|
178
180
|
"sanitizing": false,
|
|
179
181
|
"props": {},
|
|
180
|
-
"summary": "The entry's own metadata line (updated, reading time,
|
|
182
|
+
"summary": "The entry's own metadata line (updated, reading time, …)."
|
|
181
183
|
},
|
|
182
184
|
"RecentlyUpdated": {
|
|
183
185
|
"tier": "engine",
|
|
@@ -227,7 +229,7 @@
|
|
|
227
229
|
"overridable": false,
|
|
228
230
|
"sanitizing": false,
|
|
229
231
|
"props": {},
|
|
230
|
-
"summary": "The layout chain's inward slot. NOT overridable
|
|
232
|
+
"summary": "The layout chain's inward slot. NOT overridable — the chain's nesting is engine mechanics, and replacing it detaches every layer from the page it wraps."
|
|
231
233
|
},
|
|
232
234
|
"GroveNav": {
|
|
233
235
|
"tier": "chrome",
|
|
@@ -249,19 +251,144 @@
|
|
|
249
251
|
"sanitizing": false,
|
|
250
252
|
"props": {},
|
|
251
253
|
"summary": "The site footer."
|
|
254
|
+
},
|
|
255
|
+
"ThemeGallery": {
|
|
256
|
+
"tier": "engine",
|
|
257
|
+
"overridable": true,
|
|
258
|
+
"sanitizing": false,
|
|
259
|
+
"props": {},
|
|
260
|
+
"summary": "The themes gallery: one row per shipped theme, enumerated from this manifest — adding a theme adds a row with no edit to the entry."
|
|
261
|
+
},
|
|
262
|
+
"LayoutGallery": {
|
|
263
|
+
"tier": "engine",
|
|
264
|
+
"overridable": true,
|
|
265
|
+
"sanitizing": false,
|
|
266
|
+
"props": {},
|
|
267
|
+
"summary": "The layouts gallery: the starters, page variants and collection shapes grouped by their three mechanisms, enumerated from this manifest."
|
|
252
268
|
}
|
|
253
269
|
},
|
|
254
270
|
"frontmatter": {
|
|
255
271
|
"engine": [
|
|
256
272
|
"site",
|
|
273
|
+
"theme",
|
|
257
274
|
"layout",
|
|
258
275
|
"view",
|
|
259
276
|
"frame",
|
|
260
277
|
"render",
|
|
261
278
|
"nav",
|
|
262
279
|
"order",
|
|
263
|
-
"tags"
|
|
280
|
+
"tags",
|
|
281
|
+
"cover"
|
|
264
282
|
],
|
|
265
283
|
"passThrough": true
|
|
284
|
+
},
|
|
285
|
+
"themes": {
|
|
286
|
+
"default": {
|
|
287
|
+
"label": "immediately.run",
|
|
288
|
+
"preferred": "dark",
|
|
289
|
+
"ships": true,
|
|
290
|
+
"summary": "The brand applied to a reading surface — the baseline every other look is measured against."
|
|
291
|
+
},
|
|
292
|
+
"archive": {
|
|
293
|
+
"label": "Archive",
|
|
294
|
+
"preferred": "light",
|
|
295
|
+
"ships": true,
|
|
296
|
+
"summary": "Encyclopedic reference: dense, quiet, contents-forward. Source Serif 4 reading face."
|
|
297
|
+
},
|
|
298
|
+
"journal": {
|
|
299
|
+
"label": "Journal",
|
|
300
|
+
"preferred": "light",
|
|
301
|
+
"ships": true,
|
|
302
|
+
"summary": "A personal notebook or family record: warm, short measure, soft frame. Lora reading face."
|
|
303
|
+
},
|
|
304
|
+
"editorial": {
|
|
305
|
+
"label": "Editorial",
|
|
306
|
+
"preferred": "dark",
|
|
307
|
+
"ships": true,
|
|
308
|
+
"summary": "Loud on purpose — a zine, a gig listing, a release archive. Loud through weight and colour, not a new face."
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"pageVariants": {
|
|
312
|
+
"doc": {
|
|
313
|
+
"key": "layout",
|
|
314
|
+
"value": "doc",
|
|
315
|
+
"ships": true,
|
|
316
|
+
"summary": "The default reference page: contents rail, floated infobox, backlink index."
|
|
317
|
+
},
|
|
318
|
+
"post": {
|
|
319
|
+
"key": "layout",
|
|
320
|
+
"value": "post",
|
|
321
|
+
"ships": true,
|
|
322
|
+
"summary": "One measured column, no rails — the long-form entry."
|
|
323
|
+
},
|
|
324
|
+
"full": {
|
|
325
|
+
"key": "layout",
|
|
326
|
+
"value": "full",
|
|
327
|
+
"ships": true,
|
|
328
|
+
"summary": "The widest measure; drawn on the directory board rather than its own."
|
|
329
|
+
},
|
|
330
|
+
"bare": {
|
|
331
|
+
"key": "frame",
|
|
332
|
+
"value": "none",
|
|
333
|
+
"ships": true,
|
|
334
|
+
"summary": "No chrome at all; the entry supplies its own."
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
"layouts": {
|
|
338
|
+
"shell": {
|
|
339
|
+
"layoutRole": "root",
|
|
340
|
+
"arranges": "nav · sidebar · content · footer",
|
|
341
|
+
"ships": true,
|
|
342
|
+
"summary": "The site shell as copyable content. `nav: side` by default; flip the frontmatter to `nav: top` and drop the `<GroveSidebar/>` line for the top-nav arrangement."
|
|
343
|
+
},
|
|
344
|
+
"section": {
|
|
345
|
+
"layoutRole": "section",
|
|
346
|
+
"arranges": "shared hero + <Outlet/> + siblings rail",
|
|
347
|
+
"ships": true,
|
|
348
|
+
"summary": "A folder frame: one shared header above every entry in a subtree, `<ChildPages/>` below. Nest it under a root shell."
|
|
349
|
+
},
|
|
350
|
+
"hero": {
|
|
351
|
+
"layoutRole": "root",
|
|
352
|
+
"arranges": "a full-bleed image band over top navigation, then a short grid",
|
|
353
|
+
"ships": true,
|
|
354
|
+
"summary": "A landing root: `nav: top`, a hero band, then `<DocList shape=\"grid\" limit=\"4\"/>`. The picture slot renders the link-graph lattice until `cover:` lands."
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
"collections": {
|
|
358
|
+
"feed": {
|
|
359
|
+
"component": "DocList",
|
|
360
|
+
"props": {
|
|
361
|
+
"shape": "feed"
|
|
362
|
+
},
|
|
363
|
+
"ships": true,
|
|
364
|
+
"summary": "A dated list of entries — title, date, lede."
|
|
365
|
+
},
|
|
366
|
+
"image-excerpt": {
|
|
367
|
+
"component": "DocList",
|
|
368
|
+
"props": {
|
|
369
|
+
"shape": "grid"
|
|
370
|
+
},
|
|
371
|
+
"ships": true,
|
|
372
|
+
"summary": "Cards with a picture slot and an excerpt. The slot holds the link-graph lattice until `cover:` lands (R3-313); it is not gated on it."
|
|
373
|
+
},
|
|
374
|
+
"timeline": {
|
|
375
|
+
"component": "Timeline",
|
|
376
|
+
"props": {},
|
|
377
|
+
"ships": true,
|
|
378
|
+
"summary": "Entries on a dated spine; already matches the reference archetype."
|
|
379
|
+
},
|
|
380
|
+
"directory": {
|
|
381
|
+
"ships": true,
|
|
382
|
+
"summary": "A folder URL with no `index.mdx` of its own — the generated listing. Not a component call: the mechanism is the URL itself."
|
|
383
|
+
},
|
|
384
|
+
"infinite-scroll": {
|
|
385
|
+
"component": "DocList",
|
|
386
|
+
"props": {
|
|
387
|
+
"paginate": "infinite",
|
|
388
|
+
"batch": "20"
|
|
389
|
+
},
|
|
390
|
+
"ships": true,
|
|
391
|
+
"summary": "The list that grows as you scroll: a windowed feed with a keyboard-actionable sentinel and a stated end. Behaviour like this can ONLY be an engine component's literal prop — never a starter file."
|
|
392
|
+
}
|
|
266
393
|
}
|
|
267
394
|
}
|