create-obsidian-arrow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +47 -0
  2. package/index.mjs +76 -0
  3. package/package.json +40 -0
  4. package/template/.github/workflows/ci.yml +36 -0
  5. package/template/.husky/pre-commit +2 -0
  6. package/template/AGENTS.md +90 -0
  7. package/template/LICENSE +21 -0
  8. package/template/README.md +116 -0
  9. package/template/_gitignore +8 -0
  10. package/template/biome.json +30 -0
  11. package/template/docs/prompts/agent-setup.md +94 -0
  12. package/template/docs/superpowers/specs/2026-06-29-obsidian-arrow-sandbox-design.md +206 -0
  13. package/template/index.html +19 -0
  14. package/template/package.json +43 -0
  15. package/template/pnpm-lock.yaml +1408 -0
  16. package/template/scripts/install-skills.mjs +47 -0
  17. package/template/scripts/lib/extract-app-css.mjs +60 -0
  18. package/template/scripts/pull-app-css.mjs +90 -0
  19. package/template/skills/arrow-js-obsidian-patterns/SKILL.md +94 -0
  20. package/template/skills/arrow-js-obsidian-templates/SKILL.md +101 -0
  21. package/template/skills/obsidian-arrow-sandbox/SKILL.md +64 -0
  22. package/template/src/components/SettingsPanel.ts +232 -0
  23. package/template/src/data/loadStatus.ts +17 -0
  24. package/template/src/examples/ExamplesIndex.ts +36 -0
  25. package/template/src/examples/registry.ts +26 -0
  26. package/template/src/main.ts +18 -0
  27. package/template/src/router/client.ts +85 -0
  28. package/template/src/router/routeToPage.ts +57 -0
  29. package/template/src/sandbox/frame.ts +35 -0
  30. package/template/src/sandbox/layout.ts +40 -0
  31. package/template/src/sandbox/sandbox.css +125 -0
  32. package/template/src/sandbox/shell.ts +15 -0
  33. package/template/src/sandbox/theme.ts +22 -0
  34. package/template/src/sandbox/toolbar.ts +32 -0
  35. package/template/test/extract-app-css.test.mjs +70 -0
  36. package/template/test/template-footguns.test.mjs +58 -0
  37. package/template/tsconfig.json +13 -0
  38. package/template/vite.config.ts +15 -0
