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,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
|
+
});
|