create-obsidian-arrow 0.3.0 → 0.4.1

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 (38) hide show
  1. package/README.md +6 -4
  2. package/index.mjs +42 -10
  3. package/package.json +1 -1
  4. package/template/AGENTS.md +8 -6
  5. package/template/README.md +13 -10
  6. package/template/docs/prompts/agent-setup.md +8 -6
  7. package/template/docs/prompts/update-existing.md +12 -8
  8. package/template/docs/workflow.md +5 -4
  9. package/template/package.json +5 -1
  10. package/template/pnpm-lock.yaml +197 -0
  11. package/template/src/components/DiffViewer.ts +42 -0
  12. package/template/src/main.ts +3 -3
  13. package/template/stories/DiffViewer.stories.ts +75 -0
  14. package/template/{src/components → stories}/SettingsPanel.stories.ts +3 -3
  15. package/template/{src/components → stories}/Toggle.stories.ts +3 -3
  16. package/template/test/token-utils.test.mjs +1 -1
  17. package/template/test/viewer-derive.test.mjs +5 -5
  18. package/template/test/viewer-stories.test.mjs +1 -1
  19. package/template/{src → tools}/sandbox/sandbox.css +42 -0
  20. package/template/{src → tools}/viewer/derive.ts +15 -5
  21. package/template/{src → tools}/viewer/discovery.ts +6 -5
  22. /package/template/{src → tools}/router/client.ts +0 -0
  23. /package/template/{src → tools}/router/routeToPage.ts +0 -0
  24. /package/template/{src → tools}/sandbox/frame.ts +0 -0
  25. /package/template/{src → tools}/sandbox/home.ts +0 -0
  26. /package/template/{src → tools}/sandbox/layout.ts +0 -0
  27. /package/template/{src → tools}/sandbox/shell.ts +0 -0
  28. /package/template/{src → tools}/sandbox/theme.ts +0 -0
  29. /package/template/{src → tools}/sandbox/toolbar.ts +0 -0
  30. /package/template/{src → tools}/viewer/ClassesPage.ts +0 -0
  31. /package/template/{src → tools}/viewer/ComponentsIndex.ts +0 -0
  32. /package/template/{src → tools}/viewer/StoryPage.ts +0 -0
  33. /package/template/{src → tools}/viewer/TokensPage.ts +0 -0
  34. /package/template/{src → tools}/viewer/obsidian-classes.ts +0 -0
  35. /package/template/{src → tools}/viewer/sidebar.ts +0 -0
  36. /package/template/{src → tools}/viewer/stories.ts +0 -0
  37. /package/template/{src → tools}/viewer/token-utils.ts +0 -0
  38. /package/template/{src → tools}/viewer/tokens.ts +0 -0
package/README.md CHANGED
@@ -45,9 +45,11 @@ prints this hint when it detects nesting.
45
45
  ## Update an existing project
46
46
 
47
47
  The scaffolder is create-only, but `update` refreshes an existing project's
48
- **managed** tooling (`scripts/`, `skills/`, `docs/`, `.github/`, `.husky/`,
49
- `biome.json`, agent guides) and merges new `package.json` scripts/deps — it never
50
- touches your `src/`, `public/`, `index.html`, or build configs:
48
+ **managed** files (`scripts/`, `docs/`, `.github/`, `.husky/`, `biome.json`,
49
+ agent guides, `tools/viewer/`, `tools/router/`, `tools/sandbox/`, `src/main.ts`,
50
+ `src/utilities.css`, `test/`) and merges new `package.json` scripts/deps
51
+ it never touches `src/components/`, `stories/`, `public/`, `index.html`, or
52
+ build configs:
51
53
 
