@sprig-and-prose/sprig-ui-csr 0.1.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/README.md +120 -0
- package/biome.json +37 -0
- package/index.html +12 -0
- package/manifest.json +10711 -0
- package/package.json +29 -0
- package/src/App.svelte +202 -0
- package/src/cli.js +146 -0
- package/src/lib/components/ContentsCard.svelte +167 -0
- package/src/lib/components/FooterStatus.svelte +80 -0
- package/src/lib/components/GlobalSearch.svelte +451 -0
- package/src/lib/components/PageHeader.svelte +116 -0
- package/src/lib/components/Prose.svelte +260 -0
- package/src/lib/components/UniverseHeader.svelte +20 -0
- package/src/lib/data/universeStore.js +252 -0
- package/src/lib/format/title.js +97 -0
- package/src/lib/references/isWildcardPath.js +9 -0
- package/src/lib/references/linkForPath.js +65 -0
- package/src/lib/references/linkForRepository.js +42 -0
- package/src/lib/router.js +75 -0
- package/src/lib/stores/describeRenderMode.js +9 -0
- package/src/lib/stores/theme.js +98 -0
- package/src/main.js +143 -0
- package/src/pages/AnthologyPage.svelte +84 -0
- package/src/pages/ConceptPage.svelte +873 -0
- package/src/pages/HomePage.svelte +80 -0
- package/src/pages/SeriesPage.svelte +657 -0
- package/src/server.js +115 -0
- package/src/styles/app.css +353 -0
- package/tsconfig.json +16 -0
- package/vite.config.js +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sprig-and-prose/sprig-ui-csr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sprig-ui-csr": "./src/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite dev",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"start": "node src/cli.js",
|
|
12
|
+
"format": "biome format . --write",
|
|
13
|
+
"lint": "biome lint .",
|
|
14
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@sprig-and-prose/sprig-design": "^0.1.5",
|
|
18
|
+
"@sprig-and-prose/sprig-texture": "^0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@biomejs/biome": "^2.3.10",
|
|
22
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"svelte": "^5.46.0",
|
|
25
|
+
"typescript": "^5.7.2",
|
|
26
|
+
"vite": "^6.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
package/src/App.svelte
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { loadUniverseGraph } from './lib/data/universeStore.js';
|
|
3
|
+
import { getCurrentRoute, navigate } from './lib/router.js';
|
|
4
|
+
import { theme } from './lib/stores/theme.js';
|
|
5
|
+
import { describeRenderMode } from './lib/stores/describeRenderMode.js';
|
|
6
|
+
import GlobalSearch from './lib/components/GlobalSearch.svelte';
|
|
7
|
+
import HomePage from './pages/HomePage.svelte';
|
|
8
|
+
import SeriesPage from './pages/SeriesPage.svelte';
|
|
9
|
+
import AnthologyPage from './pages/AnthologyPage.svelte';
|
|
10
|
+
import ConceptPage from './pages/ConceptPage.svelte';
|
|
11
|
+
|
|
12
|
+
let loading = true;
|
|
13
|
+
let error = null;
|
|
14
|
+
let currentRoute = getCurrentRoute();
|
|
15
|
+
|
|
16
|
+
function toggleTheme() {
|
|
17
|
+
theme.update((t) => {
|
|
18
|
+
if (t === 'light') return 'dark';
|
|
19
|
+
if (t === 'dark') return 'auto';
|
|
20
|
+
return 'light'; // 'auto' -> 'light'
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get display label for theme toggle
|
|
25
|
+
$: themeLabel = (() => {
|
|
26
|
+
if ($theme === 'auto') {
|
|
27
|
+
return 'Auto';
|
|
28
|
+
}
|
|
29
|
+
return $theme === 'dark' ? 'Light' : 'Dark';
|
|
30
|
+
})();
|
|
31
|
+
|
|
32
|
+
async function loadManifest() {
|
|
33
|
+
try {
|
|
34
|
+
loading = true;
|
|
35
|
+
error = null;
|
|
36
|
+
const { describeRenderMode: mode } = await loadUniverseGraph('/api/manifest');
|
|
37
|
+
if (mode === 'lists' || mode === 'plain') {
|
|
38
|
+
describeRenderMode.set(mode);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
error = err.message || 'Failed to load manifest';
|
|
42
|
+
console.error('Error loading manifest:', err);
|
|
43
|
+
} finally {
|
|
44
|
+
loading = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleRouteChange() {
|
|
49
|
+
currentRoute = getCurrentRoute();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Intercept link clicks for client-side navigation
|
|
53
|
+
function handleLinkClick(event) {
|
|
54
|
+
const link = event.target.closest('a');
|
|
55
|
+
if (!link) return;
|
|
56
|
+
|
|
57
|
+
const href = link.getAttribute('href');
|
|
58
|
+
if (!href) return;
|
|
59
|
+
|
|
60
|
+
// Only handle internal links (not external or hash links)
|
|
61
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:') || href.startsWith('#')) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if it's an internal route
|
|
66
|
+
if (href.startsWith('/') || !href.includes('://')) {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
navigate(href);
|
|
69
|
+
handleRouteChange();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Listen for popstate (back/forward button)
|
|
74
|
+
window.addEventListener('popstate', handleRouteChange);
|
|
75
|
+
|
|
76
|
+
// Listen for link clicks
|
|
77
|
+
document.addEventListener('click', handleLinkClick);
|
|
78
|
+
|
|
79
|
+
loadManifest();
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<div class="app sprig-design">
|
|
83
|
+
<div class="page">
|
|
84
|
+
<div class="top-bar">
|
|
85
|
+
<GlobalSearch />
|
|
86
|
+
<button class="theme-toggle" on:click={toggleTheme} type="button" aria-label="Toggle theme">
|
|
87
|
+
{themeLabel}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
<main class="main-content">
|
|
91
|
+
{#if loading}
|
|
92
|
+
<div class="loading">Loading…</div>
|
|
93
|
+
{:else if error}
|
|
94
|
+
<div class="error">Error: {error}</div>
|
|
95
|
+
{:else if currentRoute}
|
|
96
|
+
{#if currentRoute.route === 'home'}
|
|
97
|
+
<HomePage />
|
|
98
|
+
{:else if currentRoute.route === 'series'}
|
|
99
|
+
<SeriesPage params={currentRoute.params} />
|
|
100
|
+
{:else if currentRoute.route === 'anthology'}
|
|
101
|
+
<AnthologyPage params={currentRoute.params} />
|
|
102
|
+
{:else if currentRoute.route === 'concept'}
|
|
103
|
+
<ConceptPage params={currentRoute.params} />
|
|
104
|
+
{:else}
|
|
105
|
+
<div class="error">Unknown route: {currentRoute.route}</div>
|
|
106
|
+
{/if}
|
|
107
|
+
{:else}
|
|
108
|
+
<div class="error">Invalid route</div>
|
|
109
|
+
{/if}
|
|
110
|
+
</main>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
.app {
|
|
116
|
+
min-height: 100vh;
|
|
117
|
+
width: 100%;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.page {
|
|
121
|
+
max-width: 1200px;
|
|
122
|
+
margin: 0 auto;
|
|
123
|
+
width: 100%;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.top-bar {
|
|
127
|
+
padding: 24px 32px;
|
|
128
|
+
border-bottom: 1px solid var(--border-color);
|
|
129
|
+
display: flex;
|
|
130
|
+
justify-content: space-between;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 1rem;
|
|
133
|
+
width: 100%;
|
|
134
|
+
position: sticky;
|
|
135
|
+
top: 0;
|
|
136
|
+
background-color: var(--bg-color);
|
|
137
|
+
z-index: 100;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.main-content {
|
|
141
|
+
padding: 64px 32px;
|
|
142
|
+
min-height: calc(100vh - 200px);
|
|
143
|
+
width: 100%;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.theme-toggle {
|
|
147
|
+
background: var(--card-bg);
|
|
148
|
+
border: none;
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
color: var(--text-color);
|
|
151
|
+
font-family: var(--font-ui);
|
|
152
|
+
font-size: var(--sp-font-small);
|
|
153
|
+
padding: 8px 12px;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
transition: background 0.2s, opacity 0.2s;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.theme-toggle:hover {
|
|
160
|
+
background: var(--hover);
|
|
161
|
+
opacity: 0.8;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.theme-toggle:focus {
|
|
165
|
+
outline: 2px solid var(--text-tertiary);
|
|
166
|
+
outline-offset: 2px;
|
|
167
|
+
border-radius: 2px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.loading,
|
|
171
|
+
.error {
|
|
172
|
+
color: var(--text-tertiary);
|
|
173
|
+
padding: 24px 0;
|
|
174
|
+
font-size: var(--sp-font-body);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.error {
|
|
178
|
+
color: #d32f2f;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@media (max-width: 768px) {
|
|
182
|
+
.top-bar {
|
|
183
|
+
padding: 20px 24px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.main-content {
|
|
187
|
+
padding: 48px 24px;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@media (max-width: 480px) {
|
|
192
|
+
.top-bar {
|
|
193
|
+
padding: 16px 20px;
|
|
194
|
+
flex-wrap: wrap;
|
|
195
|
+
gap: 8px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.main-content {
|
|
199
|
+
padding: 32px 20px;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
</style>
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
4
|
+
import { resolve, dirname, join } from 'path';
|
|
5
|
+
import { createAppServer } from './server.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Walks upward from startPath to find .sprig directory
|
|
9
|
+
* @param {string} startPath - Starting directory path
|
|
10
|
+
* @returns {string | null} - Path to directory containing .sprig, or null if not found
|
|
11
|
+
*/
|
|
12
|
+
function discoverSprigDir(startPath) {
|
|
13
|
+
let current = resolve(startPath);
|
|
14
|
+
const root = resolve('/');
|
|
15
|
+
|
|
16
|
+
while (current !== root) {
|
|
17
|
+
const sprigDirPath = join(current, '.sprig');
|
|
18
|
+
if (existsSync(sprigDirPath) && statSync(sprigDirPath).isDirectory()) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
current = dirname(current);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check root directory as well
|
|
25
|
+
const rootSprigDirPath = join(root, '.sprig');
|
|
26
|
+
if (existsSync(rootSprigDirPath) && statSync(rootSprigDirPath).isDirectory()) {
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse command-line arguments
|
|
35
|
+
* @returns {{ port: number; manifest: string; manifestSpecified: boolean }}
|
|
36
|
+
*/
|
|
37
|
+
function parseArgs() {
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
let port = 5173;
|
|
40
|
+
let manifest = '';
|
|
41
|
+
let manifestSpecified = false;
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (arg === '--port' || arg === '-p') {
|
|
46
|
+
const portValue = args[i + 1];
|
|
47
|
+
if (!portValue) {
|
|
48
|
+
console.error('Error: --port requires a value');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const parsedPort = parseInt(portValue, 10);
|
|
52
|
+
if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
53
|
+
console.error('Error: --port must be a number between 1 and 65535');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
port = parsedPort;
|
|
57
|
+
i++;
|
|
58
|
+
} else if (arg === '--manifest' || arg === '-m') {
|
|
59
|
+
const manifestValue = args[i + 1];
|
|
60
|
+
if (!manifestValue) {
|
|
61
|
+
console.error('Error: --manifest requires a value');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
manifest = manifestValue;
|
|
65
|
+
manifestSpecified = true;
|
|
66
|
+
i++;
|
|
67
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
68
|
+
console.log(`
|
|
69
|
+
Usage: sprig-ui-csr [options]
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
-p, --port <number> Port to listen on (default: 5173)
|
|
73
|
+
-m, --manifest <path> Path to manifest.json file (default: auto-discovered)
|
|
74
|
+
-h, --help Show this help message
|
|
75
|
+
|
|
76
|
+
Discovery:
|
|
77
|
+
- Walks upward from current directory to find .sprig directory
|
|
78
|
+
- Default manifest location: <root>/.sprig/manifest.json
|
|
79
|
+
- Falls back to ./manifest.json if no .sprig directory found
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
sprig-ui-csr
|
|
83
|
+
sprig-ui-csr --port 3000
|
|
84
|
+
sprig-ui-csr --manifest ./custom-manifest.json
|
|
85
|
+
sprig-ui-csr -p 8080 -m ./data/manifest.json
|
|
86
|
+
`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
} else {
|
|
89
|
+
console.error(`Error: Unknown option ${arg}`);
|
|
90
|
+
console.error('Use --help for usage information');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { port, manifest, manifestSpecified };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
function main() {
|
|
100
|
+
const { port, manifest, manifestSpecified } = parseArgs();
|
|
101
|
+
|
|
102
|
+
// Determine manifest path with fallback chain:
|
|
103
|
+
// 1. Explicit --manifest flag
|
|
104
|
+
// 2. Discovered .sprig/manifest.json
|
|
105
|
+
// 3. ./manifest.json (fallback)
|
|
106
|
+
let manifestPath = manifest;
|
|
107
|
+
|
|
108
|
+
if (!manifestSpecified) {
|
|
109
|
+
// Try to discover .sprig directory
|
|
110
|
+
const sprigRoot = discoverSprigDir(process.cwd());
|
|
111
|
+
if (sprigRoot) {
|
|
112
|
+
manifestPath = join(sprigRoot, '.sprig', 'manifest.json');
|
|
113
|
+
} else {
|
|
114
|
+
// Final fallback
|
|
115
|
+
manifestPath = './manifest.json';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const server = createAppServer(manifestPath);
|
|
120
|
+
|
|
121
|
+
server.listen(port, () => {
|
|
122
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
123
|
+
console.log(`Manifest path: ${manifestPath}`);
|
|
124
|
+
console.log('Press Ctrl+C to stop');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Handle graceful shutdown
|
|
128
|
+
process.on('SIGINT', () => {
|
|
129
|
+
console.log('\nShutting down server...');
|
|
130
|
+
server.close(() => {
|
|
131
|
+
console.log('Server stopped');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
process.on('SIGTERM', () => {
|
|
137
|
+
console.log('\nShutting down server...');
|
|
138
|
+
server.close(() => {
|
|
139
|
+
console.log('Server stopped');
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main();
|
|
146
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getNodeRoute } from '../data/universeStore.js';
|
|
3
|
+
import { getDisplayTitle } from '../format/title.js';
|
|
4
|
+
|
|
5
|
+
/** @type {import('../data/universeStore.js').NodeModel[]} */
|
|
6
|
+
export let children = [];
|
|
7
|
+
/** @type {import('../data/universeStore.js').NodeModel | null} */
|
|
8
|
+
export let currentNode = null;
|
|
9
|
+
/** @type {string | null} */
|
|
10
|
+
export let title = null;
|
|
11
|
+
|
|
12
|
+
$: titleText = title !== null
|
|
13
|
+
? title
|
|
14
|
+
: currentNode?.kind === 'book' && children.length > 0
|
|
15
|
+
? `Contents (Chapters in ${getDisplayTitle(currentNode)})`
|
|
16
|
+
: 'Contents';
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="card">
|
|
20
|
+
<div class="title">{titleText}</div>
|
|
21
|
+
|
|
22
|
+
{#if children.length > 0}
|
|
23
|
+
<ul class="list">
|
|
24
|
+
{#each children as child}
|
|
25
|
+
<li class="item">
|
|
26
|
+
<a class="link sprig-link" href={getNodeRoute(child)}>
|
|
27
|
+
<span class="name">{getDisplayTitle(child)}</span>
|
|
28
|
+
<span class="kind">{child.kind}</span>
|
|
29
|
+
</a>
|
|
30
|
+
</li>
|
|
31
|
+
{/each}
|
|
32
|
+
</ul>
|
|
33
|
+
{:else}
|
|
34
|
+
<p class="empty">No contents yet.</p>
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.card {
|
|
40
|
+
background-color: var(--card-bg);
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
padding: 24px;
|
|
43
|
+
font-size: var(--sp-font-small);
|
|
44
|
+
line-height: 1.6;
|
|
45
|
+
width: 100%;
|
|
46
|
+
position: sticky;
|
|
47
|
+
top: 32px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.title {
|
|
51
|
+
font-size: var(--sp-font-tiny);
|
|
52
|
+
color: var(--text-tertiary);
|
|
53
|
+
letter-spacing: 0.5px;
|
|
54
|
+
margin-bottom: 20px;
|
|
55
|
+
font-family: var(--font-ui);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.list {
|
|
59
|
+
list-style: none;
|
|
60
|
+
padding: 0;
|
|
61
|
+
margin: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.item {
|
|
65
|
+
margin-bottom: 20px;
|
|
66
|
+
color: var(--text-secondary);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.item:last-child {
|
|
70
|
+
margin-bottom: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.link {
|
|
74
|
+
align-items: center;
|
|
75
|
+
background: none;
|
|
76
|
+
border: none;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
display: flex;
|
|
79
|
+
font-family: inherit;
|
|
80
|
+
font-size: inherit;
|
|
81
|
+
gap: 0.75rem;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
padding: 0;
|
|
84
|
+
text-align: left;
|
|
85
|
+
width: 100%;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Remove underline from link itself, show only on name */
|
|
89
|
+
.link.sprig-link {
|
|
90
|
+
text-decoration: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Show underline only on hover - no underline at rest */
|
|
94
|
+
.link.sprig-link .name {
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.link.sprig-link:hover .name {
|
|
99
|
+
text-decoration: underline;
|
|
100
|
+
text-decoration-thickness: 1px;
|
|
101
|
+
text-underline-offset: 0.25rem;
|
|
102
|
+
text-decoration-color: rgba(0, 0, 0, 0.25);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (prefers-color-scheme: dark) {
|
|
106
|
+
:root:not([data-theme="light"]) .link.sprig-link:hover .name {
|
|
107
|
+
text-decoration-color: rgba(255, 255, 255, 0.35);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
:global(html[data-theme="dark"]) .link.sprig-link:hover .name {
|
|
112
|
+
text-decoration-color: rgba(255, 255, 255, 0.35);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:global(html[data-theme="light"]) .link.sprig-link:hover .name {
|
|
116
|
+
text-decoration-color: rgba(0, 0, 0, 0.4);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.link:focus {
|
|
120
|
+
outline: 2px solid var(--text-tertiary);
|
|
121
|
+
outline-offset: 0.25rem;
|
|
122
|
+
border-radius: 2px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.name {
|
|
126
|
+
font-size: var(--sp-font-body);
|
|
127
|
+
font-family: var(--font-prose);
|
|
128
|
+
font-weight: 400;
|
|
129
|
+
color: var(--text-color);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.kind {
|
|
133
|
+
font-size: var(--sp-font-tiny);
|
|
134
|
+
color: var(--text-tertiary);
|
|
135
|
+
font-family: var(--font-ui);
|
|
136
|
+
font-style: italic;
|
|
137
|
+
font-weight: normal;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.empty {
|
|
142
|
+
color: var(--text-tertiary);
|
|
143
|
+
font-size: var(--sp-font-small);
|
|
144
|
+
margin: 0;
|
|
145
|
+
font-family: var(--font-ui);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (max-width: 768px) {
|
|
149
|
+
.card {
|
|
150
|
+
padding: 20px;
|
|
151
|
+
font-size: var(--sp-font-small);
|
|
152
|
+
position: static;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.item {
|
|
156
|
+
margin-bottom: 20px;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@media (max-width: 480px) {
|
|
161
|
+
.card {
|
|
162
|
+
padding: 18px;
|
|
163
|
+
font-size: var(--sp-font-tiny);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
167
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/** @type {import('../data/universeStore.js').UniverseGraph | null} */
|
|
3
|
+
export let graph = null;
|
|
4
|
+
/** @type {import('../data/universeStore.js').NodeModel | null} */
|
|
5
|
+
export let root = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {any} obj
|
|
9
|
+
*/
|
|
10
|
+
const count = (obj) => (obj ? Object.keys(obj).length : 0);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<footer class="footer">
|
|
14
|
+
<div class="left">
|
|
15
|
+
<span class="label">Grounding</span>
|
|
16
|
+
<span class="value">Coming soon</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="right">
|
|
20
|
+
<span class="label">Nodes</span>
|
|
21
|
+
<span class="value">{count(graph?.nodes)}</span>
|
|
22
|
+
<span class="dot">•</span>
|
|
23
|
+
<span class="label">Relationships</span>
|
|
24
|
+
<span class="value">{count(graph?.edges)}</span>
|
|
25
|
+
</div>
|
|
26
|
+
</footer>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.footer {
|
|
30
|
+
padding: 2rem 0;
|
|
31
|
+
border-top: 1px solid var(--border-color);
|
|
32
|
+
margin-top: 4rem;
|
|
33
|
+
width: 100%;
|
|
34
|
+
overflow-x: hidden;
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
gap: 18px;
|
|
38
|
+
font-size: var(--sp-font-tiny);
|
|
39
|
+
color: var(--text-tertiary);
|
|
40
|
+
max-width: 1200px;
|
|
41
|
+
margin-left: auto;
|
|
42
|
+
margin-right: auto;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.left, .right {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
align-items: center;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.label {
|
|
53
|
+
color: var(--text-tertiary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.value {
|
|
57
|
+
color: var(--text-secondary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dot {
|
|
61
|
+
color: var(--text-tertiary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@media (max-width: 768px) {
|
|
65
|
+
.footer {
|
|
66
|
+
padding: 2rem 0;
|
|
67
|
+
margin-top: 48px;
|
|
68
|
+
font-size: var(--sp-font-tiny);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@media (max-width: 480px) {
|
|
73
|
+
.footer {
|
|
74
|
+
padding: 1.5rem 0;
|
|
75
|
+
margin-top: 40px;
|
|
76
|
+
font-size: var(--sp-font-tiny);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
|