@sprig-and-prose/sprig-ui-csr 0.1.2 → 0.2.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.
@@ -5,9 +5,7 @@
5
5
  import ContentsCard from '../lib/components/ContentsCard.svelte';
6
6
  import FooterStatus from '../lib/components/FooterStatus.svelte';
7
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';
8
+ // Reference routing stays internal for concept pages
11
9
 
12
10
  /** @type {{ universe: string, id: string }} */
13
11
  export let params;
@@ -17,7 +15,7 @@
17
15
 
18
16
  /**
19
17
  * @typedef {{ raw?:string, normalized?:string, source?:any }} TextBlock
20
- * @typedef {{ repository:string, paths:string[]|Array<{path:string, describe?:TextBlock}>, describe?:TextBlock }} ReferenceBlock
18
+ * @typedef {{ id:string, name:string, kind?:string, title?:string, describe?:TextBlock, note?:TextBlock, urls?:string[], paths?:string[], repositoryRef?:string }} ReferenceModel
21
19
  */
22
20
 
23
21
  // Decode the node ID from URL
@@ -84,133 +82,50 @@
84
82
  .filter((node) => node.kind === 'chapter' && node.id !== currentNode.id);
85
83
  })();
86
84
 
85
+ $: referenceItems = (currentNode?.references || [])
86
+ .map((id) => $universeGraph?.references?.[id])
87
+ .filter(Boolean);
88
+
87
89
  /**
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}>}
90
+ * @param {string} url
91
+ * @param {string | undefined} label
91
92
  */
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
- })();
93
+ function makeLinkLabel(url, label) {
94
+ return label && label.trim().length > 0 ? label : url;
95
+ }
145
96
 
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}`;
97
+ /**
98
+ * @param {ReferenceModel} ref
99
+ */
100
+ function getRepositoryForReference(ref) {
101
+ if (!ref.repositoryRef || !$universeGraph?.repositories) return null;
102
+ return $universeGraph.repositories[ref.repositoryRef] || null;
103
+ }
104
+
105
+ $: referenceGroups = (() => {
106
+ /** @type {Array<{ repo: any | null, refs: ReferenceModel[] }>} */
107
+ const groups = [];
108
+ const byRepo = new Map();
109
+
110
+ for (const ref of referenceItems) {
111
+ if (!ref) continue;
112
+ if (ref.repositoryRef) {
113
+ let group = byRepo.get(ref.repositoryRef);
114
+ if (!group) {
115
+ const repo = getRepositoryForReference(ref);
116
+ group = { repo, refs: [] };
117
+ byRepo.set(ref.repositoryRef, group);
118
+ groups.push(group);
161
119
  }
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}`;
120
+ group.refs.push(ref);
182
121
  } 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}`;
122
+ groups.push({ repo: null, refs: [ref] });
195
123
  }
196
- return `${normalizedBaseUrl}/blob/${defaultBranch}${normalizedPath}`;
197
124
  }
198
- return null;
199
- }
200
125
 
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
- }
126
+ return groups;
127
+ })();
212
128
 
213
- const DEFAULT_VISIBLE_PATHS = 3;
214
129
 
215
130
  $: subtitle = (() => {
216
131
  if (showContextLine) {
@@ -321,100 +236,68 @@
321
236
  </section>
322
237
  {/if}
323
238
 
324
- {#if groupedReferences.length > 0}
239
+ {#if referenceItems.length > 0}
325
240
  <section class="references">
326
241
  <h2 class="references-title">References</h2>
327
242
  <p class="references-subtitle">Where this concept appears in code and docs.</p>
328
243
 
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>
244
+ <ul class="reference-list">
245
+ {#each referenceGroups as group}
246
+ <li class="reference-item">
247
+ {#if group.repo}
248
+ <a
249
+ class="reference-repo-pill sprig-link"
250
+ href={group.repo.url}
251
+ target="_blank"
252
+ rel="noopener noreferrer"
253
+ >
254
+ {group.repo.title || group.repo.name}
255
+ </a>
256
+ {/if}
257
+ <ul class="reference-group-list">
258
+ {#each group.refs as ref}
259
+ <li class="reference-group-item">
260
+ {#if ref.urls && ref.urls.length > 0}
261
+ {#if ref.urls.length === 1}
262
+ {@const label = makeLinkLabel(ref.urls[0], ref.paths?.[0])}
263
+ <div class="reference-row">
264
+ <a class="reference-path-link sprig-link" href={ref.urls[0]} target="_blank" rel="noopener noreferrer">
265
+ {label}
266
+ </a>
267
+ {#if ref.kind}
268
+ <span class="reference-kind">{ref.kind}</span>
269
+ {/if}
270
+ </div>
271
+ {:else}
272
+ <ul class="reference-links-list">
273
+ {#each ref.urls as url, index}
274
+ {@const label = makeLinkLabel(url, ref.paths?.[index])}
275
+ <li class="reference-links-item">
276
+ <div class="reference-row">
277
+ <a class="reference-path-link sprig-link" href={url} target="_blank" rel="noopener noreferrer">
278
+ {label}
279
+ </a>
280
+ {#if ref.kind && index === 0}
281
+ <span class="reference-kind">{ref.kind}</span>
282
+ {/if}
283
+ </div>
284
+ </li>
285
+ {/each}
286
+ </ul>
383
287
  {/if}
384
288
  {/if}
385
- {#if pathEntry.perPathDescribe?.normalized}
386
- <p class="reference-description reference-description--per-path">{pathEntry.perPathDescribe.normalized}</p>
289
+ {#if ref.describe?.normalized}
290
+ <p class="reference-description">{ref.describe.normalized}</p>
291
+ {/if}
292
+ {#if ref.note?.normalized}
293
+ <p class="reference-note">{ref.note.normalized}</p>
387
294
  {/if}
388
295
  </li>
389
296
  {/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}
297
+ </ul>
298
+ </li>
299
+ {/each}
300
+ </ul>
418
301
  </section>
419
302
  {/if}
420
303
 
@@ -577,13 +460,13 @@
577
460
 
578
461
  .relationship-label {
579
462
  font-family: var(--font-ui);
580
- font-size: var(--sp-font-tiny);
463
+ font-size: var(--sp-font-small);
581
464
  color: var(--text-secondary);
582
465
  }
583
466
 
584
467
  .relationship-separator {
585
468
  font-family: var(--font-ui);
586
- font-size: var(--sp-font-tiny);
469
+ font-size: var(--sp-font-small);
587
470
  color: var(--text-tertiary);
588
471
  opacity: 0.6;
589
472
  }
@@ -637,101 +520,59 @@
637
520
  margin: 0 0 1.5rem 0;
638
521
  }
639
522
 
640
- .reference-group {
523
+ .reference-list {
524
+ list-style: none;
525
+ padding: 0;
526
+ margin: 0;
527
+ }
528
+
529
+ .reference-item {
641
530
  margin-bottom: 2rem;
642
531
  }
643
532
 
644
- .reference-group:last-child {
533
+ .reference-item:last-child {
645
534
  margin-bottom: 0;
646
535
  }
647
536
 
537
+
648
538
  .reference-repo-pill {
649
539
  display: inline-flex;
650
540
  align-items: center;
651
541
  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 {
542
+ padding: 2px 10px;
543
+ border-radius: 999px;
674
544
  font-family: var(--font-ui);
675
- font-size: var(--sp-font-tiny);
676
- letter-spacing: 0.05em;
545
+ font-size: var(--sp-font-small);
677
546
  text-transform: lowercase;
678
547
  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;
548
+ border: 1px solid var(--hairline);
549
+ margin-bottom: 1rem;
550
+ text-decoration: none;
714
551
  }
715
552
 
716
553
  .reference-path-link {
717
554
  display: block;
718
- padding: 4px 0;
719
- }
720
-
721
- .reference-path-text {
555
+ margin-top: 2px;
722
556
  font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
723
557
  font-size: var(--sp-font-body);
724
558
  color: inherit;
725
- font-variant-numeric: tabular-nums;
726
- letter-spacing: 0.01em;
559
+ text-decoration: underline;
560
+ text-decoration-thickness: 1px;
561
+ text-underline-offset: 0.1875rem;
562
+ text-decoration-color: var(--sprig-link-underline);
563
+ }
564
+
565
+ .reference-row {
566
+ display: flex;
567
+ align-items: baseline;
568
+ gap: 8px;
727
569
  }
728
570
 
729
- .reference-path-wildcard {
571
+ .reference-kind {
572
+ margin-left: auto;
730
573
  font-family: var(--font-ui);
731
- font-size: var(--sp-font-tiny);
574
+ font-size: var(--sp-font-small);
732
575
  color: var(--text-tertiary);
733
- margin-left: 4px;
734
- font-weight: 400;
735
576
  }
736
577
 
737
578
  .reference-description {
@@ -743,29 +584,37 @@
743
584
  max-width: 70ch;
744
585
  }
745
586
 
746
- .reference-description--per-path {
747
- margin-top: 6px;
587
+ .reference-note {
588
+ margin: 6px 0 0 0;
589
+ font-family: var(--font-prose);
590
+ font-size: var(--sp-font-body);
591
+ color: var(--text-secondary);
592
+ line-height: 1.5;
593
+ max-width: 70ch;
594
+ }
595
+
596
+ .reference-links-list {
597
+ list-style: none;
598
+ padding: 0;
599
+ margin: 8px 0 0 0;
748
600
  }
749
601
 
750
- .reference-description--group {
602
+ .reference-group-list {
603
+ list-style: none;
604
+ padding: 0;
751
605
  margin: 0;
752
606
  }
753
607
 
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;
608
+ .reference-group-item + .reference-group-item {
609
+ margin-top: 2rem;
765
610
  }
766
611
 
767
- .reference-expand-button:hover {
768
- opacity: 0.8;
612
+ .reference-links-item {
613
+ margin-bottom: 6px;
614
+ }
615
+
616
+ .reference-links-item:last-child {
617
+ margin-bottom: 0;
769
618
  }
770
619
 
771
620
  .documentation {
@@ -871,3 +720,4 @@
871
720
  }
872
721
  </style>
873
722
 
723
+
@@ -0,0 +1,104 @@
1
+ <script>
2
+ import { universeGraph, currentUniverseName, universeRootNode } from '../lib/data/universeStore.js';
3
+ import PageHeader from '../lib/components/PageHeader.svelte';
4
+ import Prose from '../lib/components/Prose.svelte';
5
+ import FooterStatus from '../lib/components/FooterStatus.svelte';
6
+
7
+ /** @type {{ universe: string, id: string }} */
8
+ export let params;
9
+
10
+ $: currentUniverseName.set(params.universe);
11
+ $: referenceId = decodeURIComponent(params.id);
12
+ $: reference = $universeGraph?.references?.[referenceId];
13
+
14
+ /**
15
+ * @param {string} url
16
+ * @param {string | undefined} label
17
+ */
18
+ function makeLinkLabel(url, label) {
19
+ return label && label.trim().length > 0 ? label : url;
20
+ }
21
+ </script>
22
+
23
+ {#if reference}
24
+ {@const displayTitle = reference.title || reference.name}
25
+ <PageHeader title={displayTitle} subtitle={reference.kind || ''} />
26
+
27
+ <section class="reference-section">
28
+ {#if reference.describe}
29
+ <Prose textBlock={reference.describe} />
30
+ {/if}
31
+ </section>
32
+
33
+ {#if reference.note}
34
+ <section class="reference-section">
35
+ <h2 class="reference-section-title">Note</h2>
36
+ <Prose textBlock={reference.note} />
37
+ </section>
38
+ {/if}
39
+
40
+ <section class="reference-section">
41
+ <h2 class="reference-section-title">Links</h2>
42
+ {#if reference.urls.length === 1}
43
+ {@const label = makeLinkLabel(reference.urls[0], reference.paths?.[0])}
44
+ <a class="reference-link sprig-link" href={reference.urls[0]} target="_blank" rel="noopener noreferrer">
45
+ {label}
46
+ </a>
47
+ {:else}
48
+ <ul class="reference-links-list">
49
+ {#each reference.urls as url, index}
50
+ {@const label = makeLinkLabel(url, reference.paths?.[index])}
51
+ <li class="reference-links-item">
52
+ <a class="reference-link sprig-link" href={url} target="_blank" rel="noopener noreferrer">
53
+ {label}
54
+ </a>
55
+ </li>
56
+ {/each}
57
+ </ul>
58
+ {/if}
59
+ </section>
60
+
61
+ <FooterStatus graph={$universeGraph} root={$universeRootNode} />
62
+ {:else}
63
+ <div class="loading">Reference not found.</div>
64
+ {/if}
65
+
66
+ <style>
67
+ .reference-section {
68
+ margin-top: 24px;
69
+ }
70
+
71
+ .reference-section-title {
72
+ font-family: var(--font-ui);
73
+ font-size: var(--sp-font-tiny);
74
+ color: var(--text-secondary);
75
+ margin: 0 0 0.75rem 0;
76
+ }
77
+
78
+ .reference-links-list {
79
+ list-style: none;
80
+ padding: 0;
81
+ margin: 0;
82
+ }
83
+
84
+ .reference-links-item {
85
+ margin-bottom: 0.5rem;
86
+ }
87
+
88
+ .reference-links-item:last-child {
89
+ margin-bottom: 0;
90
+ }
91
+
92
+ .reference-link {
93
+ display: inline-block;
94
+ font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
95
+ font-size: var(--sp-font-body);
96
+ }
97
+
98
+ .loading {
99
+ color: var(--text-tertiary);
100
+ padding: 24px 0;
101
+ }
102
+ </style>
103
+
104
+