@twango/twiki 0.0.0-stage → 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/.github/actions/product-cache/action.yml +32 -0
- package/.github/actions/product-cache/index.mjs +450 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bun.lock +1701 -0
- package/cli/runtime.test.ts +420 -0
- package/cli/runtime.ts +385 -0
- package/cli/twiki.ts +305 -0
- package/package.json +138 -4
- package/site/_headers +7 -0
- package/site/assets/fonts/OFL.txt +93 -0
- package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/site/assets/social-background.svg +38 -0
- package/site/bun.lock +1701 -0
- package/site/database/index.ts +7 -0
- package/site/database/schema.ts +95 -0
- package/site/drizzle.config.ts +7 -0
- package/site/migrations/0001_auth.sql +14 -0
- package/site/migrations/0002_site_access.sql +5 -0
- package/site/migrations/0003_github_username.sql +1 -0
- package/site/migrations/meta/0002_snapshot.json +432 -0
- package/site/migrations/meta/0003_snapshot.json +439 -0
- package/site/migrations/meta/_journal.json +27 -0
- package/site/package.json +120 -0
- package/site/src/app.d.ts +14 -0
- package/site/src/app.html +11 -0
- package/site/src/hooks.server.ts +12 -0
- package/site/src/lib/analytics-config.json +1 -0
- package/site/src/lib/analytics.ts +40 -0
- package/site/src/lib/auth-config.json +3 -0
- package/site/src/lib/components/SEO.svelte +113 -0
- package/site/src/lib/components/doc/AccessLink.svelte +27 -0
- package/site/src/lib/components/doc/Backlinks.svelte +59 -0
- package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
- package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
- package/site/src/lib/components/doc/DocPager.svelte +91 -0
- package/site/src/lib/components/doc/DocShell.svelte +115 -0
- package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
- package/site/src/lib/components/doc/LockedContent.svelte +29 -0
- package/site/src/lib/components/doc/NavTree.svelte +158 -0
- package/site/src/lib/components/doc/NoteActions.svelte +139 -0
- package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
- package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
- package/site/src/lib/components/doc/Sidebar.svelte +351 -0
- package/site/src/lib/components/doc/Toc.svelte +339 -0
- package/site/src/lib/components/doc/enhancements.ts +146 -0
- package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
- package/site/src/lib/components/doc/image-preview.ts +114 -0
- package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
- package/site/src/lib/components/embeds/mermaid.ts +353 -0
- package/site/src/lib/components/files/FileCode.svelte +81 -0
- package/site/src/lib/components/files/FileExplorer.svelte +74 -0
- package/site/src/lib/components/files/FileHistory.svelte +170 -0
- package/site/src/lib/components/files/FileIcon.svelte +28 -0
- package/site/src/lib/components/files/FileRmd.svelte +209 -0
- package/site/src/lib/components/files/FileTabs.svelte +180 -0
- package/site/src/lib/components/files/FileTree.svelte +67 -0
- package/site/src/lib/components/files/blame-gutter.ts +45 -0
- package/site/src/lib/components/files/code-view.ts +98 -0
- package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
- package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
- package/site/src/lib/components/graph/GraphView.svelte +100 -0
- package/site/src/lib/components/graph/graph-data.ts +137 -0
- package/site/src/lib/components/graph/graph-model.ts +103 -0
- package/site/src/lib/components/graph/index.ts +3 -0
- package/site/src/lib/components/graph/render-graph.ts +626 -0
- package/site/src/lib/components/popover/index.ts +1 -0
- package/site/src/lib/components/popover/link-popovers.ts +233 -0
- package/site/src/lib/components/popover/popover.css +82 -0
- package/site/src/lib/components/search/SearchButton.svelte +57 -0
- package/site/src/lib/components/search/SearchPalette.svelte +189 -0
- package/site/src/lib/components/search/index.ts +3 -0
- package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
- package/site/src/lib/components/search/pagefind-query.ts +22 -0
- package/site/src/lib/components/search/pagefind-url.ts +12 -0
- package/site/src/lib/components/search/search-state.svelte.ts +22 -0
- package/site/src/lib/components/ui/IconButton.svelte +30 -0
- package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
- package/site/src/lib/components/ui/index.ts +1 -0
- package/site/src/lib/config.ts +9 -0
- package/site/src/lib/content-license.ts +7 -0
- package/site/src/lib/file-history.ts +37 -0
- package/site/src/lib/file-icons.ts +27 -0
- package/site/src/lib/file-tabs.ts +72 -0
- package/site/src/lib/files.ts +73 -0
- package/site/src/lib/github-user.ts +70 -0
- package/site/src/lib/metadata.ts +166 -0
- package/site/src/lib/notebook-nav.ts +63 -0
- package/site/src/lib/publication.ts +1 -0
- package/site/src/lib/rmd.ts +156 -0
- package/site/src/lib/server/access-admin.ts +33 -0
- package/site/src/lib/server/content-model.ts +253 -0
- package/site/src/lib/server/content-provider.ts +266 -0
- package/site/src/lib/server/content.ts +277 -0
- package/site/src/lib/server/runtime-content.ts +34 -0
- package/site/src/lib/site-config.json +9 -0
- package/site/src/lib/social-image.ts +13 -0
- package/site/src/lib/styles/article.css +607 -0
- package/site/src/lib/styles/foundation.css +138 -0
- package/site/src/lib/styles/print.css +115 -0
- package/site/src/lib/styles/shell.css +182 -0
- package/site/src/lib/text-exports.ts +5 -0
- package/site/src/lib/types.ts +140 -0
- package/site/src/lib/utils.ts +6 -0
- package/site/src/routes/+error.svelte +16 -0
- package/site/src/routes/+layout.server.ts +10 -0
- package/site/src/routes/+layout.svelte +72 -0
- package/site/src/routes/+layout.ts +3 -0
- package/site/src/routes/404/+page.svelte +13 -0
- package/site/src/routes/[...slug]/+page.server.ts +24 -0
- package/site/src/routes/[...slug]/+page.svelte +180 -0
- package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
- package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
- package/site/src/routes/admin/access/+page.server.ts +53 -0
- package/site/src/routes/admin/access/+page.svelte +118 -0
- package/site/src/routes/graph.json/+server.ts +7 -0
- package/site/src/routes/index.xml/+server.ts +7 -0
- package/site/src/routes/layout.css +10 -0
- package/site/src/routes/robots.txt/+server.ts +9 -0
- package/site/src/routes/sitemap.xml/+server.ts +7 -0
- package/site/src/routes/tags/+page.server.ts +5 -0
- package/site/src/routes/tags/+page.svelte +38 -0
- package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
- package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
- package/site/static/favicon.png +0 -0
- package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
- package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
- package/site/static/fonts/Virgil.woff2 +0 -0
- package/site/svelte.config.js +71 -0
- package/site/tooling/LICENSE-quartz +25 -0
- package/site/tooling/adapter.js +55 -0
- package/site/tooling/assets.ts +323 -0
- package/site/tooling/benchmark.ts +191 -0
- package/site/tooling/build.ts +407 -0
- package/site/tooling/cache.ts +133 -0
- package/site/tooling/check-diagrams.ts +164 -0
- package/site/tooling/check-links.ts +86 -0
- package/site/tooling/check-targets.ts +139 -0
- package/site/tooling/compiler.ts +1386 -0
- package/site/tooling/deploy.ts +112 -0
- package/site/tooling/fingerprint.ts +81 -0
- package/site/tooling/lib/app-version.js +92 -0
- package/site/tooling/lib/application-asset-urls.ts +27 -0
- package/site/tooling/lib/application-cache.ts +269 -0
- package/site/tooling/lib/asset-hash-cache.ts +116 -0
- package/site/tooling/lib/autotag.ts +99 -0
- package/site/tooling/lib/build-assembly.ts +148 -0
- package/site/tooling/lib/build-workspace.ts +386 -0
- package/site/tooling/lib/cache-selection.ts +70 -0
- package/site/tooling/lib/cloudflare-prewarm.js +40 -0
- package/site/tooling/lib/comments.ts +32 -0
- package/site/tooling/lib/content-cache.ts +244 -0
- package/site/tooling/lib/content-urls.ts +50 -0
- package/site/tooling/lib/course-cache.ts +298 -0
- package/site/tooling/lib/course-files.ts +281 -0
- package/site/tooling/lib/currency.ts +33 -0
- package/site/tooling/lib/deployment-config.ts +222 -0
- package/site/tooling/lib/deployment-manifest.ts +274 -0
- package/site/tooling/lib/deployment-prepare.ts +325 -0
- package/site/tooling/lib/deployment-upload.ts +293 -0
- package/site/tooling/lib/dev-git-state.ts +130 -0
- package/site/tooling/lib/dev-history.ts +86 -0
- package/site/tooling/lib/dev-social-images.ts +38 -0
- package/site/tooling/lib/dev-state.ts +107 -0
- package/site/tooling/lib/edition-builds.ts +184 -0
- package/site/tooling/lib/edition-paths.js +29 -0
- package/site/tooling/lib/empty-tag-route.js +23 -0
- package/site/tooling/lib/file-history.ts +201 -0
- package/site/tooling/lib/file-icons.ts +93 -0
- package/site/tooling/lib/file-policy.ts +38 -0
- package/site/tooling/lib/file-search.js +13 -0
- package/site/tooling/lib/git-revisions.ts +41 -0
- package/site/tooling/lib/history-pool.ts +74 -0
- package/site/tooling/lib/history-worker.ts +22 -0
- package/site/tooling/lib/knit-rmd.R +38 -0
- package/site/tooling/lib/lastmod.ts +201 -0
- package/site/tooling/lib/markdown-files.ts +51 -0
- package/site/tooling/lib/ofm.ts +634 -0
- package/site/tooling/lib/output.ts +60 -0
- package/site/tooling/lib/product-cache.ts +1006 -0
- package/site/tooling/lib/project-config.ts +288 -0
- package/site/tooling/lib/public-assets.ts +38 -0
- package/site/tooling/lib/public-links.ts +56 -0
- package/site/tooling/lib/publishing.ts +156 -0
- package/site/tooling/lib/r-runtime.ts +257 -0
- package/site/tooling/lib/rebuild-queue.ts +25 -0
- package/site/tooling/lib/rmd-preflight.ts +166 -0
- package/site/tooling/lib/rmd-preview-cache.ts +93 -0
- package/site/tooling/lib/rmd-previews.ts +129 -0
- package/site/tooling/lib/runtime-data-modules.ts +30 -0
- package/site/tooling/lib/runtime-packaging.ts +321 -0
- package/site/tooling/lib/search-cache.js +165 -0
- package/site/tooling/lib/search-inputs.js +129 -0
- package/site/tooling/lib/slug.ts +377 -0
- package/site/tooling/lib/social-images.ts +210 -0
- package/site/tooling/lib/text-exports.ts +51 -0
- package/site/tooling/lib/visit-elements.ts +17 -0
- package/site/tooling/live-test.ts +138 -0
- package/site/tooling/pipeline.ts +103 -0
- package/site/tooling/prepare.ts +3 -0
- package/site/tooling/publish.ts +28 -0
- package/site/tooling/r-cache.ts +45 -0
- package/site/tooling/r-render-profile.json +7 -0
- package/site/tooling/search.js +94 -0
- package/site/tooling/verify-deployment.ts +109 -0
- package/site/tooling/vite-content.ts +240 -0
- package/site/tooling/wrangler-paths.js +25 -0
- package/site/tooling/wrangler.ts +19 -0
- package/site/tsconfig.json +16 -0
- package/site/vite.config.ts +40 -0
- package/site/worker/access.ts +57 -0
- package/site/worker/analytics.ts +100 -0
- package/site/worker/auth-routes.ts +78 -0
- package/site/worker/auth.ts +78 -0
- package/site/worker/content-runtime.ts +226 -0
- package/site/worker/framing.ts +4 -0
- package/site/worker/gate.ts +147 -0
- package/site/worker/index.ts +9 -0
- package/site/worker/login.ts +62 -0
- package/site/worker/publication.ts +44 -0
- package/site/worker/runtime.ts +104 -0
- package/site/worker/tsconfig.json +13 -0
- package/site/wrangler.jsonc +37 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { site } from '$lib/config';
|
|
3
|
+
import { socialImageSize, socialImageUrl } from '$lib/social-image';
|
|
4
|
+
import {
|
|
5
|
+
canonicalUrl as resolveCanonical,
|
|
6
|
+
collectionSchema,
|
|
7
|
+
serializeSchema,
|
|
8
|
+
type ContentAuthor
|
|
9
|
+
} from '$lib/metadata';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
canonical?: string;
|
|
15
|
+
type?: 'website' | 'article';
|
|
16
|
+
image?: string;
|
|
17
|
+
imageAlt?: string;
|
|
18
|
+
published?: string;
|
|
19
|
+
modified?: string;
|
|
20
|
+
author?: ContentAuthor;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
noindex?: boolean;
|
|
23
|
+
/** JSON-LD structured data (single object or array). */
|
|
24
|
+
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
title,
|
|
29
|
+
description = site.description,
|
|
30
|
+
canonical,
|
|
31
|
+
type = 'website',
|
|
32
|
+
image,
|
|
33
|
+
imageAlt = image ? (title ?? site.name) : site.description,
|
|
34
|
+
published,
|
|
35
|
+
modified,
|
|
36
|
+
author,
|
|
37
|
+
tags = [],
|
|
38
|
+
noindex = false,
|
|
39
|
+
jsonLd
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
const fullTitle = $derived(title ? `${title} | ${site.name}` : site.name);
|
|
43
|
+
const previewImage = $derived(image ?? socialImageUrl());
|
|
44
|
+
const canonicalUrl = $derived(canonical ? resolveCanonical(canonical) : undefined);
|
|
45
|
+
const schemas = $derived(
|
|
46
|
+
jsonLd ?? (canonical ? collectionSchema(canonical, title ?? site.name, description) : [])
|
|
47
|
+
);
|
|
48
|
+
const jsonLdScripts = $derived(noindex ? [] : Array.isArray(schemas) ? schemas : [schemas]);
|
|
49
|
+
|
|
50
|
+
// Build a <script type="application/ld+json"> tag. Split the literal closing
|
|
51
|
+
// tag so the Svelte parser doesn't terminate this script block early. The
|
|
52
|
+
// payload is our own serialized structured data, not user input.
|
|
53
|
+
function jsonLdTag(schema: Record<string, unknown>): string {
|
|
54
|
+
const open = '<' + 'script type="application/ld+json">';
|
|
55
|
+
const close = '<' + '/script>';
|
|
56
|
+
return open + serializeSchema(schema) + close;
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<svelte:head>
|
|
61
|
+
<title>{fullTitle}</title>
|
|
62
|
+
<meta name="description" content={description} />
|
|
63
|
+
{#if noindex}
|
|
64
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
65
|
+
{:else}
|
|
66
|
+
<meta name="robots" content="max-image-preview:large" />
|
|
67
|
+
{/if}
|
|
68
|
+
{#if canonicalUrl}
|
|
69
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
<!-- Open Graph -->
|
|
73
|
+
<meta property="og:title" content={fullTitle} />
|
|
74
|
+
<meta property="og:description" content={description} />
|
|
75
|
+
<meta property="og:type" content={type} />
|
|
76
|
+
<meta property="og:site_name" content={site.name} />
|
|
77
|
+
<meta property="og:locale" content={site.locale} />
|
|
78
|
+
{#if canonicalUrl}
|
|
79
|
+
<meta property="og:url" content={canonicalUrl} />
|
|
80
|
+
{/if}
|
|
81
|
+
{#if previewImage}
|
|
82
|
+
<meta property="og:image" content={previewImage} />
|
|
83
|
+
<meta property="og:image:secure_url" content={previewImage} />
|
|
84
|
+
<meta property="og:image:type" content="image/png" />
|
|
85
|
+
<meta property="og:image:width" content={String(socialImageSize.width)} />
|
|
86
|
+
<meta property="og:image:height" content={String(socialImageSize.height)} />
|
|
87
|
+
<meta property="og:image:alt" content={imageAlt} />
|
|
88
|
+
{/if}
|
|
89
|
+
{#if !noindex}
|
|
90
|
+
{#if author}<meta name="author" content={author.name} />{/if}
|
|
91
|
+
{#if published}<meta property="article:published_time" content={published} />{/if}
|
|
92
|
+
{#if modified}<meta property="article:modified_time" content={modified} />{/if}
|
|
93
|
+
{#if type === 'article'}
|
|
94
|
+
{#if author?.url}<meta property="article:author" content={author.url} />{/if}
|
|
95
|
+
{#each tags as tag}<meta property="article:tag" content={tag} />{/each}
|
|
96
|
+
{/if}
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
<!-- Twitter Card -->
|
|
100
|
+
<meta name="twitter:card" content={previewImage ? 'summary_large_image' : 'summary'} />
|
|
101
|
+
<meta name="twitter:title" content={fullTitle} />
|
|
102
|
+
<meta name="twitter:description" content={description} />
|
|
103
|
+
{#if previewImage}
|
|
104
|
+
<meta name="twitter:image" content={previewImage} />
|
|
105
|
+
<meta name="twitter:image:alt" content={imageAlt} />
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
<!-- JSON-LD -->
|
|
109
|
+
{#each jsonLdScripts as schema (schema)}
|
|
110
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
111
|
+
{@html jsonLdTag(schema)}
|
|
112
|
+
{/each}
|
|
113
|
+
</svelte:head>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { authEnabled } from '$lib/config';
|
|
4
|
+
import UsersRound from '@lucide/svelte/icons/users-round';
|
|
5
|
+
|
|
6
|
+
let owner = $state(false);
|
|
7
|
+
onMount(() => {
|
|
8
|
+
if (!authEnabled) return;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
fetch('/api/access', { signal: controller.signal })
|
|
11
|
+
.then(async (response) => {
|
|
12
|
+
if (response.ok) owner = (await response.json()).role === 'owner';
|
|
13
|
+
})
|
|
14
|
+
.catch(() => {});
|
|
15
|
+
return () => controller.abort();
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#if owner}
|
|
20
|
+
<a
|
|
21
|
+
href="/admin/access"
|
|
22
|
+
aria-label="Manage access"
|
|
23
|
+
title="Manage access"
|
|
24
|
+
class="inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-muted hover:bg-surface hover:text-text focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none"
|
|
25
|
+
><UsersRound size={16} /></a
|
|
26
|
+
>
|
|
27
|
+
{/if}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Backlinks panel — pages that link to the current one, styled as
|
|
3
|
+
// cca-aesthetic cards (1px border-border, rounded, surface hover, accent on
|
|
4
|
+
// hover) to match DocPager.
|
|
5
|
+
import CornerDownRight from '@lucide/svelte/icons/corner-down-right';
|
|
6
|
+
import type { BacklinkRef } from '$lib/types';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
backlinks: BacklinkRef[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { backlinks }: Props = $props();
|
|
13
|
+
|
|
14
|
+
/** Backlink slugs are display-form: '/' = home, folder pages end in '/'. */
|
|
15
|
+
function href(slug: string): string {
|
|
16
|
+
if (slug === '/') return '/';
|
|
17
|
+
return '/' + slug.replace(/\/$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Parent path of the linking page, shown as context under the title. */
|
|
21
|
+
function context(slug: string): string {
|
|
22
|
+
if (slug === '/') return '';
|
|
23
|
+
const segments = slug.replace(/\/$/, '').split('/');
|
|
24
|
+
return segments.slice(0, -1).join(' / ');
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
{#if backlinks.length > 0}
|
|
29
|
+
<section class="mt-8 border-t border-border pt-4" data-island="backlinks" aria-label="Backlinks">
|
|
30
|
+
<h2 class="m-0 mb-3 text-xs font-semibold tracking-[0.08em] text-muted uppercase">
|
|
31
|
+
Linked to this page
|
|
32
|
+
</h2>
|
|
33
|
+
<ul class="m-0 grid list-none grid-cols-2 gap-2 p-0 max-[640px]:grid-cols-1">
|
|
34
|
+
{#each backlinks as link (link.slug)}
|
|
35
|
+
<li class="min-w-0">
|
|
36
|
+
<a
|
|
37
|
+
class="group flex items-center gap-3 rounded-md border border-border px-3 py-2 no-underline transition-colors hover:border-accent hover:bg-surface focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none"
|
|
38
|
+
href={href(link.slug)}
|
|
39
|
+
>
|
|
40
|
+
<CornerDownRight
|
|
41
|
+
class="size-4 shrink-0 text-muted transition-colors group-hover:text-accent"
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
/>
|
|
44
|
+
<span class="min-w-0 flex-1">
|
|
45
|
+
<span
|
|
46
|
+
class="block truncate text-sm leading-snug font-medium text-text group-hover:text-accent"
|
|
47
|
+
>
|
|
48
|
+
{link.title}
|
|
49
|
+
</span>
|
|
50
|
+
{#if context(link.slug)}
|
|
51
|
+
<span class="block truncate text-xs text-muted">{context(link.slug)}</span>
|
|
52
|
+
{/if}
|
|
53
|
+
</span>
|
|
54
|
+
</a>
|
|
55
|
+
</li>
|
|
56
|
+
{/each}
|
|
57
|
+
</ul>
|
|
58
|
+
</section>
|
|
59
|
+
{/if}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { breadcrumbs } from '$lib/metadata';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Display route of the current page, no leading slash ('' = home). */
|
|
5
|
+
route: string;
|
|
6
|
+
/** Title for the final (unlinked) crumb. */
|
|
7
|
+
current: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { route, current }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const crumbs = $derived(breadcrumbs(route, current).slice(1, -1));
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if route !== ''}
|
|
16
|
+
<nav class="mb-3 text-[0.8125rem] text-muted" aria-label="Breadcrumbs">
|
|
17
|
+
<ol class="m-0 flex list-none flex-wrap items-center gap-1 p-0">
|
|
18
|
+
<li><a class="text-muted no-underline hover:text-accent" href="/">Home</a></li>
|
|
19
|
+
{#each crumbs as crumb (crumb.href)}
|
|
20
|
+
<li aria-hidden="true" class="select-none">❯</li>
|
|
21
|
+
<li>
|
|
22
|
+
<a class="text-muted no-underline hover:text-accent" href={crumb.href}>{crumb.label}</a>
|
|
23
|
+
</li>
|
|
24
|
+
{/each}
|
|
25
|
+
<li aria-hidden="true" class="select-none">❯</li>
|
|
26
|
+
<li aria-current="page" class="text-text">{current}</li>
|
|
27
|
+
</ol>
|
|
28
|
+
</nav>
|
|
29
|
+
{/if}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import PublicationBadge from './PublicationBadge.svelte';
|
|
3
|
+
import type { PagePublication } from '$lib/types';
|
|
4
|
+
import type { ContentAuthor } from '$lib/metadata';
|
|
5
|
+
interface Props {
|
|
6
|
+
modified?: string;
|
|
7
|
+
published?: string;
|
|
8
|
+
author?: ContentAuthor;
|
|
9
|
+
curated?: boolean;
|
|
10
|
+
/** Reading time in minutes. */
|
|
11
|
+
readingTime?: number;
|
|
12
|
+
publication?: PagePublication;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
modified,
|
|
17
|
+
published,
|
|
18
|
+
author,
|
|
19
|
+
curated = false,
|
|
20
|
+
readingTime,
|
|
21
|
+
publication
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
const publishedLabel = $derived(
|
|
24
|
+
published
|
|
25
|
+
? new Date(published).toLocaleDateString('en-US', {
|
|
26
|
+
year: 'numeric',
|
|
27
|
+
month: 'short',
|
|
28
|
+
day: 'numeric',
|
|
29
|
+
timeZone: 'UTC'
|
|
30
|
+
})
|
|
31
|
+
: undefined
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const formatted = $derived(
|
|
35
|
+
modified
|
|
36
|
+
? new Date(modified).toLocaleDateString('en-US', {
|
|
37
|
+
year: 'numeric',
|
|
38
|
+
month: 'short',
|
|
39
|
+
day: 'numeric',
|
|
40
|
+
timeZone: 'UTC'
|
|
41
|
+
})
|
|
42
|
+
: undefined
|
|
43
|
+
);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{#if formatted || readingTime || publication}
|
|
47
|
+
<p class="m-0 flex flex-wrap items-center gap-x-2 gap-y-1 text-[0.8125rem] text-muted">
|
|
48
|
+
{#if author}
|
|
49
|
+
<span
|
|
50
|
+
>{curated ? 'Curated by' : 'By'}
|
|
51
|
+
{#if author.url}<a href={author.url} rel="author">{author.name}</a>{:else}{author.name}{/if}
|
|
52
|
+
</span>
|
|
53
|
+
{/if}
|
|
54
|
+
{#if publishedLabel}<span>Published: <time datetime={published}>{publishedLabel}</time></span
|
|
55
|
+
>{/if}
|
|
56
|
+
{#if formatted}<span>Last modified: <time datetime={modified}>{formatted}</time></span>{/if}
|
|
57
|
+
{#if formatted && readingTime}<span aria-hidden="true"> · </span>{/if}
|
|
58
|
+
{#if readingTime}<span>{readingTime} min read</span>{/if}
|
|
59
|
+
{#if publication}<PublicationBadge {publication} />{/if}
|
|
60
|
+
</p>
|
|
61
|
+
{/if}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
|
4
|
+
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
5
|
+
import type { NavNode } from '$lib/types';
|
|
6
|
+
import { cn } from '$lib/utils';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
nav: NavNode[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PagerItem {
|
|
13
|
+
title: string;
|
|
14
|
+
route: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { nav }: Props = $props();
|
|
18
|
+
|
|
19
|
+
const pages = $derived(flattenNav(nav));
|
|
20
|
+
const currentRoute = $derived(normalizeRoute(page.url.pathname));
|
|
21
|
+
const currentIndex = $derived(pages.findIndex((item) => item.route === currentRoute));
|
|
22
|
+
const previousPage = $derived(currentIndex > 0 ? pages[currentIndex - 1] : undefined);
|
|
23
|
+
const nextPage = $derived(
|
|
24
|
+
currentIndex >= 0 && currentIndex < pages.length - 1 ? pages[currentIndex + 1] : undefined
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function flattenNav(nodes: NavNode[]): PagerItem[] {
|
|
28
|
+
const items: PagerItem[] = [];
|
|
29
|
+
const seenRoutes = new Set<string>();
|
|
30
|
+
|
|
31
|
+
function visit(node: NavNode): void {
|
|
32
|
+
if (node.route) {
|
|
33
|
+
const route = normalizeRoute(node.route);
|
|
34
|
+
if (!seenRoutes.has(route)) {
|
|
35
|
+
seenRoutes.add(route);
|
|
36
|
+
items.push({ title: node.title, route });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const child of node.children) visit(child);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const node of nodes) visit(node);
|
|
44
|
+
return items;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** trailingSlash is 'never' on this site: compare routes without it. */
|
|
48
|
+
function normalizeRoute(path: string): string {
|
|
49
|
+
let normalized = path.startsWith('/') ? path : `/${path}`;
|
|
50
|
+
if (normalized.length > 1 && normalized.endsWith('/')) normalized = normalized.slice(0, -1);
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const linkClass =
|
|
55
|
+
'group flex min-h-16 items-center gap-3 rounded-md border border-border px-4 py-3 no-underline transition-colors hover:border-accent hover:bg-surface focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none';
|
|
56
|
+
const labelClass = 'block text-xs font-semibold uppercase text-muted';
|
|
57
|
+
const titleClass = 'block text-sm font-semibold leading-snug text-text group-hover:text-accent';
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
{#if previousPage || nextPage}
|
|
61
|
+
<nav
|
|
62
|
+
class="mt-8 grid grid-cols-2 gap-3 border-t border-border pt-4 max-[640px]:grid-cols-1"
|
|
63
|
+
aria-label="Page navigation"
|
|
64
|
+
>
|
|
65
|
+
{#if previousPage}
|
|
66
|
+
<a class={linkClass} href={previousPage.route}>
|
|
67
|
+
<ChevronLeft class="size-5 shrink-0 text-muted transition-colors group-hover:text-accent" />
|
|
68
|
+
<span class="min-w-0">
|
|
69
|
+
<span class={labelClass}>Previous</span>
|
|
70
|
+
<span class={titleClass}>{previousPage.title}</span>
|
|
71
|
+
</span>
|
|
72
|
+
</a>
|
|
73
|
+
{:else}
|
|
74
|
+
<div class="max-[640px]:hidden" aria-hidden="true"></div>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
{#if nextPage}
|
|
78
|
+
<a class={cn(linkClass, 'justify-end text-right')} href={nextPage.route}>
|
|
79
|
+
<span class="min-w-0">
|
|
80
|
+
<span class={labelClass}>Next</span>
|
|
81
|
+
<span class={titleClass}>{nextPage.title}</span>
|
|
82
|
+
</span>
|
|
83
|
+
<ChevronRight
|
|
84
|
+
class="size-5 shrink-0 text-muted transition-colors group-hover:text-accent"
|
|
85
|
+
/>
|
|
86
|
+
</a>
|
|
87
|
+
{:else}
|
|
88
|
+
<div class="max-[640px]:hidden" aria-hidden="true"></div>
|
|
89
|
+
{/if}
|
|
90
|
+
</nav>
|
|
91
|
+
{/if}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { afterNavigate } from '$app/navigation';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import Menu from '@lucide/svelte/icons/menu';
|
|
5
|
+
import { onMount, type Snippet } from 'svelte';
|
|
6
|
+
import { provideFileWorkspace } from '$lib/components/files/file-workspace.svelte';
|
|
7
|
+
import type { NavNode } from '$lib/types';
|
|
8
|
+
import { site } from '$lib/config';
|
|
9
|
+
import { ThemeToggle } from '$lib/components/ui';
|
|
10
|
+
import IconButton from '../ui/IconButton.svelte';
|
|
11
|
+
import { SearchButton, SearchPalette } from '$lib/components/search';
|
|
12
|
+
import GraphPanel from '$lib/components/graph/GraphPanel.svelte';
|
|
13
|
+
import DocPager from './DocPager.svelte';
|
|
14
|
+
import ReaderModeToggle from './ReaderModeToggle.svelte';
|
|
15
|
+
import Sidebar from './Sidebar.svelte';
|
|
16
|
+
import Toc from './Toc.svelte';
|
|
17
|
+
import { readerMode } from './reader-mode.svelte';
|
|
18
|
+
import { createHeadingTracker } from './heading-tracker.svelte';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
nav: NavNode[];
|
|
22
|
+
children: Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { nav, children }: Props = $props();
|
|
26
|
+
const workspace = provideFileWorkspace();
|
|
27
|
+
onMount(() => workspace.restore());
|
|
28
|
+
|
|
29
|
+
let mobileNavOpen = $state(false);
|
|
30
|
+
let navigationReady = $state(false);
|
|
31
|
+
type RailPageData = { kind?: string };
|
|
32
|
+
const railPageData = $derived(page.data as RailPageData);
|
|
33
|
+
const showContentRail = $derived(railPageData.kind === 'page');
|
|
34
|
+
const heading = createHeadingTracker(() => page.data.toc ?? []);
|
|
35
|
+
|
|
36
|
+
afterNavigate(() => {
|
|
37
|
+
mobileNavOpen = false;
|
|
38
|
+
navigationReady = true;
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
class={[
|
|
44
|
+
'doc-shell min-h-screen',
|
|
45
|
+
railPageData.kind === 'file-browser' && 'doc-shell-files',
|
|
46
|
+
!showContentRail && 'doc-shell-listing',
|
|
47
|
+
readerMode.enabled && 'doc-shell-reader'
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
<header
|
|
51
|
+
class="doc-mobile-header sticky top-0 z-30 items-center gap-3 border-b border-border bg-bg px-4 py-2"
|
|
52
|
+
>
|
|
53
|
+
<IconButton
|
|
54
|
+
class="size-8 rounded-md border border-border bg-transparent text-text"
|
|
55
|
+
aria-label="Toggle navigation"
|
|
56
|
+
aria-expanded={mobileNavOpen}
|
|
57
|
+
disabled={!navigationReady}
|
|
58
|
+
onclick={() => (mobileNavOpen = !mobileNavOpen)}
|
|
59
|
+
>
|
|
60
|
+
<Menu class="size-5" />
|
|
61
|
+
</IconButton>
|
|
62
|
+
<a class="font-bold text-text no-underline" href="/">{site.name}</a>
|
|
63
|
+
<div class="ml-auto flex items-center gap-1">
|
|
64
|
+
<SearchButton variant="icon" class="size-8" />
|
|
65
|
+
<ReaderModeToggle class="size-8" />
|
|
66
|
+
<ThemeToggle class="size-8" />
|
|
67
|
+
</div>
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<aside class={['doc-sidebar-left border-r border-border bg-bg', mobileNavOpen && 'doc-nav-open']}>
|
|
71
|
+
<Sidebar {nav} />
|
|
72
|
+
</aside>
|
|
73
|
+
|
|
74
|
+
{#if readerMode.enabled}
|
|
75
|
+
<!-- floating controls so reader mode can be exited once the rails are gone -->
|
|
76
|
+
<div
|
|
77
|
+
class="doc-reader-controls fixed top-3 right-3 z-30 flex items-center gap-1 rounded-lg border border-border bg-bg/90 p-1 backdrop-blur"
|
|
78
|
+
>
|
|
79
|
+
<ReaderModeToggle class="size-8" />
|
|
80
|
+
<ThemeToggle class="size-8" />
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
{#if mobileNavOpen}
|
|
85
|
+
<button
|
|
86
|
+
class="doc-nav-backdrop fixed inset-0 z-40 cursor-pointer border-0 bg-black/40"
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label="Close navigation"
|
|
89
|
+
onclick={() => (mobileNavOpen = false)}
|
|
90
|
+
></button>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
<main class="doc-main min-w-0" class:doc-main-files={railPageData.kind === 'file-browser'}>
|
|
94
|
+
<div class={railPageData.kind === 'file-browser' ? 'h-full w-full' : 'mx-auto max-w-3xl'}>
|
|
95
|
+
{@render children()}
|
|
96
|
+
<DocPager {nav} />
|
|
97
|
+
</div>
|
|
98
|
+
</main>
|
|
99
|
+
|
|
100
|
+
{#if !readerMode.enabled && showContentRail}
|
|
101
|
+
<aside class="doc-sidebar-right" aria-label="Page tools">
|
|
102
|
+
<div class="doc-sidebar-right-inner">
|
|
103
|
+
<GraphPanel />
|
|
104
|
+
{#if (page.data.toc?.length ?? 0) > 0}
|
|
105
|
+
<details class="doc-compact-outline">
|
|
106
|
+
<summary class="cursor-pointer text-sm text-muted">On this page</summary>
|
|
107
|
+
<Toc active={heading.active} />
|
|
108
|
+
</details>
|
|
109
|
+
{/if}
|
|
110
|
+
<div class="doc-toc"><Toc active={heading.active} /></div>
|
|
111
|
+
</div>
|
|
112
|
+
</aside>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
<SearchPalette />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ContentLicense } from '$lib/content-license';
|
|
3
|
+
const { license }: { license: ContentLicense } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<svelte:head><link rel="license" href={license.url} /></svelte:head>
|
|
7
|
+
|
|
8
|
+
<aside
|
|
9
|
+
aria-label="Content license"
|
|
10
|
+
class="border-t border-border py-3 text-xs leading-relaxed text-muted"
|
|
11
|
+
data-pagefind-ignore
|
|
12
|
+
>
|
|
13
|
+
<a
|
|
14
|
+
href={license.source}
|
|
15
|
+
class="text-muted underline decoration-border underline-offset-2 hover:text-text"
|
|
16
|
+
>{license.attribution}</a
|
|
17
|
+
>
|
|
18
|
+
·
|
|
19
|
+
<a
|
|
20
|
+
href={license.url}
|
|
21
|
+
rel="license"
|
|
22
|
+
class="text-muted underline decoration-border underline-offset-2 hover:text-text"
|
|
23
|
+
>{license.name}</a
|
|
24
|
+
>, unless otherwise noted.
|
|
25
|
+
{#if license.changes}<p class="m-0 mt-1">{license.changes}</p>{/if}
|
|
26
|
+
</aside>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import LockKeyhole from '@lucide/svelte/icons/lock-keyhole';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import { authEnabled } from '$lib/config';
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div
|
|
8
|
+
class="flex items-start gap-3 rounded-md border border-border p-4 text-sm"
|
|
9
|
+
data-pagefind-ignore
|
|
10
|
+
>
|
|
11
|
+
<LockKeyhole class="mt-0.5 size-4 shrink-0 text-muted" />
|
|
12
|
+
<div>
|
|
13
|
+
<p class="m-0 font-medium">Content locked</p>
|
|
14
|
+
<p class="mt-1 mb-3 text-muted">
|
|
15
|
+
{#if authEnabled}
|
|
16
|
+
The catalog is public. Reading this content requires approved access.
|
|
17
|
+
{:else}
|
|
18
|
+
This content is unavailable on this site.
|
|
19
|
+
{/if}
|
|
20
|
+
</p>
|
|
21
|
+
{#if authEnabled}
|
|
22
|
+
<a
|
|
23
|
+
data-sveltekit-reload
|
|
24
|
+
class="text-accent hover:underline"
|
|
25
|
+
href={`/login?next=${encodeURIComponent(page.url.pathname)}`}>Sign in to read</a
|
|
26
|
+
>
|
|
27
|
+
{/if}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|