@@ -0,0 +1,206 @@
1
+ # Obsidian-Arrow Sandbox — Design
2
+
3
+ **Date:** 2026-06-29
4
+ **Location:** `/Users/kylebrodeur/workspace/arrow-ui/obsidian-arrow-sandbox`
5
+ **Status:** Draft for review
6
+
7
+ ## Purpose
8
+
9
+ A minimal, Obsidian-styled prototyping environment for building plugin UI with
10
+ [Arrow.js](https://arrow-js.com/), so we can iterate on UI/UX fast in the
11
+ browser without building/loading into Obsidian on every change. Components are
12
+ written so they copy-paste into an Obsidian `ItemView`/`Modal`/settings tab with
13
+ near-zero refactoring, and a separate agent later reconciles them into the real
14
+ plugin (`pi-vault-mind/packages/obsidian`).
15
+
16
+ Success = `pnpm dev` opens a browser page that looks like a real Obsidian pane,
17
+ rendering **one** working, reactive, Obsidian-styled Arrow component, where the
18
+ component source is plugin-ready as-is.
19
+
20
+ ## Scope
21
+
22
+ **In scope (baseline):**
23
+ - Client-only Vite + TypeScript dev environment (`pnpm dev`).
24
+ - `@arrow-js/core` **+ `@arrow-js/framework`** (client-side only). Core supplies
25
+ `reactive`, `html`, `component`, `watch`, `nextTick`; framework supplies
26
+ `boundary()` for async fallback sections. Mounted via `template(container)` —
27
+ the same call an Obsidian `ItemView.onOpen()` makes. **No `ssr`/`hydrate`.**
28
+ - A deliberate **templates showcase** exercising Arrow's template features:
29
+ reactive `${() => …}` vs static `${…}`, attribute sync with `false`-removal
30
+ (`disabled="${() => …}"`), `.property` binding (`.checked`), `@event`
31
+ (`@click`), and keyed lists (`.key(id)`).
32
+ - An async section wrapped in `boundary()` (fallback → resolved) so the baseline
33
+ itself **is** the framework evaluation — we can judge `boundary()` ergonomics
34
+ against plain reactive flags on real code.
35
+ - `index.html` wrapped in Obsidian's body classes, loading the extracted
36
+ `app.css` for full fidelity (tokens **and** semantic component classes).
37
+ - A puller script that extracts `app.css` from the local Obsidian install.
38
+ - One baseline component: a settings panel (vertical tabs + `.setting-item` +
39
+ `.checkbox-container` toggle + token-colored status line) proving the pipeline.
40
+ - A light/dark theme toggle for eyeballing both themes.
41
+
42
+ **Out of scope (deferred, additive later):**
43
+ - `@arrow-js/ssr` + `@arrow-js/hydrate` — **cut, not deferred.** See the Arrow
44
+ Layering decision record below for the reasoning.
45
+ - Porting the full chat composer (`input.ts`, `message-feed`, `model-select`,
46
+ …). The composer is the *next* component after the baseline proves out.
47
+ - An `obsidian` API shim (`setIcon`, `Notice`, …). Not needed until we port a
48
+ component that calls Obsidian APIs; the baseline uses none.
49
+ - CDP-based token capture (see "Token sourcing", option B).
50
+
51
+ ## Decision record — Arrow layering
52
+
53
+ Evidence from the installed `@arrow-js` packages (v1.0.6) + how an Obsidian
54
+ plugin runs (Electron renderer, client-only; view DOM built imperatively in
55
+ `ItemView.onOpen()`, no server, no server-rendered HTML to adopt):
56
+
57
+ | Package | Key exports | Server needed? | Verdict |
58
+ |---|---|---|---|
59
+ | `core` (13.5KB min, 0 deps) | `reactive`, `html`, `component`, `watch`, `nextTick` | no | **Use** |
60
+ | `framework` (client entry, 1.4KB, no jsdom) | `boundary()`, `render`, `toTemplate` | no | **Use** (client only) |
61
+ | `framework/ssr` | server render runtime | yes (pulls **jsdom**, Node-only) | unused |
62
+ | `ssr` | `renderToString`, `serializePayload` | yes | **Cut** |
63
+ | `hydrate` (12.7KB) | `hydrate`, `readPayload` | yes (needs SSR output) | **Cut** |
64
+
65
+ - **Cut `ssr` + `hydrate`:** they are a server→client pair. `renderToString`
66
+ needs a server to run on; `hydrate` needs server HTML + payload to adopt.
67
+ Obsidian has neither — `onOpen()` already builds the DOM client-side, so
68
+ `hydrate` would only add indirection over `template(container)`. The SSR path
69
+ also depends on `jsdom` (Node) and cannot sanely run in the renderer bundle.
70
+ - **Use `framework` client-side:** its only client-relevant feature is
71
+ `boundary()` (async fallback). Client entry is tiny and does **not** import
72
+ jsdom, so it bundles cleanly into a plugin. Adopting it now makes the baseline
73
+ double as the "should the plugin upgrade?" evaluation, scoped to `boundary()`.
74
+ - **Note:** `component()` lives in core, so reusable components don't require
75
+ framework. Porting a framework-using component to the plugin requires adding
76
+ `@arrow-js/framework` as a plugin dependency + the side-effect runtime import.
77
+
78
+ ## CSS scoping convention
79
+
80
+ 1. Components use **Obsidian's own semantic classes** (`.setting-item`,
81
+ `.clickable-icon`, `.workspace-leaf`, `.vertical-tab-nav-item`) and `var(--…)`
82
+ tokens first; add custom CSS only where Obsidian offers no class.
83
+ 2. Any custom CSS is **scoped under a container class + element type** (e.g.
84
+ `.oas-frame button.oas-theme-toggle`, specificity ≥ (0,2,1)) so it beats
85
+ Obsidian's global `button:not(.clickable-icon)` rule and never leaks — per the
86
+ `arrow-js-obsidian` skill's specificity lesson. Sandbox-only chrome lives in a
87
+ scoped `src/sandbox/sandbox.css`; component styling stays on Obsidian classes.
88
+
89
+ ## Architecture
90
+
91
+ Plain client-side single-page sandbox. No server, no SSR. Vite serves
92
+ `index.html`, which loads `public/app.css` (extracted locally via `pull-css`,
93
+ git-ignored) and a TS entry that mounts an Arrow component into `#app`.
94
+
95
+ ```
96
+ obsidian-arrow-sandbox/
97
+ ├── index.html # Obsidian body-class wrapper + app.css link + #app
98
+ ├── public/
99
+ │ └── app.css # extracted from Obsidian via pull-css (git-ignored, not redistributed)
100
+ ├── src/
101
+ │ ├── main.ts # mounts the baseline component into #app
102
+ │ ├── sandbox/
103
+ │ │ ├── frame.ts # workspace-leaf frame + theme toggle chrome
104
+ │ │ ├── theme.ts # flip body theme-dark/theme-light
105
+ │ │ └── sandbox.css # scoped sandbox-only chrome styles
106
+ │ ├── data/
107
+ │ │ └── loadStatus.ts # async data for the boundary() demo
108
+ │ └── components/
109
+ │ └── SettingsPanel.ts # the baseline Arrow component (+ boundary section)
110
+ ├── scripts/
111
+ │ └── pull-app-css.mjs # extract app.css from local Obsidian install
112
+ ├── docs/superpowers/specs/ # this spec
113
+ ├── package.json
114
+ ├── tsconfig.json
115
+ └── vite.config.ts
116
+ ```
117
+
118
+ ### `index.html`
119
+
120
+ ```html
121
+ <body class="theme-dark mod-macos is-frameless is-hidden-frameless obsidian-app
122
+ show-view-header show-inline-title">
123
+ <div id="app"></div>
124
+ <script type="module" src="/src/main.ts"></script>
125
+ </body>
126
+ ```
127
+
128
+ The body classes activate Obsidian's variable scope; without them `var(--…)`
129
+ lookups in `app.css` don't resolve. `app.css` is loaded via `<link>` in `<head>`.
130
+
131
+ ### Component model
132
+
133
+ ```ts
134
+ import '@arrow-js/framework' // side-effect: install framework runtime
135
+ import { html, reactive, component } from '@arrow-js/core'
136
+ import { boundary } from '@arrow-js/framework' // async fallback boundary
137
+
138
+ const state = reactive({ activeTab: 'general', developerMode: true })
139
+ export const SettingsPanel = component(() => html`…`) // returns an Arrow template
140
+
141
+ // main.ts
142
+ import { SettingsPanel } from './components/SettingsPanel'
143
+ SettingsPanel()(document.getElementById('app')!) // == ItemView.onOpen mount
144
+ ```
145
+
146
+ Reactive expressions use the `() => …` form per Arrow rules
147
+ (`${() => state.activeTab === 'general' ? 'is-active' : ''}`), events via
148
+ `@click`, properties via `.checked`, matching the production
149
+ `arrow-js-obsidian` skill conventions.
150
+
151
+ ## Token sourcing — extract `app.css`
152
+
153
+ The sandbox loads the **full** authored `app.css` (not a slimmed token file) so
154
+ both `var(--…)` tokens and semantic component rules render faithfully.
155
+
156
+ **Option A — asar extraction (default, validated):**
157
+ On macOS `app.css` is bundled inside
158
+ `/Applications/Obsidian.app/Contents/Resources/obsidian.asar`. `pull-app-css.mjs`
159
+ parses the asar header (a Chromium Pickle: JSON length at byte 12, JSON header at
160
+ byte 16, file data section after the 4-byte-aligned header pickle), locates
161
+ `/app.css`, slices its bytes, and writes `public/app.css`. Pure Node, no deps, no
162
+ running Obsidian.
163
+
164
+ *Validated 2026-06-29:* extracted `app.css` = 586KB, ~1948 var declarations,
165
+ contains `body.theme-dark`, `.theme-light`, `--text-accent`, `--size-4-4`,
166
+ `--interactive-accent`, `--radius-m`. (Implementation note: handle 4-byte
167
+ alignment of the data offset so the slice isn't a few bytes off.)
168
+
169
+ CLI: `pnpm pull-css` (with `--path <asar>` override and an env-var fallback for
170
+ non-default install locations). Output is **git-ignored** — Obsidian's CSS is
171
+ proprietary, so each developer extracts it from their own licensed install rather
172
+ than us redistributing it. Run `pull-css` once before `pnpm dev`.
173
+
174
+ **Option B — CDP capture (deferred, optional `--source cdp`):**
175
+ Launch/attach to a running Obsidian via Chrome DevTools Protocol
176
+ (`--remote-debugging-port`), `Runtime.evaluate` in the renderer to dump the live
177
+ stylesheet text or `getComputedStyle` variable set. Captures the user's *active*
178
+ community theme / snippets / resolved values rather than stock defaults. Useful
179
+ later for testing against a real themed environment; not part of the baseline.
180
+
181
+ ## Dev workflow
182
+
183
+ - `pnpm pull-css` — refresh `public/app.css` from the local Obsidian install.
184
+ - `pnpm dev` — Vite dev server with HMR; open the printed URL.
185
+ - Edit `src/components/*.ts`; HMR re-renders instantly.
186
+ - `pnpm typecheck` — `tsc --noEmit`.
187
+
188
+ ## Porting / reconcile story
189
+
190
+ Components are framework-free `@arrow-js/core` + Obsidian CSS classes, so moving
191
+ one into the plugin is: copy the file into `src/chat/arrow/` (or appropriate
192
+ view dir) and mount it from `ItemView.onOpen()` via `template(this.contentEl)`.
193
+ Strip any U-of-D references per project notes. No build-system translation needed
194
+ (plugin bundles `@arrow-js/core` via esbuild; sandbox imports the same package).
195
+
196
+ ## Risks / open notes
197
+
198
+ - `app.css` is ~586KB; fine for a local sandbox loaded once. Not slimming it
199
+ keeps full component-class fidelity.
200
+ - asar data-offset alignment must be correct (off-by-a-few-bytes corrupts the
201
+ slice). Validated approach; nail alignment in implementation.
202
+ - Exact set of Obsidian body classes may need tweaking to match a real pane;
203
+ start from a known-good set and adjust visually.
204
+ - Community-theme fidelity is not captured by asar extraction — that's what the
205
+ optional CDP mode is for, later.
206
+ ```
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Obsidian Arrow Sandbox</title>
7
+ <!--
8
+ Obsidian's app.css scopes its design tokens and component styles under the
9
+ body theme classes (body.theme-dark / .theme-light) and Obsidian app
10
+ classes. Without these classes the var(--…) lookups resolve to nothing.
11
+ The theme class is swapped at runtime by src/sandbox/theme.ts.
12
+ -->
13
+ <link rel="stylesheet" href="/app.css" />
14
+ </head>
15
+ <body class="theme-dark mod-macos is-frameless obsidian-app show-view-header show-inline-title">
16
+ <div id="app"></div>
17
+ <script type="module" src="/src/main.ts"></script>
18
+ </body>
19
+ </html>
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "obsidian-arrow-sandbox",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Client-only sandbox for prototyping Obsidian plugin UI with Arrow.js against Obsidian's real styling.",
6
+ "license": "MIT",
7
+ "packageManager": "pnpm@10.14.0",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "build": "vite build",
11
+ "preview": "vite preview",
12
+ "typecheck": "tsc -p tsconfig.json --noEmit",
13
+ "pull-css": "node scripts/pull-app-css.mjs",
14
+ "create:sync": "node create-obsidian-arrow/scripts/sync-template.mjs",
15
+ "lint": "biome check .",
16
+ "format": "biome check --write .",
17
+ "test": "node --test test/*.test.mjs",
18
+ "check": "biome check --write . && pnpm typecheck && pnpm test",
19
+ "ci": "biome ci . && pnpm typecheck && pnpm test && pnpm build",
20
+ "skills:install": "node scripts/install-skills.mjs --force",
21
+ "postinstall": "node scripts/install-skills.mjs",
22
+ "prepare": "husky"
23
+ },
24
+ "lint-staged": {
25
+ "*.{ts,mjs,js,json,jsonc}": "biome check --write --no-errors-on-unmatched"
26
+ },
27
+ "dependencies": {
28
+ "@arrow-js/core": "^1.0.6",
29
+ "@arrow-js/framework": "^1.0.6"
30
+ },
31
+ "devDependencies": {
32
+ "@biomejs/biome": "^1.9.4",
33
+ "@types/node": "^22.16.5",
34
+ "husky": "^9.1.6",
35
+ "lint-staged": "^15.2.10",
36
+ "typescript": "^5.9.3",
37
+ "vite": "^8.0.0"
38
+ },
39
+ "pnpm": {
40
+ "onlyBuiltDependencies": ["@biomejs/biome"]
41
+ },
42
+ "version": "0.1.0"
43
+ }