@sprig-and-prose/sprig-ui-csr 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/README.md +120 -0
- package/biome.json +37 -0
- package/index.html +12 -0
- package/manifest.json +10711 -0
- package/package.json +29 -0
- package/src/App.svelte +202 -0
- package/src/cli.js +146 -0
- package/src/lib/components/ContentsCard.svelte +167 -0
- package/src/lib/components/FooterStatus.svelte +80 -0
- package/src/lib/components/GlobalSearch.svelte +451 -0
- package/src/lib/components/PageHeader.svelte +116 -0
- package/src/lib/components/Prose.svelte +260 -0
- package/src/lib/components/UniverseHeader.svelte +20 -0
- package/src/lib/data/universeStore.js +252 -0
- package/src/lib/format/title.js +97 -0
- package/src/lib/references/isWildcardPath.js +9 -0
- package/src/lib/references/linkForPath.js +65 -0
- package/src/lib/references/linkForRepository.js +42 -0
- package/src/lib/router.js +75 -0
- package/src/lib/stores/describeRenderMode.js +9 -0
- package/src/lib/stores/theme.js +98 -0
- package/src/main.js +143 -0
- package/src/pages/AnthologyPage.svelte +84 -0
- package/src/pages/ConceptPage.svelte +873 -0
- package/src/pages/HomePage.svelte +80 -0
- package/src/pages/SeriesPage.svelte +657 -0
- package/src/server.js +115 -0
- package/src/styles/app.css +353 -0
- package/tsconfig.json +16 -0
- package/vite.config.js +10 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { universeGraph } from '../data/universeStore.js';
|
|
4
|
+
import { getNodeRoute, getAncestorChain } from '../data/universeStore.js';
|
|
5
|
+
import { getDisplayTitle } from '../format/title.js';
|
|
6
|
+
import { navigate } from '../router.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('../data/universeStore.js').UniverseGraph} UniverseGraph
|
|
10
|
+
* @typedef {import('../data/universeStore.js').NodeModel} NodeModel
|
|
11
|
+
* @typedef {{ node: NodeModel, name: string, kind: string, id: string, route: string, context: string | null }} SearchItem
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @type {HTMLInputElement | null} */
|
|
15
|
+
let searchInput = null;
|
|
16
|
+
let query = '';
|
|
17
|
+
let searchIndex = /** @type {SearchItem[]} */ ([]);
|
|
18
|
+
let activeIndex = /** @type {number | null} */ (null);
|
|
19
|
+
let listboxId = 'search-results-listbox';
|
|
20
|
+
/** @type {HTMLDivElement | null} */
|
|
21
|
+
let resultsDropdown = null;
|
|
22
|
+
let isManuallyClosed = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build context string for a node
|
|
26
|
+
* @param {UniverseGraph} graph
|
|
27
|
+
* @param {NodeModel} node
|
|
28
|
+
* @returns {string | null}
|
|
29
|
+
*/
|
|
30
|
+
function buildContext(graph, node) {
|
|
31
|
+
if (node.kind === 'series') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (node.kind === 'book') {
|
|
36
|
+
if (!node.parent) return null;
|
|
37
|
+
const series = graph.nodes[node.parent];
|
|
38
|
+
if (!series || series.kind !== 'series') return null;
|
|
39
|
+
return `Book in ${getDisplayTitle(series)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (node.kind === 'chapter') {
|
|
43
|
+
const ancestors = getAncestorChain(graph, node);
|
|
44
|
+
if (ancestors.length === 0) return null;
|
|
45
|
+
|
|
46
|
+
const book = ancestors.find(a => a.kind === 'book');
|
|
47
|
+
const series = ancestors.find(a => a.kind === 'series');
|
|
48
|
+
|
|
49
|
+
if (book && series) {
|
|
50
|
+
return `in ${getDisplayTitle(book)} (in ${getDisplayTitle(series)})`;
|
|
51
|
+
} else if (book) {
|
|
52
|
+
return `in ${getDisplayTitle(book)}`;
|
|
53
|
+
} else if (series) {
|
|
54
|
+
return `in ${getDisplayTitle(series)}`;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build search index from universe graph
|
|
64
|
+
* @param {UniverseGraph | null} graph
|
|
65
|
+
* @returns {SearchItem[]}
|
|
66
|
+
*/
|
|
67
|
+
function buildSearchIndex(graph) {
|
|
68
|
+
if (!graph || !graph.nodes) return [];
|
|
69
|
+
|
|
70
|
+
const items = [];
|
|
71
|
+
const searchableKinds = ['series', 'book', 'chapter'];
|
|
72
|
+
|
|
73
|
+
for (const node of Object.values(graph.nodes)) {
|
|
74
|
+
if (!node || !searchableKinds.includes(node.kind)) continue;
|
|
75
|
+
|
|
76
|
+
const route = getNodeRoute(node);
|
|
77
|
+
const context = buildContext(graph, node);
|
|
78
|
+
|
|
79
|
+
items.push({
|
|
80
|
+
node,
|
|
81
|
+
name: node.name,
|
|
82
|
+
kind: node.kind,
|
|
83
|
+
id: node.id,
|
|
84
|
+
route,
|
|
85
|
+
context,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return items;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Search nodes with ranking
|
|
94
|
+
* @param {SearchItem[]} index
|
|
95
|
+
* @param {string} searchQuery
|
|
96
|
+
* @returns {SearchItem[]}
|
|
97
|
+
*/
|
|
98
|
+
function searchNodes(index, searchQuery) {
|
|
99
|
+
if (!searchQuery.trim()) return [];
|
|
100
|
+
|
|
101
|
+
const queryLower = searchQuery.toLowerCase().trim();
|
|
102
|
+
const prefixMatches = [];
|
|
103
|
+
const substringMatches = [];
|
|
104
|
+
|
|
105
|
+
for (const item of index) {
|
|
106
|
+
const nameLower = item.name.toLowerCase();
|
|
107
|
+
|
|
108
|
+
if (nameLower.startsWith(queryLower)) {
|
|
109
|
+
prefixMatches.push(item);
|
|
110
|
+
} else if (nameLower.includes(queryLower)) {
|
|
111
|
+
substringMatches.push(item);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
prefixMatches.sort((a, b) => a.name.localeCompare(b.name));
|
|
116
|
+
substringMatches.sort((a, b) => a.name.localeCompare(b.name));
|
|
117
|
+
|
|
118
|
+
const results = [...prefixMatches, ...substringMatches];
|
|
119
|
+
return results.slice(0, 10);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build search index reactively when graph changes
|
|
123
|
+
$: {
|
|
124
|
+
if ($universeGraph) {
|
|
125
|
+
searchIndex = buildSearchIndex($universeGraph);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Perform search reactively
|
|
130
|
+
$: results = query.trim() ? searchNodes(searchIndex, query) : [];
|
|
131
|
+
|
|
132
|
+
// Reset active index and manual close flag when query or results change
|
|
133
|
+
$: {
|
|
134
|
+
activeIndex = null;
|
|
135
|
+
if (query.trim().length > 0) {
|
|
136
|
+
isManuallyClosed = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Compute isOpen: show dropdown if we have results, query is not empty, and not manually closed
|
|
141
|
+
$: isOpen = results.length > 0 && query.trim().length > 0 && !isManuallyClosed;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Scroll active item into view
|
|
145
|
+
*/
|
|
146
|
+
function scrollActiveIntoView() {
|
|
147
|
+
if (activeIndex === null || !resultsDropdown) return;
|
|
148
|
+
|
|
149
|
+
const optionId = `search-option-${activeIndex}`;
|
|
150
|
+
const optionElement = resultsDropdown.querySelector(`#${optionId}`);
|
|
151
|
+
if (optionElement) {
|
|
152
|
+
optionElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle keyboard navigation in search input
|
|
158
|
+
* @param {KeyboardEvent} event
|
|
159
|
+
*/
|
|
160
|
+
function handleInputKeydown(event) {
|
|
161
|
+
if (event.key === 'Tab') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (event.key === 'ArrowDown') {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
if (!isOpen || results.length === 0) return;
|
|
168
|
+
|
|
169
|
+
if (activeIndex === null) {
|
|
170
|
+
activeIndex = 0;
|
|
171
|
+
} else {
|
|
172
|
+
activeIndex = Math.min(activeIndex + 1, results.length - 1);
|
|
173
|
+
}
|
|
174
|
+
scrollActiveIntoView();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (event.key === 'ArrowUp') {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
if (!isOpen || results.length === 0) return;
|
|
181
|
+
|
|
182
|
+
if (activeIndex === null) {
|
|
183
|
+
activeIndex = results.length - 1;
|
|
184
|
+
} else {
|
|
185
|
+
activeIndex = Math.max(activeIndex - 1, 0);
|
|
186
|
+
}
|
|
187
|
+
scrollActiveIntoView();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (event.key === 'Enter' && isOpen && activeIndex !== null && results[activeIndex]) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
const item = results[activeIndex];
|
|
194
|
+
handleResultClick(item);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (event.key === 'Escape') {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
isManuallyClosed = true;
|
|
201
|
+
activeIndex = null;
|
|
202
|
+
searchInput?.focus();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle global keyboard shortcuts (for `/` to focus)
|
|
209
|
+
* @param {KeyboardEvent} event
|
|
210
|
+
*/
|
|
211
|
+
function handleGlobalKeydown(event) {
|
|
212
|
+
const activeElement = document.activeElement;
|
|
213
|
+
const isInputElement = activeElement && (
|
|
214
|
+
activeElement.tagName === 'INPUT' ||
|
|
215
|
+
activeElement.tagName === 'TEXTAREA' ||
|
|
216
|
+
(activeElement instanceof HTMLElement && activeElement.isContentEditable)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (event.key === '/' && !isInputElement) {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
searchInput?.focus();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Handle result click
|
|
227
|
+
* @param {SearchItem} item
|
|
228
|
+
*/
|
|
229
|
+
function handleResultClick(item) {
|
|
230
|
+
query = '';
|
|
231
|
+
isOpen = false;
|
|
232
|
+
activeIndex = null;
|
|
233
|
+
navigate(item.route);
|
|
234
|
+
// Trigger route change event
|
|
235
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Handle mouse hover on result item
|
|
240
|
+
* @param {number} index
|
|
241
|
+
*/
|
|
242
|
+
function handleResultHover(index) {
|
|
243
|
+
activeIndex = index;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
onMount(() => {
|
|
247
|
+
window.addEventListener('keydown', handleGlobalKeydown);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
onDestroy(() => {
|
|
251
|
+
if (typeof window !== 'undefined') {
|
|
252
|
+
window.removeEventListener('keydown', handleGlobalKeydown);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<div class="search-container">
|
|
258
|
+
<div class="search-wrapper">
|
|
259
|
+
<input
|
|
260
|
+
bind:this={searchInput}
|
|
261
|
+
type="text"
|
|
262
|
+
placeholder="Search Series, Books, Chapters… (Press / to focus)"
|
|
263
|
+
bind:value={query}
|
|
264
|
+
class="search-input"
|
|
265
|
+
role="combobox"
|
|
266
|
+
aria-label="Search"
|
|
267
|
+
aria-expanded={isOpen}
|
|
268
|
+
aria-controls={listboxId}
|
|
269
|
+
aria-activedescendant={activeIndex !== null ? `search-option-${activeIndex}` : undefined}
|
|
270
|
+
aria-autocomplete="list"
|
|
271
|
+
on:keydown={handleInputKeydown}
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
{#if isOpen && results.length > 0}
|
|
275
|
+
<div
|
|
276
|
+
bind:this={resultsDropdown}
|
|
277
|
+
class="results-dropdown"
|
|
278
|
+
role="listbox"
|
|
279
|
+
id={listboxId}
|
|
280
|
+
>
|
|
281
|
+
{#each results as item, index (item.id)}
|
|
282
|
+
{@const optionId = `search-option-${index}`}
|
|
283
|
+
{@const isActive = activeIndex === index}
|
|
284
|
+
<a
|
|
285
|
+
href={item.route}
|
|
286
|
+
class="result-item"
|
|
287
|
+
class:result-item--active={isActive}
|
|
288
|
+
role="option"
|
|
289
|
+
id={optionId}
|
|
290
|
+
aria-selected={isActive}
|
|
291
|
+
on:click|preventDefault={() => handleResultClick(item)}
|
|
292
|
+
on:mouseenter={() => handleResultHover(index)}
|
|
293
|
+
>
|
|
294
|
+
<div class="result-content">
|
|
295
|
+
<div class="result-title">{getDisplayTitle(item.node)}</div>
|
|
296
|
+
<div class="result-meta">
|
|
297
|
+
<span class="result-kind">{item.kind}</span>
|
|
298
|
+
{#if item.context}
|
|
299
|
+
<span class="result-context">{item.context}</span>
|
|
300
|
+
{/if}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</a>
|
|
304
|
+
{/each}
|
|
305
|
+
</div>
|
|
306
|
+
{/if}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<style>
|
|
311
|
+
.search-container {
|
|
312
|
+
display: flex;
|
|
313
|
+
flex: 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.search-wrapper {
|
|
317
|
+
position: relative;
|
|
318
|
+
max-width: 500px;
|
|
319
|
+
width: 100%;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.search-input {
|
|
323
|
+
width: 100%;
|
|
324
|
+
padding: 10px 14px;
|
|
325
|
+
font-family: var(--font-ui);
|
|
326
|
+
font-size: var(--sp-font-small);
|
|
327
|
+
color: var(--text-color);
|
|
328
|
+
background: var(--card-bg);
|
|
329
|
+
border: none;
|
|
330
|
+
border-radius: 4px;
|
|
331
|
+
outline: none;
|
|
332
|
+
transition: box-shadow 0.2s;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.search-input:focus {
|
|
336
|
+
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@media (prefers-color-scheme: dark) {
|
|
340
|
+
.search-input:focus {
|
|
341
|
+
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.05);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
:global(html[data-theme="dark"]) .search-input:focus {
|
|
346
|
+
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.05);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.search-input::placeholder {
|
|
350
|
+
color: var(--text-tertiary);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.results-dropdown {
|
|
354
|
+
position: absolute;
|
|
355
|
+
top: calc(100% + 4px);
|
|
356
|
+
left: 0;
|
|
357
|
+
right: 0;
|
|
358
|
+
background: var(--bg-color);
|
|
359
|
+
border: 1px solid var(--border-color);
|
|
360
|
+
border-radius: 4px;
|
|
361
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
362
|
+
max-height: 400px;
|
|
363
|
+
overflow-y: auto;
|
|
364
|
+
z-index: 1000;
|
|
365
|
+
margin-top: 4px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@media (prefers-color-scheme: dark) {
|
|
369
|
+
.results-dropdown {
|
|
370
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
:global(html[data-theme="dark"]) .results-dropdown {
|
|
375
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.result-item {
|
|
379
|
+
display: block;
|
|
380
|
+
padding: 12px 14px;
|
|
381
|
+
text-decoration: none;
|
|
382
|
+
color: inherit;
|
|
383
|
+
border-bottom: 1px solid var(--hairline);
|
|
384
|
+
transition: background-color 0.15s;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.result-item:last-child {
|
|
389
|
+
border-bottom: none;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.result-item:hover,
|
|
393
|
+
.result-item:focus {
|
|
394
|
+
background: var(--hover);
|
|
395
|
+
outline: none;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.result-item:focus-visible {
|
|
399
|
+
outline: 2px solid var(--text-tertiary);
|
|
400
|
+
outline-offset: -2px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.result-item--active {
|
|
404
|
+
background: var(--hover);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.result-item--active .result-title {
|
|
408
|
+
text-decoration: underline;
|
|
409
|
+
text-decoration-thickness: 1px;
|
|
410
|
+
text-underline-offset: 2px;
|
|
411
|
+
text-decoration-color: var(--text-secondary);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.result-content {
|
|
415
|
+
display: flex;
|
|
416
|
+
flex-direction: column;
|
|
417
|
+
gap: 4px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.result-title {
|
|
421
|
+
font-family: var(--font-prose);
|
|
422
|
+
font-size: var(--sp-font-body);
|
|
423
|
+
color: var(--text-color);
|
|
424
|
+
font-weight: 400;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.result-meta {
|
|
428
|
+
display: flex;
|
|
429
|
+
align-items: center;
|
|
430
|
+
gap: 8px;
|
|
431
|
+
font-family: var(--font-ui);
|
|
432
|
+
font-size: var(--sp-font-tiny);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.result-kind {
|
|
436
|
+
color: var(--text-tertiary);
|
|
437
|
+
text-transform: capitalize;
|
|
438
|
+
font-weight: 400;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.result-context {
|
|
442
|
+
color: var(--text-secondary);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.result-context::before {
|
|
446
|
+
content: '·';
|
|
447
|
+
margin-right: 4px;
|
|
448
|
+
color: var(--text-tertiary);
|
|
449
|
+
}
|
|
450
|
+
</style>
|
|
451
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getDisplayTitle } from '../format/title.js';
|
|
3
|
+
|
|
4
|
+
/** @type {string} */
|
|
5
|
+
export let title;
|
|
6
|
+
|
|
7
|
+
/** @type {string | null} */
|
|
8
|
+
export let subtitle = null;
|
|
9
|
+
|
|
10
|
+
/** @type {any} */
|
|
11
|
+
export let subtitleNode = null;
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<header class="header">
|
|
15
|
+
<div>
|
|
16
|
+
<h1 class="title">{title}</h1>
|
|
17
|
+
{#if subtitle}
|
|
18
|
+
<p class="subtitle">{@html subtitle}</p>
|
|
19
|
+
{:else if subtitleNode}
|
|
20
|
+
<p class="subtitle">{getDisplayTitle(subtitleNode)}</p>
|
|
21
|
+
{/if}
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.header {
|
|
27
|
+
margin-bottom: 48px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.title {
|
|
31
|
+
font-size: var(--sp-font-h1);
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
line-height: 1.2;
|
|
34
|
+
margin-bottom: 0;
|
|
35
|
+
color: var(--text-color);
|
|
36
|
+
letter-spacing: -0.5px;
|
|
37
|
+
font-family: var(--font-prose);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.subtitle {
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: var(--sp-font-body);
|
|
43
|
+
line-height: 1.6;
|
|
44
|
+
color: var(--text-color);
|
|
45
|
+
font-family: var(--font-ui);
|
|
46
|
+
margin-top: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.subtitle :global(a) {
|
|
50
|
+
color: inherit;
|
|
51
|
+
text-decoration: underline;
|
|
52
|
+
text-decoration-thickness: 1px;
|
|
53
|
+
text-underline-offset: 0.25rem;
|
|
54
|
+
text-decoration-color: rgba(0, 0, 0, 0.25);
|
|
55
|
+
transition: text-decoration-thickness 150ms ease, text-decoration-color 150ms ease;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@media (prefers-color-scheme: dark) {
|
|
59
|
+
:root:not([data-theme="light"]) .subtitle :global(a) {
|
|
60
|
+
text-decoration-color: rgba(255, 255, 255, 0.35);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:global(html[data-theme="dark"]) .subtitle :global(a) {
|
|
65
|
+
text-decoration-color: rgba(255, 255, 255, 0.35);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:global(html[data-theme="light"]) .subtitle :global(a) {
|
|
69
|
+
text-decoration-color: rgba(0, 0, 0, 0.25);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.subtitle :global(a:hover) {
|
|
73
|
+
text-decoration-thickness: 1.5px;
|
|
74
|
+
text-decoration-color: rgba(0, 0, 0, 0.4);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@media (prefers-color-scheme: dark) {
|
|
78
|
+
:root:not([data-theme="light"]) .subtitle :global(a:hover) {
|
|
79
|
+
text-decoration-color: rgba(255, 255, 255, 0.5);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
:global(html[data-theme="dark"]) .subtitle :global(a:hover) {
|
|
84
|
+
text-decoration-color: rgba(255, 255, 255, 0.5);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:global(html[data-theme="light"]) .subtitle :global(a:hover) {
|
|
88
|
+
text-decoration-color: rgba(0, 0, 0, 0.4);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (max-width: 768px) {
|
|
92
|
+
.title {
|
|
93
|
+
font-size: 36px;
|
|
94
|
+
margin-bottom: 32px;
|
|
95
|
+
letter-spacing: -0.3px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.subtitle {
|
|
99
|
+
font-size: var(--sp-font-body);
|
|
100
|
+
line-height: 1.65;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (max-width: 480px) {
|
|
105
|
+
.title {
|
|
106
|
+
font-size: 32px;
|
|
107
|
+
margin-bottom: 28px;
|
|
108
|
+
letter-spacing: -0.2px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.subtitle {
|
|
112
|
+
font-size: var(--sp-font-body);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
|