@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.
@@ -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
+