52
54
  ```sh
53
55
  npx create-obsidian-arrow update # in the project (or: update <dir>)
@@ -69,7 +71,7 @@ node create-obsidian-arrow/index.mjs update ../my-app # update
69
71
 
70
72
  A full sandbox: client-only Vite + TS, `@arrow-js/core` + `@arrow-js/framework`
71
73
  (no SSR), `routeToPage` + Navigation-API router, a Storybook-style component
72
- viewer at `/components` (co-locate `*.stories.ts` to add stories), a live token
74
+ viewer at `/components` (add `*.stories.ts` files in `stories/`), a live token
73
75
  and class reference at `/reference`, Biome + husky pre-commit + `node:test` +
74
76
  GitHub Actions CI, a `skills:install` that pulls the agent skills from the
75
77
  published repo, and the `pull-css` script that extracts Obsidian's `app.css`.
package/index.mjs CHANGED
@@ -13,9 +13,11 @@
13
13
  * Scaffold copies the vendored template/, restores .gitignore (vendored as
14
14
  * _gitignore), names the project, and runs `git init`.
15
15
  *
16
- * Update refreshes only the *managed* tooling files from the template and merges
17
- * package.json scripts + missing deps it never touches your src/, public/,
18
- * index.html, or the core build configs. Use --dry-run to preview.
16
+ * Update refreshes *managed* files from the template and merges package.json
17
+ * scripts + missing deps. Managed = scripts/, docs/, .github/, .husky/,
18
+ * biome.json, AGENTS.md, CLAUDE.md, tools/, src/main.ts, src/utilities.css,
19
+ * test/. Never touches src/components/, stories/, public/, index.html,
20
+ * vite.config.ts, tsconfig.json, or .gitignore. Use --dry-run first.
19
21
  */
20
22
  import { spawnSync } from "node:child_process";
21
23
  import fs from "node:fs";
@@ -25,12 +27,29 @@ import { fileURLToPath } from "node:url";
25
27
  const here = path.dirname(fileURLToPath(import.meta.url));
26
28
  const templateDir = path.join(here, "template");
27
29
 
28
- // Files/dirs the template owns and `update` may overwrite/merge. Everything else
29
- // (src/, public/, index.html, vite.config.ts, tsconfig.json, lockfile, .gitignore,
30
- // port-parity.json, …) is treated as user-owned and left alone. Skills aren't
31
- // here they're pulled from the published repo via `pnpm skills:update`, not
32
- // vendored into the scaffold.
33
- const MANAGED = ["scripts", "docs", ".github", ".husky", "biome.json", "AGENTS.md", "CLAUDE.md"];
30
+ // Files/dirs the template owns and `update` may overwrite/merge.
31
+ //
32
+ // src/components/ and stories/ are user-owned and never touched.
33
+ // tools/ contains all viewer/router/sandbox infrastructure and IS managed.
34
+ // Everything else (public/, index.html, vite.config.ts, tsconfig.json,
35
+ // lockfile, .gitignore, port-parity.json, …) is also user-owned. Skills aren't here — they're pulled from the published
36
+ // repo via `pnpm skills:update`, not vendored into the scaffold.
37
+ const MANAGED = [
38
+ // Tooling
39
+ "scripts",
40
+ "docs",
41
+ ".github",
42
+ ".husky",
43
+ "biome.json",
44
+ "AGENTS.md",
45
+ "CLAUDE.md",
46
+ // Viewer infrastructure — all managed; src/components/ and stories/ are user-owned
47
+ "tools",
48
+ "src/main.ts",
49
+ "src/utilities.css",
50
+ // Viewer tests
51
+ "test",
52
+ ];
34
53
 
35
54
  const argv = process.argv.slice(2);
36
55
  const dryRun = argv.includes("--dry-run");
@@ -162,6 +181,14 @@ function update(targetArg) {
162
181
  }
163
182
  const pkgChanges = mergePackageJson(path.join(root, "package.json"));
164
183
 
184
+ // Create stories/ if missing — user-owned (never overwritten), but must
185
+ // exist before the first story file can be added.
186
+ const storiesDir = path.join(root, "stories");
187
+ const storiesCreated = !fs.existsSync(storiesDir);
188
+ if (storiesCreated && !dryRun) {
189
+ fs.mkdirSync(storiesDir);
190
+ }
191
+
165
192
  const verb = dryRun ? "Would refresh" : "Refreshed";
166
193
  console.log(
167
194
  `${verb} ${written.length} managed file(s) in ${path.relative(process.cwd(), root) || "."}:`
@@ -169,13 +196,18 @@ function update(targetArg) {
169
196
  for (const file of written) {
170
197
  console.log(` ${file}`);
171
198
  }
199
+ if (storiesCreated) {
200
+ console.log(
201
+ ` stories/ ${dryRun ? "(would create empty)" : "(created empty — add your *.stories.ts files here)"}`
202
+ );
203
+ }
172
204
  if (pkgChanges.length > 0) {
173
205
  console.log(`package.json: ${dryRun ? "would update" : "updated"} ${pkgChanges.join(", ")}`);
174
206
  }
175
207
  console.log(
176
208
  dryRun
177
209
  ? "\n(dry run — nothing written.)"
178
- : "\nLeft alone: src/, public/, index.html, vite.config.ts, tsconfig.json, .gitignore.\nRun `pnpm install` then `pnpm check`. Update skills separately with `pnpm skills:update`.\n"
210
+ : "\nLeft alone: src/components/, stories/, public/, index.html, vite.config.ts, tsconfig.json, .gitignore.\n(tools/, src/main.ts, src/utilities.css, and test/ were refreshed above.)\nRun `pnpm install` then `pnpm check`. Update skills separately with `pnpm skills:update`.\n"
179
211
  );
180
212
  }
181
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-obsidian-arrow",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Scaffold an Obsidian-styled Arrow.js UI sandbox (pnpm create obsidian-arrow <dir>).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,10 +91,12 @@ in `boundary()`.
91
91
  Prefer `class="oas-flex oas-gap-2"` over `style="display:flex;gap:8px"`.
92
92
  - **Custom CSS last** — only when Obsidian has no class and utilities don't cover
93
93
  it. Scope under container + element type, use `var(--…)` tokens for values.
94
- - Add a story by creating a co-located `*.stories.ts` next to the component
95
- it appears at `/components/<slug>` automatically. See the `obsidian-arrow-stories`
96
- skill for the full `defineStories` API (variants, children, status, notes).
97
- Browse live tokens at `/reference`, curated classes at `/reference/classes`.
94
+ - Add a story by creating `stories/MyThing.stories.ts` stories live in the
95
+ top-level `stories/` directory (not in `src/`). Import via
96
+ `"../tools/viewer/stories"` and `"../src/components/MyThing"`. See the
97
+ `obsidian-arrow-stories` skill for the full `defineStories` API (variants,
98
+ children, status, notes). Browse live tokens at `/reference`, curated classes
99
+ at `/reference/classes`.
98
100
 
99
101
  ## CSS scoping
100
102
 
@@ -104,7 +106,7 @@ in `boundary()`.
104
106
  - Scope any custom rule under a container class + element type (e.g.
105
107
  `.oas-frame button.oas-theme-toggle`) so it beats Obsidian's global
106
108
  `button:not(.clickable-icon)` rule and never leaks. Sandbox-only chrome lives
107
- in `src/sandbox/sandbox.css`; component styling stays on Obsidian classes.
109
+ in `tools/sandbox/sandbox.css`; component styling stays on Obsidian classes.
108
110
 
109
111
  ## Verify before claiming done
110
112
 
@@ -126,7 +128,7 @@ is mechanical: copy the component file into the plugin's view directory and
126
128
  mount it from `ItemView.onOpen()` via `template(this.contentEl)`. If it uses
127
129
  `boundary()`/async components, add `@arrow-js/framework` to the plugin and the
128
130
  `import '@arrow-js/framework'` side-effect import. Strip any sandbox chrome
129
- (`src/sandbox/*`) — that stays here.
131
+ (`tools/sandbox/*`) — that stays here.
130
132
 
131
133
  **Bring `src/utilities.css` along.** Components may use `oas-`-prefixed utility
132
134
  classes (flex, gap, padding, typography, border — all built on Obsidian's token
@@ -24,9 +24,10 @@ Then `cd my-app && pnpm install && pnpm pull-css && pnpm dev`. A freshly
24
24
  scaffolded project passes `pnpm run ci` out of the box. The initializer's
25
25
  template is generated from this repo (`pnpm create:sync`), so it never drifts.
26
26
 
27
- **Update an existing project's tooling** (refreshes the managed files — scripts,
28
- skills, docs, agent guides, CI, `biome.json` and merges new `package.json`
29
- scripts/deps; never touches `src/`, `public/`, `index.html`, or build configs):
27
+ **Update an existing project's tooling** (refreshes managed files — scripts,
28
+ docs, CI, `biome.json`, agent guides, and the viewer/router/sandbox
29
+ infrastructure in `src/` merges new `package.json` scripts/deps; never
30
+ touches `src/components/`, `stories/`, `public/`, `index.html`, or build configs):
30
31
 
31
32
  ```sh
32
33
  npx create-obsidian-arrow update # in the project (or: update <dir>)
@@ -86,7 +87,7 @@ typecheck. CI (`.github/workflows/ci.yml`) runs the `ci` script on push/PR.
86
87
 
87
88
  ## Agent skills
88
89
 
89
- This repo is the source of truth for five [`skills`](https://github.com/vercel-labs/skills)-compatible
90
+ This repo is the source of truth for seven [`skills`](https://github.com/vercel-labs/skills)-compatible
90
91
  skills under [`skills/`](skills/) — it's a skill marketplace. Scaffolds **don't
91
92
  vendor copies**; they pull from this published repo, so installs are always
92
93
  current.
@@ -143,21 +144,23 @@ with live previews.
143
144
 
144
145
  ### Add a story
145
146
 
146
- Create `src/components/MyThing.stories.ts` next to the component:
147
+ Create `stories/MyThing.stories.ts` stories live in the top-level `stories/`
148
+ directory, keeping `src/` purely for component code:
147
149
 
148
150
  ```ts
149
- import { defineStories } from "../viewer/stories";
150
- import { MyThing } from "./MyThing";
151
+ import { defineStories } from "../tools/viewer/stories";
152
+ import { MyThing } from "../src/components/MyThing";
151
153
 
152
154
  export default defineStories({
153
155
  description: "What it demonstrates.",
156
+ status: "draft",
154
157
  variants: { default: () => MyThing() },
155
158
  });
156
159
  ```
157
160
 
158
- It appears in the sidebar and on Home automatically; the src path shown in the
159
- viewer is derived from the file location. Stories are sandbox-only — they never
160
- port to the plugin.
161
+ It appears in the sidebar and at `/components` automatically; the component path
162
+ shown in the viewer is derived from the filename. Stories are sandbox-only —
163
+ they never port to the plugin.
161
164
 
162
165
  ## Porting a component into the plugin
163
166
 
@@ -74,10 +74,11 @@ CONVENTIONS
74
74
  .vertical-tab-*, .modal, .mod-cta) and var(--…) tokens first; add custom CSS
75
75
  only when Obsidian has no class, scoped under a container class + element type
76
76
  (e.g. `.my-panel button.my-action`) so it beats Obsidian's global button rule.
77
- - Sandbox-only chrome lives in src/sandbox/* — it does NOT port to a plugin.
78
- - Add a demo by creating a co-located `*.stories.ts` next to the component (see
79
- README "Add a story"); it appears at `/components/<slug>` automatically.
80
- Browse Obsidian tokens/classes at `/reference`.
77
+ - Sandbox-only chrome lives in tools/sandbox/* — it does NOT port to a plugin.
78
+ - Add a story by creating `stories/MyThing.stories.ts` (top-level `stories/`
79
+ dir, NOT in `src/`). Import: `"../tools/viewer/stories"` and
80
+ `"../src/components/MyThing"`. It appears at `/components/<slug>` automatically.
81
+ Browse tokens at `/reference`, curated classes at `/reference/classes`.
81
82
 
82
83
  VERIFY BEFORE CLAIMING DONE
83
84
  - `pnpm typecheck && pnpm test && pnpm lint` (or `pnpm run ci` for the full chain).
@@ -92,11 +93,12 @@ components, add @arrow-js/framework to the plugin and the side-effect
92
93
  `import '@arrow-js/framework'`. Also copy src/utilities.css into the plugin once
93
94
  (all ported components share it) — components may use oas-* utility classes
94
95
  (flex, gap, padding, text, border helpers built on Obsidian's token scale).
95
- Leave src/sandbox/* behind. Guard against drift with the porting-parity check
96
+ Leave tools/sandbox/* behind. Guard against drift with the porting-parity check
96
97
  (see the arrow-js-obsidian-porting skill).
97
98
 
98
99
  MAINTENANCE (existing project)
99
- Refresh tooling later with `npx create-obsidian-arrow update` (preserves src/),
100
+ Refresh tooling with `npx create-obsidian-arrow update` (preserves src/components/
101
+ and stories/ — updates viewer, router, sandbox, utilities, and tooling files),
100
102
  update skills with `pnpm skills:update`. See the obsidian-arrow-maintenance skill.
101
103
 
102
104
  Start by scaffolding, running setup steps, then read AGENTS.md and confirm
@@ -11,10 +11,12 @@ Update this existing Obsidian Arrow project to the latest tooling and agent
11
11
  skills. Do NOT change any of my source code.
12
12
 
13
13
  HARD CONSTRAINTS
14
- - Never modify src/, public/, index.html, vite.config.ts, tsconfig.json, or my
15
- component/app code. Only managed tooling, skills, and docs may change.
14
+ - Never modify src/components/, stories/, public/, index.html, vite.config.ts,
15
+ tsconfig.json, or my component/app code. Only managed tooling, skills, docs,
16
+ and the viewer/router/sandbox infrastructure in src/ may change.
16
17
  - Preview before applying anything destructive; don't commit unless I ask.
17
- - At the end, prove src/ was untouched (`git diff --stat` shows no src/ changes).
18
+ - At the end, prove src/components/ and stories/ were untouched
19
+ (`git diff --stat` shows no changes to those paths).
18
20
 
19
21
  CONTEXT
20
22
  Scaffolded from create-obsidian-arrow. Tooling + agent skills come from the
@@ -29,12 +31,14 @@ STEPS (in order)
29
31
  project dir. If the OUTER root differs, that's where skills must be installed
30
32
  so the session's skill:// registry (repo-root + global only) can find them.
31
33
 
32
- 2. Refresh tooling — preserves src/
34
+ 2. Refresh tooling — preserves src/components/ only
33
35
  - Preview: npx create-obsidian-arrow update --dry-run
34
36
  - Apply: npx create-obsidian-arrow update
35
37
  - Then: pnpm install
36
- Refreshes scripts/, docs/, .github/, .husky/, biome.json, AGENTS.md, CLAUDE.md
37
- and merges package.json scripts/deps. It never touches src/, public/, or configs.
38
+ Refreshes scripts/, docs/, .github/, .husky/, biome.json, AGENTS.md, CLAUDE.md,
39
+ tools/, src/main.ts, src/utilities.css, test/,
40
+ and merges package.json scripts/deps. It never touches src/components/, public/,
41
+ index.html, vite.config.ts, tsconfig.json, or .gitignore.
38
42
 
39
43
  3. Reset agent skills to the current set
40
44
  - Audit: npx skills list
@@ -73,6 +77,6 @@ REPORT
73
77
  - Skills are pulled from the published repo, so step 3 is location-independent for
74
78
  the *source* — only the *install location* matters (run it at the repo root the
75
79
  agent uses; reload after).
76
- - `npx create-obsidian-arrow update` is **create-only-safe**: it refreshes managed
77
- files and merges `package.json`, but never overwrites `src/`, `public/`,
80
+ - `npx create-obsidian-arrow update` refreshes managed files and merges
81
+ `package.json`, but never overwrites `src/components/`, `stories/`, `public/`,
78
82
  `index.html`, or build configs. See the `obsidian-arrow-maintenance` skill.
@@ -49,8 +49,9 @@ Then point the agent at [`AGENTS.md`](../AGENTS.md), or brief a fresh agent with
49
49
  `skills` CLI is cwd-relative). To install them at the outer repo instead:
50
50
  `pnpm skills:install --yes --project-dir=<outer-repo>` (or `--global`).
51
51
 
52
- **Refresh an existing project's tooling** (scripts, skills, docs, CI — never your
53
- `src/`): `npx create-obsidian-arrow update` (add `--dry-run` to preview).
52
+ **Refresh an existing project's tooling** (scripts, docs, CI, viewer/router/sandbox
53
+ in `src/` never `src/components/` or `stories/`):
54
+ `npx create-obsidian-arrow update` (add `--dry-run` to preview).
54
55
 
55
56
  ## Build → verify → port loop
56
57
 
@@ -58,7 +59,7 @@ Then point the agent at [`AGENTS.md`](../AGENTS.md), or brief a fresh agent with
58
59
  # 1. Check /reference/classes in the running sandbox — does Obsidian have a class
59
60
  # for your pattern? Use it before writing custom CSS.
60
61
  # 2. Add src/components/MyThing.ts (Arrow component)
61
- # 3. Add src/components/MyThing.stories.ts (co-located story file)
62
+ # 3. Add stories/MyThing.stories.ts (story file — lives in stories/, not src/)
62
63
  pnpm dev # iterate with HMR
63
64
  # /components → index of all discovered stories
64
65
  # /components/my-thing → story for MyThing
@@ -77,7 +78,7 @@ status flag, DRY patterns) see the `obsidian-arrow-stories` skill.
77
78
  **Port a component into a plugin:** copy the component file and `src/utilities.css`
78
79
  (once, shared by all ports) into the plugin. Mount from `ItemView.onOpen()` via
79
80
  `template(this.contentEl)`. If it uses `boundary()`/async, add
80
- `@arrow-js/framework` and `import '@arrow-js/framework'`. Leave `src/sandbox/*`
81
+ `@arrow-js/framework` and `import '@arrow-js/framework'`. Leave `tools/sandbox/*`
81
82
  behind.
82
83
 
83
84
  ## Scaffold vs. clone
@@ -27,7 +27,11 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@arrow-js/core": "^1.0.6",
30
- "@arrow-js/framework": "^1.0.6"
30
+ "@arrow-js/framework": "^1.0.6",
31
+ "@codemirror/lang-markdown": "^6.5.0",
32
+ "@codemirror/merge": "^6.12.2",
33
+ "@codemirror/state": "^6.7.0",
34
+ "@codemirror/view": "^6.43.4"
31
35
  },
32
36
  "devDependencies": {
33
37
  "@biomejs/biome": "^1.9.4",
@@ -14,6 +14,18 @@ importers:
14
14
  '@arrow-js/framework':
15
15
  specifier: ^1.0.6
16
16
  version: 1.0.6
17
+ '@codemirror/lang-markdown':
18
+ specifier: ^6.5.0
19
+ version: 6.5.0
20
+ '@codemirror/merge':
21
+ specifier: ^6.12.2
22
+ version: 6.12.2
23
+ '@codemirror/state':
24
+ specifier: ^6.7.0
25
+ version: 6.7.0
26
+ '@codemirror/view':
27
+ specifier: ^6.43.4
28
+ version: 6.43.4
17
29
  devDependencies:
18
30
  '@biomejs/biome':
19
31
  specifier: ^1.9.4
@@ -99,6 +111,36 @@ packages:
99
111
  cpu: [x64]
100
112
  os: [win32]
101
113
 
114
+ '@codemirror/autocomplete@6.20.3':
115
+ resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==}
116
+
117
+ '@codemirror/lang-css@6.3.1':
118
+ resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
119
+
120
+ '@codemirror/lang-html@6.4.11':
121
+ resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
122
+
123
+ '@codemirror/lang-javascript@6.2.5':
124
+ resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
125
+
126
+ '@codemirror/lang-markdown@6.5.0':
127
+ resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==}
128
+
129
+ '@codemirror/language@6.12.4':
130
+ resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==}
131
+
132
+ '@codemirror/lint@6.9.7':
133
+ resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==}
134
+
135
+ '@codemirror/merge@6.12.2':
136
+ resolution: {integrity: sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w==}
137
+
138
+ '@codemirror/state@6.7.0':
139
+ resolution: {integrity: sha512-Zbl9NyscLMZkfXPQnNAIIAFftidrA1UbcJEIMp24C0Bukc2I5T8wJS0wsXYsnDOqCFJUeJ1BITGNs5CqPDSmSg==}
140
+
141
+ '@codemirror/view@6.43.4':
142
+ resolution: {integrity: sha512-YImu23iyKfncJzT7sRy+rEqEhSc8RhOHqDxwy4WzXRKJwYm6iwf/9OJk5ctCAdZ6yi2ZqaGEvmf55fSVqMDrgg==}
143
+
102
144
  '@csstools/color-helpers@5.1.0':
103
145
  resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
104
146
  engines: {node: '>=18'}
@@ -136,6 +178,30 @@ packages:
136
178
  '@emnapi/wasi-threads@1.2.2':
137
179
  resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
138
180
 
181
+ '@lezer/common@1.5.2':
182
+ resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
183
+
184
+ '@lezer/css@1.3.4':
185
+ resolution: {integrity: sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==}
186
+
187
+ '@lezer/highlight@1.2.3':
188
+ resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
189
+
190
+ '@lezer/html@1.3.13':
191
+ resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
192
+
193
+ '@lezer/javascript@1.5.4':
194
+ resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
195
+
196
+ '@lezer/lr@1.4.10':
197
+ resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
198
+
199
+ '@lezer/markdown@1.6.4':
200
+ resolution: {integrity: sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==}
201
+
202
+ '@marijn/find-cluster-break@1.0.3':
203
+ resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
204
+
139
205
  '@napi-rs/wasm-runtime@1.1.6':
140
206
  resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
141
207
  peerDependencies:
@@ -288,6 +354,9 @@ packages:
288
354
  resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
289
355
  engines: {node: '>=18'}
290
356
 
357
+ crelt@1.0.7:
358
+ resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
359
+
291
360
  cross-spawn@7.0.6:
292
361
  resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
293
362
  engines: {node: '>= 8'}
@@ -648,6 +717,9 @@ packages:
648
717
  resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
649
718
  engines: {node: '>=12'}
650
719
 
720
+ style-mod@4.1.3:
721
+ resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
722
+
651
723
  symbol-tree@3.2.4:
652
724
  resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
653
725
 
@@ -731,6 +803,9 @@ packages:
731
803
  yaml:
732
804
  optional: true
733
805
 
806
+ w3c-keyname@2.2.8:
807
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
808
+
734
809
  w3c-xmlserializer@5.0.0:
735
810
  resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
736
811
  engines: {node: '>=18'}
@@ -843,6 +918,87 @@ snapshots:
843
918
  '@biomejs/cli-win32-x64@1.9.4':
844
919
  optional: true
845
920
 
921
+ '@codemirror/autocomplete@6.20.3':
922
+ dependencies:
923
+ '@codemirror/language': 6.12.4
924
+ '@codemirror/state': 6.7.0
925
+ '@codemirror/view': 6.43.4
926
+ '@lezer/common': 1.5.2
927
+
928
+ '@codemirror/lang-css@6.3.1':
929
+ dependencies:
930
+ '@codemirror/autocomplete': 6.20.3
931
+ '@codemirror/language': 6.12.4
932
+ '@codemirror/state': 6.7.0
933
+ '@lezer/common': 1.5.2
934
+ '@lezer/css': 1.3.4
935
+
936
+ '@codemirror/lang-html@6.4.11':
937
+ dependencies:
938
+ '@codemirror/autocomplete': 6.20.3
939
+ '@codemirror/lang-css': 6.3.1
940
+ '@codemirror/lang-javascript': 6.2.5
941
+ '@codemirror/language': 6.12.4
942
+ '@codemirror/state': 6.7.0
943
+ '@codemirror/view': 6.43.4
944
+ '@lezer/common': 1.5.2
945
+ '@lezer/css': 1.3.4
946
+ '@lezer/html': 1.3.13
947
+
948
+ '@codemirror/lang-javascript@6.2.5':
949
+ dependencies:
950
+ '@codemirror/autocomplete': 6.20.3
951
+ '@codemirror/language': 6.12.4
952
+ '@codemirror/lint': 6.9.7
953
+ '@codemirror/state': 6.7.0
954
+ '@codemirror/view': 6.43.4
955
+ '@lezer/common': 1.5.2
956
+ '@lezer/javascript': 1.5.4
957
+
958
+ '@codemirror/lang-markdown@6.5.0':
959
+ dependencies:
960
+ '@codemirror/autocomplete': 6.20.3
961
+ '@codemirror/lang-html': 6.4.11
962
+ '@codemirror/language': 6.12.4
963
+ '@codemirror/state': 6.7.0
964
+ '@codemirror/view': 6.43.4
965
+ '@lezer/common': 1.5.2
966
+ '@lezer/markdown': 1.6.4
967
+
968
+ '@codemirror/language@6.12.4':
969
+ dependencies:
970
+ '@codemirror/state': 6.7.0
971
+ '@codemirror/view': 6.43.4
972
+ '@lezer/common': 1.5.2
973
+ '@lezer/highlight': 1.2.3
974
+ '@lezer/lr': 1.4.10
975
+ style-mod: 4.1.3
976
+
977
+ '@codemirror/lint@6.9.7':
978
+ dependencies:
979
+ '@codemirror/state': 6.7.0
980
+ '@codemirror/view': 6.43.4
981
+ crelt: 1.0.7
982
+
983
+ '@codemirror/merge@6.12.2':
984
+ dependencies:
985
+ '@codemirror/language': 6.12.4
986
+ '@codemirror/state': 6.7.0
987
+ '@codemirror/view': 6.43.4
988
+ '@lezer/highlight': 1.2.3
989
+ style-mod: 4.1.3
990
+
991
+ '@codemirror/state@6.7.0':
992
+ dependencies:
993
+ '@marijn/find-cluster-break': 1.0.3
994
+
995
+ '@codemirror/view@6.43.4':
996
+ dependencies:
997
+ '@codemirror/state': 6.7.0
998
+ crelt: 1.0.7
999
+ style-mod: 4.1.3
1000
+ w3c-keyname: 2.2.8
1001
+
846
1002
  '@csstools/color-helpers@5.1.0': {}
847
1003
 
848
1004
  '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -879,6 +1035,41 @@ snapshots:
879
1035
  tslib: 2.8.1
880
1036
  optional: true
881
1037
 
1038
+ '@lezer/common@1.5.2': {}
1039
+
1040
+ '@lezer/css@1.3.4':
1041
+ dependencies:
1042
+ '@lezer/common': 1.5.2
1043
+ '@lezer/highlight': 1.2.3
1044
+ '@lezer/lr': 1.4.10
1045
+
1046
+ '@lezer/highlight@1.2.3':
1047
+ dependencies:
1048
+ '@lezer/common': 1.5.2
1049
+
1050
+ '@lezer/html@1.3.13':
1051
+ dependencies:
1052
+ '@lezer/common': 1.5.2
1053
+ '@lezer/highlight': 1.2.3
1054
+ '@lezer/lr': 1.4.10
1055
+
1056
+ '@lezer/javascript@1.5.4':
1057
+ dependencies:
1058
+ '@lezer/common': 1.5.2
1059
+ '@lezer/highlight': 1.2.3
1060
+ '@lezer/lr': 1.4.10
1061
+
1062
+ '@lezer/lr@1.4.10':
1063
+ dependencies:
1064
+ '@lezer/common': 1.5.2
1065
+
1066
+ '@lezer/markdown@1.6.4':
1067
+ dependencies:
1068
+ '@lezer/common': 1.5.2
1069
+ '@lezer/highlight': 1.2.3
1070
+
1071
+ '@marijn/find-cluster-break@1.0.3': {}
1072
+
882
1073
  '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
883
1074
  dependencies:
884
1075
  '@emnapi/core': 1.11.1
@@ -986,6 +1177,8 @@ snapshots:
986
1177
 
987
1178
  commander@13.1.0: {}
988
1179
 
1180
+ crelt@1.0.7: {}
1181
+
989
1182
  cross-spawn@7.0.6:
990
1183
  dependencies:
991
1184
  path-key: 3.1.1
@@ -1326,6 +1519,8 @@ snapshots:
1326
1519
 
1327
1520
  strip-final-newline@3.0.0: {}
1328
1521
 
1522
+ style-mod@4.1.3: {}
1523
+
1329
1524
  symbol-tree@3.2.4: {}
1330
1525
 
1331
1526
  tinyglobby@0.2.17:
@@ -1372,6 +1567,8 @@ snapshots:
1372
1567
  fsevents: 2.3.3
1373
1568
  yaml: 2.9.0
1374
1569
 
1570
+ w3c-keyname@2.2.8: {}
1571
+
1375
1572
  w3c-xmlserializer@5.0.0:
1376
1573
  dependencies:
1377
1574
  xml-name-validator: 5.0.0
@@ -0,0 +1,42 @@
1
+ import type { ArrowExpression } from "@arrow-js/core";
2
+ import { markdown } from "@codemirror/lang-markdown";
3
+ import { MergeView } from "@codemirror/merge";
4
+ import { EditorState } from "@codemirror/state";
5
+
6
+ export interface DiffViewerOptions {
7
+ original: string;
8
+ modified: string;
9
+ /** Which side shows original (a) and which shows modified (b). Default: a-b */
10
+ orientation?: "a-b" | "b-a";
11
+ }
12
+
13
+ /**
14
+ * CodeMirror 6 MergeView — side-by-side diff using the same CM6 engine as
15
+ * Obsidian's editor. Obsidian's app.css styles .cm-* classes automatically,
16
+ * so this looks native without additional theming.
17
+ *
18
+ * Arrow's ArrowExpression type doesn't include Node but accepts it at runtime.
19
+ * The cast is intentional — CM6 manages its own DOM and Arrow inserts it as-is.
20
+ */
21
+ export function DiffViewer(options: DiffViewerOptions): ArrowExpression {
22
+ const el = document.createElement("div");
23
+ el.className = "oas-diff-viewer";
24
+
25
+ new MergeView({
26
+ a: {
27
+ doc: options.original,
28
+ extensions: [markdown(), EditorState.readOnly.of(true)],
29
+ },
30
+ b: {
31
+ doc: options.modified,
32
+ extensions: [markdown(), EditorState.readOnly.of(true)],
33
+ },
34
+ parent: el,
35
+ orientation: options.orientation ?? "a-b",
36
+ highlightChanges: true,
37
+ collapseUnchanged: { margin: 3, minSize: 4 },
38
+ });
39
+
40
+ // biome-ignore lint/suspicious/noExplicitAny: Arrow accepts Node at runtime; type doesn't include it
41
+ return el as unknown as ArrowExpression;
42
+ }
@@ -2,10 +2,10 @@
2
2
  // This is the one extra line a plugin would add to adopt @arrow-js/framework.
