@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.
Files changed (92) hide show
  1. package/README.md +122 -0
  2. package/docs/ENGINE_BOUNDARY.md +233 -0
  3. package/llms.txt +115 -0
  4. package/package.json +82 -0
  5. package/src/App.tsx +131 -0
  6. package/src/GroveApp.css +2774 -0
  7. package/src/GroveWiki.tsx +386 -0
  8. package/src/components/AssetImage.tsx +58 -0
  9. package/src/components/Backlinks.tsx +104 -0
  10. package/src/components/Callout.tsx +26 -0
  11. package/src/components/ChildPages.tsx +40 -0
  12. package/src/components/DefaultLayout.tsx +27 -0
  13. package/src/components/Directory.tsx +65 -0
  14. package/src/components/DirectoryList.test.tsx +275 -0
  15. package/src/components/DirectoryList.tsx +189 -0
  16. package/src/components/DirectoryView.tsx +68 -0
  17. package/src/components/DocList.tsx +109 -0
  18. package/src/components/DocsByTag.tsx +13 -0
  19. package/src/components/Drawer.tsx +45 -0
  20. package/src/components/EntryHeader.tsx +51 -0
  21. package/src/components/FamilyTree.tsx +95 -0
  22. package/src/components/GroveAgent.tsx +264 -0
  23. package/src/components/GroveFooter.tsx +20 -0
  24. package/src/components/GroveNav.tsx +102 -0
  25. package/src/components/Icon.tsx +56 -0
  26. package/src/components/Infobox.tsx +19 -0
  27. package/src/components/Kbd.tsx +10 -0
  28. package/src/components/KeyValue.tsx +32 -0
  29. package/src/components/Lede.tsx +6 -0
  30. package/src/components/More.tsx +10 -0
  31. package/src/components/Outlet.tsx +11 -0
  32. package/src/components/PageMeta.tsx +24 -0
  33. package/src/components/PageView.tsx +98 -0
  34. package/src/components/Quote.tsx +36 -0
  35. package/src/components/RecentlyUpdated.tsx +6 -0
  36. package/src/components/SafeEntryBody.tsx +72 -0
  37. package/src/components/SafeLayout.tsx +35 -0
  38. package/src/components/ScrollToFragment.tsx +63 -0
  39. package/src/components/Search.tsx +127 -0
  40. package/src/components/Sidebar.tsx +118 -0
  41. package/src/components/TableOfContents.test.tsx +163 -0
  42. package/src/components/TableOfContents.tsx +101 -0
  43. package/src/components/TagCloud.tsx +46 -0
  44. package/src/components/TagList.tsx +31 -0
  45. package/src/components/Timeline.tsx +55 -0
  46. package/src/components/Toc.tsx +14 -0
  47. package/src/components/WikiLink.tsx +112 -0
  48. package/src/data/themes.ts +14 -0
  49. package/src/devfs.d.ts +4 -0
  50. package/src/hooks/useContentComponents.ts +122 -0
  51. package/src/hooks/useCorpusMetadata.ts +43 -0
  52. package/src/hooks/useDirectoryListing.ts +56 -0
  53. package/src/hooks/useHeadings.ts +96 -0
  54. package/src/hooks/useOpenWikiBoot.ts +95 -0
  55. package/src/index.css +120 -0
  56. package/src/lib/compose.test.ts +92 -0
  57. package/src/lib/compose.ts +99 -0
  58. package/src/lib/content.test.ts +269 -0
  59. package/src/lib/content.ts +267 -0
  60. package/src/lib/contentRoot.ts +61 -0
  61. package/src/lib/corpusComponents.test.ts +101 -0
  62. package/src/lib/corpusComponents.ts +117 -0
  63. package/src/lib/corpusScan.test.ts +157 -0
  64. package/src/lib/corpusScan.ts +105 -0
  65. package/src/lib/directory.test.ts +216 -0
  66. package/src/lib/directory.ts +262 -0
  67. package/src/lib/fragment.test.ts +88 -0
  68. package/src/lib/fragment.ts +55 -0
  69. package/src/lib/frontmatter.ts +26 -0
  70. package/src/lib/layout.ts +84 -0
  71. package/src/lib/openWiki.test.ts +216 -0
  72. package/src/lib/openWiki.ts +84 -0
  73. package/src/lib/queries.test.ts +74 -0
  74. package/src/lib/queries.ts +84 -0
  75. package/src/lib/safeIntrinsics.test.tsx +99 -0
  76. package/src/lib/safeIntrinsics.tsx +77 -0
  77. package/src/lib/safeRender.test.ts +359 -0
  78. package/src/lib/safeSources.ts +25 -0
  79. package/src/lib/shell.ts +71 -0
  80. package/src/lib/sourceCache.test.ts +66 -0
  81. package/src/lib/sourceCache.ts +42 -0
  82. package/src/lib/tocScroll.test.ts +71 -0
  83. package/src/lib/tocScroll.ts +93 -0
  84. package/src/lib/wiki.test.ts +194 -0
  85. package/src/lib/wiki.ts +175 -0
  86. package/src/lib.ts +54 -0
  87. package/src/main.tsx +19 -0
  88. package/src/mdx.d.ts +9 -0
  89. package/src/mdxComponents.ts +89 -0
  90. package/src/test/setup.ts +19 -0
  91. package/viewer-manifest.schema.json +62 -0
  92. 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,6 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ // Import-free engine component: the oversized opening paragraph of an entry.
4
+ export default function Lede({ children }: { children?: ReactNode }) {
5
+ return <p className="grove-lede">{children}</p>;
6
+ }
@@ -0,0 +1,10 @@
1
+ // Import-free engine component: marks the excerpt cutoff in an entry body.
2
+ export default function More() {
3
+ return (
4
+ <div className="grove-more" aria-hidden="true">
5
+ <span>●</span>
6
+ <span>●</span>
7
+ <span>●</span>
8
+ </div>
9
+ );
10
+ }
@@ -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
+ }