@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 +1 -2
- package/src/App.svelte +41 -8
- package/src/lib/components/GlobalSearch.svelte +45 -3
- package/src/lib/data/universeStore.js +22 -5
- package/src/lib/router.js +12 -0
- package/src/pages/ConceptPage.svelte +146 -296
- package/src/pages/ReferencePage.svelte +104 -0
- package/src/pages/SeriesPage.svelte +146 -296
- package/src/lib/references/isWildcardPath.js +0 -9
- package/src/lib/references/linkForPath.js +0 -65
- package/src/lib/references/linkForRepository.js +0 -42
package/package.json
CHANGED
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
|
-
|
|
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={
|
|
144
|
-
{:else if currentRoute.route === 'anthology'}
|
|
145
|
-
<AnthologyPage params={
|
|
146
|
-
{:else if currentRoute.route === 'concept'}
|
|
147
|
-
<ConceptPage 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 {{
|
|
8
|
-
* @typedef {{ id:string,
|
|
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,
|
|
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(
|
|
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
|
}
|