@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,351 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { tick, untrack } from 'svelte';
|
|
4
|
+
import { fade } from 'svelte/transition';
|
|
5
|
+
import { prefersReducedMotion } from 'svelte/motion';
|
|
6
|
+
import BookOpen from '@lucide/svelte/icons/book-open';
|
|
7
|
+
import FolderCode from '@lucide/svelte/icons/folder-code';
|
|
8
|
+
import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
|
|
9
|
+
import LogOut from '@lucide/svelte/icons/log-out';
|
|
10
|
+
import LogIn from '@lucide/svelte/icons/log-in';
|
|
11
|
+
import { siGithub } from 'simple-icons';
|
|
12
|
+
const publicEdition = $derived(page.data.edition === 'public');
|
|
13
|
+
import type { NavNode } from '$lib/types';
|
|
14
|
+
import { isNotebookRoute } from '$lib/notebook-nav';
|
|
15
|
+
import { authEnabled, site } from '$lib/config';
|
|
16
|
+
import { ThemeToggle } from '$lib/components/ui';
|
|
17
|
+
import IconButton from '$lib/components/ui/IconButton.svelte';
|
|
18
|
+
import NavTree from './NavTree.svelte';
|
|
19
|
+
import ReaderModeToggle from './ReaderModeToggle.svelte';
|
|
20
|
+
import AccessLink from './AccessLink.svelte';
|
|
21
|
+
import FileExplorer from '$lib/components/files/FileExplorer.svelte';
|
|
22
|
+
import { fileWorkspace } from '$lib/components/files/file-workspace.svelte';
|
|
23
|
+
import { SearchButton } from '$lib/components/search';
|
|
24
|
+
|
|
25
|
+
const { nav }: { nav: NavNode[] } = $props();
|
|
26
|
+
const workspace = fileWorkspace();
|
|
27
|
+
const uid = $props.id();
|
|
28
|
+
const path = $derived(decodeURI(page.url.pathname));
|
|
29
|
+
const course = $derived(
|
|
30
|
+
nav.find((node) => node.children.length && path.split('/')[1] === node.segment)
|
|
31
|
+
);
|
|
32
|
+
const courseNodes = $derived(
|
|
33
|
+
(course?.children ?? [])
|
|
34
|
+
.map((node) => ({
|
|
35
|
+
...node,
|
|
36
|
+
title:
|
|
37
|
+
node.segment === 'README'
|
|
38
|
+
? 'Overview'
|
|
39
|
+
: ((
|
|
40
|
+
{
|
|
41
|
+
lectures: 'Lectures',
|
|
42
|
+
homework: 'Homework',
|
|
43
|
+
exams: 'Exams',
|
|
44
|
+
projects: 'Projects'
|
|
45
|
+
} as Record<string, string>
|
|
46
|
+
)[node.title] ?? node.title)
|
|
47
|
+
}))
|
|
48
|
+
.sort((a, b) => Number(b.segment === 'README') - Number(a.segment === 'README'))
|
|
49
|
+
);
|
|
50
|
+
let choosing = $state(false);
|
|
51
|
+
let explorer = $state<'notes' | 'files'>(
|
|
52
|
+
untrack(() =>
|
|
53
|
+
page.data.kind === 'file-browser' && !isNotebookRoute(nav, path) ? 'files' : 'notes'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
$effect(() => {
|
|
57
|
+
explorer = page.data.kind === 'file-browser' && !isNotebookRoute(nav, path) ? 'files' : 'notes';
|
|
58
|
+
});
|
|
59
|
+
let query = $state('');
|
|
60
|
+
let search = $state<HTMLInputElement>();
|
|
61
|
+
let switcher: HTMLButtonElement;
|
|
62
|
+
let viewport = $state<HTMLElement>();
|
|
63
|
+
let expanded = $state<Record<string, boolean>>({});
|
|
64
|
+
function semester(node: NavNode) {
|
|
65
|
+
const match = /^(sp|su|fa|wi)(\d{2})-/.exec(node.segment);
|
|
66
|
+
return match
|
|
67
|
+
? `${{ sp: 'Spring', su: 'Summer', fa: 'Fall', wi: 'Winter' }[match[1]]} 20${match[2]}`
|
|
68
|
+
: 'Other';
|
|
69
|
+
}
|
|
70
|
+
function label(node: NavNode) {
|
|
71
|
+
return node.segment
|
|
72
|
+
.replace(/^(sp|su|fa|wi)\d{2}-/, '')
|
|
73
|
+
.replace(/([a-z])(\d)/, '$1 $2')
|
|
74
|
+
.toUpperCase();
|
|
75
|
+
}
|
|
76
|
+
function landing(node: NavNode) {
|
|
77
|
+
return node.children.find((child) => child.segment === 'README')?.route ?? node.route ?? '/';
|
|
78
|
+
}
|
|
79
|
+
function semesterOrder(node: NavNode) {
|
|
80
|
+
const match = /^(sp|su|fa|wi)(\d{2})-/.exec(node.segment);
|
|
81
|
+
return match ? Number(match[2]) * 10 + ['wi', 'sp', 'su', 'fa'].indexOf(match[1]) : -1;
|
|
82
|
+
}
|
|
83
|
+
const groups = $derived.by(() => {
|
|
84
|
+
const result = new Map<string, NavNode[]>();
|
|
85
|
+
for (const node of [...nav].sort(
|
|
86
|
+
(a, b) =>
|
|
87
|
+
semesterOrder(b) - semesterOrder(a) ||
|
|
88
|
+
label(a).localeCompare(label(b), undefined, { numeric: true })
|
|
89
|
+
)) {
|
|
90
|
+
if (
|
|
91
|
+
!node.children.length ||
|
|
92
|
+
!`${label(node)} ${semester(node)} ${node.title}`
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.includes(query.toLowerCase().trim())
|
|
95
|
+
)
|
|
96
|
+
continue;
|
|
97
|
+
const group = semester(node);
|
|
98
|
+
result.set(group, [...(result.get(group) ?? []), node]);
|
|
99
|
+
}
|
|
100
|
+
return [...result];
|
|
101
|
+
});
|
|
102
|
+
async function toggleCourses() {
|
|
103
|
+
choosing = !choosing;
|
|
104
|
+
query = '';
|
|
105
|
+
if (choosing) {
|
|
106
|
+
await tick();
|
|
107
|
+
search?.focus();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
$effect(() => {
|
|
111
|
+
const current = path;
|
|
112
|
+
untrack(() => {
|
|
113
|
+
choosing = false;
|
|
114
|
+
query = '';
|
|
115
|
+
const next = { ...expanded };
|
|
116
|
+
function reveal(nodes: NavNode[]): boolean {
|
|
117
|
+
let found = false;
|
|
118
|
+
for (const node of nodes) {
|
|
119
|
+
const childActive = reveal(node.children);
|
|
120
|
+
if (childActive && node.route) next[node.route] = true;
|
|
121
|
+
if (childActive || node.route === current) found = true;
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
reveal(nav);
|
|
126
|
+
expanded = next;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
$effect(() => {
|
|
130
|
+
path;
|
|
131
|
+
if (!viewport || explorer !== 'notes') return;
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
const link = viewport?.querySelector<HTMLElement>('[aria-current="page"]');
|
|
134
|
+
if (!link || !viewport || !link.getClientRects().length) return;
|
|
135
|
+
const bounds = viewport.getBoundingClientRect(),
|
|
136
|
+
row = link.getBoundingClientRect();
|
|
137
|
+
if (row.top < bounds.top || row.bottom > bounds.bottom)
|
|
138
|
+
viewport.scrollTo({
|
|
139
|
+
top:
|
|
140
|
+
viewport.scrollTop + row.top - bounds.top - viewport.clientHeight / 2 + row.height / 2,
|
|
141
|
+
behavior: prefersReducedMotion.current ? 'instant' : 'smooth'
|
|
142
|
+
});
|
|
143
|
+
}, 200);
|
|
144
|
+
return () => clearTimeout(timer);
|
|
145
|
+
});
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<div class="doc-sidebar-content flex h-full min-h-0 flex-col gap-3">
|
|
149
|
+
<a class="text-[1.05rem] font-bold tracking-[-0.01em] text-text no-underline" href="/"
|
|
150
|
+
>{site.name}</a
|
|
151
|
+
>
|
|
152
|
+
<SearchButton />
|
|
153
|
+
<button
|
|
154
|
+
class="course-switcher"
|
|
155
|
+
bind:this={switcher}
|
|
156
|
+
aria-expanded={choosing || !course}
|
|
157
|
+
aria-controls={`${uid}-courses`}
|
|
158
|
+
onclick={toggleCourses}
|
|
159
|
+
>
|
|
160
|
+
<span
|
|
161
|
+
><small>{course ? semester(course) : 'Workspace'}</small><strong
|
|
162
|
+
>{course ? label(course) : 'Browse courses'}</strong
|
|
163
|
+
></span
|
|
164
|
+
>
|
|
165
|
+
<ChevronsUpDown size={15} />
|
|
166
|
+
</button>
|
|
167
|
+
{#if course && !choosing}
|
|
168
|
+
<div
|
|
169
|
+
class="inline-flex shrink-0 self-start gap-0.5 rounded-full bg-surface p-0.5"
|
|
170
|
+
role="group"
|
|
171
|
+
aria-label="Explorer view"
|
|
172
|
+
>
|
|
173
|
+
{#each ['notes', 'files'] as view}
|
|
174
|
+
<button
|
|
175
|
+
class="flex cursor-pointer items-center gap-1.5 rounded-full px-2.5 py-1 text-[0.6875rem] leading-4 text-muted aria-pressed:bg-bg aria-pressed:text-text"
|
|
176
|
+
aria-pressed={explorer === view}
|
|
177
|
+
aria-controls={`${uid}-courses`}
|
|
178
|
+
onclick={() => (explorer = view as 'notes' | 'files')}
|
|
179
|
+
>
|
|
180
|
+
{#if view === 'notes'}<BookOpen size={12} />Notes{:else}<FolderCode size={12} />Files{/if}
|
|
181
|
+
</button>
|
|
182
|
+
{/each}
|
|
183
|
+
</div>
|
|
184
|
+
{/if}
|
|
185
|
+
<nav
|
|
186
|
+
id={`${uid}-courses`}
|
|
187
|
+
class="course-navigation min-h-0 flex-1 overflow-y-auto"
|
|
188
|
+
aria-label="Documentation"
|
|
189
|
+
bind:this={viewport}
|
|
190
|
+
>
|
|
191
|
+
{#if choosing || !course}
|
|
192
|
+
<div>
|
|
193
|
+
<input
|
|
194
|
+
bind:this={search}
|
|
195
|
+
bind:value={query}
|
|
196
|
+
aria-label="Find a course"
|
|
197
|
+
placeholder="Find a course…"
|
|
198
|
+
onkeydown={(event) => {
|
|
199
|
+
if (event.key === 'Escape') {
|
|
200
|
+
choosing = false;
|
|
201
|
+
query = '';
|
|
202
|
+
switcher.focus();
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
{#each groups as [name, courses]}
|
|
207
|
+
<p class="group-label">{name}</p>
|
|
208
|
+
{#each courses as item}
|
|
209
|
+
<a
|
|
210
|
+
class="course-option"
|
|
211
|
+
href={landing(item)}
|
|
212
|
+
onclick={() => (choosing = false)}
|
|
213
|
+
aria-current={item === course ? 'true' : undefined}>{label(item)}</a
|
|
214
|
+
>
|
|
215
|
+
{/each}
|
|
216
|
+
{/each}
|
|
217
|
+
{#if !groups.length}<p class="empty">No courses match “{query}”.</p>{/if}
|
|
218
|
+
<div class="workspace-links">
|
|
219
|
+
<a class="course-option" href="/">Workspace home</a>
|
|
220
|
+
{#each nav.filter((node) => !node.children.length && node.route) as node}<a
|
|
221
|
+
class="course-option"
|
|
222
|
+
href={node.route}>{node.title}</a
|
|
223
|
+
>{/each}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
{:else}
|
|
227
|
+
{#key course.segment}
|
|
228
|
+
{#if explorer === 'notes'}
|
|
229
|
+
<div in:fade={{ duration: prefersReducedMotion.current ? 0 : 120 }}>
|
|
230
|
+
<NavTree nodes={courseNodes} bind:expanded />
|
|
231
|
+
</div>
|
|
232
|
+
{:else}
|
|
233
|
+
<FileExplorer course={course.segment} />
|
|
234
|
+
{/if}
|
|
235
|
+
{/key}
|
|
236
|
+
{/if}
|
|
237
|
+
</nav>
|
|
238
|
+
<div class="flex shrink-0 items-center justify-between border-t border-border pt-3">
|
|
239
|
+
<a
|
|
240
|
+
href="https://github.com/twangodev/twiki"
|
|
241
|
+
target="_blank"
|
|
242
|
+
rel="noopener noreferrer"
|
|
243
|
+
aria-label="Source on GitHub"
|
|
244
|
+
title="Source on GitHub"
|
|
245
|
+
class="inline-flex size-9 items-center justify-center rounded-lg text-muted hover:bg-surface hover:text-text"
|
|
246
|
+
>
|
|
247
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
248
|
+
<path d={siGithub.path} />
|
|
249
|
+
</svg>
|
|
250
|
+
</a>
|
|
251
|
+
<ThemeToggle /><ReaderModeToggle />
|
|
252
|
+
{#if authEnabled}
|
|
253
|
+
<AccessLink />
|
|
254
|
+
{#if publicEdition}
|
|
255
|
+
<a
|
|
256
|
+
href={`/login?next=${encodeURIComponent(page.url.pathname)}`}
|
|
257
|
+
data-sveltekit-reload
|
|
258
|
+
aria-label="Sign in"
|
|
259
|
+
title="Sign in"
|
|
260
|
+
class="inline-flex size-9 items-center justify-center rounded-lg text-muted hover:bg-surface hover:text-text"
|
|
261
|
+
><LogIn size={16} /></a
|
|
262
|
+
>
|
|
263
|
+
{:else}
|
|
264
|
+
<form method="post" action="/logout" onsubmit={() => workspace.clear()}>
|
|
265
|
+
<IconButton type="submit" aria-label="Sign out" title="Sign out"
|
|
266
|
+
><LogOut size={16} /></IconButton
|
|
267
|
+
>
|
|
268
|
+
</form>
|
|
269
|
+
{/if}
|
|
270
|
+
{/if}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<style>
|
|
275
|
+
.course-switcher {
|
|
276
|
+
display: flex;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
align-items: center;
|
|
279
|
+
text-align: left;
|
|
280
|
+
padding: 0.5rem;
|
|
281
|
+
border: 1px solid var(--color-border);
|
|
282
|
+
border-radius: 0.375rem;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
color: var(--color-text);
|
|
285
|
+
}
|
|
286
|
+
.course-switcher small,
|
|
287
|
+
.course-switcher strong {
|
|
288
|
+
display: block;
|
|
289
|
+
}
|
|
290
|
+
.course-switcher small {
|
|
291
|
+
font-size: 0.625rem;
|
|
292
|
+
color: var(--color-muted);
|
|
293
|
+
margin-bottom: 0.125rem;
|
|
294
|
+
}
|
|
295
|
+
.course-switcher strong {
|
|
296
|
+
font-size: 0.8125rem;
|
|
297
|
+
font-weight: 600;
|
|
298
|
+
}
|
|
299
|
+
.course-navigation {
|
|
300
|
+
scrollbar-width: thin;
|
|
301
|
+
overflow-anchor: none;
|
|
302
|
+
overscroll-behavior: contain;
|
|
303
|
+
}
|
|
304
|
+
input {
|
|
305
|
+
width: 100%;
|
|
306
|
+
padding: 0.5rem;
|
|
307
|
+
font-size: 0.8125rem;
|
|
308
|
+
border: 1px solid var(--color-border);
|
|
309
|
+
border-radius: 0.25rem;
|
|
310
|
+
background: var(--color-bg);
|
|
311
|
+
}
|
|
312
|
+
.group-label {
|
|
313
|
+
margin: 1rem 0 0.25rem;
|
|
314
|
+
padding-inline: 0.5rem;
|
|
315
|
+
font-size: 0.625rem;
|
|
316
|
+
letter-spacing: 0.06em;
|
|
317
|
+
text-transform: uppercase;
|
|
318
|
+
color: var(--color-muted);
|
|
319
|
+
}
|
|
320
|
+
.course-option {
|
|
321
|
+
display: block;
|
|
322
|
+
padding: 0.375rem 0.5rem;
|
|
323
|
+
color: var(--color-text);
|
|
324
|
+
font-size: 0.8125rem;
|
|
325
|
+
text-decoration: none;
|
|
326
|
+
border-radius: 0.25rem;
|
|
327
|
+
}
|
|
328
|
+
.course-option:hover,
|
|
329
|
+
.course-switcher:hover {
|
|
330
|
+
background: var(--color-surface);
|
|
331
|
+
}
|
|
332
|
+
.course-option[aria-current] {
|
|
333
|
+
color: var(--color-accent);
|
|
334
|
+
}
|
|
335
|
+
.workspace-links {
|
|
336
|
+
border-top: 1px solid var(--color-border);
|
|
337
|
+
margin-top: 1rem;
|
|
338
|
+
padding-top: 0.5rem;
|
|
339
|
+
}
|
|
340
|
+
.empty {
|
|
341
|
+
font-size: 0.8125rem;
|
|
342
|
+
color: var(--color-muted);
|
|
343
|
+
padding: 0.5rem;
|
|
344
|
+
}
|
|
345
|
+
button:focus-visible,
|
|
346
|
+
a:focus-visible,
|
|
347
|
+
input:focus-visible {
|
|
348
|
+
outline: 2px solid var(--color-accent);
|
|
349
|
+
outline-offset: -1px;
|
|
350
|
+
}
|
|
351
|
+
</style>
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import { Spring, prefersReducedMotion } from 'svelte/motion';
|
|
5
|
+
import { slide } from 'svelte/transition';
|
|
6
|
+
import { cubicOut } from 'svelte/easing';
|
|
7
|
+
import type { TocEntry } from '$lib/types';
|
|
8
|
+
|
|
9
|
+
const uid = $props.id();
|
|
10
|
+
const { active }: { active: string } = $props();
|
|
11
|
+
const items = $derived((page.data.toc as TocEntry[] | undefined) ?? []);
|
|
12
|
+
type Node = TocEntry & { children: Node[]; ancestors: string[] };
|
|
13
|
+
const tree = $derived.by(() => {
|
|
14
|
+
const roots: Node[] = [],
|
|
15
|
+
parents: Node[] = [];
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
while (parents.length && parents[parents.length - 1].level >= item.level) parents.pop();
|
|
18
|
+
const node: Node = { ...item, children: [], ancestors: parents.map((p) => p.id) };
|
|
19
|
+
(parents.at(-1)?.children ?? roots).push(node);
|
|
20
|
+
parents.push(node);
|
|
21
|
+
}
|
|
22
|
+
return roots;
|
|
23
|
+
});
|
|
24
|
+
const nodes = $derived.by(() => {
|
|
25
|
+
const result = new Map<string, Node>();
|
|
26
|
+
function visit(branch: Node[]) {
|
|
27
|
+
for (const node of branch) {
|
|
28
|
+
result.set(node.id, node);
|
|
29
|
+
visit(node.children);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
visit(tree);
|
|
33
|
+
return result;
|
|
34
|
+
});
|
|
35
|
+
let focused = $state('');
|
|
36
|
+
let overrides = $state<Record<string, boolean>>({});
|
|
37
|
+
let expandAll = $state(false);
|
|
38
|
+
let list = $state<HTMLDivElement>();
|
|
39
|
+
let markerVisible = $state(false);
|
|
40
|
+
const marker = new Spring({ y: 0, height: 0 }, { stiffness: 0.2, damping: 0.85 });
|
|
41
|
+
const documentRoot = $derived(tree.length === 1 && tree[0].level === 1 ? tree[0].id : '');
|
|
42
|
+
function isOpen(node: Node): boolean {
|
|
43
|
+
if (nodes.get(focused)?.ancestors.includes(node.id)) return true;
|
|
44
|
+
return (
|
|
45
|
+
expandAll ||
|
|
46
|
+
(overrides[node.id] ??
|
|
47
|
+
Boolean(
|
|
48
|
+
node.id === documentRoot ||
|
|
49
|
+
nodes.get(active)?.ancestors.includes(node.id) ||
|
|
50
|
+
active === node.id
|
|
51
|
+
))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function measureMarker() {
|
|
55
|
+
const link = Array.from(
|
|
56
|
+
list?.querySelectorAll<HTMLAnchorElement>('a[aria-current="location"]') ?? []
|
|
57
|
+
).find((link) => link.getAttribute('href') === `#${encodeURIComponent(active)}`);
|
|
58
|
+
markerVisible = Boolean(link && link.getClientRects().length);
|
|
59
|
+
if (!link || !markerVisible || !list) return;
|
|
60
|
+
const box = link.getBoundingClientRect();
|
|
61
|
+
void marker.set(
|
|
62
|
+
{ y: box.top - list.getBoundingClientRect().top, height: box.height },
|
|
63
|
+
{ instant: prefersReducedMotion.current }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
function followActive() {
|
|
67
|
+
const viewport = list?.closest<HTMLElement>('.doc-toc');
|
|
68
|
+
const link = Array.from(
|
|
69
|
+
list?.querySelectorAll<HTMLAnchorElement>('a[aria-current="location"]') ?? []
|
|
70
|
+
).find((link) => link.getAttribute('href') === `#${encodeURIComponent(active)}`);
|
|
71
|
+
if (!link?.getClientRects().length) return;
|
|
72
|
+
if (viewport && viewport.clientHeight < viewport.scrollHeight) {
|
|
73
|
+
const box = link.getBoundingClientRect();
|
|
74
|
+
const bounds = viewport.getBoundingClientRect();
|
|
75
|
+
const headerHeight =
|
|
76
|
+
viewport.querySelector('.toc-header')?.getBoundingClientRect().height ?? 0;
|
|
77
|
+
const top = bounds.top + headerHeight + 24;
|
|
78
|
+
const bottom = bounds.bottom - 12;
|
|
79
|
+
// Leave room for nearby headings instead of riding the viewport edge.
|
|
80
|
+
const cushion = Math.min(48, Math.max(0, (bottom - top - box.height) / 4));
|
|
81
|
+
const delta =
|
|
82
|
+
box.top < top
|
|
83
|
+
? box.top - top - cushion
|
|
84
|
+
: box.bottom > bottom
|
|
85
|
+
? box.bottom - bottom + cushion
|
|
86
|
+
: 0;
|
|
87
|
+
if (Math.abs(delta) > 1)
|
|
88
|
+
viewport.scrollTo({
|
|
89
|
+
top: viewport.scrollTop + delta,
|
|
90
|
+
behavior: prefersReducedMotion.current ? 'instant' : 'smooth'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
$effect(() => {
|
|
95
|
+
items;
|
|
96
|
+
untrack(() => {
|
|
97
|
+
overrides = {};
|
|
98
|
+
expandAll = false;
|
|
99
|
+
focused = '';
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
$effect(() => {
|
|
103
|
+
active;
|
|
104
|
+
expandAll;
|
|
105
|
+
overrides;
|
|
106
|
+
focused;
|
|
107
|
+
if (!list) return;
|
|
108
|
+
let followTimer: ReturnType<typeof setTimeout>;
|
|
109
|
+
function measure() {
|
|
110
|
+
measureMarker();
|
|
111
|
+
// Branch slides resize on every frame. Follow once they settle, rather
|
|
112
|
+
// than repeatedly restarting a scroll against a moving target.
|
|
113
|
+
clearTimeout(followTimer);
|
|
114
|
+
followTimer = setTimeout(followActive, prefersReducedMotion.current ? 0 : 80);
|
|
115
|
+
}
|
|
116
|
+
const frame = requestAnimationFrame(measure);
|
|
117
|
+
const observer = new ResizeObserver(measure);
|
|
118
|
+
observer.observe(list);
|
|
119
|
+
const viewport = list.closest('.doc-toc');
|
|
120
|
+
if (viewport) observer.observe(viewport);
|
|
121
|
+
return () => {
|
|
122
|
+
cancelAnimationFrame(frame);
|
|
123
|
+
clearTimeout(followTimer);
|
|
124
|
+
observer.disconnect();
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
{#snippet branches(branch: Node[], depth = 0)}
|
|
130
|
+
<ul class="toc-branches" class:nested={depth > 0 && depth <= 2}>
|
|
131
|
+
{#each branch as item (item.id)}
|
|
132
|
+
<li>
|
|
133
|
+
{#if depth > 2 && branch[0] === item}
|
|
134
|
+
<div class="parent-context">↳ {nodes.get(item.ancestors.at(-1) ?? '')?.text}</div>
|
|
135
|
+
{/if}
|
|
136
|
+
<div class="toc-row">
|
|
137
|
+
{#if item.children.length}
|
|
138
|
+
<button
|
|
139
|
+
class="branch-toggle"
|
|
140
|
+
aria-label={`Toggle ${item.text} sections`}
|
|
141
|
+
aria-expanded={isOpen(item)}
|
|
142
|
+
aria-controls={`${uid}-${item.id}`}
|
|
143
|
+
onfocus={() => (focused = item.id)}
|
|
144
|
+
onblur={() => (focused = '')}
|
|
145
|
+
onclick={() => {
|
|
146
|
+
overrides = { ...overrides, [item.id]: !isOpen(item) };
|
|
147
|
+
expandAll = false;
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<svg
|
|
151
|
+
class:open={isOpen(item)}
|
|
152
|
+
width="12"
|
|
153
|
+
height="12"
|
|
154
|
+
viewBox="0 0 16 16"
|
|
155
|
+
aria-hidden="true"><path d="m6 4 4 4-4 4" /></svg
|
|
156
|
+
>
|
|
157
|
+
</button>
|
|
158
|
+
{:else}<span class="leaf-spacer"></span>{/if}
|
|
159
|
+
<a
|
|
160
|
+
class:parent={item.children.length > 0}
|
|
161
|
+
href={`#${encodeURIComponent(item.id)}`}
|
|
162
|
+
aria-current={active === item.id ? 'location' : undefined}
|
|
163
|
+
onfocus={() => (focused = item.id)}
|
|
164
|
+
onblur={() => (focused = '')}>{item.text}</a
|
|
165
|
+
>
|
|
166
|
+
</div>
|
|
167
|
+
<div id={`${uid}-${item.id}`}>
|
|
168
|
+
{#if item.children.length && isOpen(item)}
|
|
169
|
+
<div
|
|
170
|
+
onoutrostart={(event) => {
|
|
171
|
+
// Outgoing Svelte blocks retain their old attributes until the slide ends.
|
|
172
|
+
// Remove their stale current marker and keyboard/accessibility targets now.
|
|
173
|
+
event.currentTarget.inert = true;
|
|
174
|
+
event.currentTarget
|
|
175
|
+
.querySelectorAll('[aria-current]')
|
|
176
|
+
.forEach((link) => link.removeAttribute('aria-current'));
|
|
177
|
+
}}
|
|
178
|
+
onintrostart={(event) => {
|
|
179
|
+
event.currentTarget.inert = false;
|
|
180
|
+
}}
|
|
181
|
+
transition:slide={{
|
|
182
|
+
duration: prefersReducedMotion.current ? 0 : 180,
|
|
183
|
+
easing: cubicOut
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{@render branches(item.children, depth + (item.id === documentRoot ? 0 : 1))}
|
|
187
|
+
</div>
|
|
188
|
+
{/if}
|
|
189
|
+
</div>
|
|
190
|
+
</li>
|
|
191
|
+
{/each}
|
|
192
|
+
</ul>
|
|
193
|
+
{/snippet}
|
|
194
|
+
|
|
195
|
+
{#if items.length > 0}
|
|
196
|
+
<nav aria-label="On this page">
|
|
197
|
+
<div class="toc-header">
|
|
198
|
+
<p>On this page</p>
|
|
199
|
+
{#if items.some((item) => nodes.get(item.id)?.children.length)}
|
|
200
|
+
<button
|
|
201
|
+
class="mode-toggle"
|
|
202
|
+
onclick={() => {
|
|
203
|
+
expandAll = !expandAll;
|
|
204
|
+
overrides = {};
|
|
205
|
+
}}>{expandAll ? 'Focus' : 'Expand all'}</button
|
|
206
|
+
>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|
|
209
|
+
<div class="toc-list" bind:this={list}>
|
|
210
|
+
<span
|
|
211
|
+
class="position-marker"
|
|
212
|
+
aria-hidden="true"
|
|
213
|
+
style:opacity={markerVisible ? 1 : 0}
|
|
214
|
+
style:transform={`translateY(${marker.current.y}px)`}
|
|
215
|
+
style:height={`${marker.current.height}px`}
|
|
216
|
+
></span>
|
|
217
|
+
{@render branches(tree)}
|
|
218
|
+
</div>
|
|
219
|
+
</nav>
|
|
220
|
+
{/if}
|
|
221
|
+
|
|
222
|
+
<style>
|
|
223
|
+
nav {
|
|
224
|
+
font-size: 0.8125rem;
|
|
225
|
+
}
|
|
226
|
+
.toc-header {
|
|
227
|
+
display: flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: space-between;
|
|
230
|
+
gap: 0.5rem;
|
|
231
|
+
margin-bottom: 0.375rem;
|
|
232
|
+
}
|
|
233
|
+
p {
|
|
234
|
+
margin: 0;
|
|
235
|
+
font-size: 0.7rem;
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
letter-spacing: 0.08em;
|
|
238
|
+
text-transform: uppercase;
|
|
239
|
+
color: var(--color-muted);
|
|
240
|
+
}
|
|
241
|
+
button {
|
|
242
|
+
color: var(--color-muted);
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
border-radius: 0.25rem;
|
|
245
|
+
}
|
|
246
|
+
.mode-toggle {
|
|
247
|
+
font-size: 0.6875rem;
|
|
248
|
+
padding: 0.25rem;
|
|
249
|
+
}
|
|
250
|
+
.toc-list {
|
|
251
|
+
position: relative;
|
|
252
|
+
padding-left: 0.25rem;
|
|
253
|
+
}
|
|
254
|
+
.position-marker {
|
|
255
|
+
position: absolute;
|
|
256
|
+
top: 0;
|
|
257
|
+
left: 0;
|
|
258
|
+
width: 2px;
|
|
259
|
+
border-radius: 2px;
|
|
260
|
+
background: var(--color-accent);
|
|
261
|
+
pointer-events: none;
|
|
262
|
+
}
|
|
263
|
+
.toc-branches {
|
|
264
|
+
list-style: none;
|
|
265
|
+
margin: 0;
|
|
266
|
+
padding: 0;
|
|
267
|
+
}
|
|
268
|
+
.toc-branches.nested {
|
|
269
|
+
margin-left: 0.5rem;
|
|
270
|
+
padding-left: 0.5rem;
|
|
271
|
+
border-left: 1px solid var(--color-border);
|
|
272
|
+
}
|
|
273
|
+
.toc-row {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: flex-start;
|
|
276
|
+
}
|
|
277
|
+
.branch-toggle,
|
|
278
|
+
.leaf-spacer {
|
|
279
|
+
flex: 0 0 1rem;
|
|
280
|
+
width: 1rem;
|
|
281
|
+
height: 1.625rem;
|
|
282
|
+
display: grid;
|
|
283
|
+
place-items: center;
|
|
284
|
+
}
|
|
285
|
+
svg {
|
|
286
|
+
fill: none;
|
|
287
|
+
stroke: currentColor;
|
|
288
|
+
stroke-width: 1.5;
|
|
289
|
+
transition: transform 180ms;
|
|
290
|
+
}
|
|
291
|
+
svg.open {
|
|
292
|
+
transform: rotate(90deg);
|
|
293
|
+
}
|
|
294
|
+
a {
|
|
295
|
+
display: block;
|
|
296
|
+
min-width: 0;
|
|
297
|
+
flex: 1;
|
|
298
|
+
border-radius: 0.25rem;
|
|
299
|
+
padding: 0.25rem;
|
|
300
|
+
color: var(--color-muted);
|
|
301
|
+
line-height: 1.4;
|
|
302
|
+
overflow-wrap: anywhere;
|
|
303
|
+
text-decoration: none;
|
|
304
|
+
transition:
|
|
305
|
+
color 120ms,
|
|
306
|
+
background-color 120ms;
|
|
307
|
+
}
|
|
308
|
+
a.parent {
|
|
309
|
+
font-weight: 500;
|
|
310
|
+
color: var(--color-text);
|
|
311
|
+
}
|
|
312
|
+
a[aria-current],
|
|
313
|
+
a:hover,
|
|
314
|
+
button:hover {
|
|
315
|
+
color: var(--color-accent);
|
|
316
|
+
}
|
|
317
|
+
a[aria-current] {
|
|
318
|
+
background: var(--color-surface);
|
|
319
|
+
}
|
|
320
|
+
a:focus-visible,
|
|
321
|
+
button:focus-visible {
|
|
322
|
+
outline: 2px solid var(--color-accent);
|
|
323
|
+
outline-offset: 1px;
|
|
324
|
+
}
|
|
325
|
+
.parent-context {
|
|
326
|
+
padding: 0.25rem 0.25rem 0 1.25rem;
|
|
327
|
+
font-size: 0.625rem;
|
|
328
|
+
color: var(--color-muted);
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
text-overflow: ellipsis;
|
|
331
|
+
white-space: nowrap;
|
|
332
|
+
}
|
|
333
|
+
@media (prefers-reduced-motion: reduce) {
|
|
334
|
+
svg,
|
|
335
|
+
a {
|
|
336
|
+
transition: none;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
</style>
|