@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,873 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { universeGraph, currentUniverseName, universeRootNode, getNodeRoute, getRelationshipsForNode, getAncestorChain } from '../lib/data/universeStore.js';
|
|
3
|
+
import { getDisplayTitle } from '../lib/format/title.js';
|
|
4
|
+
import Prose from '../lib/components/Prose.svelte';
|
|
5
|
+
import ContentsCard from '../lib/components/ContentsCard.svelte';
|
|
6
|
+
import FooterStatus from '../lib/components/FooterStatus.svelte';
|
|
7
|
+
import PageHeader from '../lib/components/PageHeader.svelte';
|
|
8
|
+
import { linkForPath } from '../lib/references/linkForPath.js';
|
|
9
|
+
import { linkForRepository } from '../lib/references/linkForRepository.js';
|
|
10
|
+
import { isWildcardPath } from '../lib/references/isWildcardPath.js';
|
|
11
|
+
|
|
12
|
+
/** @type {{ universe: string, id: string }} */
|
|
13
|
+
export let params;
|
|
14
|
+
|
|
15
|
+
// Update universe name reactively when params change
|
|
16
|
+
$: currentUniverseName.set(params.universe);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{ raw?:string, normalized?:string, source?:any }} TextBlock
|
|
20
|
+
* @typedef {{ repository:string, paths:string[]|Array<{path:string, describe?:TextBlock}>, describe?:TextBlock }} ReferenceBlock
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Decode the node ID from URL
|
|
24
|
+
$: nodeId = decodeURIComponent(params.id);
|
|
25
|
+
$: currentNode = $universeGraph?.nodes[nodeId];
|
|
26
|
+
$: children = currentNode?.children?.map((id) => $universeGraph?.nodes[id]).filter(Boolean) || [];
|
|
27
|
+
$: relationships = currentNode ? getRelationshipsForNode($universeGraph, nodeId) : [];
|
|
28
|
+
$: ancestors = currentNode ? getAncestorChain($universeGraph, currentNode) : [];
|
|
29
|
+
$: showContextLine = currentNode && (currentNode.kind === 'book' || currentNode.kind === 'chapter' || (currentNode.kind === 'concept' && currentNode.parent));
|
|
30
|
+
$: documentation = currentNode?.documentation || [];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the anthology node for a series, if it belongs to one
|
|
34
|
+
* @param {any} graph
|
|
35
|
+
* @param {any} seriesNode
|
|
36
|
+
* @returns {any|null}
|
|
37
|
+
*/
|
|
38
|
+
function getAnthologyForSeries(graph, seriesNode) {
|
|
39
|
+
if (!seriesNode?.parent || !graph) return null;
|
|
40
|
+
const parent = graph.nodes[seriesNode.parent];
|
|
41
|
+
return parent?.kind === 'anthology' ? parent : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the anthology context for any node by finding its parent series
|
|
46
|
+
* @param {any} graph
|
|
47
|
+
* @param {any} node
|
|
48
|
+
* @returns {any|null}
|
|
49
|
+
*/
|
|
50
|
+
function getAnthologyForNode(graph, node) {
|
|
51
|
+
if (!node || !graph) return null;
|
|
52
|
+
|
|
53
|
+
if (node.kind === 'series') {
|
|
54
|
+
return getAnthologyForSeries(graph, node);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let currentParentId = node.parent;
|
|
58
|
+
while (currentParentId) {
|
|
59
|
+
const parentNode = graph.nodes[currentParentId];
|
|
60
|
+
if (!parentNode) break;
|
|
61
|
+
|
|
62
|
+
if (parentNode.kind === 'series') {
|
|
63
|
+
return getAnthologyForSeries(graph, parentNode);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
currentParentId = parentNode.parent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
$: currentAnthology = currentNode && $universeGraph
|
|
73
|
+
? getAnthologyForNode($universeGraph, currentNode)
|
|
74
|
+
: null;
|
|
75
|
+
|
|
76
|
+
// Get chapter siblings when viewing a chapter page (excluding current chapter)
|
|
77
|
+
$: chapterSiblings = (() => {
|
|
78
|
+
if (!currentNode || currentNode.kind !== 'chapter' || !$universeGraph) return [];
|
|
79
|
+
const book = ancestors[0];
|
|
80
|
+
if (!book || book.kind !== 'book') return [];
|
|
81
|
+
return (book.children || [])
|
|
82
|
+
.map((id) => $universeGraph.nodes[id])
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.filter((node) => node.kind === 'chapter' && node.id !== currentNode.id);
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalize reference paths to handle both Shape A (strings) and Shape B (objects)
|
|
89
|
+
* @param {ReferenceBlock} ref
|
|
90
|
+
* @returns {Array<{path:string, describe?:TextBlock}>}
|
|
91
|
+
*/
|
|
92
|
+
function normalizePaths(ref) {
|
|
93
|
+
if (!ref.paths || ref.paths.length === 0) return [];
|
|
94
|
+
|
|
95
|
+
// Shape B: paths is array of objects
|
|
96
|
+
if (typeof ref.paths[0] === 'object' && ref.paths[0] !== null && 'path' in ref.paths[0]) {
|
|
97
|
+
return ref.paths.map((/** @type {{path:string, describe?:TextBlock}} */ p) => ({
|
|
98
|
+
path: p.path,
|
|
99
|
+
describe: p.describe,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Shape A: paths is array of strings
|
|
104
|
+
return ref.paths.map((/** @type {string} */ p) => ({
|
|
105
|
+
path: p,
|
|
106
|
+
describe: undefined,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Group references by repository, preserving source order
|
|
111
|
+
$: groupedReferences = (() => {
|
|
112
|
+
const refs = currentNode?.references || [];
|
|
113
|
+
if (refs.length === 0) return [];
|
|
114
|
+
|
|
115
|
+
const groups = new Map();
|
|
116
|
+
const order = [];
|
|
117
|
+
|
|
118
|
+
for (const ref of refs) {
|
|
119
|
+
const repo = ref.repository;
|
|
120
|
+
if (!groups.has(repo)) {
|
|
121
|
+
groups.set(repo, {
|
|
122
|
+
referenceGroups: [],
|
|
123
|
+
});
|
|
124
|
+
order.push(repo);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const group = groups.get(repo);
|
|
128
|
+
const normalizedPaths = normalizePaths(ref);
|
|
129
|
+
const pathEntries = normalizedPaths.map((pathEntry) => ({
|
|
130
|
+
path: pathEntry.path,
|
|
131
|
+
perPathDescribe: pathEntry.describe,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
group.referenceGroups.push({
|
|
135
|
+
paths: pathEntries,
|
|
136
|
+
groupDescribe: ref.describe?.normalized || undefined,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return order.map((repo) => ({
|
|
141
|
+
repository: repo,
|
|
142
|
+
referenceGroups: groups.get(repo).referenceGroups,
|
|
143
|
+
}));
|
|
144
|
+
})();
|
|
145
|
+
|
|
146
|
+
// Expand/collapse state per repository group
|
|
147
|
+
let expandedGroups = new Set();
|
|
148
|
+
|
|
149
|
+
// Reactive repository URLs and path link generator - need to read from store reactively
|
|
150
|
+
$: repositoryUrls = (() => {
|
|
151
|
+
const graph = $universeGraph;
|
|
152
|
+
if (!graph?.repositories) return {};
|
|
153
|
+
const urls = {};
|
|
154
|
+
for (const [repoName, repoConfig] of Object.entries(graph.repositories)) {
|
|
155
|
+
const { kind, options } = repoConfig;
|
|
156
|
+
if (kind === 'sprig-repository-github') {
|
|
157
|
+
if (options?.url) {
|
|
158
|
+
urls[repoName] = options.url.endsWith('/') ? options.url.slice(0, -1) : options.url;
|
|
159
|
+
} else if (options?.owner && options?.repo) {
|
|
160
|
+
urls[repoName] = `https://github.com/${options.owner}/${options.repo}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return urls;
|
|
165
|
+
})();
|
|
166
|
+
|
|
167
|
+
// Helper function to generate path URLs reactively
|
|
168
|
+
function getPathUrl(repository, path) {
|
|
169
|
+
const graph = $universeGraph;
|
|
170
|
+
if (!graph?.repositories) return null;
|
|
171
|
+
const repoConfig = graph.repositories[repository];
|
|
172
|
+
if (!repoConfig) return null;
|
|
173
|
+
const { kind, options } = repoConfig;
|
|
174
|
+
const defaultBranch = options?.defaultBranch || 'main';
|
|
175
|
+
|
|
176
|
+
if (kind === 'sprig-repository-github') {
|
|
177
|
+
let baseUrl;
|
|
178
|
+
if (options?.url) {
|
|
179
|
+
baseUrl = options.url;
|
|
180
|
+
} else if (options?.owner && options?.repo) {
|
|
181
|
+
baseUrl = `https://github.com/${options.owner}/${options.repo}`;
|
|
182
|
+
} else {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
186
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
187
|
+
|
|
188
|
+
if (isWildcardPath(path)) {
|
|
189
|
+
const lastSlashIndex = normalizedPath.lastIndexOf('/');
|
|
190
|
+
if (lastSlashIndex > 0) {
|
|
191
|
+
const folderPath = normalizedPath.slice(0, lastSlashIndex);
|
|
192
|
+
return `${normalizedBaseUrl}/tree/${defaultBranch}${folderPath}`;
|
|
193
|
+
}
|
|
194
|
+
return `${normalizedBaseUrl}/tree/${defaultBranch}`;
|
|
195
|
+
}
|
|
196
|
+
return `${normalizedBaseUrl}/blob/${defaultBranch}${normalizedPath}`;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {string} repository
|
|
203
|
+
*/
|
|
204
|
+
function toggleGroup(repository) {
|
|
205
|
+
if (expandedGroups.has(repository)) {
|
|
206
|
+
expandedGroups.delete(repository);
|
|
207
|
+
} else {
|
|
208
|
+
expandedGroups.add(repository);
|
|
209
|
+
}
|
|
210
|
+
expandedGroups = expandedGroups; // Trigger reactivity
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const DEFAULT_VISIBLE_PATHS = 3;
|
|
214
|
+
|
|
215
|
+
$: subtitle = (() => {
|
|
216
|
+
if (showContextLine) {
|
|
217
|
+
if (currentNode.kind === 'book' && ancestors[0]) {
|
|
218
|
+
const series = ancestors[0];
|
|
219
|
+
const seriesRoute = getNodeRoute(series);
|
|
220
|
+
return `A book in the <a href="${seriesRoute}">${getDisplayTitle(series)}</a> series`;
|
|
221
|
+
} else if (currentNode.kind === 'chapter' && ancestors[0] && ancestors[1]) {
|
|
222
|
+
const book = ancestors[0];
|
|
223
|
+
const series = ancestors[1];
|
|
224
|
+
const bookRoute = getNodeRoute(book);
|
|
225
|
+
const seriesRoute = getNodeRoute(series);
|
|
226
|
+
return `A chapter in <a href="${bookRoute}">${getDisplayTitle(book)}</a>, within the <a href="${seriesRoute}">${getDisplayTitle(series)}</a> series`;
|
|
227
|
+
} else if (currentNode.kind === 'concept' && currentNode.parent && $universeGraph) {
|
|
228
|
+
const parentNode = $universeGraph.nodes[currentNode.parent];
|
|
229
|
+
if (parentNode) {
|
|
230
|
+
const parentRoute = getNodeRoute(parentNode);
|
|
231
|
+
const parentTitle = getDisplayTitle(parentNode);
|
|
232
|
+
|
|
233
|
+
if (parentNode.kind === 'book' && ancestors.length > 0) {
|
|
234
|
+
const series = ancestors.find(a => a.kind === 'series');
|
|
235
|
+
if (series) {
|
|
236
|
+
const seriesRoute = getNodeRoute(series);
|
|
237
|
+
return `A concept in <a href="${parentRoute}">${parentTitle}</a>, within the <a href="${seriesRoute}">${getDisplayTitle(series)}</a> series`;
|
|
238
|
+
} else {
|
|
239
|
+
return `A concept in <a href="${parentRoute}">${parentTitle}</a>`;
|
|
240
|
+
}
|
|
241
|
+
} else if (parentNode.kind === 'series') {
|
|
242
|
+
return `A concept in the <a href="${parentRoute}">${parentTitle}</a> series`;
|
|
243
|
+
} else if (parentNode.kind === 'anthology') {
|
|
244
|
+
return `A concept in <a href="${parentRoute}">${parentTitle}</a>`;
|
|
245
|
+
} else {
|
|
246
|
+
return `A concept in <a href="${parentRoute}">${parentTitle}</a>`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return `${currentNode.kind} in ${params.universe}`;
|
|
251
|
+
} else {
|
|
252
|
+
if (currentNode.kind === 'concept') {
|
|
253
|
+
return `A concept in ${params.universe}`;
|
|
254
|
+
} else {
|
|
255
|
+
return `${currentNode.kind} in ${params.universe}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
})();
|
|
259
|
+
</script>
|
|
260
|
+
|
|
261
|
+
{#if currentNode}
|
|
262
|
+
<PageHeader title={getDisplayTitle(currentNode)} {subtitle} />
|
|
263
|
+
|
|
264
|
+
<div class="grid">
|
|
265
|
+
<section class="narrative">
|
|
266
|
+
<Prose textBlock={currentNode.describe} />
|
|
267
|
+
</section>
|
|
268
|
+
|
|
269
|
+
{#if children.length > 0}
|
|
270
|
+
<aside class="index">
|
|
271
|
+
<ContentsCard children={children} currentNode={currentNode} />
|
|
272
|
+
</aside>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{#if currentNode.kind === 'chapter' && chapterSiblings.length > 0}
|
|
277
|
+
<section class="chapter-navigation">
|
|
278
|
+
<h2 class="chapter-navigation-title">In this book</h2>
|
|
279
|
+
<ul class="chapter-navigation-list">
|
|
280
|
+
{#each chapterSiblings as chapter}
|
|
281
|
+
<li class="chapter-navigation-item">
|
|
282
|
+
<a
|
|
283
|
+
class="chapter-navigation-link sprig-link"
|
|
284
|
+
href={getNodeRoute(chapter)}
|
|
285
|
+
>
|
|
286
|
+
{getDisplayTitle(chapter)}
|
|
287
|
+
</a>
|
|
288
|
+
</li>
|
|
289
|
+
{/each}
|
|
290
|
+
</ul>
|
|
291
|
+
</section>
|
|
292
|
+
{/if}
|
|
293
|
+
|
|
294
|
+
{#if relationships.length > 0}
|
|
295
|
+
<section class="relationships">
|
|
296
|
+
<h2 class="relationships-title">Relationships</h2>
|
|
297
|
+
<ul class="relationships-list">
|
|
298
|
+
{#each relationships as rel}
|
|
299
|
+
{@const otherAnthology = $universeGraph
|
|
300
|
+
? getAnthologyForNode($universeGraph, rel.otherNode)
|
|
301
|
+
: null}
|
|
302
|
+
{@const isDifferentAnthology = otherAnthology && (
|
|
303
|
+
!currentAnthology || currentAnthology.id !== otherAnthology.id
|
|
304
|
+
)}
|
|
305
|
+
{@const displayName = isDifferentAnthology
|
|
306
|
+
? `${getDisplayTitle(rel.otherNode)} (${getDisplayTitle(otherAnthology)})`
|
|
307
|
+
: getDisplayTitle(rel.otherNode)}
|
|
308
|
+
<li class="relationship-item">
|
|
309
|
+
<a class="relationship-link sprig-link" href={getNodeRoute(rel.otherNode)}>
|
|
310
|
+
<span class="relationship-label">{rel.label}</span>
|
|
311
|
+
<span class="relationship-separator">→</span>
|
|
312
|
+
<span class="relationship-name">{displayName}</span>
|
|
313
|
+
<span class="relationship-kind">{rel.otherNode.kind}</span>
|
|
314
|
+
</a>
|
|
315
|
+
{#if rel.desc}
|
|
316
|
+
<p class="relationship-description">{rel.desc}</p>
|
|
317
|
+
{/if}
|
|
318
|
+
</li>
|
|
319
|
+
{/each}
|
|
320
|
+
</ul>
|
|
321
|
+
</section>
|
|
322
|
+
{/if}
|
|
323
|
+
|
|
324
|
+
{#if groupedReferences.length > 0}
|
|
325
|
+
<section class="references">
|
|
326
|
+
<h2 class="references-title">References</h2>
|
|
327
|
+
<p class="references-subtitle">Where this concept appears in code and docs.</p>
|
|
328
|
+
|
|
329
|
+
{#each groupedReferences as group}
|
|
330
|
+
{@const repoUrl = repositoryUrls[group.repository] || null}
|
|
331
|
+
{@const allPaths = group.referenceGroups.flatMap(rg => rg.paths)}
|
|
332
|
+
{@const isExpanded = expandedGroups.has(group.repository)}
|
|
333
|
+
{@const visibleRefGroups = (() => {
|
|
334
|
+
if (isExpanded) return group.referenceGroups;
|
|
335
|
+
let pathCount = 0;
|
|
336
|
+
const visible = [];
|
|
337
|
+
for (const refGroup of group.referenceGroups) {
|
|
338
|
+
if (pathCount + refGroup.paths.length <= DEFAULT_VISIBLE_PATHS) {
|
|
339
|
+
visible.push(refGroup);
|
|
340
|
+
pathCount += refGroup.paths.length;
|
|
341
|
+
} else {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return visible;
|
|
346
|
+
})()}
|
|
347
|
+
{@const hiddenCount = allPaths.length - visibleRefGroups.reduce((sum, rg) => sum + rg.paths.length, 0)}
|
|
348
|
+
|
|
349
|
+
<div class="reference-group">
|
|
350
|
+
{#if repoUrl}
|
|
351
|
+
<a class="reference-repo-pill" href={repoUrl} target="_blank" rel="noopener noreferrer">
|
|
352
|
+
<span class="reference-repo-label">repo</span>
|
|
353
|
+
<span class="reference-repo-name">{group.repository}</span>
|
|
354
|
+
</a>
|
|
355
|
+
{:else}
|
|
356
|
+
<div class="reference-repo-pill reference-repo-pill--no-link">
|
|
357
|
+
<span class="reference-repo-label">repo</span>
|
|
358
|
+
<span class="reference-repo-name">{group.repository}</span>
|
|
359
|
+
</div>
|
|
360
|
+
{/if}
|
|
361
|
+
|
|
362
|
+
<ul class="reference-paths-list">
|
|
363
|
+
{#each visibleRefGroups as refGroup, refGroupIndex}
|
|
364
|
+
{#each refGroup.paths as pathEntry, pathIndex}
|
|
365
|
+
{@const path = pathEntry.path}
|
|
366
|
+
{@const url = getPathUrl(group.repository, path)}
|
|
367
|
+
{@const isWildcard = isWildcardPath(path)}
|
|
368
|
+
{@const isLastInRefGroup = pathIndex === refGroup.paths.length - 1}
|
|
369
|
+
{@const isFirstInRefGroup = pathIndex === 0}
|
|
370
|
+
{@const isFirstRefGroup = refGroupIndex === 0}
|
|
371
|
+
<li class="reference-path-item" class:reference-path-item--tight={!isLastInRefGroup} class:reference-path-item--spaced={isFirstInRefGroup && !isFirstRefGroup}>
|
|
372
|
+
{#if url}
|
|
373
|
+
<a class="reference-path-link sprig-link sprig-link--quiet" href={url} target="_blank" rel="noopener noreferrer">
|
|
374
|
+
<span class="reference-path-text">{path}</span>
|
|
375
|
+
{#if isWildcard}
|
|
376
|
+
<span class="reference-path-wildcard">(pattern)</span>
|
|
377
|
+
{/if}
|
|
378
|
+
</a>
|
|
379
|
+
{:else}
|
|
380
|
+
<span class="reference-path-text">{path}</span>
|
|
381
|
+
{#if isWildcard}
|
|
382
|
+
<span class="reference-path-wildcard">(pattern)</span>
|
|
383
|
+
{/if}
|
|
384
|
+
{/if}
|
|
385
|
+
{#if pathEntry.perPathDescribe?.normalized}
|
|
386
|
+
<p class="reference-description reference-description--per-path">{pathEntry.perPathDescribe.normalized}</p>
|
|
387
|
+
{/if}
|
|
388
|
+
</li>
|
|
389
|
+
{/each}
|
|
390
|
+
{#if refGroup.groupDescribe}
|
|
391
|
+
<li class="reference-path-item reference-path-item--describe">
|
|
392
|
+
<p class="reference-description reference-description--group">{refGroup.groupDescribe}</p>
|
|
393
|
+
</li>
|
|
394
|
+
{/if}
|
|
395
|
+
{/each}
|
|
396
|
+
</ul>
|
|
397
|
+
|
|
398
|
+
{#if hiddenCount > 0 && !isExpanded}
|
|
399
|
+
<button
|
|
400
|
+
class="reference-expand-button"
|
|
401
|
+
on:click={() => toggleGroup(group.repository)}
|
|
402
|
+
type="button"
|
|
403
|
+
>
|
|
404
|
+
+ {hiddenCount} more path{hiddenCount === 1 ? '' : 's'}
|
|
405
|
+
</button>
|
|
406
|
+
{/if}
|
|
407
|
+
{#if isExpanded && hiddenCount > 0}
|
|
408
|
+
<button
|
|
409
|
+
class="reference-expand-button"
|
|
410
|
+
on:click={() => toggleGroup(group.repository)}
|
|
411
|
+
type="button"
|
|
412
|
+
>
|
|
413
|
+
Show less
|
|
414
|
+
</button>
|
|
415
|
+
{/if}
|
|
416
|
+
</div>
|
|
417
|
+
{/each}
|
|
418
|
+
</section>
|
|
419
|
+
{/if}
|
|
420
|
+
|
|
421
|
+
{#if documentation.length > 0}
|
|
422
|
+
<section class="documentation">
|
|
423
|
+
<h2 class="documentation-title">Documentation</h2>
|
|
424
|
+
<p class="documentation-subtitle">Additional documentation for this concept.</p>
|
|
425
|
+
|
|
426
|
+
<ul class="documentation-list">
|
|
427
|
+
{#each documentation as doc}
|
|
428
|
+
{@const cleanPath = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path}
|
|
429
|
+
{@const pathWithoutDocs = cleanPath.startsWith('docs/') ? cleanPath.slice(5) : cleanPath}
|
|
430
|
+
{@const pathParts = pathWithoutDocs.split('/').map(p => encodeURIComponent(p))}
|
|
431
|
+
{@const docUrl = `/universes/${params.universe}/docs/${pathParts.join('/')}`}
|
|
432
|
+
{@const docTitle = doc.title || doc.path.split('/').pop() || doc.path}
|
|
433
|
+
<li class="documentation-item">
|
|
434
|
+
<a class="documentation-link sprig-link" href={docUrl}>
|
|
435
|
+
<span class="documentation-title-text">{docTitle}</span>
|
|
436
|
+
{#if doc.kind}
|
|
437
|
+
<span class="documentation-kind">{doc.kind}</span>
|
|
438
|
+
{/if}
|
|
439
|
+
</a>
|
|
440
|
+
{#if doc.path !== docTitle}
|
|
441
|
+
<p class="documentation-path">{doc.path}</p>
|
|
442
|
+
{/if}
|
|
443
|
+
{#if doc.describe?.normalized}
|
|
444
|
+
<p class="documentation-description">{doc.describe.normalized}</p>
|
|
445
|
+
{/if}
|
|
446
|
+
</li>
|
|
447
|
+
{/each}
|
|
448
|
+
</ul>
|
|
449
|
+
</section>
|
|
450
|
+
{/if}
|
|
451
|
+
|
|
452
|
+
<FooterStatus graph={$universeGraph} root={$universeRootNode} />
|
|
453
|
+
{:else}
|
|
454
|
+
<div class="loading">Loading…</div>
|
|
455
|
+
{/if}
|
|
456
|
+
|
|
457
|
+
<style>
|
|
458
|
+
.grid {
|
|
459
|
+
display: grid;
|
|
460
|
+
grid-template-columns: 1fr 350px;
|
|
461
|
+
gap: 64px;
|
|
462
|
+
align-items: start;
|
|
463
|
+
max-width: 1200px;
|
|
464
|
+
margin: 0 auto;
|
|
465
|
+
width: 100%;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.narrative {
|
|
469
|
+
max-width: 680px;
|
|
470
|
+
min-width: 0;
|
|
471
|
+
width: 100%;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.index {
|
|
475
|
+
min-width: 0;
|
|
476
|
+
width: 100%;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.chapter-navigation {
|
|
480
|
+
margin-top: 20px;
|
|
481
|
+
padding-top: 18px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.chapter-navigation-title {
|
|
485
|
+
font-family: var(--font-prose);
|
|
486
|
+
font-size: var(--sp-font-small);
|
|
487
|
+
letter-spacing: 0;
|
|
488
|
+
text-transform: none;
|
|
489
|
+
color: var(--text-secondary);
|
|
490
|
+
margin: 0 0 0.5rem 0;
|
|
491
|
+
font-weight: 400;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.chapter-navigation-list {
|
|
495
|
+
list-style: none;
|
|
496
|
+
padding: 0;
|
|
497
|
+
margin: 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.chapter-navigation-item {
|
|
501
|
+
margin-bottom: 0.4rem;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.chapter-navigation-item:last-child {
|
|
505
|
+
margin-bottom: 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.chapter-navigation-link {
|
|
509
|
+
display: inline-block;
|
|
510
|
+
font-family: var(--font-prose);
|
|
511
|
+
font-size: var(--sp-font-body);
|
|
512
|
+
font-weight: 400;
|
|
513
|
+
color: inherit;
|
|
514
|
+
padding: 2px 0;
|
|
515
|
+
text-decoration: underline;
|
|
516
|
+
text-decoration-thickness: 1px;
|
|
517
|
+
text-underline-offset: 0.1875rem;
|
|
518
|
+
text-decoration-color: var(--sprig-link-underline);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.chapter-navigation-link:hover {
|
|
522
|
+
text-decoration-thickness: 1.5px;
|
|
523
|
+
text-decoration-color: var(--sprig-link-underline-hover);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.relationships {
|
|
527
|
+
margin-top: 32px;
|
|
528
|
+
padding-top: 24px;
|
|
529
|
+
border-top: 1px solid var(--hairline);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.relationships-title {
|
|
533
|
+
font-family: var(--font-ui);
|
|
534
|
+
font-size: var(--sp-font-tiny);
|
|
535
|
+
letter-spacing: 0.02em;
|
|
536
|
+
text-transform: none;
|
|
537
|
+
color: var(--text-secondary);
|
|
538
|
+
margin: 0 0 0.75rem 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.relationships-list {
|
|
542
|
+
list-style: none;
|
|
543
|
+
padding: 0;
|
|
544
|
+
margin: 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.relationship-item {
|
|
548
|
+
margin-bottom: 2rem;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.relationship-item:last-child {
|
|
552
|
+
margin-bottom: 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.relationship-link {
|
|
556
|
+
display: flex;
|
|
557
|
+
align-items: center;
|
|
558
|
+
gap: 8px;
|
|
559
|
+
padding: 4px 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.relationship-link.sprig-link {
|
|
563
|
+
text-decoration: none;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.relationship-link.sprig-link .relationship-name {
|
|
567
|
+
text-decoration: underline;
|
|
568
|
+
text-decoration-thickness: 1px;
|
|
569
|
+
text-underline-offset: 0.1875rem;
|
|
570
|
+
text-decoration-color: var(--sprig-link-underline);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.relationship-link.sprig-link:hover .relationship-name {
|
|
574
|
+
text-decoration-thickness: 1.5px;
|
|
575
|
+
text-decoration-color: var(--sprig-link-underline-hover);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.relationship-label {
|
|
579
|
+
font-family: var(--font-ui);
|
|
580
|
+
font-size: var(--sp-font-tiny);
|
|
581
|
+
color: var(--text-secondary);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.relationship-separator {
|
|
585
|
+
font-family: var(--font-ui);
|
|
586
|
+
font-size: var(--sp-font-tiny);
|
|
587
|
+
color: var(--text-tertiary);
|
|
588
|
+
opacity: 0.6;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.relationship-name {
|
|
592
|
+
font-family: var(--font-prose);
|
|
593
|
+
font-size: var(--sp-font-small);
|
|
594
|
+
color: inherit;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.relationship-kind {
|
|
598
|
+
font-family: var(--font-ui);
|
|
599
|
+
font-size: var(--sp-font-tiny);
|
|
600
|
+
color: var(--text-tertiary);
|
|
601
|
+
margin-left: auto;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.relationship-description {
|
|
605
|
+
margin: 8px 0 0 0;
|
|
606
|
+
font-family: var(--font-prose);
|
|
607
|
+
font-size: var(--sp-font-body);
|
|
608
|
+
color: var(--text-secondary);
|
|
609
|
+
line-height: 1.6;
|
|
610
|
+
max-width: 70ch;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.loading {
|
|
614
|
+
color: var(--text-tertiary);
|
|
615
|
+
padding: 24px 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.references {
|
|
619
|
+
margin-top: 32px;
|
|
620
|
+
padding-top: 24px;
|
|
621
|
+
border-top: 1px solid var(--hairline);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.references-title {
|
|
625
|
+
font-family: var(--font-ui);
|
|
626
|
+
font-size: var(--sp-font-tiny);
|
|
627
|
+
letter-spacing: 0.02em;
|
|
628
|
+
text-transform: none;
|
|
629
|
+
color: var(--text-secondary);
|
|
630
|
+
margin: 0 0 0.75rem 0;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.references-subtitle {
|
|
634
|
+
font-family: var(--font-ui);
|
|
635
|
+
font-size: var(--sp-font-tiny);
|
|
636
|
+
color: var(--text-tertiary);
|
|
637
|
+
margin: 0 0 1.5rem 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.reference-group {
|
|
641
|
+
margin-bottom: 2rem;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.reference-group:last-child {
|
|
645
|
+
margin-bottom: 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.reference-repo-pill {
|
|
649
|
+
display: inline-flex;
|
|
650
|
+
align-items: center;
|
|
651
|
+
gap: 6px;
|
|
652
|
+
padding: 4px 10px;
|
|
653
|
+
background: var(--card-bg);
|
|
654
|
+
border-radius: 6px;
|
|
655
|
+
text-decoration: none;
|
|
656
|
+
color: inherit;
|
|
657
|
+
margin-bottom: 0.75rem;
|
|
658
|
+
transition: opacity 0.2s;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.reference-repo-pill:hover {
|
|
662
|
+
opacity: 0.8;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.reference-repo-pill--no-link {
|
|
666
|
+
cursor: default;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.reference-repo-pill--no-link:hover {
|
|
670
|
+
opacity: 1;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.reference-repo-label {
|
|
674
|
+
font-family: var(--font-ui);
|
|
675
|
+
font-size: var(--sp-font-tiny);
|
|
676
|
+
letter-spacing: 0.05em;
|
|
677
|
+
text-transform: lowercase;
|
|
678
|
+
color: var(--text-tertiary);
|
|
679
|
+
font-weight: 400;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.reference-repo-name {
|
|
683
|
+
font-family: var(--font-ui);
|
|
684
|
+
font-size: var(--sp-font-tiny);
|
|
685
|
+
color: var(--text-secondary);
|
|
686
|
+
font-weight: 400;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.reference-paths-list {
|
|
690
|
+
list-style: none;
|
|
691
|
+
padding: 0;
|
|
692
|
+
margin: 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.reference-path-item {
|
|
696
|
+
margin-bottom: 0.75rem;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.reference-path-item:last-child {
|
|
700
|
+
margin-bottom: 0;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.reference-path-item--tight {
|
|
704
|
+
margin-bottom: 0.25rem;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.reference-path-item--spaced {
|
|
708
|
+
margin-top: 0.75rem;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.reference-path-item--describe {
|
|
712
|
+
margin-top: 0.5rem;
|
|
713
|
+
margin-bottom: 0.75rem;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.reference-path-link {
|
|
717
|
+
display: block;
|
|
718
|
+
padding: 4px 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.reference-path-text {
|
|
722
|
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
723
|
+
font-size: var(--sp-font-body);
|
|
724
|
+
color: inherit;
|
|
725
|
+
font-variant-numeric: tabular-nums;
|
|
726
|
+
letter-spacing: 0.01em;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.reference-path-wildcard {
|
|
730
|
+
font-family: var(--font-ui);
|
|
731
|
+
font-size: var(--sp-font-tiny);
|
|
732
|
+
color: var(--text-tertiary);
|
|
733
|
+
margin-left: 4px;
|
|
734
|
+
font-weight: 400;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.reference-description {
|
|
738
|
+
margin: 6px 0 0 0;
|
|
739
|
+
font-family: var(--font-prose);
|
|
740
|
+
font-size: var(--sp-font-body);
|
|
741
|
+
color: var(--text-secondary);
|
|
742
|
+
line-height: 1.5;
|
|
743
|
+
max-width: 70ch;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.reference-description--per-path {
|
|
747
|
+
margin-top: 6px;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.reference-description--group {
|
|
751
|
+
margin: 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.reference-expand-button {
|
|
755
|
+
background: none;
|
|
756
|
+
border: none;
|
|
757
|
+
color: var(--text-tertiary);
|
|
758
|
+
font-family: var(--font-ui);
|
|
759
|
+
font-size: var(--sp-font-tiny);
|
|
760
|
+
padding: 4px 0;
|
|
761
|
+
margin-top: 0.5rem;
|
|
762
|
+
cursor: pointer;
|
|
763
|
+
text-decoration: underline;
|
|
764
|
+
text-underline-offset: 2px;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.reference-expand-button:hover {
|
|
768
|
+
opacity: 0.8;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.documentation {
|
|
772
|
+
margin-top: 32px;
|
|
773
|
+
padding-top: 24px;
|
|
774
|
+
border-top: 1px solid var(--hairline);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.documentation-title {
|
|
778
|
+
font-family: var(--font-ui);
|
|
779
|
+
font-size: var(--sp-font-tiny);
|
|
780
|
+
letter-spacing: 0.02em;
|
|
781
|
+
text-transform: none;
|
|
782
|
+
color: var(--text-secondary);
|
|
783
|
+
margin: 0 0 0.75rem 0;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.documentation-subtitle {
|
|
787
|
+
font-family: var(--font-ui);
|
|
788
|
+
font-size: var(--sp-font-tiny);
|
|
789
|
+
color: var(--text-tertiary);
|
|
790
|
+
margin: 0 0 1.5rem 0;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.documentation-list {
|
|
794
|
+
list-style: none;
|
|
795
|
+
padding: 0;
|
|
796
|
+
margin: 0;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.documentation-item {
|
|
800
|
+
margin-bottom: 1.5rem;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.documentation-item:last-child {
|
|
804
|
+
margin-bottom: 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.documentation-link {
|
|
808
|
+
display: flex;
|
|
809
|
+
align-items: center;
|
|
810
|
+
gap: 8px;
|
|
811
|
+
padding: 4px 0;
|
|
812
|
+
text-decoration: none;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.documentation-title-text {
|
|
816
|
+
font-family: var(--font-prose);
|
|
817
|
+
font-size: var(--sp-font-body);
|
|
818
|
+
color: inherit;
|
|
819
|
+
text-decoration: underline;
|
|
820
|
+
text-decoration-thickness: 1px;
|
|
821
|
+
text-underline-offset: 0.1875rem;
|
|
822
|
+
text-decoration-color: var(--sprig-link-underline);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.documentation-link:hover .documentation-title-text {
|
|
826
|
+
text-decoration-thickness: 1.5px;
|
|
827
|
+
text-decoration-color: var(--sprig-link-underline-hover);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.documentation-kind {
|
|
831
|
+
font-family: var(--font-ui);
|
|
832
|
+
font-size: var(--sp-font-tiny);
|
|
833
|
+
letter-spacing: 0.05em;
|
|
834
|
+
text-transform: lowercase;
|
|
835
|
+
color: var(--text-tertiary);
|
|
836
|
+
font-weight: 400;
|
|
837
|
+
padding: 2px 6px;
|
|
838
|
+
background: var(--card-bg);
|
|
839
|
+
border-radius: 4px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.documentation-path {
|
|
843
|
+
margin: 6px 0 0 0;
|
|
844
|
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
845
|
+
font-size: var(--sp-font-tiny);
|
|
846
|
+
color: var(--text-tertiary);
|
|
847
|
+
font-variant-numeric: tabular-nums;
|
|
848
|
+
letter-spacing: 0.01em;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.documentation-description {
|
|
852
|
+
margin: 8px 0 0 0;
|
|
853
|
+
font-family: var(--font-prose);
|
|
854
|
+
font-size: var(--sp-font-body);
|
|
855
|
+
color: var(--text-secondary);
|
|
856
|
+
line-height: 1.5;
|
|
857
|
+
max-width: 70ch;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
@media (max-width: 768px) {
|
|
861
|
+
.grid {
|
|
862
|
+
grid-template-columns: 1fr;
|
|
863
|
+
gap: 32px;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
@media (max-width: 480px) {
|
|
868
|
+
.grid {
|
|
869
|
+
gap: 32px;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
</style>
|
|
873
|
+
|