create-obsidian-arrow 0.5.1 → 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 +57 -12
  10. package/template/README.md +66 -31
  11. package/template/_gitignore +4 -1
  12. package/template/biome.json +7 -1
  13. package/template/docs/prompts/agent-setup.md +22 -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 +158 -0
  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,32 +1,43 @@
1
1
  import { html } from "@arrow-js/core";
2
2
  import type { ArrowTemplate } from "@arrow-js/core";
3
- import { MIN_WIDTH, WIDTH_PRESETS, layoutState, setWidth } from "./layout";
3
+ import { MIN_WIDTH, WIDTH_PRESETS, layoutState, pageState, setWidth } from "./layout";
4
+ import { themeState, toggleTheme } from "./theme";
4
5
 
5
6
  /**
6
- * Sandbox toolbar above the pane: panel-width controls for testing components
7
- * at the widths a real Obsidian side panel can be dragged to. Sandbox chrome —
8
- * not part of the component under test.
7
+ * Sandbox toolbar: home, breadcrumb, panel-width controls, and theme toggle.
8
+ * All sandbox-global chrome lives here so the story panels stay content-only.
9
9
  */
10
10
  const onRangeInput = (event: Event): void => {
11
11
  setWidth(Number((event.target as HTMLInputElement).value));
12
12
  };
13
13
 
14
14
  export const Toolbar = (): ArrowTemplate => html`
15
- <div class="oas-toolbar">
16
- <span class="oas-toolbar-label">Panel width</span>
15
+ <div class="oasbox-toolbar">
16
+ <a class="clickable-icon oasbox-home" href="/" aria-label="Home">⌂</a>
17
+ ${() =>
18
+ pageState.breadcrumb
19
+ ? html`<span class="oasbox-breadcrumb">${() => pageState.breadcrumb}</span>`
20
+ : ""}
21
+ <div class="oasbox-toolbar-divider"></div>
22
+ <span class="oasbox-toolbar-label">Panel width</span>
17
23
  <input
18
- class="oas-width-range"
24
+ class="oasbox-width-range"
19
25
  type="range"
20
26
  min="${MIN_WIDTH}"
21
27
  max="${() => window.innerWidth}"
22
28
  .value="${() => String(layoutState.width)}"
23
29
  @input="${onRangeInput}"
24
30
  />
25
- <span class="oas-width-readout">${() => `${layoutState.width}px`}</span>
31
+ <span class="oasbox-width-readout">${() => `${layoutState.width}px`}</span>
26
32
  ${WIDTH_PRESETS.map((width) =>
27
- html`<button class="oas-preset" @click="${() => setWidth(width)}">${width}</button>`.key(
33
+ html`<button class="oasbox-preset" @click="${() => setWidth(width)}">${width}</button>`.key(
28
34
  width
29
35
  )
30
36
  )}
37
+ <button
38
+ class="clickable-icon oasbox-theme-toggle"
39
+ aria-label="Toggle theme"
40
+ @click="${toggleTheme}"
41
+ >${() => (themeState.theme === "theme-dark" ? "☾" : "☀")}</button>
31
42
  </div>
32
43
  `;
@@ -4,7 +4,7 @@ import { classGroups } from "./obsidian-classes";
4
4
 
