create-obsidian-arrow 0.5.0 → 0.5.2

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 (61) hide show
  1. package/README.md +7 -7
  2. package/cli/create.mjs +65 -0
  3. package/cli/detect-pm.mjs +20 -0
  4. package/cli/lib.mjs +117 -0
  5. package/cli/refresh.mjs +65 -0
  6. package/index.mjs +47 -204
  7. package/package.json +11 -2
  8. package/template/.husky/pre-commit +3 -2
  9. package/template/AGENTS.md +58 -12
  10. package/template/README.md +67 -31
  11. package/template/_gitignore +4 -1
  12. package/template/biome.json +7 -1
  13. package/template/docs/prompts/agent-setup.md +24 -20
  14. package/template/docs/prompts/update-existing.md +3 -3
  15. package/template/docs/workflow.md +11 -7
  16. package/template/package.json +15 -14
  17. package/template/src/components/DiffViewer/DiffViewer.css +41 -0
  18. package/template/src/components/DiffViewer/DiffViewer.ts +55 -0
  19. package/template/src/components/EmptyState/EmptyState.css +5 -5
  20. package/template/src/components/EmptyState/EmptyState.ts +5 -9
  21. package/template/src/utilities.css +259 -1
  22. package/template/src/views/DiffViewer/DiffViewerView.css +42 -0
  23. package/template/src/views/DiffViewer/DiffViewerView.ts +53 -0
  24. package/template/src/views/ExampleView/ExampleView.ts +92 -0
  25. package/template/stories/components/ComponentShell.stories.ts +28 -0
  26. package/template/stories/components/EmptyState.stories.ts +1 -0
  27. package/template/stories/components/LoadingState.stories.ts +1 -0
  28. package/template/stories/components/Toggle.stories.ts +50 -0
  29. package/template/stories/views/DiffViewer/DiffViewer.stories.ts +94 -0
  30. package/template/stories/views/EditorView.stories.ts +55 -0
  31. package/template/stories/views/ExampleView/ExampleView.stories.ts +15 -0
  32. package/template/stories/views/PanelView.stories.ts +61 -0
  33. package/template/stories/views/SettingsPanel/SettingsPanel.stories.ts +14 -0
  34. package/template/test/css-structure.test.mjs +112 -0
  35. package/template/test/template-footguns.test.mjs +85 -6
  36. package/template/test/viewer-stories.test.mjs +12 -0
  37. package/template/tools/router/client.ts +26 -4
  38. package/template/tools/router/routeToPage.ts +29 -13
  39. package/template/tools/sandbox/frame.ts +7 -27
  40. package/template/tools/sandbox/home.ts +6 -11
  41. package/template/tools/sandbox/layout.ts +24 -2
  42. package/template/tools/sandbox/sandbox.css +188 -226
  43. package/template/tools/sandbox/shell.ts +2 -2
  44. package/template/tools/sandbox/toolbar.ts +20 -9
  45. package/template/tools/viewer/ClassesPage.ts +7 -7
  46. package/template/tools/viewer/ComponentsIndex.ts +3 -3
  47. package/template/tools/viewer/StoryPage.ts +53 -40
  48. package/template/tools/viewer/TokensPage.ts +10 -10
  49. package/template/tools/viewer/ViewsIndex.ts +66 -0
  50. package/template/tools/viewer/discovery.ts +2 -0
  51. package/template/tools/viewer/obsidian-classes.ts +1 -1
  52. package/template/tools/viewer/sidebar.ts +27 -38
  53. package/template/tools/viewer/stories.ts +16 -2
  54. package/template/.github/workflows/ci.yml +0 -36
  55. package/template/pnpm-lock.yaml +0 -1608
  56. package/template/scripts/create-component.mjs +0 -101
  57. package/template/scripts/create-view.mjs +0 -75
  58. package/template/src/components/DiffViewer.ts +0 -42
  59. package/template/stories/DiffViewer.stories.ts +0 -75
  60. package/template/stories/SettingsPanel.stories.ts +0 -11
  61. package/template/stories/Toggle.stories.ts +0 -28
