@sprig-and-prose/sprig-ui-csr 0.1.1 → 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 series pages
11
9
 
12
10
  /** @type {{ universe: string, series: string }} */
13
11
  export let params;
@@ -21,7 +19,7 @@
21
19
 
22
20
  /**
23
21
  * @typedef {{ raw?:string, normalized?:string, source?:any }} TextBlock
24
- * @typedef {{ repository:string, paths:string[]|Array<{path:string, describe?:TextBlock}>, describe?:TextBlock }} ReferenceBlock
22
+ * @typedef {{ id:string, name:string, kind?:string, title?:string, describe?:TextBlock, note?:TextBlock, urls?:string[], paths?:string[], repositoryRef?:string }} ReferenceModel
25
23
  */
26
24
 
27
25
  $: subtitle = (() => {
@@ -52,133 +50,50 @@
52
50
  : `A series in ${params.universe}`;
53
51
  })();
54
52
 
53
+ $: referenceItems = ($currentSeries?.references || [])
54
+ .map((id) => $universeGraph?.references?.[id])
55
+ .filter(Boolean);
56
+
55
57
  /**
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}>}
58
+ * @param {string} url
59
+ * @param {string | undefined} label
59
60
  */
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
- })();
61
+ function makeLinkLabel(url, label) {
62
+ return label && label.trim().length > 0 ? label : url;
63
+ }
113
64
 
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}`;
65
+ /**
66
+ * @param {ReferenceModel} ref
67
+ */
68
+ function getRepositoryForReference(ref) {
69
+ if (!ref.repositoryRef || !$universeGraph?.repositories) return null;
70
+ return $universeGraph.repositories[ref.repositoryRef] || null;
71
+ }
72
+
73
+ $: referenceGroups = (() => {
74
+ /** @type {Array<{ repo: any | null, refs: ReferenceModel[] }>} */
75
+ const groups = [];
76
+ const byRepo = new Map();
77
+
78
+ for (const ref of referenceItems) {
79
+ if (!ref) continue;
80
+ if (ref.repositoryRef) {
81
+ let group = byRepo.get(ref.repositoryRef);
82
+ if (!group) {
83
+ const repo = getRepositoryForReference(ref);
84
+ group = { repo, refs: [] };
85
+ byRepo.set(ref.repositoryRef, group);
86
+ groups.push(group);
129
87
  }
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}`;
88
+ group.refs.push(ref);
150
89
  } else {
151
- return null;
90
+ groups.push({ repo: null, refs: [ref] });
152
91
  }
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
92
  }
166
- return null;
167
- }
168
93
 
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
- }
94
+ return groups;
95
+ })();
180
96
 
181
- const DEFAULT_VISIBLE_PATHS = 3;
182
97
 
