@tavora/create-module 0.0.0-stage → 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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ What changed in `@tavora/create-module`, newest first.
4
+
5
+ ## 0.1.0
6
+
7
+ - First release on npm: `npm create @tavora/module` makes a vanilla TypeScript, Vue or React module project.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wellbrained
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @tavora/create-module
2
+
3
+ Makes a new [Tavora](https://mytavora.app) module project.
4
+
5
+ ```sh
6
+ npm create @tavora/module
7
+ # or
8
+ pnpm create @tavora/module
9
+ ```
10
+
11
+ It asks for a folder, the module's name, its id (`publisher.module-name`) and a template: **vanilla** TypeScript, **vue** or **react**. Give them up front to skip the questions:
12
+
13
+ ```sh
14
+ npm create @tavora/module raid-timer -- --template vue --id example.raid-timer --name "Raid Timer"
15
+ pnpm create @tavora/module raid-timer --template vue --id example.raid-timer --name "Raid Timer"
16
+ ```
17
+
18
+ `--yes` takes the suggestions for anything not given.
19
+
20
+ The project builds with Vite, has a `dev` script that rebuilds on every change (add the folder in Tavora under **Settings > Modules > Add a development folder**), and a `zip` script that packs and signs it with `@tavora/pack`. Its manifest sets `minTavora` to the oldest Tavora that has everything the SDK it starts with offers. The project's README says the rest.
21
+
22
+ More in the [developer docs](https://docs.mytavora.app/guide/getting-started).
@@ -0,0 +1,29 @@
1
+ export declare const TEMPLATES: readonly ["vanilla", "vue", "react"];
2
+ export type Template = (typeof TEMPLATES)[number];
3
+ export interface ModuleOptions {
4
+ /** The project folder to make; it must not exist yet, or be empty. */
5
+ dir: string;
6
+ /** The module id, publisher.module-name. */
7
+ id: string;
8
+ /** The name players see. */
9
+ name: string;
10
+ template: Template;
11
+ /** The package manager the commands in the README use. */
12
+ packageManager?: "npm" | "pnpm" | "yarn";
13
+ }
14
+ export declare class CreateError extends Error {
15
+ }
16
+ /** A folder name as a lowercase slug: letters, digits and dashes, starting with a letter. */
17
+ export declare function slugOf(folder: string): string;
18
+ /** What the starter offers when nothing was given: a name and an id from the folder's name. */
19
+ export declare function defaultsFor(folder: string): {
20
+ id: string;
21
+ name: string;
22
+ };
23
+ /** Why an id cannot be used, or null. */
24
+ export declare function idProblem(id: string): string | null;
25
+ /** Why a name cannot be used, or null. */
26
+ export declare function nameProblem(name: string): string | null;
27
+ /** Makes the project and answers with the files it wrote, relative to `dir`. */
28
+ export declare function createModule(options: ModuleOptions): Promise<string[]>;
29
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,SAAS,sCAAuC,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAElD,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;CAC1C;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAKzC,6FAA6F;AAC7F,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAO7C;AAED,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQxE;AAED,yCAAyC;AACzC,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInD;AAED,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEvD;AAyBD,gFAAgF;AAChF,wBAAsB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAgC5E"}
package/dist/create.js ADDED
@@ -0,0 +1,104 @@
1
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { isModuleId } from "@tavora/module-schema";
5
+ import { VERSIONS } from "./versions.js";
6
+ // Makes a new module project from a template: the shared files (manifest, README, styles,
7
+ // .gitignore) and the template's own (package.json, build config, page and code). Only the
8
+ // placeholders listed here are filled in; anything else in braces (such as Vue's own {{ }}) is
9
+ // left as it is.
10
+ export const TEMPLATES = ["vanilla", "vue", "react"];
11
+ export class CreateError extends Error {
12
+ }
13
+ /** The templates folder, next to dist/ (built) and src/ (in Tavora's repository). */
14
+ const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
15
+ /** A folder name as a lowercase slug: letters, digits and dashes, starting with a letter. */
16
+ export function slugOf(folder) {
17
+ const slug = path
18
+ .basename(path.resolve(folder))
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, "-")
21
+ .replace(/^-+|-+$/g, "");
22
+ return /^[a-z]/.test(slug) ? slug.slice(0, 60) : `module-${slug}`.replace(/-+$/, "").slice(0, 60);
23
+ }
24
+ /** What the starter offers when nothing was given: a name and an id from the folder's name. */
25
+ export function defaultsFor(folder) {
26
+ const slug = slugOf(folder);
27
+ const name = slug
28
+ .split("-")
29
+ .filter(Boolean)
30
+ .map((word) => word[0].toUpperCase() + word.slice(1))
31
+ .join(" ");
32
+ return { id: `my.${slug}`, name: name || "My Module" };
33
+ }
34
+ /** Why an id cannot be used, or null. */
35
+ export function idProblem(id) {
36
+ if (!isModuleId(id))
37
+ return "an id looks like publisher.module-name: 2 to 5 parts of lowercase letters, digits and dashes, separated by dots, each starting with a letter";
38
+ if (id.startsWith("tavora."))
39
+ return "ids starting with tavora. are Tavora's own";
40
+ return null;
41
+ }
42
+ /** Why a name cannot be used, or null. */
43
+ export function nameProblem(name) {
44
+ return name.trim() === "" || name.length > 80 ? "a name is 1 to 80 characters" : null;
45
+ }
46
+ const run = (manager) => (manager === "pnpm" ? "pnpm" : manager === "yarn" ? "yarn" : "npm run");
47
+ const exec = (manager) => (manager === "pnpm" ? "pnpm exec" : manager === "yarn" ? "yarn" : "npx");
48
+ /** Fills the placeholders of one file, escaped for where they stand. */
49
+ function fill(text, file, values) {
50
+ const escape = file.endsWith(".json")
51
+ ? (value) => JSON.stringify(value).slice(1, -1)
52
+ : file.endsWith(".html")
53
+ ? (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
54
+ : (value) => value;
55
+ return text.replace(/\{\{(\w+)\}\}/g, (match, key) => (key in values ? escape(values[key]) : match));
56
+ }
57
+ async function files(root, prefix = "") {
58
+ const found = [];
59
+ for (const entry of await readdir(path.join(root, prefix), { withFileTypes: true })) {
60
+ const relative = path.join(prefix, entry.name);
61
+ if (entry.isDirectory())
62
+ found.push(...(await files(root, relative)));
63
+ else
64
+ found.push(relative);
65
+ }
66
+ return found;
67
+ }
68
+ /** Makes the project and answers with the files it wrote, relative to `dir`. */
69
+ export async function createModule(options) {
70
+ const problem = idProblem(options.id) ?? nameProblem(options.name);
71
+ if (problem)
72
+ throw new CreateError(problem);
73
+ if (!TEMPLATES.includes(options.template))
74
+ throw new CreateError(`the template is one of ${TEMPLATES.join(", ")}`);
75
+ const dir = path.resolve(options.dir);
76
+ const existing = await readdir(dir).catch(() => null);
77
+ if (existing && existing.length > 0)
78
+ throw new CreateError(`${dir} is not empty`);
79
+ const values = {
80
+ id: options.id,
81
+ name: options.name.trim(),
82
+ description: `${options.name.trim()}, a Tavora module.`,
83
+ package: slugOf(dir),
84
+ sdkVersion: VERSIONS.sdk,
85
+ packVersion: VERSIONS.pack,
86
+ minTavora: VERSIONS.minTavora,
87
+ run: run(options.packageManager),
88
+ exec: exec(options.packageManager),
89
+ };
90
+ const written = [];
91
+ for (const source of ["shared", options.template]) {
92
+ const root = path.join(TEMPLATES_DIR, source);
93
+ for (const file of await files(root)) {
94
+ // npm drops .gitignore from published packages, so it travels as gitignore.
95
+ const target = file === "gitignore" ? ".gitignore" : file;
96
+ const text = await readFile(path.join(root, file), "utf8");
97
+ await mkdir(path.dirname(path.join(dir, target)), { recursive: true });
98
+ await writeFile(path.join(dir, target), fill(text, target, values));
99
+ written.push(target.split(path.sep).join("/"));
100
+ }
101
+ }
102
+ return written.sort();
103
+ }
104
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // npm create @tavora/module [folder] -- [--template vanilla|vue|react] [--id publisher.name] [--name "Name"] [--yes]
3
+ // pnpm create @tavora/module [folder] [--template ...]
4
+ // Makes a new Tavora module project. Asks for what was not given, unless --yes (or no terminal).
5
+ import path from "node:path";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { parseArgs } from "node:util";
8
+ import { createModule, CreateError, defaultsFor, idProblem, nameProblem, TEMPLATES } from "./create.js";
9
+ const USAGE = "usage: npm create @tavora/module [folder] -- [--template vanilla|vue|react] [--id publisher.module-name] [--name \"Module name\"] [--yes]";
10
+ /** The package manager that ran the starter, so the printed commands fit. */
11
+ function packageManager() {
12
+ const agent = process.env.npm_config_user_agent ?? "";
13
+ return agent.startsWith("pnpm") ? "pnpm" : agent.startsWith("yarn") ? "yarn" : "npm";
14
+ }
15
+ async function main() {
16
+ let parsed;
17
+ try {
18
+ parsed = parseArgs({
19
+ allowPositionals: true,
20
+ options: { template: { type: "string", short: "t" }, id: { type: "string" }, name: { type: "string" }, yes: { type: "boolean", short: "y" }, help: { type: "boolean", short: "h" } },
21
+ });
22
+ }
23
+ catch (error) {
24
+ console.error(`${error.message}\n${USAGE}`);
25
+ return 2;
26
+ }
27
+ const { values, positionals } = parsed;
28
+ if (values.help || positionals.length > 1) {
29
+ console.log(USAGE);
30
+ return values.help ? 0 : 2;
31
+ }
32
+ const interactive = !values.yes && process.stdin.isTTY === true;
33
+ const prompt = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
34
+ /** Asks until the answer is fine; Enter takes the suggestion. */
35
+ const ask = async (question, suggestion, problem = () => null) => {
36
+ if (!prompt)
37
+ return suggestion;
38
+ for (;;) {
39
+ const answer = (await prompt.question(`${question} (${suggestion}): `)).trim() || suggestion;
40
+ const wrong = problem(answer);
41
+ if (!wrong)
42
+ return answer;
43
+ console.log(` ${wrong}`);
44
+ }
45
+ };
46
+ try {
47
+ const folder = positionals[0] ?? (await ask("Project folder", "my-tavora-module"));
48
+ const defaults = defaultsFor(folder);
49
+ const name = values.name ?? (await ask("Module name", defaults.name, nameProblem));
50
+ const id = values.id ?? (await ask("Module id, publisher.module-name", defaults.id, idProblem));
51
+ const template = (values.template ?? (await ask(`Template, ${TEMPLATES.join(" / ")}`, "vanilla", (value) => (TEMPLATES.includes(value) ? null : `one of ${TEMPLATES.join(", ")}`))));
52
+ const manager = packageManager();
53
+ await createModule({ dir: folder, id, name, template, packageManager: manager });
54
+ const run = manager === "npm" ? "npm run" : manager;
55
+ console.log(`\nMade ${name} (${id}) in ${path.resolve(folder)}.\n`);
56
+ console.log("Next:");
57
+ console.log(` cd ${folder}`);
58
+ console.log(` ${manager} install`);
59
+ console.log(` ${run} dev`);
60
+ console.log("Then in Tavora: Settings > Modules > Add a development folder, and choose that folder.");
61
+ console.log("The README says how to share the module and show your name on it.");
62
+ return 0;
63
+ }
64
+ catch (error) {
65
+ if (error instanceof CreateError) {
66
+ console.error(`create-module: ${error.message}`);
67
+ return 1;
68
+ }
69
+ throw error;
70
+ }
71
+ finally {
72
+ prompt?.close();
73
+ }
74
+ }
75
+ main().then((code) => (process.exitCode = code), (error) => {
76
+ console.error(error);
77
+ process.exitCode = 1;
78
+ });
79
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare const VERSIONS: {
2
+ readonly sdk: "0.2.2";
3
+ readonly pack: "0.1.0";
4
+ readonly minTavora: "0.17.12";
5
+ };
6
+ //# sourceMappingURL=versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versions.d.ts","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;;;;CAIX,CAAC"}
@@ -0,0 +1,9 @@
1
+ // What a new module starts with: the SDK and pack versions released together with this starter,
2
+ // and the oldest Tavora that has everything that SDK offers (its MIN_TAVORA). A test keeps these
3
+ // equal to the packages in Tavora's repository, so a release never ships an old starter.
4
+ export const VERSIONS = {
5
+ sdk: "0.2.2",
6
+ pack: "0.1.0",
7
+ minTavora: "0.17.12",
8
+ };
9
+ //# sourceMappingURL=versions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,iGAAiG;AACjG,yFAAyF;AACzF,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG,EAAE,OAAO;IACZ,IAAI,EAAE,OAAO;IACb,SAAS,EAAE,SAAS;CACZ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@tavora/create-module",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Makes a new Tavora module project: npm create @tavora/module.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "tavora",
8
+ "module",
9
+ "create",
10
+ "starter",
11
+ "template"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/wellbrained/tavora.git",
16
+ "directory": "packages/create-module"
17
+ },
18
+ "homepage": "https://docs.mytavora.app/guide/getting-started",
19
+ "bugs": "https://github.com/wellbrained/tavora/issues",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "bin": {
24
+ "create-module": "./dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "templates",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22.12"
34
+ },
35
+ "dependencies": {
36
+ "@tavora/module-schema": "^0.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.20.5",
40
+ "@tavora/sdk": "0.2.2"
41
+ },
42
+ "license": "MIT",
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json",
45
+ "typecheck": "tsc --noEmit -p ."
46
+ }
6
47
  }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{package}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite build --watch",
