janela 0.14.1 → 0.14.3

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.
Files changed (33) hide show
  1. package/README.md +16 -3
  2. package/api/index.d.ts +11 -1
  3. package/bin/janela.mjs +72 -12
  4. package/bin/lib.mjs +27 -0
  5. package/package.json +1 -1
  6. package/templates/index.html +224 -53
  7. package/templates/janela.conf.json +2 -2
  8. package/templates/react/files/index.html +1 -0
  9. package/templates/react/files/janela.conf.json +2 -2
  10. package/templates/react/files/src/App.tsx +154 -37
  11. package/templates/react/files/src/main.tsx +1 -0
  12. package/templates/react/files/src/styles.css +137 -0
  13. package/templates/react/files/src-host/main.ts +82 -0
  14. package/templates/solid/files/index.html +1 -0
  15. package/templates/solid/files/janela.conf.json +2 -2
  16. package/templates/solid/files/src/App.tsx +145 -35
  17. package/templates/solid/files/src/main.tsx +1 -0
  18. package/templates/solid/files/src/styles.css +137 -0
  19. package/templates/solid/files/src-host/main.ts +82 -0
  20. package/templates/svelte/files/index.html +1 -0
  21. package/templates/svelte/files/janela.conf.json +2 -2
  22. package/templates/svelte/files/src/App.svelte +116 -27
  23. package/templates/svelte/files/src/main.ts +1 -0
  24. package/templates/svelte/files/src/styles.css +137 -0
  25. package/templates/svelte/files/src-host/main.ts +82 -0
  26. package/templates/vue/files/index.html +1 -0
  27. package/templates/vue/files/janela.conf.json +2 -2
  28. package/templates/vue/files/src/App.vue +99 -20
  29. package/templates/vue/files/src/main.ts +1 -0
  30. package/templates/vue/files/src/styles.css +137 -0
  31. package/templates/vue/files/src-host/main.ts +82 -0
  32. package/templates/react/files/src/App.css +0 -3
  33. package/templates/solid/files/src/App.css +0 -3
package/README.md CHANGED
@@ -6,9 +6,11 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
6
6
  Node, no Electron. The backend is TypeScript compiled to a native binary by
