@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.
Files changed (225) hide show
  1. package/.github/actions/product-cache/action.yml +32 -0
  2. package/.github/actions/product-cache/index.mjs +450 -0
  3. package/LICENSE +21 -0
  4. package/README.md +146 -2
  5. package/bun.lock +1701 -0
  6. package/cli/runtime.test.ts +420 -0
  7. package/cli/runtime.ts +385 -0
  8. package/cli/twiki.ts +305 -0
  9. package/package.json +138 -4
  10. package/site/_headers +7 -0
  11. package/site/assets/fonts/OFL.txt +93 -0
  12. package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
  13. package/site/assets/social-background.svg +38 -0
  14. package/site/bun.lock +1701 -0
  15. package/site/database/index.ts +7 -0
  16. package/site/database/schema.ts +95 -0
  17. package/site/drizzle.config.ts +7 -0
  18. package/site/migrations/0001_auth.sql +14 -0
  19. package/site/migrations/0002_site_access.sql +5 -0
  20. package/site/migrations/0003_github_username.sql +1 -0
  21. package/site/migrations/meta/0002_snapshot.json +432 -0
  22. package/site/migrations/meta/0003_snapshot.json +439 -0
  23. package/site/migrations/meta/_journal.json +27 -0
  24. package/site/package.json +120 -0
  25. package/site/src/app.d.ts +14 -0
  26. package/site/src/app.html +11 -0
  27. package/site/src/hooks.server.ts +12 -0
  28. package/site/src/lib/analytics-config.json +1 -0
  29. package/site/src/lib/analytics.ts +40 -0
  30. package/site/src/lib/auth-config.json +3 -0
  31. package/site/src/lib/components/SEO.svelte +113 -0
  32. package/site/src/lib/components/doc/AccessLink.svelte +27 -0
  33. package/site/src/lib/components/doc/Backlinks.svelte +59 -0
  34. package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
  35. package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
  36. package/site/src/lib/components/doc/DocPager.svelte +91 -0
  37. package/site/src/lib/components/doc/DocShell.svelte +115 -0
  38. package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
  39. package/site/src/lib/components/doc/LockedContent.svelte +29 -0
  40. package/site/src/lib/components/doc/NavTree.svelte +158 -0
  41. package/site/src/lib/components/doc/NoteActions.svelte +139 -0
  42. package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
  43. package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
  44. package/site/src/lib/components/doc/Sidebar.svelte +351 -0
  45. package/site/src/lib/components/doc/Toc.svelte +339 -0
  46. package/site/src/lib/components/doc/enhancements.ts +146 -0
  47. package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
  48. package/site/src/lib/components/doc/image-preview.ts +114 -0
  49. package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
  50. package/site/src/lib/components/embeds/mermaid.ts +353 -0
  51. package/site/src/lib/components/files/FileCode.svelte +81 -0
  52. package/site/src/lib/components/files/FileExplorer.svelte +74 -0
  53. package/site/src/lib/components/files/FileHistory.svelte +170 -0
  54. package/site/src/lib/components/files/FileIcon.svelte +28 -0
  55. package/site/src/lib/components/files/FileRmd.svelte +209 -0
  56. package/site/src/lib/components/files/FileTabs.svelte +180 -0
  57. package/site/src/lib/components/files/FileTree.svelte +67 -0
  58. package/site/src/lib/components/files/blame-gutter.ts +45 -0
  59. package/site/src/lib/components/files/code-view.ts +98 -0
  60. package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
  61. package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
  62. package/site/src/lib/components/graph/GraphView.svelte +100 -0
  63. package/site/src/lib/components/graph/graph-data.ts +137 -0
  64. package/site/src/lib/components/graph/graph-model.ts +103 -0
  65. package/site/src/lib/components/graph/index.ts +3 -0
  66. package/site/src/lib/components/graph/render-graph.ts +626 -0
  67. package/site/src/lib/components/popover/index.ts +1 -0
  68. package/site/src/lib/components/popover/link-popovers.ts +233 -0
  69. package/site/src/lib/components/popover/popover.css +82 -0
  70. package/site/src/lib/components/search/SearchButton.svelte +57 -0
  71. package/site/src/lib/components/search/SearchPalette.svelte +189 -0
  72. package/site/src/lib/components/search/index.ts +3 -0
  73. package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
  74. package/site/src/lib/components/search/pagefind-query.ts +22 -0
  75. package/site/src/lib/components/search/pagefind-url.ts +12 -0
  76. package/site/src/lib/components/search/search-state.svelte.ts +22 -0
  77. package/site/src/lib/components/ui/IconButton.svelte +30 -0
  78. package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
  79. package/site/src/lib/components/ui/index.ts +1 -0
  80. package/site/src/lib/config.ts +9 -0
  81. package/site/src/lib/content-license.ts +7 -0
  82. package/site/src/lib/file-history.ts +37 -0
  83. package/site/src/lib/file-icons.ts +27 -0
  84. package/site/src/lib/file-tabs.ts +72 -0
  85. package/site/src/lib/files.ts +73 -0
  86. package/site/src/lib/github-user.ts +70 -0
  87. package/site/src/lib/metadata.ts +166 -0
  88. package/site/src/lib/notebook-nav.ts +63 -0
  89. package/site/src/lib/publication.ts +1 -0
  90. package/site/src/lib/rmd.ts +156 -0
  91. package/site/src/lib/server/access-admin.ts +33 -0
  92. package/site/src/lib/server/content-model.ts +253 -0
  93. package/site/src/lib/server/content-provider.ts +266 -0
  94. package/site/src/lib/server/content.ts +277 -0
  95. package/site/src/lib/server/runtime-content.ts +34 -0
  96. package/site/src/lib/site-config.json +9 -0
  97. package/site/src/lib/social-image.ts +13 -0
  98. package/site/src/lib/styles/article.css +607 -0
  99. package/site/src/lib/styles/foundation.css +138 -0
  100. package/site/src/lib/styles/print.css +115 -0
  101. package/site/src/lib/styles/shell.css +182 -0
  102. package/site/src/lib/text-exports.ts +5 -0
  103. package/site/src/lib/types.ts +140 -0
  104. package/site/src/lib/utils.ts +6 -0
  105. package/site/src/routes/+error.svelte +16 -0
  106. package/site/src/routes/+layout.server.ts +10 -0
  107. package/site/src/routes/+layout.svelte +72 -0
  108. package/site/src/routes/+layout.ts +3 -0
  109. package/site/src/routes/404/+page.svelte +13 -0
  110. package/site/src/routes/[...slug]/+page.server.ts +24 -0
  111. package/site/src/routes/[...slug]/+page.svelte +180 -0
  112. package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
  113. package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
  114. package/site/src/routes/admin/access/+page.server.ts +53 -0
  115. package/site/src/routes/admin/access/+page.svelte +118 -0
  116. package/site/src/routes/graph.json/+server.ts +7 -0
  117. package/site/src/routes/index.xml/+server.ts +7 -0
  118. package/site/src/routes/layout.css +10 -0
  119. package/site/src/routes/robots.txt/+server.ts +9 -0
  120. package/site/src/routes/sitemap.xml/+server.ts +7 -0
  121. package/site/src/routes/tags/+page.server.ts +5 -0
  122. package/site/src/routes/tags/+page.svelte +38 -0
  123. package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
  124. package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
  125. package/site/static/favicon.png +0 -0
  126. package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
  127. package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
  128. package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
  129. package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
  130. package/site/static/fonts/Virgil.woff2 +0 -0
  131. package/site/svelte.config.js +71 -0
  132. package/site/tooling/LICENSE-quartz +25 -0
  133. package/site/tooling/adapter.js +55 -0
  134. package/site/tooling/assets.ts +323 -0
  135. package/site/tooling/benchmark.ts +191 -0
  136. package/site/tooling/build.ts +407 -0
  137. package/site/tooling/cache.ts +133 -0
  138. package/site/tooling/check-diagrams.ts +164 -0
  139. package/site/tooling/check-links.ts +86 -0
  140. package/site/tooling/check-targets.ts +139 -0
  141. package/site/tooling/compiler.ts +1386 -0
  142. package/site/tooling/deploy.ts +112 -0
  143. package/site/tooling/fingerprint.ts +81 -0
  144. package/site/tooling/lib/app-version.js +92 -0
  145. package/site/tooling/lib/application-asset-urls.ts +27 -0
  146. package/site/tooling/lib/application-cache.ts +269 -0
  147. package/site/tooling/lib/asset-hash-cache.ts +116 -0
  148. package/site/tooling/lib/autotag.ts +99 -0
  149. package/site/tooling/lib/build-assembly.ts +148 -0
  150. package/site/tooling/lib/build-workspace.ts +386 -0
  151. package/site/tooling/lib/cache-selection.ts +70 -0
  152. package/site/tooling/lib/cloudflare-prewarm.js +40 -0
  153. package/site/tooling/lib/comments.ts +32 -0
  154. package/site/tooling/lib/content-cache.ts +244 -0
  155. package/site/tooling/lib/content-urls.ts +50 -0
  156. package/site/tooling/lib/course-cache.ts +298 -0
  157. package/site/tooling/lib/course-files.ts +281 -0
  158. package/site/tooling/lib/currency.ts +33 -0
  159. package/site/tooling/lib/deployment-config.ts +222 -0
  160. package/site/tooling/lib/deployment-manifest.ts +274 -0
  161. package/site/tooling/lib/deployment-prepare.ts +325 -0
  162. package/site/tooling/lib/deployment-upload.ts +293 -0
  163. package/site/tooling/lib/dev-git-state.ts +130 -0
  164. package/site/tooling/lib/dev-history.ts +86 -0
  165. package/site/tooling/lib/dev-social-images.ts +38 -0
  166. package/site/tooling/lib/dev-state.ts +107 -0
  167. package/site/tooling/lib/edition-builds.ts +184 -0
  168. package/site/tooling/lib/edition-paths.js +29 -0
  169. package/site/tooling/lib/empty-tag-route.js +23 -0
  170. package/site/tooling/lib/file-history.ts +201 -0
  171. package/site/tooling/lib/file-icons.ts +93 -0
  172. package/site/tooling/lib/file-policy.ts +38 -0
  173. package/site/tooling/lib/file-search.js +13 -0
  174. package/site/tooling/lib/git-revisions.ts +41 -0
  175. package/site/tooling/lib/history-pool.ts +74 -0
  176. package/site/tooling/lib/history-worker.ts +22 -0
  177. package/site/tooling/lib/knit-rmd.R +38 -0
  178. package/site/tooling/lib/lastmod.ts +201 -0
  179. package/site/tooling/lib/markdown-files.ts +51 -0
  180. package/site/tooling/lib/ofm.ts +634 -0
  181. package/site/tooling/lib/output.ts +60 -0
  182. package/site/tooling/lib/product-cache.ts +1006 -0
  183. package/site/tooling/lib/project-config.ts +288 -0
  184. package/site/tooling/lib/public-assets.ts +38 -0
  185. package/site/tooling/lib/public-links.ts +56 -0
  186. package/site/tooling/lib/publishing.ts +156 -0
  187. package/site/tooling/lib/r-runtime.ts +257 -0
  188. package/site/tooling/lib/rebuild-queue.ts +25 -0
  189. package/site/tooling/lib/rmd-preflight.ts +166 -0
  190. package/site/tooling/lib/rmd-preview-cache.ts +93 -0
  191. package/site/tooling/lib/rmd-previews.ts +129 -0
  192. package/site/tooling/lib/runtime-data-modules.ts +30 -0
  193. package/site/tooling/lib/runtime-packaging.ts +321 -0
  194. package/site/tooling/lib/search-cache.js +165 -0
  195. package/site/tooling/lib/search-inputs.js +129 -0
  196. package/site/tooling/lib/slug.ts +377 -0
  197. package/site/tooling/lib/social-images.ts +210 -0
  198. package/site/tooling/lib/text-exports.ts +51 -0
  199. package/site/tooling/lib/visit-elements.ts +17 -0
  200. package/site/tooling/live-test.ts +138 -0
  201. package/site/tooling/pipeline.ts +103 -0
  202. package/site/tooling/prepare.ts +3 -0
  203. package/site/tooling/publish.ts +28 -0
  204. package/site/tooling/r-cache.ts +45 -0
  205. package/site/tooling/r-render-profile.json +7 -0
  206. package/site/tooling/search.js +94 -0
  207. package/site/tooling/verify-deployment.ts +109 -0
  208. package/site/tooling/vite-content.ts +240 -0
  209. package/site/tooling/wrangler-paths.js +25 -0
  210. package/site/tooling/wrangler.ts +19 -0
  211. package/site/tsconfig.json +16 -0
  212. package/site/vite.config.ts +40 -0
  213. package/site/worker/access.ts +57 -0
  214. package/site/worker/analytics.ts +100 -0
  215. package/site/worker/auth-routes.ts +78 -0
  216. package/site/worker/auth.ts +78 -0
  217. package/site/worker/content-runtime.ts +226 -0
  218. package/site/worker/framing.ts +4 -0
  219. package/site/worker/gate.ts +147 -0
  220. package/site/worker/index.ts +9 -0
  221. package/site/worker/login.ts +62 -0
  222. package/site/worker/publication.ts +44 -0
  223. package/site/worker/runtime.ts +104 -0
  224. package/site/worker/tsconfig.json +13 -0
  225. package/site/wrangler.jsonc +37 -0
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import type { NavNode } from '$lib/types';
3
+ import Self from './NavTree.svelte';
4
+ import { page } from '$app/state';
5
+ import { slide } from 'svelte/transition';
6
+ import { prefersReducedMotion } from 'svelte/motion';
7
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
8
+ import LockKeyhole from '@lucide/svelte/icons/lock-keyhole';
9
+ let {
10
+ nodes,
11
+ depth = 0,
12
+ expanded = $bindable({})
13
+ }: {
14
+ nodes: NavNode[];
15
+ depth?: number;
16
+ expanded?: Record<string, boolean>;
17
+ } = $props();
18
+ const uid = $props.id();
19
+ const currentPath = $derived(decodeURI(page.url.pathname).replace(/\/$/, '') || '/');
20
+ function key(node: NavNode) {
21
+ return node.route ?? `${depth}-${node.segment}`;
22
+ }
23
+ function active(node: NavNode): boolean {
24
+ return node.route === currentPath;
25
+ }
26
+ function containsActive(node: NavNode): boolean {
27
+ return active(node) || node.children.some(containsActive);
28
+ }
29
+ function open(node: NavNode) {
30
+ return expanded[key(node)] ?? containsActive(node);
31
+ }
32
+ </script>
33
+
34
+ <ul class:nested={depth > 0}>
35
+ {#each nodes as node (node.segment)}
36
+ <li>
37
+ <div class="row" class:active={active(node)}>
38
+ {#if node.children.length}
39
+ <button
40
+ aria-label={`Toggle ${node.title}`}
41
+ aria-expanded={open(node)}
42
+ aria-controls={`${uid}-${node.segment}`}
43
+ onclick={() => (expanded = { ...expanded, [key(node)]: !open(node) })}
44
+ >
45
+ <ChevronRight size={13} class={open(node) ? 'expanded' : ''} />
46
+ </button>
47
+ {:else}<span class="spacer"></span>{/if}
48
+ {#if node.route}
49
+ <a
50
+ href={node.route}
51
+ title={node.title}
52
+ aria-current={active(node) ? 'page' : undefined}
53
+ class:section={node.children.length > 0}>{node.title}</a
54
+ >
55
+ {:else}<span class="label">{node.title}</span>{/if}
56
+ {#if node.locked}<LockKeyhole
57
+ class="mt-2 mr-1 ml-auto size-3 shrink-0 text-muted"
58
+ aria-label="Locked"
59
+ />{/if}
60
+ </div>
61
+ <div id={`${uid}-${node.segment}`}>
62
+ {#if node.children.length && open(node)}
63
+ <div transition:slide={{ duration: prefersReducedMotion.current ? 0 : 150 }}>
64
+ <Self nodes={node.children} depth={depth + 1} bind:expanded />
65
+ </div>
66
+ {/if}
67
+ </div>
68
+ </li>
69
+ {/each}
70
+ </ul>
71
+
72
+ <style>
73
+ ul {
74
+ list-style: none;
75
+ padding: 0;
76
+ margin: 0;
77
+ }
78
+ ul.nested {
79
+ margin-left: 0.5rem;
80
+ padding-left: 0.375rem;
81
+ border-left: 1px solid var(--color-border);
82
+ }
83
+ .row {
84
+ display: flex;
85
+ align-items: flex-start;
86
+ position: relative;
87
+ border-radius: 0.25rem;
88
+ }
89
+ .row.active {
90
+ background: var(--color-accent-soft, #f7e4e1);
91
+ }
92
+ .row.active::before {
93
+ content: '';
94
+ position: absolute;
95
+ inset: 3px auto 3px 0;
96
+ width: 2px;
97
+ border-radius: 2px;
98
+ background: var(--color-accent);
99
+ }
100
+ button,
101
+ .spacer {
102
+ flex: 0 0 1.125rem;
103
+ width: 1.125rem;
104
+ height: 1.875rem;
105
+ display: grid;
106
+ place-items: center;
107
+ }
108
+ button {
109
+ cursor: pointer;
110
+ color: var(--color-muted);
111
+ }
112
+ button :global(svg) {
113
+ transition: transform 150ms;
114
+ }
115
+ button :global(.expanded) {
116
+ transform: rotate(90deg);
117
+ }
118
+ a,
119
+ .label {
120
+ padding: 0.375rem 0.25rem;
121
+ font-size: 0.8125rem;
122
+ line-height: 1.4;
123
+ color: var(--color-muted);
124
+ text-decoration: none;
125
+ overflow-wrap: anywhere;
126
+ min-width: 0;
127
+ flex: 1;
128
+ }
129
+ a {
130
+ display: -webkit-box;
131
+ -webkit-line-clamp: 2;
132
+ line-clamp: 2;
133
+ -webkit-box-orient: vertical;
134
+ overflow: hidden;
135
+ }
136
+ a.section {
137
+ font-weight: 500;
138
+ color: var(--color-text);
139
+ }
140
+ a[aria-current] {
141
+ color: var(--color-accent);
142
+ font-weight: 500;
143
+ }
144
+ .row:hover {
145
+ background: var(--color-surface);
146
+ }
147
+ a:focus-visible,
148
+ button:focus-visible {
149
+ outline: 2px solid var(--color-accent);
150
+ outline-offset: -1px;
151
+ border-radius: 3px;
152
+ }
153
+ @media (prefers-reduced-motion: reduce) {
154
+ button :global(svg) {
155
+ transition: none;
156
+ }
157
+ }
158
+ </style>
@@ -0,0 +1,139 @@
1
+ <script lang="ts">
2
+ import Copy from '@lucide/svelte/icons/copy';
3
+ import FileText from '@lucide/svelte/icons/file-text';
4
+ import ChevronDown from '@lucide/svelte/icons/chevron-down';
5
+ import Printer from '@lucide/svelte/icons/printer';
6
+ import { Popover } from 'bits-ui';
7
+ import { textExportUrl } from '$lib/text-exports';
8
+ import { prepareDiagramsForPrint } from '$lib/components/embeds/mermaid';
9
+
10
+ let { slug }: { slug: string } = $props();
11
+ let open = $state(false);
12
+ let status = $state('');
13
+ let busy = $state(false);
14
+ let markdown = $state<string>();
15
+ let loading = $state(false);
16
+ let printing = $state(false);
17
+
18
+ async function printNote(event: MouseEvent) {
19
+ const article = (event.currentTarget as HTMLElement).closest('article');
20
+ if (!article || printing) return;
21
+ printing = true;
22
+ try {
23
+ await Promise.all([
24
+ document.fonts.ready,
25
+ prepareDiagramsForPrint(article),
26
+ ...Array.from(article.querySelectorAll('img'), async (image) => {
27
+ image.loading = 'eager';
28
+ await image.decode().catch(() => {});
29
+ })
30
+ ]);
31
+ if (article.isConnected) window.print();
32
+ } finally {
33
+ printing = false;
34
+ }
35
+ }
36
+ const actionClass =
37
+ 'flex w-full items-center gap-2 rounded px-2.5 py-2 text-left text-sm text-text no-underline hover:bg-surface focus-visible:outline-2 focus-visible:outline-accent disabled:opacity-50';
38
+
39
+ // Fetch while the menu is opening so clipboard writes start directly from
40
+ // the click, without a network await consuming browser user activation.
41
+ async function prepareMarkdown() {
42
+ if (markdown !== undefined || loading) return;
43
+ loading = true;
44
+ try {
45
+ const response = await fetch(textExportUrl(slug));
46
+ if (
47
+ !response.ok ||
48
+ response.redirected ||
49
+ response.headers.get('content-type')?.includes('text/html')
50
+ )
51
+ throw new Error('Text export unavailable');
52
+ markdown = await response.text();
53
+ status = '';
54
+ } catch {
55
+ status = 'Could not load Markdown. Use the text links below.';
56
+ } finally {
57
+ loading = false;
58
+ }
59
+ }
60
+
61
+ async function copy() {
62
+ if (markdown === undefined || busy) return;
63
+ busy = true;
64
+ status = '';
65
+ try {
66
+ await navigator.clipboard.writeText(markdown);
67
+ status = 'Markdown copied.';
68
+ } catch {
69
+ status = 'Could not copy. Open Markdown below to copy manually.';
70
+ } finally {
71
+ busy = false;
72
+ }
73
+ }
74
+ </script>
75
+
76
+ <div class="note-actions not-prose mb-5 flex flex-wrap items-center gap-2" data-pagefind-ignore>
77
+ <button
78
+ type="button"
79
+ class="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs text-muted hover:bg-surface hover:text-text focus-visible:outline-2 focus-visible:outline-accent"
80
+ title="Print this note or save it as a PDF"
81
+ disabled={printing}
82
+ onclick={printNote}
83
+ >
84
+ <Printer class="size-3.5" aria-hidden="true" />{printing ? 'Preparing…' : 'Print / PDF'}
85
+ </button>
86
+ <Popover.Root
87
+ bind:open
88
+ onOpenChange={(value) => {
89
+ if (value) void prepareMarkdown();
90
+ }}
91
+ >
92
+ <Popover.Trigger
93
+ openOnHover
94
+ openDelay={150}
95
+ closeDelay={250}
96
+ class="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs text-muted hover:bg-surface hover:text-text focus-visible:outline-2 focus-visible:outline-accent"
97
+ >
98
+ <Copy class="size-3.5" aria-hidden="true" />Use this note<ChevronDown
99
+ class="size-3"
100
+ aria-hidden="true"
101
+ />
102
+ </Popover.Trigger>
103
+ <Popover.Portal>
104
+ <Popover.Content
105
+ role="dialog"
106
+ aria-label="Note actions"
107
+ side="bottom"
108
+ align="start"
109
+ sideOffset={4}
110
+ collisionPadding={12}
111
+ class="not-prose z-50 w-64 max-w-[calc(100vw-1.5rem)] rounded-lg border border-border bg-bg p-1 text-text shadow-md"
112
+ >
113
+ <button class={actionClass} disabled={busy || markdown === undefined} onclick={() => copy()}
114
+ ><Copy class="size-4" aria-hidden="true" />Copy Markdown</button
115
+ >
116
+ <div class="my-1 border-t border-border"></div>
117
+ <a
118
+ class={actionClass}
119
+ href={textExportUrl(slug)}
120
+ data-sveltekit-reload
121
+ data-no-popover="true"
122
+ ><FileText class="size-4" aria-hidden="true" />View Markdown
123
+ <span class="ml-auto text-xs text-muted">.md</span></a
124
+ >
125
+ <a
126
+ class={actionClass}
127
+ href={textExportUrl(slug, 'txt')}
128
+ data-sveltekit-reload
129
+ data-no-popover="true"
130
+ ><FileText class="size-4" aria-hidden="true" />View text
131
+ <span class="ml-auto text-xs text-muted">.txt</span></a
132
+ >
133
+ <p class="m-0 px-2.5 text-xs text-muted" role="status">
134
+ {loading ? 'Loading Markdown…' : status}
135
+ </p>
136
+ </Popover.Content>
137
+ </Popover.Portal>
138
+ </Popover.Root>
139
+ </div>
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import Globe from '@lucide/svelte/icons/globe';
3
+ import Lock from '@lucide/svelte/icons/lock';
4
+ import { Tooltip } from 'bits-ui';
5
+ import { onMount } from 'svelte';
6
+ import type { PagePublication } from '$lib/types';
7
+
8
+ const { publication }: { publication: PagePublication } = $props();
9
+ let ready = $state(false);
10
+ onMount(() => {
11
+ ready = true;
12
+ });
13
+ const label = $derived(publication.public ? 'Public' : 'Private');
14
+ const description = $derived(
15
+ publication.reason ??
16
+ (publication.public ? 'Available to everyone.' : 'Only available to approved users.')
17
+ );
18
+ </script>
19
+
20
+ <Tooltip.Provider delayDuration={200}>
21
+ <Tooltip.Root>
22
+ <Tooltip.Trigger
23
+ class={[
24
+ 'inline-flex cursor-help items-center gap-1 rounded-full border-0 bg-surface px-2 py-0.5 text-[0.6875rem] leading-4 font-medium ring-1 ring-inset ring-border focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
25
+ publication.public && 'text-accent'
26
+ ]}
27
+ aria-label={`Page visibility: ${label}`}
28
+ disabled={!ready}
29
+ >
30
+ {#if publication.public}<Globe size={12} aria-hidden="true" />{:else}<Lock
31
+ size={12}
32
+ aria-hidden="true"
33
+ />{/if}
34
+ {label}
35
+ </Tooltip.Trigger>
36
+ <Tooltip.Portal>
37
+ <Tooltip.Content
38
+ role="tooltip"
39
+ side="bottom"
40
+ align="end"
41
+ sideOffset={6}
42
+ collisionPadding={12}
43
+ class="z-50 max-w-[min(22rem,calc(100vw-1.5rem))] rounded-md border border-border bg-surface px-3 py-2 text-xs break-words text-text shadow-sm"
44
+ >
45
+ {description}
46
+ </Tooltip.Content>
47
+ </Tooltip.Portal>
48
+ </Tooltip.Root>
49
+ </Tooltip.Provider>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import BookOpen from '@lucide/svelte/icons/book-open';
3
+ import IconButton from '../ui/IconButton.svelte';
4
+ import type { ClassValue } from 'svelte/elements';
5
+ import { readerMode } from './reader-mode.svelte';
6
+
7
+ let { class: className }: { class?: ClassValue } = $props();
8
+ </script>
9
+
10
+ <IconButton
11
+ onclick={() => readerMode.toggle()}
12
+ aria-label="Toggle reader mode"
13
+ aria-pressed={readerMode.enabled}
14
+ title="Toggle reader mode"
15
+ active={readerMode.enabled}
16
+ class={className}
17
+ >
18
+ <BookOpen class="size-4" />
19
+ </IconButton>