@@ -1,4 +1,4 @@
1
- .empty-state {
1
+ .oas-empty-state {
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  align-items: center;
@@ -8,23 +8,23 @@
8
8
  color: var(--text-muted);
9
9
  }
10
10
 
11
- .empty-state-icon {
11
+ .oas-empty-icon {
12
12
  font-size: var(--font-ui-larger, 2rem);
13
13
  line-height: 1;
14
14
  color: var(--text-faint);
15
15
  }
16
16
 
17
- .empty-state-title {
17
+ .oas-empty-title {
18
18
  font-size: var(--font-ui-medium);
19
19
  font-weight: var(--font-semibold);
20
20
  color: var(--text-normal);
21
21
  }
22
22
 
23
- .empty-state-description {
23
+ .oas-empty-desc {
24
24
  font-size: var(--font-ui-small);
25
25
  max-width: 280px;
26
26
  }
27
27
 
28
- .empty-state-action {
28
+ .oas-empty-action {
29
29
  margin-top: var(--size-4-2);
30
30
  }
@@ -12,18 +12,14 @@ export interface EmptyStateOptions {
12
12
 
13
13
  export function EmptyState(options: EmptyStateOptions): ArrowExpression {
14
14
  return html`
15
- <div class="empty-state">
16
- ${options.icon ? html`<div class="empty-state-icon">${icon(options.icon)}</div>` : ""}
17
- <div class="empty-state-title">${options.title}</div>
18
- ${
19
- options.description
20
- ? html`<div class="empty-state-description">${options.description}</div>`
21
- : ""
22
- }
15
+ <div class="oas-empty-state">
16
+ ${options.icon ? html`<div class="oas-empty-icon">${icon(options.icon)}</div>` : ""}
17
+ <div class="oas-empty-title">${options.title}</div>
18
+ ${options.description ? html`<div class="oas-empty-desc">${options.description}</div>` : ""}
23
19
  ${
24
20
  options.action
25
21
  ? html`<button
26
- class="mod-cta empty-state-action"
22
+ class="mod-cta oas-empty-action"
27
23
  @click="${options.action.onClick}"
28
24
  >
29
25
  ${options.action.label}
@@ -52,7 +52,56 @@
52
52
  flex-shrink: 0;
53
53
  }
54
54
 
55
- /* ── Spacing (Obsidian 4-px step scale) ─────────────────────── */
55
+ /*
56
+ * Spacing — two Obsidian token families:
57
+ *
58
+ * 4-px scale oas-gap-N / oas-p-N / oas-px-N / oas-py-N / oas-mt-N / oas-mb-N
59
+ * N=1 → --size-4-1 (4px) N=2 → --size-4-2 (8px) N=3 → --size-4-3 (12px)
60
+ * N=4 → --size-4-4 (16px) N=5 → --size-4-5 (20px) N=6 → --size-4-6 (24px)
61
+ *
62
+ * 2-px sub-scale oas-gap-2-1 / oas-p-2-1 … (mirrors Obsidian token name)
63
+ * 2-1 → --size-2-1 (2px) 2-3 → --size-2-3 (6px)
64
+ */
65
+
66
+ /* 2-px sub-scale ── */
67
+ .oas-gap-2-1 {
68
+ gap: var(--size-2-1);
69
+ }
70
+ .oas-p-2-1 {
71
+ padding: var(--size-2-1);
72
+ }
73
+ .oas-px-2-1 {
74
+ padding-inline: var(--size-2-1);
75
+ }
76
+ .oas-py-2-1 {
77
+ padding-block: var(--size-2-1);
78
+ }
79
+ .oas-mt-2-1 {
80
+ margin-top: var(--size-2-1);
81
+ }
82
+ .oas-mb-2-1 {
83
+ margin-bottom: var(--size-2-1);
84
+ }
85
+ .oas-gap-2-3 {
86
+ gap: var(--size-2-3);
87
+ }
88
+ .oas-p-2-3 {
89
+ padding: var(--size-2-3);
90
+ }
91
+ .oas-px-2-3 {
92
+ padding-inline: var(--size-2-3);
93
+ }
94
+ .oas-py-2-3 {
95
+ padding-block: var(--size-2-3);
96
+ }
97
+ .oas-mt-2-3 {
98
+ margin-top: var(--size-2-3);
99
+ }
100
+ .oas-mb-2-3 {
101
+ margin-bottom: var(--size-2-3);
102
+ }
103
+
104
+ /* 4-px scale ── */
56
105
  .oas-gap-1 {
57
106
  gap: var(--size-4-1);
58
107
  }
@@ -65,6 +114,12 @@
65
114
  .oas-gap-4 {
66
115
  gap: var(--size-4-4);
67
116
  }
117
+ .oas-gap-5 {
118
+ gap: var(--size-4-5);
119
+ }
120
+ .oas-gap-6 {
121
+ gap: var(--size-4-6);
122
+ }
68
123
  .oas-p-1 {
69
124
  padding: var(--size-4-1);
70
125
  }
@@ -77,12 +132,27 @@
77
132
  .oas-p-4 {
78
133
  padding: var(--size-4-4);
79
134
  }
135
+ .oas-p-5 {
136
+ padding: var(--size-4-5);
137
+ }
138
+ .oas-p-6 {
139
+ padding: var(--size-4-6);
140
+ }
80
141
  .oas-px-2 {
81
142
  padding-inline: var(--size-4-2);
82
143
  }
83
144
  .oas-px-3 {
84
145
  padding-inline: var(--size-4-3);
85
146
  }
147
+ .oas-px-4 {
148
+ padding-inline: var(--size-4-4);
149
+ }
150
+ .oas-px-5 {
151
+ padding-inline: var(--size-4-5);
152
+ }
153
+ .oas-px-6 {
154
+ padding-inline: var(--size-4-6);
155
+ }
86
156
  .oas-py-1 {
87
157
  padding-block: var(--size-4-1);
88
158
  }
@@ -92,6 +162,15 @@
92
162
  .oas-py-3 {
93
163
  padding-block: var(--size-4-3);
94
164
  }
165
+ .oas-py-4 {
166
+ padding-block: var(--size-4-4);
167
+ }
168
+ .oas-py-5 {
169
+ padding-block: var(--size-4-5);
170
+ }
171
+ .oas-py-6 {
172
+ padding-block: var(--size-4-6);
173
+ }
95
174
  .oas-mt-1 {
96
175
  margin-top: var(--size-4-1);
97
176
  }
@@ -101,6 +180,15 @@
101
180
  .oas-mt-3 {
102
181
  margin-top: var(--size-4-3);
103
182
  }
183
+ .oas-mt-4 {
184
+ margin-top: var(--size-4-4);
185
+ }
186
+ .oas-mt-5 {
187
+ margin-top: var(--size-4-5);
188
+ }
189
+ .oas-mt-6 {
190
+ margin-top: var(--size-4-6);
191
+ }
104
192
  .oas-mb-1 {
105
193
  margin-bottom: var(--size-4-1);
106
194
  }
@@ -110,6 +198,15 @@
110
198
  .oas-mb-3 {
111
199
  margin-bottom: var(--size-4-3);
112
200
  }
201
+ .oas-mb-4 {
202
+ margin-bottom: var(--size-4-4);
203
+ }
204
+ .oas-mb-5 {
205
+ margin-bottom: var(--size-4-5);
206
+ }
207
+ .oas-mb-6 {
208
+ margin-bottom: var(--size-4-6);
209
+ }
113
210
  .oas-ml-auto {
114
211
  margin-left: auto;
115
212
  }
@@ -195,6 +292,9 @@
195
292
  .oas-rounded-m {
196
293
  border-radius: var(--radius-m);
197
294
  }
295
+ .oas-rounded-l {
296
+ border-radius: var(--radius-l);
297
+ }
198
298
 
199
299
  /* ── Interaction ─────────────────────────────────────────────── */
200
300
  .oas-cursor-pointer {
@@ -203,3 +303,161 @@
203
303
  .oas-select-none {
204
304
  user-select: none;
205
305
  }
306
+
307
+ /* ── Badges ──────────────────────────────────────────────────── */
308
+ /* Status and label badges. Portable — copy with components into plugin. */
309
+ .oas-badge {
310
+ display: inline-flex;
311
+ align-items: center;
312
+ padding: 1px var(--size-4-1);
313
+ border-radius: var(--radius-s);
314
+ font-size: var(--font-ui-smaller);
315
+ font-weight: var(--font-medium);
316
+ vertical-align: middle;
317
+ margin-left: var(--size-4-1);
318
+ }
319
+ .oas-badge.is-live {
320
+ background: color-mix(in srgb, var(--text-success) 15%, var(--background-primary));
321
+ color: var(--text-success);
322
+ }
323
+ .oas-badge.is-draft {
324
+ background: var(--background-modifier-hover);
325
+ color: var(--text-faint);
326
+ }
327
+
328
+ /* ── Collapsible card ────────────────────────────────────────── */
329
+ /* Expandable section with left-border accent. Portable — copy with components. */
330
+ .oas-card {
331
+ border-bottom: 1px solid var(--background-modifier-border);
332
+ border-left: 3px solid var(--background-modifier-border-hover);
333
+ }
334
+ .oas-card.is-expanded {
335
+ border-left-color: var(--interactive-accent);
336
+ }
337
+ .oas-card-header {
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: space-between;
341
+ gap: var(--size-4-2);
342
+ padding: var(--size-4-3);
343
+ cursor: pointer;
344
+ font-size: var(--font-ui-small);
345
+ font-weight: var(--font-semibold);
346
+ color: var(--text-normal);
347
+ user-select: none;
348
+ }
349
+ .oas-card-header:hover {
350
+ background: var(--background-modifier-hover);
351
+ }
352
+ .oas-card-title {
353
+ flex: 1;
354
+ min-width: 0;
355
+ overflow: hidden;
356
+ text-overflow: ellipsis;
357
+ white-space: nowrap;
358
+ }
359
+ .oas-card-chevron {
360
+ flex-shrink: 0;
361
+ color: var(--text-faint);
362
+ transition: transform 0.15s;
363
+ font-size: var(--font-ui-medium);
364
+ line-height: 1;
365
+ }
366
+ .oas-card.is-expanded .oas-card-chevron {
367
+ transform: rotate(90deg);
368
+ }
369
+ .oas-card-body {
370
+ display: none;
371
+ border-top: 1px solid var(--background-modifier-border);
372
+ }
373
+ .oas-card.is-expanded .oas-card-body {
374
+ display: block;
375
+ }
376
+ .oas-card-desc {
377
+ margin: 0;
378
+ padding: var(--size-4-2) var(--size-4-3);
379
+ color: var(--text-muted);
380
+ font-size: var(--font-ui-smaller);
381
+ }
382
+ .oas-card-note {
383
+ margin: 0;
384
+ padding: 0 var(--size-4-3) var(--size-4-2);
385
+ color: var(--text-muted);
386
+ font-size: var(--font-ui-smaller);
387
+ }
388
+ .oas-card-children {
389
+ display: flex;
390
+ align-items: baseline;
391
+ flex-wrap: wrap;
392
+ gap: var(--size-4-1);
393
+ padding: var(--size-4-1) var(--size-4-3);
394
+ }
395
+ .oas-card-children-label {
396
+ color: var(--text-faint);
397
+ font-size: var(--font-ui-smaller);
398
+ }
399
+ .oas-card-actions {
400
+ padding: var(--size-4-2) var(--size-4-3);
401
+ }
402
+
403
+ /* ── View & component shells ─────────────────────────────────── */
404
+ /*
405
+ * Portable structural shells. Copy these with your view/component into the
406
+ * plugin — they rely only on Obsidian tokens and need no sandbox-specific CSS.
407
+ *
408
+ * View shell: apply oas-shell-view to the root element your view returns.
409
+ * The view-content div (Obsidian) is the scroll container; oas-shell-view
410
+ * fills it as a flex column so the body can grow and the footer stays pinned.
411
+ *
412
+ * Component shell: apply oas-shell-panel to a standalone component root for a
413
+ * card-style container with border, radius, and padding.
414
+ */
415
+ .oas-shell-view {
416
+ display: flex;
417
+ flex-direction: column;
418
+ height: 100%;
419
+ min-height: 0;
420
+ }
421
+ /* Header: pinned to the top, never shrinks, has a min-height floor but grows
422
+ * to fit taller content (e.g. a wrapping toolbar). */
423
+ .oas-shell-view-header {
424
+ flex: 0 0 auto;
425
+ min-height: var(--header-height);
426
+ }
427
+ /* Body: fills all space between header and footer; scrolls its own overflow. */
428
+ .oas-shell-view-body {
429
+ flex: 1 1 auto;
430
+ min-height: 0;
431
+ overflow-y: auto;
432
+ }
433
+ /* Footer: pinned to the bottom, never shrinks, has a min-height floor but
434
+ * expands to fit contained components (e.g. a composer that grows with input). */
435
+ .oas-shell-view-footer {
436
+ flex: 0 0 auto;
437
+ min-height: var(--header-height);
438
+ border-top: 1px solid var(--background-modifier-border);
439
+ }
440
+ .oas-shell-panel {
441
+ display: flex;
442
+ flex-direction: column;
443
+ gap: var(--size-4-2);
444
+ padding: var(--size-4-3);
445
+ background: var(--background-primary);
446
+ border: 1px solid var(--background-modifier-border);
447
+ border-radius: var(--radius-m);
448
+ }
449
+
450
+ /* ── Readable width (editor-style views) ─────────────────────────
451
+ * Reproduces Obsidian's "readable line length" for a *custom* view: caps
452
+ * content at the same --file-line-width token the markdown editor uses and
453
+ * centers it, with the editor's horizontal --file-margins. A custom ItemView
454
+ * does not inherit Obsidian's markdown editor chrome, so an editor-style
455
+ * (note/document) view wraps its content in this class to match. Portable: the
456
+ * tokens resolve against the active theme in both the sandbox and the plugin.
457
+ */
458
+ .oas-readable-width {
459
+ width: 100%;
460
+ max-width: var(--file-line-width);
461
+ margin-inline: auto;
462
+ padding-inline: var(--file-margins-x);
463
+ }
@@ -0,0 +1,42 @@
1
+ /* DiffViewer as a full-pane editor view. */
2
+
3
+ /* Label row above each editor pane. */
4
+ .oas-diff-view-labels {
5
+ display: flex;
6
+ border-bottom: 1px solid var(--background-modifier-border);
7
+ flex-shrink: 0;
8
+ }
9
+
10
+ .oas-diff-view-label {
11
+ flex: 1;
12
+ padding: var(--size-2-1) var(--size-4-3);
13
+ font-size: var(--font-ui-smaller);
14
+ color: var(--text-muted);
15
+ overflow: hidden;
16
+ text-overflow: ellipsis;
17
+ white-space: nowrap;
18
+ border-right: 1px solid var(--background-modifier-border);
19
+ }
20
+
21
+ .oas-diff-view-label:last-child {
22
+ border-right: none;
23
+ }
24
+
25
+ /* Body: fills remaining height. position: relative lets the diff viewer use inset: 0. */
26
+ .oas-diff-view-body {
27
+ flex: 1;
28
+ min-height: 0;
29
+ overflow: hidden;
30
+ position: relative;
31
+ }
32
+
33
+ /* Absolute positioning gives CM6 an explicit pixel height so height: 100% resolves
34
+ correctly on .cm-mergeView. Flex-resolved heights (flex: 1 + height: auto) don't
35
+ propagate to percentage-height grandchildren in all browsers. */
36
+ .oas-diff-view-body .oas-diff-viewer {
37
+ position: absolute;
38
+ inset: 0;
39
+ height: auto;
40
+ border: none;
41
+ border-radius: 0;
42
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * DiffViewerView — full-pane editor view wrapping the DiffViewer component.
3
+ *
4
+ * Shell anatomy:
5
+ * oas-shell-view-header pinned header: file labels + accept/reject actions
6
+ * oas-diff-view-labels per-pane filename labels (original | modified)
7
+ * oas-diff-view-body fills remaining height; CM6 handles internal scroll
8
+ *
9
+ * Duplicate this folder, rename DiffViewerView → YourDiffView, and wire in
10
+ * your real file content. The oas-shell-* classes handle the flex layout.
11
+ */
12
+
13
+ import { html } from "@arrow-js/core";
14
+ import type { ArrowExpression } from "@arrow-js/core";
15
+ import { DiffViewer } from "../../components/DiffViewer/DiffViewer";
16
+ import "./DiffViewerView.css";
17
+
18
+ export interface DiffViewerViewOptions {
19
+ original: string;
20
+ modified: string;
21
+ originalLabel?: string;
22
+ modifiedLabel?: string;
23
+ }
24
+
25
+ export function DiffViewerView({
26
+ original,
27
+ modified,
28
+ originalLabel = "Original",
29
+ modifiedLabel = "Modified",
30
+ }: DiffViewerViewOptions): ArrowExpression {
31
+ return html`
32
+ <div class="oas-shell-view">
33
+ <div class="oas-shell-view-header">
34
+ <div class="oas-flex oas-items-center oas-justify-between oas-px-3 oas-py-2-1">
35
+ <span class="oas-font-semibold oas-text-sm">Changes</span>
36
+ <div class="oas-flex oas-gap-2">
37
+ <button class="mod-cta" style="font-size: var(--font-ui-smaller);">Accept all</button>
38
+ <button class="mod-destructive" style="font-size: var(--font-ui-smaller);">Reject all</button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="oas-diff-view-labels">
44
+ <div class="oas-diff-view-label">${originalLabel}</div>
45
+ <div class="oas-diff-view-label">${modifiedLabel}</div>
46
+ </div>
47
+
48
+ <div class="oas-diff-view-body">
49
+ ${DiffViewer({ original, modified })}
50
+ </div>
51
+ </div>
52
+ `;
53
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ExampleView — a fuller reference for a new Obsidian plugin view.
3
+ *
4
+ * Shows header + scrollable list body + pinned footer input, matching the
5
+ * ChatView shell pattern from pi-vault-mind. Duplicate this folder, rename
6
+ * ExampleView → YourView everywhere, and replace the stub content.
7
+ *
8
+ * Shell anatomy (oas-shell-* classes handle all layout — no flex CSS to write):
9
+ * oas-shell-view-header pinned top bar: title + actions
10
+ * oas-shell-view-body scrollable content area
11
+ * oas-shell-view-footer pinned bottom bar: input + action button
12
+ */
13
+
14
+ import { html } from "@arrow-js/core";
15
+ import type { ArrowExpression, ArrowTemplate } from "@arrow-js/core";
16
+ import { EmptyState } from "../../components/EmptyState/EmptyState";
17
+
18
+ /** Stub item type — replace with your real data model. */
19
+ interface ExampleItem {
20
+ id: string;
21
+ label: string;
22
+ note?: string;
23
+ }
24
+
25
+ const STUB_ITEMS: ExampleItem[] = [
26
+ { id: "1", label: "First item", note: "This is an example note." },
27
+ { id: "2", label: "Second item" },
28
+ { id: "3", label: "Third item", note: "Another note." },
29
+ { id: "4", label: "Fourth item" },
30
+ { id: "5", label: "Fifth item", note: "More content here." },
31
+ { id: "6", label: "Sixth item" },
32
+ { id: "7", label: "Seventh item", note: "Scroll to see this." },
33
+ { id: "8", label: "Eighth item" },
34
+ ];
35
+
36
+ function ItemRow(item: ExampleItem): ArrowTemplate {
37
+ return html`
38
+ <div class="setting-item">
39
+ <div class="setting-item-info">
40
+ <div class="setting-item-name">${item.label}</div>
41
+ ${item.note ? html`<div class="setting-item-description">${item.note}</div>` : ""}
42
+ </div>
43
+ <div class="setting-item-control">
44
+ <button class="clickable-icon" aria-label="Options">⋯</button>
45
+ </div>
46
+ </div>
47
+ `;
48
+ }
49
+
50
+ export function ExampleView(items: ExampleItem[] = STUB_ITEMS): ArrowExpression {
51
+ return html`
52
+ <div class="oas-shell-view">
53
+
54
+ <div class="oas-shell-view-header">
55
+ <div class="oas-flex oas-items-center oas-justify-between oas-px-3 oas-py-2-1">
56
+ <span class="oas-font-semibold oas-text-sm">Example View</span>
57
+ <div class="oas-flex oas-gap-1">
58
+ <button class="clickable-icon" aria-label="Filter">⊟</button>
59
+ <button class="mod-cta" style="font-size: var(--font-ui-smaller);">New item</button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="oas-shell-view-body">
65
+ ${
66
+ items.length === 0
67
+ ? EmptyState({
68
+ icon: "file",
69
+ title: "No items yet",
70
+ description: "Create your first item to get started.",
71
+ action: { label: "New item", onClick: () => {} },
72
+ })
73
+ : ""
74
+ }
75
+ ${items.map((item) => ItemRow(item))}
76
+ </div>
77
+
78
+ <div class="oas-shell-view-footer">
79
+ <div class="oas-flex oas-items-center oas-gap-2 oas-px-3 oas-py-2">
80
+ <input
81
+ type="text"
82
+ class="search-input"
83
+ placeholder="Quick add…"
84
+ style="flex: 1;"
85
+ />
86
+ <button class="mod-cta" aria-label="Add">+</button>
87
+ </div>
88
+ </div>
89
+
90
+ </div>
91
+ `;
92
+ }
@@ -0,0 +1,28 @@
1
+ import { html } from "@arrow-js/core";
2
+ import { defineStories } from "../../tools/viewer/stories";
3
+
4
+ export default defineStories({
5
+ title: "Structural / Card",
6
+ description:
7
+ "Standard panel container for standalone components. Use oas-shell-panel as the root wrapper when a component needs its own card frame.",
8
+ status: "live",
9
+ componentPath: "src/utilities.css",
10
+ variants: {
11
+ default: () => html`
12
+ <div class="oas-shell-panel" style="max-width: 320px;">
13
+ <div class="setting-item-name">Panel heading</div>
14
+ <div class="setting-item-description">Panel description or sub-content goes here.</div>
15
+ <div><button class="mod-cta">Primary action</button></div>
16
+ </div>
17
+ `,
18
+ with_badge: () => html`
19
+ <div class="oas-shell-panel" style="max-width: 320px;">
20
+ <div class="oas-flex oas-items-center oas-justify-between">
21
+ <div class="setting-item-name">Panel with badge</div>
22
+ <span class="oas-badge is-live">live</span>
23
+ </div>
24
+ <div class="setting-item-description">Panel using oas-badge alongside shell.</div>
25
+ </div>
26
+ `,
27
+ },
28
+ });
@@ -4,6 +4,7 @@ import { defineStories } from "../../tools/viewer/stories";
4
4
  export default defineStories({
5
5
  description: "Reusable empty state for any view — icon, title, description, optional action.",
6
6
  status: "live",
7
+ componentPath: "src/components/EmptyState/EmptyState.ts",
7
8
  variants: {
8
9
  default: () => EmptyState({ title: "Nothing here yet" }),
9
10
  "with description": () => {
@@ -4,6 +4,7 @@ import { defineStories } from "../../tools/viewer/stories";
4
4
  export default defineStories({
5
5
  description: "Loading indicator for async view content.",
6
6
  status: "live",
7
+ componentPath: "src/components/LoadingState.ts",
7
8
  variants: {
8
9
  default: () => LoadingState(),
9
10
  "with message": () => LoadingState("Loading sessions…"),
@@ -0,0 +1,50 @@
1
+ import { html, reactive } from "@arrow-js/core";
2
+ import { Toggle } from "../../src/components/SettingsPanel";
3
+ import { defineStories } from "../../tools/viewer/stories";
4
+
5
+ export default defineStories({
6
+ title: "Settings / Toggle",
7
+ description:
8
+ "Obsidian-native checkbox toggle used in settings rows. Pass a reactive getter and an onToggle handler.",
9
+ status: "live",
10
+ componentPath: "src/components/SettingsPanel.ts",
11
+ variants: {
12
+ on: () => {
13
+ const state = reactive({ enabled: true });
14
+ return Toggle(
15
+ () => state.enabled,
16
+ () => {
17
+ state.enabled = !state.enabled;
18
+ }
19
+ );
20
+ },
21
+ off: () => {
22
+ const state = reactive({ enabled: false });
23
+ return Toggle(
24
+ () => state.enabled,
25
+ () => {
26
+ state.enabled = !state.enabled;
27
+ }
28
+ );
29
+ },
30
+ in_setting_row: () => {
31
+ const state = reactive({ enabled: true });
32
+ return html`
33
+ <div class="setting-item">
34
+ <div class="setting-item-info">
35
+ <div class="setting-item-name">Enable feature</div>
36
+ <div class="setting-item-description">Toggles the feature on or off.</div>
37
+ </div>
38
+ <div class="setting-item-control">
39
+ ${Toggle(
40
+ () => state.enabled,
41
+ () => {
42
+ state.enabled = !state.enabled;
43
+ }
44
+ )}
45
+ </div>
46
+ </div>
47
+ `;
48
+ },
49
+ },
50
+ });