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,22 @@
1
+ import { reactive } from "@arrow-js/core";
2
+
3
+ /**
4
+ * Sandbox-only theme state. Obsidian toggles `body.theme-dark` /
5
+ * `body.theme-light`; we mirror that so app.css resolves the right token set.
6
+ * This file is sandbox chrome — it does NOT get ported into the plugin.
7
+ */
8
+ export type ObsidianTheme = "theme-dark" | "theme-light";
9
+
10
+ export const themeState = reactive<{ theme: ObsidianTheme }>({ theme: "theme-dark" });
11
+
12
+ /** Apply the current theme class to <body>, removing the other. */
13
+ export function applyTheme(): void {
14
+ const body = document.body;
15
+ body.classList.remove("theme-dark", "theme-light");
16
+ body.classList.add(themeState.theme);
17
+ }
18
+
19
+ export function toggleTheme(): void {
20
+ themeState.theme = themeState.theme === "theme-dark" ? "theme-light" : "theme-dark";
21
+ applyTheme();
22
+ }
@@ -0,0 +1,32 @@
1
+ import { html } from "@arrow-js/core";
2
+ import type { ArrowTemplate } from "@arrow-js/core";
3
+ import { MIN_WIDTH, WIDTH_PRESETS, layoutState, setWidth } from "./layout";
4
+
5
+ /**
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.
9
+ */
10
+ const onRangeInput = (event: Event): void => {
11
+ setWidth(Number((event.target as HTMLInputElement).value));
12
+ };
13
+
14
+ export const Toolbar = (): ArrowTemplate => html`
15
+ <div class="oas-toolbar">
16
+ <span class="oas-toolbar-label">Panel width</span>
17
+ <input
18
+ class="oas-width-range"
19
+ type="range"
20
+ min="${MIN_WIDTH}"
21
+ max="${() => window.innerWidth}"
22
+ .value="${() => String(layoutState.width)}"
23
+ @input="${onRangeInput}"
24
+ />
25
+ <span class="oas-width-readout">${() => `${layoutState.width}px`}</span>
26
+ ${WIDTH_PRESETS.map((width) =>
27
+ html`<button class="oas-preset" @click="${() => setWidth(width)}">${width}</button>`.key(
28
+ width
29
+ )
30
+ )}
31
+ </div>
32
+ `;
@@ -0,0 +1,70 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import {
4
+ assertLooksLikeAppCss,
5
+ extractAppCss,
6
+ findEntry,
7
+ readAsarHeader,
8
+ } from "../scripts/lib/extract-app-css.mjs";
9
+
10
+ /**
11
+ * Build a minimal but format-accurate asar buffer wrapping `css` at a nested
12
+ * path (styles/app.css), to exercise the recursive walk and the data-offset
13
+ * alignment. The value written at byte 8 (inner payload size) is deliberately
14
+ * 4 less than byte 4 (outer pickle size) — mirroring real asars — so a parser
15
+ * that reads offset 8 instead of 4 computes a short dataStart and slices the
16
+ * wrong bytes. That is the alignment regression this fixture guards.
17
+ */
18
+ function buildSyntheticAsar(css) {
19
+ const cssLen = Buffer.byteLength(css, "utf8");
20
+ const json = JSON.stringify({
21
+ files: { styles: { files: { "app.css": { offset: "0", size: cssLen } } } },
22
+ });
23
+ const jsonLen = Buffer.byteLength(json, "utf8");
24
+ const dataStart = 16 + jsonLen;
25
+ const headerPickleSize = dataStart - 8; // so dataStart === 8 + readUInt32LE(4)
26
+
27
+ const buf = Buffer.alloc(dataStart + cssLen);
28
+ buf.writeUInt32LE(4, 0);
29
+ buf.writeUInt32LE(headerPickleSize, 4); // outer pickle size — the correct one
30
+ buf.writeUInt32LE(headerPickleSize - 4, 8); // inner payload — the wrong one to read
31
+ buf.writeUInt32LE(jsonLen, 12);
32
+ buf.write(json, 16, "utf8");
33
+ buf.write(css, dataStart, "utf8");
34
+ return buf;
35
+ }
36
+
37
+ const SAMPLE_CSS = ":root{--text-accent:#705dcf}\nbody.theme-dark{--background-primary:#1e1e1e}\n";
38
+
39
+ test("extractAppCss returns the embedded css verbatim", () => {
40
+ const buf = buildSyntheticAsar(SAMPLE_CSS);
41
+ assert.equal(extractAppCss(buf), SAMPLE_CSS);
42
+ });
43
+
44
+ test("readAsarHeader uses the 4-byte-aligned outer offset (byte 4, not 8)", () => {
45
+ const buf = buildSyntheticAsar(SAMPLE_CSS);
46
+ const { dataStart } = readAsarHeader(buf);
47
+ // Reading byte 8 would give dataStart - 4 and corrupt the slice.
48
+ assert.equal(
49
+ buf.toString("utf8", dataStart, dataStart + Buffer.byteLength(SAMPLE_CSS)),
50
+ SAMPLE_CSS
51
+ );
52
+ });
53
+
54
+ test("findEntry walks nested directories", () => {
55
+ const { header } = readAsarHeader(buildSyntheticAsar(SAMPLE_CSS));
56
+ const entry = findEntry(header, "app.css");
57
+ assert.ok(entry);
58
+ assert.equal(entry.offset, "0");
59
+ });
60
+
61
+ test("extractAppCss throws when the file is absent", () => {
62
+ const buf = buildSyntheticAsar(SAMPLE_CSS);
63
+ assert.throws(() => extractAppCss(buf, "missing.css"), /not found/);
64
+ });
65
+
66
+ test("assertLooksLikeAppCss accepts real-looking css and rejects garbage", () => {
67
+ assert.doesNotThrow(() => assertLooksLikeAppCss(SAMPLE_CSS));
68
+ assert.throws(() => assertLooksLikeAppCss("}}}}garbage"), /does not start as expected/);
69
+ assert.throws(() => assertLooksLikeAppCss(":root{--other:1}"), /missing expected Obsidian/);
70
+ });
@@ -0,0 +1,58 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { test } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ /**
8
+ * Arrow v1.0.6 footgun guards. Arrow parses html`` templates by treating HTML
9
+ * comments as expression-slot markers, so a *literal* HTML comment inside a
10
+ * template inflates the slot count and throws "Invalid HTML position" at
11
+ * render. Every module under src/ is an Arrow component module, so no literal
12
+ * HTML comment should appear in any of them. (Use JS // comments instead.)
13
+ */
14
+
15
+ const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "src");
16
+
17
+ function tsFiles(dir) {
18
+ const out = [];
19
+ for (const name of fs.readdirSync(dir)) {
20
+ const full = path.join(dir, name);
21
+ const stat = fs.statSync(full);
22
+ if (stat.isDirectory()) out.push(...tsFiles(full));
23
+ else if (name.endsWith(".ts")) out.push(full);
24
+ }
25
+ return out;
26
+ }
27
+
28
+ test("no literal HTML comments in Arrow template modules", () => {
29
+ const offenders = tsFiles(srcDir).filter((file) =>
30
+ fs.readFileSync(file, "utf8").includes("<!--")
31
+ );
32
+ assert.deepEqual(
33
+ offenders.map((f) => path.relative(srcDir, f)),
34
+ [],
35
+ "HTML comments break Arrow templates — move them to JS // comments"
36
+ );
37
+ });
38
+
39
+ /**
40
+ * Footgun #3 (type-level): an Arrow `@event` handler must type its parameter as
41
+ * `Event`, not a narrowed subtype like `MouseEvent`. Parameter contravariance
42
+ * makes `(e: MouseEvent) => void` fail to assign to Arrow's ArrowExpression
43
+ * (TS2345). `tsc` catches this, but this guard flags the common *inline* form
44
+ * with a clearer message. Fix: type the param `Event` and narrow inside.
45
+ */
46
+ const NARROWED_INLINE_HANDLER =
47
+ /@[\w-]+="\$\{\s*(?:async\s*)?\(?[^)]*:\s*(?:Mouse|Keyboard|Pointer|Input|Focus|Touch|Wheel|Drag|Clipboard|Submit|Composition|Animation|Transition|UI)Event\b/;
48
+
49
+ test("inline @event handlers type the param as Event, not a narrowed subtype", () => {
50
+ const offenders = tsFiles(srcDir).filter((file) =>
51
+ NARROWED_INLINE_HANDLER.test(fs.readFileSync(file, "utf8"))
52
+ );
53
+ assert.deepEqual(
54
+ offenders.map((f) => path.relative(srcDir, f)),
55
+ [],
56
+ "Arrow @event handlers must use (e: Event), not a narrowed subtype (e.g. MouseEvent); narrow inside the handler instead"
57
+ );
58
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "types": ["node", "vite/client"]
11
+ },
12
+ "include": ["src/**/*.ts", "vite.config.ts"]
13
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ // Client-only sandbox. No SSR/hydration — an Obsidian plugin renders entirely
4
+ // in the Electron renderer, so we mirror that: a single client bundle mounted
5
+ // into #app, exactly like ItemView.onOpen() mounts into contentEl.
6
+ export default defineConfig({
7
+ server: {
8
+ host: "127.0.0.1",
9
+ port: 5173,
10
+ },
11
+ optimizeDeps: {
12
+ // Arrow ships pre-built ESM; let Vite serve it as-is rather than pre-bundle.
13
+ exclude: ["@arrow-js/core", "@arrow-js/framework"],
14
+ },
15
+ });