@immediately-run/grove 0.1.1
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 +122 -0
- package/docs/ENGINE_BOUNDARY.md +233 -0
- package/llms.txt +115 -0
- package/package.json +82 -0
- package/src/App.tsx +131 -0
- package/src/GroveApp.css +2774 -0
- package/src/GroveWiki.tsx +386 -0
- package/src/components/AssetImage.tsx +58 -0
- package/src/components/Backlinks.tsx +104 -0
- package/src/components/Callout.tsx +26 -0
- package/src/components/ChildPages.tsx +40 -0
- package/src/components/DefaultLayout.tsx +27 -0
- package/src/components/Directory.tsx +65 -0
- package/src/components/DirectoryList.test.tsx +275 -0
- package/src/components/DirectoryList.tsx +189 -0
- package/src/components/DirectoryView.tsx +68 -0
- package/src/components/DocList.tsx +109 -0
- package/src/components/DocsByTag.tsx +13 -0
- package/src/components/Drawer.tsx +45 -0
- package/src/components/EntryHeader.tsx +51 -0
- package/src/components/FamilyTree.tsx +95 -0
- package/src/components/GroveAgent.tsx +264 -0
- package/src/components/GroveFooter.tsx +20 -0
- package/src/components/GroveNav.tsx +102 -0
- package/src/components/Icon.tsx +56 -0
- package/src/components/Infobox.tsx +19 -0
- package/src/components/Kbd.tsx +10 -0
- package/src/components/KeyValue.tsx +32 -0
- package/src/components/Lede.tsx +6 -0
- package/src/components/More.tsx +10 -0
- package/src/components/Outlet.tsx +11 -0
- package/src/components/PageMeta.tsx +24 -0
- package/src/components/PageView.tsx +98 -0
- package/src/components/Quote.tsx +36 -0
- package/src/components/RecentlyUpdated.tsx +6 -0
- package/src/components/SafeEntryBody.tsx +72 -0
- package/src/components/SafeLayout.tsx +35 -0
- package/src/components/ScrollToFragment.tsx +63 -0
- package/src/components/Search.tsx +127 -0
- package/src/components/Sidebar.tsx +118 -0
- package/src/components/TableOfContents.test.tsx +163 -0
- package/src/components/TableOfContents.tsx +101 -0
- package/src/components/TagCloud.tsx +46 -0
- package/src/components/TagList.tsx +31 -0
- package/src/components/Timeline.tsx +55 -0
- package/src/components/Toc.tsx +14 -0
- package/src/components/WikiLink.tsx +112 -0
- package/src/data/themes.ts +14 -0
- package/src/devfs.d.ts +4 -0
- package/src/hooks/useContentComponents.ts +122 -0
- package/src/hooks/useCorpusMetadata.ts +43 -0
- package/src/hooks/useDirectoryListing.ts +56 -0
- package/src/hooks/useHeadings.ts +96 -0
- package/src/hooks/useOpenWikiBoot.ts +95 -0
- package/src/index.css +120 -0
- package/src/lib/compose.test.ts +92 -0
- package/src/lib/compose.ts +99 -0
- package/src/lib/content.test.ts +269 -0
- package/src/lib/content.ts +267 -0
- package/src/lib/contentRoot.ts +61 -0
- package/src/lib/corpusComponents.test.ts +101 -0
- package/src/lib/corpusComponents.ts +117 -0
- package/src/lib/corpusScan.test.ts +157 -0
- package/src/lib/corpusScan.ts +105 -0
- package/src/lib/directory.test.ts +216 -0
- package/src/lib/directory.ts +262 -0
- package/src/lib/fragment.test.ts +88 -0
- package/src/lib/fragment.ts +55 -0
- package/src/lib/frontmatter.ts +26 -0
- package/src/lib/layout.ts +84 -0
- package/src/lib/openWiki.test.ts +216 -0
- package/src/lib/openWiki.ts +84 -0
- package/src/lib/queries.test.ts +74 -0
- package/src/lib/queries.ts +84 -0
- package/src/lib/safeIntrinsics.test.tsx +99 -0
- package/src/lib/safeIntrinsics.tsx +77 -0
- package/src/lib/safeRender.test.ts +359 -0
- package/src/lib/safeSources.ts +25 -0
- package/src/lib/shell.ts +71 -0
- package/src/lib/sourceCache.test.ts +66 -0
- package/src/lib/sourceCache.ts +42 -0
- package/src/lib/tocScroll.test.ts +71 -0
- package/src/lib/tocScroll.ts +93 -0
- package/src/lib/wiki.test.ts +194 -0
- package/src/lib/wiki.ts +175 -0
- package/src/lib.ts +54 -0
- package/src/main.tsx +19 -0
- package/src/mdx.d.ts +9 -0
- package/src/mdxComponents.ts +89 -0
- package/src/test/setup.ts +19 -0
- package/viewer-manifest.schema.json +62 -0
- package/viewer.manifest.json +267 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Link, requestEdit } from '@immediately-run/sdk';
|
|
2
|
+
import { useShell } from '../lib/shell';
|
|
3
|
+
import { THEMES } from '../data/themes';
|
|
4
|
+
import Icon from './Icon';
|
|
5
|
+
|
|
6
|
+
// `<GroveNav/>` — the top navigation bar. Import-free in MDX, so a layout places
|
|
7
|
+
// it with `<GroveNav/>`. Its interactive state (drawer / search / theme menu)
|
|
8
|
+
// lives in the shell context; this component only arranges and drives it.
|
|
9
|
+
export default function GroveNav() {
|
|
10
|
+
const {
|
|
11
|
+
siteTitle,
|
|
12
|
+
navItems,
|
|
13
|
+
entryKey,
|
|
14
|
+
writable,
|
|
15
|
+
theme,
|
|
16
|
+
setTheme,
|
|
17
|
+
light,
|
|
18
|
+
setLight,
|
|
19
|
+
menuOpen,
|
|
20
|
+
setMenuOpen,
|
|
21
|
+
setSearchOpen,
|
|
22
|
+
setDrawerOpen,
|
|
23
|
+
} = useShell();
|
|
24
|
+
|
|
25
|
+
const askGrove = () => {
|
|
26
|
+
const el = (document.querySelector('.ga-foot input') || document.querySelector('.ga-line input')) as HTMLElement | null;
|
|
27
|
+
el?.focus();
|
|
28
|
+
};
|
|
29
|
+
const newEntry = () => requestEdit({ path: 'content/untitled.mdx' }).catch(() => undefined);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<nav className="grove-nav">
|
|
33
|
+
<button className="grove-hamburger icbtn" aria-label="Menu" onClick={() => setDrawerOpen(true)}>
|
|
34
|
+
<Icon name="list" />
|
|
35
|
+
</button>
|
|
36
|
+
<Link href="/" className="grove-brand">
|
|
37
|
+
<span className="tile" style={{ background: 'var(--grad)' }} />
|
|
38
|
+
{siteTitle}
|
|
39
|
+
</Link>
|
|
40
|
+
<div className="grove-nav__links">
|
|
41
|
+
{navItems.map((n) => (
|
|
42
|
+
<Link key={n.key} href={n.href} data-cur={n.key === entryKey ? '1' : '0'}>
|
|
43
|
+
{n.label}
|
|
44
|
+
</Link>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
<div className="grove-nav__cluster">
|
|
48
|
+
<button className="icbtn" aria-label="Search" onClick={() => setSearchOpen(true)}>
|
|
49
|
+
<Icon name="search" />
|
|
50
|
+
</button>
|
|
51
|
+
{writable && (
|
|
52
|
+
<button className="icbtn" aria-label="New entry" onClick={newEntry}>
|
|
53
|
+
<Icon name="plus" />
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
<button className="icbtn" aria-label="Ask Grove" onClick={askGrove}>
|
|
57
|
+
<Icon name="message" />
|
|
58
|
+
</button>
|
|
59
|
+
<div className="grove-nav__more">
|
|
60
|
+
<button className="icbtn" title="Theme" aria-expanded={menuOpen} onClick={() => setMenuOpen((o) => !o)}>
|
|
61
|
+
☀
|
|
62
|
+
</button>
|
|
63
|
+
{menuOpen ? (
|
|
64
|
+
<>
|
|
65
|
+
<div className="gtm__scrim" onClick={() => setMenuOpen(false)} />
|
|
66
|
+
<div className="grove-theme-menu" role="menu">
|
|
67
|
+
<div className="gtm__h">Theme</div>
|
|
68
|
+
<div className="gtm__list">
|
|
69
|
+
{THEMES.map((t) => (
|
|
70
|
+
<button
|
|
71
|
+
key={t.id}
|
|
72
|
+
className="gtm__row"
|
|
73
|
+
data-cur={theme === t.id ? '1' : '0'}
|
|
74
|
+
onClick={() => setTheme(t.id)}
|
|
75
|
+
>
|
|
76
|
+
<span className="gtm__sw" style={{ background: t.swatch }} />
|
|
77
|
+
<span className="gtm__name">{t.label}</span>
|
|
78
|
+
{theme === t.id ? <span className="gtm__ck"><Icon name="check" /></span> : null}
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
{theme === 'default' ? (
|
|
83
|
+
<div className="gtm__appearance">
|
|
84
|
+
<div className="gtm__sub">Appearance</div>
|
|
85
|
+
<div className="gtm__seg">
|
|
86
|
+
<button data-on={!light ? '1' : '0'} onClick={() => setLight(false)}>
|
|
87
|
+
<Icon name="moon" /> Dark
|
|
88
|
+
</button>
|
|
89
|
+
<button data-on={light ? '1' : '0'} onClick={() => setLight(true)}>
|
|
90
|
+
<Icon name="sun" /> Light
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
</>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</nav>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
// The Lucide micro-set Grove needs, inlined as path data so the app carries no
|
|
4
|
+
// icon dependency and every glyph inherits `currentColor` + the CSS-sized box
|
|
5
|
+
// (HANDOFF constraint 4: Lucide, ~1.75 stroke, no emoji). One default-exported
|
|
6
|
+
// component keyed by `name` keeps the Fast-Refresh "one component per file" rule.
|
|
7
|
+
const PATHS: Record<string, string> = {
|
|
8
|
+
search: 'M21 21l-4.34-4.34M17 11a6 6 0 11-12 0 6 6 0 0112 0z',
|
|
9
|
+
plus: 'M5 12h14M12 5v14',
|
|
10
|
+
pencil: 'M21.17 6.83l-3-3L4 18l-1 4 4-1zM14.5 6.5l3 3',
|
|
11
|
+
send: 'M12 19V5M5 12l7-7 7 7',
|
|
12
|
+
x: 'M18 6L6 18M6 6l12 12',
|
|
13
|
+
'chevron-right': 'M9 6l6 6-6 6',
|
|
14
|
+
'chevron-down': 'M6 9l6 6 6-6',
|
|
15
|
+
unlink:
|
|
16
|
+
'M16.5 9.4l2.1-2.1a3 3 0 000-4.2l-.7-.7a3 3 0 00-4.2 0L11.6 4.5M7.5 14.6l-2.1 2.1a3 3 0 000 4.2l.7.7a3 3 0 004.2 0l1.9-1.9M8 12l8 0',
|
|
17
|
+
alert: 'M10.3 3.8L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L14.7 3.8a2 2 0 00-3.4 0zM12 9v4M12 17h.01',
|
|
18
|
+
sparkles:
|
|
19
|
+
'M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9zM19 3v4M21 5h-4M5 17v2M6 18H4',
|
|
20
|
+
folder: 'M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z',
|
|
21
|
+
file: 'M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8zM14 3v5h5M9 13h6M9 17h6',
|
|
22
|
+
'file-plus': 'M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8zM14 3v5h5M12 12v6M9 15h6',
|
|
23
|
+
check: 'M20 6L9 17l-5-5',
|
|
24
|
+
undo: 'M3 7v6h6M3 13a9 9 0 109-9 9 9 0 00-7.5 4',
|
|
25
|
+
external: 'M15 3h6v6M10 14L21 3M19 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h6',
|
|
26
|
+
sun: 'M12 17a5 5 0 100-10 5 5 0 000 10zM12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
|
|
27
|
+
moon: 'M21 12.8A9 9 0 1111.2 3a7 7 0 009.8 9.8z',
|
|
28
|
+
message: 'M21 11.5a8.5 8.5 0 01-12.3 7.6L3 21l1.9-5.7A8.5 8.5 0 1121 11.5z',
|
|
29
|
+
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
|
|
30
|
+
list: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01',
|
|
31
|
+
stop: 'M6 6h12v12H6z',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface IconProps extends SVGProps<SVGSVGElement> {
|
|
35
|
+
name: keyof typeof PATHS | string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function Icon({ name, ...rest }: IconProps) {
|
|
39
|
+
const d = PATHS[name] || '';
|
|
40
|
+
return (
|
|
41
|
+
<svg
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth={1.75}
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{d.split('M').filter(Boolean).map((seg, i) => (
|
|
52
|
+
<path key={i} d={'M' + seg} />
|
|
53
|
+
))}
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
title?: string;
|
|
5
|
+
cover?: boolean;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Import-free engine component: a sidebar fact panel. Authors put a markdown
|
|
10
|
+
// list of `- Key: value` lines inside; it renders as the panel body.
|
|
11
|
+
export default function Infobox({ title, cover, children }: Props) {
|
|
12
|
+
return (
|
|
13
|
+
<aside className="grove-infobox">
|
|
14
|
+
{title ? <div className="grove-infobox__h">{title}</div> : null}
|
|
15
|
+
{cover ? <div className="grove-infobox__cover" /> : null}
|
|
16
|
+
<div className="grove-infobox__body">{children}</div>
|
|
17
|
+
</aside>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Import-free engine component: a keyboard key cap, e.g. <Kbd>⌘K</Kbd>.
|
|
8
|
+
export default function Kbd({ children }: Props) {
|
|
9
|
+
return <kbd className="grove-kbd">{children}</kbd>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Pair {
|
|
4
|
+
/** Label — rendered in Space Mono, uppercased. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Value — a plain string or arbitrary ReactNode (links, code, etc.). */
|
|
7
|
+
value: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Label/value pairs, rendered top-to-bottom as <dt>/<dd> rows. */
|
|
12
|
+
pairs: Pair[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Import-free engine component: a compact definition list of label/value pairs
|
|
16
|
+
// (contact details, metadata, quick facts). `label` is set in Space Mono and
|
|
17
|
+
// uppercased; `value` may be a string or any ReactNode, so links and inline
|
|
18
|
+
// code compose naturally. Rendered as a <dl> of <dt>/<dd> pairs with hairline
|
|
19
|
+
// row separators and a panel background, using the same design tokens as the
|
|
20
|
+
// rest of the component vocabulary.
|
|
21
|
+
export default function KeyValue({ pairs }: Props) {
|
|
22
|
+
return (
|
|
23
|
+
<dl className="grove-keyvalue">
|
|
24
|
+
{pairs.map((p, i) => (
|
|
25
|
+
<div className="grove-keyvalue__row" key={i}>
|
|
26
|
+
<dt className="grove-keyvalue__label">{p.label}</dt>
|
|
27
|
+
<dd className="grove-keyvalue__value">{p.value}</dd>
|
|
28
|
+
</div>
|
|
29
|
+
))}
|
|
30
|
+
</dl>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { OutletContext } from '../lib/shell';
|
|
3
|
+
|
|
4
|
+
// `<Outlet/>` — the injection point inside a layout (`_layout.mdx`). It renders
|
|
5
|
+
// whatever the engine placed one level inward: the next nested layout, or — at
|
|
6
|
+
// the bottom of the chain — the page itself. Import-free in MDX (registered in
|
|
7
|
+
// GROVE_MDX), so a layout author just writes `<Outlet/>` where the page goes.
|
|
8
|
+
// The React Router analogue, adapted to Grove's content-is-chrome model.
|
|
9
|
+
export default function Outlet() {
|
|
10
|
+
return <>{useContext(OutletContext)}</>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { useFileMetadata } from '@immediately-run/sdk';
|
|
4
|
+
import { TinkerableContext } from '@immediately-run/sdk/TinkerableContext';
|
|
5
|
+
import { sandboxPathToKey } from '../lib/content';
|
|
6
|
+
|
|
7
|
+
// `<PageMeta/>` — the current entry's date + tags as a styled strip, for authors
|
|
8
|
+
// who want the meta inside the body (the entry header already renders one).
|
|
9
|
+
export default function PageMeta() {
|
|
10
|
+
const ctx = useContext(TinkerableContext) as any;
|
|
11
|
+
const key = sandboxPathToKey(ctx?.navigationState?.sandboxPath || '/');
|
|
12
|
+
const meta = useFileMetadata(key) as any;
|
|
13
|
+
if (!meta) return null;
|
|
14
|
+
const tags: string[] = Array.isArray(meta.tags) ? meta.tags.filter((t: string) => !t.startsWith('ui/')) : [];
|
|
15
|
+
return (
|
|
16
|
+
<div className="grove-meta">
|
|
17
|
+
{meta.date && <span>{meta.date}</span>}
|
|
18
|
+
{tags.length ? <span className="dot">·</span> : null}
|
|
19
|
+
{tags.map((t) => (
|
|
20
|
+
<span key={t} className="grove-tag">#{t}</span>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Include, Link } from '@immediately-run/sdk';
|
|
3
|
+
import { useShell } from '../lib/shell';
|
|
4
|
+
import { keyToHref, keyToRepoRel } from '../lib/content';
|
|
5
|
+
import { crumb } from '../lib/wiki';
|
|
6
|
+
import { requestEdit } from '@immediately-run/sdk';
|
|
7
|
+
import DirectoryView from './DirectoryView';
|
|
8
|
+
import EntryHeader from './EntryHeader';
|
|
9
|
+
import SafeEntryBody from './SafeEntryBody';
|
|
10
|
+
import ScrollToFragment from './ScrollToFragment';
|
|
11
|
+
import Toc from './Toc';
|
|
12
|
+
import Backlinks from './Backlinks';
|
|
13
|
+
import Icon from './Icon';
|
|
14
|
+
|
|
15
|
+
declare const module: any;
|
|
16
|
+
|
|
17
|
+
// `<PageView/>` — the reading view for the current entry: the 404/missing state,
|
|
18
|
+
// or the entry header + prose body (+ ToC / backlinks rails). This is what the
|
|
19
|
+
// INNERMOST `<Outlet/>` renders at the bottom of the layout chain. It carries no
|
|
20
|
+
// site chrome (nav / sidebar / footer) — that's the layout's job — so the page
|
|
21
|
+
// stays free of shell concerns.
|
|
22
|
+
export default function PageView() {
|
|
23
|
+
const { entryKey, includePath, layout, showRails, mins, missing, suggestion, writable, vw, safe, directory } = useShell();
|
|
24
|
+
|
|
25
|
+
// A folder URL. `checking` renders nothing rather than the 404: the readdir that
|
|
26
|
+
// decides between them is one RPC away, and a 404 that appears and then turns into a
|
|
27
|
+
// listing reads as a broken link that healed itself.
|
|
28
|
+
if (directory.status === 'checking') return <div className="grove-state" data-state="checking" />;
|
|
29
|
+
if (directory.status === 'ready') return <DirectoryView />;
|
|
30
|
+
|
|
31
|
+
if (missing) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="grove-state">
|
|
34
|
+
<div className="grove-state__art" />
|
|
35
|
+
<h2>No entry at <code>{keyToRepoRel(entryKey).replace(/^content/, '')}</code>.</h2>
|
|
36
|
+
<p>
|
|
37
|
+
That link points to an entry that doesn’t exist yet.
|
|
38
|
+
{suggestion ? <> Did you mean <Link className="grove-wikilink" data-state="ok" href={keyToHref(suggestion)}>{crumb(suggestion)}</Link>?</> : null}
|
|
39
|
+
</p>
|
|
40
|
+
<div className="grove-state__actions">
|
|
41
|
+
<Link className="btn-ghost" href="/"><Icon name="chevron-right" /> Back to home</Link>
|
|
42
|
+
{writable ? <button className="btn-primary" onClick={() => requestEdit({ path: keyToRepoRel(entryKey) }).catch(() => undefined)}><Icon name="file-plus" /> Create it</button> : null}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<article className="grove-page" data-layout={layout}>
|
|
50
|
+
<div className="gp-main">
|
|
51
|
+
<EntryHeader entryKey={entryKey} writable={writable} mins={mins} />
|
|
52
|
+
{showRails && vw === 'mobile' ? (
|
|
53
|
+
<details className="grove-toc__disclosure">
|
|
54
|
+
<summary>On this page</summary>
|
|
55
|
+
<Toc entryKey={entryKey} />
|
|
56
|
+
</details>
|
|
57
|
+
) : null}
|
|
58
|
+
<div className="grove-prose">
|
|
59
|
+
{/* Interpreter mode (R3-213) renders the raw entry as data — no author JS runs;
|
|
60
|
+
executable mode compiles + runs the MDX via <Include>. Both are supported,
|
|
61
|
+
and since R3-252 the choice is per-entry as well as wiki-wide.
|
|
62
|
+
|
|
63
|
+
The compiled branch mounts <ScrollToFragment> too — deep-link landing is not
|
|
64
|
+
a safe-path feature — but deliberately carries NO `data-entry` marker, and
|
|
65
|
+
both halves of that are load-bearing (R3-252).
|
|
66
|
+
|
|
67
|
+
`key={entryKey}` forces an unmount across a client-side navigation. Without
|
|
68
|
+
it React reuses this subtree and keeps the PREVIOUS document mounted while
|
|
69
|
+
the new one compiles, so `#sec-4` resolves against the wrong entry — every
|
|
70
|
+
entry here numbers its sections from 1, so that lookup always succeeds and
|
|
71
|
+
always lands wrong. Measured on the host: a citation to core_concepts §4
|
|
72
|
+
("4 — Principal") scrolled to a different document's "4. The provider-facing
|
|
73
|
+
contract" (R3-249's hazard, reintroduced).
|
|
74
|
+
|
|
75
|
+
No marker, because `<Include>` owns an internal <Suspense> and this wrapper
|
|
76
|
+
is OUTSIDE it — so a marker here would appear the instant we navigate, while
|
|
77
|
+
the body is still compiling, and claim a commit that has not happened. That
|
|
78
|
+
is worse than no signal: `data-entry` means "this subtree holds entry X's
|
|
79
|
+
committed body" everywhere else (`SafeEntryBody` puts it inside the suspended
|
|
80
|
+
subtree, so it is true there), and the on-host harness waits on it. Keying
|
|
81
|
+
already removes the hazard the marker defended against — the outgoing
|
|
82
|
+
document is gone from the DOM, not lingering — so `resolveFragmentTarget`'s
|
|
83
|
+
documented document-wide fallback for the compiled path is safe here. */}
|
|
84
|
+
{safe ? (
|
|
85
|
+
<SafeEntryBody entryKey={entryKey} />
|
|
86
|
+
) : (
|
|
87
|
+
<div className="grove-entry-body" key={entryKey}>
|
|
88
|
+
<Include filename={includePath} baseModule={module} />
|
|
89
|
+
<ScrollToFragment entryKey={entryKey} />
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
{showRails ? <Backlinks /> : null}
|
|
94
|
+
</div>
|
|
95
|
+
{showRails && vw === 'desktop' ? <Toc entryKey={entryKey} /> : null}
|
|
96
|
+
</article>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import WikiLink from './WikiLink';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Attribution — a person, role, or document. Rendered in mono as "— cite". */
|
|
6
|
+
cite?: string;
|
|
7
|
+
/** Wiki path of the entry the quote is drawn from, e.g.
|
|
8
|
+
* "content/handbook/onboarding.mdx". When set, a "Source →" link is shown. */
|
|
9
|
+
source?: string;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Import-free engine component: a pull-quote with a gradient accent spine and a
|
|
14
|
+
// large gradient opening quote mark. `cite` is the attribution (Space Mono);
|
|
15
|
+
// `source`, when given, links to the entry the quote came from — its
|
|
16
|
+
// resolved / broken / self state is decided by the shared <WikiLink> resolver, so
|
|
17
|
+
// a moved or missing target reads as broken rather than a dead link (no
|
|
18
|
+
// hand-rolled href logic here, just the wiki path → in-app href normalization).
|
|
19
|
+
export default function Quote({ cite, source, children }: Props) {
|
|
20
|
+
const href = source ? (source.startsWith('/') ? source : `/${source}`) : null;
|
|
21
|
+
return (
|
|
22
|
+
<figure className="grove-quote">
|
|
23
|
+
<blockquote className="grove-quote__body">{children}</blockquote>
|
|
24
|
+
{(cite || href) && (
|
|
25
|
+
<figcaption className="grove-quote__cap">
|
|
26
|
+
{cite ? <span className="grove-quote__cite">{cite}</span> : null}
|
|
27
|
+
{href ? (
|
|
28
|
+
<WikiLink href={href} className="grove-quote__src">
|
|
29
|
+
Source →
|
|
30
|
+
</WikiLink>
|
|
31
|
+
) : null}
|
|
32
|
+
</figcaption>
|
|
33
|
+
)}
|
|
34
|
+
</figure>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import DocList from './DocList';
|
|
2
|
+
|
|
3
|
+
// `<RecentlyUpdated limit>` — a compact, date-sorted feed of the latest entries.
|
|
4
|
+
export default function RecentlyUpdated({ limit = 6, title }: { limit?: string | number; title?: string }) {
|
|
5
|
+
return <DocList shape="feed" sort="date" limit={limit} title={title ?? 'Recently updated'} />;
|
|
6
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Suspense, use } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
SafeContent,
|
|
4
|
+
RenderExportedComponentContext,
|
|
5
|
+
useFileMetadata,
|
|
6
|
+
} from '@immediately-run/sdk';
|
|
7
|
+
import { SAFE_MDX } from '../mdxComponents';
|
|
8
|
+
import { safeSources } from '../lib/safeSources';
|
|
9
|
+
import ScrollToFragment from './ScrollToFragment';
|
|
10
|
+
|
|
11
|
+
// The NON-EXECUTABLE (interpreter) body renderer — TRUST_MODES §5.1 / R3-213. It reads
|
|
12
|
+
// the entry's RAW `.mdx` and renders it through the SDK safe renderer (parseSafeMdast +
|
|
13
|
+
// renderMdast): **no author JavaScript executes** — a stray `{fetch(...)}` in any entry
|
|
14
|
+
// is inert literal text. This is the alternative to `<Include>` (the executable/executor
|
|
15
|
+
// path, where compiled MDX runs) that `PageView` uses when the wiki is NOT in interpreter
|
|
16
|
+
// mode. The engine keeps BOTH; the wiki declares which via the home entry's `render:` flag,
|
|
17
|
+
// so a trusted first-party grove deployment can still use standard, executable MDX.
|
|
18
|
+
//
|
|
19
|
+
// Uniformity (R3-213): the component map is `SAFE_MDX` — the SDK's DEFAULT_MDX_COMPONENTS
|
|
20
|
+
// (the platform Admonition / WikiLink / HeadingAnchor, carrying the R3-212 deep-link
|
|
21
|
+
// resolver) merged UNDER the Grove vocabulary, the SAME merge `boot()` does for the compiled
|
|
22
|
+
// path — so the admonitions / wiki-links / `sec-…` heading ids the shared remark plugins emit
|
|
23
|
+
// render identically in both standards. Since R3-263 that map lives in `mdxComponents.ts` and
|
|
24
|
+
// is shared with `SafeLayout`, so the body and the shell around it cannot resolve differently.
|
|
25
|
+
// We publish `RenderExportedComponentContext` with this entry's path so the WikiLink resolver
|
|
26
|
+
// learns the current file and a relative `[[target]]` (and its `#sec-…` fragment) resolves
|
|
27
|
+
// exactly as it does under `<Include>`.
|
|
28
|
+
|
|
29
|
+
/** Strip a leading YAML frontmatter block (and an optional BOM) so only the body reaches
|
|
30
|
+
* the safe renderer. */
|
|
31
|
+
function stripFrontmatter(src: string): string {
|
|
32
|
+
return src.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function SafeBody({ entryKey }: { entryKey: string }) {
|
|
36
|
+
const raw = use(safeSources.read(entryKey));
|
|
37
|
+
// Consume the entry's metadata so this re-renders if frontmatter changes (parity with
|
|
38
|
+
// the compiled path's reactivity); the body itself is the stripped source.
|
|
39
|
+
useFileMetadata(entryKey);
|
|
40
|
+
const body = stripFrontmatter(raw);
|
|
41
|
+
return (
|
|
42
|
+
<RenderExportedComponentContext
|
|
43
|
+
value={{ evaluationContext: { evaluation: { module: { filepath: entryKey } } } } as never}
|
|
44
|
+
>
|
|
45
|
+
{/* `data-entry` marks WHICH entry these bytes are, and it is rendered INSIDE the
|
|
46
|
+
suspended subtree — so it flips only when the new body actually commits, never
|
|
47
|
+
while the previous entry is still on screen. `<ScrollToFragment>` waits for it
|
|
48
|
+
before scrolling. Without that signal a deep-link scrolls the OUTGOING document:
|
|
49
|
+
every spec has `sec-1 … sec-N`, so the id the incoming page wants is already in
|
|
50
|
+
the DOM, and a scroll fired at navigation time lands on the wrong page's section
|
|
51
|
+
and is then thrown away by the re-render (R3-249). */}
|
|
52
|
+
<div className="grove-entry-body" data-entry={entryKey}>
|
|
53
|
+
<SafeContent source={body} components={SAFE_MDX as never} fallback={<div className="grove-prose__loading" />} />
|
|
54
|
+
{/* Deep-link landing lives INSIDE the committed body, not outside watching for it.
|
|
55
|
+
Mounted anywhere above this boundary it is unmounted by the very transition it
|
|
56
|
+
is waiting on — measured on the real host: it polled 159 times and was torn
|
|
57
|
+
down at the exact moment the new body arrived. Here it mounts *because* the
|
|
58
|
+
content did, so there is nothing to wait for. */}
|
|
59
|
+
<ScrollToFragment entryKey={entryKey} />
|
|
60
|
+
</div>
|
|
61
|
+
</RenderExportedComponentContext>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Interpreter-mode entry body: the raw `.mdx` rendered as data (no author JS runs). */
|
|
66
|
+
export default function SafeEntryBody({ entryKey }: { entryKey: string }) {
|
|
67
|
+
return (
|
|
68
|
+
<Suspense fallback={<div className="grove-prose__loading" />}>
|
|
69
|
+
<SafeBody entryKey={entryKey} />
|
|
70
|
+
</Suspense>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Include } from '@immediately-run/sdk';
|
|
2
|
+
import { SAFE_MDX } from '../mdxComponents';
|
|
3
|
+
|
|
4
|
+
// A `_layout.mdx` rendered NON-EXECUTABLY — now a thin call to the PLATFORM's interpreter
|
|
5
|
+
// include rather than Grove's own reader. (R3-263: (a) built it here, (b) moved it into the
|
|
6
|
+
// SDK, this switch consumes it.)
|
|
7
|
+
//
|
|
8
|
+
// WHY THE LAYOUT NEEDS THIS AT ALL. `renderLayers` used to send every layer through
|
|
9
|
+
// `<Include>` — the compiled path — *regardless* of `render: safe`. So an interpreter-mode
|
|
10
|
+
// wiki still EXECUTED author JavaScript out of one content file, and the non-executable
|
|
11
|
+
// guarantee had a hole in the shell rather than in the entries. It was also inoperable under
|
|
12
|
+
// dispatch: the compiled path evaluates an app-source module, which a mount-resident layout
|
|
13
|
+
// is not.
|
|
14
|
+
//
|
|
15
|
+
// WHY IT IS NOW THE SDK'S JOB. The same capability is what whiteboard object bodies and the
|
|
16
|
+
// `AGENT_AUTHORING §10` MDX-from-mount gate need — a gate `TRUST_MODES §5.1` says MUST
|
|
17
|
+
// terminate in the safe renderer and never in compiled MDX. Keeping a second implementation
|
|
18
|
+
// here would let the platform path and the path Grove actually exercises drift apart; and
|
|
19
|
+
// because the SDK's own render-level coverage is bounded by its CJS test runner, this wiki
|
|
20
|
+
// exercising the real thing is what keeps the platform path honest.
|
|
21
|
+
//
|
|
22
|
+
// WHAT GROVE STILL OWNS: the component map. `<Include mode="interpreted">` resolves components
|
|
23
|
+
// from `useMDXComponents` — the map `boot({ mdxComponents })` established — which for this app
|
|
24
|
+
// is `SAFE_MDX` (`mdxComponents.ts`): the SDK defaults under the Grove vocabulary, plus the
|
|
25
|
+
// sanitizing structural tags (`lib/safeIntrinsics.tsx`). That is app policy and stays here.
|
|
26
|
+
//
|
|
27
|
+
// THE AUTHORING CONSTRAINTS ARE UNCHANGED and documented in `docs/ENGINE_BOUNDARY.md §6`:
|
|
28
|
+
// literal attributes only; an unregistered tag collapses to a Fragment that keeps its
|
|
29
|
+
// children; an `import` renders as visible prose rather than resolving; and a block tag must
|
|
30
|
+
// open on its own line or micromark consumes it as an HTML block.
|
|
31
|
+
|
|
32
|
+
/** Interpreter-mode layout layer: the raw `_layout.mdx` rendered as data (no author JS runs). */
|
|
33
|
+
export default function SafeLayout({ layoutKey }: { layoutKey: string }) {
|
|
34
|
+
return <Include filename={layoutKey} mode="interpreted" components={SAFE_MDX as never} />;
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useContext, useEffect } from 'react';
|
|
3
|
+
import { TinkerableContext } from '@immediately-run/sdk/TinkerableContext';
|
|
4
|
+
import { fragmentOf, resolveFragmentTarget } from '../lib/fragment';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Land a deep-link on its section.
|
|
8
|
+
*
|
|
9
|
+
* **Mounted inside the entry body it belongs to** (`SafeEntryBody`), which is the whole
|
|
10
|
+
* trick. The obvious placement — up in `PageView`, watching for the new content to arrive —
|
|
11
|
+
* does not work: the component is unmounted by the very transition it is waiting on.
|
|
12
|
+
* Measured on the real host, it polled 159 times and was torn down at the exact moment the
|
|
13
|
+
* new body committed, so it never saw the thing it existed to see. Rendered *within* the
|
|
14
|
+
* body, it mounts because the content did; the first attempt succeeds and there is nothing
|
|
15
|
+
* to wait for.
|
|
16
|
+
*
|
|
17
|
+
* The target is still resolved against `data-entry` rather than looked up blindly, because
|
|
18
|
+
* the outgoing document lingers briefly during a navigation and every document in this
|
|
19
|
+
* corpus numbers its sections from 1 — `#sec-4` exists in almost all of them, so an
|
|
20
|
+
* unscoped lookup can land on the page you just left (R3-249).
|
|
21
|
+
*
|
|
22
|
+
* Requires a host that forwards the fragment (`site-main` #272). Before that fix
|
|
23
|
+
* `navigationState.hash` was always empty inside the sandbox, and no app could do this at
|
|
24
|
+
* all. The SDK's own `<ScrollAfterNavigation>` is not used: it scrolls on its first hit and
|
|
25
|
+
* returns, and with repeated section ids that first hit is the outgoing document's.
|
|
26
|
+
*/
|
|
27
|
+
export default function ScrollToFragment({ entryKey }: { entryKey: string }) {
|
|
28
|
+
const ctx = useContext(TinkerableContext) as any;
|
|
29
|
+
const hash = ctx?.navigationState?.hash ?? '';
|
|
30
|
+
const frag = fragmentOf(hash);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!frag || typeof document === 'undefined') return;
|
|
34
|
+
let done = false;
|
|
35
|
+
|
|
36
|
+
const attempt = () => {
|
|
37
|
+
if (done) return true;
|
|
38
|
+
const el = resolveFragmentTarget(document, entryKey, frag);
|
|
39
|
+
if (!el) return false;
|
|
40
|
+
el.scrollIntoView();
|
|
41
|
+
done = true;
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// The first attempt succeeds on the safe path — that effect runs after the body has
|
|
46
|
+
// committed. On the COMPILED path it cannot: `<Include>` owns an internal Suspense, so
|
|
47
|
+
// this mounts while the entry is still being compiled in-browser, and the retry is the
|
|
48
|
+
// whole mechanism rather than a fallback.
|
|
49
|
+
//
|
|
50
|
+
// The window is sized from measurement, not taste (R3-252). Time from mount to a
|
|
51
|
+
// rendered body on the real host: ~2.6s for a 3K entry, ~12s for 37K, ~24s for the
|
|
52
|
+
// 282K `UI_AS_APPS_SPEC`. The old 5s bound was tuned for the safe renderer (~2–10s)
|
|
53
|
+
// and silently expired mid-compile on any large spec — the citation landed on the
|
|
54
|
+
// page and never reached the section. 30s clears the largest entry with margin;
|
|
55
|
+
// polling is 200ms, it stops on the first success, and it cannot outlive the entry.
|
|
56
|
+
if (attempt()) return;
|
|
57
|
+
const timer = setInterval(() => { if (attempt()) clearInterval(timer); }, 200);
|
|
58
|
+
const stop = setTimeout(() => clearInterval(timer), 30_000);
|
|
59
|
+
return () => { clearInterval(timer); clearTimeout(stop); };
|
|
60
|
+
}, [entryKey, frag]);
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|