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
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mmamedel/janela/main/packages/janela/brand/janela-mark-dark.svg">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/mmamedel/janela/main/packages/janela/brand/janela-mark-light.svg" alt="" width="88" align="right">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
1
6
|
# janela
|
|
2
7
|
|
|
3
8
|
> *janela* — Portuguese for **window**.
|
|
@@ -6,11 +11,12 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
|
|
|
6
11
|
Node, no Electron. The backend is TypeScript compiled to a native binary by
|
|
7
12
|
[scriptc](https://scriptc.dev); the window is the OS webview via
|
|
8
13
|
[webview/webview](https://github.com/webview/webview). A desktop binary comes
|
|
9
|
-
out around
|
|
14
|
+
out around 190–390 KB — 191 KB for the smallest template — with no bundled
|
|
10
15
|
browser and no bundled runtime; iOS and Android bundles land around
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
14
20
|
[docs/frontend.md](../../docs/frontend.md).
|
|
15
21
|
|
|
16
22
|
Five targets, one runtime — the same `main.ts`, the same typed contract and the
|
|
@@ -31,7 +37,11 @@ desktop-only for now; on mobile they report clearly when called.
|
|
|
31
37
|
## Quick start
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
|
-
|
|
40
|
+
pnpm create janela # the front door: prompts, scaffolds, installs
|
|
41
|
+
# (npm create janela@latest / yarn / bun also work)
|
|
42
|
+
|
|
43
|
+
# or drive the CLI yourself:
|
|
44
|
+
npm install -g janela
|
|
35
45
|
janela --version # which janela you actually have
|
|
36
46
|
janela init my-app # (or `jn init my-app`)
|
|
37
47
|
cd my-app
|
|
@@ -663,4 +673,7 @@ gh release create v<version> --generate-notes --verify-tag
|
|
|
663
673
|
## License
|
|
664
674
|
|
|
665
675
|
MIT. Bundles [webview/webview](https://github.com/webview/webview) headers
|
|
666
|
-
(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,257 +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
|
-
|
|
11
|
-
a janela frontend is embedded in the binary, so every asset has to be
|
|
12
|
-
inlined anyway, and a window needs no artwork. */
|
|
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. */
|
|
13
11
|
:root {
|
|
14
12
|
color-scheme: light dark;
|
|
15
13
|
--bg: #f6f6f7;
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
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);
|
|
14
|
+
--ink: #18181b;
|
|
15
|
+
--ink-dim: #71717a;
|
|
16
|
+
--field: #ffffff;
|
|
17
|
+
--accent: #1c6fd4;
|
|
18
|
+
--shadow: 0 2px 2px rgb(0 0 0 / 0.08);
|
|
23
19
|
}
|
|
24
20
|
@media (prefers-color-scheme: dark) {
|
|
25
21
|
:root {
|
|
26
22
|
--bg: #17171a;
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
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);
|
|
23
|
+
--ink: #fafafa;
|
|
24
|
+
--ink-dim: #a1a1aa;
|
|
25
|
+
--field: #0f0f11;
|
|
26
|
+
--accent: #58a6ff;
|
|
27
|
+
--shadow: 0 2px 2px rgb(0 0 0 / 0.3);
|
|
34
28
|
}
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
* { box-sizing: border-box; }
|
|
38
32
|
body {
|
|
39
33
|
margin: 0;
|
|
40
|
-
min-height: 100vh;
|
|
41
|
-
display: flex;
|
|
42
|
-
justify-content: center;
|
|
43
34
|
background: var(--bg);
|
|
44
|
-
color: var(--
|
|
45
|
-
font:
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
font: 16px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
37
|
+
text-rendering: optimizeLegibility;
|
|
46
38
|
-webkit-font-smoothing: antialiased;
|
|
47
39
|
}
|
|
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
40
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
41
|
+
.container {
|
|
42
|
+
padding: 10vh 20px 40px;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
76
46
|
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
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
.
|
|
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; }
|
|
104
77
|
|
|
105
78
|
input, button {
|
|
106
|
-
font: inherit;
|
|
107
79
|
border-radius: 8px;
|
|
108
|
-
border: 1px solid
|
|
109
|
-
padding:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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;
|
|
113
88
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
button { cursor: pointer; }
|
|
90
|
+
button:hover { border-color: var(--accent); }
|
|
91
|
+
button:active { border-color: var(--accent); filter: brightness(0.97); }
|
|
117
92
|
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
93
|
|
|
94
|
+
/* Reserved so the layout does not jump when the answer arrives. */
|
|
95
|
+
.greeting { min-height: 1.5em; margin: 20px 0 0; }
|
|
143
96
|
</style>
|
|
144
97
|
</head>
|
|
145
98
|
<body>
|
|
146
|
-
<main>
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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>
|
|
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>
|
|
195
107
|
</div>
|
|
108
|
+
<p class="hint">Click the janela logo to learn more.</p>
|
|
196
109
|
|
|
197
|
-
<
|
|
198
|
-
<
|
|
199
|
-
<button
|
|
200
|
-
</
|
|
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>
|
|
201
115
|
</main>
|
|
202
116
|
|
|
203
117
|
<script>
|
|
204
118
|
// This template has no bundler, so it uses the `janela` global that the
|
|
205
119
|
// host injects before the page loads. Projects with a build step should
|
|
206
|
-
// `import { invoke
|
|
207
|
-
//
|
|
120
|
+
// `import { invoke } from "janela/api"` instead — same function, but
|
|
121
|
+
// typed and resolvable by the bundler.
|
|
208
122
|
window.onload = async () => {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
el.textContent = text;
|
|
212
|
-
el.classList.add("filled");
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
janela.listen("added", (sum) => {
|
|
216
|
-
const li = document.createElement("li");
|
|
217
|
-
li.textContent = "host emitted 'added': " + sum;
|
|
218
|
-
$("events").prepend(li);
|
|
219
|
-
});
|
|
123
|
+
const out = document.getElementById("greeting");
|
|
124
|
+
const input = document.getElementById("greet-input");
|
|
220
125
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
await janela.invoke("log", "page loaded");
|
|
225
|
-
|
|
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 })}`);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
$("wait").onclick = async (e) => {
|
|
233
|
-
e.target.disabled = true;
|
|
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;
|
|
239
|
-
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 });
|
|
240
129
|
};
|
|
241
130
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
|
|
246
|
-
};
|
|
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");
|
|
247
134
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const r = await janela.invoke("openFile", {});
|
|
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)}`);
|
|
135
|
+
document.getElementById("greet-form").onsubmit = (e) => {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
greet(input.value || "__NAME__");
|
|
255
138
|
};
|
|
256
|
-
|
|
257
|
-
$("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
|
|
258
|
-
$("quit").onclick = () => janela.invoke("quit");
|
|
259
139
|
};
|
|
260
140
|
</script>
|
|
261
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
|
}
|