@sprig-and-prose/sprig-ui-csr 0.1.0 → 0.1.2

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.0",
3
+ "version": "0.1.2",
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,5 +1,6 @@
1
1
  <script>
2
- import { loadUniverseGraph } from './lib/data/universeStore.js';
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { loadUniverseGraph, autoSelectFirstUniverse, universeGraph } from './lib/data/universeStore.js';
3
4
  import { getCurrentRoute, navigate } from './lib/router.js';
4
5
  import { theme } from './lib/stores/theme.js';
5
6
  import { describeRenderMode } from './lib/stores/describeRenderMode.js';
@@ -13,6 +14,12 @@
13
14
  let error = null;
14
15
  let currentRoute = getCurrentRoute();
15
16
 
17
+ // Reactively auto-select first universe when on home route and graph is loaded
18
+ // This runs whenever universeGraph or currentRoute changes
19
+ $: if ($universeGraph && currentRoute?.route === 'home') {
20
+ autoSelectFirstUniverse($universeGraph);
21
+ }
22
+
16
23
  function toggleTheme() {
17
24
  theme.update((t) => {
18
25
  if (t === 'light') return 'dark';
@@ -34,6 +41,9 @@
34
41
  loading = true;
35
42
  error = null;
36
43
  const { describeRenderMode: mode } = await loadUniverseGraph('/api/manifest');
44
+
45
+ // Auto-selection is handled reactively above
46
+
37
47
  if (mode === 'lists' || mode === 'plain') {
38
48
  describeRenderMode.set(mode);
39
49
  }
@@ -47,6 +57,7 @@
47
57
 
48
58
  function handleRouteChange() {
49
59
  currentRoute = getCurrentRoute();
60
+ // Auto-selection will happen reactively when route changes
50
61
  }
51
62
 
52
63
  // Intercept link clicks for client-side navigation
@@ -76,7 +87,50 @@
76
87
  // Listen for link clicks
77
88
  document.addEventListener('click', handleLinkClick);
78
89
 
79
- loadManifest();
90
+ // Set up Server-Sent Events for manifest updates
91
+ let eventSource = null;
92
+
93
+ onMount(() => {
94
+ if (typeof EventSource !== 'undefined') {
95
+ try {
96
+ eventSource = new EventSource('/api/events');
97
+ console.log('SSE EventSource created');
98
+
99
+ // Listen for manifest change events
100
+ eventSource.addEventListener('manifest', async (e) => {
101
+ console.log('SSE manifest event received:', e.data);
102
+ await loadManifest();
103
+ });
104
+
105
+ // Optional: refetch on connection open to be safe
106
+ eventSource.addEventListener('open', () => {
107
+ console.log('SSE connection opened');
108
+ });
109
+
110
+ // Handle errors (EventSource auto-reconnects, so we can ignore or log minimally)
111
+ eventSource.addEventListener('error', (e) => {
112
+ // EventSource will auto-reconnect, so we don't need to do anything
113
+ // Just log if needed for debugging
114
+ if (eventSource && eventSource.readyState === EventSource.CLOSED) {
115
+ console.log('SSE connection closed');
116
+ } else if (eventSource && eventSource.readyState === EventSource.CONNECTING) {
117
+ console.log('SSE reconnecting...');
118
+ }
119
+ });
120
+ } catch (err) {
121
+ console.error('Failed to create EventSource:', err);
122
+ }
123
+ }
124
+
125
+ loadManifest();
126
+ });
127
+
128
+ onDestroy(() => {
129
+ if (eventSource) {
130
+ eventSource.close();
131
+ console.log('SSE EventSource closed');
132
+ }
133
+ });
80
134
  </script>
81
135
 
82
136
  <div class="app sprig-design">
@@ -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;
@@ -259,7 +285,7 @@
259
285
  <input
260
286
  bind:this={searchInput}
261
287
  type="text"
262
- placeholder="Search Series, Books, Chapters… (Press / to focus)"
288
+ placeholder="Search Series, Books, Chapters, Concepts… (Press / to focus)"
263
289
  bind:value={query}
264
290
  class="search-input"
265
291
  role="combobox"
@@ -1,4 +1,4 @@
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
@@ -12,7 +12,7 @@ import { writable, derived } from 'svelte/store';
12
12
 
13
13
  export const universeGraph = writable(/** @type {UniverseGraph|null} */ (null));
14
14
 
15
- export const currentUniverseName = writable('Amaranthine');
15
+ export const currentUniverseName = writable(/** @type {string|null} */ (null));
16
16
 
17
17
  export const currentUniverse = derived(
18
18
  [universeGraph, currentUniverseName],
@@ -223,6 +223,22 @@ export function getNodeRoute(node) {
223
223
  return `/universes/${universe}/concept/${encodeURIComponent(node.id)}`;
224
224
  }
225
225
 
226
+ /**
227
+ * Auto-select the first universe if current universe is null or doesn't exist
228
+ * @param {UniverseGraph} graph - The loaded universe graph
229
+ */
230
+ export function autoSelectFirstUniverse(graph) {
231
+ if (!graph || !graph.universes) return;
232
+
233
+ const currentName = get(currentUniverseName);
234
+ const universeNames = Object.keys(graph.universes);
235
+
236
+ // If no universe is selected, or the selected one doesn't exist, select the first one
237
+ if (universeNames.length > 0 && (!currentName || !graph.universes[currentName])) {
238
+ currentUniverseName.set(universeNames[0]);
239
+ }
240
+ }
241
+
226
242
  /**
227
243
  * Fetch UniverseGraph JSON from a static path.
228
244
  * @param {string} url