janela 0.15.0 → 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 +9 -5
- package/package.json +1 -1
- package/templates/index.html +87 -198
- package/templates/main.ts +6 -68
- package/templates/react/files/src/App.tsx +44 -148
- package/templates/react/files/src/styles.css +61 -99
- package/templates/react/files/src-host/main.ts +10 -111
- package/templates/solid/files/src/App.tsx +39 -141
- package/templates/solid/files/src/styles.css +61 -99
- package/templates/solid/files/src-host/main.ts +10 -111
- package/templates/svelte/files/src/App.svelte +25 -110
- package/templates/svelte/files/src/styles.css +61 -99
- package/templates/svelte/files/src-host/main.ts +10 -111
- package/templates/vue/files/src/App.vue +27 -106
- package/templates/vue/files/src/styles.css +61 -99
- package/templates/vue/files/src-host/main.ts +10 -111
package/README.md
CHANGED
|
@@ -11,11 +11,12 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
|
|
|
11
11
|
Node, no Electron. The backend is TypeScript compiled to a native binary by
|
|
12
12
|
[scriptc](https://scriptc.dev); the window is the OS webview via
|
|
13
13
|
[webview/webview](https://github.com/webview/webview). A desktop binary comes
|
|
14
|
-
out around
|
|
14
|
+
out around 190–390 KB — 191 KB for the smallest template — with no bundled
|
|
15
15
|
browser and no bundled runtime; iOS and Android bundles land around
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
195–388 KB. Those are the *starter's* figures: it calls one command, so it
|
|
17
|
+
links almost nothing beyond the runtime. Reaching for a platform API adds its
|
|
18
|
+
code — the native file dialog and the worker-thread reader together are about
|
|
19
|
+
35 KB — which is the shape worth remembering: what you pay is what you call. Per-template figures are in
|
|
19
20
|
[docs/frontend.md](../../docs/frontend.md).
|
|
20
21
|
|
|
21
22
|
Five targets, one runtime — the same `main.ts`, the same typed contract and the
|
|
@@ -672,4 +673,7 @@ gh release create v<version> --generate-notes --verify-tag
|
|
|
672
673
|
## License
|
|
673
674
|
|
|
674
675
|
MIT. Bundles [webview/webview](https://github.com/webview/webview) headers
|
|
675
|
-
(MIT, © Serge Zaitsev and contributors).
|
|
676
|
+
(MIT, © Serge Zaitsev and contributors). The starter page takes its shape from
|
|
677
|
+
[create-tauri-app](https://github.com/tauri-apps/create-tauri-app) (MIT), and
|
|
678
|
+
the Vite, Vue, React, Svelte and Solid marks it inlines are each project's
|
|
679
|
+
own.
|
package/package.json
CHANGED
package/templates/index.html
CHANGED
|
@@ -5,248 +5,137 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>__NAME__</title>
|
|
7
7
|
<style>
|
|
8
|
-
/*
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/* The look of create-tauri-app's starter, in janela's own tokens: a
|
|
9
|
+
centred column, the logos with a hover glow in each project's colour,
|
|
10
|
+
and one round trip you can drive. All of it is yours to replace. */
|
|
11
11
|
:root {
|
|
12
12
|
color-scheme: light dark;
|
|
13
13
|
--bg: #f6f6f7;
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--line: #e4e4e7;
|
|
14
|
+
--ink: #18181b;
|
|
15
|
+
--ink-dim: #71717a;
|
|
16
|
+
--field: #ffffff;
|
|
18
17
|
--accent: #1c6fd4;
|
|
19
|
-
--
|
|
20
|
-
--shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(0 0 0 / 0.12);
|
|
18
|
+
--shadow: 0 2px 2px rgb(0 0 0 / 0.08);
|
|
21
19
|
}
|
|
22
20
|
@media (prefers-color-scheme: dark) {
|
|
23
21
|
:root {
|
|
24
22
|
--bg: #17171a;
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--line: #2e2e34;
|
|
23
|
+
--ink: #fafafa;
|
|
24
|
+
--ink-dim: #a1a1aa;
|
|
25
|
+
--field: #0f0f11;
|
|
29
26
|
--accent: #58a6ff;
|
|
30
|
-
--
|
|
31
|
-
--shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -12px rgb(0 0 0 / 0.5);
|
|
27
|
+
--shadow: 0 2px 2px rgb(0 0 0 / 0.3);
|
|
32
28
|
}
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
* { box-sizing: border-box; }
|
|
36
32
|
body {
|
|
37
33
|
margin: 0;
|
|
38
|
-
min-height: 100vh;
|
|
39
|
-
display: flex;
|
|
40
|
-
justify-content: center;
|
|
41
34
|
background: var(--bg);
|
|
42
|
-
color: var(--
|
|
43
|
-
font:
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
font: 16px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
37
|
+
text-rendering: optimizeLegibility;
|
|
44
38
|
-webkit-font-smoothing: antialiased;
|
|
45
39
|
}
|
|
46
|
-
main { width: 100%; max-width: 760px; padding: 26px 22px 32px; }
|
|
47
|
-
|
|
48
|
-
/* --- header ------------------------------------------------------- */
|
|
49
|
-
header { text-align: center; margin-bottom: 20px; }
|
|
50
|
-
/* The mark: a round window, which is what janela means. Inlined rather
|
|
51
|
-
than linked, because a janela frontend is embedded in the binary and
|
|
52
|
-
every asset has to be inlined anyway — and inline rather than a CSS
|
|
53
|
-
data URI so ONE copy serves both themes: the frame follows the text
|
|
54
|
-
colour, the glass follows the accent. Percent-encoding a data URI
|
|
55
|
-
would also have cost 4x the bytes. Delete it and the header still
|
|
56
|
-
works. */
|
|
57
|
-
.mark { display: block; width: 56px; height: 56px; margin: 0 auto 14px; }
|
|
58
|
-
h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; font-weight: 650; }
|
|
59
|
-
.sub { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
|
|
60
40
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
border: 1px solid var(--line);
|
|
67
|
-
border-radius: 10px;
|
|
68
|
-
background: var(--accent-soft);
|
|
69
|
-
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
41
|
+
.container {
|
|
42
|
+
padding: 10vh 20px 40px;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
70
46
|
text-align: center;
|
|
71
|
-
overflow-wrap: anywhere;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* --- cards -------------------------------------------------------- */
|
|
75
|
-
/* auto-fit rather than a fixed column count: the same page is the iOS and
|
|
76
|
-
Android frontend, where it must read as a single column. */
|
|
77
|
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 13px; }
|
|
78
|
-
.card {
|
|
79
|
-
background: var(--card);
|
|
80
|
-
border: 1px solid var(--line);
|
|
81
|
-
border-radius: 12px;
|
|
82
|
-
padding: 15px 17px;
|
|
83
|
-
box-shadow: var(--shadow);
|
|
84
|
-
}
|
|
85
|
-
.card h2 {
|
|
86
|
-
margin: 0 0 2px;
|
|
87
|
-
font-size: 13px; font-weight: 600;
|
|
88
|
-
letter-spacing: 0.04em; text-transform: uppercase;
|
|
89
|
-
color: var(--muted);
|
|
90
47
|
}
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
48
|
+
h1 { margin: 0; font-size: 2em; letter-spacing: -0.02em; }
|
|
49
|
+
.hint { color: var(--ink-dim); margin: 4px 0 18px; }
|
|
50
|
+
|
|
51
|
+
.row { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
52
|
+
.logos { margin: 26px 0 8px; }
|
|
53
|
+
.logos a { display: block; }
|
|
54
|
+
.logo {
|
|
55
|
+
/* content-box on purpose: the page sets border-box globally, which
|
|
56
|
+
would make 6em include the padding and shrink the mark by half. */
|
|
57
|
+
box-sizing: content-box;
|
|
58
|
+
height: 6em; padding: 1.5em;
|
|
59
|
+
will-change: filter; transition: filter 0.75s;
|
|
96
60
|
}
|
|
97
|
-
|
|
61
|
+
/* Each glow is the project's own colour — the same idea as Tauri's
|
|
62
|
+
starter, which is where the shape of this page comes from. */
|
|
63
|
+
/* The mark's frame is currentColor by design, so it follows the text.
|
|
64
|
+
Inside the link it would follow the LINK colour instead — hence the
|
|
65
|
+
explicit ink here. */
|
|
66
|
+
.logo.janela { color: var(--ink); }
|
|
67
|
+
.logo.janela:hover { filter: drop-shadow(0 0 2em var(--accent)); }
|
|
68
|
+
.logo.vite:hover { filter: drop-shadow(0 0 2em #747bff); }
|
|
69
|
+
.logo.vue:hover { filter: drop-shadow(0 0 2em #42b883); }
|
|
70
|
+
.logo.react:hover { filter: drop-shadow(0 0 2em #61dafb); }
|
|
71
|
+
.logo.svelte:hover { filter: drop-shadow(0 0 2em #ff3e00); }
|
|
72
|
+
.logo.solid:hover { filter: drop-shadow(0 0 2em #4f88c6); }
|
|
73
|
+
@media (prefers-reduced-motion: reduce) { .logo { transition: none; } }
|
|
74
|
+
|
|
75
|
+
a { color: var(--accent); text-decoration: none; font-weight: 500; }
|
|
76
|
+
a:hover { text-decoration: underline; }
|
|
98
77
|
|
|
99
78
|
input, button {
|
|
100
|
-
font: inherit;
|
|
101
79
|
border-radius: 8px;
|
|
102
|
-
border: 1px solid
|
|
103
|
-
padding:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
80
|
+
border: 1px solid transparent;
|
|
81
|
+
padding: 0.6em 1.2em;
|
|
82
|
+
font: inherit;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
color: var(--ink);
|
|
85
|
+
background: var(--field);
|
|
86
|
+
box-shadow: var(--shadow);
|
|
87
|
+
transition: border-color 0.25s;
|
|
107
88
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
button { cursor: pointer; }
|
|
90
|
+
button:hover { border-color: var(--accent); }
|
|
91
|
+
button:active { border-color: var(--accent); filter: brightness(0.97); }
|
|
111
92
|
input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
112
|
-
button { cursor: pointer; font-weight: 500; }
|
|
113
|
-
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
114
|
-
button:disabled { opacity: 0.5; cursor: default; }
|
|
115
|
-
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
116
|
-
button.primary:hover:not(:disabled) { filter: brightness(1.08); color: #fff; }
|
|
117
|
-
|
|
118
|
-
.result { margin: 12px 0 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
|
|
119
|
-
.result.filled { color: var(--text); }
|
|
120
|
-
pre.result { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 11em; overflow: auto; margin-top: 12px; }
|
|
121
|
-
|
|
122
|
-
/* A dot that keeps moving while a 2s command is pending. If it stalls,
|
|
123
|
-
the host has blocked the UI thread — which is the bug this proves is
|
|
124
|
-
absent. */
|
|
125
|
-
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
|
|
126
|
-
@keyframes pulse { 0%, 100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
|
|
127
|
-
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 1; } }
|
|
128
|
-
|
|
129
|
-
ul.events { list-style: none; margin: 0; padding: 0; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
130
|
-
ul.events li { padding: 3px 0; border-bottom: 1px dashed var(--line); }
|
|
131
|
-
ul.events li:last-child { border-bottom: 0; }
|
|
132
|
-
ul.events:empty::after { content: "nothing yet — press add"; color: var(--muted); font-family: inherit; }
|
|
133
|
-
|
|
134
|
-
footer { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
|
|
135
|
-
footer code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; font-size: 12px; }
|
|
136
93
|
|
|
94
|
+
/* Reserved so the layout does not jump when the answer arrives. */
|
|
95
|
+
.greeting { min-height: 1.5em; margin: 20px 0 0; }
|
|
137
96
|
</style>
|
|
138
97
|
</head>
|
|
139
98
|
<body>
|
|
140
|
-
<main>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
<div class="grid">
|
|
150
|
-
<section class="card">
|
|
151
|
-
<h2>Typed commands</h2>
|
|
152
|
-
<p class="why">The name, the arguments and the result are all checked at compile time — no codegen, because both sides are TypeScript.</p>
|
|
153
|
-
<div class="row">
|
|
154
|
-
<input id="a" type="number" value="2" aria-label="first number" />
|
|
155
|
-
<span aria-hidden="true">+</span>
|
|
156
|
-
<input id="b" type="number" value="40" aria-label="second number" />
|
|
157
|
-
<button id="add" class="primary">add</button>
|
|
158
|
-
</div>
|
|
159
|
-
<p class="result" id="sum">—</p>
|
|
160
|
-
<h3>Events from the host</h3>
|
|
161
|
-
<ul class="events" id="events"></ul>
|
|
162
|
-
</section>
|
|
163
|
-
|
|
164
|
-
<section class="card">
|
|
165
|
-
<h2>Async without blocking</h2>
|
|
166
|
-
<p class="why">The window keeps answering while this is pending: the dot keeps moving and <strong>add</strong> still works.</p>
|
|
167
|
-
<div class="row">
|
|
168
|
-
<button id="wait">wait 2s</button>
|
|
169
|
-
<span id="spinner" hidden><span class="pulse"></span></span>
|
|
170
|
-
</div>
|
|
171
|
-
<p class="result" id="waited">—</p>
|
|
172
|
-
</section>
|
|
173
|
-
|
|
174
|
-
<section class="card wide">
|
|
175
|
-
<h2>Files and the window</h2>
|
|
176
|
-
<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>
|
|
177
|
-
<div class="row">
|
|
178
|
-
<input id="path" class="grow" type="text" value="janela.conf.json" aria-label="file to read" />
|
|
179
|
-
<button id="read">read</button>
|
|
180
|
-
<button id="open">pick a file…</button>
|
|
181
|
-
<input id="title" class="grow" type="text" value="__NAME__ — renamed" aria-label="window title" />
|
|
182
|
-
<button id="retitle">set title</button>
|
|
183
|
-
</div>
|
|
184
|
-
<pre class="result" id="file">—</pre>
|
|
185
|
-
</section>
|
|
99
|
+
<main class="container">
|
|
100
|
+
<h1>Welcome to janela</h1>
|
|
101
|
+
<p class="hint">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
|
|
102
|
+
|
|
103
|
+
<div class="row logos">
|
|
104
|
+
<a href="https://mmamedel.github.io/janela/" target="_blank" rel="noreferrer">
|
|
105
|
+
<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>
|
|
106
|
+
</a>
|
|
186
107
|
</div>
|
|
108
|
+
<p class="hint">Click the janela logo to learn more.</p>
|
|
187
109
|
|
|
188
|
-
<
|
|
189
|
-
<
|
|
190
|
-
<button
|
|
191
|
-
</
|
|
110
|
+
<form class="row" id="greet-form">
|
|
111
|
+
<input id="greet-input" placeholder="Enter a name..." aria-label="a name to greet" />
|
|
112
|
+
<button type="submit">Greet</button>
|
|
113
|
+
</form>
|
|
114
|
+
<p class="greeting" id="greeting"></p>
|
|
192
115
|
</main>
|
|
193
116
|
|
|
194
117
|
<script>
|
|
195
118
|
// This template has no bundler, so it uses the `janela` global that the
|
|
196
119
|
// host injects before the page loads. Projects with a build step should
|
|
197
|
-
// `import { invoke
|
|
198
|
-
//
|
|
120
|
+
// `import { invoke } from "janela/api"` instead — same function, but
|
|
121
|
+
// typed and resolvable by the bundler.
|
|
199
122
|
window.onload = async () => {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
el.textContent = text;
|
|
203
|
-
el.classList.add("filled");
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
janela.listen("added", (sum) => {
|
|
207
|
-
const li = document.createElement("li");
|
|
208
|
-
li.textContent = "host emitted 'added': " + sum;
|
|
209
|
-
$("events").prepend(li);
|
|
210
|
-
});
|
|
123
|
+
const out = document.getElementById("greeting");
|
|
124
|
+
const input = document.getElementById("greet-input");
|
|
211
125
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await janela.invoke("log", "page loaded");
|
|
216
|
-
|
|
217
|
-
$("add").onclick = async () => {
|
|
218
|
-
const a = Number($("a").value);
|
|
219
|
-
const b = Number($("b").value);
|
|
220
|
-
show($("sum"), `add(${a}, ${b}) → ${await janela.invoke("add", { a, b })}`);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
$("wait").onclick = async (e) => {
|
|
224
|
-
e.target.disabled = true;
|
|
225
|
-
$("spinner").hidden = false;
|
|
226
|
-
show($("waited"), "waiting… try add, it still answers");
|
|
227
|
-
const answer = await janela.invoke("wait", { ms: 2000 });
|
|
228
|
-
show($("waited"), answer);
|
|
229
|
-
$("spinner").hidden = true;
|
|
230
|
-
e.target.disabled = false;
|
|
126
|
+
// `greet` is a command in src-host/main.ts, compiled to machine code.
|
|
127
|
+
const greet = async (name) => {
|
|
128
|
+
out.textContent = await janela.invoke("greet", { name });
|
|
231
129
|
};
|
|
232
130
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
|
|
237
|
-
};
|
|
131
|
+
// Greeted once on load, so the round trip is visible without a click.
|
|
132
|
+
await greet("__NAME__");
|
|
133
|
+
await janela.invoke("log", "page loaded");
|
|
238
134
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const r = await janela.invoke("openFile", {});
|
|
243
|
-
if (!r.ok) show($("file"), "error: " + r.error);
|
|
244
|
-
else if (r.cancelled) show($("file"), "cancelled");
|
|
245
|
-
else show($("file"), `${r.path} — ${r.length} chars\n\n${r.text.slice(0, 400)}`);
|
|
135
|
+
document.getElementById("greet-form").onsubmit = (e) => {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
greet(input.value || "__NAME__");
|
|
246
138
|
};
|
|
247
|
-
|
|
248
|
-
$("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
|
|
249
|
-
$("quit").onclick = () => janela.invoke("quit");
|
|
250
139
|
};
|
|
251
140
|
</script>
|
|
252
141
|
</body>
|
package/templates/main.ts
CHANGED
|
@@ -3,83 +3,21 @@
|
|
|
3
3
|
// Register commands here; the page calls them with `await janela.invoke(name, args)`.
|
|
4
4
|
// Handlers take the arguments as a value and return a value — the runtime owns
|
|
5
5
|
// JSON at the boundary, so there is no parsing or stringifying to do here.
|
|
6
|
+
//
|
|
7
|
+
// This template has no bundler, so it uses the untyped `JanelaApp`. The
|
|
8
|
+
// framework templates declare a contract instead and the page is checked
|
|
9
|
+
// against it; see `janela init --template vue`.
|
|
6
10
|
|
|
7
11
|
import type { JanelaApp } from "janela/host";
|
|
8
12
|
|
|
9
13
|
export function setup(app: JanelaApp): void {
|
|
10
|
-
app.command("add", (args) => {
|
|
11
|
-
const a = args as { a: number; b: number };
|
|
12
|
-
const sum = a.a + a.b;
|
|
13
|
-
// Backend→frontend event, just to show the channel exists.
|
|
14
|
-
app.emit("added", sum);
|
|
15
|
-
return sum;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
14
|
app.command("greet", (args) => {
|
|
19
15
|
const a = args as { name: string };
|
|
20
|
-
return "Hello, " + a.name + "
|
|
16
|
+
return "Hello, " + a.name + "! You've been greeted from a native TypeScript binary.";
|
|
21
17
|
});
|
|
22
18
|
|
|
19
|
+
// Anything the host logs goes to the terminal that `janela dev` runs in.
|
|
23
20
|
app.command("log", (args) => {
|
|
24
21
|
console.log("[host] page says:", args as string);
|
|
25
22
|
});
|
|
26
|
-
|
|
27
|
-
// An async command: answers later, without freezing the window. The page
|
|
28
|
-
// still just does `await janela.invoke("wait", { ms: 1000 })`.
|
|
29
|
-
app.commandAsync("wait", (args, resolve) => {
|
|
30
|
-
const a = args as { ms: number };
|
|
31
|
-
app.sleep(a.ms, () => {
|
|
32
|
-
resolve("waited " + a.ms + "ms without blocking the UI");
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// File I/O without freezing the window: the read happens on a worker thread
|
|
37
|
-
// in the shim. Use this instead of node:fs readFileSync, which blocks the
|
|
38
|
-
// host loop — and with it the whole UI — until the syscall returns.
|
|
39
|
-
app.commandAsync("readFile", (args, resolve) => {
|
|
40
|
-
const a = args as { path: string };
|
|
41
|
-
app.readFileAsync(a.path, (err, text) => {
|
|
42
|
-
if (err !== null) {
|
|
43
|
-
resolve({ ok: false, error: err });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
resolve({ ok: true, length: text.length, text: text });
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// The native "open" dialog, paired with the reader above — picking a file is
|
|
51
|
-
// what makes readFileAsync useful. commandAsync is the right shape here: the
|
|
52
|
-
// page's promise stays parked while the user takes as long as they like, and
|
|
53
|
-
// the window carries on serving other calls meanwhile.
|
|
54
|
-
app.commandAsync("openFile", (_args, resolve) => {
|
|
55
|
-
app.openFileDialog(
|
|
56
|
-
{ title: "Pick a file", filters: [{ name: "Text", extensions: ["txt", "md"] }] },
|
|
57
|
-
(paths, err) => {
|
|
58
|
-
if (err !== undefined) {
|
|
59
|
-
resolve({ ok: false, error: err });
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (paths === null) {
|
|
63
|
-
resolve({ ok: true, cancelled: true });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
app.readFileAsync(paths[0], (rerr, text) => {
|
|
67
|
-
resolve(
|
|
68
|
-
rerr !== null
|
|
69
|
-
? { ok: false, error: rerr }
|
|
70
|
-
: { ok: true, path: paths[0], length: text.length, text: text },
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// The window is yours to change at runtime, not just at startup.
|
|
78
|
-
app.command("setTitle", (args) => {
|
|
79
|
-
app.setTitle((args as { title: string }).title);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
app.command("quit", (_args) => {
|
|
83
|
-
app.quit();
|
|
84
|
-
});
|
|
85
23
|
}
|