@mvarble/mesearch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/app/ambient.d.ts +24 -0
  4. package/app/app.html +28 -0
  5. package/app/components/Proof.svelte +19 -0
  6. package/app/components/Statement.svelte +19 -0
  7. package/app/error.html +23 -0
  8. package/app/hooks.server.ts +8 -0
  9. package/app/lib/format.ts +6 -0
  10. package/app/lib/server/refs.ts +32 -0
  11. package/app/lib/types.ts +16 -0
  12. package/app/params/kind.ts +5 -0
  13. package/app/routes/+error.svelte +37 -0
  14. package/app/routes/+layout.svelte +41 -0
  15. package/app/routes/+layout.ts +2 -0
  16. package/app/routes/+page.server.ts +37 -0
  17. package/app/routes/+page.svelte +212 -0
  18. package/app/routes/+page.ts +18 -0
  19. package/app/routes/[kind=kind]/[slug]/+page.server.ts +62 -0
  20. package/app/routes/[kind=kind]/[slug]/+page.svelte +116 -0
  21. package/app/routes/[kind=kind]/[slug]/+page.ts +8 -0
  22. package/app/routes/search.json/+server.ts +30 -0
  23. package/app/static/favicon.svg +1 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +146 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config.d.ts +26 -0
  28. package/dist/config.js +82 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/headings.d.ts +2 -0
  31. package/dist/headings.js +17 -0
  32. package/dist/headings.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/init.d.ts +6 -0
  37. package/dist/init.js +214 -0
  38. package/dist/init.js.map +1 -0
  39. package/dist/kit.d.ts +4 -0
  40. package/dist/kit.js +180 -0
  41. package/dist/kit.js.map +1 -0
  42. package/dist/paths.d.ts +8 -0
  43. package/dist/paths.js +16 -0
  44. package/dist/paths.js.map +1 -0
  45. package/dist/project.d.ts +26 -0
  46. package/dist/project.js +92 -0
  47. package/dist/project.js.map +1 -0
  48. package/dist/resolve.d.ts +2 -0
  49. package/dist/resolve.js +51 -0
  50. package/dist/resolve.js.map +1 -0
  51. package/package.json +83 -0
  52. package/templates/AGENTS.md +64 -0
  53. package/templates/content/concepts/example-concept/description.md +1 -0
  54. package/templates/content/concepts/example-concept/index.md +16 -0
  55. package/templates/content/description.md +1 -0
  56. package/templates/content/writeups/example-writeup/index.md +7 -0
  57. package/templates/mesearch.config.ts +22 -0
  58. package/tsconfig.project.json +16 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Varble
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mvarble/mesearch/main/logo.svg" alt="mesearch logo; man on toilet looking at phone" width="128">
3
+ </p>
4
+
5
+ # mesearch
6
+
7
+ An opinionated static site for a library of notes --- the kind an agent writes for you while you learn something. You keep documents in a fixed layout under `content/`; mesearch turns them into a site that reads like a textbook, with the math rendered, a map of how the documents depend on one another, an index, and sequences to read in order.
8
+
9
+ ```sh
10
+ pnpm add @mvarble/mesearch
11
+ pnpm exec mesearch init # content/, config, stylesheet, AGENTS.md, lint config
12
+ pnpm dev # live site while you write
13
+ pnpm build # static site in build/
14
+ ```
15
+
16
+ Where [mkdoc](https://github.com/mvarble/mkdoc) renders one document without a project around it, mesearch is installed in a `package.json` and builds a whole site. Where [the blog](https://github.com/mvarble/blog) is a SvelteKit app you maintain, mesearch is the SvelteKit app: a project holds only documents and a little configuration, and everything about how the site looks and works ships in this package.
17
+
18
+ ## A project
19
+
20
+ ```
21
+ project/
22
+ package.json depends on @mvarble/mesearch
23
+ AGENTS.md how agents should write for this site
24
+ mesearch.config.ts title, base path, KaTeX macros
25
+ mesearch.css optional: overrides for any of the site's variables
26
+ content/
27
+ description.md what the site is about; opens the home page
28
+ concepts/<slug>/index.md one concept per folder
29
+ concepts/<slug>/description.md optional preview, for the map and the index
30
+ writeups/<slug>/index.md one writeup per folder
31
+ writeups/<slug>/description.md
32
+ sequences/<slug>/index.md writeups to be read in order
33
+ ```
34
+
35
+ Documents are `.md` or `.svx` ([mdsvex](https://mdsvex.pngwn.io/), which can import and render Svelte components). A document's folder holds whatever else it uses --- images, data, components --- referenced relatively. Packages a document imports (`three`, say) are installed in the project like any other dependency.
36
+
37
+ ### Frontmatter
38
+
39
+ ```yaml
40
+ ---
41
+ title: Net interest margin
42
+ created: 2026-10-01
43
+ updated: 2026-10-03
44
+ depends_on: [basis-points, writeups/bank-balance-sheets]
45
+ katex_macros:
46
+ '\NIM': '\mathrm{NIM}'
47
+ ---
48
+ ```
49
+
50
+ - `title` is required, and may contain inline math.
51
+ - `created` and `updated` default to the first and last commits that touched the document's folder, then to file times.
52
+ - `depends_on` names what a reader needs first, by slug: a bare slug when it is unambiguous between concepts and writeups, `concepts/<slug>` or `writeups/<slug>` otherwise.
53
+ - `katex_macros` are added, for this document, to mesearch's own macros (`\bbR`, `\calF`, `\bfx`, `\defeq`, …) and the site's.
54
+ - A sequence lists its writeups in reading order under `documents:`.
55
+
56
+ ### Links, equations and the map
57
+
58
+ Link documents the way their folders sit on disk --- `[basis points](../basis-points/)`, `[the writeup](../../writeups/x/index.md#section)` --- and mesearch rewrites the links to site URLs. A link to a document that does not exist is reported when the site builds.
59
+
60
+ `$$ ... @tag(slug) $$` numbers a display equation and `[](eq:slug)` refers to it as `(1)`. From another document the reference is `eq:concepts/<slug>/<eq-slug>`.
61
+
62
+ A theorem, definition or remark is a small document with `type: statement` and a `kind`, shown with `<Statement {...theorem} />` (from `@mvarble/mesearch/Statement.svelte`). Statements count along with the document's equations, and `[%full](statement:slug)` refers to one as "Theorem 2". `@mvarble/mesearch/Proof.svelte` frames a proof.
63
+
64
+ Bibliography entries go in `.bib` files anywhere under `content/`. `[](cite:key)` renders as `[Foll99]` and `[Theorem 1.8](cite:key)` as `[Foll99, Theorem 1.8]`. Hovering a citation shows the reference. Following it jumps to a short list of references at the end of the page, which holds only what that page cites. There is no site-wide bibliography page.
65
+
66
+ The map on the home page is drawn from all of this:
67
+
68
+ - Every concept and writeup is a node.
69
+ - A solid arrow runs from each `depends_on` entry to the document that lists it. Neighbours in a sequence are joined the same way, so following the arrows is a reading order.
70
+ - A dashed line joins two documents when one links to the other and neither depends on the other.
71
+
72
+ ## What the site does
73
+
74
+ **The home page** opens with the site's description. Below it is the map, laid out by a force simulation in which nodes repel each other, edges pull, and each node is drawn towards a band set by its depth in the dependency order. The layout is computed when the site builds, so the map works without JavaScript. With JavaScript, nodes can be dragged, the map pans and zooms (Ctrl/⌘ + scroll, or pinch), and clicking a node opens a preview with its description and its neighbours. The preview is a drawer beside the map on wide screens and a sheet from the bottom on narrow ones. Then come the sequences, and the index, which can be filtered and sorted by date updated, date created or title.
75
+
76
+ **Each document** has a header with its kind, title, dates, reading time and what it builds on, and its sequence position when it has one. On wide screens two sidebars flank the text: on the left, the sequence it belongs to, drawn as a line of stations; on the right, its headings, with the section being read marked as you scroll. On narrower screens both fold into the header. The footer links what it leads to, what it relates to, and the previous and next parts of its sequence.
77
+
78
+ **Everywhere**, a slim rail down the left edge (a floating dock at the bottom of a phone) holds search, the map, the index and the theme. Search (`/` or Ctrl/⌘-K) looks through titles, summaries and headings. The theme follows the device until you choose light or dark, and a page read without JavaScript is light.
79
+
80
+ Text is justified and hyphenated, and inline math never breaks across a line. Display math scrolls sideways when it is too wide, with its equation number kept in view.
81
+
82
+ ## Configuration
83
+
84
+ ```ts
85
+ // mesearch.config.ts
86
+ import { defineConfig } from '@mvarble/mesearch';
87
+
88
+ export default defineConfig({
89
+ title: 'Probability, from the ground up',
90
+ author: 'Ada',
91
+ base: '/probability', // when served from a subdirectory, as on GitHub Pages
92
+ katexMacros: { '\\PP': '\\mathbb{P}' },
93
+ graph: { charge: -900, linkDistance: 80, gravity: 0.02 },
94
+ });
95
+ ```
96
+
97
+ ## Styling
98
+
99
+ Every colour, font, size and spacing is a CSS variable. `mesearch init` writes a `mesearch.css` listing them all, commented out at their defaults, for both the light and the dark theme. Uncomment one to change it. The file loads after mesearch's own styles, so any other rule in it applies as well, and it can `@import` a font from Google Fonts or from npm.
100
+
101
+ ## Commands
102
+
103
+ ```
104
+ mesearch init [dir] scaffold a project (never overwrites without --force)
105
+ mesearch dev serve the site, updating as documents change
106
+ -p, --port <n> --host [addr] --open
107
+ mesearch build write the static site
108
+ -o, --out <dir> (default: build)
109
+ --base <path> URL prefix, overriding the config
110
+ --force overwrite an output directory mesearch did not write
111
+ ```
112
+
113
+ The site in `build/` is plain files: serve it from anywhere.
114
+
115
+ ## Writing with agents
116
+
117
+ `AGENTS.md` describes the layout, the frontmatter, the link conventions and the prose register for any agent writing in the project, and ends with a section for opinions particular to the site. The [`/explain` and `/explain-concept`](https://github.com/mvarble/config/tree/main/dot_pi/agent/extensions/explain) pi commands write into this layout and defer to `AGENTS.md` on anything particular to the site. Their own `authoring.md` covers how to write the mathematics, numbered equations, statements, proofs and citations.
118
+
119
+ ## How it works
120
+
121
+ mesearch is a SvelteKit app shipped as source in this package. On every run the CLI writes a small `.mesearch/` folder into the project, holding a `svelte.config.js` and a `vite.config.js` that each call into this package, and runs SvelteKit from there with its routes pointed at the app here. The project needs no SvelteKit of its own; Svelte and SvelteKit always resolve to mesearch's copies, so a project cannot end up with two of them.
122
+
123
+ The content goes through [`@mvarble/mesearch-cms`](https://github.com/mvarble/mesearch/tree/main/packages/cms) and [`@mvarble/mesearch-markdown`](https://github.com/mvarble/mesearch/tree/main/packages/markdown). The pages are built from [`@mvarble/mesearch-ui`](https://github.com/mvarble/mesearch/tree/main/packages/ui). The blog shares all three.
@@ -0,0 +1,24 @@
1
+ // Modules served by mesearch's Vite plugins.
2
+
3
+ declare module '$cms' {
4
+ export const cms: import('@mvarble/mesearch-cms/presets/mesearch').MesearchCms;
5
+ }
6
+
7
+ declare module '$cms/loaders' {
8
+ const loaders: Record<string, () => Promise<{ default: import('svelte').Component }>>;
9
+ export default loaders;
10
+ }
11
+
12
+ declare module '$site' {
13
+ const site: {
14
+ title: string;
15
+ author?: string;
16
+ lang: string;
17
+ base: string;
18
+ katexMacros: Record<string, string>;
19
+ graph: { charge: number; linkDistance: number; gravity: number };
20
+ };
21
+ export default site;
22
+ }
23
+
24
+ declare module '$site/user-styles';
package/app/app.html ADDED
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="%mesearch.lang%" data-theme="light">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
7
+ <script>
8
+ // Before anything paints: the stored theme, or else the system's.
9
+ // A page read without JavaScript keeps the light theme it ships with.
10
+ (function () {
11
+ var root = document.documentElement;
12
+ var theme;
13
+ root.classList.add('js');
14
+ try {
15
+ theme = localStorage.getItem('mesearch:theme');
16
+ } catch (e) {}
17
+ if (theme !== 'light' && theme !== 'dark') {
18
+ theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
19
+ }
20
+ root.dataset.theme = theme;
21
+ })();
22
+ </script>
23
+ %sveltekit.head%
24
+ </head>
25
+ <body data-sveltekit-preload-data="hover">
26
+ <div style="display: contents">%sveltekit.body%</div>
27
+ </body>
28
+ </html>
@@ -0,0 +1,19 @@
1
+ <!--
2
+ A proof, for a document to wrap one in:
3
+
4
+ import Proof from '@mvarble/mesearch/Proof.svelte';
5
+
6
+ <Proof>
7
+
8
+ Take any open cover ...
9
+
10
+ </Proof>
11
+ -->
12
+ <script lang="ts">
13
+ import type { ComponentProps } from 'svelte';
14
+ import { Proof } from '@mvarble/mesearch-ui';
15
+
16
+ let props: ComponentProps<typeof Proof> = $props();
17
+ </script>
18
+
19
+ <Proof {...props} />
@@ -0,0 +1,19 @@
1
+ <!--
2
+ What a document imports to show a statement --- a theorem, a definition ---
3
+ written as a document of its own:
4
+
5
+ import Statement from '@mvarble/mesearch/Statement.svelte';
6
+ import * as heineBorel from './statements/heine-borel.md';
7
+
8
+ <Statement {...heineBorel} />
9
+
10
+ The content layer numbers it along with the page's equations.
11
+ -->
12
+ <script lang="ts">
13
+ import type { ComponentProps } from 'svelte';
14
+ import { Statement } from '@mvarble/mesearch-ui';
15
+
16
+ let props: ComponentProps<typeof Statement> = $props();
17
+ </script>
18
+
19
+ <Statement {...props} />
package/app/error.html ADDED
@@ -0,0 +1,23 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>%sveltekit.error.message%</title>
6
+ <style>
7
+ body {
8
+ font:
9
+ 17px/1.6 Georgia,
10
+ serif;
11
+ color: #1a1d1c;
12
+ background: #f9f8f3;
13
+ display: grid;
14
+ place-items: center;
15
+ min-height: 100vh;
16
+ margin: 0;
17
+ }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <p>%sveltekit.status% · %sveltekit.error.message%</p>
22
+ </body>
23
+ </html>
@@ -0,0 +1,8 @@
1
+ import type { Handle } from '@sveltejs/kit';
2
+ import site from '$site';
3
+
4
+ // The shell's `lang` comes from the site's config.
5
+ export const handle: Handle = ({ event, resolve }) =>
6
+ resolve(event, {
7
+ transformPageChunk: ({ html }) => html.replace('%mesearch.lang%', site.lang),
8
+ });
@@ -0,0 +1,6 @@
1
+ import site from '$site';
2
+
3
+ export { plain } from '@mvarble/mesearch-ui';
4
+
5
+ // A site path under the configured base, always with a trailing slash.
6
+ export const href = (pathname: string) => `${site.base}/${pathname ? pathname + '/' : ''}`;
@@ -0,0 +1,32 @@
1
+ import type { MesearchDocument } from '@mvarble/mesearch-cms/presets/mesearch';
2
+ import { inlineHtml } from '@mvarble/mesearch-ui/server';
3
+ import site from '$site';
4
+
5
+ import { href, plain } from '$lib/format';
6
+ import type { DocumentRef, IndexEntry } from '$lib/types';
7
+
8
+ // Titles and summaries as HTML, their math rendered with the site's macros
9
+ // and the document's own.
10
+ const html = (doc: MesearchDocument, text: string) =>
11
+ inlineHtml(text, { ...site.katexMacros, ...doc.katexMacros });
12
+
13
+ export const toRef = (doc: MesearchDocument): DocumentRef => ({
14
+ key: doc.key,
15
+ kind: doc.kind,
16
+ url: href(doc.key),
17
+ title: plain(doc.title),
18
+ titleHtml: html(doc, doc.title),
19
+ });
20
+
21
+ export const toIndexEntry = (doc: MesearchDocument): IndexEntry => ({
22
+ ...toRef(doc),
23
+ summary: plain(doc.summary),
24
+ summaryHtml: html(doc, doc.summary),
25
+ created: doc.created,
26
+ updated: doc.updated,
27
+ readingMinutes: doc.readingMinutes,
28
+ sequences: doc.sequences,
29
+ descriptionFilename: doc.descriptionFilename,
30
+ });
31
+
32
+ export { html as inlineHtml };
@@ -0,0 +1,16 @@
1
+ import type { IndexEntry as UiIndexEntry } from '@mvarble/mesearch-ui';
2
+
3
+ export type {
4
+ DocumentRef,
5
+ GraphLink,
6
+ GraphNode,
7
+ SearchEntry,
8
+ TocEntry,
9
+ TrackItem,
10
+ } from '@mvarble/mesearch-ui';
11
+
12
+ // An index entry, with what the home page needs besides.
13
+ export interface IndexEntry extends UiIndexEntry {
14
+ sequences: string[];
15
+ descriptionFilename?: string;
16
+ }
@@ -0,0 +1,5 @@
1
+ import type { ParamMatcher } from '@sveltejs/kit';
2
+
3
+ // The folders documents live in, which are also the first segment of a URL.
4
+ export const match: ParamMatcher = (param) =>
5
+ param == 'concepts' || param == 'writeups' || param == 'sequences';
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import site from '$site';
4
+ import { href } from '$lib/format';
5
+ </script>
6
+
7
+ <svelte:head>
8
+ <title>{page.status} · {site.title}</title>
9
+ </svelte:head>
10
+
11
+ <main id="main" class="error">
12
+ <p class="eyebrow">{page.status}</p>
13
+ <h1>{page.status == 404 ? 'Not in the library' : 'Something went wrong'}</h1>
14
+ <p>{page.error?.message}</p>
15
+ <p><a href={href('')}>Back to {site.title}</a></p>
16
+ </main>
17
+
18
+ <style>
19
+ .error {
20
+ width: min(var(--measure), 100% - 2 * var(--gutter));
21
+ margin: 0 auto;
22
+ padding: 6rem 0;
23
+ }
24
+
25
+ .eyebrow {
26
+ color: var(--accent);
27
+ }
28
+
29
+ h1 {
30
+ font: 600 2.4rem / 1.1 var(--font-serif);
31
+ margin: 0.5rem 0 1rem;
32
+ }
33
+
34
+ a {
35
+ color: var(--accent);
36
+ }
37
+ </style>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import '@mvarble/mesearch-ui/fonts';
3
+ import 'katex/dist/katex.min.css';
4
+ import '@mvarble/mesearch-markdown/styles/katex.css';
5
+ import '@mvarble/mesearch-ui/styles/index.css';
6
+ // The project's own stylesheet, last, so that it can override anything.
7
+ import '$site/user-styles';
8
+
9
+ import { goto, invalidateAll } from '$app/navigation';
10
+ import { Shell, configure } from '@mvarble/mesearch-ui';
11
+ import site from '$site';
12
+ import { href } from '$lib/format';
13
+
14
+ let { children } = $props();
15
+
16
+ configure({ lang: site.lang });
17
+
18
+ // While the dev server runs, a change to the content refreshes the page's
19
+ // data in place rather than reloading it.
20
+ if (import.meta.hot) {
21
+ import.meta.hot.on('mesearch-cms:update', () => void invalidateAll());
22
+ }
23
+ </script>
24
+
25
+ <svelte:head>
26
+ <meta name="generator" content="mesearch" />
27
+ {#if site.author}<meta name="author" content={site.author} />{/if}
28
+ </svelte:head>
29
+
30
+ <Shell
31
+ title={site.title}
32
+ home={href('')}
33
+ links={[
34
+ { url: `${href('')}#atlas`, label: 'Graph', icon: 'graph' },
35
+ { url: `${href('')}#index`, label: 'Index', icon: 'list' },
36
+ ]}
37
+ search={`${site.base}/search.json`}
38
+ navigate={goto}
39
+ >
40
+ {@render children()}
41
+ </Shell>
@@ -0,0 +1,2 @@
1
+ export const prerender = true;
2
+ export const trailingSlash = 'always';
@@ -0,0 +1,37 @@
1
+ import { layoutGraph } from '@mvarble/mesearch-ui/server';
2
+ import { cms } from '$cms';
3
+ import site from '$site';
4
+
5
+ import { toIndexEntry, toRef } from '$lib/server/refs';
6
+
7
+ export const load = () => {
8
+ const documents = cms.documents.list().map(toIndexEntry);
9
+ const byKey = new Map(documents.map((doc) => [doc.key, doc]));
10
+
11
+ // The graph is laid out here, at build time, so the page arrives with a
12
+ // finished picture whether or not its JavaScript ever runs.
13
+ const graph = cms.graph();
14
+ const nodes = layoutGraph(
15
+ graph.nodes
16
+ .map((key) => byKey.get(key))
17
+ .filter((doc) => !!doc)
18
+ .map((doc) => ({ ...doc, sequenced: doc.sequences.length > 0 })),
19
+ graph.edges,
20
+ site.graph,
21
+ );
22
+
23
+ const sequences = cms.sequences.list().map((sequence) => ({
24
+ ...toIndexEntry(sequence),
25
+ documents: sequence.documents
26
+ .map((key) => cms.documents.get(key))
27
+ .filter((doc) => !!doc)
28
+ .map(toRef),
29
+ }));
30
+
31
+ return {
32
+ description: cms.description(),
33
+ documents,
34
+ sequences,
35
+ graph: { nodes, links: graph.edges },
36
+ };
37
+ };
@@ -0,0 +1,212 @@
1
+ <script lang="ts">
2
+ import { Graph, Index } from '@mvarble/mesearch-ui';
3
+ import site from '$site';
4
+ import { href } from '$lib/format';
5
+
6
+ let { data } = $props();
7
+
8
+ let entries = $derived(Object.fromEntries(data.documents.map((doc) => [doc.key, doc])));
9
+ let counts = $derived({
10
+ concepts: data.documents.filter((doc) => doc.kind == 'concept').length,
11
+ writeups: data.documents.filter((doc) => doc.kind == 'writeup').length,
12
+ sequences: data.sequences.length,
13
+ });
14
+ const plural = (n: number, word: string) => `${n} ${word}${n == 1 ? '' : 's'}`;
15
+ </script>
16
+
17
+ <svelte:head>
18
+ <title>{site.title}</title>
19
+ </svelte:head>
20
+
21
+ <main id="main" class="home">
22
+ <header class="hero">
23
+ <p class="eyebrow">{site.author ?? 'A library of notes'}</p>
24
+ <h1>{site.title}</h1>
25
+ {#if data.Description}
26
+ <div class="lede prose">
27
+ <data.Description />
28
+ </div>
29
+ {/if}
30
+ <p class="counts">
31
+ {[
32
+ plural(counts.concepts, 'concept'),
33
+ plural(counts.writeups, 'writeup'),
34
+ ...(counts.sequences ? [plural(counts.sequences, 'sequence')] : []),
35
+ ].join(' · ')}
36
+ </p>
37
+ </header>
38
+
39
+ <section id="atlas" class="atlas" aria-labelledby="atlas-heading">
40
+ <div class="section-head">
41
+ <h2 id="atlas-heading">The map</h2>
42
+ <p>
43
+ Arrows run from what a document builds on to the document itself, so following them
44
+ is a reading order. Dashed lines join documents that refer to one another.
45
+ </p>
46
+ </div>
47
+ <Graph
48
+ nodes={data.graph.nodes}
49
+ links={data.graph.links}
50
+ {entries}
51
+ descriptions={data.descriptions}
52
+ tuning={site.graph}
53
+ />
54
+ </section>
55
+
56
+ {#if data.sequences.length}
57
+ <section class="sequences" aria-labelledby="sequences-heading">
58
+ <div class="section-head">
59
+ <h2 id="sequences-heading">Sequences</h2>
60
+ <p>Writeups meant to be read in order.</p>
61
+ </div>
62
+ <ul class="cards">
63
+ {#each data.sequences as sequence (sequence.key)}
64
+ <li class="card kind-sequence">
65
+ <a href={href(sequence.key)}>
66
+ <span class="eyebrow">{plural(sequence.documents.length, 'part')}</span>
67
+ <span class="card-title">{@html sequence.titleHtml}</span>
68
+ {#if sequence.summaryHtml}<span class="card-summary"
69
+ >{@html sequence.summaryHtml}</span
70
+ >{/if}
71
+ <ol class="parts">
72
+ {#each sequence.documents as part (part.key)}
73
+ <li>{@html part.titleHtml}</li>
74
+ {/each}
75
+ </ol>
76
+ </a>
77
+ </li>
78
+ {/each}
79
+ </ul>
80
+ </section>
81
+ {/if}
82
+
83
+ <section id="index" class="index-section" aria-labelledby="index-heading">
84
+ <div class="section-head">
85
+ <h2 id="index-heading">Index</h2>
86
+ <p>Everything, most recently changed first.</p>
87
+ </div>
88
+ <Index entries={data.documents} />
89
+ </section>
90
+ </main>
91
+
92
+ <style>
93
+ .home {
94
+ width: min(72rem, 100% - 2 * var(--gutter));
95
+ margin: 0 auto;
96
+ padding: clamp(2.5rem, 6vw, 5rem) 0 4rem;
97
+ }
98
+
99
+ .hero {
100
+ max-width: var(--measure);
101
+ margin-bottom: clamp(2.5rem, 6vw, 4.5rem);
102
+ }
103
+
104
+ .hero .eyebrow {
105
+ color: var(--accent);
106
+ margin: 0 0 0.9rem;
107
+ }
108
+
109
+ h1 {
110
+ margin: 0 0 1.25rem;
111
+ font: 600 clamp(2.6rem, 1.6rem + 4vw, 4.2rem) / 1.02 var(--font-serif);
112
+ letter-spacing: -0.025em;
113
+ text-wrap: balance;
114
+ }
115
+
116
+ .lede {
117
+ font-size: 1.2rem;
118
+ line-height: 1.65;
119
+ color: var(--ink-soft);
120
+ }
121
+
122
+ .counts {
123
+ margin: 1.5rem 0 0;
124
+ font: 500 var(--font-size-ui) / 1.4 var(--font-ui);
125
+ color: var(--muted);
126
+ letter-spacing: 0.02em;
127
+ }
128
+
129
+ section {
130
+ margin-bottom: clamp(3rem, 7vw, 5rem);
131
+ scroll-margin-top: 1.5rem;
132
+ }
133
+
134
+ .section-head {
135
+ display: flex;
136
+ flex-wrap: wrap;
137
+ align-items: baseline;
138
+ gap: 0.4rem 1.5rem;
139
+ margin-bottom: 1.25rem;
140
+ padding-bottom: 0.75rem;
141
+ border-bottom: 1px solid var(--rule);
142
+ }
143
+
144
+ h2 {
145
+ margin: 0;
146
+ font: 600 1.6rem / 1.2 var(--font-serif);
147
+ }
148
+
149
+ .section-head p {
150
+ flex: 1 1 20rem;
151
+ margin: 0;
152
+ font: 400 0.9rem / 1.5 var(--font-ui);
153
+ color: var(--muted);
154
+ }
155
+
156
+ .cards {
157
+ display: grid;
158
+ grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
159
+ gap: 1rem;
160
+ margin: 0;
161
+ padding: 0;
162
+ list-style: none;
163
+ }
164
+
165
+ .card a {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 0.45rem;
169
+ height: 100%;
170
+ padding: 1.2rem 1.3rem;
171
+ border: 1px solid var(--rule);
172
+ border-top: 3px solid var(--kind);
173
+ border-radius: var(--radius);
174
+ background: var(--paper-raised);
175
+ color: inherit;
176
+ text-decoration: none;
177
+ transition:
178
+ box-shadow 150ms,
179
+ transform 150ms;
180
+ }
181
+
182
+ .card a:hover {
183
+ box-shadow: var(--shadow);
184
+ transform: translateY(-1px);
185
+ }
186
+
187
+ .card .eyebrow {
188
+ color: var(--kind);
189
+ }
190
+
191
+ .card-title {
192
+ font: 600 1.25rem / 1.25 var(--font-serif);
193
+ }
194
+
195
+ .card-summary {
196
+ font-size: 0.95rem;
197
+ color: var(--ink-soft);
198
+ line-height: 1.55;
199
+ }
200
+
201
+ .parts {
202
+ margin: 0.3rem 0 0;
203
+ padding-left: 1.3rem;
204
+ font: 0.85rem / 1.6 var(--font-ui);
205
+ color: var(--muted);
206
+ }
207
+
208
+ .parts li::marker {
209
+ color: var(--kind);
210
+ font-variant-numeric: tabular-nums;
211
+ }
212
+ </style>