@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprig-and-prose/sprig-ui-csr",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sprig-ui-csr": "./src/cli.js"
@@ -26,4 +26,3 @@
26
26
  "vite": "^6.0.0"
27
27
  }
28
28
  }
29
-
package/src/App.svelte CHANGED
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { onMount, onDestroy } from 'svelte';
3
- import { loadUniverseGraph } from './lib/data/universeStore.js';
3
+ import { loadUniverseGraph, autoSelectFirstUniverse, universeGraph } from './lib/data/universeStore.js';
4
4
  import { getCurrentRoute, navigate } from './lib/router.js';
5
5
  import { theme } from './lib/stores/theme.js';
6
6
  import { describeRenderMode } from './lib/stores/describeRenderMode.js';
@@ -9,11 +9,32 @@
9
9
  import SeriesPage from './pages/SeriesPage.svelte';
10
10
  import AnthologyPage from './pages/AnthologyPage.svelte';
11
11
  import ConceptPage from './pages/ConceptPage.svelte';
12
+ import ReferencePage from './pages/ReferencePage.svelte';
12
13
 
13
14
  let loading = true;
15
+ /** @type {string | null} */
14
16
  let error = null;
15
17
  let currentRoute = getCurrentRoute();
16
18
 
19
+ $: seriesParams = currentRoute?.route === 'series'
20
+ ? /** @type {{ universe: string, series: string }} */ (currentRoute.params)
21
+ : null;
22
+ $: anthologyParams = currentRoute?.route === 'anthology'
23
+ ? /** @type {{ universe: string, anthology: string }} */ (currentRoute.params)
24
+ : null;
25
+ $: conceptParams = currentRoute?.route === 'concept'
26
+ ? /** @type {{ universe: string, id: string }} */ (currentRoute.params)
27
+ : null;
28
+ $: referenceParams = currentRoute?.route === 'reference'
29
+ ? /** @type {{ universe: string, id: string }} */ (currentRoute.params)
30
+ : null;
31
+
32
+ // Reactively auto-select first universe when on home route and graph is loaded
33
+ // This runs whenever universeGraph or currentRoute changes
34
+ $: if ($universeGraph && currentRoute?.route === 'home') {
35
+ autoSelectFirstUniverse($universeGraph);
36
+ }
37
+
17
38
  function toggleTheme() {
18
39
  theme.update((t) => {
19
40
  if (t === 'light') return 'dark';
@@ -35,11 +56,15 @@
35
56
  loading = true;
36
57
  error = null;
37
58
  const { describeRenderMode: mode } = await loadUniverseGraph('/api/manifest');
59
+
60
+ // Auto-selection is handled reactively above
61
+
38
62
  if (mode === 'lists' || mode === 'plain') {
39
63
  describeRenderMode.set(mode);
40
64
  }
41
65
  } catch (err) {
42
- error = err.message || 'Failed to load manifest';
66
+ const message = err instanceof Error ? err.message : 'Failed to load manifest';
67
+ error = message;
43
68
  console.error('Error loading manifest:', err);
44
69
  } finally {
45
70
  loading = false;
@@ -48,10 +73,15 @@
48
73
 
49
74
  function handleRouteChange() {
50
75
  currentRoute = getCurrentRoute();
76
+ // Auto-selection will happen reactively when route changes
51
77
  }
52
78
 
53
79
  // Intercept link clicks for client-side navigation
80
+ /**
81
+ * @param {MouseEvent} event
82
+ */
54
83
  function handleLinkClick(event) {
84
+ if (!(event.target instanceof HTMLElement)) return;
55
85
  const link = event.target.closest('a');
56
86
  if (!link) return;
57
87
 
@@ -78,6 +108,7 @@
78
108
  document.addEventListener('click', handleLinkClick);
79
109
 
80
110
  // Set up Server-Sent Events for manifest updates
111
+ /** @type {EventSource | null} */
81
112
  let eventSource = null;
82
113
 
83
114
  onMount(() => {
@@ -139,12 +170,14 @@
139
170
  {:else if currentRoute}
140
171
  {#if currentRoute.route === 'home'}
141
172
  <HomePage />
142
- {:else if currentRoute.route === 'series'}
143
- <SeriesPage params={currentRoute.params} />
144
- {:else if currentRoute.route === 'anthology'}
145
- <AnthologyPage params={currentRoute.params} />
146
- {:else if currentRoute.route === 'concept'}
147
- <ConceptPage params={currentRoute.params} />
173
+ {:else if currentRoute.route === 'series' && seriesParams}
174
+ <SeriesPage params={seriesParams} />
175
+ {:else if currentRoute.route === 'anthology' && anthologyParams}
176
+ <AnthologyPage params={anthologyParams} />
177
+ {:else if currentRoute.route === 'concept' && conceptParams}
178
+ <ConceptPage params={conceptParams} />
179
+ {:else if currentRoute.route === 'reference' && referenceParams}
180
+ <ReferencePage params={referenceParams} />
148
181
  {:else}
149
182
  <div class="error">Unknown route: {currentRoute.route}</div>
150
183
  {/if}
@@ -8,7 +8,7 @@
8
8
  /**
9
9
  * @typedef {import('../data/universeStore.js').UniverseGraph} UniverseGraph
10
10
  * @typedef {import('../data/universeStore.js').NodeModel} NodeModel
11
- * @typedef {{ node: NodeModel, name: string, kind: string, id: string, route: string, context: string | null }} SearchItem
11
+ * @typedef {{ node: NodeModel | any, name: string, kind: string, id: string, route: string, context: string | null }} SearchItem
12
12
  */
13
13
 
14
14
  /** @type {HTMLInputElement | null} */
@@ -56,6 +56,32 @@
56
56
  return null;
57
57
  }
58
58
 
59
+ if (node.kind === 'concept') {
60
+ if (!node.parent) return null;
61
+
62
+ const ancestors = getAncestorChain(graph, node);
63
+ const parent = graph.nodes[node.parent];
64
+
65
+ if (!parent) return null;
66
+
67
+ // Check if concept is in a book or chapter
68
+ const book = ancestors.find(a => a.kind === 'book');
69
+ const series = ancestors.find(a => a.kind === 'series');
70
+
71
+ if (book && series) {
72
+ return `in ${getDisplayTitle(book)} (in ${getDisplayTitle(series)})`;
73
+ } else if (book) {
74
+ return `in ${getDisplayTitle(book)}`;
75
+ } else if (series) {
76
+ return `in ${getDisplayTitle(series)}`;
77
+ } else if (parent.kind === 'series') {
78
+ return `in ${getDisplayTitle(parent)}`;
79
+ } else if (parent.kind === 'anthology') {
80
+ return `in ${getDisplayTitle(parent)}`;
81
+ }
82
+ return null;
83
+ }
84
+
59
85
  return null;
60
86
  }
61
87
 
@@ -68,7 +94,7 @@
68
94
  if (!graph || !graph.nodes) return [];
69
95
 
70
96
  const items = [];
71
- const searchableKinds = ['series', 'book', 'chapter'];
97
+ const searchableKinds = ['series', 'book', 'chapter', 'concept'];
72
98
 
73
99
  for (const node of Object.values(graph.nodes)) {
74
100
  if (!node || !searchableKinds.includes(node.kind)) continue;
@@ -86,6 +112,22 @@
86
112
  });
87
113
  }
88
114
 
115
+ if (graph.references) {
116
+ for (const ref of Object.values(graph.references)) {
117
+ if (!ref || !ref.id) continue;
118
+ const universeName = ref.id.split(':')[0];
119
+ const route = `/universes/${universeName}/reference/${encodeURIComponent(ref.id)}`;
120
+ items.push({
121
+ node: ref,
122
+ name: ref.title || ref.name,
123
+ kind: 'reference',
124
+ id: ref.id,
125
+ route,
126
+ context: null,
127
+ });
128
+ }
129
+ }
130
+
89
131
  return items;
90
132
  }
91
133
 
@@ -259,7 +301,7 @@
259
301
  <input
260
302
  bind:this={searchInput}
261
303
  type="text"
262
- placeholder="Search Series, Books, Chapters… (Press / to focus)"
304
+ placeholder="Search Series, Books, Chapters, Concepts… (Press / to focus)"
263
305
  bind:value={query}
264
306
  class="search-input"
265
307
  role="combobox"
@@ -1,18 +1,19 @@
1
- import { writable, derived } from 'svelte/store';
1
+ import { writable, derived, get } from 'svelte/store';
2
2
 
3
3
  /**
4
4
  * @typedef {{ line:number, col:number, offset:number }} Pos
5
5
  * @typedef {{ file:string, start:Pos, end:Pos }} SourceSpan
6
6
  * @typedef {{ raw:string, normalized?:string, source:SourceSpan }} TextBlock
7
- * @typedef {{ repository:string, paths:string[], describe?:TextBlock, source:SourceSpan }} ReferenceBlock
8
- * @typedef {{ id:string, kind:'universe'|'anthology'|'series'|'book'|'chapter'|'concept'|'relates', name:string, title?:string, parent?:string, children:string[], container?:string, describe?:TextBlock, endpoints?:string[], from?:Record<string, any>, references?:ReferenceBlock[] }} NodeModel
7
+ * @typedef {{ id:string, name:string, url:string, title?:string, describe?:TextBlock, note?:TextBlock }} RepositoryModel
8
+ * @typedef {{ id:string, name:string, kind?:string, title?:string, describe?:TextBlock, note?:TextBlock, urls:string[], repositoryRef?:string, paths?:string[] }} ReferenceModel
9
+ * @typedef {{ id:string, kind:'universe'|'anthology'|'series'|'book'|'chapter'|'concept'|'relates', name:string, title?:string, parent?:string, children:string[], container?:string, describe?:TextBlock, endpoints?:string[], from?:Record<string, any>, references?:string[] }} NodeModel
9
10
  * @typedef {{ name:string, root:string }} UniverseModel
10
- * @typedef {{ version:1, universes:Record<string, UniverseModel>, nodes:Record<string, NodeModel>, edges:Record<string, any>, diagnostics:any[], repositories?:Record<string, any>, generatedAt?:string }} UniverseGraph
11
+ * @typedef {{ version:1, universes:Record<string, UniverseModel>, nodes:Record<string, NodeModel>, edges:Record<string, any>, diagnostics:any[], repositories?:Record<string, RepositoryModel>, references?:Record<string, ReferenceModel>, generatedAt?:string }} UniverseGraph
11
12
  */
12
13
 
13
14
  export const universeGraph = writable(/** @type {UniverseGraph|null} */ (null));
14
15
 
15
- export const currentUniverseName = writable('Amaranthine');
16
+ export const currentUniverseName = writable(/** @type {string|null} */ (null));
16
17
 
17
18
  export const currentUniverse = derived(
18
19
  [universeGraph, currentUniverseName],
@@ -223,6 +224,22 @@ export function getNodeRoute(node) {
223
224
  return `/universes/${universe}/concept/${encodeURIComponent(node.id)}`;
224
225
  }
225
226
 
227
+ /**
228
+ * Auto-select the first universe if current universe is null or doesn't exist
229
+ * @param {UniverseGraph} graph - The loaded universe graph
230
+ */
231
+ export function autoSelectFirstUniverse(graph) {
232
+ if (!graph || !graph.universes) return;
233
+
234
+ const currentName = get(currentUniverseName);
235
+ const universeNames = Object.keys(graph.universes);
236
+
237
+ // If no universe is selected, or the selected one doesn't exist, select the first one
238
+ if (universeNames.length > 0 && (!currentName || !graph.universes[currentName])) {
239
+ currentUniverseName.set(universeNames[0]);
240
+ }
241
+ }
242
+
226
243
  /**
227
244
  * Fetch UniverseGraph JSON from a static path.
228
245
  * @param {string} url
package/src/lib/router.js CHANGED
@@ -52,6 +52,18 @@ export function parseRoute(pathname) {
52
52
  };
53
53
  }
54
54
 
55
+ // Match /universes/:universe/reference/:id
56
+ const referenceMatch = path.match(/^universes\/([^/]+)\/reference\/(.+)$/);
57
+ if (referenceMatch) {
58
+ return {
59
+ route: 'reference',
60
+ params: {
61
+ universe: referenceMatch[1],
62
+ id: referenceMatch[2],
63
+ },
64
+ };
65
+ }
66
+
55
67
  // Unknown route
56
68
  return null;
57
69
  }