183
98
  /**
184
99
  * Get the anthology node for a series, if it belongs to one
@@ -270,100 +185,68 @@
270
185
  </section>
271
186
  {/if}
272
187
 
273
- {#if groupedReferences.length > 0}
188
+ {#if referenceItems.length > 0}
274
189
  <section class="references">
275
190
  <h2 class="references-title">References</h2>
276
191
  <p class="references-subtitle">Where this concept appears in code and docs.</p>
277
192
 
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>
193
+ <ul class="reference-list">
194
+ {#each referenceGroups as group}
195
+ <li class="reference-item">
196
+ {#if group.repo}
197
+ <a
198
+ class="reference-repo-pill sprig-link"
199
+ href={group.repo.url}
200
+ target="_blank"
201
+ rel="noopener noreferrer"
202
+ >
203
+ {group.repo.title || group.repo.name}
204
+ </a>
205
+ {/if}
206
+ <ul class="reference-group-list">
207
+ {#each group.refs as ref}
208
+ <li class="reference-group-item">
209
+ {#if ref.urls && ref.urls.length > 0}
210
+ {#if ref.urls.length === 1}
211
+ {@const label = makeLinkLabel(ref.urls[0], ref.paths?.[0])}
212
+ <div class="reference-row">
213
+ <a class="reference-path-link sprig-link" href={ref.urls[0]} target="_blank" rel="noopener noreferrer">
214
+ {label}
215
+ </a>
216
+ {#if ref.kind}
217
+ <span class="reference-kind">{ref.kind}</span>
218
+ {/if}
219
+ </div>
220
+ {:else}
221
+ <ul class="reference-links-list">
222
+ {#each ref.urls as url, index}
223
+ {@const label = makeLinkLabel(url, ref.paths?.[index])}
224
+ <li class="reference-links-item">
225
+ <div class="reference-row">
226
+ <a class="reference-path-link sprig-link" href={url} target="_blank" rel="noopener noreferrer">
227
+ {label}
228
+ </a>
229
+ {#if ref.kind && index === 0}
230
+ <span class="reference-kind">{ref.kind}</span>
231
+ {/if}
232
+ </div>
233
+ </li>
234
+ {/each}
235
+ </ul>
332
236
  {/if}
333
237
  {/if}
334
- {#if pathEntry.perPathDescribe?.normalized}
335
- <p class="reference-description reference-description--per-path">{pathEntry.perPathDescribe.normalized}</p>
238
+ {#if ref.describe?.normalized}
239
+ <p class="reference-description">{ref.describe.normalized}</p>
240
+ {/if}
241
+ {#if ref.note?.normalized}
242
+ <p class="reference-note">{ref.note.normalized}</p>
336
243
  {/if}
337
244
  </li>
338
245
  {/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}
246
+ </ul>
247
+ </li>
248
+ {/each}
249
+ </ul>
367
250
  </section>
368
251
  {/if}
369
252
 
@@ -450,13 +333,13 @@
450
333
 
451
334
  .relationship-label {
452
335
  font-family: var(--font-ui);
453
- font-size: var(--sp-font-tiny);
336
+ font-size: var(--sp-font-small);
454
337
  color: var(--text-secondary);
455
338
  }
456
339
 
457
340
  .relationship-separator {
458
341
  font-family: var(--font-ui);
459
- font-size: var(--sp-font-tiny);
342
+ font-size: var(--sp-font-small);
460
343
  color: var(--text-tertiary);
461
344
  opacity: 0.6;
462
345
  }
@@ -510,101 +393,59 @@
510
393
  margin: 0 0 1.5rem 0;
511
394
  }
512
395
 
513
- .reference-group {
396
+ .reference-list {
397
+ list-style: none;
398
+ padding: 0;
399
+ margin: 0;
400
+ }
401
+
402
+ .reference-item {
514
403
  margin-bottom: 2rem;
515
404
  }
516
405
 
517
- .reference-group:last-child {
406
+ .reference-item:last-child {
518
407
  margin-bottom: 0;
519
408
  }
520
409
 
410
+
521
411
  .reference-repo-pill {
522
412
  display: inline-flex;
523
413
  align-items: center;
524
414
  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 {
415
+ padding: 2px 10px;
416
+ border-radius: 999px;
547
417
  font-family: var(--font-ui);
548
- font-size: var(--sp-font-tiny);
549
- letter-spacing: 0.05em;
418
+ font-size: var(--sp-font-small);
550
419
  text-transform: lowercase;
551
420
  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;
421
+ border: 1px solid var(--hairline);
422
+ margin-bottom: 1rem;
423
+ text-decoration: none;
587
424
  }
588
425
 
589
426
  .reference-path-link {
590
427
  display: block;
591
- padding: 4px 0;
592
- }
593
-
594
- .reference-path-text {
428
+ margin-top: 2px;
595
429
  font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
596
430
  font-size: var(--sp-font-body);
597
431
  color: inherit;
598
- font-variant-numeric: tabular-nums;
599
- letter-spacing: 0.01em;
432
+ text-decoration: underline;
433
+ text-decoration-thickness: 1px;
434
+ text-underline-offset: 0.1875rem;
435
+ text-decoration-color: var(--sprig-link-underline);
600
436
  }
601
437
 
602
- .reference-path-wildcard {
438
+ .reference-row {
439
+ display: flex;
440
+ align-items: baseline;
441
+ gap: 8px;
442
+ }
443
+
444
+ .reference-kind {
445
+ margin-left: auto;
603
446
  font-family: var(--font-ui);
604
- font-size: var(--sp-font-tiny);
447
+ font-size: var(--sp-font-small);
605
448
  color: var(--text-tertiary);
606
- margin-left: 4px;
607
- font-weight: 400;
608
449
  }
609
450
 
610
451
  .reference-description {
@@ -616,29 +457,37 @@
616
457
  max-width: 70ch;
617
458
  }
618
459
 
619
- .reference-description--per-path {
620
- margin-top: 6px;
460
+ .reference-note {
461
+ margin: 6px 0 0 0;
462
+ font-family: var(--font-prose);
463
+ font-size: var(--sp-font-body);
464
+ color: var(--text-secondary);
465
+ line-height: 1.5;
466
+ max-width: 70ch;
467
+ }
468
+
469
+ .reference-links-list {
470
+ list-style: none;
471
+ padding: 0;
472
+ margin: 8px 0 0 0;
621
473
  }
622
474
 
623
- .reference-description--group {
475
+ .reference-group-list {
476
+ list-style: none;
477
+ padding: 0;
624
478
  margin: 0;
625
479
  }
626
480
 
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;
481
+ .reference-group-item + .reference-group-item {
482
+ margin-top: 2rem;
638
483
  }
639
484
 
640
- .reference-expand-button:hover {
641
- opacity: 0.8;
485
+ .reference-links-item {
486
+ margin-bottom: 6px;
487
+ }
488
+
489
+ .reference-links-item:last-child {
490
+ margin-bottom: 0;
642
491
  }
643
492
 
644
493
  @media (max-width: 768px) {
@@ -655,3 +504,4 @@
655
504
  }
656
505
  </style>
657
506
 
507
+
@@ -1,9 +0,0 @@
1
- /**
2
- * Checks if a path contains a wildcard pattern
3
- * @param {string} path - File path to check
4
- * @returns {boolean} - True if path contains '*'
5
- */
6
- export function isWildcardPath(path) {
7
- return typeof path === 'string' && path.includes('*');
8
- }
9
-
@@ -1,65 +0,0 @@
1
- import { get } from 'svelte/store';
2
- import { universeGraph } from '../data/universeStore.js';
3
- import { isWildcardPath } from './isWildcardPath.js';
4
-
5
- /**
6
- * Builds a URL for a file path in a repository
7
- * @param {string} repository - Repository key (e.g., "amaranthine")
8
- * @param {string} path - File path (e.g., "/backends/api/src/routers/players.js" or "/data/actions/*.yaml")
9
- * @returns {string | null} - Full URL to the file/folder, or null if repository is unconfigured
10
- */
11
- export function linkForPath(repository, path) {
12
- const graph = get(universeGraph);
13
- if (!graph?.repositories) {
14
- return null;
15
- }
16
-
17
- const repoConfig = graph.repositories[repository];
18
- if (!repoConfig) {
19
- return null;
20
- }
21
-
22
- const { kind, options } = repoConfig;
23
- const defaultBranch = options?.defaultBranch || 'main';
24
-
25
- // Handle GitHub repositories
26
- if (kind === 'sprig-repository-github') {
27
- // Prefer url if available, otherwise build from owner/repo
28
- let baseUrl;
29
- if (options?.url) {
30
- baseUrl = options.url;
31
- } else {
32
- const owner = options?.owner;
33
- const repo = options?.repo;
34
- if (!owner || !repo) {
35
- return null;
36
- }
37
- baseUrl = `https://github.com/${owner}/${repo}`;
38
- }
39
-
40
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
41
-
42
- // Ensure path begins with /
43
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
44
-
45
- // Handle wildcard paths - link to folder view
46
- if (isWildcardPath(path)) {
47
- // Derive folder by taking everything up to the last '/'
48
- // e.g., "/data/actions/*.yaml" -> "/data/actions"
49
- const lastSlashIndex = normalizedPath.lastIndexOf('/');
50
- if (lastSlashIndex > 0) {
51
- const folderPath = normalizedPath.slice(0, lastSlashIndex);
52
- // Build GitHub tree URL: baseUrl/tree/branch/folder
53
- return `${normalizedBaseUrl}/tree/${defaultBranch}${folderPath}`;
54
- }
55
- // Fallback: if no slash, link to repo root
56
- return `${normalizedBaseUrl}/tree/${defaultBranch}`;
57
- }
58
-
59
- // Build GitHub blob URL: baseUrl/blob/branch/path
60
- return `${normalizedBaseUrl}/blob/${defaultBranch}${normalizedPath}`;
61
- }
62
-
63
- return null;
64
- }
65
-
@@ -1,42 +0,0 @@
1
- import { get } from 'svelte/store';
2
- import { universeGraph } from '../data/universeStore.js';
3
-
4
- /**
5
- * Builds a URL for a repository root
6
- * @param {string} repository - Repository key (e.g., "amaranthine")
7
- * @returns {string | null} - Full URL to the repository root, or null if repository is unconfigured
8
- */
9
- export function linkForRepository(repository) {
10
- const graph = get(universeGraph);
11
- if (!graph?.repositories) {
12
- return null;
13
- }
14
-
15
- const repoConfig = graph.repositories[repository];
16
- if (!repoConfig) {
17
- return null;
18
- }
19
-
20
- const { kind, options } = repoConfig;
21
-
22
- // Handle GitHub repositories
23
- if (kind === 'sprig-repository-github') {
24
- // Prefer url if available, otherwise build from owner/repo
25
- if (options?.url) {
26
- const baseUrl = options.url;
27
- // Remove trailing slash if present
28
- return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
29
- }
30
-
31
- const owner = options?.owner;
32
- const repo = options?.repo;
33
- if (owner && repo) {
34
- const baseUrl = `https://github.com/${owner}/${repo}`;
35
- // Remove trailing slash if present
36
- return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
37
- }
38
- }
39
-
40
- return null;
41
- }
42
-