7
7
  [scriptc](https://scriptc.dev); the window is the OS webview via
8
8
  [webview/webview](https://github.com/webview/webview). A desktop binary comes
9
- out around 200400 KB — 191 KB for the smallest template — with no bundled
9
+ out around 225420 KB — 225 KB for the smallest template — with no bundled
10
10
  browser and no bundled runtime; iOS and Android bundles land around
11
- 211415 KB. Per-template figures are in
11
+ 228421 KB. Those are the *starter's* figures, and about 35 KB of each is the
12
+ native file dialog and worker-thread reader it demonstrates: delete the "Files
13
+ and the window" card and its two commands and the smallest binary is 212 KB. Per-template figures are in
12
14
  [docs/frontend.md](../../docs/frontend.md).
13
15
 
14
16
  Five targets, one runtime — the same `main.ts`, the same typed contract and the
@@ -30,6 +32,7 @@ desktop-only for now; on mobile they report clearly when called.
30
32
 
31
33
  ```bash
32
34
  npm install -g janela # or: npx janela init my-app
35
+ janela --version # which janela you actually have
33
36
  janela init my-app # (or `jn init my-app`)
34
37
  cd my-app
35
38
  janela dev # build + run with logs in the terminal
@@ -40,10 +43,20 @@ Or start from a frontend framework:
40
43
 
41
44
  ```bash
42
45
  janela init my-app --template vue # or react | svelte | solid | vanilla
43
- cd my-app && npm install
46
+ cd my-app # deps are already installed
44
47
  janela dev # Vite dev server + HMR, in a native window
45
48
  ```
46
49
 
50
+ `init` installs the template's dependencies with whichever package manager
51
+ ran it — `pnpm janela init` uses pnpm — so `janela dev` works straight
52
+ afterwards. Pass `--no-install` to skip that, and if the install fails the
53
+ project is still written; the message names the one command to retry.
54
+
55
+ A `--template` a globally installed CLI has never heard of is now an error
56
+ rather than a silent fallback, and `janela --version` says which one you are
57
+ running — worth checking first if a scaffolded project comes out looking
58
+ nothing like the screenshots.
59
+
47
60
  `vanilla` is the default and needs no frontend toolchain at all. With a
48
61
  framework, `janela dev` runs your Vite dev server and points the window at it,
49
62
  and `janela build` flattens the production bundle into the binary.
package/api/index.d.ts CHANGED
@@ -94,10 +94,20 @@ export interface JanelaClient<A> {
94
94
  /**
95
95
  * Call a declared command. Unknown names and wrong argument shapes are
96
96
  * compile errors, and the result type comes from the contract.
97
+ *
98
+ * A command declared to take nothing — `quit: () => void` — normalises to
99
+ * `args: null`, and for those the argument is omitted rather than passed as
100
+ * an explicit `null`. That is what the rest-tuple is for: it makes the
101
+ * parameter optional for exactly those commands, and required with the
102
+ * declared shape for every other one. The bracketed `[X] extends [null]`
103
+ * form is deliberate — a bare `null extends X` would also match a command
104
+ * whose arguments are legitimately nullable.
97
105
  */
98
106
  invoke<K extends keyof CommandsOf<A> & string>(
99
107
  name: K,
100
- args: CommandsOf<A>[K]["args"],
108
+ ...args: [CommandsOf<A>[K]["args"]] extends [null]
109
+ ? [args?: null]
110
+ : [args: CommandsOf<A>[K]["args"]]
101
111
  ): Promise<CommandsOf<A>[K]["result"]>;
102
112
  /**
103
113
  * Subscribe to a declared event; the payload type is inferred. Returns a
package/bin/janela.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // janela — the CLI (the tauri-cli analogue).
3
3
  //
4
- // janela init <name> scaffold a new project
4
+ // janela init <name> scaffold a new project (and install its dependencies)
5
5
  // janela build compile the project to a native binary (+ .app on macOS)
6
6
  // janela dev build, then run the binary with logs in the terminal
7
+ // janela --version which janela this is
7
8
  //
8
9
  // A project is: index.html (frontend), src-host/main.ts (commands),
9
10
  // janela.conf.json (window + bundle config). Everything else — the C shim over
@@ -20,12 +21,15 @@ import { dirname, join, relative, resolve, sep } from "node:path";
20
21
  import { fileURLToPath } from "node:url";
21
22
  import {
22
23
  ANDROID_ABI, ANDROID_TARGET_SDK, androidConf, ffiManifest, iosConf,
23
- libraryProfile, mimeFor, NAME_RE, patchPeSubsystem, PeError,
24
- rewriteHostSpecifier, suggestName,
24
+ installCommand, libraryProfile, mimeFor, NAME_RE, packageManager as pmFor,
25
+ patchPeSubsystem, PeError, rewriteHostSpecifier, suggestName,
25
26
  } from "./lib.mjs";
26
27
 
27
28
  const KIT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
29
  const require = createRequire(join(KIT, "package.json"));
30
+ // createRequire is rooted AT the package.json, so "./package.json" is this
31
+ // package's own — not the bin/ directory's and not the workspace's.
32
+ const VERSION = require("./package.json").version;
29
33
 
30
34
  function fail(msg) {
31
35
  console.error(`janela: ${msg}`);
@@ -103,8 +107,9 @@ function viteCommand(root) {
103
107
  if (existsSync(shim)) return { argv: [shim], shell: process.platform === "win32" };
104
108
 
105
109
  fail(
106
- "this project has a vite config but no local vite — run your package manager's " +
107
- "install first (npm install / pnpm install)",
110
+ "this project has a vite config but no local vite — its dependencies are not " +
111
+ `installed yet:\n ${installCommand(pmFor(process.env)).join(" ")}\n` +
112
+ " (janela init installs them for you unless you pass --no-install)",
108
113
  );
109
114
  }
110
115
 
@@ -1156,7 +1161,29 @@ function copyTemplate(from, to, name) {
1156
1161
 
1157
1162
  // Best-effort repair of a rejected name, so the error can suggest something
1158
1163
  // that would have worked instead of only stating the rule.
1159
- function init(name, template) {
1164
+ /**
1165
+ * Install a scaffolded project's dependencies.
1166
+ *
1167
+ * Returns true on success. A failure is NOT fatal: the project is already
1168
+ * written, and the useful thing to do is name the one command to retry rather
1169
+ * than exit non-zero on a tree that is fine. Offline, an unreachable registry
1170
+ * and missing auth all land here.
1171
+ *
1172
+ * Windows needs a shell: npm/pnpm/yarn are .cmd shims, and Node has refused to
1173
+ * spawn those directly since the CVE-2024-27980 fix.
1174
+ */
1175
+ function installDeps(dir, pm) {
1176
+ const cmd = installCommand(pm);
1177
+ console.log(`janela: installing dependencies with ${pm}…`);
1178
+ const r = spawnSync(cmd[0], cmd.slice(1), {
1179
+ stdio: "inherit",
1180
+ cwd: dir,
1181
+ shell: process.platform === "win32",
1182
+ });
1183
+ return r.status === 0;
1184
+ }
1185
+
1186
+ function init(name, template, { install = true } = {}) {
1160
1187
  if (!name) {
1161
1188
  fail(
1162
1189
  "no project name given.\n" +
@@ -1212,8 +1239,27 @@ function init(name, template) {
1212
1239
  writeFileSync(join(dir, ".gitignore"), ".janela/\nnode_modules/\ndist/\n");
1213
1240
  writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
1214
1241
 
1215
- const install = template === "vanilla" ? "" : "npm install && ";
1216
- console.log(`janela: created ${name}/ (${template}) next: cd ${name} && ${install}janela dev`);
1242
+ // The no-framework template has no dependencies to install: it needs no
1243
+ // frontend toolchain, and `janela dev` works on it straight out of init.
1244
+ const hasDeps = template !== "vanilla";
1245
+ if (!hasDeps || !install) {
1246
+ // Only a skipped install on a template that HAS dependencies leaves the
1247
+ // caller something to run.
1248
+ const manual = hasDeps ? `${installCommand(pmFor(process.env)).join(" ")} && ` : "";
1249
+ console.log(`janela: created ${name}/ (${template}) — next: cd ${name} && ${manual}janela dev`);
1250
+ return;
1251
+ }
1252
+
1253
+ const pm = pmFor(process.env);
1254
+ if (installDeps(dir, pm)) {
1255
+ console.log(`janela: created ${name}/ (${template}) — next: cd ${name} && janela dev`);
1256
+ } else {
1257
+ console.log(
1258
+ `janela: created ${name}/ (${template}), but installing its dependencies failed.\n` +
1259
+ ` The project is written and fine — retry the install and you are set:\n` +
1260
+ ` cd ${name} && ${installCommand(pm).join(" ")} && janela dev`,
1261
+ );
1262
+ }
1217
1263
  }
1218
1264
 
1219
1265
  // ---- dev --------------------------------------------------------------------
@@ -1277,6 +1323,16 @@ async function dev(root) {
1277
1323
  const argv = process.argv.slice(2);
1278
1324
  const cmd = argv[0];
1279
1325
 
1326
+ // Answered before anything else, because the question it settles is "is the
1327
+ // janela running this command the one I think it is?". A globally installed
1328
+ // CLI several minor versions behind accepts `init` and quietly ignores flags it
1329
+ // has never heard of — `--template` did not exist before 0.3.0 — so the only
1330
+ // symptom is a project that came out looking wrong.
1331
+ if (cmd === "--version" || cmd === "-v") {
1332
+ console.log(VERSION);
1333
+ process.exit(0);
1334
+ }
1335
+
1280
1336
  function flag(name, fallback) {
1281
1337
  const eq = argv.find((a) => a.startsWith(`--${name}=`));
1282
1338
  if (eq) return eq.slice(name.length + 3);
@@ -1335,9 +1391,11 @@ function assertPositionals(max) {
1335
1391
 
1336
1392
  switch (cmd) {
1337
1393
  case "init":
1338
- assertKnownFlags(["template"]);
1394
+ assertKnownFlags(["template", "no-install"]);
1339
1395
  assertPositionals(1);
1340
- init(positionals()[0], flag("template", "vanilla"));
1396
+ init(positionals()[0], flag("template", "vanilla"), {
1397
+ install: !argv.includes("--no-install"),
1398
+ });
1341
1399
  break;
1342
1400
  case "build":
1343
1401
  assertKnownFlags(["target"]);
@@ -1356,9 +1414,11 @@ switch (cmd) {
1356
1414
  break;
1357
1415
  default:
1358
1416
  console.log(
1359
- "usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
1417
+ `janela ${VERSION}\n\n` +
1418
+ "usage: janela init <name> [--template vanilla|vue|react|svelte|solid] [--no-install]\n" +
1360
1419
  " janela build [--target desktop|ios|android]\n" +
1361
- " janela dev [--target desktop|ios|android]",
1420
+ " janela dev [--target desktop|ios|android]\n" +
1421
+ " janela --version",
1362
1422
  );
1363
1423
  process.exit(cmd ? 1 : 0);
1364
1424
  }
package/bin/lib.mjs CHANGED
@@ -345,3 +345,30 @@ export function patchPeSubsystem(input) {
345
345
  buf.writeUInt16LE(IMAGE_SUBSYSTEM_WINDOWS_GUI, subOff);
346
346
  return { patched: true, buf };
347
347
  }
348
+
349
+ /**
350
+ * Which package manager to install a scaffolded project with.
351
+ *
352
+ * `npm_config_user_agent` is set by whichever manager ran janela, so
353
+ * `pnpm janela init` installs with pnpm. Run as a plain global binary it is
354
+ * unset, and npm is the only manager guaranteed to exist alongside node.
355
+ *
356
+ * Takes the environment rather than reading process.env, so it is testable.
357
+ */
358
+ export function packageManager(env = {}) {
359
+ const ua = env.npm_config_user_agent ?? "";
360
+ for (const pm of ["pnpm", "yarn", "bun"]) if (ua.startsWith(`${pm}/`)) return pm;
361
+ return "npm";
362
+ }
363
+
364
+ /**
365
+ * The install invocation for a manager.
366
+ *
367
+ * yarn's is the bare command. npm gets --no-fund --no-audit: neither is useful
368
+ * on a freshly scaffolded tree, and the audit is the slowest part of it.
369
+ */
370
+ export function installCommand(pm) {
371
+ if (pm === "yarn") return ["yarn"];
372
+ if (pm === "npm") return ["npm", "install", "--no-fund", "--no-audit"];
373
+ return [pm, "install"];
374
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "description": "Desktop, iOS and Android apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,33 +1,204 @@
1
1
  <!doctype html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__NAME__</title>
5
7
  <style>
6
- body { font: 16px -apple-system, sans-serif; padding: 2rem; }
7
- input[type="number"] { width: 5em; }
8
- #events { color: #666; font-size: 13px; }
8
+ /* Everything here is yours to replace. It is deliberately one file with
9
+ no dependencies: this template has no bundler, so what you see is what
10
+ runs. The mark below is drawn in CSS rather than shipped as an image —
11
+ a janela frontend is embedded in the binary, so every asset has to be
12
+ inlined anyway, and a window needs no artwork. */
13
+ :root {
14
+ color-scheme: light dark;
15
+ --bg: #f6f6f7;
16
+ --card: #ffffff;
17
+ --text: #18181b;
18
+ --muted: #71717a;
19
+ --line: #e4e4e7;
20
+ --accent: #2563eb;
21
+ --accent-soft: #eff4ff;
22
+ --shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(0 0 0 / 0.12);
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ :root {
26
+ --bg: #17171a;
27
+ --card: #202024;
28
+ --text: #fafafa;
29
+ --muted: #a1a1aa;
30
+ --line: #2e2e34;
31
+ --accent: #60a5fa;
32
+ --accent-soft: #1d2433;
33
+ --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -12px rgb(0 0 0 / 0.5);
34
+ }
35
+ }
36
+
37
+ * { box-sizing: border-box; }
38
+ body {
39
+ margin: 0;
40
+ min-height: 100vh;
41
+ display: flex;
42
+ justify-content: center;
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
46
+ -webkit-font-smoothing: antialiased;
47
+ }
48
+ main { width: 100%; max-width: 760px; padding: 26px 22px 32px; }
49
+
50
+ /* --- header ------------------------------------------------------- */
51
+ header { text-align: center; margin-bottom: 20px; }
52
+ .mark {
53
+ width: 56px; height: 44px; margin: 0 auto 14px;
54
+ border: 2px solid var(--text); border-radius: 8px;
55
+ display: flex; flex-direction: column; overflow: hidden;
56
+ }
57
+ .mark .bar {
58
+ height: 13px; flex: none;
59
+ border-bottom: 2px solid var(--text);
60
+ display: flex; align-items: center; gap: 3px; padding: 0 4px;
61
+ }
62
+ .mark .bar i { width: 3px; height: 3px; border-radius: 50%; background: var(--text); }
63
+ .mark .pane { flex: 1; background: linear-gradient(135deg, var(--accent), transparent 70%); opacity: 0.85; }
64
+ h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; font-weight: 650; }
65
+ .sub { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
66
+
67
+ /* The one line that proves the whole stack: this string was produced by
68
+ TypeScript compiled to machine code, and handed to the page. */
69
+ .greeting {
70
+ margin: 0 0 20px;
71
+ padding: 11px 16px;
72
+ border: 1px solid var(--line);
73
+ border-radius: 10px;
74
+ background: var(--accent-soft);
75
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
76
+ text-align: center;
77
+ overflow-wrap: anywhere;
78
+ }
79
+
80
+ /* --- cards -------------------------------------------------------- */
81
+ /* auto-fit rather than a fixed column count: the same page is the iOS and
82
+ Android frontend, where it must read as a single column. */
83
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 13px; }
84
+ .card {
85
+ background: var(--card);
86
+ border: 1px solid var(--line);
87
+ border-radius: 12px;
88
+ padding: 15px 17px;
89
+ box-shadow: var(--shadow);
90
+ }
91
+ .card h2 {
92
+ margin: 0 0 2px;
93
+ font-size: 13px; font-weight: 600;
94
+ letter-spacing: 0.04em; text-transform: uppercase;
95
+ color: var(--muted);
96
+ }
97
+ .card.wide { grid-column: 1 / -1; }
98
+ .card p.why { margin: 0 0 11px; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
99
+ .card h3 {
100
+ margin: 16px 0 6px; font-size: 11px; font-weight: 600;
101
+ letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
102
+ }
103
+ .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
104
+
105
+ input, button {
106
+ font: inherit;
107
+ border-radius: 8px;
108
+ border: 1px solid var(--line);
109
+ padding: 8px 12px;
110
+ background: var(--card);
111
+ color: var(--text);
112
+ transition: border-color 0.15s, background 0.15s;
113
+ }
114
+ input { min-width: 0; }
115
+ input[type="number"] { width: 5.5em; text-align: center; }
116
+ input.grow { flex: 1 1 12em; }
117
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
118
+ button { cursor: pointer; font-weight: 500; }
119
+ button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
120
+ button:disabled { opacity: 0.5; cursor: default; }
121
+ button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
122
+ button.primary:hover:not(:disabled) { filter: brightness(1.08); color: #fff; }
123
+
124
+ .result { margin: 12px 0 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
125
+ .result.filled { color: var(--text); }
126
+ pre.result { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 11em; overflow: auto; margin-top: 12px; }
127
+
128
+ /* A dot that keeps moving while a 2s command is pending. If it stalls,
129
+ the host has blocked the UI thread — which is the bug this proves is
130
+ absent. */
131
+ .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
132
+ @keyframes pulse { 0%, 100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
133
+ @media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 1; } }
134
+
135
+ ul.events { list-style: none; margin: 0; padding: 0; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
136
+ ul.events li { padding: 3px 0; border-bottom: 1px dashed var(--line); }
137
+ ul.events li:last-child { border-bottom: 0; }
138
+ ul.events:empty::after { content: "nothing yet — press add"; color: var(--muted); font-family: inherit; }
139
+
140
+ footer { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
141
+ footer code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; font-size: 12px; }
142
+
9
143
  </style>
10
144
  </head>
11
145
  <body>
12
- <h1>__NAME__</h1>
13
- <p>
14
- <input id="a" type="number" value="2" /> +
15
- <input id="b" type="number" value="40" />
16
- <button id="add">add</button>
17
- <button id="wait">wait 2s (async)</button>
18
- <button id="quit">Quit</button>
19
- </p>
20
- <p>
21
- <input id="path" type="text" value="janela.conf.json" size="32" />
22
- <button id="read">read file (async)</button>
23
- <button id="open">pick a file…</button>
24
- </p>
25
- <p>
26
- <input id="title" type="text" value="__NAME__ — renamed" size="24" />
27
- <button id="retitle">set window title</button>
28
- </p>
29
- <pre id="out">booting…</pre>
30
- <ul id="events"></ul>
146
+ <main>
147
+ <header>
148
+ <div class="mark" aria-hidden="true">
149
+ <div class="bar"><i></i><i></i><i></i></div>
150
+ <div class="pane"></div>
151
+ </div>
152
+ <h1>__NAME__</h1>
153
+ <p class="sub">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
154
+ </header>
155
+
156
+ <p class="greeting" id="out">booting…</p>
157
+
158
+ <div class="grid">
159
+ <section class="card">
160
+ <h2>Typed commands</h2>
161
+ <p class="why">The name, the arguments and the result are all checked at compile time — no codegen, because both sides are TypeScript.</p>
162
+ <div class="row">
163
+ <input id="a" type="number" value="2" aria-label="first number" />
164
+ <span aria-hidden="true">+</span>
165
+ <input id="b" type="number" value="40" aria-label="second number" />
166
+ <button id="add" class="primary">add</button>
167
+ </div>
168
+ <p class="result" id="sum">—</p>
169
+ <h3>Events from the host</h3>
170
+ <ul class="events" id="events"></ul>
171
+ </section>
172
+
173
+ <section class="card">
174
+ <h2>Async without blocking</h2>
175
+ <p class="why">The window keeps answering while this is pending: the dot keeps moving and <strong>add</strong> still works.</p>
176
+ <div class="row">
177
+ <button id="wait">wait 2s</button>
178
+ <span id="spinner" hidden><span class="pulse"></span></span>
179
+ </div>
180
+ <p class="result" id="waited">—</p>
181
+ </section>
182
+
183
+ <section class="card wide">
184
+ <h2>Files and the window</h2>
185
+ <p class="why">Reads run on a worker thread, so a large file never freezes the window. The dialog is the real native one.</p>
186
+ <div class="row">
187
+ <input id="path" class="grow" type="text" value="janela.conf.json" aria-label="file to read" />
188
+ <button id="read">read</button>
189
+ <button id="open">pick a file…</button>
190
+ <input id="title" class="grow" type="text" value="__NAME__ — renamed" aria-label="window title" />
191
+ <button id="retitle">set title</button>
192
+ </div>
193
+ <pre class="result" id="file">—</pre>
194
+ </section>
195
+ </div>
196
+
197
+ <footer>
198
+ <span>Edit <code>index.html</code> for the page and <code>src-host/main.ts</code> for the commands.</span>
199
+ <button id="quit">Quit</button>
200
+ </footer>
201
+ </main>
31
202
 
32
203
  <script>
33
204
  // This template has no bundler, so it uses the `janela` global that the
@@ -35,56 +206,56 @@
35
206
  // `import { invoke, listen } from "janela/api"` instead — same functions,
36
207
  // but typed and resolvable by the bundler.
37
208
  window.onload = async () => {
38
- const out = document.getElementById("out");
39
- const events = document.getElementById("events");
209
+ const $ = (id) => document.getElementById(id);
210
+ const show = (el, text) => {
211
+ el.textContent = text;
212
+ el.classList.add("filled");
213
+ };
40
214
 
41
215
  janela.listen("added", (sum) => {
42
216
  const li = document.createElement("li");
43
- li.textContent = "event 'added' from host: " + sum;
44
- events.prepend(li);
217
+ li.textContent = "host emitted 'added': " + sum;
218
+ $("events").prepend(li);
45
219
  });
46
220
 
47
- out.textContent = await janela.invoke("greet", { name: "__NAME__" });
221
+ // The greeting is a round trip: the string is built in compiled
222
+ // TypeScript and returned to the page.
223
+ $("out").textContent = await janela.invoke("greet", { name: "__NAME__" });
48
224
  await janela.invoke("log", "page loaded");
49
225
 
50
- document.getElementById("add").onclick = async () => {
51
- const a = Number(document.getElementById("a").value);
52
- const b = Number(document.getElementById("b").value);
53
- const sum = await janela.invoke("add", { a, b });
54
- out.textContent = `add(${a}, ${b}) -> ${sum}`;
226
+ $("add").onclick = async () => {
227
+ const a = Number($("a").value);
228
+ const b = Number($("b").value);
229
+ show($("sum"), `add(${a}, ${b}) → ${await janela.invoke("add", { a, b })}`);
55
230
  };
56
- // The window stays responsive while this is pending: the spinner keeps
57
- // animating and "add" still works.
58
- document.getElementById("wait").onclick = async (e) => {
231
+
232
+ $("wait").onclick = async (e) => {
59
233
  e.target.disabled = true;
60
- out.textContent = "waiting… (try 'add' — it still answers)";
61
- out.textContent = await janela.invoke("wait", { ms: 2000 });
234
+ $("spinner").hidden = false;
235
+ show($("waited"), "waiting… try add, it still answers");
236
+ const answer = await janela.invoke("wait", { ms: 2000 });
237
+ show($("waited"), answer);
238
+ $("spinner").hidden = true;
62
239
  e.target.disabled = false;
63
240
  };
64
241
 
65
- // Reads run on a worker thread in the shim, so even a large file
66
- // does not freeze the window while it loads.
67
- document.getElementById("read").onclick = async () => {
68
- const path = document.getElementById("path").value;
242
+ $("read").onclick = async () => {
243
+ const path = $("path").value;
69
244
  const r = await janela.invoke("readFile", { path });
70
- out.textContent = r.ok
71
- ? `${path}: ${r.length} chars\n\n` + r.text.slice(0, 400)
72
- : "error: " + r.error;
245
+ show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
73
246
  };
74
247
 
75
248
  // The native open dialog. The window keeps serving other commands
76
249
  // while the user is deciding.
77
- document.getElementById("open").onclick = async () => {
250
+ $("open").onclick = async () => {
78
251
  const r = await janela.invoke("openFile", {});
79
- if (!r.ok) out.textContent = "error: " + r.error;
80
- else if (r.cancelled) out.textContent = "cancelled";
81
- else out.textContent = `${r.path}: ${r.length} chars\n\n` + r.text.slice(0, 400);
252
+ if (!r.ok) show($("file"), "error: " + r.error);
253
+ else if (r.cancelled) show($("file"), "cancelled");
254
+ else show($("file"), `${r.path} ${r.length} chars\n\n${r.text.slice(0, 400)}`);
82
255
  };
83
256
 
84
- document.getElementById("retitle").onclick = () =>
85
- janela.invoke("setTitle", { title: document.getElementById("title").value });
86
-
87
- document.getElementById("quit").onclick = () => janela.invoke("quit");
257
+ $("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
258
+ $("quit").onclick = () => janela.invoke("quit");
88
259
  };
89
260
  </script>
90
261
  </body>
@@ -4,8 +4,8 @@
4
4
  "version": "0.1.0",
5
5
  "window": {
6
6
  "title": "__NAME__",
7
- "width": 640,
8
- "height": 420
7
+ "width": 820,
8
+ "height": 700
9
9
  },
10
10
  "ios": {
11
11
  "identifier": "dev.janela.__NAME__",
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
6
  <title>__NAME__</title>
6
7
  </head>
7
8
  <body>
@@ -4,8 +4,8 @@
4
4
  "version": "0.1.0",
5
5
  "window": {
6
6
  "title": "__NAME__",
7
- "width": 640,
8
- "height": 420
7
+ "width": 820,
8
+ "height": 700
9
9
  },
10
10
  "ios": {
11
11
  "identifier": "dev.janela.__NAME__",