3
3
  import "@arrow-js/framework";
4
4
 
5
- import { startRouter } from "./router/client";
6
- import { applyTheme } from "./sandbox/theme";
5
+ import { startRouter } from "../tools/router/client";
6
+ import { applyTheme } from "../tools/sandbox/theme";
7
7
  import "./utilities.css";
8
- import "./sandbox/sandbox.css";
8
+ import "../tools/sandbox/sandbox.css";
9
9
 
10
10
  applyTheme();
11
11
 
@@ -0,0 +1,75 @@
1
+ import { DiffViewer } from "../src/components/DiffViewer";
2
+ import { defineStories } from "../tools/viewer/stories";
3
+
4
+ const ORIGINAL = `---
5
+ title: Meeting Notes
6
+ status: draft
7
+ ---
8
+
9
+ # Team Standup
10
+
11
+ Quick notes from today's standup.
12
+
13
+ ## Done
14
+
15
+ - Reviewed the PR for the search panel
16
+ - Fixed the token filter in the reference viewer
17
+
18
+ ## In Progress
19
+
20
+ - Arrow component for the diff viewer
21
+ - Documentation updates
22
+
23
+ ## Notes
24
+
25
+ See the project board for full task breakdown.
26
+ `;
27
+
28
+ const MODIFIED = `---
29
+ title: Meeting Notes
30
+ status: complete
31
+ tags: [standup, team]
32
+ ---
33
+
34
+ # Team Standup — 2026-07-02
35
+
36
+ Notes from today's standup.
37
+
38
+ ## Done
39
+
40
+ - Reviewed and merged the PR for the search panel
41
+ - Fixed the token filter in the reference viewer
42
+ - Added DiffViewer component to the sandbox
43
+
44
+ ## In Progress
45
+
46
+ - Documentation updates
47
+ - Editor pane integration
48
+
49
+ ## Notes
50
+
51
+ See the project board for full task breakdown.
52
+ Next standup: Thursday.
53
+ `;
54
+
55
+ const SHORT_ORIGINAL = `function greet(name: string): string {
56
+ return "Hello, " + name;
57
+ }
58
+ `;
59
+
60
+ const SHORT_MODIFIED = `function greet(name: string, greeting = "Hello"): string {
61
+ return \`\${greeting}, \${name}!\`;
62
+ }
63
+ `;
64
+
65
+ export default defineStories({
66
+ description:
67
+ "CodeMirror 6 MergeView — side-by-side diff using the same engine as Obsidian's editor. Changed lines highlight inline; unchanged sections collapse.",
68
+ status: "draft",
69
+ variants: {
70
+ "markdown document": () => DiffViewer({ original: ORIGINAL, modified: MODIFIED }),
71
+ "code snippet": () => DiffViewer({ original: SHORT_ORIGINAL, modified: SHORT_MODIFIED }),
72
+ "reversed (b-a)": () =>
73
+ DiffViewer({ original: ORIGINAL, modified: MODIFIED, orientation: "b-a" }),
74
+ },
75
+ });
@@ -1,11 +1,11 @@
1
- import { defineStories } from "../viewer/stories";
2
- import { SettingsPanel } from "./SettingsPanel";
1
+ import { SettingsPanel } from "../src/components/SettingsPanel";
2
+ import { defineStories } from "../tools/viewer/stories";
3
3
 
