@mvarble/mesearch-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/dist/ambient.d.ts +2 -0
- package/dist/components/Article.svelte +226 -0
- package/dist/components/Article.svelte.d.ts +16 -0
- package/dist/components/ArticleHeader.svelte +134 -0
- package/dist/components/ArticleHeader.svelte.d.ts +19 -0
- package/dist/components/Chips.svelte +22 -0
- package/dist/components/Chips.svelte.d.ts +8 -0
- package/dist/components/Graph.svelte +727 -0
- package/dist/components/Graph.svelte.d.ts +16 -0
- package/dist/components/GraphPanel.svelte +328 -0
- package/dist/components/GraphPanel.svelte.d.ts +17 -0
- package/dist/components/Icon.svelte +67 -0
- package/dist/components/Icon.svelte.d.ts +8 -0
- package/dist/components/Index.svelte +283 -0
- package/dist/components/Index.svelte.d.ts +9 -0
- package/dist/components/Pager.svelte +71 -0
- package/dist/components/Pager.svelte.d.ts +12 -0
- package/dist/components/Palette.svelte +333 -0
- package/dist/components/Palette.svelte.d.ts +9 -0
- package/dist/components/Parts.svelte +62 -0
- package/dist/components/Parts.svelte.d.ts +11 -0
- package/dist/components/Proof.svelte +39 -0
- package/dist/components/Proof.svelte.d.ts +8 -0
- package/dist/components/Rail.svelte +186 -0
- package/dist/components/Rail.svelte.d.ts +13 -0
- package/dist/components/References.svelte +94 -0
- package/dist/components/References.svelte.d.ts +8 -0
- package/dist/components/SequenceTrack.svelte +186 -0
- package/dist/components/SequenceTrack.svelte.d.ts +11 -0
- package/dist/components/Shell.svelte +82 -0
- package/dist/components/Shell.svelte.d.ts +14 -0
- package/dist/components/Statement.svelte +105 -0
- package/dist/components/Statement.svelte.d.ts +11 -0
- package/dist/components/ThemeToggle.svelte +19 -0
- package/dist/components/ThemeToggle.svelte.d.ts +3 -0
- package/dist/components/Toc.svelte +145 -0
- package/dist/components/Toc.svelte.d.ts +8 -0
- package/dist/fonts.d.ts +4 -0
- package/dist/fonts.js +6 -0
- package/dist/fuzzy.d.ts +12 -0
- package/dist/fuzzy.js +57 -0
- package/dist/graph-layout.d.ts +35 -0
- package/dist/graph-layout.js +98 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +30 -0
- package/dist/reading.svelte.d.ts +3 -0
- package/dist/reading.svelte.js +4 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +115 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +28 -0
- package/dist/styles/base.css +136 -0
- package/dist/styles/index.css +6 -0
- package/dist/styles/prose.css +203 -0
- package/dist/styles/tokens.css +111 -0
- package/dist/theme.svelte.d.ts +12 -0
- package/dist/theme.svelte.js +66 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +3 -0
- package/package.json +66 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Varble
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mvarble/mesearch-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Svelte 5 components and stylesheets that [mesearch](https://github.com/mvarble/mesearch) sites are made of, published so that another SvelteKit site can look and behave the same. [The blog](https://github.com/mvarble/blog) uses it this way.
|
|
4
|
+
|
|
5
|
+
You get:
|
|
6
|
+
|
|
7
|
+
- the rail (a dock on phones) with its search palette and theme toggle;
|
|
8
|
+
- the force-laid-out map of documents and its detail panel;
|
|
9
|
+
- the fuzzy index;
|
|
10
|
+
- a three-column article layout with a scroll-spied table of contents and a sequence track;
|
|
11
|
+
- the light and dark tokens behind all of it.
|
|
12
|
+
|
|
13
|
+
Nothing here knows how a site stores content or lays out its URLs. Every component is handed its data, with each link's `url` already worked out.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add @mvarble/mesearch-ui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setting up a site
|
|
20
|
+
|
|
21
|
+
**Root layout.** Load the styles, then wrap the pages in `Shell`:
|
|
22
|
+
|
|
23
|
+
```svelte
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import '@mvarble/mesearch-ui/fonts'; // the default typefaces; optional
|
|
26
|
+
import 'katex/dist/katex.min.css';
|
|
27
|
+
import '@mvarble/mesearch-markdown/styles/katex.css';
|
|
28
|
+
import '@mvarble/mesearch-ui/styles/index.css';
|
|
29
|
+
import { goto } from '$app/navigation';
|
|
30
|
+
import { configure, Shell } from '@mvarble/mesearch-ui';
|
|
31
|
+
|
|
32
|
+
let { children } = $props();
|
|
33
|
+
configure({ lang: 'en', kinds: { post: 'Post' } });
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<Shell
|
|
37
|
+
title="My site"
|
|
38
|
+
home="/"
|
|
39
|
+
links={[{ url: '/posts/', label: 'Posts', icon: 'pen' }]}
|
|
40
|
+
search="/search.json"
|
|
41
|
+
navigate={goto}
|
|
42
|
+
>
|
|
43
|
+
{@render children()}
|
|
44
|
+
</Shell>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Theme script.** Paste `THEME_SCRIPT` into a `<script>` in `app.html`'s `<head>`, and give `<html>` the attribute `data-theme="light"`.
|
|
48
|
+
|
|
49
|
+
- It applies the reader's stored choice, or the system's, before anything paints.
|
|
50
|
+
- It adds the `js` class that the controls key on.
|
|
51
|
+
- A page read without JavaScript stays light.
|
|
52
|
+
|
|
53
|
+
**Search.** `search` points at a JSON array of `SearchEntry`, usually a prerendered endpoint. The array is fetched the first time the palette opens. You can pass a function returning the entries instead.
|
|
54
|
+
|
|
55
|
+
## Components
|
|
56
|
+
|
|
57
|
+
| component | what it is |
|
|
58
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
59
|
+
| `Shell` | Rail, palette, skip link, and the margin the rail needs. Starts the theme. A `logo` snippet replaces the title's initials on the rail. |
|
|
60
|
+
| `Article` | A document page. The header, the prose, then `after` and a `footer`. `left` and `right` snippets sit in the margins on wide screens and fold into disclosures under the header otherwise. |
|
|
61
|
+
| `ArticleHeader` | Breadcrumbs, title, dates, reading time, and "Builds on" chips. |
|
|
62
|
+
| `Toc` | Nested contents with scroll-spy. |
|
|
63
|
+
| `SequenceTrack` | A sequence as a line of stations, with nested stops for sections and the current stop filled. |
|
|
64
|
+
| `Pager`, `Parts`, `Chips` | Previous and next, a numbered list of parts, and a row of document links. |
|
|
65
|
+
| `Statement`, `Proof` | A theorem, definition or remark spread in from its own document, with its heading running into its first line and claims set in italics; and a proof ending in ∎. |
|
|
66
|
+
| `References` | A page's own short reference list, at its end. Each entry is the target of the citations to it and is highlighted on arrival. |
|
|
67
|
+
| `Graph` | The map, with nodes laid out in advance by `layoutGraph`. It is drawn without JavaScript and simulated live with it. Clicking a node opens `GraphPanel` (a drawer, or a bottom sheet on phones). |
|
|
68
|
+
| `Index` | Every entry, with fuzzy filtering, sorting by date or title, and kind filters. |
|
|
69
|
+
| `Rail`, `Palette`, `ThemeToggle`, `Icon` | The pieces `Shell` is made of, if you want to assemble them differently. |
|
|
70
|
+
|
|
71
|
+
## Server helpers
|
|
72
|
+
|
|
73
|
+
Import these from `@mvarble/mesearch-ui/server` and run them in a `load` function. The page then ships finished HTML instead of KaTeX and d3.
|
|
74
|
+
|
|
75
|
+
- `inlineHtml(text, macros?)`: a title or summary as HTML, with its `$...$` math rendered over the base macros.
|
|
76
|
+
- `layoutGraph(documents, links, tuning?)`: runs the force simulation to rest and returns the nodes for `Graph`.
|
|
77
|
+
- `referenceHtml(entry, macros?)`: a bibliography entry as one short paragraph of HTML, for `References`.
|
|
78
|
+
|
|
79
|
+
## Styling
|
|
80
|
+
|
|
81
|
+
Everything is drawn from CSS variables defined in `styles/tokens.css`: fonts, the paper and ink colours for both themes, the accent, the colour of each kind, sizes, and radii.
|
|
82
|
+
|
|
83
|
+
- To restyle a site, override any of them after the stylesheet loads. Override dark values under `:root[data-theme='dark']`.
|
|
84
|
+
- `styles/index.css` is the tokens, `base.css` and `prose.css` together. Import them separately to leave one out.
|
|
85
|
+
- A kind of document gets its colour from a `.kind-<name>` class. Any kind without one of its own takes the accent colour.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, type Snippet } from 'svelte';
|
|
3
|
+
import { reading } from '../reading.svelte.js';
|
|
4
|
+
|
|
5
|
+
// A document's page: the article in a column of comfortable measure, with
|
|
6
|
+
// a sidebar on either side when the screen is wide enough. On narrower
|
|
7
|
+
// screens the sidebars fold into disclosures under the header. Scrolling
|
|
8
|
+
// through the article moves the rail's progress line.
|
|
9
|
+
//
|
|
10
|
+
// A sidebar snippet is rendered twice, once in the margin and once folded,
|
|
11
|
+
// and is told which it is.
|
|
12
|
+
let {
|
|
13
|
+
header,
|
|
14
|
+
children,
|
|
15
|
+
after,
|
|
16
|
+
footer,
|
|
17
|
+
left,
|
|
18
|
+
right,
|
|
19
|
+
leftLabel = 'Sequence',
|
|
20
|
+
leftNote,
|
|
21
|
+
rightLabel = 'Contents',
|
|
22
|
+
kind,
|
|
23
|
+
}: {
|
|
24
|
+
header: Snippet;
|
|
25
|
+
// The document itself, styled as prose.
|
|
26
|
+
children: Snippet;
|
|
27
|
+
// Anything between the document and the footer, not styled as prose.
|
|
28
|
+
after?: Snippet;
|
|
29
|
+
footer?: Snippet;
|
|
30
|
+
left?: Snippet<[folded: boolean]>;
|
|
31
|
+
right?: Snippet<[folded: boolean]>;
|
|
32
|
+
// What a folded sidebar's disclosure says, and a quieter aside after
|
|
33
|
+
// it ("2 of 5").
|
|
34
|
+
leftLabel?: string;
|
|
35
|
+
leftNote?: string;
|
|
36
|
+
rightLabel?: string;
|
|
37
|
+
kind?: string;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
let article: HTMLElement | undefined = $state();
|
|
41
|
+
|
|
42
|
+
onMount(() => {
|
|
43
|
+
let frame = 0;
|
|
44
|
+
const update = () => {
|
|
45
|
+
frame = 0;
|
|
46
|
+
if (!article) return;
|
|
47
|
+
// Nothing read at the top of the page, everything once the end of
|
|
48
|
+
// the article is on screen.
|
|
49
|
+
const end = article.getBoundingClientRect().bottom + scrollY - innerHeight;
|
|
50
|
+
reading.progress = end <= 0 ? 1 : Math.min(1, Math.max(0, scrollY / end));
|
|
51
|
+
};
|
|
52
|
+
const schedule = () => (frame ||= requestAnimationFrame(update));
|
|
53
|
+
update();
|
|
54
|
+
addEventListener('scroll', schedule, { passive: true });
|
|
55
|
+
addEventListener('resize', schedule);
|
|
56
|
+
return () => {
|
|
57
|
+
removeEventListener('scroll', schedule);
|
|
58
|
+
removeEventListener('resize', schedule);
|
|
59
|
+
cancelAnimationFrame(frame);
|
|
60
|
+
reading.progress = null;
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<main id="main" class="page {kind ? `kind-${kind}` : ''}">
|
|
66
|
+
{#if left}
|
|
67
|
+
<aside class="left">
|
|
68
|
+
<div class="sticky">{@render left(false)}</div>
|
|
69
|
+
</aside>
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
<article bind:this={article}>
|
|
73
|
+
<div class="head">
|
|
74
|
+
{@render header()}
|
|
75
|
+
{#if left || right}
|
|
76
|
+
<div class="folded">
|
|
77
|
+
{#if left}
|
|
78
|
+
<details>
|
|
79
|
+
<summary>
|
|
80
|
+
{leftLabel}
|
|
81
|
+
{#if leftNote}<span class="note">{leftNote}</span>{/if}
|
|
82
|
+
</summary>
|
|
83
|
+
<div class="folded-body">{@render left(true)}</div>
|
|
84
|
+
</details>
|
|
85
|
+
{/if}
|
|
86
|
+
{#if right}
|
|
87
|
+
<details>
|
|
88
|
+
<summary>{rightLabel}</summary>
|
|
89
|
+
<div class="folded-body">{@render right(true)}</div>
|
|
90
|
+
</details>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="prose content">
|
|
97
|
+
{@render children()}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{@render after?.()}
|
|
101
|
+
|
|
102
|
+
{#if footer}
|
|
103
|
+
<footer class="foot">{@render footer()}</footer>
|
|
104
|
+
{/if}
|
|
105
|
+
</article>
|
|
106
|
+
|
|
107
|
+
{#if right}
|
|
108
|
+
<aside class="right">
|
|
109
|
+
<div class="sticky">{@render right(false)}</div>
|
|
110
|
+
</aside>
|
|
111
|
+
{/if}
|
|
112
|
+
</main>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
.page {
|
|
116
|
+
display: grid;
|
|
117
|
+
grid-template-columns: 1fr min(var(--measure), 100% - 2 * var(--gutter)) 1fr;
|
|
118
|
+
padding: clamp(2.25rem, 6vw, 4.5rem) 0 4rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
article {
|
|
122
|
+
grid-column: 2;
|
|
123
|
+
min-width: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.left,
|
|
127
|
+
.right {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sticky {
|
|
132
|
+
position: sticky;
|
|
133
|
+
top: 2.5rem;
|
|
134
|
+
max-height: calc(100vh - 5rem);
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 2rem;
|
|
139
|
+
scrollbar-width: thin;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.head {
|
|
143
|
+
margin-bottom: 2.5rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.folded {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
gap: 0.5rem;
|
|
150
|
+
margin-top: 1.5rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
details {
|
|
154
|
+
border: 1px solid var(--rule);
|
|
155
|
+
border-radius: var(--radius);
|
|
156
|
+
background: var(--paper-raised);
|
|
157
|
+
font-family: var(--font-ui);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
summary {
|
|
161
|
+
padding: 0.65rem 0.9rem;
|
|
162
|
+
font: 500 0.88rem / 1.3 var(--font-ui);
|
|
163
|
+
color: var(--ink-soft);
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.note {
|
|
168
|
+
color: var(--faint);
|
|
169
|
+
letter-spacing: 0.06em;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
details[open] summary {
|
|
173
|
+
border-bottom: 1px solid var(--rule);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.folded-body {
|
|
177
|
+
padding: 1rem 0.9rem 0.5rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.content {
|
|
181
|
+
padding-top: 0.25rem;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.foot {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
gap: 1.75rem;
|
|
188
|
+
margin-top: 3.5rem;
|
|
189
|
+
padding-top: 2rem;
|
|
190
|
+
border-top: 1px solid var(--rule);
|
|
191
|
+
font-family: var(--font-ui);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@media (min-width: 1280px) {
|
|
195
|
+
/* The sidebars keep their room, and a little air outside them: a
|
|
196
|
+
* measure wider than what is left between them narrows to fit. */
|
|
197
|
+
.page {
|
|
198
|
+
grid-template-columns:
|
|
199
|
+
minmax(calc(var(--sidebar-left) + 4.5rem), 1fr)
|
|
200
|
+
minmax(0, var(--measure))
|
|
201
|
+
minmax(calc(var(--sidebar-right) + 4.5rem), 1fr);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.left {
|
|
205
|
+
display: block;
|
|
206
|
+
grid-column: 1;
|
|
207
|
+
grid-row: 1;
|
|
208
|
+
justify-self: end;
|
|
209
|
+
width: var(--sidebar-left);
|
|
210
|
+
margin-right: 3rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.right {
|
|
214
|
+
display: block;
|
|
215
|
+
grid-column: 3;
|
|
216
|
+
grid-row: 1;
|
|
217
|
+
justify-self: start;
|
|
218
|
+
width: var(--sidebar-right);
|
|
219
|
+
margin-left: 3rem;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.folded {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
header: Snippet;
|
|
4
|
+
children: Snippet;
|
|
5
|
+
after?: Snippet;
|
|
6
|
+
footer?: Snippet;
|
|
7
|
+
left?: Snippet<[folded: boolean]>;
|
|
8
|
+
right?: Snippet<[folded: boolean]>;
|
|
9
|
+
leftLabel?: string;
|
|
10
|
+
leftNote?: string;
|
|
11
|
+
rightLabel?: string;
|
|
12
|
+
kind?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const Article: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type Article = ReturnType<typeof Article>;
|
|
16
|
+
export default Article;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { formatDate, isoDate } from '../settings.js';
|
|
4
|
+
import type { DocumentRef } from '../types.js';
|
|
5
|
+
import Chips from './Chips.svelte';
|
|
6
|
+
|
|
7
|
+
// A document's header: where it sits (`Site / Writeup · Sequence 2 of 3`),
|
|
8
|
+
// its title, when it was written and revised, and what it builds on.
|
|
9
|
+
let {
|
|
10
|
+
crumbs = [],
|
|
11
|
+
titleHtml,
|
|
12
|
+
created,
|
|
13
|
+
updated,
|
|
14
|
+
readingMinutes,
|
|
15
|
+
buildsOn = [],
|
|
16
|
+
buildsOnLabel = 'Builds on',
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
// The trail above the title. A crumb with a URL is a link; `note` is
|
|
20
|
+
// set beside it in a lighter colour.
|
|
21
|
+
crumbs?: Array<{ label: string; url?: string; note?: string }>;
|
|
22
|
+
titleHtml: string;
|
|
23
|
+
created?: Date;
|
|
24
|
+
updated?: Date;
|
|
25
|
+
readingMinutes?: number;
|
|
26
|
+
buildsOn?: DocumentRef[];
|
|
27
|
+
buildsOnLabel?: string;
|
|
28
|
+
// Anything else, under the dates: tags, say.
|
|
29
|
+
children?: Snippet;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let revised = $derived(
|
|
33
|
+
!!updated && (!created || isoDate(updated) != isoDate(created)) ? updated : undefined,
|
|
34
|
+
);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<header>
|
|
38
|
+
{#if crumbs.length}
|
|
39
|
+
<p class="eyebrow trail">
|
|
40
|
+
{#each crumbs as crumb, i (i)}
|
|
41
|
+
{#if i > 0}<span class="separator" aria-hidden="true">{i == 1 ? '/' : '·'}</span
|
|
42
|
+
>{/if}
|
|
43
|
+
{#if crumb.url}<a href={crumb.url}>{crumb.label}</a>{:else}<span class="here"
|
|
44
|
+
>{crumb.label}</span
|
|
45
|
+
>{/if}
|
|
46
|
+
{#if crumb.note}<span class="note">{crumb.note}</span>{/if}
|
|
47
|
+
{/each}
|
|
48
|
+
</p>
|
|
49
|
+
{/if}
|
|
50
|
+
<h1>{@html titleHtml}</h1>
|
|
51
|
+
{#if created || revised || readingMinutes}
|
|
52
|
+
<p class="meta">
|
|
53
|
+
{#if created}
|
|
54
|
+
<span>Written <time datetime={isoDate(created)}>{formatDate(created)}</time></span>
|
|
55
|
+
{/if}
|
|
56
|
+
{#if revised}
|
|
57
|
+
<span>Revised <time datetime={isoDate(revised)}>{formatDate(revised)}</time></span>
|
|
58
|
+
{/if}
|
|
59
|
+
{#if readingMinutes}<span>{readingMinutes} min read</span>{/if}
|
|
60
|
+
</p>
|
|
61
|
+
{/if}
|
|
62
|
+
{@render children?.()}
|
|
63
|
+
{#if buildsOn.length}
|
|
64
|
+
<div class="builds-on">
|
|
65
|
+
<span class="label">{buildsOnLabel}</span>
|
|
66
|
+
<Chips refs={buildsOn} />
|
|
67
|
+
</div>
|
|
68
|
+
{/if}
|
|
69
|
+
</header>
|
|
70
|
+
|
|
71
|
+
<style>
|
|
72
|
+
.trail {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
align-items: baseline;
|
|
76
|
+
gap: 0.5em;
|
|
77
|
+
margin: 0 0 1.1rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.trail a {
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.trail a:hover {
|
|
86
|
+
color: var(--ink);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.here {
|
|
90
|
+
color: var(--kind, var(--accent));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.separator,
|
|
94
|
+
.note {
|
|
95
|
+
color: var(--faint);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.note {
|
|
99
|
+
letter-spacing: 0.06em;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
h1 {
|
|
103
|
+
margin: 0 0 1rem;
|
|
104
|
+
font: 600 var(--font-size-title) / 1.08 var(--font-heading);
|
|
105
|
+
letter-spacing: -0.02em;
|
|
106
|
+
text-wrap: balance;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.meta {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
gap: 0.35rem 1.25rem;
|
|
113
|
+
margin: 0;
|
|
114
|
+
font: 400 0.85rem / 1.5 var(--font-ui);
|
|
115
|
+
color: var(--muted);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.builds-on {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 0.5rem 0.75rem;
|
|
123
|
+
margin-top: 1.4rem;
|
|
124
|
+
padding-top: 1.1rem;
|
|
125
|
+
border-top: 1px solid var(--rule);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.label {
|
|
129
|
+
font: 600 var(--font-size-eyebrow) / 1 var(--font-ui);
|
|
130
|
+
letter-spacing: 0.12em;
|
|
131
|
+
text-transform: uppercase;
|
|
132
|
+
color: var(--muted);
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DocumentRef } from '../types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
crumbs?: Array<{
|
|
5
|
+
label: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
note?: string;
|
|
8
|
+
}>;
|
|
9
|
+
titleHtml: string;
|
|
10
|
+
created?: Date;
|
|
11
|
+
updated?: Date;
|
|
12
|
+
readingMinutes?: number;
|
|
13
|
+
buildsOn?: DocumentRef[];
|
|
14
|
+
buildsOnLabel?: string;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
};
|
|
17
|
+
declare const ArticleHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
18
|
+
type ArticleHeader = ReturnType<typeof ArticleHeader>;
|
|
19
|
+
export default ArticleHeader;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DocumentRef } from '../types.js';
|
|
3
|
+
|
|
4
|
+
// Documents mentioned in passing, as a row of small labelled links, under
|
|
5
|
+
// an optional heading.
|
|
6
|
+
let { refs, heading }: { refs: DocumentRef[]; heading?: string } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
{#if refs.length}
|
|
10
|
+
{#if heading}<h2 class="eyebrow">{heading}</h2>{/if}
|
|
11
|
+
<ul class="chips">
|
|
12
|
+
{#each refs as ref (ref.key)}
|
|
13
|
+
<li><a class="chip kind-{ref.kind}" href={ref.url}>{ref.title}</a></li>
|
|
14
|
+
{/each}
|
|
15
|
+
</ul>
|
|
16
|
+
{/if}
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
h2 {
|
|
20
|
+
margin: 0 0 0.6rem;
|
|
21
|
+
}
|
|
22
|
+
</style>
|