create-obsidian-arrow 0.2.1 → 0.3.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.
Files changed (34) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/template/AGENTS.md +27 -3
  4. package/template/README.md +40 -1
  5. package/template/_gitignore +3 -0
  6. package/template/docs/prompts/agent-setup.md +22 -11
  7. package/template/docs/prompts/update-existing.md +1 -1
  8. package/template/docs/workflow.md +18 -6
  9. package/template/package.json +1 -1
  10. package/template/src/components/SettingsPanel.stories.ts +11 -0
  11. package/template/src/components/SettingsPanel.ts +1 -1
  12. package/template/src/components/Toggle.stories.ts +28 -0
  13. package/template/src/main.ts +1 -0
  14. package/template/src/router/client.ts +15 -2
  15. package/template/src/router/routeToPage.ts +75 -28
  16. package/template/src/sandbox/home.ts +135 -0
  17. package/template/src/sandbox/sandbox.css +307 -0
  18. package/template/src/utilities.css +205 -0
  19. package/template/src/viewer/ClassesPage.ts +37 -0
  20. package/template/src/viewer/ComponentsIndex.ts +56 -0
  21. package/template/src/viewer/StoryPage.ts +73 -0
  22. package/template/src/viewer/TokensPage.ts +82 -0
  23. package/template/src/viewer/derive.ts +81 -0
  24. package/template/src/viewer/discovery.ts +63 -0
  25. package/template/src/viewer/obsidian-classes.ts +269 -0
  26. package/template/src/viewer/sidebar.ts +55 -0
  27. package/template/src/viewer/stories.ts +83 -0
  28. package/template/src/viewer/token-utils.ts +84 -0
  29. package/template/src/viewer/tokens.ts +30 -0
  30. package/template/test/token-utils.test.mjs +65 -0
  31. package/template/test/viewer-derive.test.mjs +65 -0
  32. package/template/test/viewer-stories.test.mjs +44 -0
  33. package/template/src/examples/ExamplesIndex.ts +0 -36
  34. package/template/src/examples/registry.ts +0 -26
