@frostime/siyuan-hspa
v0.1.1
Published
Iframe-based HTML single page UI toolkit for SiYuan plugins.
Readme
@frostime/siyuan-hspa
Iframe-based HTML Single Page Application toolkit for SiYuan plugins.
HSPA pages are standalone HTML files loaded in plugin-owned iframes. The host plugin opens the page as a SiYuan tab/dialog and injects window.pluginSdk for kernel APIs, SiYuan UI helpers, and host-defined functions.
Primary agent guide: skill/hspa/SKILL.md. Load it before implementing or reviewing HSPA pages in downstream plugin projects.
Install
pnpm add @frostime/siyuan-hspa
pnpm add -D siyuan vite-plugin-static-copysiyuan is a peer dependency because host plugins already compile against SiYuan's plugin API.
CLI
pnpm exec siyuan-hspa init --dry-run
pnpm exec siyuan-hspa init --yes
pnpm exec siyuan-hspa doctor
pnpm exec siyuan-hspa doctor --jsoninit copies project-local agent assets:
.agents/skills/siyuan-hspa/SKILL.md
src/pages/hspa-demo.htmlSafety contract:
- writes require
--yes --dry-runwrites nothing- existing files are skipped unless
--forceis set - writes stay inside
--cwd - no
docs/directory is created
doctor is read-only. It checks dependencies, Vite static-copy wiring, project-local SKILL presence, and common HTML authoring mistakes.
Vite setup
// vite.config.ts
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { hspaStaticCopyTargets } from '@frostime/siyuan-hspa/vite';
export default defineConfig({
plugins: [
viteStaticCopy({
targets: [
...hspaStaticCopyTargets(),
{ src: 'src/pages/*.html', dest: 'pages' },
],
}),
],
});Default copied assets:
dist/hspa/styles/hspa-mini.css
dist/hspa/scripts/alpine.min.jsHTML pages run under SiYuan's plugin asset route. Use the plugin manifest name from plugin.json, not a relative filesystem path:
<link rel="stylesheet" href="/plugins/<plugin-name>/hspa/styles/hspa-mini.css">
<script src="/plugins/<plugin-name>/hspa/scripts/alpine.min.js" defer></script>pnpm exec siyuan-hspa init --yes generates src/pages/hspa-demo.html with <plugin-name> replaced from plugin.json#name when available.
Open a page
import { hspaPageUrl, openIframeTab } from '@frostime/siyuan-hspa';
import type { Plugin } from 'siyuan';
export function openDemo(plugin: Plugin) {
openIframeTab(plugin, {
tabId: 'demo-hspa',
title: 'Demo HSPA',
icon: 'iconHTML5',
iframeConfig: {
type: 'url',
source: hspaPageUrl(plugin, 'hspa-demo.html'),
inject: {
presetSdk: true,
customSdk: {
getGreeting: () => 'hello from host plugin',
},
},
},
});
}customSdk is flat-merged into window.pluginSdk; use pluginSdk.getGreeting(), not pluginSdk.customSdk.getGreeting().
HTML page pattern
<link rel="stylesheet" href="/plugins/<plugin-name>/hspa/styles/hspa-mini.css">
<main class="page">
<section class="card">
<button id="ping" class="btn btn-primary">Ping</button>
</section>
</main>
<script>
window.addEventListener('pluginSdkReady', async () => {
const sdk = window.pluginSdk;
document.getElementById('ping').addEventListener('click', () => {
sdk.showMessage('pong');
});
});
</script>Rules:
- Wait for
pluginSdkReadybefore readingwindow.pluginSdk. - Use same-origin plugin pages, usually
/plugins/<plugin>/pages/*.html. - Use
pluginSdk.showMessage()/pluginSdk.confirm()instead of native dialogs. hspa-mini.cssis not Tailwind; use documented classes such asbtn btn-primaryandtext-muted.
Exports
import {
createIframePage,
createSiyuanIframePage,
openIframeTab,
openIframeDialog,
buildPresetSdk,
pluginAssetUrl,
hspaPageUrl,
} from '@frostime/siyuan-hspa';
import { hspaStaticCopyTargets } from '@frostime/siyuan-hspa/vite';Package contents
assets/hspa/ hspa-mini.css and Alpine vendor asset
examples/ Vanilla, Alpine, and runnable mock plugin examples
skill/hspa/SKILL.md Primary agent-facing usage guideValidation
pnpm run type-check
pnpm build
node dist/cli.js --help
node dist/cli.js doctor --cwd examples/mock-plugin
pnpm pack --dry-run
cd examples/mock-plugin
pnpm exec tsc --noEmit --pretty false
pnpm build