@kaptive/create-widget 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 state systems gmbh
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
13
+ all 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 ADDED
@@ -0,0 +1,56 @@
1
+ # @kaptive/create-widget
2
+
3
+ Scaffolds a new [Kaptive](https://kaptive.ch) custom widget project.
4
+
5
+ A Kaptive custom widget is a small web app that a workspace admin uploads as a bundle and drops into any project. This scaffolds everything needed to build one, typed parameters included.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npm create @kaptive/widget
11
+ ```
12
+
13
+ or, with a directory and options up front:
14
+
15
+ ```bash
16
+ npm create @kaptive/widget my-widget -- --name "Weather" --id weather
17
+ ```
18
+
19
+ Prompts for a project directory, display name, stable widget id, description, and framework (React today), then writes a ready-to-run project. `-y`/`--yes` accepts the defaults without prompting, for a non-interactive run.
20
+
21
+ ### Options
22
+
23
+ | Flag | What it sets |
24
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `--name <name>` | Display name shown in the Kaptive editor. |
26
+ | `--id <id>` | Stable widget id: 3–64 lowercase letters, digits or dashes, starting with a letter or digit. Chosen once and never changed — re-uploading a bundle with the same id creates a new version of the same widget rather than a second one. |
27
+ | `--description <text>` | One-line description. |
28
+ | `--framework <id>` | Widget framework. Only `react` today. |
29
+ | `-y, --yes` | Accept the defaults without prompting. |
30
+
31
+ ## What you get
32
+
33
+ - A Vite + React project with `@kaptive/cli`'s plugin already wired up.
34
+ - `kaptive.manifest.json`, declaring the parameters your widget needs.
35
+ - `kaptive.dev.json`, standing in for a real player while you run `npm run dev`.
36
+ - A generated, always-up-to-date `src/kaptive-env.d.ts`, so `parameters.yourKey` is typed straight from the manifest.
37
+
38
+ ## Next steps
39
+
40
+ ```bash
41
+ cd my-widget
42
+ npm install
43
+ npm run dev
44
+ ```
45
+
46
+ Edit `kaptive.manifest.json` to declare parameters — `text`, `number`, `boolean`, `color`, and `asset` are the available types, each becoming a field in the widget's properties panel in the Kaptive editor. Edit `kaptive.dev.json` to try out values while you develop; there's no live editor to ask, so the dev server reads from that file and reloads when it changes.
47
+
48
+ When you're ready to ship:
49
+
50
+ ```bash
51
+ npm run build
52
+ ```
53
+
54
+ produces `<id>-<version>.zip`. Hand it to a workspace admin, who uploads it under **Settings → Widgets** — from then on, anyone in the workspace can drop it into a project. Bump `version` in the manifest before each upload; Kaptive rejects a bundle declaring a version it already has.
55
+
56
+ See the scaffolded project's own README for the full parameter reference and what `kaptive.ready()` gives your widget at runtime, or the [`@kaptive/widget-api`](https://www.npmjs.com/package/@kaptive/widget-api) and [`@kaptive/cli`](https://www.npmjs.com/package/@kaptive/cli) packages it depends on.
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { stdin, stdout } from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
7
+ import { WIDGET_MANIFEST_FILENAME, parseWidgetManifest, widgetIdSchema } from "@kaptive/widget-api/manifest";
8
+ import { createInterface } from "node:readline/promises";
9
+ //#region src/scaffold.ts
10
+ /**
11
+ * React is the only framework for now. The list is what the prompt renders, so
12
+ * adding another one is a matter of dropping in a template directory and an
13
+ * entry here.
14
+ */
15
+ var FRAMEWORKS = [{
16
+ id: "react",
17
+ label: "React",
18
+ template: "react"
19
+ }];
20
+ /**
21
+ * Template files whose name cannot ship as-is: npm strips `.gitignore` from
22
+ * published packages, and a leading dot invites tooling to treat the file as
23
+ * configuration for the CLI package itself.
24
+ */
25
+ var RENAMES = { _gitignore: ".gitignore" };
26
+ /** Derives a valid widget id from a display name. */
27
+ function slugifyWidgetId(input) {
28
+ return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64).replace(/-+$/, "");
29
+ }
30
+ function isValidWidgetId(id) {
31
+ return widgetIdSchema.safeParse(id).success;
32
+ }
33
+ /** Derives an npm-safe package name from a display name. */
34
+ function toPackageName(input) {
35
+ return slugifyWidgetId(input) || "kaptive-widget";
36
+ }
37
+ /**
38
+ * Substitutes `{{token}}` placeholders. Values going into a `.json` file are
39
+ * JSON-escaped, so a widget name containing a quote produces a valid manifest
40
+ * rather than a broken one.
41
+ */
42
+ function applyReplacements(content, values, asJson) {
43
+ const escape = (value) => asJson ? JSON.stringify(value).slice(1, -1) : value;
44
+ return content.replace(/\{\{(name|id|description|packageName)\}\}/g, (_, key) => escape(values[key]));
45
+ }
46
+ /** True when `directory` does not exist, or exists and holds nothing. */
47
+ async function isDirectoryEmpty(directory) {
48
+ try {
49
+ return (await readdir(directory)).length === 0;
50
+ } catch {
51
+ return true;
52
+ }
53
+ }
54
+ /** Copies a template into `targetDir`, substituting placeholders as it goes. */
55
+ async function scaffold(options) {
56
+ const written = [];
57
+ async function copyDirectory(from, to) {
58
+ await mkdir(to, { recursive: true });
59
+ for (const entry of await readdir(from, { withFileTypes: true })) {
60
+ const source = path.join(from, entry.name);
61
+ const name = RENAMES[entry.name] ?? entry.name;
62
+ const destination = path.join(to, name);
63
+ if (entry.isDirectory()) {
64
+ await copyDirectory(source, destination);
65
+ continue;
66
+ }
67
+ if (!entry.isFile()) continue;
68
+ const content = await readFile(source, "utf8");
69
+ await writeFile(destination, applyReplacements(content, options.values, name.endsWith(".json")), "utf8");
70
+ written.push(path.relative(options.targetDir, destination));
71
+ }
72
+ }
73
+ await copyDirectory(options.templateDir, options.targetDir);
74
+ const manifestFile = path.join(options.targetDir, WIDGET_MANIFEST_FILENAME);
75
+ const result = parseWidgetManifest(JSON.parse(await readFile(manifestFile, "utf8")));
76
+ if (!result.ok) throw new Error(`Generated ${WIDGET_MANIFEST_FILENAME} is invalid:\n ${result.errors.join("\n ")}`);
77
+ return written.sort();
78
+ }
79
+ //#endregion
80
+ //#region src/prompts.ts
81
+ function createPrompter(interactive) {
82
+ if (!interactive) return {
83
+ ask: (_q, fallback) => Promise.resolve(fallback),
84
+ close: () => {}
85
+ };
86
+ const rl = createInterface({
87
+ input: stdin,
88
+ output: stdout
89
+ });
90
+ return {
91
+ async ask(question, fallback) {
92
+ return (await rl.question(`${question} (${fallback}) `)).trim() || fallback;
93
+ },
94
+ close: () => {
95
+ rl.close();
96
+ }
97
+ };
98
+ }
99
+ async function pickFramework(prompter, interactive, requested) {
100
+ if (requested) {
101
+ const match = FRAMEWORKS.find((framework) => framework.id === requested);
102
+ if (!match) throw new Error(`Unknown framework "${requested}". Available: ${FRAMEWORKS.map((f) => f.id).join(", ")}`);
103
+ return match;
104
+ }
105
+ const first = FRAMEWORKS[0];
106
+ if (!first) throw new Error("No widget templates are available");
107
+ if (FRAMEWORKS.length === 1 || !interactive) return first;
108
+ const options = FRAMEWORKS.map((framework, index) => ` ${index + 1}) ${framework.label}`).join("\n");
109
+ console.log(`\nFramework:\n${options}`);
110
+ for (;;) {
111
+ const answer = await prompter.ask("Choose a framework", first.id);
112
+ const match = FRAMEWORKS[Number(answer) - 1] ?? FRAMEWORKS.find((framework) => framework.id === answer.trim().toLowerCase());
113
+ if (match) return match;
114
+ console.log(` "${answer}" is not one of the options.`);
115
+ }
116
+ }
117
+ /**
118
+ * A non-interactive prompter always echoes the same fallback back, so a
119
+ * validating retry loop over it would spin forever on an invalid fallback —
120
+ * most commonly a suggested id slugified down to under 3 characters from a
121
+ * short `--name`. Non-interactive mode therefore gets exactly one try.
122
+ */
123
+ async function askWidgetId(prompter, interactive, provided, suggestion) {
124
+ if (provided) {
125
+ if (!isValidWidgetId(provided)) throw new Error(`"${provided}" is not a valid widget id: use 3-64 lowercase letters, digits or dashes, starting with a letter or digit.`);
126
+ return provided;
127
+ }
128
+ if (!interactive) {
129
+ if (isValidWidgetId(suggestion)) return suggestion;
130
+ throw new Error(`Could not derive a valid widget id from "${suggestion}" — pass --id explicitly (3-64 lowercase letters, digits or dashes, starting with a letter or digit).`);
131
+ }
132
+ for (;;) {
133
+ const answer = await prompter.ask("Widget id", suggestion);
134
+ if (isValidWidgetId(answer)) return answer;
135
+ console.log(" Use 3-64 lowercase letters, digits or dashes, starting with a letter or digit.");
136
+ }
137
+ }
138
+ //#endregion
139
+ //#region src/index.ts
140
+ var DEFAULT_DIRECTORY = "my-kaptive-widget";
141
+ /** Turns a directory name into a plausible display name. */
142
+ function titleFromDirectory(directory) {
143
+ const words = path.basename(path.resolve(directory)).replace(/[-_]+/g, " ").trim();
144
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : "My widget";
145
+ }
146
+ async function run(directory, opts) {
147
+ const interactive = Boolean(stdin.isTTY) && !opts.yes;
148
+ const prompter = createPrompter(interactive);
149
+ try {
150
+ console.log("\nCreating a Kaptive widget.\n");
151
+ const targetDirectory = directory ?? await prompter.ask("Project directory", DEFAULT_DIRECTORY);
152
+ const targetDir = path.resolve(targetDirectory);
153
+ if (!await isDirectoryEmpty(targetDir)) throw new Error(`${targetDir} already exists and is not empty.`);
154
+ const name = opts.name ?? await prompter.ask("Widget name", titleFromDirectory(targetDirectory));
155
+ const id = await askWidgetId(prompter, interactive, opts.id, slugifyWidgetId(name) || slugifyWidgetId(path.basename(targetDir)));
156
+ const description = opts.description ?? await prompter.ask("Description", `${name} widget for Kaptive`);
157
+ const framework = await pickFramework(prompter, interactive, opts.framework);
158
+ const written = await scaffold({
159
+ targetDir,
160
+ templateDir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "templates", framework.template),
161
+ values: {
162
+ name,
163
+ id,
164
+ description,
165
+ packageName: toPackageName(name)
166
+ }
167
+ });
168
+ const relative = path.relative(process.cwd(), targetDir) || ".";
169
+ console.log(`\nCreated ${written.length} files in ${relative}\n\nNext steps:\n cd ${relative}\n npm install\n npm run dev\n\nEdit kaptive.manifest.json to declare the parameters your widget needs,\nand kaptive.dev.json to set their values while you develop.\n\nWhen you are ready, "npm run build" produces ${id}-0.1.0.zip for a\nworkspace admin to upload under Settings → Widgets.\n`);
170
+ } finally {
171
+ prompter.close();
172
+ }
173
+ }
174
+ var program = new Command("create-widget").description("Scaffold a Kaptive custom widget").argument("[directory]", "Directory to create the widget in").option("--name <name>", "Display name shown in the Kaptive editor").option("--id <id>", "Stable widget id (lowercase letters, digits, dashes)").option("--description <text>", "One-line description").option("--framework <id>", `Widget framework: ${FRAMEWORKS.map((f) => f.id).join(", ")}`).option("-y, --yes", "Accept the defaults without prompting").action(run);
175
+ try {
176
+ await program.parseAsync(process.argv);
177
+ } catch (error) {
178
+ console.error(`\n${error instanceof Error ? error.message : String(error)}\n`);
179
+ process.exitCode = 1;
180
+ }
181
+ //#endregion
182
+ export {};
@@ -0,0 +1,14 @@
1
+ import { type Framework } from "./scaffold.js";
2
+ export interface Prompter {
3
+ ask: (question: string, fallback: string) => Promise<string>;
4
+ close: () => void;
5
+ }
6
+ export declare function createPrompter(interactive: boolean): Prompter;
7
+ export declare function pickFramework(prompter: Prompter, interactive: boolean, requested: string | undefined): Promise<Framework>;
8
+ /**
9
+ * A non-interactive prompter always echoes the same fallback back, so a
10
+ * validating retry loop over it would spin forever on an invalid fallback —
11
+ * most commonly a suggested id slugified down to under 3 characters from a
12
+ * short `--name`. Non-interactive mode therefore gets exactly one try.
13
+ */
14
+ export declare function askWidgetId(prompter: Prompter, interactive: boolean, provided: string | undefined, suggestion: string): Promise<string>;
@@ -0,0 +1,38 @@
1
+ export interface Framework {
2
+ id: string;
3
+ label: string;
4
+ /** Directory under `templates/`. */
5
+ template: string;
6
+ }
7
+ /**
8
+ * React is the only framework for now. The list is what the prompt renders, so
9
+ * adding another one is a matter of dropping in a template directory and an
10
+ * entry here.
11
+ */
12
+ export declare const FRAMEWORKS: readonly Framework[];
13
+ export interface TemplateValues {
14
+ name: string;
15
+ id: string;
16
+ description: string;
17
+ packageName: string;
18
+ }
19
+ /** Derives a valid widget id from a display name. */
20
+ export declare function slugifyWidgetId(input: string): string;
21
+ export declare function isValidWidgetId(id: string): boolean;
22
+ /** Derives an npm-safe package name from a display name. */
23
+ export declare function toPackageName(input: string): string;
24
+ /**
25
+ * Substitutes `{{token}}` placeholders. Values going into a `.json` file are
26
+ * JSON-escaped, so a widget name containing a quote produces a valid manifest
27
+ * rather than a broken one.
28
+ */
29
+ export declare function applyReplacements(content: string, values: TemplateValues, asJson: boolean): string;
30
+ /** True when `directory` does not exist, or exists and holds nothing. */
31
+ export declare function isDirectoryEmpty(directory: string): Promise<boolean>;
32
+ export interface ScaffoldOptions {
33
+ targetDir: string;
34
+ templateDir: string;
35
+ values: TemplateValues;
36
+ }
37
+ /** Copies a template into `targetDir`, substituting placeholders as it goes. */
38
+ export declare function scaffold(options: ScaffoldOptions): Promise<string[]>;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@kaptive/create-widget",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a Kaptive custom widget.",
5
+ "keywords": [
6
+ "kaptive",
7
+ "widget",
8
+ "scaffold",
9
+ "create",
10
+ "digital-signage"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "homepage": "https://kaptive.ch",
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "templates"
24
+ ],
25
+ "bin": {
26
+ "create-kaptive-widget": "./dist/index.js",
27
+ "create-widget": "./dist/index.js"
28
+ },
29
+ "dependencies": {
30
+ "commander": "^14.0.0",
31
+ "@kaptive/widget-api": "^0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^24.0.0",
35
+ "eslint": "^10.0.0",
36
+ "prettier": "^3.8.1",
37
+ "typescript": "6.0.3",
38
+ "vite": "^8.0.0",
39
+ "vitest": "^4.0.0",
40
+ "@repo/eslint-config": "0.0.0",
41
+ "@repo/typescript-config": "0.0.0"
42
+ },
43
+ "scripts": {
44
+ "build": "vite build && tsc -p tsconfig.build.json",
45
+ "lint": "eslint . --max-warnings 0 --cache --cache-location node_modules/.cache/eslint/",
46
+ "format": "prettier --write .",
47
+ "check-format": "prettier --check .",
48
+ "check-types": "tsc --noEmit",
49
+ "test": "vitest run"
50
+ }
51
+ }
@@ -0,0 +1,66 @@
1
+ # {{name}}
2
+
3
+ A custom widget for [Kaptive](https://kaptive.app).
4
+
5
+ ## Get started
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ ## Parameters
13
+
14
+ Declare whatever your widget needs in `kaptive.manifest.json`:
15
+
16
+ ```json
17
+ { "key": "title", "type": "text", "label": "Title", "default": "Hello" }
18
+ ```
19
+
20
+ Types are `text`, `number`, `boolean`, `color` and `asset`. Each one turns into
21
+ a field in the widget's properties panel in the Kaptive editor, and it is typed
22
+ in your code — `parameters.title` is a `string`, `parameters.count` is a
23
+ `number`. The types are generated from the manifest, so you never write them
24
+ yourself.
25
+
26
+ A `color` arrives as a hex string. An `asset` lets someone pick a file — from
27
+ the workspace media library, or one sitting on the player's own storage — and
28
+ arrives as a URL you can drop into an `<img>`, or `null` if nothing is picked
29
+ yet. `accept` narrows which library categories are offered (`image`, `video`,
30
+ `pdf`, `model`):
31
+
32
+ ```json
33
+ { "key": "logo", "type": "asset", "label": "Logo", "accept": ["image"] }
34
+ ```
35
+
36
+ There is no editor while you develop, so values come from `kaptive.dev.json`.
37
+ Edit that file and the page reloads. Point an asset parameter at any URL there
38
+ — a file in `public/`, or a remote one — or simulate a file on a real player's
39
+ own storage with `{ "source": "local", "localFilePath": "..." }`.
40
+
41
+ ## Ship it
42
+
43
+ ```bash
44
+ npm run build
45
+ ```
46
+
47
+ You get `{{id}}-<version>.zip`. Hand it to a workspace admin, who uploads it
48
+ under **Settings → Widgets** — from then on anyone can drop it into a project.
49
+
50
+ Bump `version` in the manifest before each upload; Kaptive rejects a bundle
51
+ declaring a version it already has. Uploading replaces the widget on every
52
+ screen using it, within seconds.
53
+
54
+ ## Good to know
55
+
56
+ - Your widget is transparent and gets resized freely in the editor. Looking
57
+ right at any size is up to you.
58
+ - `kaptive.ready()` also gives you `player` (device name, orientation, time
59
+ zone, resolution) and `block` (your size in pixels).
60
+ - `fetch` and `localStorage` work as usual. Your widget runs on its own origin,
61
+ so anything you store stays yours and survives updates and reboots.
62
+ - Parameters are read once at start-up. If someone changes one, the widget
63
+ restarts with the new values.
64
+ - Your widget also works deployed outside Kaptive entirely. Add a
65
+ `kaptive.prod.json` (same shape as `kaptive.dev.json`) to bake in real
66
+ values for that case; without one, it falls back to the manifest's defaults.
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+
4
+ # Generated from kaptive.manifest.json on every dev/build/check-types run.
5
+ src/kaptive-env.d.ts
6
+
7
+ # Packaged bundles
8
+ *.zip
@@ -0,0 +1,12 @@
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.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,12 @@
1
+ {
2
+ "parameters": {
3
+ "title": "{{name}}",
4
+ "count": 42
5
+ },
6
+ "player": {
7
+ "deviceName": "My Development screen",
8
+ "timezone": "Europe/Zurich",
9
+ "orientation": 0,
10
+ "resolution": { "width": 1920, "height": 1080 }
11
+ }
12
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "{{id}}",
3
+ "name": "{{name}}",
4
+ "version": "0.1.0",
5
+ "apiVersion": 1,
6
+ "description": "{{description}}",
7
+ "entry": "index.html",
8
+ "parameters": [
9
+ {
10
+ "key": "title",
11
+ "type": "text",
12
+ "label": "Title",
13
+ "default": "{{name}}",
14
+ "maxLength": 60
15
+ },
16
+ {
17
+ "key": "count",
18
+ "type": "number",
19
+ "label": "Count",
20
+ "default": 0
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "kaptive widget dev",
8
+ "build": "kaptive widget build",
9
+ "validate": "kaptive widget validate",
10
+ "check-types": "kaptive widget types && tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@kaptive/widget-api": "^0.1.0",
14
+ "react": "^19.2.4",
15
+ "react-dom": "^19.2.4"
16
+ },
17
+ "devDependencies": {
18
+ "@kaptive/cli": "^0.1.0",
19
+ "@types/react": "^19.2.14",
20
+ "@types/react-dom": "^19.2.3",
21
+ "@vitejs/plugin-react": "^6.0.1",
22
+ "typescript": "^6.0.3",
23
+ "vite": "^8.0.1"
24
+ }
25
+ }
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from "react";
2
+ import { kaptive, type KaptiveParameters } from "@kaptive/widget-api";
3
+
4
+ export function App() {
5
+ const [parameters, setParameters] = useState<KaptiveParameters | null>(null);
6
+ const [error, setError] = useState<string | null>(null);
7
+
8
+ useEffect(() => {
9
+ let active = true;
10
+
11
+ // The values someone set in the Kaptive editor, typed from your manifest.
12
+ kaptive
13
+ .ready()
14
+ .then((context) => {
15
+ if (active) setParameters(context.parameters);
16
+ })
17
+ .catch((cause: unknown) => {
18
+ if (active) {
19
+ setError(cause instanceof Error ? cause.message : String(cause));
20
+ }
21
+ });
22
+
23
+ return () => {
24
+ active = false;
25
+ };
26
+ }, []);
27
+
28
+ if (error) return <main className="widget message">{error}</main>;
29
+ if (!parameters) return <main className="widget message">Loading…</main>;
30
+
31
+ return (
32
+ <main className="widget">
33
+ <h1>{parameters.title}</h1>
34
+ <p>{parameters.count}</p>
35
+ </main>
36
+ );
37
+ }
@@ -0,0 +1,43 @@
1
+ /* No background, so the widget sits on the project's own. Add one if you want. */
2
+ html,
3
+ body,
4
+ #root {
5
+ margin: 0;
6
+ height: 100%;
7
+ overflow: hidden;
8
+ background: transparent;
9
+ }
10
+
11
+ .widget {
12
+ box-sizing: border-box;
13
+ height: 100%;
14
+ display: flex;
15
+ flex-direction: column;
16
+ justify-content: center;
17
+ gap: 0.2em;
18
+
19
+ font-family:
20
+ system-ui,
21
+ -apple-system,
22
+ "Segoe UI",
23
+ sans-serif;
24
+ color: #111827;
25
+ }
26
+
27
+ /* Viewport units, so the text reads well at whatever size the block is. */
28
+ .widget h1 {
29
+ margin: 0;
30
+ font-size: clamp(1rem, 9vh, 6rem);
31
+ line-height: 1.1;
32
+ }
33
+
34
+ .widget p {
35
+ margin: 0;
36
+ font-size: clamp(0.875rem, 5vh, 3rem);
37
+ opacity: 0.6;
38
+ }
39
+
40
+ .widget.message {
41
+ font-size: 0.875rem;
42
+ opacity: 0.7;
43
+ }
@@ -0,0 +1,13 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./app.css";
5
+
6
+ const container = document.getElementById("root");
7
+ if (!container) throw new Error("Missing #root element in index.html");
8
+
9
+ createRoot(container).render(
10
+ <StrictMode>
11
+ <App />
12
+ </StrictMode>,
13
+ );
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "strict": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "isolatedModules": true,
14
+ "resolveJsonModule": true,
15
+ "skipLibCheck": true,
16
+ "noEmit": true,
17
+ "types": ["vite/client"]
18
+ },
19
+ "include": ["src", "vite.config.ts"]
20
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { kaptiveWidget } from "@kaptive/cli";
4
+
5
+ export default defineConfig({
6
+ // Keeps asset paths relative — each widget is served from its own origin root.
7
+ base: "./",
8
+ plugins: [react(), kaptiveWidget()],
9
+ });