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.
- package/README.md +47 -0
- package/index.mjs +76 -0
- package/package.json +40 -0
- package/template/.github/workflows/ci.yml +36 -0
- package/template/.husky/pre-commit +2 -0
- package/template/AGENTS.md +90 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/_gitignore +8 -0
- package/template/biome.json +30 -0
- package/template/docs/prompts/agent-setup.md +94 -0
- package/template/docs/superpowers/specs/2026-06-29-obsidian-arrow-sandbox-design.md +206 -0
- package/template/index.html +19 -0
- package/template/package.json +43 -0
- package/template/pnpm-lock.yaml +1408 -0
- package/template/scripts/install-skills.mjs +47 -0
- package/template/scripts/lib/extract-app-css.mjs +60 -0
- package/template/scripts/pull-app-css.mjs +90 -0
- package/template/skills/arrow-js-obsidian-patterns/SKILL.md +94 -0
- package/template/skills/arrow-js-obsidian-templates/SKILL.md +101 -0
- package/template/skills/obsidian-arrow-sandbox/SKILL.md +64 -0
- package/template/src/components/SettingsPanel.ts +232 -0
- package/template/src/data/loadStatus.ts +17 -0
- package/template/src/examples/ExamplesIndex.ts +36 -0
- package/template/src/examples/registry.ts +26 -0
- package/template/src/main.ts +18 -0
- package/template/src/router/client.ts +85 -0
- package/template/src/router/routeToPage.ts +57 -0
- package/template/src/sandbox/frame.ts +35 -0
- package/template/src/sandbox/layout.ts +40 -0
- package/template/src/sandbox/sandbox.css +125 -0
- package/template/src/sandbox/shell.ts +15 -0
- package/template/src/sandbox/theme.ts +22 -0
- package/template/src/sandbox/toolbar.ts +32 -0
- package/template/test/extract-app-css.test.mjs +70 -0
- package/template/test/template-footguns.test.mjs +58 -0
- package/template/tsconfig.json +13 -0
- 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
|
+
}
|