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.
- package/README.md +19 -6
- package/package.json +1 -1
- package/templates/index.html +89 -209
- package/templates/main.ts +6 -68
- package/templates/react/files/src/App.tsx +44 -155
- package/templates/react/files/src/styles.css +63 -109
- package/templates/react/files/src-host/main.ts +10 -111
- package/templates/solid/files/src/App.tsx +39 -148
- package/templates/solid/files/src/styles.css +63 -109
- package/templates/solid/files/src-host/main.ts +10 -111
- package/templates/svelte/files/src/App.svelte +25 -113
- package/templates/svelte/files/src/styles.css +63 -109
- package/templates/svelte/files/src-host/main.ts +10 -111
- package/templates/vue/files/src/App.vue +25 -107
- package/templates/vue/files/src/styles.css +63 -109
- package/templates/vue/files/src-host/main.ts +10 -111
|
@@ -1,172 +1,61 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { createClient } from "janela/api";
|
|
3
|
-
// A type-only import of the host's contract
|
|
4
|
-
//
|
|
3
|
+
// A type-only import of the host's contract: command names, argument shapes
|
|
4
|
+
// and results are checked against it, with no code generation, because both
|
|
5
|
+
// sides are TypeScript and this import erases at compile time.
|
|
5
6
|
import type { App as HostApp } from "../src-host/main";
|
|
6
7
|
|
|
7
8
|
const client = createClient<HostApp>();
|
|
8
9
|
|
|
9
10
|
export default function App() {
|
|
10
|
-
const [
|
|
11
|
-
const [
|
|
12
|
-
const [b, setB] = useState(40);
|
|
13
|
-
const [sum, setSum] = useState("—");
|
|
14
|
-
const [waited, setWaited] = useState("—");
|
|
15
|
-
const [waiting, setWaiting] = useState(false);
|
|
16
|
-
const [events, setEvents] = useState<string[]>([]);
|
|
17
|
-
const [path, setPath] = useState("janela.conf.json");
|
|
18
|
-
const [title, setTitle] = useState("__NAME__ — renamed");
|
|
19
|
-
const [fileOut, setFileOut] = useState("—");
|
|
11
|
+
const [name, setName] = useState("");
|
|
12
|
+
const [greeting, setGreeting] = useState("");
|
|
20
13
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
// `greet` is a command in src-host/main.ts, compiled to machine code.
|
|
15
|
+
async function greet(who: string) {
|
|
16
|
+
setGreeting(await client.invoke("greet", { name: who || "__NAME__" }));
|
|
17
|
+
}
|
|
24
18
|
|
|
19
|
+
// StrictMode runs effects twice in development; the guard keeps the greeting
|
|
20
|
+
// from being fetched twice on mount.
|
|
21
|
+
const started = useRef(false);
|
|
25
22
|
useEffect(() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
if (!started.current) {
|
|
32
|
-
started.current = true;
|
|
33
|
-
client.invoke("greet", { name: "__NAME__" }).then(setGreeting);
|
|
34
|
-
client.invoke("log", "page loaded");
|
|
35
|
-
}
|
|
36
|
-
return off;
|
|
23
|
+
if (started.current) return;
|
|
24
|
+
started.current = true;
|
|
25
|
+
// Greeted once on load, so the round trip is visible without a click.
|
|
26
|
+
greet("");
|
|
27
|
+
client.invoke("log", "page loaded");
|
|
37
28
|
}, []);
|
|
38
29
|
|
|
39
|
-
async function add() {
|
|
40
|
-
setSum(`add(${a}, ${b}) → ${await client.invoke("add", { a, b })}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function wait() {
|
|
44
|
-
setWaiting(true);
|
|
45
|
-
setWaited("waiting… try add, it still answers");
|
|
46
|
-
setWaited(await client.invoke("wait", { ms: 2000 }));
|
|
47
|
-
setWaiting(false);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function read() {
|
|
51
|
-
const r = await client.invoke("readFile", { path });
|
|
52
|
-
setFileOut(r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : `error: ${r.error}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// The native open dialog. The window keeps serving other commands while the
|
|
56
|
-
// user is deciding.
|
|
57
|
-
async function pick() {
|
|
58
|
-
const r = await client.invoke("openFile");
|
|
59
|
-
if (!r.ok) setFileOut(`error: ${r.error}`);
|
|
60
|
-
else if (r.cancelled) setFileOut("cancelled");
|
|
61
|
-
else setFileOut(`${r.path} — ${r.length} chars\n\n${r.text.slice(0, 400)}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
30
|
return (
|
|
65
|
-
<main>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<div className="pane" />
|
|
74
|
-
</div>
|
|
75
|
-
<h1>__NAME__</h1>
|
|
76
|
-
<p className="sub">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
|
|
77
|
-
</header>
|
|
78
|
-
|
|
79
|
-
<p className="greeting">{greeting}</p>
|
|
80
|
-
|
|
81
|
-
<div className="grid">
|
|
82
|
-
<section className="card">
|
|
83
|
-
<h2>Typed commands</h2>
|
|
84
|
-
<p className="why">
|
|
85
|
-
The name, the arguments and the result are all checked at compile time — no codegen,
|
|
86
|
-
because both sides are TypeScript.
|
|
87
|
-
</p>
|
|
88
|
-
<div className="row">
|
|
89
|
-
<input
|
|
90
|
-
type="number"
|
|
91
|
-
value={a}
|
|
92
|
-
aria-label="first number"
|
|
93
|
-
onChange={(e) => setA(Number(e.target.value))}
|
|
94
|
-
/>
|
|
95
|
-
<span aria-hidden="true">+</span>
|
|
96
|
-
<input
|
|
97
|
-
type="number"
|
|
98
|
-
value={b}
|
|
99
|
-
aria-label="second number"
|
|
100
|
-
onChange={(e) => setB(Number(e.target.value))}
|
|
101
|
-
/>
|
|
102
|
-
<button className="primary" onClick={add}>
|
|
103
|
-
add
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
<p className={sum === "—" ? "result" : "result filled"}>{sum}</p>
|
|
107
|
-
<h3>Events from the host</h3>
|
|
108
|
-
<ul className="events">
|
|
109
|
-
{events.map((e, i) => (
|
|
110
|
-
<li key={i}>{e}</li>
|
|
111
|
-
))}
|
|
112
|
-
</ul>
|
|
113
|
-
</section>
|
|
114
|
-
|
|
115
|
-
<section className="card">
|
|
116
|
-
<h2>Async without blocking</h2>
|
|
117
|
-
<p className="why">
|
|
118
|
-
The window keeps answering while this is pending: the dot keeps moving and{" "}
|
|
119
|
-
<strong>add</strong> still works.
|
|
120
|
-
</p>
|
|
121
|
-
<div className="row">
|
|
122
|
-
<button disabled={waiting} onClick={wait}>
|
|
123
|
-
wait 2s
|
|
124
|
-
</button>
|
|
125
|
-
{waiting && (
|
|
126
|
-
<span>
|
|
127
|
-
<span className="pulse" />
|
|
128
|
-
</span>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
<p className={waited === "—" ? "result" : "result filled"}>{waited}</p>
|
|
132
|
-
</section>
|
|
133
|
-
|
|
134
|
-
<section className="card wide">
|
|
135
|
-
<h2>Files and the window</h2>
|
|
136
|
-
<p className="why">
|
|
137
|
-
Reads run on a worker thread, so a large file never freezes the window. The dialog is
|
|
138
|
-
the real native one.
|
|
139
|
-
</p>
|
|
140
|
-
<div className="row">
|
|
141
|
-
<input
|
|
142
|
-
className="grow"
|
|
143
|
-
type="text"
|
|
144
|
-
value={path}
|
|
145
|
-
aria-label="file to read"
|
|
146
|
-
onChange={(e) => setPath(e.target.value)}
|
|
147
|
-
/>
|
|
148
|
-
<button onClick={read}>read</button>
|
|
149
|
-
<button onClick={pick}>pick a file…</button>
|
|
150
|
-
<input
|
|
151
|
-
className="grow"
|
|
152
|
-
type="text"
|
|
153
|
-
value={title}
|
|
154
|
-
aria-label="window title"
|
|
155
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
156
|
-
/>
|
|
157
|
-
<button onClick={() => client.invoke("setTitle", { title })}>set title</button>
|
|
158
|
-
</div>
|
|
159
|
-
<pre className={fileOut === "—" ? "result" : "result filled"}>{fileOut}</pre>
|
|
160
|
-
</section>
|
|
31
|
+
<main className="container">
|
|
32
|
+
<h1>Welcome to janela + React</h1>
|
|
33
|
+
<p className="hint">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
|
|
34
|
+
|
|
35
|
+
<div className="row logos">
|
|
36
|
+
<a href="https://vite.dev" target="_blank" rel="noreferrer"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" className="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>
|
|
37
|
+
<a href="https://mmamedel.github.io/janela/" target="_blank" rel="noreferrer"><svg className="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>
|
|
38
|
+
<a href="https://react.dev" target="_blank" rel="noreferrer"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" className="iconify iconify--logos logo react" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg></a>
|
|
161
39
|
</div>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
40
|
+
<p className="hint">Click on the janela, Vite, and React logos to learn more.</p>
|
|
41
|
+
|
|
42
|
+
<form
|
|
43
|
+
className="row"
|
|
44
|
+
onSubmit={(e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
greet(name);
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
id="greet-input"
|
|
51
|
+
value={name}
|
|
52
|
+
placeholder="Enter a name..."
|
|
53
|
+
aria-label="a name to greet"
|
|
54
|
+
onChange={(e) => setName(e.currentTarget.value)}
|
|
55
|
+
/>
|
|
56
|
+
<button type="submit">Greet</button>
|
|
57
|
+
</form>
|
|
58
|
+
<p className="greeting">{greeting}</p>
|
|
170
59
|
</main>
|
|
171
60
|
);
|
|
172
61
|
}
|
|
@@ -1,137 +1,91 @@
|
|
|
1
|
-
/* The
|
|
2
|
-
with
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
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
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
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(--
|
|
41
|
-
font:
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
.
|
|
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
|
|
105
|
-
padding:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
4
|
-
//
|
|
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
|
|
7
|
-
//
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
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.
|
|
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 + "
|
|
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
|
}
|