4
4
  export default defineStories({
5
5
  description: "Vertical tabs, toggles, a keyed list, and an async boundary() section.",
6
+ status: "live",
6
7
  variants: {
7
8
  default: () => SettingsPanel(),
8
9
  },
9
10
  children: ["toggle"],
10
- status: "live",
11
11
  });
@@ -1,10 +1,11 @@
1
1
  import { reactive } from "@arrow-js/core";
2
- import { defineStories } from "../viewer/stories";
3
- import { Toggle } from "./SettingsPanel";
2
+ import { Toggle } from "../src/components/SettingsPanel";
3
+ import { defineStories } from "../tools/viewer/stories";
4
4
 
5
5
  export default defineStories({
6
6
  description: "Obsidian checkbox-container toggle used by SettingsPanel.",
7
7
  componentPath: "src/components/SettingsPanel.ts",
8
+ status: "live",
8
9
  variants: {
9
10
  interactive: () => {
10
11
  const state = reactive({ on: true });
@@ -24,5 +25,4 @@ export default defineStories({
24
25
  notes: "Static off state (click does nothing).",
25
26
  },
26
27
  },
27
- status: "live",
28
28
  });
@@ -5,7 +5,7 @@ import {
5
5
  filterTokens,
6
6
  groupTokens,
7
7
  parseCustomProps,
8
- } from "../src/viewer/token-utils.ts";
8
+ } from "../tools/viewer/token-utils.ts";
9
9
 
10
10
  test("parseCustomProps extracts custom property declarations from rule text", () => {
11
11
  const css = "body.theme-dark { --text-accent: #a288ff; --size-4-2: 8px; color: red; }";
@@ -5,7 +5,7 @@ import {
5
5
  kebabCase,
6
6
  storyMetaFromGlobKey,
7
7
  titleFromSlug,
8
- } from "../src/viewer/derive.ts";
8
+ } from "../tools/viewer/derive.ts";
9
9
 
10
10
  test("kebabCase converts PascalCase and spaces/underscores", () => {
11
11
  assert.equal(kebabCase("SettingsPanel"), "settings-panel");
@@ -19,16 +19,16 @@ test("titleFromSlug start-cases", () => {
19
19
  });
20
20
 
21
21
  test("storyMetaFromGlobKey derives slug + repo-relative paths", () => {
22
- const meta = storyMetaFromGlobKey("../components/SettingsPanel.stories.ts");
22
+ const meta = storyMetaFromGlobKey("../../stories/SettingsPanel.stories.ts");
23
23
  assert.equal(meta.slug, "settings-panel");
24
- assert.equal(meta.storiesPath, "src/components/SettingsPanel.stories.ts");
24
+ assert.equal(meta.storiesPath, "stories/SettingsPanel.stories.ts");
25
25
  assert.equal(meta.componentPath, "src/components/SettingsPanel.ts");
26
26
  });
27
27
 
28
28
  test("storyMetaFromGlobKey handles nested directories", () => {
29
- const meta = storyMetaFromGlobKey("../components/chat/MessageFeed.stories.ts");
29
+ const meta = storyMetaFromGlobKey("../../stories/chat/MessageFeed.stories.ts");
30
30
  assert.equal(meta.slug, "message-feed");
31
- assert.equal(meta.storiesPath, "src/components/chat/MessageFeed.stories.ts");
31
+ assert.equal(meta.storiesPath, "stories/chat/MessageFeed.stories.ts");
32
32
  assert.equal(meta.componentPath, "src/components/chat/MessageFeed.ts");
33
33
  });
34
34
 
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
- import { defineStories, normalizeVariants, validateStoryDef } from "../src/viewer/stories.ts";
3
+ import { defineStories, normalizeVariants, validateStoryDef } from "../tools/viewer/stories.ts";
4
4
 
5
5
  test("defineStories is an identity that preserves the def", () => {
6
6
  const def = { variants: { default: () => "x" } };
@@ -430,3 +430,45 @@ body {
430
430
  width: auto;
431
431
  max-width: 100%;
432
432
  }
433
+
434
+ /* DiffViewer — CodeMirror 6 MergeView container */
435
+ .oas-diff-viewer {
436
+ height: 480px;
437
+ overflow: hidden;
438
+ border: 1px solid var(--background-modifier-border);
439
+ border-radius: var(--radius-m);
440
+ font-family: var(--font-monospace);
441
+ font-size: var(--font-ui-small);
442
+ }
443
+
444
+ .oas-diff-viewer .cm-mergeView {
445
+ height: 100%;
446
+ }
447
+
448
+ .oas-diff-viewer .cm-editor {
449
+ height: 100%;
450
+ background: var(--background-primary);
451
+ color: var(--text-normal);
452
+ }
453
+
454
+ .oas-diff-viewer .cm-scroller {
455
+ overflow: auto;
456
+ font-family: var(--font-monospace);
457
+ }
458
+
459
+ .oas-diff-viewer .cm-mergeViewEditor {
460
+ border-right: 1px solid var(--background-modifier-border);
461
+ }
462
+
463
+ .oas-diff-viewer .cm-mergeViewEditor:last-child {
464
+ border-right: none;
465
+ }
466
+
467
+ .oas-diff-viewer .cm-deletedChunk {
468
+ background: color-mix(in srgb, var(--color-red) 15%, transparent);
469
+ }
470
+
471
+ .oas-diff-viewer .cm-insertedChunk,
472
+ .oas-diff-viewer .cm-changedLine {
473
+ background: color-mix(in srgb, var(--color-green) 12%, transparent);
474
+ }
@@ -2,8 +2,8 @@
2
2
  * Pure derivation helpers for the component viewer. DOM-free so node:test can
3
3
  * cover them directly (via --experimental-strip-types).
4
4
  *
5
- * Glob keys come from import.meta.glob in src/viewer/discovery.ts, so they are
6
- * relative to src/viewer/ (e.g. "../components/SettingsPanel.stories.ts").
5
+ * Glob keys come from import.meta.glob in src/viewer/discovery.ts, relative to
6
+ * src/viewer/ e.g. "../../stories/SettingsPanel.stories.ts".
7
7
  */
8
8
 
9
9
  export interface StoryPathMeta {
@@ -27,10 +27,20 @@ export function titleFromSlug(slug: string): string {
27
27
  .join(" ");
28
28
  }
29
29
 
30
- /** "../components/Foo.stories.ts" → repo-relative paths + slug. */
30
+ /**
31
+ * "../../stories/Foo.stories.ts" → project-relative paths + slug.
32
+ *
33
+ * Glob keys are relative to src/viewer/, so "../../" reaches the project root.
34
+ * Default componentPath mirrors the stories/ subtree into src/components/:
35
+ * stories/Foo.stories.ts → src/components/Foo.ts
36
+ * stories/chat/Foo.stories.ts → src/components/chat/Foo.ts
37
+ * Override with componentPath in the story def when the component lives elsewhere.
38
+ */
31
39
  export function storyMetaFromGlobKey(globKey: string): StoryPathMeta {
32
- const storiesPath = `src/${globKey.replace(/^(\.\.\/|\.\/)+/, "")}`;
33
- const componentPath = storiesPath.replace(/\.stories\.ts$/, ".ts");
40
+ const storiesPath = globKey.replace(/^(\.\.\/)+/, "");
41
+ const componentPath = storiesPath
42
+ .replace(/^stories\//, "src/components/")
43
+ .replace(/\.stories\.ts$/, ".ts");
34
44
  const base = storiesPath.split("/").pop() ?? "";
35
45
  const slug = kebabCase(base.replace(/\.stories\.ts$/, ""));
36
46
  return { slug, storiesPath, componentPath };
@@ -3,10 +3,11 @@ import type { StoryDef, StoryVariant } from "./stories";
3
3
  import { normalizeVariants, validateStoryDef } from "./stories";
4
4
 
5
5
  /**
6
- * Discovers co-located *.stories.ts files at build time. The glob key IS the
7
- * file path, so src locations shown in the viewer are derived, never
8
- * hand-maintained. One malformed story file is skipped (with a console.warn
9
- * and an entry in invalidStories) it never blanks the viewer.
6
+ * Discovers stories/*.stories.ts files at build time. Stories live in the
7
+ * top-level stories/ directory (not in src/), keeping src/ pure component code.
8
+ * The glob key IS the file path, so src locations shown in the viewer are
9
+ * derived, never hand-maintained. One malformed story file is skipped (with a
10
+ * console.warn and an entry in invalidStories) — it never blanks the viewer.
10
11
  */
11
12
 
12
13
  export interface DiscoveredStory {
@@ -25,7 +26,7 @@ export interface InvalidStory {
25
26
  reason: string;
26
27
  }
27
28
 
28
- const modules = import.meta.glob("../components/**/*.stories.ts", { eager: true }) as Record<
29
+ const modules = import.meta.glob("../../stories/**/*.stories.ts", { eager: true }) as Record<
29
30
  string,
30
31
  { default?: unknown }
31
32
  >;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes