@kubohiroya/turbowarp-title-menu 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.
@@ -0,0 +1,10 @@
1
+ export { createAppShellApplicationMenu as createApplicationMenu } from '@kubohiroya/turbowarp-app-shell';
2
+ export type { AppShellApplicationMenuAction as ApplicationMenuAction, AppShellApplicationMenuOptions as ApplicationMenuOptions } from '@kubohiroya/turbowarp-app-shell';
3
+ export { createDslFilesDialog } from './dsl-files-dialog.js';
4
+ export type { DslFilesDialog, DslFilesDialogLocaleText, DslFilesDialogOptions } from './dsl-files-dialog.js';
5
+ export { createDslStore, defaultDslSort, DslStoreError, readDslFile } from './dsl-store.js';
6
+ export type { DslFileRecord, DslFileSummary, DslSort, DslSortDirection, DslSortField, DslStore, DslStoreErrorCode, DslStoreOptions } from './dsl-store.js';
7
+ export { dslOpenEventName, dslReloadEventName } from './events.js';
8
+ export type { DslSourceEventDetail } from './events.js';
9
+ export { createTitleDialog } from './title-dialog.js';
10
+ export type { TitleDialog, TitleDialogOptions, TitleDialogLocaleText } from './title-dialog.js';
@@ -0,0 +1,7 @@
1
+ export type DomDocument = Document;
2
+ export type DomElement = HTMLElement;
3
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
4
+ export declare function requireDocument(value: unknown): DomDocument;
5
+ export declare function requireElement(value: unknown, name: string): DomElement;
6
+ export declare function ensureRelativeMount(mount: DomElement): () => void;
7
+ export declare function invokeSafely(operation: () => unknown | Promise<unknown>, onError?: (error: unknown) => void): void;
@@ -0,0 +1,51 @@
1
+ import { type DslFileSummary, type DslSort } from './dsl-store';
2
+ /**
3
+ * The stage-mounted DSL file manager.
4
+ *
5
+ * The dialog owns presentation and interaction state only: sort order, which row is being renamed,
6
+ * and which row is awaiting delete confirmation. Every storage operation is a caller-supplied
7
+ * callback, so the same dialog works against the IndexedDB store, a test double, or a future
8
+ * file-system backend.
9
+ */
10
+ export interface DslFilesDialogLocaleText {
11
+ title: string;
12
+ add: string;
13
+ open: string;
14
+ rename: string;
15
+ remove: string;
16
+ confirmRemove: string;
17
+ confirm: string;
18
+ cancel: string;
19
+ close: string;
20
+ sortByName: string;
21
+ sortByDate: string;
22
+ sortBySize: string;
23
+ empty: string;
24
+ }
25
+ export interface DslFilesDialogOptions {
26
+ document?: Document;
27
+ mount?: HTMLElement;
28
+ locales: Record<string, DslFilesDialogLocaleText>;
29
+ initialLocale?: string;
30
+ initialSort?: DslSort;
31
+ list(sort: DslSort): Promise<readonly DslFileSummary[]>;
32
+ onOpen(id: string): unknown | Promise<unknown>;
33
+ onAdd(): unknown | Promise<unknown>;
34
+ onRename(id: string, name: string): unknown | Promise<unknown>;
35
+ onRemove(id: string): unknown | Promise<unknown>;
36
+ describeError?(error: unknown): string;
37
+ formatSize?(byteLength: number): string;
38
+ formatDate?(isoDate: string): string;
39
+ onError?(error: unknown): void;
40
+ }
41
+ export interface DslFilesDialog {
42
+ readonly element: HTMLElement;
43
+ readonly locale: string;
44
+ readonly sort: DslSort;
45
+ show(locale?: string): Promise<string>;
46
+ hide(): void;
47
+ refresh(): Promise<void>;
48
+ setLocale(locale: string): string;
49
+ dispose(): void;
50
+ }
51
+ export declare function createDslFilesDialog(options: DslFilesDialogOptions): DslFilesDialog;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Multi-file DSL storage backed by IndexedDB.
3
+ *
4
+ * The previous single-slot `localStorage` design could only remember the last opened file. A venue
5
+ * keeps several performances side by side, so records are addressed by a stable id, names are unique
6
+ * so a list stays meaningful, and the store never deletes a record on its own: exceeding the file
7
+ * limit fails loudly instead of silently dropping someone's DSL.
8
+ */
9
+ export interface DslFileRecord {
10
+ readonly id: string;
11
+ readonly name: string;
12
+ readonly source: string;
13
+ readonly byteLength: number;
14
+ readonly savedAt: string;
15
+ readonly updatedAt: string;
16
+ }
17
+ export type DslFileSummary = Omit<DslFileRecord, 'source'>;
18
+ export type DslSortField = 'name' | 'updatedAt' | 'byteLength';
19
+ export type DslSortDirection = 'asc' | 'desc';
20
+ export interface DslSort {
21
+ readonly field: DslSortField;
22
+ readonly direction: DslSortDirection;
23
+ }
24
+ export type DslStoreErrorCode = 'unavailable' | 'invalid-name' | 'invalid-source' | 'name-taken' | 'too-large' | 'too-many' | 'not-found' | 'quota' | 'failed';
25
+ export declare class DslStoreError extends Error {
26
+ readonly code: DslStoreErrorCode;
27
+ constructor(code: DslStoreErrorCode, message: string, cause?: unknown);
28
+ }
29
+ export interface DslStoreOptions {
30
+ readonly indexedDB?: IDBFactory;
31
+ readonly databaseName?: string;
32
+ readonly maxSourceBytes?: number;
33
+ readonly maxFileCount?: number;
34
+ readonly now?: () => Date;
35
+ readonly createId?: () => string;
36
+ }
37
+ export interface DslStore {
38
+ readonly databaseName: string;
39
+ list(sort?: DslSort): Promise<DslFileSummary[]>;
40
+ count(): Promise<number>;
41
+ get(id: string): Promise<DslFileRecord | null>;
42
+ save(file: {
43
+ name: string;
44
+ source: string;
45
+ }): Promise<DslFileRecord>;
46
+ rename(id: string, name: string): Promise<DslFileRecord>;
47
+ remove(id: string): Promise<void>;
48
+ clear(): Promise<void>;
49
+ lastOpened(): Promise<DslFileRecord | null>;
50
+ markOpened(id: string): Promise<void>;
51
+ close(): void;
52
+ }
53
+ export declare const defaultDslSort: DslSort;
54
+ export declare function createDslStore(options?: DslStoreOptions): DslStore;
55
+ /** Reads a picked browser file into a record shape the store accepts. */
56
+ export declare function readDslFile(file: File, maxSourceBytes?: number): Promise<{
57
+ name: string;
58
+ source: string;
59
+ }>;
@@ -0,0 +1,13 @@
1
+ import type { DslFileRecord } from './dsl-store';
2
+ export declare const dslOpenEventName = "turbowarp-title-menu:dsl-open";
3
+ export declare const dslReloadEventName = "turbowarp-title-menu:dsl-reload";
4
+ export interface DslSourceEventDetail {
5
+ record: DslFileRecord;
6
+ }
7
+ /**
8
+ * Announces an opened DSL source on the window.
9
+ *
10
+ * The extension also starts a Scratch hat, but a packaged host that embeds this extension may run
11
+ * its own runtime outside the VM, so the DOM event stays the transport that assumes no Scratch.
12
+ */
13
+ export declare function dispatchDslSourceEvent(type: string, record: DslFileRecord): void;
@@ -0,0 +1,14 @@
1
+ import type { DslFilesDialogLocaleText } from './dsl-files-dialog';
2
+ import type { TitleDialogLocaleText } from './title-dialog';
3
+ export type SupportedLocale = 'en' | 'ja';
4
+ export interface MenuActionLabels {
5
+ files: string;
6
+ reload: string;
7
+ about: string;
8
+ close: string;
9
+ }
10
+ export declare const titleLocales: Readonly<Record<SupportedLocale, TitleDialogLocaleText>>;
11
+ export declare const menuLocales: Readonly<Record<SupportedLocale, MenuActionLabels>>;
12
+ export declare const dslFilesLocales: Readonly<Record<SupportedLocale, DslFilesDialogLocaleText>>;
13
+ /** Turns a store error code into operator-facing text, falling back to the raw message. */
14
+ export declare function describeStoreError(locale: SupportedLocale, error: unknown): string;
@@ -0,0 +1,29 @@
1
+ export type TitleMenuLocale = string;
2
+ export interface TitleDialogLocaleText {
3
+ title: string;
4
+ author?: string;
5
+ license?: string;
6
+ website: string;
7
+ close: string;
8
+ language?: string;
9
+ }
10
+ export interface TitleDialogOptions {
11
+ document?: Document;
12
+ mount?: HTMLElement;
13
+ locales: Record<string, TitleDialogLocaleText>;
14
+ initialLocale?: TitleMenuLocale;
15
+ websiteUrl?: string;
16
+ onWebsite?: () => unknown | Promise<unknown>;
17
+ onClose?: () => unknown | Promise<unknown>;
18
+ onLocaleChange?: (locale: TitleMenuLocale) => unknown | Promise<unknown>;
19
+ onError?: (error: unknown) => void;
20
+ }
21
+ export interface TitleDialog {
22
+ readonly element: HTMLElement;
23
+ readonly locale: TitleMenuLocale;
24
+ show(locale?: TitleMenuLocale): TitleMenuLocale;
25
+ hide(): void;
26
+ setLocale(locale: TitleMenuLocale): TitleMenuLocale;
27
+ dispose(): void;
28
+ }
29
+ export declare function createTitleDialog(options: TitleDialogOptions): TitleDialog;
@@ -0,0 +1,63 @@
1
+ # アーキテクチャ
2
+
3
+ [English](architecture.md)
4
+
5
+ ## ビルド出力
6
+
7
+ このプロジェクトは実行時の動作と互換性メタデータを分離し、リポジトリに保存された同じソース定義から両方を生成します。
8
+
9
+ ```text
10
+ src/index.ts + src/extension.ts
11
+ -> vite-plugin-turbowarp-extension
12
+ -> dist/<extension>.js
13
+
14
+ src/config.ts + src/block-definitions.json
15
+ -> extension-api-manifest Viteプラグイン
16
+ -> dist/extension-manifest.json
17
+ ```
18
+
19
+ manifestプラグインはViteのビルド後フェーズで実行されます。これにより、JavaScriptプラグインの単一出力検証を維持しながら、TurboWarpバンドルの完成後にだけmanifestを追加します。
20
+
21
+ ## 拡張機能API manifest v1
22
+
23
+ `schemas/extension-manifest.schema.json`が規範となるJSON Schemaです。`formatVersion`は`1`で、互換性のないmanifest形式を導入するときに変更する必要があります。
24
+
25
+ v1契約は次の情報を含みます。
26
+
27
+ - TurboWarp拡張機能のID
28
+ - 各ブロックのopcodeとブロック種類
29
+ - 各引数のID、引数種類、任意のメニュー参照
30
+ - 各メニューのIDとReporterブロックを受け付けるかどうか
31
+
32
+ ブロック、引数、メニューは、シリアライズ前に識別子で並べ替えられます。テキスト、説明、既定値、静的メニュー項目は、保存済みプロジェクトのAPI参照を識別しないため、意図的に除外しています。そのため互換性チェッカーは、API変更とドキュメントまたはローカライズの変更を区別できます。
33
+
34
+ ## 差分の検出
35
+
36
+ `dist/`はリリース成果物としてコミットされます。`npm run check:dist`は両方のファイルを再ビルドし、`dist/`配下に変更、削除、未追跡ファイルがある場合に失敗します。これにより、ローカル検証とCIの両方でmanifestとバンドルの差分を検出できます。
37
+
38
+ ## turbowarp-app-shellとの層の分け方
39
+
40
+ `@kubohiroya/turbowarp-app-shell`はDOMの機構を所有し、Scratch APIもアプリの語彙も持ちません。
41
+ 本パッケージはその上の層で、primitiveを合成してタイトル画面、アプリケーションメニュー、DSL
42
+ ファイル保管を作り、blockとして公開する完成した拡張です。
43
+
44
+ アプリケーションメニューは`createAppShellApplicationMenu`をそのまま使っており、同じ部品の二重
45
+ 実装ではありません。項目は本拡張の語彙なので、別の項目が必要なホストはcomposition API経由で同じ
46
+ primitiveを自分で合成します。タイトルダイアログだけは本パッケージが持ちます。app-shellにはabout
47
+ パネルのprimitiveがなく、こちらはタイトル・作者・ライセンス・Webサイトをまとめて表示するため、
48
+ website/closeのボタン対とは別物だからです。
49
+
50
+ ## DSLファイルの保管
51
+
52
+ `src/dsl-store.ts`は複数のDSLファイルをIndexedDBに保管します。データベースは、生成したidをkeyに
53
+ 持ち`name`に一意indexを張った`files` storeと、最後に開いたidを持つ`meta` storeで構成します。
54
+
55
+ 名前を一意にしているのは、一覧の意味を保ち、同じファイルを再度追加したときに重複を増やさず上書き
56
+ するためです。storeが自分でレコードを消すことはありません。上限を超えるファイルや、件数上限を超える
57
+ 追加は、型付きの`DslStoreError`で拒否します。黙って以前のDSLを失うより、理由を運用者に伝えることを
58
+ 優先します。並べ替えはsummaryを読んだ後にメモリ上で行い、indexを増やさず、名前による同点処理で
59
+ 順序を安定させます。
60
+
61
+ `src/dsl-files-dialog.ts`は表示と操作の状態だけを持ちます。並べ替え順、名前を編集中の行、削除の
62
+ 確認待ちの行の3つです。保管処理はすべて呼び出し側のコールバックなので、IndexedDBのstoreでも、
63
+ テスト用の代替でも、将来のファイルシステム実装でも、ダイアログを変えずに使えます。
@@ -0,0 +1,73 @@
1
+ # Architecture
2
+
3
+ [日本語](architecture.ja.md)
4
+
5
+ ## Build outputs
6
+
7
+ The project keeps runtime behavior and compatibility metadata separate while generating both from
8
+ the same checked-in source definitions.
9
+
10
+ ```text
11
+ src/index.ts + src/extension.ts
12
+ -> vite-plugin-turbowarp-extension
13
+ -> dist/<extension>.js
14
+
15
+ src/config.ts + src/block-definitions.json
16
+ -> extension-api-manifest Vite plugin
17
+ -> dist/extension-manifest.json
18
+ ```
19
+
20
+ The manifest plugin runs in Vite's post-build phase. This preserves the JavaScript plugin's
21
+ single-output validation and adds the manifest only after the TurboWarp bundle is complete.
22
+
23
+ ## Extension API manifest v1
24
+
25
+ `schemas/extension-manifest.schema.json` is the normative JSON Schema. `formatVersion` is `1` and
26
+ must change when an incompatible manifest shape is introduced.
27
+
28
+ The v1 contract contains:
29
+
30
+ - the TurboWarp extension ID;
31
+ - each block opcode and block type;
32
+ - each argument ID, argument type, and optional menu reference;
33
+ - each menu ID and whether it accepts reporter blocks.
34
+
35
+ Blocks, arguments, and menus are sorted by their identifiers before serialization. Text,
36
+ descriptions, default values, and static menu items are intentionally excluded because they do not
37
+ identify saved-project API references. A compatibility checker can therefore distinguish API
38
+ changes from documentation or localization changes.
39
+
40
+ ## Drift detection
41
+
42
+ `dist/` is committed as a release artifact. `npm run check:dist` rebuilds both files and fails when
43
+ Git reports any modified, deleted, or untracked file below `dist/`. This catches manifest and bundle
44
+ drift in local checks and CI.
45
+
46
+ ## Layering against turbowarp-app-shell
47
+
48
+ `@kubohiroya/turbowarp-app-shell` owns the DOM mechanics and knows no Scratch API and no application
49
+ vocabulary. This package is the layer above it: a finished TurboWarp extension that composes those
50
+ primitives into a title screen, an application menu, and DSL file storage, and publishes them as
51
+ blocks.
52
+
53
+ The application menu is `createAppShellApplicationMenu` used directly, not a second implementation
54
+ of the same control. Its actions are this extension's own vocabulary, so a host project that needs
55
+ different actions composes the same primitive through the composition API instead of inheriting a
56
+ fixed set. The title dialog stays local because app-shell has no about-panel primitive: it presents
57
+ title, author, license, and website together rather than a website/close control pair.
58
+
59
+ ## DSL file storage
60
+
61
+ `src/dsl-store.ts` keeps many DSL files in IndexedDB. The database holds a `files` store keyed by a
62
+ generated id with a unique index on `name`, and a `meta` store holding the last opened id.
63
+
64
+ Names are unique so a listing stays meaningful and re-adding the same file updates it in place
65
+ instead of accumulating duplicates. The store never evicts a record: a file over the byte limit, or
66
+ one past the file-count limit, is refused with a typed `DslStoreError` so the operator learns why
67
+ rather than losing an earlier DSL silently. Sorting happens in memory after reading the summaries,
68
+ which keeps the index set small and the order stable through a name tiebreak.
69
+
70
+ `src/dsl-files-dialog.ts` owns presentation and interaction state only: the sort order, the row
71
+ being renamed, and the row awaiting delete confirmation. Every storage operation is a caller
72
+ supplied callback, so the dialog works against the IndexedDB store, a test double, or a future
73
+ file-system backend without change.
@@ -0,0 +1,34 @@
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>TurboWarp Title Menu documentation</title>
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.6;color:#222}
9
+ code{background:#f3f3f3;padding:.1em .3em;border-radius:.25em}
10
+ pre{background:#f6f8fa;padding:1rem;overflow:auto;border-radius:.5rem}
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>TurboWarp Title Menu</h1>
15
+ <p>Reusable title, menu, and DSL source storage controls for TurboWarp projects.</p>
16
+
17
+ <h2>Blocks</h2>
18
+ <ul>
19
+ <li><code>show title dialog</code>: shows title, author, license, official website, and close controls above the stage.</li>
20
+ <li><code>show application menu</code>: shows a stage-mounted menu for DSL open, reload, about, and close actions.</li>
21
+ <li><code>has saved DSL source?</code>: reports whether localStorage contains a saved DSL source.</li>
22
+ <li><code>saved DSL file name</code>: reports the stored DSL source file name.</li>
23
+ </ul>
24
+
25
+ <h2>Composition API</h2>
26
+ <p>Host runtimes can import <code>createTitleDialog</code>, <code>createApplicationMenu</code>, and <code>createDslStorage</code> from <code>@kubohiroya/turbowarp-title-menu</code>, or use the <code>TurboWarpTitleMenu</code> global exposed by the standalone extension bundle.</p>
27
+
28
+ <h2>Safety</h2>
29
+ <p>The extension is unsandboxed because it mounts DOM controls and uses browser storage. Load reviewed builds only.</p>
30
+
31
+ <h2>Development</h2>
32
+ <p>See the repository README and architecture documentation for build, test, release, and compatibility details.</p>
33
+ </body>
34
+ </html>
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@kubohiroya/turbowarp-title-menu",
3
+ "version": "0.1.0",
4
+ "description": "Reusable title, application menu, and DSL source storage controls for TurboWarp.",
5
+ "author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
6
+ "license": "MPL-2.0",
7
+ "homepage": "https://github.com/kubohiroya/turbowarp-title-menu#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kubohiroya/turbowarp-title-menu.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/kubohiroya/turbowarp-title-menu/issues"
14
+ },
15
+ "keywords": [
16
+ "turbowarp",
17
+ "scratch",
18
+ "extension",
19
+ "menu",
20
+ "title",
21
+ "localstorage",
22
+ "dsl",
23
+ "vite",
24
+ "typescript"
25
+ ],
26
+ "type": "module",
27
+ "types": "./dist/types/composition.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/types/composition.d.ts",
31
+ "import": "./dist/lib/composition.js"
32
+ },
33
+ "./composition": {
34
+ "types": "./dist/types/composition.d.ts",
35
+ "import": "./dist/lib/composition.js"
36
+ },
37
+ "./extension": "./dist/turbowarp-title-menu.js",
38
+ "./extension-manifest.json": "./dist/extension-manifest.json"
39
+ },
40
+ "files": [
41
+ "dist/",
42
+ "docs/",
43
+ "schemas/",
44
+ "src/",
45
+ "README.md",
46
+ "README.ja.md",
47
+ "LICENSE"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org/"
52
+ },
53
+ "packageManager": "pnpm@11.11.0",
54
+ "engines": {
55
+ "node": ">=22"
56
+ },
57
+ "scripts": {
58
+ "dev": "vite build --watch",
59
+ "typecheck": "tsc --noEmit",
60
+ "build": "vite build && tsc -p tsconfig.lib.json",
61
+ "lint": "eslint src tests vite.config.ts",
62
+ "test": "vitest run",
63
+ "docs": "node scripts/generate-readme.mjs",
64
+ "docs:check": "pnpm run docs && git diff --exit-code -- README.md",
65
+ "repo:check": "node scripts/check-repo.mjs",
66
+ "pack:check": "npm pack --dry-run --ignore-scripts",
67
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run docs:check && pnpm run check:dist && pnpm run repo:check && pnpm run pack:check",
68
+ "check:dist": "pnpm run build && node scripts/check-dist.mjs"
69
+ },
70
+ "devDependencies": {
71
+ "@eslint/js": "^9.39.5",
72
+ "@kubohiroya/vite-plugin-turbowarp-extension": "0.1.1",
73
+ "@types/node": "^24.13.3",
74
+ "eslint": "^9.39.5",
75
+ "fake-indexeddb": "6.2.5",
76
+ "typescript": "^5.9.3",
77
+ "typescript-eslint": "^8.66.0",
78
+ "vite": "^7.3.6",
79
+ "vitest": "^4.1.10"
80
+ },
81
+ "dependencies": {
82
+ "@kubohiroya/turbowarp-app-shell": "0.2.0"
83
+ }
84
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/kubohiroya/turbowarp-title-menu/main/schemas/extension-manifest.schema.json",
4
+ "title": "TurboWarp Extension API Manifest",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["formatVersion", "id", "blocks", "menus"],
8
+ "properties": {
9
+ "formatVersion": {"const": 1},
10
+ "id": {"type": "string", "pattern": "^[a-z0-9]+$"},
11
+ "blocks": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["opcode", "blockType", "arguments"],
17
+ "properties": {
18
+ "opcode": {"type": "string", "minLength": 1},
19
+ "blockType": {"type": "string", "minLength": 1},
20
+ "arguments": {
21
+ "type": "array",
22
+ "items": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["id", "type"],
26
+ "properties": {
27
+ "id": {"type": "string", "minLength": 1},
28
+ "type": {"type": "string", "minLength": 1},
29
+ "menu": {"type": "string", "minLength": 1}
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ },
36
+ "menus": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "required": ["id", "acceptReporters"],
42
+ "properties": {
43
+ "id": {"type": "string", "minLength": 1},
44
+ "acceptReporters": {"type": "boolean"}
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "extensionName": "TurboWarp Title Menu",
3
+ "blocks": [
4
+ {
5
+ "opcode": "showTitle",
6
+ "blockType": "COMMAND",
7
+ "text": "show title dialog",
8
+ "description": "Shows the configured title dialog above the TurboWarp stage."
9
+ },
10
+ {
11
+ "opcode": "showMenu",
12
+ "blockType": "COMMAND",
13
+ "text": "show application menu",
14
+ "description": "Shows the application menu above the TurboWarp stage."
15
+ },
16
+ {
17
+ "opcode": "showDslFiles",
18
+ "blockType": "COMMAND",
19
+ "text": "show DSL file manager",
20
+ "description": "Shows the dialog that adds, opens, renames, deletes, and sorts stored DSL files."
21
+ },
22
+ {
23
+ "opcode": "whenDslSourceOpened",
24
+ "blockType": "HAT",
25
+ "text": "when a DSL source is opened",
26
+ "description": "Runs after the operator opens a stored DSL file, or after the opened source is announced again."
27
+ },
28
+ {
29
+ "opcode": "reloadOpenedDsl",
30
+ "blockType": "COMMAND",
31
+ "text": "reload the opened DSL source",
32
+ "description": "Announces the currently opened DSL source again without showing a dialog."
33
+ },
34
+ {
35
+ "opcode": "openedDslName",
36
+ "blockType": "REPORTER",
37
+ "text": "opened DSL file name",
38
+ "description": "Returns the name of the DSL file that is currently open, or an empty string."
39
+ },
40
+ {
41
+ "opcode": "openedDslSource",
42
+ "blockType": "REPORTER",
43
+ "text": "opened DSL source",
44
+ "description": "Returns the text of the DSL file that is currently open, or an empty string."
45
+ },
46
+ {
47
+ "opcode": "hasSavedDsl",
48
+ "blockType": "BOOLEAN",
49
+ "text": "has a saved DSL file?",
50
+ "description": "Reports whether at least one DSL file is stored in IndexedDB."
51
+ },
52
+ {
53
+ "opcode": "savedDslCount",
54
+ "blockType": "REPORTER",
55
+ "text": "saved DSL file count",
56
+ "description": "Returns how many DSL files are stored in IndexedDB."
57
+ },
58
+ {
59
+ "opcode": "lastDslError",
60
+ "blockType": "REPORTER",
61
+ "text": "last DSL storage error",
62
+ "description": "Returns the most recent DSL storage failure in the interface language, or an empty string."
63
+ }
64
+ ]
65
+ }
@@ -0,0 +1,26 @@
1
+ export {createAppShellApplicationMenu as createApplicationMenu} from '@kubohiroya/turbowarp-app-shell';
2
+ export type {
3
+ AppShellApplicationMenuAction as ApplicationMenuAction,
4
+ AppShellApplicationMenuOptions as ApplicationMenuOptions
5
+ } from '@kubohiroya/turbowarp-app-shell';
6
+ export {createDslFilesDialog} from './dsl-files-dialog.js';
7
+ export type {
8
+ DslFilesDialog,
9
+ DslFilesDialogLocaleText,
10
+ DslFilesDialogOptions
11
+ } from './dsl-files-dialog.js';
12
+ export {createDslStore, defaultDslSort, DslStoreError, readDslFile} from './dsl-store.js';
13
+ export type {
14
+ DslFileRecord,
15
+ DslFileSummary,
16
+ DslSort,
17
+ DslSortDirection,
18
+ DslSortField,
19
+ DslStore,
20
+ DslStoreErrorCode,
21
+ DslStoreOptions
22
+ } from './dsl-store.js';
23
+ export {dslOpenEventName, dslReloadEventName} from './events.js';
24
+ export type {DslSourceEventDetail} from './events.js';
25
+ export {createTitleDialog} from './title-dialog.js';
26
+ export type {TitleDialog, TitleDialogOptions, TitleDialogLocaleText} from './title-dialog.js';
package/src/config.ts ADDED
@@ -0,0 +1,13 @@
1
+ export const extensionConfig = {
2
+ id: 'kubohiroyaturbowarptitlemenu',
3
+ slug: 'turbowarp-title-menu',
4
+ name: 'TurboWarp Title Menu',
5
+ description: 'Reusable title, application menu, and DSL source storage controls for TurboWarp.',
6
+ author: 'Hiroya Kubo',
7
+ license: 'MPL-2.0',
8
+ unsandboxed: true,
9
+ homepage: 'https://github.com/kubohiroya/turbowarp-title-menu',
10
+ docsURI: 'https://kubohiroya.github.io/turbowarp-title-menu/',
11
+ blockIconURI:
12
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PHJlY3QgeD0iNSIgeT0iOCIgd2lkdGg9IjM4IiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzAwN2Q2NiIvPjxyZWN0IHg9IjkiIHk9IjEyIiB3aWR0aD0iMzAiIGhlaWdodD0iMjQiIHJ4PSIyIiBmaWxsPSIjZjRmZmZiIi8+PHBhdGggZD0iTTE1IDE5aDE4TTE1IDI0aDE4TTE1IDI5aDEyIiBzdHJva2U9IiMwMDdkNjYiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+'
13
+ } as const;
package/src/dom.ts ADDED
@@ -0,0 +1,39 @@
1
+ export type DomDocument = Document;
2
+ export type DomElement = HTMLElement;
3
+
4
+ export function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ export function requireDocument(value: unknown): DomDocument {
9
+ if (!isRecord(value) || typeof value.createElement !== 'function') {
10
+ throw new TypeError('document must provide createElement');
11
+ }
12
+ return value as unknown as DomDocument;
13
+ }
14
+
15
+ export function requireElement(value: unknown, name: string): DomElement {
16
+ if (!isRecord(value) || typeof value.appendChild !== 'function') {
17
+ throw new TypeError(`${name} must be a DOM element`);
18
+ }
19
+ return value as unknown as DomElement;
20
+ }
21
+
22
+ export function ensureRelativeMount(mount: DomElement): () => void {
23
+ const previous = mount.style.position;
24
+ if (previous === '' || previous === 'static') {
25
+ mount.style.position = 'relative';
26
+ return () => {
27
+ mount.style.position = previous;
28
+ };
29
+ }
30
+ return () => {};
31
+ }
32
+
33
+ export function invokeSafely(operation: () => unknown | Promise<unknown>, onError?: (error: unknown) => void): void {
34
+ try {
35
+ Promise.resolve(operation()).catch((error: unknown) => onError?.(error));
36
+ } catch (error) {
37
+ onError?.(error);
38
+ }
39
+ }