5
5
  export const ClassesPage = component(() => {
6
6
  return html`
7
- <div class="oas-reference">
7
+ <div class="oasbox-reference">
8
8
  <div class="setting-item setting-item-heading">
9
9
  <div class="setting-item-info">
10
10
  <div class="setting-item-name">Obsidian classes</div>
@@ -15,17 +15,17 @@ export const ClassesPage = component(() => {
15
15
  </div>
16
16
  ${classGroups.map(
17
17
  (group) => html`
18
- <div class="oas-class-group">
18
+ <div class="oasbox-class-group">
19
19
  <div class="vertical-tab-header-group-title">${group.label}</div>
20
20
  ${group.entries.map(
21
21
  (entry) => html`
22
- <div class="oas-class-entry">
23
- <div class="oas-class-head">
22
+ <div class="oasbox-class-entry">
23
+ <div class="oasbox-class-head">
24
24
  <code>${entry.className}</code>
25
- <button class="oas-copy" @click="${() => copyText(entry.className)}">Copy</button>
25
+ <button class="oasbox-copy" @click="${() => copyText(entry.className)}">Copy</button>
26
26
  </div>
27
- <div class="oas-class-when">${entry.whenToUse}</div>
28
- <div class="oas-class-preview">${entry.preview()}</div>
27
+ <div class="oasbox-class-when">${entry.whenToUse}</div>
28
+ <div class="oasbox-class-preview">${entry.preview()}</div>
29
29
  </div>
30
30
  `
31
31
  )}
@@ -5,7 +5,7 @@ import { stories } from "./discovery";
5
5
  export function ComponentsIndex(): ArrowExpression {
6
6
  if (stories.length === 0) {
7
7
  return html`
8
- <div class="oas-settings">
8
+ <div class="oasbox-settings">
9
9
  <div class="setting-item setting-item-heading">
10
10
  <div class="setting-item-info">
11
11
  <div class="setting-item-name">Components</div>
@@ -18,7 +18,7 @@ export function ComponentsIndex(): ArrowExpression {
18
18
  `;
19
19
  }
20
20
  return html`
21
- <div class="oas-settings">
21
+ <div class="oasbox-settings">
22
22
  <div class="setting-item setting-item-heading">
23
23
  <div class="setting-item-info">
24
24
  <div class="setting-item-name">Components</div>
@@ -46,7 +46,7 @@ export function ComponentsIndex(): ArrowExpression {
46
46
  }
47
47
  </div>
48
48
  <div class="setting-item-control">
49
- <a class="mod-cta oas-open-link" href="${path}">Open →</a>
49
+ <a class="mod-cta oasbox-open-link" href="${path}">Open →</a>
50
50
  </div>
51
51
  </div>
52
52
  `.key(story.slug);
@@ -9,59 +9,61 @@ export function copyText(text: string): void {
9
9
 
10
10
  function pathRow(label: string, path: string): ArrowExpression {
11
11
  return html`
12
- <div class="oas-story-path">
13
- <span class="oas-path-label">${label}</span>
14
- <code>${path}</code>
15
- <button class="oas-copy" @click="${() => copyText(path)}">Copy</button>
12
+ <div class="oasbox-story-path">
13
+ <span class="oasbox-path-label">${label}</span>
14
+ <code class="oasbox-path-code" title="${path}">${path}</code>
15
+ <button class="oasbox-copy" @click="${() => copyText(path)}">Copy</button>
16
16
  </div>
17
17
  `;
18
18
  }
19
19
 
20
- /** Metadata panel: title, status, variant tabs, notes, paths, children.
21
- * No rendered component used for kind: "component" left panel. */
20
+ /** Metadata panel: story info, references, variants, notes, children.
21
+ * Sections are visually separated with labels. Used as the left panel for
22
+ * both component and view stories. */
22
23
  export function StoryPageDetails(story: DiscoveredStory, variantName: string): ArrowExpression {
23
24
  const variantNames = Object.keys(story.variants);
24
25
  const variant = story.variants[variantName];
26
+ const badge =
27
+ story.status === "live"
28
+ ? html`<span class="oas-badge is-live">live</span>`
29
+ : html`<span class="oas-badge is-draft">draft</span>`;
30
+
25
31
  return html`
26
- <div class="oas-story">
27
- <div class="setting-item setting-item-heading">
28
- <div class="setting-item-info">
29
- <div class="setting-item-name">
30
- ${story.title}
31
- ${
32
- story.status === "live"
33
- ? html`<span class="oas-badge is-live">live</span>`
34
- : html`<span class="oas-badge is-draft">draft</span>`
35
- }
36
- </div>
37
- ${
38
- story.description
39
- ? html`<div class="setting-item-description">${story.description}</div>`
40
- : ""
41
- }
42
- </div>
32
+ <div class="oasbox-story">
33
+ <div class="oasbox-story-header">
34
+ <div class="oasbox-story-title">${story.title} ${badge}</div>
35
+ ${story.description ? html`<div class="oasbox-story-desc">${story.description}</div>` : ""}
43
36
  </div>
44
- <div class="oas-story-meta">
37
+ <div class="oasbox-story-section">
38
+ <div class="oasbox-section-label">References</div>
45
39
  ${pathRow("component", story.componentPath)}
46
40
  ${pathRow("stories", story.storiesPath)}
47
41
  </div>
48
- <div class="oas-variants">
49
- ${variantNames.map((name) => {
50
- const cls = name === variantName ? "oas-variant is-active" : "oas-variant";
51
- const href = `/components/${story.slug}?variant=${encodeURIComponent(name)}`;
52
- return html`<a class="${cls}" href="${href}">${name}</a>`;
53
- })}
54
- </div>
55
- ${variant?.notes ? html`<div class="oas-story-notes">${variant.notes}</div>` : ""}
42
+ ${
43
+ variantNames.length > 1
44
+ ? html`<div class="oasbox-story-section">
45
+ <div class="oasbox-section-label">Variants</div>
46
+ <div class="oasbox-variants">
47
+ ${variantNames.map((name) => {
48
+ const cls = name === variantName ? "oasbox-variant is-active" : "oasbox-variant";
49
+ const href = `/components/${story.slug}?variant=${encodeURIComponent(name)}`;
50
+ return html`<a class="${cls}" href="${href}">${name}</a>`;
51
+ })}
52
+ </div>
53
+ </div>`
54
+ : ""
55
+ }
56
+
57
+ ${variant?.notes ? html`<div class="oasbox-story-notes">${variant.notes}</div>` : ""}
56
58
  ${
57
59
  story.children.length > 0
58
- ? html`<div class="oas-story-children">
60
+ ? html`<div class="oasbox-story-children">
59
61
  ${story.children.map((slug) => {
60
62
  const child = findStory(slug);
61
63
  const childHref = `/components/${slug}`;
62
64
  return child
63
- ? html`<a class="oas-child" href="${childHref}">${child.title} →</a>`
64
- : html`<span class="oas-child-missing">${slug} (missing story)</span>`;
65
+ ? html`<a class="oasbox-child" href="${childHref}">${child.title} →</a>`
66
+ : html`<span class="oasbox-child-missing">${slug} (missing story)</span>`;
65
67
  })}
66
68
  </div>`
67
69
  : ""
@@ -76,16 +78,27 @@ export function StoryPageCanvas(story: DiscoveredStory, variantName: string): Ar
76
78
  const variant = story.variants[variantName];
77
79
  const rendered = variant
78
80
  ? variant.render()
79
- : html`<div class="oas-story-missing">No variant "${variantName}" — pick one above.</div>`;
81
+ : html`<div class="oasbox-story-missing">No variant "${variantName}" — pick one above.</div>`;
80
82
  return story.decorator ? story.decorator(rendered) : rendered;
81
83
  }
82
84
 
83
- /** Full story page: details + canvas together.
84
- * Used for kind: "view" — StoryPageDetails provides the .oas-story wrapper. */
85
+ /** @deprecated Use StoryPageDetails + StoryPageCanvas via the router's viewCanvas flag instead. */
85
86
  export function StoryPageView(story: DiscoveredStory, variantName: string): ArrowExpression {
87
+ const variantNames = Object.keys(story.variants);
88
+ const hasMultiple = variantNames.length > 1;
86
89
  return html`
87
- ${StoryPageDetails(story, variantName)}
88
- <div class="oas-story-canvas">
90
+ ${
91
+ hasMultiple
92
+ ? html`<div class="oasbox-view-variant-bar">
93
+ ${variantNames.map((name) => {
94
+ const cls = name === variantName ? "oasbox-variant is-active" : "oasbox-variant";
95
+ const href = `/components/${story.slug}?variant=${encodeURIComponent(name)}`;
96
+ return html`<a class="${cls}" href="${href}">${name}</a>`;
97
+ })}
98
+ </div>`
99
+ : ""
100
+ }
101
+ <div class="oasbox-story-view">
89
102
  ${StoryPageCanvas(story, variantName)}
90
103
  </div>
91
104
  `;
@@ -14,20 +14,20 @@ function tokenRow(decl: TokenDecl) {
14
14
  return resolveToken(decl.name) || decl.value;
15
15
  };
16
16
  return html`
17
- <div class="oas-token-row">
18
- <code class="oas-token-name">${decl.name}</code>
19
- <span class="oas-token-value">${() => resolved()}</span>
17
+ <div class="oasbox-token-row">
18
+ <code class="oasbox-token-name">${decl.name}</code>
19
+ <span class="oasbox-token-value">${() => resolved()}</span>
20
20
  ${() => {
21
21
  const kind = classifyValue(resolved());
22
22
  if (kind === "color") {
23
- return html`<span class="oas-swatch" style="${() => `background: ${resolved()};`}"></span>`;
23
+ return html`<span class="oasbox-swatch" style="${() => `background: ${resolved()};`}"></span>`;
24
24
  }
25
25
  if (kind === "length") {
26
- return html`<span class="oas-sizebar" style="${() => `width: ${resolved()};`}"></span>`;
26
+ return html`<span class="oasbox-sizebar" style="${() => `width: ${resolved()};`}"></span>`;
27
27
  }
28
- return html`<span class="oas-swatch-none"></span>`;
28
+ return html`<span class="oasbox-swatch-none"></span>`;
29
29
  }}
30
- <button class="oas-copy" @click="${() => copyText(`var(${decl.name})`)}">Copy</button>
30
+ <button class="oasbox-copy" @click="${() => copyText(`var(${decl.name})`)}">Copy</button>
31
31
  </div>
32
32
  `;
33
33
  }
@@ -35,7 +35,7 @@ function tokenRow(decl: TokenDecl) {
35
35
  export const TokensPage = component(() => {
36
36
  const decls = collectTokenDecls();
37
37
  return html`
38
- <div class="oas-reference">
38
+ <div class="oasbox-reference">
39
39
  <div class="setting-item setting-item-heading">
40
40
  <div class="setting-item-info">
41
41
  <div class="setting-item-name">Obsidian tokens (${String(decls.length)})</div>
@@ -58,7 +58,7 @@ export const TokensPage = component(() => {
58
58
  : ""
59
59
  }
60
60
  <input
61
- class="oas-token-filter"
61
+ class="oasbox-token-filter"
62
62
  type="search"
63
63
  placeholder="Filter tokens…"
64
64
  .value="${() => state.query}"
@@ -69,7 +69,7 @@ export const TokensPage = component(() => {
69
69
  ${() =>
70
70
  groupTokens(filterTokens(decls, state.query)).map((group) =>
71
71
  html`
72
- <div class="oas-token-group">
72
+ <div class="oasbox-token-group">
73
73
  <div class="vertical-tab-header-group-title">
74
74
  ${group.label} (${String(group.tokens.length)})
75
75
  </div>
@@ -0,0 +1,66 @@
1
+ import { html } from "@arrow-js/core";
2
+ import type { ArrowExpression } from "@arrow-js/core";
3
+ import { stories } from "./discovery";
4
+
5
+ /**
6
+ * Index page for all kind: "view" stories. Mirrors ComponentsIndex for views.
7
+ */
8
+ export function ViewsIndex(): ArrowExpression {
9
+ const views = stories.filter((s) => s.kind === "view");
10
+
11
+ if (views.length === 0) {
12
+ return html`
13
+ <div class="oasbox-settings">
14
+ <div class="setting-item setting-item-heading">
15
+ <div class="setting-item-info">
16
+ <div class="setting-item-name">Views</div>
17
+ <div class="setting-item-description">
18
+ No view stories found. Create a
19
+ <code>*.stories.ts</code>
20
+ file under
21
+ <code>stories/views/</code>
22
+ to register a view.
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ `;
28
+ }
29
+
30
+ return html`
31
+ <div class="oasbox-settings">
32
+ <div class="setting-item setting-item-heading">
33
+ <div class="setting-item-info">
34
+ <div class="setting-item-name">Views</div>
35
+ <div class="setting-item-description">
36
+ ${views.length} ${views.length === 1 ? "view" : "views"} — click to open.
37
+ </div>
38
+ </div>
39
+ </div>
40
+ ${views.map((story) => {
41
+ const path = `/components/${story.slug}`;
42
+ const badge =
43
+ story.status === "live"
44
+ ? html`<span class="oas-badge is-live">live</span>`
45
+ : html`<span class="oas-badge is-draft">draft</span>`;
46
+ return html`
47
+ <div class="setting-item">
48
+ <div class="setting-item-info">
49
+ <div class="setting-item-name">
50
+ <a href="${path}">${story.title}</a> ${badge}
51
+ </div>
52
+ ${
53
+ story.description
54
+ ? html`<div class="setting-item-description">${story.description}</div>`
55
+ : ""
56
+ }
57
+ </div>
58
+ <div class="setting-item-control">
59
+ <a class="mod-cta oasbox-open-link" href="${path}">Open →</a>
60
+ </div>
61
+ </div>
62
+ `.key(story.slug);
63
+ })}
64
+ </div>
65
+ `;
66
+ }
@@ -21,6 +21,7 @@ export interface DiscoveredStory {
21
21
  children: string[];
22
22
  status: "live" | "draft";
23
23
  kind: "view" | "component";
24
+ surface: "panel" | "editor";
24
25
  decorator?: (content: ArrowExpression) => ArrowExpression;
25
26
  }
26
27
 
@@ -57,6 +58,7 @@ for (const [globKey, mod] of Object.entries(modules)) {
57
58
  children: def.children ?? [],
58
59
  status: def.status ?? "draft",
59
60
  kind: def.kind ?? autoKind,
61
+ surface: def.surface ?? "panel",
60
62
  decorator: def.decorator,
61
63
  });
62
64
  }
@@ -101,7 +101,7 @@ export const classGroups: ClassGroup[] = [
101
101
  whenToUse:
102
102
  "Dialog card (.modal-title + .modal-content). Preview is contained by a transformed wrapper.",
103
103
  preview: () => html`
104
- <div class="modal oas-modal-preview">
104
+ <div class="modal oasbox-modal-preview">
105
105
  <div class="modal-title">Modal title</div>
106
106
  <div class="modal-content">Modal content goes here.</div>
107
107
  </div>
@@ -1,54 +1,43 @@
1
- import { html } from "@arrow-js/core";
2
- import type { ArrowTemplate } from "@arrow-js/core";
1
+ import { html, reactive } from "@arrow-js/core";
3
2
  import type { ArrowExpression } from "@arrow-js/core";
4
- import type { TreeNode } from "./derive";
5
- import { findStory, invalidStories, storyTree } from "./discovery";
6
3
 
7
4
  /**
8
- * Viewer navigation: component tree (children indented under parents) plus the
9
- * Reference section. Rendered OUTSIDE the pane, as the first child of the
10
- * stage. Sandbox chrome — never ports to a plugin.
5
+ * Viewer navigation sidebar. Sandbox chrome never ports to a plugin.
11
6
  */
12
7
 
8
+ const sidebarState = reactive({ collapsed: false });
9
+
13
10
  function navClass(active: boolean): string {
14
11
  return active
15
- ? "vertical-tab-nav-item oas-nav-item is-active"
16
- : "vertical-tab-nav-item oas-nav-item";
12
+ ? "vertical-tab-nav-item oasbox-nav-item is-active"
13
+ : "vertical-tab-nav-item oasbox-nav-item";
17
14
  }
18
15
 
19
- function nodeRows(node: TreeNode, activePath: string, depth: number): ArrowTemplate | string {
20
- const story = findStory(node.slug);
21
- if (!story) {
22
- return "";
23
- }
24
- const href = `/components/${node.slug}`;
25
- return html`
26
- <a
27
- class="${navClass(activePath === href)}"
28
- style="${`padding-left: calc(var(--size-4-3) * ${depth + 1});`}"
29
- href="${href}"
30
- >${story.title}</a>
31
- ${node.children.map((child) => nodeRows(child, activePath, depth + 1))}
32
- `;
16
+ function toggleSidebar(): void {
17
+ sidebarState.collapsed = !sidebarState.collapsed;
33
18
  }
34
19
 
35
20
  export function ViewerSidebar(activePath: string): ArrowExpression {
21
+ const onComponents = activePath.startsWith("/components");
22
+ const onViews = activePath.startsWith("/views");
36
23
  return html`
37
- <nav class="oas-sidebar">
38
- <div class="vertical-tab-header-group">
39
- <div class="vertical-tab-header-group-title">Components</div>
40
- ${storyTree.roots.map((node) => nodeRows(node, activePath, 0))}
41
- ${invalidStories.map(
42
- (bad) =>
43
- html`<div class="vertical-tab-nav-item oas-nav-invalid" title="${bad.reason}">
44
- invalid: ${bad.storiesPath}
45
- </div>`
46
- )}
47
- </div>
48
- <div class="vertical-tab-header-group">
49
- <div class="vertical-tab-header-group-title">Reference</div>
50
- <a class="${navClass(activePath === "/reference")}" href="/reference">Tokens</a>
51
- <a class="${navClass(activePath === "/reference/classes")}" href="/reference/classes">Classes</a>
24
+ <nav class="${() => `oasbox-sidebar${sidebarState.collapsed ? " is-collapsed" : ""}`}">
25
+ <button
26
+ class="oasbox-sidebar-toggle clickable-icon"
27
+ aria-label="Toggle sidebar"
28
+ @click="${toggleSidebar}"
29
+ >${() => (sidebarState.collapsed ? "›" : "‹")}</button>
30
+ <div class="oasbox-sidebar-content">
31
+ <div class="vertical-tab-header-group">
32
+ <div class="vertical-tab-header-group-title">Sandbox</div>
33
+ <a class="${navClass(onComponents)}" href="/components">Components</a>
34
+ <a class="${navClass(onViews)}" href="/views">Views</a>
35
+ </div>
36
+ <div class="vertical-tab-header-group">
37
+ <div class="vertical-tab-header-group-title">Reference</div>
38
+ <a class="${navClass(activePath === "/reference")}" href="/reference">Tokens</a>
39
+ <a class="${navClass(activePath === "/reference/classes")}" href="/reference/classes">Classes</a>
40
+ </div>
52
41
  </div>
53
42
  </nav>
54
43
  `;
@@ -18,12 +18,21 @@ export interface StoryDef {
18
18
  title?: string;
19
19
  /** One-line description shown in the tree and header. */
20
20
  description?: string;
21
- /** Repo-relative override for where the component lives (e.g. a subcomponent
22
- * defined inside its parent's file). Defaults to the stories path minus `.stories`. */
21
+ /** Repo-relative override for where the component lives. Defaults to mirroring
22
+ * the stories/ subtree into src/components/ (stories/Foo.stories.ts
23
+ * src/components/Foo.ts), so set this for views (src/views/…) and for a
24
+ * subcomponent defined inside its parent's file. */
23
25
  componentPath?: string;
24
26
  /** "view" = full pane frame. "component" = centered canvas with separate details panel.
25
27
  * Auto-detected from stories/ path if omitted. */
26
28
  kind?: "view" | "component";
29
+ /** Obsidian surface a view replicates — orthogonal to `kind`.
30
+ * "panel" (default) = full-bleed custom view (workspace leaf).
31
+ * "editor" = readable line width (note/document style): the sandbox widens
32
+ * the pane so the centering is visible. The width itself comes from the
33
+ * component wrapping its content in the portable `oas-readable-width` utility
34
+ * (scaffolded by `create:view --editor`), so it ports 1:1 into the plugin. */
35
+ surface?: "panel" | "editor";
27
36
  /** Wraps the rendered variant — use for ancestor class context.
28
37
  * Example: (content) => html`<div class="my-shell">${content}</div>` */
29
38
  decorator?: (content: ArrowExpression) => ArrowExpression;
@@ -80,6 +89,11 @@ export function validateStoryDef(def: unknown): ValidationResult {
80
89
  return { ok: false, reason: '"kind" must be "view" or "component"' };
81
90
  }
82
91
  }
92
+ if ("surface" in def) {
93
+ if (def.surface !== "panel" && def.surface !== "editor") {
94
+ return { ok: false, reason: '"surface" must be "panel" or "editor"' };
95
+ }
96
+ }
83
97
  if ("decorator" in def) {
84
98
  if (typeof def.decorator !== "function") {
85
99
  return { ok: false, reason: '"decorator" must be a function' };
@@ -1,36 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
-
8
- jobs:
9
- check:
10
- runs-on: ubuntu-latest
11
- steps:
12
- - uses: actions/checkout@v4
13
-
14
- - uses: pnpm/action-setup@v4
15
- with:
16
- version: 10.14.0
17
-
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: 22
21
- cache: pnpm
22
-
23
- - name: Install
24
- run: pnpm install --frozen-lockfile
25
-
26
- - name: Lint + format check (Biome)
27
- run: pnpm biome ci .
28
-
29
- - name: Typecheck
30
- run: pnpm typecheck
31
-
32
- - name: Test
33
- run: pnpm test
34
-
35
- - name: Build
36
- run: pnpm build