8
+ "build": "vite build",
9
+ "zip": "tavora-pack ."
10
+ },
11
+ "dependencies": {
12
+ "@tavora/sdk": "^{{sdkVersion}}",
13
+ "react": "^19.3.0",
14
+ "react-dom": "^19.3.0"
15
+ },
16
+ "devDependencies": {
17
+ "@tavora/pack": "^{{packVersion}}",
18
+ "@types/react": "^19.3.0",
19
+ "@types/react-dom": "^19.3.0",
20
+ "@vitejs/plugin-react": "^6.1.2",
21
+ "typescript": "^5.9.3",
22
+ "vite": "^8.3.4"
23
+ }
24
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>{{name}}</title>
6
+ <link rel="stylesheet" href="./style.css" />
7
+ <script type="module" src="./main.tsx"></script>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,67 @@
1
+ import { connect, type TavoraSDK, type TavoraTheme } from "@tavora/sdk";
2
+ import { useEffect, useState } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+
5
+ // This page runs inside a Tavora panel; in a normal browser connect() fails. Build it and add this
6
+ // folder in Tavora (Settings > Modules > Add a development folder) to see it.
7
+
8
+ /** Your texts in English and German; Tavora says which language the player uses. */
9
+ const TEXTS = {
10
+ en: { connecting: "Connecting to Tavora…", playing: "Playing {game}.", clicked: "Clicked {count} times in this game." },
11
+ de: { connecting: "Verbinde mit Tavora …", playing: "Du spielst {game}.", clicked: "{count}-mal in diesem Spiel geklickt." },
12
+ };
13
+ type Language = keyof typeof TEXTS;
14
+ const text = (language: Language, key: keyof (typeof TEXTS)["en"], params: Record<string, string | number> = {}) =>
15
+ TEXTS[language][key].replace(/\{(\w+)\}/g, (match, name: string) => String(params[name] ?? match));
16
+
17
+ /** Tavora's theme as CSS variables (--background, --foreground, --primary, ...), so the module looks like Tavora. */
18
+ function applyTheme(theme: TavoraTheme) {
19
+ const root = document.documentElement;
20
+ for (const [name, value] of Object.entries(theme.tokens)) root.style.setProperty(`--${name}`, value);
21
+ root.style.setProperty("--radius", `${theme.radius}px`);
22
+ root.style.colorScheme = theme.mode;
23
+ }
24
+
25
+ function App() {
26
+ const [tavora, setTavora] = useState<TavoraSDK | null>(null);
27
+ const [language, setLanguage] = useState<Language>("en");
28
+ const [game, setGame] = useState("");
29
+ const [count, setCount] = useState(0);
30
+ const [failed, setFailed] = useState<string | null>(null);
31
+
32
+ useEffect(() => {
33
+ const pick = (code: string): Language => (code.startsWith("de") ? "de" : "en");
34
+ connect().then(
35
+ async (sdk) => {
36
+ applyTheme(await sdk.ui.getTheme());
37
+ sdk.ui.onThemeChange(applyTheme);
38
+ setLanguage(pick(await sdk.ui.getLanguage()));
39
+ sdk.ui.onLanguageChange((code) => setLanguage(pick(code)));
40
+ setGame((await sdk.game.getCurrent()).displayName);
41
+ // storage keeps small settings for this module in the current game.
42
+ setCount((await sdk.storage.get<number>("count")) ?? 0);
43
+ setTavora(sdk);
44
+ },
45
+ (error: unknown) => setFailed(`Not connected to Tavora: ${error instanceof Error ? error.message : String(error)}`),
46
+ );
47
+ }, []);
48
+
49
+ const add = () => {
50
+ const next = count + 1;
51
+ setCount(next);
52
+ void tavora?.storage.set("count", next);
53
+ };
54
+
55
+ const status = failed ?? (tavora ? `${text(language, "playing", { game })} ${text(language, "clicked", { count })}` : text(language, "connecting"));
56
+ return (
57
+ <main>
58
+ <h1>{document.title}</h1>
59
+ <p id="status">{status}</p>
60
+ <button type="button" disabled={!tavora} onClick={add}>
61
+ +1
62
+ </button>
63
+ </main>
64
+ );
65
+ }
66
+
67
+ createRoot(document.getElementById("app")!).render(<App />);
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "isolatedModules": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src", "vite.config.ts"]
14
+ }
@@ -0,0 +1,15 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vite";
3
+
4
+ // A module ships as one self-contained folder: relative paths, nothing loaded from the internet.
5
+ export default defineConfig({
6
+ root: "src",
7
+ base: "./",
8
+ plugins: [react()],
9
+ build: {
10
+ outDir: "../dist",
11
+ emptyOutDir: true,
12
+ target: "chrome130",
13
+ modulePreload: { polyfill: false },
14
+ },
15
+ });
@@ -0,0 +1,36 @@
1
+ # {{name}}
2
+
3
+ A [Tavora](https://mytavora.app) module, made with `@tavora/create-module`. Tavora shows it in a panel next to a game; it talks to Tavora through [`@tavora/sdk`](https://docs.mytavora.app/api/).
4
+
5
+ ## Develop
6
+
7
+ ```sh
8
+ {{run}} install
9
+ {{run}} dev
10
+ ```
11
+
12
+ `dev` builds the module into `dist/` and builds it again on every change. In Tavora, open **Settings > Modules > Add a development folder** and choose this folder. Add the module to a game with **Add module**; its panels reload by themselves after every build.
13
+
14
+ - `tavora.module.json` is the manifest: its id, name, the panel's smallest size, and what it asks the player for (websites to reach, connectors). See the [manifest reference](https://docs.mytavora.app/reference/manifest).
15
+ - `minTavora` is the oldest Tavora the module works with. Raise it when you use something newer from the SDK (the [changelog](https://docs.mytavora.app/reference/changelog) says which Tavora added what).
16
+
17
+ ## Share
18
+
19
+ ```sh
20
+ {{run}} build
21
+ {{run}} zip
22
+ ```
23
+
24
+ `zip` packs the built module into `{{id}}-0.1.0.zip`, signed with your publisher key. The first run makes the key in `~/.tavora/publisher-key.pem`. **Back it up and never share it**: the data your module saves for players belongs to that key, so a new key means a new publisher. Players install the zip with **Settings > Modules > Install from zip**; a newer zip signed with the same key updates it.
25
+
26
+ ## Show your name instead of a fingerprint
27
+
28
+ In Tavora, sign in under **Settings > Account**, choose a developer name under **Module developer** and press **Link a publisher key**. Then run the command it shows here, for example:
29
+
30
+ ```sh
31
+ {{exec}} tavora-pack link ABCD-EFGH
32
+ ```
33
+
34
+ Players then see your modules as "by" your name.
35
+
36
+ More in the [developer docs](https://docs.mytavora.app/).
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ *.zip
4
+ *.pem
@@ -0,0 +1,17 @@
1
+ /* Tavora gives the page its fonts and the look of buttons and fields. The colours come from
2
+ Tavora's theme as CSS variables (main.ts sets them), so the module matches every theme. */
3
+ body {
4
+ margin: 0;
5
+ background: var(--background);
6
+ color: var(--foreground);
7
+ }
8
+ main {
9
+ padding: 16px;
10
+ }
11
+ h1 {
12
+ margin: 0 0 8px;
13
+ font-size: 16px;
14
+ }
15
+ #status {
16
+ color: var(--muted-foreground);
17
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://docs.mytavora.app/schema/tavora.module.schema.json",
3
+ "id": "{{id}}",
4
+ "name": "{{name}}",
5
+ "description": "{{description}}",
6
+ "version": "0.1.0",
7
+ "apiVersion": "1",
8
+ "minTavora": "{{minTavora}}",
9
+ "entry": "dist/index.html",
10
+ "games": [],
11
+ "viewport": { "minWidth": 240, "minHeight": 160 },
12
+ "permissions": { "network": [] }
13
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "{{package}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite build --watch",
8
+ "build": "vite build",
9
+ "zip": "tavora-pack ."
10
+ },
11
+ "dependencies": {
12
+ "@tavora/sdk": "^{{sdkVersion}}"
13
+ },
14
+ "devDependencies": {
15
+ "@tavora/pack": "^{{packVersion}}",
16
+ "typescript": "^5.9.3",
17
+ "vite": "^8.3.4"
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>{{name}}</title>
6
+ <link rel="stylesheet" href="./style.css" />
7
+ <script type="module" src="./main.ts"></script>
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>{{name}}</h1>
12
+ <p id="status">Connecting to Tavora…</p>
13
+ <button type="button" id="counter" disabled>+1</button>
14
+ </main>
15
+ </body>
16
+ </html>
@@ -0,0 +1,54 @@
1
+ import { connect, type TavoraSDK, type TavoraTheme } from "@tavora/sdk";
2
+
3
+ // This page runs inside a Tavora panel; in a normal browser connect() fails. Build it and add this
4
+ // folder in Tavora (Settings > Modules > Add a development folder) to see it.
5
+
6
+ const status = document.querySelector<HTMLParagraphElement>("#status")!;
7
+ const counter = document.querySelector<HTMLButtonElement>("#counter")!;
8
+
9
+ /** Your texts in English and German; Tavora says which language the player uses. */
10
+ const TEXTS = {
11
+ en: { playing: "Playing {game}.", clicked: "Clicked {count} times in this game." },
12
+ de: { playing: "Du spielst {game}.", clicked: "{count}-mal in diesem Spiel geklickt." },
13
+ };
14
+ let language: keyof typeof TEXTS = "en";
15
+ const text = (key: keyof (typeof TEXTS)["en"], params: Record<string, string | number>) =>
16
+ TEXTS[language][key].replace(/\{(\w+)\}/g, (match, name: string) => String(params[name] ?? match));
17
+
18
+ /** Tavora's theme as CSS variables (--background, --foreground, --primary, ...), so the module looks like Tavora. */
19
+ function applyTheme(theme: TavoraTheme) {
20
+ const root = document.documentElement;
21
+ for (const [name, value] of Object.entries(theme.tokens)) root.style.setProperty(`--${name}`, value);
22
+ root.style.setProperty("--radius", `${theme.radius}px`);
23
+ root.style.colorScheme = theme.mode;
24
+ }
25
+
26
+ async function start(tavora: TavoraSDK) {
27
+ applyTheme(await tavora.ui.getTheme());
28
+ tavora.ui.onThemeChange(applyTheme);
29
+ const setLanguage = (code: string) => (language = code.startsWith("de") ? "de" : "en");
30
+ setLanguage(await tavora.ui.getLanguage());
31
+
32
+ const game = await tavora.game.getCurrent();
33
+ // storage keeps small settings for this module in the current game.
34
+ let count = (await tavora.storage.get<number>("count")) ?? 0;
35
+ const render = () => {
36
+ status.textContent = `${text("playing", { game: game.displayName })} ${text("clicked", { count })}`;
37
+ };
38
+ tavora.ui.onLanguageChange((code) => {
39
+ setLanguage(code);
40
+ render();
41
+ });
42
+ render();
43
+
44
+ counter.disabled = false;
45
+ counter.addEventListener("click", async () => {
46
+ count += 1;
47
+ render();
48
+ await tavora.storage.set("count", count);
49
+ });
50
+ }
51
+
52
+ connect().then(start, (error: unknown) => {
53
+ status.textContent = `Not connected to Tavora: ${error instanceof Error ? error.message : String(error)}`;
54
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "isolatedModules": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src", "vite.config.ts"]
13
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ // A module ships as one self-contained folder: relative paths, nothing loaded from the internet.
4
+ export default defineConfig({
5
+ root: "src",
6
+ base: "./",
7
+ build: {
8
+ outDir: "../dist",
9
+ emptyOutDir: true,
10
+ target: "chrome130",
11
+ modulePreload: { polyfill: false },
12
+ },
13
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "{{package}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite build --watch",
8
+ "build": "vite build",
9
+ "zip": "tavora-pack ."
10
+ },
11
+ "dependencies": {
12
+ "@tavora/sdk": "^{{sdkVersion}}",
13
+ "vue": "^3.5.43"
14
+ },
15
+ "devDependencies": {
16
+ "@tavora/pack": "^{{packVersion}}",
17
+ "@vitejs/plugin-vue": "^6.0.9",
18
+ "typescript": "^5.9.3",
19
+ "vite": "^8.3.4"
20
+ }
21
+ }
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ // This page runs inside a Tavora panel; in a normal browser connect() fails. Build it and add this
3
+ // folder in Tavora (Settings > Modules > Add a development folder) to see it.
4
+ import { connect, type TavoraSDK, type TavoraTheme } from "@tavora/sdk";
5
+ import { computed, onMounted, ref, shallowRef } from "vue";
6
+
7
+ /** Your texts in English and German; Tavora says which language the player uses. */
8
+ const TEXTS = {
9
+ en: { connecting: "Connecting to Tavora…", playing: "Playing {game}.", clicked: "Clicked {count} times in this game." },
10
+ de: { connecting: "Verbinde mit Tavora …", playing: "Du spielst {game}.", clicked: "{count}-mal in diesem Spiel geklickt." },
11
+ };
12
+ const language = ref<keyof typeof TEXTS>("en");
13
+ const text = (key: keyof (typeof TEXTS)["en"], params: Record<string, string | number> = {}) =>
14
+ TEXTS[language.value][key].replace(/\{(\w+)\}/g, (match, name: string) => String(params[name] ?? match));
15
+
16
+ const title = document.title;
17
+ const tavora = shallowRef<TavoraSDK | null>(null);
18
+ const game = ref("");
19
+ const count = ref(0);
20
+ const failed = ref<string | null>(null);
21
+ const status = computed(() => (failed.value ? failed.value : tavora.value ? `${text("playing", { game: game.value })} ${text("clicked", { count: count.value })}` : text("connecting")));
22
+
23
+ /** Tavora's theme as CSS variables (--background, --foreground, --primary, ...), so the module looks like Tavora. */
24
+ function applyTheme(theme: TavoraTheme) {
25
+ const root = document.documentElement;
26
+ for (const [name, value] of Object.entries(theme.tokens)) root.style.setProperty(`--${name}`, value);
27
+ root.style.setProperty("--radius", `${theme.radius}px`);
28
+ root.style.colorScheme = theme.mode;
29
+ }
30
+
31
+ onMounted(async () => {
32
+ try {
33
+ const sdk = await connect();
34
+ applyTheme(await sdk.ui.getTheme());
35
+ sdk.ui.onThemeChange(applyTheme);
36
+ const setLanguage = (code: string) => (language.value = code.startsWith("de") ? "de" : "en");
37
+ setLanguage(await sdk.ui.getLanguage());
38
+ sdk.ui.onLanguageChange(setLanguage);
39
+ game.value = (await sdk.game.getCurrent()).displayName;
40
+ // storage keeps small settings for this module in the current game.
41
+ count.value = (await sdk.storage.get<number>("count")) ?? 0;
42
+ tavora.value = sdk;
43
+ } catch (error) {
44
+ failed.value = `Not connected to Tavora: ${error instanceof Error ? error.message : String(error)}`;
45
+ }
46
+ });
47
+
48
+ async function add() {
49
+ count.value += 1;
50
+ await tavora.value?.storage.set("count", count.value);
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <main>
56
+ <h1>{{ title }}</h1>
57
+ <p id="status">{{ status }}</p>
58
+ <button type="button" :disabled="!tavora" @click="add">+1</button>
59
+ </main>
60
+ </template>
@@ -0,0 +1,5 @@
1
+ declare module "*.vue" {
2
+ import type { DefineComponent } from "vue";
3
+ const component: DefineComponent;
4
+ export default component;
5
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>{{name}}</title>
6
+ <link rel="stylesheet" href="./style.css" />
7
+ <script type="module" src="./main.ts"></script>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,4 @@
1
+ import { createApp } from "vue";
2
+ import App from "./App.vue";
3
+
4
+ createApp(App).mount("#app");
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "isolatedModules": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src", "vite.config.ts"]
14
+ }
@@ -0,0 +1,15 @@
1
+ import vue from "@vitejs/plugin-vue";
2
+ import { defineConfig } from "vite";
3
+
4
+ // A module ships as one self-contained folder: relative paths, nothing loaded from the internet.
5
+ export default defineConfig({
6
+ root: "src",
7
+ base: "./",
8
+ plugins: [vue()],
9
+ build: {
10
+ outDir: "../dist",
11
+ emptyOutDir: true,
12
+ target: "chrome130",
13
+ modulePreload: { polyfill: false },
14
+ },
15
+ });