@sprig-and-prose/sprig-ui 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/dist/assets/index-B6I7oo2K.js +1 -0
- package/dist/assets/index-O71xRVzw.js +4 -0
- package/dist/assets/index-coj3G3o6.css +1 -0
- package/dist/index.html +13 -0
- package/package.json +33 -0
- package/src/App.svelte +277 -0
- package/src/cli.js +146 -0
- package/src/lib/components/ContentsCard.svelte +283 -0
- package/src/lib/components/FooterStatus.svelte +83 -0
- package/src/lib/components/GlobalSearch.svelte +428 -0
- package/src/lib/components/NodePageLayout.svelte +65 -0
- package/src/lib/components/PageHeader.svelte +116 -0
- package/src/lib/components/Prose.svelte +260 -0
- package/src/lib/components/RelationshipsSection.svelte +122 -0
- package/src/lib/components/UniverseHeader.svelte +20 -0
- package/src/lib/data/universeStore.js +350 -0
- package/src/lib/format/kind.js +26 -0
- package/src/lib/format/title.js +97 -0
- package/src/lib/router.js +39 -0
- package/src/lib/stores/describeRenderMode.js +9 -0
- package/src/lib/stores/theme.js +98 -0
- package/src/main.js +154 -0
- package/src/pages/ConceptPage.svelte +88 -0
- package/src/pages/HomePage.svelte +48 -0
- package/src/server.js +115 -0
- package/src/styles/app.css +353 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { describeRenderMode } from '../stores/describeRenderMode.js';
|
|
3
|
+
|
|
4
|
+
/** @type {import('../data/universeStore.js').TextBlock | { raw?:string, normalized?:string } | undefined} */
|
|
5
|
+
export let textBlock;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse text into paragraphs (plain mode)
|
|
9
|
+
*/
|
|
10
|
+
const toParagraphs = (s) => {
|
|
11
|
+
if (!s) return [];
|
|
12
|
+
// Split on blank lines; preserve paragraphs.
|
|
13
|
+
const parts = s.split(/\n\s*\n/g);
|
|
14
|
+
return parts.map((p) =>
|
|
15
|
+
p
|
|
16
|
+
.split('\n') // soft breaks
|
|
17
|
+
.map((x) => x.trim())
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join(' ')
|
|
20
|
+
).filter(Boolean);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse text into structured blocks (lists mode)
|
|
25
|
+
* Returns array of { type: 'paragraph' | 'unordered-list' | 'ordered-list', content: string | string[] }
|
|
26
|
+
*/
|
|
27
|
+
const parseMarkdownLite = (s) => {
|
|
28
|
+
if (!s) return [];
|
|
29
|
+
|
|
30
|
+
// Split by blank lines to get blocks
|
|
31
|
+
const blocks = s.split(/\n\s*\n/g).filter(block => block.trim());
|
|
32
|
+
const result = [];
|
|
33
|
+
|
|
34
|
+
const unorderedPattern = /^[-—*]\s+/;
|
|
35
|
+
const orderedPattern = /^\d+\.\s+/;
|
|
36
|
+
|
|
37
|
+
for (const block of blocks) {
|
|
38
|
+
const lines = block.split('\n').map(line => line.trim()).filter(Boolean);
|
|
39
|
+
if (lines.length === 0) continue;
|
|
40
|
+
|
|
41
|
+
// Find where list items start (if any)
|
|
42
|
+
let unorderedStart = -1;
|
|
43
|
+
let orderedStart = -1;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
if (unorderedStart === -1 && unorderedPattern.test(lines[i])) {
|
|
47
|
+
unorderedStart = i;
|
|
48
|
+
}
|
|
49
|
+
if (orderedStart === -1 && orderedPattern.test(lines[i])) {
|
|
50
|
+
orderedStart = i;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle unordered list
|
|
55
|
+
if (unorderedStart >= 0 && (orderedStart === -1 || unorderedStart < orderedStart)) {
|
|
56
|
+
// Extract leading paragraph text (if any)
|
|
57
|
+
if (unorderedStart > 0) {
|
|
58
|
+
const leadingText = lines.slice(0, unorderedStart).join(' ');
|
|
59
|
+
if (leadingText.trim()) {
|
|
60
|
+
result.push({ type: 'paragraph', content: leadingText });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract list items (from first list item to end of block, or until non-list item)
|
|
65
|
+
const listItems = [];
|
|
66
|
+
let currentItem = null;
|
|
67
|
+
for (let i = unorderedStart; i < lines.length; i++) {
|
|
68
|
+
if (unorderedPattern.test(lines[i])) {
|
|
69
|
+
// If we have a current item, save it before starting a new one
|
|
70
|
+
if (currentItem !== null) {
|
|
71
|
+
listItems.push(currentItem);
|
|
72
|
+
}
|
|
73
|
+
const match = lines[i].match(/^[-—*]\s+(.+)$/);
|
|
74
|
+
currentItem = match && match[1] ? match[1] : '';
|
|
75
|
+
} else {
|
|
76
|
+
// Continuation line - append to current item
|
|
77
|
+
if (currentItem !== null) {
|
|
78
|
+
currentItem += ' ' + lines[i];
|
|
79
|
+
} else {
|
|
80
|
+
// Stop at first non-list item if we haven't started a list yet
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Don't forget the last item
|
|
86
|
+
if (currentItem !== null) {
|
|
87
|
+
listItems.push(currentItem);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (listItems.length > 0) {
|
|
91
|
+
result.push({ type: 'unordered-list', content: listItems });
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle ordered list
|
|
97
|
+
if (orderedStart >= 0) {
|
|
98
|
+
// Extract leading paragraph text (if any)
|
|
99
|
+
if (orderedStart > 0) {
|
|
100
|
+
const leadingText = lines.slice(0, orderedStart).join(' ');
|
|
101
|
+
if (leadingText.trim()) {
|
|
102
|
+
result.push({ type: 'paragraph', content: leadingText });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extract list items
|
|
107
|
+
const listItems = [];
|
|
108
|
+
let currentItem = null;
|
|
109
|
+
for (let i = orderedStart; i < lines.length; i++) {
|
|
110
|
+
if (orderedPattern.test(lines[i])) {
|
|
111
|
+
// If we have a current item, save it before starting a new one
|
|
112
|
+
if (currentItem !== null) {
|
|
113
|
+
listItems.push(currentItem);
|
|
114
|
+
}
|
|
115
|
+
const match = lines[i].match(/^\d+\.\s+(.+)$/);
|
|
116
|
+
currentItem = match && match[1] ? match[1] : '';
|
|
117
|
+
} else {
|
|
118
|
+
// Continuation line - append to current item
|
|
119
|
+
if (currentItem !== null) {
|
|
120
|
+
currentItem += ' ' + lines[i];
|
|
121
|
+
} else {
|
|
122
|
+
// Stop at first non-list item if we haven't started a list yet
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Don't forget the last item
|
|
128
|
+
if (currentItem !== null) {
|
|
129
|
+
listItems.push(currentItem);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (listItems.length > 0) {
|
|
133
|
+
result.push({ type: 'ordered-list', content: listItems });
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Otherwise, treat as paragraph (preserve literal text)
|
|
139
|
+
const paragraphText = lines.join(' ');
|
|
140
|
+
result.push({ type: 'paragraph', content: paragraphText });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
$: text = textBlock?.normalized ?? textBlock?.raw ?? '';
|
|
147
|
+
$: mode = $describeRenderMode;
|
|
148
|
+
$: paragraphs = mode === 'plain' ? toParagraphs(text) : null;
|
|
149
|
+
$: blocks = mode === 'lists' ? parseMarkdownLite(text) : null;
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
{#if mode === 'plain'}
|
|
153
|
+
{#if paragraphs && paragraphs.length}
|
|
154
|
+
<div class="prose sp-prose">
|
|
155
|
+
{#each paragraphs as p}
|
|
156
|
+
<p>{p}</p>
|
|
157
|
+
{/each}
|
|
158
|
+
</div>
|
|
159
|
+
{:else}
|
|
160
|
+
<div class="empty">No description yet.</div>
|
|
161
|
+
{/if}
|
|
162
|
+
{:else if mode === 'lists'}
|
|
163
|
+
{#if blocks && blocks.length}
|
|
164
|
+
<div class="prose sp-prose">
|
|
165
|
+
{#each blocks as block}
|
|
166
|
+
{#if block.type === 'paragraph'}
|
|
167
|
+
<p>{block.content}</p>
|
|
168
|
+
{:else if block.type === 'unordered-list'}
|
|
169
|
+
<ul>
|
|
170
|
+
{#each block.content as item}
|
|
171
|
+
<li>{item}</li>
|
|
172
|
+
{/each}
|
|
173
|
+
</ul>
|
|
174
|
+
{:else if block.type === 'ordered-list'}
|
|
175
|
+
<ol>
|
|
176
|
+
{#each block.content as item}
|
|
177
|
+
<li>{item}</li>
|
|
178
|
+
{/each}
|
|
179
|
+
</ol>
|
|
180
|
+
{/if}
|
|
181
|
+
{/each}
|
|
182
|
+
</div>
|
|
183
|
+
{:else}
|
|
184
|
+
<div class="empty">No description yet.</div>
|
|
185
|
+
{/if}
|
|
186
|
+
{/if}
|
|
187
|
+
|
|
188
|
+
<style>
|
|
189
|
+
.prose {
|
|
190
|
+
max-width: 70ch;
|
|
191
|
+
font-size: var(--sp-font-body);
|
|
192
|
+
line-height: var(--sp-line-body);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
p {
|
|
196
|
+
font-size: var(--sp-font-body);
|
|
197
|
+
line-height: var(--sp-line-body);
|
|
198
|
+
margin-bottom: var(--sp-space-4);
|
|
199
|
+
color: var(--text-color);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
p:last-child {
|
|
203
|
+
margin-bottom: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ul {
|
|
207
|
+
list-style: none;
|
|
208
|
+
padding-left: 0;
|
|
209
|
+
margin-bottom: var(--sp-space-6);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ul li {
|
|
213
|
+
font-size: var(--sp-font-body);
|
|
214
|
+
line-height: var(--sp-line-loose);
|
|
215
|
+
color: var(--text-secondary);
|
|
216
|
+
margin-bottom: var(--sp-space-3);
|
|
217
|
+
padding-left: var(--sp-space-6);
|
|
218
|
+
position: relative;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
ul li::before {
|
|
222
|
+
content: '—';
|
|
223
|
+
position: absolute;
|
|
224
|
+
left: 0;
|
|
225
|
+
color: var(--text-tertiary);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ul li:last-child {
|
|
229
|
+
margin-bottom: 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ol {
|
|
233
|
+
margin-bottom: var(--sp-space-6);
|
|
234
|
+
padding-left: var(--sp-space-6);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ol li {
|
|
238
|
+
font-size: var(--sp-font-body);
|
|
239
|
+
line-height: var(--sp-line-body);
|
|
240
|
+
margin-bottom: var(--sp-space-3);
|
|
241
|
+
color: var(--text-color);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
ol li:last-child {
|
|
245
|
+
margin-bottom: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.empty {
|
|
249
|
+
color: var(--text-tertiary);
|
|
250
|
+
font-size: var(--sp-font-small);
|
|
251
|
+
padding: 10px 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@media (max-width: 480px) {
|
|
255
|
+
ul li {
|
|
256
|
+
padding-left: 20px;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
260
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getDisplayTitle } from '../format/title.js';
|
|
3
|
+
import { getDisplayKind } from '../format/kind.js';
|
|
4
|
+
import { getNodeRoute } from '../data/universeStore.js';
|
|
5
|
+
|
|
6
|
+
/** @type {Array<any>} */
|
|
7
|
+
export let relationships = [];
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#if relationships.length > 0}
|
|
11
|
+
<section class="relationships">
|
|
12
|
+
<h2 class="relationships-title">Relationships</h2>
|
|
13
|
+
<ul class="relationships-list">
|
|
14
|
+
{#each relationships as rel}
|
|
15
|
+
<li class="relationship-item">
|
|
16
|
+
<a class="relationship-link sprig-link" href={getNodeRoute(rel.otherNode)}>
|
|
17
|
+
<span class="relationship-label">{rel.label}</span>
|
|
18
|
+
<span class="relationship-separator">→</span>
|
|
19
|
+
<span class="relationship-name">{getDisplayTitle(rel.otherNode)}</span>
|
|
20
|
+
<span class="relationship-kind">{getDisplayKind(rel.otherNode)}</span>
|
|
21
|
+
</a>
|
|
22
|
+
{#if rel.desc}
|
|
23
|
+
<p class="relationship-description">{rel.desc}</p>
|
|
24
|
+
{/if}
|
|
25
|
+
</li>
|
|
26
|
+
{/each}
|
|
27
|
+
</ul>
|
|
28
|
+
</section>
|
|
29
|
+
{/if}
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
.relationships {
|
|
33
|
+
margin-top: 32px;
|
|
34
|
+
padding-top: 24px;
|
|
35
|
+
border-top: 1px solid var(--hairline);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.relationships-title {
|
|
39
|
+
font-family: var(--font-ui);
|
|
40
|
+
font-size: var(--sp-font-tiny);
|
|
41
|
+
letter-spacing: 0.02em;
|
|
42
|
+
text-transform: none;
|
|
43
|
+
color: var(--text-secondary);
|
|
44
|
+
margin: 0 0 0.75rem 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.relationships-list {
|
|
48
|
+
list-style: none;
|
|
49
|
+
padding: 0;
|
|
50
|
+
margin: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.relationship-item {
|
|
54
|
+
margin-bottom: 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.relationship-item:last-child {
|
|
58
|
+
margin-bottom: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.relationship-link {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 8px;
|
|
65
|
+
padding: 4px 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.relationship-link.sprig-link {
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.relationship-link.sprig-link .relationship-name {
|
|
73
|
+
text-decoration: underline;
|
|
74
|
+
text-decoration-thickness: 1px;
|
|
75
|
+
text-underline-offset: 0.1875rem;
|
|
76
|
+
text-decoration-color: var(--sprig-link-underline);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.relationship-link.sprig-link:hover .relationship-name {
|
|
80
|
+
text-decoration-thickness: 1.5px;
|
|
81
|
+
text-decoration-color: var(--sprig-link-underline-hover);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.relationship-label {
|
|
85
|
+
font-family: var(--font-ui);
|
|
86
|
+
font-size: var(--sp-font-small);
|
|
87
|
+
color: var(--text-secondary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.relationship-separator {
|
|
91
|
+
font-family: var(--font-ui);
|
|
92
|
+
font-size: var(--sp-font-small);
|
|
93
|
+
color: var(--text-tertiary);
|
|
94
|
+
opacity: 0.6;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.relationship-name {
|
|
98
|
+
font-family: var(--font-prose);
|
|
99
|
+
font-size: var(--sp-font-small);
|
|
100
|
+
color: inherit;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.relationship-kind {
|
|
104
|
+
font-family: var(--font-ui);
|
|
105
|
+
font-size: var(--sp-font-tiny);
|
|
106
|
+
color: var(--text-tertiary);
|
|
107
|
+
margin-left: auto;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.relationship-description {
|
|
111
|
+
margin: 8px 0 0 0;
|
|
112
|
+
font-family: var(--font-prose);
|
|
113
|
+
font-size: var(--sp-font-body);
|
|
114
|
+
color: var(--text-secondary);
|
|
115
|
+
line-height: 1.6;
|
|
116
|
+
max-width: 70ch;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.relationship-item:not(:last-child) .relationship-description {
|
|
120
|
+
margin-bottom: 1.5rem;
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import PageHeader from './PageHeader.svelte';
|
|
3
|
+
import { getDisplayTitle } from '../format/title.js';
|
|
4
|
+
|
|
5
|
+
/** @type {{ node: any }} */
|
|
6
|
+
export let node;
|
|
7
|
+
|
|
8
|
+
const firstSentence = (s) => {
|
|
9
|
+
if (!s) return '';
|
|
10
|
+
const idx = s.indexOf('.');
|
|
11
|
+
return idx >= 0 ? s.slice(0, idx + 1) : s;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
$: subtitle = node.describe?.normalized || node.describe?.raw
|
|
15
|
+
? firstSentence(node.describe?.normalized ?? node.describe?.raw)
|
|
16
|
+
: null;
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<PageHeader title={getDisplayTitle(node)} {subtitle} />
|
|
20
|
+
|