create-deck-plugin 1.4.0 → 1.5.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/id.mjs ADDED
@@ -0,0 +1,8 @@
1
+ // A plugin's id from the name someone typed. The id is a config key and a path component in Deck,
2
+ // so it is letters, digits and dashes, starting with a letter or digit; "" when nothing usable is
3
+ // left. Deck's "Create a plugin" makes the same id (src/core/pluginid.ts), and
4
+ // src/core/pluginid.check.mjs holds the two to each other.
5
+ export const pluginId = (raw) => {
6
+ const id = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
7
+ return /^[a-z0-9]/.test(id) ? id : "";
8
+ };
package/index.mjs CHANGED
@@ -1,102 +1,110 @@
1
- #!/usr/bin/env node
2
- // Scaffold a Deck plugin.
3
- //
4
- // npm create deck-plugin my-thing
5
- //
6
- // Writes a folder that already builds, already has a git repository, and already knows how to
7
- // install itself into Deck. The real work is in deck-plugin-sdk, which this installs — a
8
- // scaffold on its own would freeze the shim and the types at the moment of creation, which is
9
- // the thing that rots.
10
- import { execFileSync } from "node:child_process";
11
- import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
- import { dirname, join, resolve } from "node:path";
13
- import { fileURLToPath } from "node:url";
14
-
15
- const here = dirname(fileURLToPath(import.meta.url));
16
-
17
- const raw = process.argv[2];
18
- if (!raw) {
19
- console.error("usage: npm create deck-plugin <name>");
20
- process.exit(1);
21
- }
22
-
23
- // The id is a config key and a path component in Deck, so it is checked here rather than at
24
- // install time. Same rule the loader applies.
25
- const id = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
26
- if (!id || !/^[a-z0-9]/.test(id)) {
27
- console.error(`"${raw}" does not reduce to a usable id (letters, digits and dashes)`);
28
- process.exit(1);
29
- }
30
-
31
- const dir = resolve(process.cwd(), id);
32
- if (existsSync(dir) && readdirSync(dir).length) {
33
- console.error(`${dir} already exists and is not empty`);
34
- process.exit(1);
35
- }
36
-
37
- const name = id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
38
- const component = name.replace(/[^A-Za-z0-9]/g, "") || "Plugin";
39
-
40
- // The template is the SDK's — one copy, so the scaffold and the thing it installs cannot
41
- // disagree. Resolved through node rather than by relative path, because once published these
42
- // are two packages rather than two folders.
43
- const sdkTemplate = existsSync(join(here, "template"))
44
- ? join(here, "template") // published: copied in at pack time
45
- : join(here, "..", "sdk", "template"); // in the repo: the SDK's own
46
- cpSync(sdkTemplate, dir, { recursive: true });
47
- // npm refuses to publish a file called .gitignore, so the template ships it renamed.
48
- if (existsSync(join(dir, "gitignore"))) renameSync(join(dir, "gitignore"), join(dir, ".gitignore"));
49
-
50
- for (const rel of ["plugin.json", "package.json", "src/plugin.tsx", "README.md"]) {
51
- const p = join(dir, rel);
52
- if (!existsSync(p)) continue;
53
- writeFileSync(p, readFileSync(p, "utf8")
54
- .replaceAll("__ID__", id)
55
- .replaceAll("__NAME__", name)
56
- .replaceAll("__COMPONENT__", component));
57
- }
58
-
59
- /**
60
- * Run a command in the new plugin's directory.
61
- *
62
- * npm needs care on Windows. `npm` there is npm.cmd, and recent Node refuses to execFile a .cmd
63
- * at all — EINVAL, a deliberate fix for an argument-injection class of bug — so the scaffolder
64
- * silently installed nothing for every Windows user, which is every Deck user. Running it
65
- * through a shell works but is itself deprecated for the same reason, so npm's own JS entry
66
- * point is located and run with node instead: no shell, no .cmd, nothing to escape.
67
- */
68
- function npmCli() {
69
- // npm sits beside node in the same installation, as lib/node_modules/npm/bin/npm-cli.js.
70
- const guess = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
71
- if (existsSync(guess)) return guess;
72
- const unix = join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
73
- return existsSync(unix) ? unix : null;
74
- }
75
-
76
- const run = (cmd, args) => {
77
- try {
78
- if (cmd === "npm") {
79
- const cli = npmCli();
80
- if (!cli) return false;
81
- execFileSync(process.execPath, [cli, ...args], { cwd: dir, stdio: "inherit" });
82
- } else {
83
- execFileSync(cmd, args, { cwd: dir, stdio: "inherit" });
84
- }
85
- return true;
86
- } catch { return false; }
87
- };
88
-
89
- console.log(`\nInstalling deck-plugin-sdk…`);
90
- const npm = "npm";
91
- const installed = run(npm, ["install", "--save-dev", "deck-plugin-sdk",
92
- "react", "lucide-react", "@types/react"]);
93
-
94
- // A git repository from the start, because a plugin's home is its own repository — that is how
95
- // it gets published, and how it gets listed in the registry.
96
- run("git", ["init", "-q", "-b", "main"]);
97
-
98
- console.log(`\n${name} is in ${id}/\n`);
99
- console.log(" cd " + id);
100
- if (!installed) console.log(" npm install # this failed; run it yourself");
101
- console.log(" npm run dev # build, and rebuild as you edit");
102
- console.log(" npm run install-local # copy it into Deck and load it\n");
1
+ #!/usr/bin/env node
2
+ // Scaffold a Deck plugin.
3
+ //
4
+ // npm create deck-plugin my-thing
5
+ //
6
+ // Writes a folder that already builds, already has a git repository, and already knows how to
7
+ // install itself into Deck. The real work is in deck-plugin-sdk, which this installs — a
8
+ // scaffold on its own would freeze the shim and the types at the moment of creation, which is
9
+ // the thing that rots.
10
+ import { execFileSync } from "node:child_process";
11
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { pluginId } from "./id.mjs";
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+
18
+ const raw = process.argv[2];
19
+ if (!raw) {
20
+ console.error("usage: npm create deck-plugin <name>");
21
+ process.exit(1);
22
+ }
23
+
24
+ // The id is a config key and a path component in Deck, so it is checked here rather than at
25
+ // install time. Same rule the loader applies.
26
+ const id = pluginId(raw);
27
+ if (!id) {
28
+ console.error(`"${raw}" does not reduce to a usable id (letters, digits and dashes)`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const dir = resolve(process.cwd(), id);
33
+ if (existsSync(dir) && readdirSync(dir).length) {
34
+ console.error(`${dir} already exists and is not empty`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const name = id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
39
+ const component = name.replace(/[^A-Za-z0-9]/g, "") || "Plugin";
40
+
41
+ // The template is the SDK's — one copy, so the scaffold and the thing it installs cannot
42
+ // disagree. Resolved through node rather than by relative path, because once published these
43
+ // are two packages rather than two folders.
44
+ // The repo's own copy first: the one beside this file is what the last `npm publish` copied in,
45
+ // and is stale from the next change to sdk/template on.
46
+ const repoSdk = join(here, "..", "sdk");
47
+ const inRepo = existsSync(join(repoSdk, "template"));
48
+ const sdkTemplate = inRepo
49
+ ? join(repoSdk, "template") // in the repo: the SDK's own
50
+ : join(here, "template"); // published: copied in at pack time
51
+ cpSync(sdkTemplate, dir, { recursive: true });
52
+ // npm refuses to publish a file called .gitignore, so the template ships it renamed.
53
+ if (existsSync(join(dir, "gitignore"))) renameSync(join(dir, "gitignore"), join(dir, ".gitignore"));
54
+
55
+ for (const rel of ["plugin.json", "package.json", "src/plugin.tsx", "README.md", "AGENTS.md"]) {
56
+ const p = join(dir, rel);
57
+ if (!existsSync(p)) continue;
58
+ writeFileSync(p, readFileSync(p, "utf8")
59
+ .replaceAll("__ID__", id)
60
+ .replaceAll("__NAME__", name)
61
+ .replaceAll("__COMPONENT__", component));
62
+ }
63
+
64
+ /**
65
+ * Run a command in the new plugin's directory.
66
+ *
67
+ * npm needs care on Windows. `npm` there is npm.cmd, and recent Node refuses to execFile a .cmd
68
+ * at all — EINVAL, a deliberate fix for an argument-injection class of bug — so the scaffolder
69
+ * silently installed nothing for every Windows user, which is every Deck user. Running it
70
+ * through a shell works but is itself deprecated for the same reason, so npm's own JS entry
71
+ * point is located and run with node instead: no shell, no .cmd, nothing to escape.
72
+ */
73
+ function npmCli() {
74
+ // npm sits beside node in the same installation, as lib/node_modules/npm/bin/npm-cli.js.
75
+ const guess = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
76
+ if (existsSync(guess)) return guess;
77
+ const unix = join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
78
+ return existsSync(unix) ? unix : null;
79
+ }
80
+
81
+ const run = (cmd, args) => {
82
+ try {
83
+ if (cmd === "npm") {
84
+ const cli = npmCli();
85
+ if (!cli) return false;
86
+ execFileSync(process.execPath, [cli, ...args], { cwd: dir, stdio: "inherit" });
87
+ } else {
88
+ execFileSync(cmd, args, { cwd: dir, stdio: "inherit" });
89
+ }
90
+ return true;
91
+ } catch { return false; }
92
+ };
93
+
94
+ console.log(`\nInstalling deck-plugin-sdk…`);
95
+ const npm = "npm";
96
+ // Run from the repo, the SDK beside it: the template is the repo's, and the published SDK may not
97
+ // have caught up with it yet.
98
+ const installed = run(npm, ["install", "--save-dev", inRepo ? repoSdk : "deck-plugin-sdk",
99
+ "react", "lucide-react", "@types/react"]);
100
+
101
+ // A git repository from the start, because a plugin's home is its own repository — that is how
102
+ // it gets published, and how it gets listed in the registry.
103
+ run("git", ["init", "-q", "-b", "main"]);
104
+
105
+ console.log(`\n${name} is in ${id}/\n`);
106
+ console.log(" cd " + id);
107
+ if (!installed) console.log(" npm install # this failed; run it yourself");
108
+ console.log(" npm run dev # rebuild as you edit; each save shows in your test Deck");
109
+ console.log(" npm run install-local # when it is done: install it into your real Deck");
110
+ console.log("\nStart a test Deck from Deck's Settings › Plugins › Open a test Deck.\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-deck-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Scaffold a Deck plugin.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "index.mjs",
12
+ "id.mjs",
12
13
  "template"
13
14
  ],
14
15
  "engines": {
@@ -0,0 +1,77 @@
1
+ # __NAME__: notes for AI agents
2
+
3
+ This is a plugin for Deck, a Windows app (Tauri, React 19). It is
4
+ bundled into one `plugin.js` that Deck imports at run time, inside Deck's own window. Read this
5
+ before changing anything.
6
+
7
+ ## Commands
8
+
9
+ ```sh
10
+ npm run dev # build on every save and install into the test Deck, which reloads it
11
+ npm run build # build once into plugin.js
12
+ npm run install-local # build and install into the user's real Deck
13
+ npx deck-plugin check # validate plugin.json
14
+ ```
15
+
16
+ The test Deck is a second Deck with its own data, started with
17
+ `& "$env:LOCALAPPDATA\Deck\deck.exe" --test` (PowerShell). Develop against it, never the real
18
+ Deck: `npm run dev` only ever writes there. Run `npm run install-local` only when the user asks
19
+ for the plugin to be installed for real. Its log is `%APPDATA%\Deck-Dev\logs\deck-<date>.log`;
20
+ the plugin's `console.warn` and `console.error` lines land there.
21
+
22
+ ## Files
23
+
24
+ - `plugin.json`: `id` (letters, digits, dashes; it is a folder name and a config key), `name`,
25
+ `description`, `icon` (a lucide icon name), `color`, `version`, `minDeck`.
26
+ - `src/plugin.tsx`: the entry. Its exports are what Deck uses (below).
27
+ - `src/mcp.ts`, if present: tools for Deck's MCP server, built to `mcp.js`.
28
+ - `plugin.js` (and `mcp.js`): the build's output, gitignored. Never edit them.
29
+ - **The API is `node_modules/deck-plugin-sdk/types/core/bridge.d.ts`**: every function Deck
30
+ exposes, with its documentation, generated from Deck's own source. Read it rather than guessing
31
+ a name. A plugin imports it as `deck-plugin-sdk/bridge`.
32
+
33
+ ## What the plugin can export
34
+
35
+ `default` (a tab), `PaneView` (a view that can sit in a split pane, given `{ cwd, runInTerminal }`),
36
+ `Status` (a footer readout), `Panel` (a footer drawer tab), `commands` (palette entries),
37
+ `Settings` (a settings panel), `keybindings`, `Provider` (wraps the app, for state that must
38
+ survive the tab closing), `Bar`, and `dispose`. At least one; none is required on its own.
39
+
40
+ ## Rules that are easy to break
41
+
42
+ - **Import Deck's copies.** `react`, `lucide-react` and the `deck-plugin-sdk/*` modules resolve to
43
+ Deck's own at run time. Never add a second React, or a UI library that bundles one: two
44
+ Reacts in one tree break hooks.
45
+ - **No native code.** A plugin can only call what Deck already exposes (`deck-plugin-sdk/bridge`).
46
+ To run a program, use `exec` or `procStream`; to read and write files, the `fs*` calls.
47
+ - **Opening files**: `export const opens = [".ext"]` with a `PaneView` makes Deck open those files
48
+ in the plugin, each as its own tab, given as the `file` prop. Open one yourself with `openFile`
49
+ from `deck-plugin-sdk/terminals`; save with `fsWriteBytes`.
50
+ - **npm packages are fine** (`npm install` and import): the build bundles them, their images,
51
+ fonts and CSS included, into `plugin.js`. `?raw` on an import gives a file's text (a worker).
52
+ A build step of your own (an esbuild plugin) goes in `deck-plugin.config.mjs` as
53
+ `export const esbuildPlugins = [...]`. Not a package that needs Node (`fs`, `child_process`),
54
+ one that bundles its own React, or one that starts a Web Worker from a file.
55
+ - **Storage goes through `configRead` / `configWrite`** with plain names (`"settings"`, or
56
+ `"notes.md"` for a plain file). Deck keeps them in the plugin's own folder. A name never
57
+ written reads as `""`. Do not write a config whose text has not changed: every write wakes
58
+ every config listener in Deck.
59
+ - **`listen("config-changed", ...)` gets a `string[]` of names**: act only when it includes one
60
+ of yours.
61
+ - **Deck reloads the plugin on every rebuild.** Anything module scope starts (a `setInterval`,
62
+ a `listen`, a window listener) must be stopped in an exported `dispose()`, or the old copy keeps
63
+ running beside the new one. Anything a component creates outside the DOM (an `Audio`, a timer,
64
+ a socket) is stopped in its effect's cleanup.
65
+ - **A poll stops when nobody can see it**: skip ticks while `document.hidden`, and do not run it
66
+ while the readout it feeds is switched off.
67
+ - **Styling uses Deck's stylesheet.** Tailwind classes Deck does not already use do not exist.
68
+ Use Deck's semantic tokens (`text-text-primary`, `text-text-secondary`, `text-text-muted`,
69
+ `bg-elev`, `border-subtle`, `var(--accent)`) and Deck's components from `deck-plugin-sdk/ui`, so
70
+ the plugin follows the user's theme.
71
+ - **No error is dropped silently.** A failed save, load or action logs
72
+ `console.warn("[__ID__] what failed", e)`.
73
+
74
+ ## More
75
+
76
+ The full guide (exports, storage, testing, publishing) is in Deck's **Docs** tab, which also has
77
+ the API reference for the Deck that is installed.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -1,17 +1,20 @@
1
- # __NAME__
2
-
3
- A plugin for [Deck](https://deck.hasan-zarif.com).
4
-
5
- ## Developing
6
-
7
- ```sh
8
- npm run dev # build, and rebuild as you edit
9
- npm run install-local # copy it into Deck, which loads it without a restart
10
- ```
11
-
12
- `src/plugin.tsx` is the entry point; its default export is the tab.
13
-
14
- ## What it does
15
-
16
- Say what this plugin is for. This file is what the registry shows on its page, so it is worth
17
- writing for somebody deciding whether to install it rather than for yourself.
1
+ # __NAME__
2
+
3
+ A plugin for Deck.
4
+
5
+ ## Developing
6
+
7
+ ```sh
8
+ npm run dev # rebuild as you edit; each save shows in your test Deck
9
+ npm run install-local # when it is done: install it into your real Deck
10
+ ```
11
+
12
+ Start a test Deck from Settings › Plugins › Open a test Deck: it keeps its own settings and data,
13
+ so trying the plugin never touches your real Deck.
14
+
15
+ `src/plugin.tsx` is the entry point; its default export is the tab.
16
+
17
+ ## What it does
18
+
19
+ Say what this plugin is for. This file is what the registry shows on its page, so it is worth
20
+ writing for somebody deciding whether to install it rather than for yourself.
@@ -1,4 +1,4 @@
1
- node_modules/
2
- # Copied in by the build from the SDK — not yours to edit, and not worth committing.
3
- shim/
4
- types/
1
+ node_modules/
2
+ # What the build writes. A release carries plugin.json and these; the repository holds the source.
3
+ plugin.js
4
+ mcp.js
@@ -1,9 +1,9 @@
1
- {
2
- "id": "__ID__",
3
- "name": "__NAME__",
4
- "description": "One line on what this is for, shown under its name in Settings.",
5
- "icon": "Sparkles",
6
- "color": "#a78bfa",
7
- "version": "0.1.0",
8
- "minDeck": "0.17.0"
9
- }
1
+ {
2
+ "id": "__ID__",
3
+ "name": "__NAME__",
4
+ "description": "One line on what this is for, shown under its name in Settings.",
5
+ "icon": "Sparkles",
6
+ "color": "#a78bfa",
7
+ "version": "0.1.0",
8
+ "minDeck": "1.5.0"
9
+ }
@@ -0,0 +1,16 @@
1
+ // What importing a file that is not code gives you, so the editor knows. The build inlines each
2
+ // into plugin.js: an image or font as a data: URL, a stylesheet as its text (or, imported with
3
+ // nothing bound — `import "x.css"` — applied to the page), a .wasm file as its bytes.
4
+ declare module "*.png" { const url: string; export default url; }
5
+ declare module "*.jpg" { const url: string; export default url; }
6
+ declare module "*.jpeg" { const url: string; export default url; }
7
+ declare module "*.gif" { const url: string; export default url; }
8
+ declare module "*.webp" { const url: string; export default url; }
9
+ declare module "*.svg" { const url: string; export default url; }
10
+ declare module "*.woff2" { const url: string; export default url; }
11
+ declare module "*.css" { const text: string; export default text; }
12
+ declare module "*.wasm" { const bytes: Uint8Array; export default bytes; }
13
+ // `import text from "some/file?raw"`: any file, as its text.
14
+ declare module "*?raw" { const text: string; export default text; }
15
+ // `import data from "some/file?base64"`: any file's bytes, as a base64 string.
16
+ declare module "*?base64" { const base64: string; export default base64; }
@@ -1,65 +1,65 @@
1
- // __NAME__ — a Deck plugin.
2
- //
3
- // The default export is the tab. A plugin can export more than that — a footer readout, a
4
- // settings panel, palette commands, a pane view — and `plugins/API.md` in the Deck repository
5
- // lists every slot. One export is enough to start.
6
- import { useState } from "react";
7
- import { Sparkles } from "lucide-react";
8
- import { configRead, configWrite } from "../shim/bridge.js";
9
-
10
- /** Where this plugin's own settings live. The `plugin-` prefix keeps them on this machine. */
11
- const CONFIG = "plugin-__ID__";
12
-
13
- export default function __COMPONENT__() {
14
- const [note, setNote] = useState("");
15
- const [saved, setSaved] = useState(false);
16
-
17
- // Deck's config store, the same one its own modules use. Reading a key that has never been
18
- // written resolves to an empty string rather than throwing.
19
- const load = async () => {
20
- const raw = await configRead(CONFIG).catch(() => "");
21
- setNote(raw.trim() ? (JSON.parse(raw) as { note?: string }).note ?? "" : "");
22
- };
23
-
24
- const save = async () => {
25
- await configWrite(CONFIG, JSON.stringify({ note }));
26
- setSaved(true);
27
- setTimeout(() => setSaved(false), 1500);
28
- };
29
-
30
- return (
31
- <div className="flex h-full flex-col gap-4 p-6">
32
- <header className="flex items-center gap-3">
33
- <Sparkles size={18} style={{ color: "var(--accent)" }} />
34
- <h1 className="text-base font-semibold text-text-primary">__NAME__</h1>
35
- </header>
36
-
37
- <p className="max-w-[60ch] text-sm text-text-secondary">
38
- Edit <code className="rounded bg-elev px-1.5 py-0.5 text-text-primary">src/plugin.tsx</code>,
39
- run <code className="rounded bg-elev px-1.5 py-0.5 text-text-primary">npm run dev</code>,
40
- and Deck reloads the plugin as you save.
41
- </p>
42
-
43
- <div className="flex max-w-md items-center gap-2">
44
- <input
45
- value={note}
46
- onChange={(e) => setNote(e.target.value)}
47
- onFocus={() => void load()}
48
- placeholder="Type something, then save it"
49
- className="min-w-0 flex-1 rounded-lg border border-subtle bg-elev px-3 py-2 text-sm text-text-primary outline-none focus:border-accent"
50
- />
51
- <button
52
- onClick={() => void save()}
53
- className="shrink-0 rounded-lg border border-subtle bg-elev px-3 py-2 text-sm text-text-secondary hover:border-accent hover:text-text-primary"
54
- >
55
- {saved ? "Saved" : "Save"}
56
- </button>
57
- </div>
58
-
59
- <p className="text-xs text-text-muted">
60
- That writes to Deck's config store under <code>{CONFIG}</code>, which stays on this
61
- machine.
62
- </p>
63
- </div>
64
- );
65
- }
1
+ // __NAME__ — a Deck plugin.
2
+ //
3
+ // The default export is the tab. A plugin can export more than that — a footer readout, a
4
+ // settings panel, palette commands, a pane view — and `docs/guide/api.md` in the Deck repository
5
+ // lists every slot. One export is enough to start.
6
+ import { useState } from "react";
7
+ import { Sparkles } from "lucide-react";
8
+ import { configRead, configWrite } from "deck-plugin-sdk/bridge";
9
+
10
+ /** This plugin's own settings. Deck keeps a plugin's configs in its own folder, so a plain name. */
11
+ const CONFIG = "settings";
12
+
13
+ export default function __COMPONENT__() {
14
+ const [note, setNote] = useState("");
15
+ const [saved, setSaved] = useState(false);
16
+
17
+ // Deck's config store, the same one its own modules use. Reading a key that has never been
18
+ // written resolves to an empty string rather than throwing.
19
+ const load = async () => {
20
+ const raw = await configRead(CONFIG).catch(() => "");
21
+ setNote(raw.trim() ? (JSON.parse(raw) as { note?: string }).note ?? "" : "");
22
+ };
23
+
24
+ const save = async () => {
25
+ await configWrite(CONFIG, JSON.stringify({ note }));
26
+ setSaved(true);
27
+ setTimeout(() => setSaved(false), 1500);
28
+ };
29
+
30
+ return (
31
+ <div className="flex h-full flex-col gap-4 p-6">
32
+ <header className="flex items-center gap-3">
33
+ <Sparkles size={18} style={{ color: "var(--accent)" }} />
34
+ <h1 className="text-base font-semibold text-text-primary">__NAME__</h1>
35
+ </header>
36
+
37
+ <p className="max-w-[60ch] text-sm text-text-secondary">
38
+ Edit <code className="rounded bg-elev px-1.5 py-0.5 text-text-primary">src/plugin.tsx</code>,
39
+ run <code className="rounded bg-elev px-1.5 py-0.5 text-text-primary">npm run dev</code>,
40
+ and Deck reloads the plugin as you save.
41
+ </p>
42
+
43
+ <div className="flex max-w-md items-center gap-2">
44
+ <input
45
+ value={note}
46
+ onChange={(e) => setNote(e.target.value)}
47
+ onFocus={() => void load()}
48
+ placeholder="Type something, then save it"
49
+ className="min-w-0 flex-1 rounded-lg border border-subtle bg-elev px-3 py-2 text-sm text-text-primary outline-none focus:border-accent"
50
+ />
51
+ <button
52
+ onClick={() => void save()}
53
+ className="shrink-0 rounded-lg border border-subtle bg-elev px-3 py-2 text-sm text-text-secondary hover:border-accent hover:text-text-primary"
54
+ >
55
+ {saved ? "Saved" : "Save"}
56
+ </button>
57
+ </div>
58
+
59
+ <p className="text-xs text-text-muted">
60
+ That writes to Deck's config store under <code>{CONFIG}</code>, which stays on this
61
+ machine.
62
+ </p>
63
+ </div>
64
+ );
65
+ }