janela 0.14.3 → 0.16.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.
@@ -1,130 +1,42 @@
1
1
  <script lang="ts">
2
- import { onDestroy, onMount } from "svelte";
2
+ import { onMount } from "svelte";
3
3
  import { createClient } from "janela/api";
4
- // A type-only import of the host's contract. It is erased at compile time,
5
- // so no host code is bundled into the page — this is purely a type edge.
4
+ // A type-only import of the host's contract: command names, argument shapes
5
+ // and results are checked against it, with no code generation, because both
6
+ // sides are TypeScript and this import erases at compile time.
6
7
  import type { App } from "../src-host/main";
7
8
 
8
9
  const client = createClient<App>();
9
10
 
10
- let greeting = $state("booting…");
11
- let a = $state(2);
12
- let b = $state(40);
13
- let sum = $state("—");
14
- let waited = $state("—");
15
- let waiting = $state(false);
16
- let events = $state<string[]>([]);
17
- let path = $state("janela.conf.json");
18
- let title = $state("__NAME__ — renamed");
19
- let fileOut = $state("—");
11
+ let name = $state("");
12
+ let greeting = $state("");
20
13
 
21
- // The event name is checked against the contract and `value` is inferred.
22
- // `on` returns a disposer.
23
- const off = client.on("added", (value) => {
24
- events = [`host emitted 'added': ${value}`, ...events];
25
- });
26
- onDestroy(off);
14
+ // `greet` is a command in src-host/main.ts, compiled to machine code.
15
+ async function greet() {
16
+ greeting = await client.invoke("greet", { name: name || "__NAME__" });
17
+ }
27
18
 
19
+ // Greeted once on load, so the round trip is visible without a click.
28
20
  onMount(async () => {
29
- greeting = await client.invoke("greet", { name: "__NAME__" });
21
+ await greet();
30
22
  await client.invoke("log", "page loaded");
31
23
  });
32
-
33
- async function add() {
34
- sum = `add(${a}, ${b}) → ${await client.invoke("add", { a: Number(a), b: Number(b) })}`;
35
- }
36
-
37
- async function wait() {
38
- waiting = true;
39
- waited = "waiting… try add, it still answers";
40
- waited = await client.invoke("wait", { ms: 2000 });
41
- waiting = false;
42
- }
43
-
44
- async function read() {
45
- const r = await client.invoke("readFile", { path });
46
- fileOut = r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : `error: ${r.error}`;
47
- }
48
-
49
- // The native open dialog. The window keeps serving other commands while the
50
- // user is deciding.
51
- async function pick() {
52
- const r = await client.invoke("openFile");
53
- if (!r.ok) fileOut = `error: ${r.error}`;
54
- else if (r.cancelled) fileOut = "cancelled";
55
- else fileOut = `${r.path} — ${r.length} chars\n\n${r.text.slice(0, 400)}`;
56
- }
57
24
  </script>
58
25
 