@@ -80,6 +80,7 @@ body {
80
80
 
81
81
  .oas-frame .oas-view-content {
82
82
  flex: 1;
83
+ min-height: 0;
83
84
  overflow-y: auto;
84
85
  padding: var(--size-4-3);
85
86
  }
@@ -109,6 +110,11 @@ body {
109
110
  align-items: center;
110
111
  }
111
112
 
113
+ .oas-frame .setting-item-control button.oas-recheck {
114
+ margin-left: var(--size-4-2);
115
+ font-size: var(--font-ui-smaller);
116
+ }
117
+
112
118
  /* Drag-to-resize handle on the pane's right edge (Obsidian-like). */
113
119
  .oas-frame .oas-resize-handle {
114
120
  position: absolute;
@@ -123,3 +129,304 @@ body {
123
129
  background: var(--interactive-accent);
124
130
  opacity: 0.5;
125
131
  }
132
+
133
+ /* Collapsible card — Getting Started accordion and other expandable sections. */
134
+ .oas-card {
135
+ border-bottom: 1px solid var(--background-modifier-border);
136
+ border-left: 3px solid var(--background-modifier-border-hover);
137
+ }
138
+
139
+ .oas-card.is-expanded {
140
+ border-left-color: var(--interactive-accent);
141
+ }
142
+
143
+ .oas-card-header {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ gap: var(--size-4-2);
148
+ padding: var(--size-4-3) var(--size-4-3);
149
+ cursor: pointer;
150
+ font-size: var(--font-ui-small);
151
+ font-weight: var(--font-semibold);
152
+ color: var(--text-normal);
153
+ user-select: none;
154
+ }
155
+
156
+ .oas-card-header:hover {
157
+ background: var(--background-modifier-hover);
158
+ }
159
+
160
+ .oas-card-title {
161
+ flex: 1;
162
+ min-width: 0;
163
+ overflow: hidden;
164
+ text-overflow: ellipsis;
165
+ white-space: nowrap;
166
+ }
167
+
168
+ .oas-card-chevron {
169
+ flex-shrink: 0;
170
+ color: var(--text-faint);
171
+ transition: transform 0.15s;
172
+ font-size: var(--font-ui-medium);
173
+ line-height: 1;
174
+ }
175
+
176
+ .oas-card.is-expanded .oas-card-chevron {
177
+ transform: rotate(90deg);
178
+ }
179
+
180
+ .oas-card-body {
181
+ display: none;
182
+ border-top: 1px solid var(--background-modifier-border);
183
+ }
184
+
185
+ .oas-card.is-expanded .oas-card-body {
186
+ display: block;
187
+ }
188
+
189
+ .oas-card-desc {
190
+ margin: 0;
191
+ padding: var(--size-4-2) var(--size-4-3);
192
+ color: var(--text-muted);
193
+ font-size: var(--font-ui-smaller);
194
+ }
195
+
196
+ .oas-card-note {
197
+ margin: 0;
198
+ padding: 0 var(--size-4-3) var(--size-4-2);
199
+ color: var(--text-muted);
200
+ font-size: var(--font-ui-smaller);
201
+ }
202
+
203
+ .oas-card-children {
204
+ display: flex;
205
+ align-items: baseline;
206
+ flex-wrap: wrap;
207
+ gap: var(--size-4-1);
208
+ padding: var(--size-4-1) var(--size-4-3);
209
+ }
210
+
211
+ .oas-card-children-label {
212
+ color: var(--text-faint);
213
+ font-size: var(--font-ui-smaller);
214
+ }
215
+
216
+ .oas-card-actions {
217
+ padding: var(--size-4-2) var(--size-4-3);
218
+ }
219
+
220
+ /* Gallery: vertical stack of cards below the heading */
221
+ .oas-gallery {
222
+ border-top: 1px solid var(--background-modifier-border);
223
+ }
224
+
225
+ /* Status badges for story live/draft state */
226
+ .oas-badge {
227
+ display: inline-flex;
228
+ align-items: center;
229
+ padding: 1px var(--size-4-1);
230
+ border-radius: var(--radius-s);
231
+ font-size: var(--font-ui-smaller);
232
+ font-weight: var(--font-medium);
233
+ vertical-align: middle;
234
+ margin-left: var(--size-4-1);
235
+ }
236
+
237
+ .oas-badge.is-live {
238
+ background: var(--background-modifier-success);
239
+ color: var(--text-success);
240
+ }
241
+
242
+ .oas-badge.is-draft {
243
+ background: var(--background-modifier-hover);
244
+ color: var(--text-faint);
245
+ }
246
+
247
+ /* Component viewer: sidebar (first child of the stage) + story page chrome. */
248
+ .oas-shell nav.oas-sidebar {
249
+ flex: 0 0 220px;
250
+ overflow-y: auto;
251
+ padding: var(--size-4-3) 0;
252
+ background: var(--background-secondary);
253
+ border-right: 1px solid var(--background-modifier-border);
254
+ }
255
+
256
+ .oas-shell nav.oas-sidebar a.oas-nav-item {
257
+ display: block;
258
+ text-decoration: none;
259
+ }
260
+
261
+ .oas-shell nav.oas-sidebar .oas-nav-invalid {
262
+ color: var(--text-error);
263
+ font-size: var(--font-ui-smaller);
264
+ }
265
+
266
+ .oas-frame .oas-story-meta {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: var(--size-4-1);
270
+ margin: var(--size-4-2) 0;
271
+ }
272
+
273
+ .oas-frame .oas-story-path {
274
+ display: flex;
275
+ align-items: center;
276
+ gap: var(--size-4-2);
277
+ font-size: var(--font-ui-smaller);
278
+ color: var(--text-muted);
279
+ }
280
+
281
+ .oas-frame .oas-story-path code {
282
+ font-family: var(--font-monospace);
283
+ color: var(--text-normal);
284
+ }
285
+
286
+ .oas-frame button.oas-copy {
287
+ height: var(--size-4-5);
288
+ padding: 0 var(--size-4-2);
289
+ font-size: var(--font-ui-smaller);
290
+ }
291
+
292
+ .oas-frame .oas-variants {
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: var(--size-4-1);
296
+ margin: var(--size-4-2) 0;
297
+ }
298
+
299
+ .oas-frame a.oas-variant {
300
+ padding: var(--size-4-1) var(--size-4-2);
301
+ border-radius: var(--radius-s);
302
+ background: var(--background-modifier-hover);
303
+ color: var(--text-normal);
304
+ font-size: var(--font-ui-smaller);
305
+ text-decoration: none;
306
+ }
307
+
308
+ .oas-frame a.oas-variant.is-active {
309
+ background: var(--interactive-accent);
310
+ color: var(--text-on-accent);
311
+ }
312
+
313
+ .oas-frame .oas-story-notes {
314
+ margin: var(--size-4-2) 0;
315
+ color: var(--text-muted);
316
+ font-size: var(--font-ui-small);
317
+ }
318
+
319
+ .oas-frame .oas-story-children {
320
+ display: flex;
321
+ flex-wrap: wrap;
322
+ gap: var(--size-4-2);
323
+ margin: var(--size-4-2) 0;
324
+ }
325
+
326
+ .oas-frame a.oas-child {
327
+ color: var(--text-accent);
328
+ font-size: var(--font-ui-small);
329
+ text-decoration: none;
330
+ }
331
+
332
+ .oas-frame .oas-child-missing {
333
+ color: var(--text-error);
334
+ font-size: var(--font-ui-small);
335
+ }
336
+
337
+ .oas-frame .oas-story-canvas {
338
+ margin-top: var(--size-4-3);
339
+ padding-top: var(--size-4-3);
340
+ border-top: 1px solid var(--background-modifier-border);
341
+ }
342
+
343
+ .oas-frame .oas-story-missing {
344
+ color: var(--text-error);
345
+ }
346
+
347
+ /* Reference index: token rows and class previews. */
348
+ .oas-frame input.oas-token-filter {
349
+ width: 100%;
350
+ margin: var(--size-4-2) 0 var(--size-4-3) 0;
351
+ }
352
+
353
+ .oas-frame .oas-token-group {
354
+ margin-bottom: var(--size-4-3);
355
+ }
356
+
357
+ .oas-frame .oas-token-row {
358
+ display: grid;
359
+ grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) 48px auto;
360
+ align-items: center;
361
+ gap: var(--size-4-2);
362
+ padding: var(--size-4-1) 0;
363
+ border-bottom: 1px solid var(--background-modifier-border);
364
+ font-size: var(--font-ui-smaller);
365
+ }
366
+
367
+ .oas-frame .oas-token-name {
368
+ font-family: var(--font-monospace);
369
+ overflow-wrap: anywhere;
370
+ }
371
+
372
+ .oas-frame .oas-token-value {
373
+ color: var(--text-muted);
374
+ overflow-wrap: anywhere;
375
+ }
376
+
377
+ .oas-frame .oas-swatch {
378
+ width: var(--size-4-4);
379
+ height: var(--size-4-4);
380
+ border-radius: var(--radius-s);
381
+ border: 1px solid var(--background-modifier-border);
382
+ justify-self: start;
383
+ }
384
+
385
+ .oas-frame .oas-sizebar {
386
+ height: var(--size-4-2);
387
+ max-width: 48px;
388
+ background: var(--text-accent);
389
+ border-radius: var(--radius-s);
390
+ justify-self: start;
391
+ }
392
+
393
+ .oas-frame .oas-swatch-none {
394
+ width: var(--size-4-4);
395
+ }
396
+
397
+ .oas-frame .oas-class-group {
398
+ margin-bottom: var(--size-4-4);
399
+ }
400
+
401
+ .oas-frame .oas-class-entry {
402
+ margin: var(--size-4-3) 0;
403
+ }
404
+
405
+ .oas-frame .oas-class-head {
406
+ display: flex;
407
+ align-items: center;
408
+ gap: var(--size-4-2);
409
+ font-family: var(--font-monospace);
410
+ font-size: var(--font-ui-small);
411
+ }
412
+
413
+ .oas-frame .oas-class-when {
414
+ margin: var(--size-4-1) 0;
415
+ color: var(--text-muted);
416
+ font-size: var(--font-ui-smaller);
417
+ }
418
+
419
+ /* transform creates a containing block, so position:fixed descendants (e.g.
420
+ .modal) stay inside the preview box instead of covering the viewport. */
421
+ .oas-frame .oas-class-preview {
422
+ padding: var(--size-4-3);
423
+ border: 1px dashed var(--background-modifier-border);
424
+ border-radius: var(--radius-m);
425
+ transform: translate(0);
426
+ }
427
+
428
+ .oas-frame .modal.oas-modal-preview {
429
+ position: static;
430
+ width: auto;
431
+ max-width: 100%;
432
+ }
@@ -0,0 +1,205 @@
1
+ /*
2
+ * Portable utility classes for Obsidian Arrow components.
3
+ *
4
+ * These travel with component ports into the plugin — copy this file once into
5
+ * the plugin's styles directory and import it alongside your component CSS.
6
+ * All classes are prefixed `oas-` to avoid any conflict with Obsidian's own
7
+ * selectors. Values use Obsidian's CSS custom property scale so they track the
8
+ * active theme automatically.
9
+ */
10
+
11
+ /* ── Layout ──────────────────────────────────────────────────── */
12
+ .oas-flex {
13
+ display: flex;
14
+ }
15
+ .oas-inline-flex {
16
+ display: inline-flex;
17
+ }
18
+ .oas-flex-col {
19
+ flex-direction: column;
20
+ }
21
+ .oas-flex-wrap {
22
+ flex-wrap: wrap;
23
+ }
24
+ .oas-items-start {
25
+ align-items: flex-start;
26
+ }
27
+ .oas-items-center {
28
+ align-items: center;
29
+ }
30
+ .oas-items-end {
31
+ align-items: flex-end;
32
+ }
33
+ .oas-items-baseline {
34
+ align-items: baseline;
35
+ }
36
+ .oas-justify-start {
37
+ justify-content: flex-start;
38
+ }
39
+ .oas-justify-center {
40
+ justify-content: center;
41
+ }
42
+ .oas-justify-end {
43
+ justify-content: flex-end;
44
+ }
45
+ .oas-justify-between {
46
+ justify-content: space-between;
47
+ }
48
+ .oas-grow {
49
+ flex: 1;
50
+ }
51
+ .oas-shrink-0 {
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ /* ── Spacing (Obsidian 4-px step scale) ─────────────────────── */
56
+ .oas-gap-1 {
57
+ gap: var(--size-4-1);
58
+ }
59
+ .oas-gap-2 {
60
+ gap: var(--size-4-2);
61
+ }
62
+ .oas-gap-3 {
63
+ gap: var(--size-4-3);
64
+ }
65
+ .oas-gap-4 {
66
+ gap: var(--size-4-4);
67
+ }
68
+ .oas-p-1 {
69
+ padding: var(--size-4-1);
70
+ }
71
+ .oas-p-2 {
72
+ padding: var(--size-4-2);
73
+ }
74
+ .oas-p-3 {
75
+ padding: var(--size-4-3);
76
+ }
77
+ .oas-p-4 {
78
+ padding: var(--size-4-4);
79
+ }
80
+ .oas-px-2 {
81
+ padding-inline: var(--size-4-2);
82
+ }
83
+ .oas-px-3 {
84
+ padding-inline: var(--size-4-3);
85
+ }
86
+ .oas-py-1 {
87
+ padding-block: var(--size-4-1);
88
+ }
89
+ .oas-py-2 {
90
+ padding-block: var(--size-4-2);
91
+ }
92
+ .oas-py-3 {
93
+ padding-block: var(--size-4-3);
94
+ }
95
+ .oas-mt-1 {
96
+ margin-top: var(--size-4-1);
97
+ }
98
+ .oas-mt-2 {
99
+ margin-top: var(--size-4-2);
100
+ }
101
+ .oas-mt-3 {
102
+ margin-top: var(--size-4-3);
103
+ }
104
+ .oas-mb-1 {
105
+ margin-bottom: var(--size-4-1);
106
+ }
107
+ .oas-mb-2 {
108
+ margin-bottom: var(--size-4-2);
109
+ }
110
+ .oas-mb-3 {
111
+ margin-bottom: var(--size-4-3);
112
+ }
113
+ .oas-ml-auto {
114
+ margin-left: auto;
115
+ }
116
+
117
+ /* ── Sizing ──────────────────────────────────────────────────── */
118
+ .oas-w-full {
119
+ width: 100%;
120
+ }
121
+ .oas-min-w-0 {
122
+ min-width: 0;
123
+ }
124
+ .oas-min-h-0 {
125
+ min-height: 0;
126
+ }
127
+
128
+ /* ── Typography ──────────────────────────────────────────────── */
129
+ .oas-text-xs {
130
+ font-size: var(--font-ui-smaller);
131
+ }
132
+ .oas-text-sm {
133
+ font-size: var(--font-ui-small);
134
+ }
135
+ .oas-text-md {
136
+ font-size: var(--font-ui-medium);
137
+ }
138
+ .oas-font-medium {
139
+ font-weight: var(--font-medium);
140
+ }
141
+ .oas-font-semibold {
142
+ font-weight: var(--font-semibold);
143
+ }
144
+ .oas-font-mono {
145
+ font-family: var(--font-monospace);
146
+ }
147
+ .oas-leading-1 {
148
+ line-height: 1;
149
+ }
150
+ .oas-text-normal {
151
+ color: var(--text-normal);
152
+ }
153
+ .oas-text-muted {
154
+ color: var(--text-muted);
155
+ }
156
+ .oas-text-faint {
157
+ color: var(--text-faint);
158
+ }
159
+ .oas-text-accent {
160
+ color: var(--text-accent);
161
+ }
162
+ .oas-text-success {
163
+ color: var(--text-success);
164
+ }
165
+ .oas-text-error {
166
+ color: var(--text-error);
167
+ }
168
+
169
+ /* ── Overflow ────────────────────────────────────────────────── */
170
+ .oas-truncate {
171
+ overflow: hidden;
172
+ text-overflow: ellipsis;
173
+ white-space: nowrap;
174
+ }
175
+ .oas-overflow-hidden {
176
+ overflow: hidden;
177
+ }
178
+ .oas-overflow-auto {
179
+ overflow: auto;
180
+ }
181
+
182
+ /* ── Border ──────────────────────────────────────────────────── */
183
+ .oas-border {
184
+ border: 1px solid var(--background-modifier-border);
185
+ }
186
+ .oas-border-b {
187
+ border-bottom: 1px solid var(--background-modifier-border);
188
+ }
189
+ .oas-border-t {
190
+ border-top: 1px solid var(--background-modifier-border);
191
+ }
192
+ .oas-rounded-s {
193
+ border-radius: var(--radius-s);
194
+ }
195
+ .oas-rounded-m {
196
+ border-radius: var(--radius-m);
197
+ }
198
+
199
+ /* ── Interaction ─────────────────────────────────────────────── */
200
+ .oas-cursor-pointer {
201
+ cursor: pointer;
202
+ }
203
+ .oas-select-none {
204
+ user-select: none;
205
+ }
@@ -0,0 +1,37 @@
1
+ import { component, html } from "@arrow-js/core";
2
+ import { copyText } from "./StoryPage";
3
+ import { classGroups } from "./obsidian-classes";
4
+
5
+ export const ClassesPage = component(() => {
6
+ return html`
7
+ <div class="oas-reference">
8
+ <div class="setting-item setting-item-heading">
9
+ <div class="setting-item-info">
10
+ <div class="setting-item-name">Obsidian classes</div>
11
+ <div class="setting-item-description">
12
+ Curated pattern classes worth leveraging, rendered live against app.css.
13
+ </div>
14
+ </div>
15
+ </div>
16
+ ${classGroups.map(
17
+ (group) => html`
18
+ <div class="oas-class-group">
19
+ <div class="vertical-tab-header-group-title">${group.label}</div>
20
+ ${group.entries.map(
21
+ (entry) => html`
22
+ <div class="oas-class-entry">
23
+ <div class="oas-class-head">
24
+ <code>${entry.className}</code>
25
+ <button class="oas-copy" @click="${() => copyText(entry.className)}">Copy</button>
26
+ </div>
27
+ <div class="oas-class-when">${entry.whenToUse}</div>
28
+ <div class="oas-class-preview">${entry.preview()}</div>
29
+ </div>
30
+ `
31
+ )}
32
+ </div>
33
+ `
34
+ )}
35
+ </div>
36
+ `;
37
+ });
@@ -0,0 +1,56 @@
1
+ import { html } from "@arrow-js/core";
2
+ import type { ArrowExpression } from "@arrow-js/core";
3
+ import { stories } from "./discovery";
4
+
5
+ export function ComponentsIndex(): ArrowExpression {
6
+ if (stories.length === 0) {
7
+ return html`
8
+ <div class="oas-settings">
9
+ <div class="setting-item setting-item-heading">
10
+ <div class="setting-item-info">
11
+ <div class="setting-item-name">Components</div>
12
+ <div class="setting-item-description">
13
+ No stories found. Create a <code>*.stories.ts</code> file next to a component.
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ `;
19
+ }
20
+ return html`
21
+ <div class="oas-settings">
22
+ <div class="setting-item setting-item-heading">
23
+ <div class="setting-item-info">
24
+ <div class="setting-item-name">Components</div>
25
+ <div class="setting-item-description">
26
+ ${stories.length} ${stories.length === 1 ? "story" : "stories"} — click to open.
27
+ </div>
28
+ </div>
29
+ </div>
30
+ ${stories.map((story) => {
31
+ const path = `/components/${story.slug}`;
32
+ const badge =
33
+ story.status === "live"
34
+ ? html`<span class="oas-badge is-live">live</span>`
35
+ : html`<span class="oas-badge is-draft">draft</span>`;
36
+ return html`
37
+ <div class="setting-item">
38
+ <div class="setting-item-info">
39
+ <div class="setting-item-name">
40
+ <a href="${path}">${story.title}</a> ${badge}
41
+ </div>
42
+ ${
43
+ story.description
44
+ ? html`<div class="setting-item-description">${story.description}</div>`
45
+ : ""
46
+ }
47
+ </div>
48
+ <div class="setting-item-control">
49
+ <a class="mod-cta oas-open-link" href="${path}">Open →</a>
50
+ </div>
51
+ </div>
52
+ `.key(story.slug);
53
+ })}
54
+ </div>
55
+ `;
56
+ }
@@ -0,0 +1,73 @@
1
+ import { html } from "@arrow-js/core";
2
+ import type { ArrowExpression } from "@arrow-js/core";
3
+ import type { DiscoveredStory } from "./discovery";
4
+ import { findStory } from "./discovery";
5
+
6
+ /** Copy to clipboard, best-effort (clipboard API needs a secure context). */
7
+ export function copyText(text: string): void {
8
+ void navigator.clipboard?.writeText(text);
9
+ }
10
+
11
+ function pathRow(label: string, path: string): ArrowExpression {
12
+ return html`
13
+ <div class="oas-story-path">
14
+ <span class="oas-path-label">${label}</span>
15
+ <code>${path}</code>
16
+ <button class="oas-copy" @click="${() => copyText(path)}">Copy</button>
17
+ </div>
18
+ `;
19
+ }
20
+
21
+ export function StoryPage(story: DiscoveredStory, variantName: string): ArrowExpression {
22
+ const variant = story.variants[variantName];
23
+ const variantNames = Object.keys(story.variants);
24
+ const badge =
25
+ story.status === "live"
26
+ ? html`<span class="oas-badge is-live">live</span>`
27
+ : html`<span class="oas-badge is-draft">draft</span>`;
28
+ return html`
29
+ <div class="oas-story">
30
+ <div class="setting-item setting-item-heading">
31
+ <div class="setting-item-info">
32
+ <div class="setting-item-name">${story.title} ${badge}</div>
33
+ ${
34
+ story.description
35
+ ? html`<div class="setting-item-description">${story.description}</div>`
36
+ : ""
37
+ }
38
+ </div>
39
+ </div>
40
+ <div class="oas-story-meta">
41
+ ${pathRow("component", story.componentPath)}
42
+ ${pathRow("stories", story.storiesPath)}
43
+ </div>
44
+ <div class="oas-variants">
45
+ ${variantNames.map((name) => {
46
+ const cls = name === variantName ? "oas-variant is-active" : "oas-variant";
47
+ const href = `/components/${story.slug}?variant=${encodeURIComponent(name)}`;
48
+ return html`<a class="${cls}" href="${href}">${name}</a>`;
49
+ })}
50
+ </div>
51
+ ${variant?.notes ? html`<div class="oas-story-notes">${variant.notes}</div>` : ""}
52
+ ${
53
+ story.children.length > 0
54
+ ? html`<div class="oas-story-children">
55
+ ${story.children.map((slug) => {
56
+ const child = findStory(slug);
57
+ return child
58
+ ? html`<a class="oas-child" href="${`/components/${slug}`}">${child.title} →</a>`
59
+ : html`<span class="oas-child-missing">${slug} (missing story)</span>`;
60
+ })}
61
+ </div>`
62
+ : ""
63
+ }
64
+ <div class="oas-story-canvas">
65
+ ${
66
+ variant
67
+ ? variant.render()
68
+ : html`<div class="oas-story-missing">No variant "${variantName}" — pick one above.</div>`
69
+ }
70
+ </div>
71
+ </div>
72
+ `;
73
+ }