@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,657 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { currentSeries, seriesChildren, seriesRelationships, universeGraph, currentUniverseName, universeRootNode, getNodeRoute, currentSeriesId } 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, series: string }} */
|
|
13
|
+
export let params;
|
|
14
|
+
|
|
15
|
+
// Update stores reactively when params change
|
|
16
|
+
$: {
|
|
17
|
+
const seriesId = `${params.universe}:series:${params.series}`;
|
|
18
|
+
currentSeriesId.set(seriesId);
|
|
19
|
+
currentUniverseName.set(params.universe);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {{ raw?:string, normalized?:string, source?:any }} TextBlock
|
|
24
|
+
* @typedef {{ repository:string, paths:string[]|Array<{path:string, describe?:TextBlock}>, describe?:TextBlock }} ReferenceBlock
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
$: subtitle = (() => {
|
|
28
|
+
if (!$currentSeries || !$universeGraph) {
|
|
29
|
+
return $universeRootNode
|
|
30
|
+
? `A series in <a href="/">${getDisplayTitle($universeRootNode) || params.universe}</a>`
|
|
31
|
+
: `A series in ${params.universe}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if series belongs to an anthology
|
|
35
|
+
const parentId = $currentSeries.parent;
|
|
36
|
+
if (parentId) {
|
|
37
|
+
const parentNode = $universeGraph.nodes[parentId];
|
|
38
|
+
if (parentNode && parentNode.kind === 'anthology') {
|
|
39
|
+
// Series is in an anthology
|
|
40
|
+
const anthologyRoute = getNodeRoute(parentNode);
|
|
41
|
+
const anthologyName = getDisplayTitle(parentNode);
|
|
42
|
+
const universeLink = $universeRootNode
|
|
43
|
+
? `<a href="/">${getDisplayTitle($universeRootNode) || params.universe}</a>`
|
|
44
|
+
: params.universe;
|
|
45
|
+
return `A series in <a href="${anthologyRoute}">${anthologyName}</a> (in ${universeLink})`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Series is not in an anthology
|
|
50
|
+
return $universeRootNode
|
|
51
|
+
? `A series in <a href="/">${getDisplayTitle($universeRootNode) || params.universe}</a>`
|
|
52
|
+
: `A series in ${params.universe}`;
|
|
53
|
+
})();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalize reference paths to handle both Shape A (strings) and Shape B (objects)
|
|
57
|
+
* @param {ReferenceBlock} ref
|
|
58
|
+
* @returns {Array<{path:string, describe?:TextBlock}>}
|
|
59
|
+
*/
|
|
60
|
+
function normalizePaths(ref) {
|
|
61
|
+
if (!ref.paths || ref.paths.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
// Shape B: paths is array of objects
|
|
64
|
+
if (typeof ref.paths[0] === 'object' && ref.paths[0] !== null && 'path' in ref.paths[0]) {
|
|
65
|
+
return ref.paths.map((/** @type {{path:string, describe?:TextBlock}} */ p) => ({
|
|
66
|
+
path: p.path,
|
|
67
|
+
describe: p.describe,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Shape A: paths is array of strings
|
|
72
|
+
return ref.paths.map((/** @type {string} */ p) => ({
|
|
73
|
+
path: p,
|
|
74
|
+
describe: undefined, // No per-path describe in Shape A
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Group references by repository, preserving source order
|
|
79
|
+
$: groupedReferences = (() => {
|
|
80
|
+
const refs = $currentSeries?.references || [];
|
|
81
|
+
if (refs.length === 0) return [];
|
|
82
|
+
|
|
83
|
+
const groups = new Map();
|
|
84
|
+
const order = [];
|
|
85
|
+
|
|
86
|
+
for (const ref of refs) {
|
|
87
|
+
const repo = ref.repository;
|
|
88
|
+
if (!groups.has(repo)) {
|
|
89
|
+
groups.set(repo, {
|
|
90
|
+
referenceGroups: [],
|
|
91
|
+
});
|
|
92
|
+
order.push(repo);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const group = groups.get(repo);
|
|
96
|
+
const normalizedPaths = normalizePaths(ref);
|
|
97
|
+
const pathEntries = normalizedPaths.map((pathEntry) => ({
|
|
98
|
+
path: pathEntry.path,
|
|
99
|
+
perPathDescribe: pathEntry.describe,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
group.referenceGroups.push({
|
|
103
|
+
paths: pathEntries,
|
|
104
|
+
groupDescribe: ref.describe?.normalized || undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return order.map((repo) => ({
|
|
109
|
+
repository: repo,
|
|
110
|
+
referenceGroups: groups.get(repo).referenceGroups,
|
|
111
|
+
}));
|
|
112
|
+
})();
|
|
113
|
+
|
|
114
|
+
// Expand/collapse state per repository group
|
|
115
|
+
let expandedGroups = new Set();
|
|
116
|
+
|
|
117
|
+
// Reactive repository URLs and path link generator - need to read from store reactively
|
|
118
|
+
$: repositoryUrls = (() => {
|
|
119
|
+
const graph = $universeGraph;
|
|
120
|
+
if (!graph?.repositories) return {};
|
|
121
|
+
const urls = {};
|
|
122
|
+
for (const [repoName, repoConfig] of Object.entries(graph.repositories)) {
|
|
123
|
+
const { kind, options } = repoConfig;
|
|
124
|
+
if (kind === 'sprig-repository-github') {
|
|
125
|
+
if (options?.url) {
|
|
126
|
+
urls[repoName] = options.url.endsWith('/') ? options.url.slice(0, -1) : options.url;
|
|
127
|
+
} else if (options?.owner && options?.repo) {
|
|
128
|
+
urls[repoName] = `https://github.com/${options.owner}/${options.repo}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return urls;
|
|
133
|
+
})();
|
|
134
|
+
|
|
135
|
+
// Helper function to generate path URLs reactively
|
|
136
|
+
function getPathUrl(repository, path) {
|
|
137
|
+
const graph = $universeGraph;
|
|
138
|
+
if (!graph?.repositories) return null;
|
|
139
|
+
const repoConfig = graph.repositories[repository];
|
|
140
|
+
if (!repoConfig) return null;
|
|
141
|
+
const { kind, options } = repoConfig;
|
|
142
|
+
const defaultBranch = options?.defaultBranch || 'main';
|
|
143
|
+
|
|
144
|
+
if (kind === 'sprig-repository-github') {
|
|
145
|
+
let baseUrl;
|
|
146
|
+
if (options?.url) {
|
|
147
|
+
baseUrl = options.url;
|
|
148
|
+
} else if (options?.owner && options?.repo) {
|
|
149
|
+
baseUrl = `https://github.com/${options.owner}/${options.repo}`;
|
|
150
|
+
} else {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
154
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
155
|
+
|
|
156
|
+
if (isWildcardPath(path)) {
|
|
157
|
+
const lastSlashIndex = normalizedPath.lastIndexOf('/');
|
|
158
|
+
if (lastSlashIndex > 0) {
|
|
159
|
+
const folderPath = normalizedPath.slice(0, lastSlashIndex);
|
|
160
|
+
return `${normalizedBaseUrl}/tree/${defaultBranch}${folderPath}`;
|
|
161
|
+
}
|
|
162
|
+
return `${normalizedBaseUrl}/tree/${defaultBranch}`;
|
|
163
|
+
}
|
|
164
|
+
return `${normalizedBaseUrl}/blob/${defaultBranch}${normalizedPath}`;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {string} repository
|
|
171
|
+
*/
|
|
172
|
+
function toggleGroup(repository) {
|
|
173
|
+
if (expandedGroups.has(repository)) {
|
|
174
|
+
expandedGroups.delete(repository);
|
|
175
|
+
} else {
|
|
176
|
+
expandedGroups.add(repository);
|
|
177
|
+
}
|
|
178
|
+
expandedGroups = expandedGroups; // Trigger reactivity
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const DEFAULT_VISIBLE_PATHS = 3;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get the anthology node for a series, if it belongs to one
|
|
185
|
+
* @param {any} graph
|
|
186
|
+
* @param {any} seriesNode
|
|
187
|
+
* @returns {any|null}
|
|
188
|
+
*/
|
|
189
|
+
function getAnthologyForSeries(graph, seriesNode) {
|
|
190
|
+
if (!seriesNode?.parent || !graph) return null;
|
|
191
|
+
const parent = graph.nodes[seriesNode.parent];
|
|
192
|
+
return parent?.kind === 'anthology' ? parent : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the anthology context for any node by finding its parent series
|
|
197
|
+
* @param {any} graph
|
|
198
|
+
* @param {any} node
|
|
199
|
+
* @returns {any|null}
|
|
200
|
+
*/
|
|
201
|
+
function getAnthologyForNode(graph, node) {
|
|
202
|
+
if (!node || !graph) return null;
|
|
203
|
+
|
|
204
|
+
if (node.kind === 'series') {
|
|
205
|
+
return getAnthologyForSeries(graph, node);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let currentParentId = node.parent;
|
|
209
|
+
while (currentParentId) {
|
|
210
|
+
const parentNode = graph.nodes[currentParentId];
|
|
211
|
+
if (!parentNode) break;
|
|
212
|
+
|
|
213
|
+
if (parentNode.kind === 'series') {
|
|
214
|
+
return getAnthologyForSeries(graph, parentNode);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
currentParentId = parentNode.parent;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$: currentSeriesAnthology = $currentSeries && $universeGraph
|
|
224
|
+
? getAnthologyForSeries($universeGraph, $currentSeries)
|
|
225
|
+
: null;
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
{#if $currentSeries}
|
|
229
|
+
<PageHeader title={getDisplayTitle($currentSeries)} {subtitle} />
|
|
230
|
+
|
|
231
|
+
<div class="grid">
|
|
232
|
+
<section class="narrative">
|
|
233
|
+
{#if $currentSeries.describe}
|
|
234
|
+
<Prose textBlock={$currentSeries.describe} />
|
|
235
|
+
{/if}
|
|
236
|
+
</section>
|
|
237
|
+
|
|
238
|
+
<aside class="index">
|
|
239
|
+
<ContentsCard children={$seriesChildren} />
|
|
240
|
+
</aside>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{#if $seriesRelationships.length > 0}
|
|
244
|
+
<section class="relationships">
|
|
245
|
+
<h2 class="relationships-title">Relationships</h2>
|
|
246
|
+
<ul class="relationships-list">
|
|
247
|
+
{#each $seriesRelationships as rel}
|
|
248
|
+
{@const otherAnthology = $universeGraph
|
|
249
|
+
? getAnthologyForNode($universeGraph, rel.otherNode)
|
|
250
|
+
: null}
|
|
251
|
+
{@const isDifferentAnthology = otherAnthology && (
|
|
252
|
+
!currentSeriesAnthology || currentSeriesAnthology.id !== otherAnthology.id
|
|
253
|
+
)}
|
|
254
|
+
{@const displayName = isDifferentAnthology
|
|
255
|
+
? `${getDisplayTitle(rel.otherNode)} (${getDisplayTitle(otherAnthology)})`
|
|
256
|
+
: getDisplayTitle(rel.otherNode)}
|
|
257
|
+
<li class="relationship-item">
|
|
258
|
+
<a class="relationship-link sprig-link" href={getNodeRoute(rel.otherNode)}>
|
|
259
|
+
<span class="relationship-label">{rel.label}</span>
|
|
260
|
+
<span class="relationship-separator">→</span>
|
|
261
|
+
<span class="relationship-name">{displayName}</span>
|
|
262
|
+
<span class="relationship-kind">{rel.otherNode.kind}</span>
|
|
263
|
+
</a>
|
|
264
|
+
{#if rel.desc}
|
|
265
|
+
<p class="relationship-description">{rel.desc}</p>
|
|
266
|
+
{/if}
|
|
267
|
+
</li>
|
|
268
|
+
{/each}
|
|
269
|
+
</ul>
|
|
270
|
+
</section>
|
|
271
|
+
{/if}
|
|
272
|
+
|
|
273
|
+
{#if groupedReferences.length > 0}
|
|
274
|
+
<section class="references">
|
|
275
|
+
<h2 class="references-title">References</h2>
|
|
276
|
+
<p class="references-subtitle">Where this concept appears in code and docs.</p>
|
|
277
|
+
|
|
278
|
+
{#each groupedReferences as group}
|
|
279
|
+
{@const repoUrl = repositoryUrls[group.repository] || null}
|
|
280
|
+
{@const allPaths = group.referenceGroups.flatMap(rg => rg.paths)}
|
|
281
|
+
{@const isExpanded = expandedGroups.has(group.repository)}
|
|
282
|
+
{@const visibleRefGroups = (() => {
|
|
283
|
+
if (isExpanded) return group.referenceGroups;
|
|
284
|
+
let pathCount = 0;
|
|
285
|
+
const visible = [];
|
|
286
|
+
for (const refGroup of group.referenceGroups) {
|
|
287
|
+
if (pathCount + refGroup.paths.length <= DEFAULT_VISIBLE_PATHS) {
|
|
288
|
+
visible.push(refGroup);
|
|
289
|
+
pathCount += refGroup.paths.length;
|
|
290
|
+
} else {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return visible;
|
|
295
|
+
})()}
|
|
296
|
+
{@const hiddenCount = allPaths.length - visibleRefGroups.reduce((sum, rg) => sum + rg.paths.length, 0)}
|
|
297
|
+
|
|
298
|
+
<div class="reference-group">
|
|
299
|
+
{#if repoUrl}
|
|
300
|
+
<a class="reference-repo-pill" href={repoUrl} target="_blank" rel="noopener noreferrer">
|
|
301
|
+
<span class="reference-repo-label">repo</span>
|
|
302
|
+
<span class="reference-repo-name">{group.repository}</span>
|
|
303
|
+
</a>
|
|
304
|
+
{:else}
|
|
305
|
+
<div class="reference-repo-pill reference-repo-pill--no-link">
|
|
306
|
+
<span class="reference-repo-label">repo</span>
|
|
307
|
+
<span class="reference-repo-name">{group.repository}</span>
|
|
308
|
+
</div>
|
|
309
|
+
{/if}
|
|
310
|
+
|
|
311
|
+
<ul class="reference-paths-list">
|
|
312
|
+
{#each visibleRefGroups as refGroup, refGroupIndex}
|
|
313
|
+
{#each refGroup.paths as pathEntry, pathIndex}
|
|
314
|
+
{@const path = pathEntry.path}
|
|
315
|
+
{@const url = getPathUrl(group.repository, path)}
|
|
316
|
+
{@const isWildcard = isWildcardPath(path)}
|
|
317
|
+
{@const isLastInRefGroup = pathIndex === refGroup.paths.length - 1}
|
|
318
|
+
{@const isFirstInRefGroup = pathIndex === 0}
|
|
319
|
+
{@const isFirstRefGroup = refGroupIndex === 0}
|
|
320
|
+
<li class="reference-path-item" class:reference-path-item--tight={!isLastInRefGroup} class:reference-path-item--spaced={isFirstInRefGroup && !isFirstRefGroup}>
|
|
321
|
+
{#if url}
|
|
322
|
+
<a class="reference-path-link sprig-link sprig-link--quiet" href={url} target="_blank" rel="noopener noreferrer">
|
|
323
|
+
<span class="reference-path-text">{path}</span>
|
|
324
|
+
{#if isWildcard}
|
|
325
|
+
<span class="reference-path-wildcard">(pattern)</span>
|
|
326
|
+
{/if}
|
|
327
|
+
</a>
|
|
328
|
+
{:else}
|
|
329
|
+
<span class="reference-path-text">{path}</span>
|
|
330
|
+
{#if isWildcard}
|
|
331
|
+
<span class="reference-path-wildcard">(pattern)</span>
|
|
332
|
+
{/if}
|
|
333
|
+
{/if}
|
|
334
|
+
{#if pathEntry.perPathDescribe?.normalized}
|
|
335
|
+
<p class="reference-description reference-description--per-path">{pathEntry.perPathDescribe.normalized}</p>
|
|
336
|
+
{/if}
|
|
337
|
+
</li>
|
|
338
|
+
{/each}
|
|
339
|
+
{#if refGroup.groupDescribe}
|
|
340
|
+
<li class="reference-path-item reference-path-item--describe">
|
|
341
|
+
<p class="reference-description reference-description--group">{refGroup.groupDescribe}</p>
|
|
342
|
+
</li>
|
|
343
|
+
{/if}
|
|
344
|
+
{/each}
|
|
345
|
+
</ul>
|
|
346
|
+
|
|
347
|
+
{#if hiddenCount > 0 && !isExpanded}
|
|
348
|
+
<button
|
|
349
|
+
class="reference-expand-button"
|
|
350
|
+
on:click={() => toggleGroup(group.repository)}
|
|
351
|
+
type="button"
|
|
352
|
+
>
|
|
353
|
+
+ {hiddenCount} more path{hiddenCount === 1 ? '' : 's'}
|
|
354
|
+
</button>
|
|
355
|
+
{/if}
|
|
356
|
+
{#if isExpanded && hiddenCount > 0}
|
|
357
|
+
<button
|
|
358
|
+
class="reference-expand-button"
|
|
359
|
+
on:click={() => toggleGroup(group.repository)}
|
|
360
|
+
type="button"
|
|
361
|
+
>
|
|
362
|
+
Show less
|
|
363
|
+
</button>
|
|
364
|
+
{/if}
|
|
365
|
+
</div>
|
|
366
|
+
{/each}
|
|
367
|
+
</section>
|
|
368
|
+
{/if}
|
|
369
|
+
|
|
370
|
+
{#if $universeGraph && $universeRootNode}
|
|
371
|
+
<FooterStatus graph={$universeGraph} root={$universeRootNode} />
|
|
372
|
+
{/if}
|
|
373
|
+
{:else}
|
|
374
|
+
<div class="loading">Loading…</div>
|
|
375
|
+
{/if}
|
|
376
|
+
|
|
377
|
+
<style>
|
|
378
|
+
.grid {
|
|
379
|
+
display: grid;
|
|
380
|
+
grid-template-columns: 1fr 350px;
|
|
381
|
+
gap: 64px;
|
|
382
|
+
align-items: start;
|
|
383
|
+
max-width: 1200px;
|
|
384
|
+
margin: 0 auto;
|
|
385
|
+
width: 100%;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.narrative {
|
|
389
|
+
max-width: 680px;
|
|
390
|
+
min-width: 0;
|
|
391
|
+
width: 100%;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.index {
|
|
395
|
+
min-width: 0;
|
|
396
|
+
width: 100%;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.relationships {
|
|
400
|
+
margin-top: 32px;
|
|
401
|
+
padding-top: 24px;
|
|
402
|
+
border-top: 1px solid var(--hairline);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.relationships-title {
|
|
406
|
+
font-family: var(--font-ui);
|
|
407
|
+
font-size: var(--sp-font-tiny);
|
|
408
|
+
letter-spacing: 0.02em;
|
|
409
|
+
text-transform: none;
|
|
410
|
+
color: var(--text-secondary);
|
|
411
|
+
margin: 0 0 0.75rem 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.relationships-list {
|
|
415
|
+
list-style: none;
|
|
416
|
+
padding: 0;
|
|
417
|
+
margin: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.relationship-item {
|
|
421
|
+
margin-bottom: 2rem;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.relationship-item:last-child {
|
|
425
|
+
margin-bottom: 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.relationship-link {
|
|
429
|
+
display: flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
gap: 8px;
|
|
432
|
+
padding: 4px 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.relationship-link.sprig-link {
|
|
436
|
+
text-decoration: none;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.relationship-link.sprig-link .relationship-name {
|
|
440
|
+
text-decoration: underline;
|
|
441
|
+
text-decoration-thickness: 1px;
|
|
442
|
+
text-underline-offset: 0.1875rem;
|
|
443
|
+
text-decoration-color: var(--sprig-link-underline);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.relationship-link.sprig-link:hover .relationship-name {
|
|
447
|
+
text-decoration-thickness: 1.5px;
|
|
448
|
+
text-decoration-color: var(--sprig-link-underline-hover);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.relationship-label {
|
|
452
|
+
font-family: var(--font-ui);
|
|
453
|
+
font-size: var(--sp-font-tiny);
|
|
454
|
+
color: var(--text-secondary);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.relationship-separator {
|
|
458
|
+
font-family: var(--font-ui);
|
|
459
|
+
font-size: var(--sp-font-tiny);
|
|
460
|
+
color: var(--text-tertiary);
|
|
461
|
+
opacity: 0.6;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.relationship-name {
|
|
465
|
+
font-family: var(--font-prose);
|
|
466
|
+
font-size: var(--sp-font-small);
|
|
467
|
+
color: inherit;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.relationship-kind {
|
|
471
|
+
font-family: var(--font-ui);
|
|
472
|
+
font-size: var(--sp-font-tiny);
|
|
473
|
+
color: var(--text-tertiary);
|
|
474
|
+
margin-left: auto;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.relationship-description {
|
|
478
|
+
margin: 8px 0 0 0;
|
|
479
|
+
font-family: var(--font-prose);
|
|
480
|
+
font-size: var(--sp-font-body);
|
|
481
|
+
color: var(--text-secondary);
|
|
482
|
+
line-height: 1.6;
|
|
483
|
+
max-width: 70ch;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.loading {
|
|
487
|
+
color: var(--text-tertiary);
|
|
488
|
+
padding: 24px 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.references {
|
|
492
|
+
margin-top: 32px;
|
|
493
|
+
padding-top: 24px;
|
|
494
|
+
border-top: 1px solid var(--hairline);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.references-title {
|
|
498
|
+
font-family: var(--font-ui);
|
|
499
|
+
font-size: var(--sp-font-tiny);
|
|
500
|
+
letter-spacing: 0.02em;
|
|
501
|
+
text-transform: none;
|
|
502
|
+
color: var(--text-secondary);
|
|
503
|
+
margin: 0 0 0.75rem 0;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.references-subtitle {
|
|
507
|
+
font-family: var(--font-ui);
|
|
508
|
+
font-size: var(--sp-font-tiny);
|
|
509
|
+
color: var(--text-tertiary);
|
|
510
|
+
margin: 0 0 1.5rem 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.reference-group {
|
|
514
|
+
margin-bottom: 2rem;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.reference-group:last-child {
|
|
518
|
+
margin-bottom: 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.reference-repo-pill {
|
|
522
|
+
display: inline-flex;
|
|
523
|
+
align-items: center;
|
|
524
|
+
gap: 6px;
|
|
525
|
+
padding: 4px 10px;
|
|
526
|
+
background: var(--card-bg);
|
|
527
|
+
border-radius: 6px;
|
|
528
|
+
text-decoration: none;
|
|
529
|
+
color: inherit;
|
|
530
|
+
margin-bottom: 0.75rem;
|
|
531
|
+
transition: opacity 0.2s;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.reference-repo-pill:hover {
|
|
535
|
+
opacity: 0.8;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.reference-repo-pill--no-link {
|
|
539
|
+
cursor: default;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.reference-repo-pill--no-link:hover {
|
|
543
|
+
opacity: 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.reference-repo-label {
|
|
547
|
+
font-family: var(--font-ui);
|
|
548
|
+
font-size: var(--sp-font-tiny);
|
|
549
|
+
letter-spacing: 0.05em;
|
|
550
|
+
text-transform: lowercase;
|
|
551
|
+
color: var(--text-tertiary);
|
|
552
|
+
font-weight: 400;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.reference-repo-name {
|
|
556
|
+
font-family: var(--font-ui);
|
|
557
|
+
font-size: var(--sp-font-tiny);
|
|
558
|
+
color: var(--text-secondary);
|
|
559
|
+
font-weight: 400;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.reference-paths-list {
|
|
563
|
+
list-style: none;
|
|
564
|
+
padding: 0;
|
|
565
|
+
margin: 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.reference-path-item {
|
|
569
|
+
margin-bottom: 0.75rem;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.reference-path-item:last-child {
|
|
573
|
+
margin-bottom: 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.reference-path-item--tight {
|
|
577
|
+
margin-bottom: 0.25rem;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.reference-path-item--spaced {
|
|
581
|
+
margin-top: 0.75rem;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.reference-path-item--describe {
|
|
585
|
+
margin-top: 0.5rem;
|
|
586
|
+
margin-bottom: 0.75rem;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.reference-path-link {
|
|
590
|
+
display: block;
|
|
591
|
+
padding: 4px 0;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.reference-path-text {
|
|
595
|
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
596
|
+
font-size: var(--sp-font-body);
|
|
597
|
+
color: inherit;
|
|
598
|
+
font-variant-numeric: tabular-nums;
|
|
599
|
+
letter-spacing: 0.01em;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.reference-path-wildcard {
|
|
603
|
+
font-family: var(--font-ui);
|
|
604
|
+
font-size: var(--sp-font-tiny);
|
|
605
|
+
color: var(--text-tertiary);
|
|
606
|
+
margin-left: 4px;
|
|
607
|
+
font-weight: 400;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.reference-description {
|
|
611
|
+
margin: 6px 0 0 0;
|
|
612
|
+
font-family: var(--font-prose);
|
|
613
|
+
font-size: var(--sp-font-body);
|
|
614
|
+
color: var(--text-secondary);
|
|
615
|
+
line-height: 1.5;
|
|
616
|
+
max-width: 70ch;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.reference-description--per-path {
|
|
620
|
+
margin-top: 6px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.reference-description--group {
|
|
624
|
+
margin: 0;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.reference-expand-button {
|
|
628
|
+
background: none;
|
|
629
|
+
border: none;
|
|
630
|
+
color: var(--text-tertiary);
|
|
631
|
+
font-family: var(--font-ui);
|
|
632
|
+
font-size: var(--sp-font-tiny);
|
|
633
|
+
padding: 4px 0;
|
|
634
|
+
margin-top: 0.5rem;
|
|
635
|
+
cursor: pointer;
|
|
636
|
+
text-decoration: underline;
|
|
637
|
+
text-underline-offset: 2px;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.reference-expand-button:hover {
|
|
641
|
+
opacity: 0.8;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
@media (max-width: 768px) {
|
|
645
|
+
.grid {
|
|
646
|
+
grid-template-columns: 1fr;
|
|
647
|
+
gap: 32px;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
@media (max-width: 480px) {
|
|
652
|
+
.grid {
|
|
653
|
+
gap: 32px;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
</style>
|
|
657
|
+
|