@sprig-and-prose/tutorial-svelte 0.2.3 → 0.2.4

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/tutorial-svelte",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "A calm SvelteKit package that transforms markdown files into paged tutorial routes",
6
6
  "main": "src/index.js",
@@ -18,6 +18,9 @@
18
18
  | {
19
19
  kind: 'stub';
20
20
  firstPagePath: string;
21
+ title?: string;
22
+ pages?: Array<{ title: string; path: string }>;
23
+ introContent?: string | null;
21
24
  }
22
25
  | {
23
26
  kind: 'error';
@@ -240,8 +243,28 @@
240
243
  </div>
241
244
  {:else if data.kind === 'stub'}
242
245
  <div class="stub-page">
243
- <p>Start at page 1</p>
244
- <a href={data.firstPagePath}>Begin</a>
246
+ {#if data.title}
247
+ <h1>{data.title}</h1>
248
+ {/if}
249
+ {#if data.introContent}
250
+ <div class="stub-intro">
251
+ {@html data.introContent}
252
+ </div>
253
+ {/if}
254
+ {#if data.pages && data.pages.length > 0}
255
+ <h2 class="stub-toc-header">Table of contents</h2>
256
+ <ol class="stub-toc-list">
257
+ {#each data.pages as page}
258
+ <li class="stub-toc-item">
259
+ <a href={page.path} class="stub-toc-link">{page.title}</a>
260
+ </li>
261
+ {/each}
262
+ </ol>
263
+ {/if}
264
+ <nav class="tutorial-nav">
265
+ <span class="nav-link nav-previous nav-placeholder"></span>
266
+ <a href={data.firstPagePath} class="nav-link nav-next">Next</a>
267
+ </nav>
245
268
  </div>
246
269
  {:else if data.kind === 'error'}
247
270
  <div class="error-state">
@@ -306,13 +329,40 @@
306
329
  .stub-page {
307
330
  max-width: 70ch;
308
331
  margin: 0 auto;
309
- padding: 4rem 1rem;
310
- text-align: center;
332
+ padding: 2rem 1rem;
333
+ }
334
+
335
+ .stub-page h1 {
336
+ margin: 0;
337
+ }
338
+
339
+ .stub-intro {
340
+ margin: 2rem 0;
341
+ }
342
+
343
+ .stub-toc-header {
344
+ margin: 2rem 0 1rem 0;
345
+ font-size: 1.25rem;
346
+ font-weight: 600;
347
+ }
348
+
349
+ .stub-toc-list {
350
+ padding-left: 1.5rem;
351
+ margin: 0 0 2rem 0;
352
+ }
353
+
354
+ .stub-toc-item {
355
+ margin-bottom: 1rem;
311
356
  }
312
357
 
313
- .stub-page a {
358
+ .stub-toc-link {
314
359
  color: var(--text-color);
315
- text-decoration: underline;
360
+ text-decoration: none;
361
+ font-size: 1.125rem;
362
+ }
363
+
364
+ .stub-toc-link:hover {
365
+ color: var(--text-secondary);
316
366
  }
317
367
 
318
368
  .error-state {
@@ -125,10 +125,79 @@ export function createTutorialLoad({ routeBase, modules, renderMarkdown, enableD
125
125
  // Handle stub route (segment root)
126
126
  if (resolved.type === 'stub') {
127
127
  const firstPagePath = `${routeBase}/${resolved.segmentPath}/1`;
128
+
129
+ // Load and parse the segment file to get title and pages
130
+ const module = modules[resolved.filePath];
131
+ let content = '';
132
+
133
+ // Handle different module formats (eager vs lazy loading)
134
+ if (typeof module === 'function') {
135
+ // Lazy-loaded module - await it
136
+ const loaded = await module();
137
+ content = typeof loaded === 'string' ? loaded : loaded?.default || String(loaded?.default || '');
138
+ } else if (typeof module === 'string') {
139
+ content = module;
140
+ } else if (module?.default) {
141
+ content = typeof module.default === 'string' ? module.default : String(module.default);
142
+ } else if (typeof module === 'object' && 'default' in module) {
143
+ content = String(module.default);
144
+ }
145
+
146
+ const { pages, segmentTitle } = parseSegment(content);
147
+
148
+ // Extract content between H1 and first H2
149
+ let introContent = '';
150
+ const lines = content.split('\n');
151
+ let h1Index = -1;
152
+ let firstH2Index = -1;
153
+ let inCodeBlock = false;
154
+
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const line = lines[i];
157
+ const trimmed = line.trim();
158
+ const rawTrimmed = line.trimStart();
159
+
160
+ // Check for fenced code blocks
161
+ if (/^[`~]{3,}/.test(rawTrimmed)) {
162
+ inCodeBlock = !inCodeBlock;
163
+ continue;
164
+ }
165
+
166
+ // Find H1
167
+ if (h1Index === -1 && !inCodeBlock && trimmed.startsWith('# ') && !trimmed.startsWith('##')) {
168
+ h1Index = i;
169
+ continue;
170
+ }
171
+
172
+ // Find first H2
173
+ if (h1Index !== -1 && firstH2Index === -1 && !inCodeBlock && trimmed.startsWith('## ') && !trimmed.startsWith('###')) {
174
+ firstH2Index = i;
175
+ break;
176
+ }
177
+ }
178
+
179
+ // Extract content between H1 and first H2
180
+ if (h1Index !== -1 && firstH2Index !== -1 && firstH2Index > h1Index + 1) {
181
+ const introLines = lines.slice(h1Index + 1, firstH2Index);
182
+ const introText = introLines.join('\n').trim();
183
+ if (introText) {
184
+ introContent = await renderMarkdown(introText);
185
+ }
186
+ }
187
+
188
+ // Build pages array with paths
189
+ const pagesWithPaths = pages.map((page, index) => ({
190
+ title: page.title,
191
+ path: `${routeBase}/${resolved.segmentPath}/${index + 1}`,
192
+ }));
193
+
128
194
  return {
129
195
  kind: 'stub',
130
196
  segmentPath: resolved.segmentPath,
131
197
  firstPagePath,
198
+ title: segmentTitle || '',
199
+ pages: pagesWithPaths,
200
+ introContent: introContent || null,
132
201
  };
133
202
  }
134
203