59
- <main>
60
- <header>
61
- <div class="mark" aria-hidden="true">
62
- <div class="bar"><i></i><i></i><i></i></div>
63
- <div class="pane"></div>
64
- </div>
65
- <h1>__NAME__</h1>
66
- <p class="sub">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
67
- </header>
68
-
69
- <p class="greeting">{greeting}</p>
70
-
71
- <div class="grid">
72
- <section class="card">
73
- <h2>Typed commands</h2>
74
- <p class="why">
75
- The name, the arguments and the result are all checked at compile time — no codegen,
76
- because both sides are TypeScript.
77
- </p>
78
- <div class="row">
79
- <input type="number" bind:value={a} aria-label="first number" />
80
- <span aria-hidden="true">+</span>
81
- <input type="number" bind:value={b} aria-label="second number" />
82
- <button class="primary" onclick={add}>add</button>
83
- </div>
84
- <p class="result" class:filled={sum !== "—"}>{sum}</p>
85
- <h3>Events from the host</h3>
86
- <ul class="events">
87
- {#each events as e, i (i)}
88
- <li>{e}</li>
89
- {/each}
90
- </ul>
91
- </section>
92
-
93
- <section class="card">
94
- <h2>Async without blocking</h2>
95
- <p class="why">
96
- The window keeps answering while this is pending: the dot keeps moving and
97
- <strong>add</strong> still works.
98
- </p>
99
- <div class="row">
100
- <button disabled={waiting} onclick={wait}>wait 2s</button>
101
- {#if waiting}<span><span class="pulse"></span></span>{/if}
102
- </div>
103
- <p class="result" class:filled={waited !== "—"}>{waited}</p>
104
- </section>
26
+ <main class="container">
27
+ <h1>Welcome to janela + Svelte</h1>
28
+ <p class="hint">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
105
29
 
106
- <section class="card wide">
107
- <h2>Files and the window</h2>
108
- <p class="why">
109
- Reads run on a worker thread, so a large file never freezes the window. The dialog is the
110
- real native one.
111
- </p>
112
- <div class="row">
113
- <input class="grow" type="text" bind:value={path} aria-label="file to read" />
114
- <button onclick={read}>read</button>
115
- <button onclick={pick}>pick a file…</button>
116
- <input class="grow" type="text" bind:value={title} aria-label="window title" />
117
- <button onclick={() => client.invoke("setTitle", { title })}>set title</button>
30
+ <div class="row logos">
31
+ <a href="https://vite.dev" target="_blank" rel="noreferrer"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos logo vite" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg></a>
32
+ <a href="https://mmamedel.github.io/janela/" target="_blank" rel="noreferrer"><svg class="logo janela" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970.64 970.64" role="img" aria-label="janela"><path fill="currentColor" d="M970.64,485.32c0,268.03-217.28,485.32-485.32,485.32S0,753.35,0,485.32,217.28,0,485.32,0s485.32,217.28,485.32,485.32ZM938.11,485.34c0-250.08-202.73-452.8-452.8-452.8S32.51,235.26,32.51,485.34s202.73,452.8,452.8,452.8,452.8-202.73,452.8-452.8Z"/><path fill="currentColor" d="M892.79,485.25c0,225.05-182.44,407.49-407.49,407.49S77.82,710.3,77.82,485.25,260.26,77.77,485.31,77.77s407.49,182.44,407.49,407.49ZM470.27,112.09v335.5c0,1.05,13.11,12.99,15.36,14.43,1.65-2.06,14.64-13.4,14.64-14.43V112.59c0-1.1-2.44-1.43-3.45-1.55-8.42-.99-18.1.83-26.55,1.05ZM858.27,438.09c-20.14-166.94-158.46-303.59-325-324v324h325ZM437.27,115.09c-21.65,1.62-44.22,7.07-65,13.5-137.62,42.61-242.41,165.92-260,309.5h325V115.09ZM109.27,500.09h338.5c1,0,12.36-12.44,13.54-14.5,0-1-12.54-14.5-13.54-14.5H109.27c-1.81,9.94-.24,19.09,0,29ZM861.27,471.09h-338.5c-1,0-13.53,13.5-13.54,14.5.52,2.76,3.61,3.57,5.57,5.47,1.64,1.59,7.37,9.03,7.97,9.03h338.5c.9-9.78.9-19.22,0-29ZM500.27,860.09v-336.5c0-1.03-12.98-12.37-14.64-14.43-2.26,1.44-15.36,13.37-15.36,14.43v336.5h30ZM437.27,533.09H112.27c19.95,166.03,159.46,303.1,325,323v-323ZM858.27,533.09h-325v324c165.97-22.51,304.48-156.63,325-324Z"/><path fill="var(--accent, #1c6fd4)" d="M858.27,438.09h-325V114.09c166.54,20.41,304.86,157.06,325,324Z"/><path fill="var(--accent, #1c6fd4)" d="M858.27,533.09c-20.52,167.37-159.03,301.49-325,324v-324h325Z"/><path fill="var(--accent, #1c6fd4)" d="M437.27,533.09v323c-165.54-19.9-305.05-156.97-325-323h325Z"/><path fill="var(--accent, #1c6fd4)" d="M437.27,115.09v323H112.27c17.6-143.57,122.38-266.89,260-309.5,20.78-6.43,43.34-11.88,65-13.5ZM324.93,239.42c-5.22-5.22-14.48-5.8-20.62-1.79-23.51,27.2-56.28,50.77-79.07,77.93-3.21,3.83-6.62,7.81-7,13.05-.97,13.35,13.8,21.88,24.98,14.93,28.09-28.84,58.16-56.14,84.78-86.22,2.71-5.58,1.25-13.58-3.07-17.9ZM375.06,280.3c-4.15.41-7.65,2.76-10.8,5.27-2.42,1.93-17.06,17.64-18.19,19.81-7.64,14.76,7.17,29.85,21.55,22.56,2.83-1.44,19.3-18.18,21.64-21.36,8.21-11.17-.52-27.62-14.2-26.28ZM310.03,344.3c-2.88.42-6.45,2.49-8.78,4.26-4.52,3.45-32.55,31.28-34.83,35.17-8.84,15.07,8.47,31.23,23.59,21.59,4.35-2.77,34.16-32.7,36.28-36.72,6.36-12.04-2.95-26.26-16.27-24.31Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M500.27,860.09h-30v-336.5c0-1.05,13.11-12.99,15.36-14.43,1.65,2.06,14.64,13.4,14.64,14.43v336.5Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M470.27,112.09c8.45-.22,18.13-2.04,26.55-1.05,1.01.12,3.45.46,3.45,1.55v335c0,1.03-12.98,12.37-14.64,14.43-2.26-1.44-15.36-13.37-15.36-14.43V112.09Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M109.27,500.09c-.24-9.91-1.81-19.06,0-29h338.5c1,0,13.54,13.5,13.54,14.5-1.17,2.06-12.54,14.5-13.54,14.5H109.27Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M861.27,471.09c.9,9.78.9,19.22,0,29h-338.5c-.6,0-6.34-7.44-7.97-9.03-1.96-1.9-5.04-2.72-5.57-5.47,0-1,12.54-14.5,13.54-14.5h338.5Z"/><path fill="currentColor" d="M324.93,239.42c4.32,4.32,5.78,12.32,3.07,17.9-26.62,30.07-56.7,57.37-84.78,86.22-11.18,6.94-25.95-1.58-24.98-14.93.38-5.24,3.79-9.22,7-13.05,22.8-27.16,55.56-50.73,79.07-77.93,6.13-4.01,15.39-3.43,20.62,1.79Z"/><path fill="currentColor" d="M310.03,344.3c13.33-1.95,22.63,12.27,16.27,24.31-2.12,4.02-31.93,33.94-36.28,36.72-15.12,9.64-32.44-6.52-23.59-21.59,2.28-3.89,30.31-31.72,34.83-35.17,2.33-1.78,5.9-3.84,8.78-4.26Z"/><path fill="currentColor" d="M375.06,280.3c13.69-1.35,22.42,15.11,14.2,26.28-2.34,3.18-18.81,19.93-21.64,21.36-14.39,7.3-29.19-7.8-21.55-22.56,1.13-2.18,15.76-17.88,18.19-19.81,3.16-2.51,6.65-4.86,10.8-5.27Z"/></svg></a>
33
+ <a href="https://svelte.dev" target="_blank" rel="noreferrer"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos logo svelte" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg></a>
118
34
  </div>
119
- <pre class="result" class:filled={fileOut !== "—"}>{fileOut}</pre>
120
- </section>
121
- </div>
35
+ <p class="hint">Click on the janela, Vite, and Svelte logos to learn more.</p>
122
36
 
123
- <footer>
124
- <span>
125
- Edit <code>src/App.svelte</code> for the page and <code>src-host/main.ts</code> for the
126
- commands.
127
- </span>
128
- <button onclick={() => client.invoke("quit")}>Quit</button>
129
- </footer>
37
+ <form class="row" onsubmit={(e) => { e.preventDefault(); greet(); }}>
38
+ <input id="greet-input" bind:value={name} placeholder="Enter a name..." aria-label="a name to greet" />
39
+ <button type="submit">Greet</button>
40
+ </form>
41
+ <p class="greeting">{greeting}</p>
130
42
  </main>
@@ -1,137 +1,91 @@
1
- /* The janela starter's look. Shared by every template so a project scaffolded
2
- with --template vue looks the same as one scaffolded without a framework.
3
- All of it is yours to replace.
4
-
5
- The window mark in App is drawn in CSS rather than shipped as an image: a
6
- janela frontend is embedded in the binary, so every asset has to be inlined
7
- anyway, and a window needs no artwork. */
1
+ /* The look of create-tauri-app's starter, in janela's own tokens: a centred
2
+ column, the logos with a hover glow in each project's colour, and one round
3
+ trip you can drive. Shared by every template, so a project scaffolded with
4
+ --template vue looks the same as one scaffolded without a framework. All of
5
+ it is yours to replace. */
8
6
 
9
7
  :root {
10
8
  color-scheme: light dark;
11
9
  --bg: #f6f6f7;
12
- --card: #ffffff;
13
- --text: #18181b;
14
- --muted: #71717a;
15
- --line: #e4e4e7;
16
- --accent: #2563eb;
17
- --accent-soft: #eff4ff;
18
- --shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(0 0 0 / 0.12);
10
+ --ink: #18181b;
11
+ --ink-dim: #71717a;
12
+ --field: #ffffff;
13
+ --accent: #1c6fd4;
14
+ --shadow: 0 2px 2px rgb(0 0 0 / 0.08);
19
15
  }
20
16
  @media (prefers-color-scheme: dark) {
21
17
  :root {
22
18
  --bg: #17171a;
23
- --card: #202024;
24
- --text: #fafafa;
25
- --muted: #a1a1aa;
26
- --line: #2e2e34;
27
- --accent: #60a5fa;
28
- --accent-soft: #1d2433;
29
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -12px rgb(0 0 0 / 0.5);
19
+ --ink: #fafafa;
20
+ --ink-dim: #a1a1aa;
21
+ --field: #0f0f11;
22
+ --accent: #58a6ff;
23
+ --shadow: 0 2px 2px rgb(0 0 0 / 0.3);
30
24
  }
31
25
  }
32
26
 
33
27
  * { box-sizing: border-box; }
34
28
  body {
35
29
  margin: 0;
36
- min-height: 100vh;
37
- display: flex;
38
- justify-content: center;
39
30
  background: var(--bg);
40
- color: var(--text);
41
- font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
31
+ color: var(--ink);
32
+ font: 16px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
33
+ text-rendering: optimizeLegibility;
42
34
  -webkit-font-smoothing: antialiased;
43
35
  }
44
- main { width: 100%; max-width: 760px; padding: 26px 22px 32px; }
45
-
46
- /* --- header ------------------------------------------------------- */
47
- header { text-align: center; margin-bottom: 20px; }
48
- .mark {
49
- width: 56px; height: 44px; margin: 0 auto 14px;
50
- border: 2px solid var(--text); border-radius: 8px;
51
- display: flex; flex-direction: column; overflow: hidden;
52
- }
53
- .mark .bar {
54
- height: 13px; flex: none;
55
- border-bottom: 2px solid var(--text);
56
- display: flex; align-items: center; gap: 3px; padding: 0 4px;
57
- }
58
- .mark .bar i { width: 3px; height: 3px; border-radius: 50%; background: var(--text); }
59
- .mark .pane { flex: 1; background: linear-gradient(135deg, var(--accent), transparent 70%); opacity: 0.85; }
60
- h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; font-weight: 650; }
61
- .sub { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
62
36
 
63
- /* The one line that proves the whole stack: this string was produced by
64
- TypeScript compiled to machine code, and handed to the page. */
65
- .greeting {
66
- margin: 0 0 20px;
67
- padding: 11px 16px;
68
- border: 1px solid var(--line);
69
- border-radius: 10px;
70
- background: var(--accent-soft);
71
- font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
37
+ .container {
38
+ padding: 10vh 20px 40px;
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
72
42
  text-align: center;
73
- overflow-wrap: anywhere;
74
43
  }
44
+ h1 { margin: 0; font-size: 2em; letter-spacing: -0.02em; }
45
+ .hint { color: var(--ink-dim); margin: 4px 0 18px; }
75
46
 
76
- /* --- cards -------------------------------------------------------- */
77
- /* auto-fit rather than a fixed column count: the same page is the iOS and
78
- Android frontend, where it must read as a single column. */
79
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 13px; }
80
- .card {
81
- background: var(--card);
82
- border: 1px solid var(--line);
83
- border-radius: 12px;
84
- padding: 15px 17px;
85
- box-shadow: var(--shadow);
86
- }
87
- .card h2 {
88
- margin: 0 0 2px;
89
- font-size: 13px; font-weight: 600;
90
- letter-spacing: 0.04em; text-transform: uppercase;
91
- color: var(--muted);
47
+ .row { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; }
48
+ .logos { margin: 26px 0 8px; }
49
+ .logos a { display: block; }
50
+ .logo {
51
+ /* content-box on purpose: the page sets border-box globally, which
52
+ would make 6em include the padding and shrink the mark by half. */
53
+ box-sizing: content-box;
54
+ height: 6em; padding: 1.5em;
55
+ will-change: filter; transition: filter 0.75s;
92
56
  }
93
- .card.wide { grid-column: 1 / -1; }
94
- .card p.why { margin: 0 0 11px; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
95
- .card h3 {
96
- margin: 16px 0 6px; font-size: 11px; font-weight: 600;
97
- letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
98
- }
99
- .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
57
+ /* Each glow is the project's own colour — the same idea as Tauri's
58
+ starter, which is where the shape of this page comes from. */
59
+ /* The mark's frame is currentColor by design, so it follows the text.
60
+ Inside the link it would follow the LINK colour instead — hence the
61
+ explicit ink here. */
62
+ .logo.janela { color: var(--ink); }
63
+ .logo.janela:hover { filter: drop-shadow(0 0 2em var(--accent)); }
64
+ .logo.vite:hover { filter: drop-shadow(0 0 2em #747bff); }
65
+ .logo.vue:hover { filter: drop-shadow(0 0 2em #42b883); }
66
+ .logo.react:hover { filter: drop-shadow(0 0 2em #61dafb); }
67
+ .logo.svelte:hover { filter: drop-shadow(0 0 2em #ff3e00); }
68
+ .logo.solid:hover { filter: drop-shadow(0 0 2em #4f88c6); }
69
+ @media (prefers-reduced-motion: reduce) { .logo { transition: none; } }
70
+
71
+ a { color: var(--accent); text-decoration: none; font-weight: 500; }
72
+ a:hover { text-decoration: underline; }
100
73
 
101
74
  input, button {
102
- font: inherit;
103
75
  border-radius: 8px;
104
- border: 1px solid var(--line);
105
- padding: 8px 12px;
106
- background: var(--card);
107
- color: var(--text);
108
- transition: border-color 0.15s, background 0.15s;
76
+ border: 1px solid transparent;
77
+ padding: 0.6em 1.2em;
78
+ font: inherit;
79
+ font-weight: 500;
80
+ color: var(--ink);
81
+ background: var(--field);
82
+ box-shadow: var(--shadow);
83
+ transition: border-color 0.25s;
109
84
  }
110
- input { min-width: 0; }
111
- input[type="number"] { width: 5.5em; text-align: center; }
112
- input.grow { flex: 1 1 12em; }
85
+ button { cursor: pointer; }
86
+ button:hover { border-color: var(--accent); }
87
+ button:active { border-color: var(--accent); filter: brightness(0.97); }
113
88
  input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
114
- button { cursor: pointer; font-weight: 500; }
115
- button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
116
- button:disabled { opacity: 0.5; cursor: default; }
117
- button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
118
- button.primary:hover:not(:disabled) { filter: brightness(1.08); color: #fff; }
119
-
120
- .result { margin: 12px 0 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
121
- .result.filled { color: var(--text); }
122
- pre.result { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 11em; overflow: auto; margin-top: 12px; }
123
-
124
- /* A dot that keeps moving while a 2s command is pending. If it stalls,
125
- the host has blocked the UI thread — which is the bug this proves is
126
- absent. */
127
- .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
128
- @keyframes pulse { 0%, 100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
129
- @media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 1; } }
130
-
131
- ul.events { list-style: none; margin: 0; padding: 0; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
132
- ul.events li { padding: 3px 0; border-bottom: 1px dashed var(--line); }
133
- ul.events li:last-child { border-bottom: 0; }
134
- ul.events:empty::after { content: "nothing yet — press add"; color: var(--muted); font-family: inherit; }
135
89
 
136
- footer { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
137
- footer code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; font-size: 12px; }
90
+ /* Reserved so the layout does not jump when the answer arrives. */
91
+ .greeting { min-height: 1.5em; margin: 20px 0 0; }
@@ -1,10 +1,10 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // The two tables below are the single declaration of what this app exposes,
4
- // and `App` names an app that carries them. The frontend imports `App` with
3
+ // The table below is the single declaration of what this app exposes, and
4
+ // `App` names an app that carries it. The frontend imports `App` with
5
5
  // `import type`, so the page is checked against these exact types — command
6
- // names, argument shapes, results and event payloads — with no code
7
- // generation and nothing to keep in sync.
6
+ // names, argument shapes and results — with no code generation and nothing to
7
+ // keep in sync.
8
8
  //
9
9
  // A command that returns nothing is declared `() => void` and its handler
10
10
  // returns `null`; every command answers the page's promise with a value.
@@ -13,131 +13,30 @@ import type { JanelaApp } from "janela/host";
13
13
 
14
14
  /** Every command this app answers. Declared once; the page checks against it. */
15
15
  export type AppCommands = {
16
- add: (args: { a: number; b: number }) => number;
17
16
  greet: (args: { name: string }) => string;
18
17
  log: (args: string) => void;
19
- wait: (args: { ms: number }) => string;
20
- readFile: (args: { path: string }) => ReadResult;
21
- openFile: () => PickResult;
22
- setTitle: (args: { title: string }) => void;
23
- quit: () => void;
24
18
  };
25
19
 
26
20
  /**
27
- * Results are declared as TOTAL records rather than unions of shapes: every
28
- * field is always present, and `error` is null on success. A discriminated
29
- * union would read better in isolation, but the page would then have to narrow
30
- * before touching `text` — and the point of this template is to show the type
31
- * edge working, not to teach narrowing.
21
+ * Every event this app emits, and what each one carries. Empty to start with —
22
+ * add one and `app.emit` is checked against it, as is `client.on` in the page.
32
23
  */
33
- export type ReadResult = { ok: boolean; error: string | null; length: number; text: string };
34
- export type PickResult = {
35
- ok: boolean;
36
- error: string | null;
37
- cancelled: boolean;
38
- path: string;
39
- length: number;
40
- text: string;
41
- };
42
-
43
- /** Every event this app emits, and what each one carries. */
44
- export type AppEvents = {
45
- added: number;
46
- };
24
+ export type AppEvents = {};
47
25
 
48
26
  /** The contract the page imports with `import type { App } from "../src-host/main"`. */
49
27
  export type App = JanelaApp<AppCommands, AppEvents>;
50
28
 
51
29
  // Typing the app with the contract is what makes `app.command` checked: the
52
30
  // name must be one of the declared ones, `args` is inferred from it, and the
53
- // return value has to match. Same for `app.emit`.
31
+ // return value has to match.
54
32
  export function setup(app: App): void {
55
- app.command("add", (args) => {
56
- const sum = args.a + args.b;
57
- app.emit("added", sum);
58
- return sum;
59
- });
60
-
61
33
  app.command("greet", (args) => {
62
- return "Hello, " + args.name + " — from the native TS binary";
34
+ return "Hello, " + args.name + "! You've been greeted from a native TypeScript binary.";
63
35
  });
64
36
 
37
+ // Anything the host logs goes to the terminal that `janela dev` runs in.
65
38
  app.command("log", (args) => {
66
39
  console.log("[host] page says:", args);
67
40
  return null;
68
41
  });
69
-
70
- // An async command: answers later, without freezing the window.
71
- app.commandAsync("wait", (args, resolve) => {
72
- app.sleep(args.ms, () => {
73
- resolve("waited " + args.ms + "ms without blocking the UI");
74
- });
75
- });
76
-
77
- // File I/O without freezing the window: the read happens on a worker thread
78
- // in the shim. Use this instead of node:fs readFileSync, which blocks the
79
- // host loop — and with it the whole UI — until the syscall returns.
80
- app.commandAsync("readFile", (args, resolve) => {
81
- app.readFileAsync(args.path, (err, text) => {
82
- // Annotated, not inferred: a bare `null` in a returned literal infers as
83
- // `null | undefined`, which does not lift into the declared
84
- // `string | null` (scriptc SC2002).
85
- const answer: ReadResult =
86
- err !== null
87
- ? { ok: false, error: err, length: 0, text: "" }
88
- : { ok: true, error: null, length: text.length, text: text };
89
- resolve(answer);
90
- });
91
- });
92
-
93
- // The native "open" dialog, paired with the reader above — picking a file is
94
- // what makes readFileAsync useful. commandAsync is the right shape here: the
95
- // page's promise stays parked while the user takes as long as they like, and
96
- // the window carries on serving other calls meanwhile.
97
- app.commandAsync("openFile", (_args, resolve) => {
98
- app.openFileDialog(
99
- { title: "Pick a file", filters: [{ name: "Text", extensions: ["txt", "md"] }] },
100
- (paths, err) => {
101
- if (err !== undefined) {
102
- const failed: PickResult = {
103
- ok: false, error: err, cancelled: false, path: "", length: 0, text: "",
104
- };
105
- resolve(failed);
106
- return;
107
- }
108
- if (paths === null) {
109
- const cancelled: PickResult = {
110
- ok: true, error: null, cancelled: true, path: "", length: 0, text: "",
111
- };
112
- resolve(cancelled);
113
- return;
114
- }
115
- app.readFileAsync(paths[0], (rerr, text) => {
116
- const answer: PickResult =
117
- rerr !== null
118
- ? { ok: false, error: rerr, cancelled: false, path: paths[0], length: 0, text: "" }
119
- : {
120
- ok: true,
121
- error: null,
122
- cancelled: false,
123
- path: paths[0],
124
- length: text.length,
125
- text: text,
126
- };
127
- resolve(answer);
128
- });
129
- },
130
- );
131
- });
132
-
133
- // The window is yours to change at runtime, not just at startup.
134
- app.command("setTitle", (args) => {
135
- app.setTitle(args.title);
136
- return null;
137
- });
138
-
139
- app.command("quit", (_args) => {
140
- app.quit();
141
- return null;
142
- });
143
42
  }