@owear/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +10 -8
  2. package/src/ow.js +548 -25
  3. package/template/README.md +34 -28
  4. package/template/app/main.ts +273 -4
  5. package/template/index.html +117 -28
  6. package/template/package.json +1 -1
  7. package/template/public/favicon.svg +4 -0
  8. package/template/src/assets/dancing-script.woff2 +0 -0
  9. package/template/src/assets/jetbrains-mono.woff2 +0 -0
  10. package/template/src/ow.d.ts +9 -1
  11. package/template/src/renderer.ts +489 -1
  12. package/template/src/style.css +280 -56
  13. package/template-installer/README.md +38 -0
  14. package/template-installer/app/main.ts +22 -0
  15. package/template-installer/index.html +70 -0
  16. package/template-installer/package.json +22 -0
  17. package/template-installer/src/assets/dancing-script.woff2 +0 -0
  18. package/template-installer/src/assets/jetbrains-mono.woff2 +0 -0
  19. package/template-installer/src/installer.ts +171 -0
  20. package/template-installer/src/style.css +442 -0
  21. package/template-installer/tsconfig.json +13 -0
  22. package/template-installer/uninstaller/app/main.ts +16 -0
  23. package/template-installer/uninstaller/index.html +54 -0
  24. package/template-installer/uninstaller/package.json +22 -0
  25. package/template-installer/uninstaller/src/uninstaller.ts +101 -0
  26. package/template-installer/uninstaller/vite.config.ts +10 -0
  27. package/template-installer/vite.config.ts +15 -0
  28. package/tools/owear-installer.mjs +86 -0
  29. package/tools/owear-linux.mjs +158 -0
  30. package/tools/owear-msi.mjs +113 -0
  31. package/tools/owear-pack.mjs +93 -0
  32. package/tools/owear-sign.mjs +204 -0
  33. package/tools/owear-update.mjs +150 -0
  34. package/LICENSE +0 -201
@@ -0,0 +1,171 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // src/installer.ts — UI del instalador (vanilla). Corre en el renderer y usa
5
+ // SOLO la API nativa `installer` (builtin, siempre presente) vía `window.ow`.
6
+ // No depende de módulos .owm (dialog, etc.).
7
+ //
8
+ // Personalízalo a gusto (React, Vue, Tailwind…): esto es sólo el default.
9
+
10
+ import './style.css'
11
+
12
+ interface OwBridge {
13
+ invoke<T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T>
14
+ }
15
+ const ow = (window as unknown as { ow: OwBridge }).ow
16
+ const wid = (window as unknown as { __owWindowId: number }).__owWindowId
17
+
18
+ const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
19
+ document.querySelector(sel) as T
20
+ const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
21
+ ow.invoke<T>(module, fn, ...args)
22
+
23
+ interface PayloadEntry {
24
+ rel: string
25
+ dst: string
26
+ size: number
27
+ dir: boolean
28
+ }
29
+ interface InstallerInfo {
30
+ appId?: string
31
+ appName?: string
32
+ version?: string
33
+ publisher?: string
34
+ }
35
+ interface Target {
36
+ layout?: 'minimal' | 'layout'
37
+ dir?: string
38
+ shortcuts?: string[]
39
+ }
40
+ interface Bridge {
41
+ app?: { id?: string; name?: string; version?: string; publisher?: string }
42
+ targets?: Record<string, Target>
43
+ order?: string[]
44
+ }
45
+
46
+ const platform: string = (() => {
47
+ const p = navigator.userAgent.toLowerCase()
48
+ return p.includes('win') ? 'win' : p.includes('mac') ? 'mac' : 'linux'
49
+ })()
50
+
51
+ let mode: 'minimal' | 'layout' = 'minimal'
52
+ let plan: PayloadEntry[] = []
53
+ let busy = false
54
+
55
+ function fmtSize(bytes: number): string {
56
+ if (!bytes) return ''
57
+ if (bytes < 1024) return `${bytes} B`
58
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
59
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`
60
+ }
61
+
62
+ function setStatus(msg: string, kind: 'ok' | 'err' | '' = ''): void {
63
+ const el = $('#status')
64
+ el.textContent = msg
65
+ el.dataset.kind = kind
66
+ }
67
+
68
+ async function boot(): Promise<void> {
69
+ $('#win-min').addEventListener('click', () => void invoke('ow-window', 'minimize', wid))
70
+ $('#win-max').addEventListener('click', () => void invoke('ow-window', 'maximize', wid))
71
+ $('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', wid))
72
+
73
+ const info = await invoke<InstallerInfo>('installer', 'info').catch(() => ({}))
74
+ const bridge = await invoke<Bridge | null>('installer', 'bridge').catch(() => null)
75
+ const t: Target = bridge?.targets?.[platform] ?? {}
76
+ mode = t.layout ?? 'minimal'
77
+
78
+ const name = info.appName ?? bridge?.app?.name ?? 'la app'
79
+ const version = info.version ?? bridge?.app?.version ?? ''
80
+ $('#summary').textContent =
81
+ `Se instalará ${name}${version ? ` ${version}` : ''} · modo ${mode} · ${platform}`
82
+
83
+ // Destino: preferimos el del bridge si es ABSOLUTO; si no, el default real.
84
+ let value = t.dir ?? ''
85
+ if (!value.startsWith('/')) {
86
+ value = await invoke<string>('installer', 'defaultDir').catch(() => '')
87
+ }
88
+ ;($('#dir') as HTMLInputElement).value = value
89
+
90
+ await refreshPlan()
91
+ $('#install').removeAttribute('disabled')
92
+ }
93
+
94
+ async function refreshPlan(): Promise<void> {
95
+ const bridge = await invoke<Bridge | null>('installer', 'bridge').catch(() => null)
96
+ plan = await invoke<PayloadEntry[]>('installer', 'plan', {
97
+ mode,
98
+ layout: mode === 'minimal' ? 'flat' : 'tree',
99
+ order: bridge?.order,
100
+ })
101
+ const total = plan.reduce((n, e) => n + e.size, 0)
102
+ $('#count').textContent = `${plan.length} · ${fmtSize(total)}`
103
+ $('#plan').textContent = plan
104
+ .slice(0, 300)
105
+ .map((e) => `${e.dst}${e.dir ? '/' : ''}${e.size ? ` ${fmtSize(e.size)}` : ''}`)
106
+ .join('\n')
107
+ }
108
+
109
+ async function pickDir(): Promise<void> {
110
+ const current = ($('#dir') as HTMLInputElement).value.trim()
111
+ try {
112
+ // Selector NATIVO del builtin installer (no usa el módulo `dialog`).
113
+ const dir = await invoke<string | null>('installer', 'chooseDir', {
114
+ title: 'Elegir carpeta de instalación',
115
+ defaultPath: current,
116
+ })
117
+ if (dir) ($('#dir') as HTMLInputElement).value = dir
118
+ } catch {
119
+ /* cancelado */
120
+ }
121
+ }
122
+
123
+ function targetExec(dir: string): string {
124
+ const top = plan.find((e) => !e.dir && !e.dst.includes('/')) ?? plan[0]
125
+ return top ? `${dir}/${top.dst}` : ''
126
+ }
127
+
128
+ async function doInstall(): Promise<void> {
129
+ if (busy) return
130
+ busy = true
131
+ const dir = ($('#dir') as HTMLInputElement).value.trim()
132
+ if (!dir) {
133
+ setStatus('Elige un directorio de instalación', 'err')
134
+ busy = false
135
+ return
136
+ }
137
+ $('#install').setAttribute('disabled', 'true')
138
+ $('#cancel').removeAttribute('hidden')
139
+ $('#bar').removeAttribute('hidden')
140
+ setStatus('Instalando…')
141
+
142
+ try {
143
+ const res = await invoke<{ files: number; dir: string }>('installer', 'install', {
144
+ dir,
145
+ mode,
146
+ layout: mode === 'minimal' ? 'flat' : 'tree',
147
+ })
148
+ $('#bar').value = 100
149
+ setStatus(`Instalado en ${res.dir} (${res.files} ficheros)`, 'ok')
150
+
151
+ if (($('#shortcut') as HTMLInputElement).checked) {
152
+ await invoke('installer', 'shortcuts', { execPath: targetExec(res.dir) }).catch(() => false)
153
+ }
154
+ if (($('#launch') as HTMLInputElement).checked) {
155
+ await invoke('installer', 'launch', { path: targetExec(res.dir) }).catch(() => false)
156
+ await invoke('ow-window', 'close', wid)
157
+ }
158
+ } catch (e) {
159
+ setStatus(e instanceof Error ? e.message : String(e), 'err')
160
+ $('#install').removeAttribute('disabled')
161
+ } finally {
162
+ busy = false
163
+ $('#cancel').setAttribute('hidden', 'true')
164
+ }
165
+ }
166
+
167
+ $('#pick').addEventListener('click', () => void pickDir())
168
+ $('#install').addEventListener('click', () => void doInstall())
169
+ $('#cancel').addEventListener('click', () => void invoke('ow-window', 'close', wid))
170
+
171
+ void boot()
@@ -0,0 +1,442 @@
1
+ /* Copyright 2026 Owear Contributors
2
+ * SPDX-License-Identifier: Apache-2.0
3
+ *
4
+ * Estilos del instalador — mismos tokens/componentes que el Starter de Owear
5
+ * (paleta oscura + acento rosado, JetBrains Mono + Dancing Script).
6
+ */
7
+
8
+ :root {
9
+ color-scheme: dark;
10
+
11
+ /* colores base */
12
+ --bg: #282c34;
13
+ --bg-deep: #21252b;
14
+ --panel: #2c313a;
15
+ --panel-2: #21252b;
16
+ --line: #3b4048;
17
+ --line-2: #4b5263;
18
+ --text: #abb2bf;
19
+ --text-strong: #dcdfe4;
20
+ --muted: #5c6370;
21
+ --purple: #c678dd;
22
+ --green: #98c379;
23
+ --red: #e06c75;
24
+ --cyan: #56b6c2;
25
+
26
+ /* acento: rosado seco */
27
+ --accent: #d3869b;
28
+ --accent-hover: #c06f86;
29
+ --accent-ink: #241820;
30
+
31
+ --radius: 10px;
32
+ --btn: #2c313a;
33
+ --hover: rgba(255, 255, 255, 0.06);
34
+ --hover-strong: rgba(255, 255, 255, 0.14);
35
+ }
36
+
37
+ :root[data-theme="light"] {
38
+ color-scheme: light;
39
+ --bg: #fafafa;
40
+ --bg-deep: #f0f0f1;
41
+ --panel: #ffffff;
42
+ --panel-2: #f0f0f1;
43
+ --line: #d7d7d9;
44
+ --line-2: #b9b9bc;
45
+ --text: #383a42;
46
+ --text-strong: #1f2126;
47
+ --muted: #9aa0a6;
48
+ --purple: #a626a4;
49
+ --green: #50a14f;
50
+ --red: #e45649;
51
+ --cyan: #0184bc;
52
+ --accent: #b5586e;
53
+ --accent-hover: #9d4760;
54
+ --accent-ink: #ffffff;
55
+ --btn: #e8e8ea;
56
+ --hover: rgba(0, 0, 0, 0.05);
57
+ --hover-strong: rgba(0, 0, 0, 0.1);
58
+ }
59
+
60
+ @font-face {
61
+ font-family: "Dancing Script";
62
+ font-style: normal;
63
+ font-weight: 600;
64
+ font-display: swap;
65
+ src: url("./assets/dancing-script.woff2") format("woff2");
66
+ }
67
+
68
+ @font-face {
69
+ font-family: "JetBrains Mono";
70
+ font-style: normal;
71
+ font-weight: 400 700;
72
+ font-display: swap;
73
+ src: url("./assets/jetbrains-mono.woff2") format("woff2");
74
+ }
75
+
76
+ * {
77
+ box-sizing: border-box;
78
+ }
79
+
80
+ html,
81
+ body {
82
+ height: 100%;
83
+ }
84
+
85
+ body {
86
+ margin: 0;
87
+ background: var(--bg);
88
+ color: var(--text);
89
+ font: 14px/1.55 "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo,
90
+ Consolas, monospace;
91
+ display: flex;
92
+ flex-direction: column;
93
+ overflow: hidden;
94
+ user-select: none;
95
+ -webkit-user-select: none;
96
+ }
97
+
98
+ /* ── Titlebar ─────────────────────────────────────────────────────────── */
99
+ .titlebar {
100
+ height: 40px;
101
+ flex: 0 0 auto;
102
+ display: flex;
103
+ align-items: stretch;
104
+ background: var(--bg-deep);
105
+ }
106
+
107
+ .titlebar__drag {
108
+ flex: 1;
109
+ min-width: 0;
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 10px;
113
+ padding: 0 14px;
114
+ }
115
+
116
+ .brand__mark {
117
+ width: 12px;
118
+ height: 12px;
119
+ border-radius: 3px;
120
+ background: var(--accent);
121
+ }
122
+
123
+ .brand__name {
124
+ color: var(--text-strong);
125
+ font-weight: 600;
126
+ letter-spacing: 0.2px;
127
+ white-space: nowrap;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ }
131
+
132
+ .titlebar__controls {
133
+ display: flex;
134
+ }
135
+
136
+ .winbtn {
137
+ width: 46px;
138
+ border: 0;
139
+ background: transparent;
140
+ color: var(--muted);
141
+ cursor: pointer;
142
+ display: grid;
143
+ place-items: center;
144
+ }
145
+
146
+ .winbtn svg {
147
+ fill: none;
148
+ stroke: currentColor;
149
+ stroke-width: 1.4;
150
+ }
151
+
152
+ .winbtn:hover {
153
+ background: var(--btn);
154
+ color: var(--text-strong);
155
+ }
156
+
157
+ .winbtn--close:hover {
158
+ background: var(--red);
159
+ color: #fff;
160
+ }
161
+
162
+ /* ── Layout ───────────────────────────────────────────────────────────── */
163
+ .page {
164
+ flex: 1;
165
+ min-height: 0;
166
+ overflow: auto;
167
+ padding: 26px clamp(18px, 4vw, 44px) 96px;
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 20px;
171
+ max-width: 860px;
172
+ width: 100%;
173
+ margin: 0 auto;
174
+ }
175
+
176
+ .hero {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: 10px;
180
+ }
181
+
182
+ h1 {
183
+ margin: 0;
184
+ color: var(--text-strong);
185
+ font-family: "Dancing Script", cursive;
186
+ font-weight: 600;
187
+ font-size: clamp(30px, 4vw, 44px);
188
+ line-height: 1.15;
189
+ }
190
+
191
+ .lead {
192
+ margin: 0;
193
+ max-width: 72ch;
194
+ color: var(--muted);
195
+ font-size: 15px;
196
+ }
197
+
198
+ .lead strong {
199
+ color: var(--text);
200
+ }
201
+
202
+ .board {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 16px;
206
+ padding: 16px;
207
+ background: var(--bg-deep);
208
+ border-radius: var(--radius);
209
+ }
210
+
211
+ .card {
212
+ background: var(--panel);
213
+ border-radius: var(--radius);
214
+ padding: 16px 18px;
215
+ }
216
+
217
+ .card--grow {
218
+ min-width: 0;
219
+ display: flex;
220
+ flex-direction: column;
221
+ }
222
+
223
+ .card__head {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: space-between;
227
+ margin-bottom: 12px;
228
+ }
229
+
230
+ .card__head h2 {
231
+ margin: 0;
232
+ font-size: 12px;
233
+ text-transform: uppercase;
234
+ letter-spacing: 0.07em;
235
+ color: var(--muted);
236
+ }
237
+
238
+ .badge {
239
+ font-size: 12px;
240
+ color: var(--purple);
241
+ background: var(--panel-2);
242
+ border: 1px solid var(--line);
243
+ border-radius: 999px;
244
+ padding: 2px 10px;
245
+ font-variant-numeric: tabular-nums;
246
+ }
247
+
248
+ .hint {
249
+ margin: 0 0 12px;
250
+ color: var(--muted);
251
+ }
252
+
253
+ .hint code,
254
+ .lead code {
255
+ background: var(--panel-2);
256
+ border: 1px solid var(--line);
257
+ border-radius: 6px;
258
+ padding: 1px 6px;
259
+ font-size: 12.5px;
260
+ }
261
+
262
+ code {
263
+ color: var(--purple);
264
+ }
265
+
266
+ .console {
267
+ flex: 1;
268
+ min-height: 150px;
269
+ max-height: 42vh;
270
+ margin: 0;
271
+ overflow: auto;
272
+ background: var(--bg-deep);
273
+ border-radius: 8px;
274
+ padding: 12px 14px;
275
+ font: 12.5px/1.6 "JetBrains Mono", ui-monospace, monospace;
276
+ color: var(--text);
277
+ white-space: pre-wrap;
278
+ word-break: break-word;
279
+ user-select: text;
280
+ -webkit-user-select: text;
281
+ }
282
+
283
+ /* ── Formulario ───────────────────────────────────────────────────────── */
284
+ .row {
285
+ display: flex;
286
+ gap: 8px;
287
+ align-items: center;
288
+ }
289
+
290
+ .row + .row {
291
+ margin-top: 12px;
292
+ }
293
+
294
+ .field {
295
+ flex: 1;
296
+ min-width: 0;
297
+ font: inherit;
298
+ color: var(--text);
299
+ background: var(--bg-deep);
300
+ border: 1px solid var(--line);
301
+ border-radius: 8px;
302
+ padding: 8px 10px;
303
+ }
304
+
305
+ .field:focus {
306
+ outline: none;
307
+ border-color: var(--accent);
308
+ }
309
+
310
+ .opt {
311
+ display: flex;
312
+ align-items: center;
313
+ gap: 8px;
314
+ color: var(--text);
315
+ cursor: pointer;
316
+ }
317
+
318
+ .opt input {
319
+ accent-color: var(--accent);
320
+ }
321
+
322
+ /* ── Botones (igual que el Starter) ───────────────────────────────────── */
323
+ .btn {
324
+ position: relative;
325
+ isolation: isolate;
326
+ font: inherit;
327
+ color: var(--text);
328
+ background: var(--btn);
329
+ border: 0;
330
+ border-radius: 10px;
331
+ padding: 8px 14px;
332
+ cursor: pointer;
333
+ }
334
+
335
+ .btn::before {
336
+ content: "";
337
+ position: absolute;
338
+ inset: 3px;
339
+ border-radius: 7px;
340
+ background: transparent;
341
+ transition: inset 0.12s ease, border-radius 0.12s ease, background 0.12s ease;
342
+ z-index: -1;
343
+ pointer-events: none;
344
+ }
345
+
346
+ .btn:hover::before {
347
+ background: var(--hover);
348
+ }
349
+
350
+ .btn:active::before {
351
+ inset: 0;
352
+ border-radius: 10px;
353
+ background: var(--hover-strong);
354
+ }
355
+
356
+ .btn--primary {
357
+ background: var(--accent);
358
+ color: var(--accent-ink);
359
+ font-weight: 600;
360
+ }
361
+
362
+ .btn--primary:hover::before {
363
+ background: rgba(255, 255, 255, 0.16);
364
+ }
365
+
366
+ .btn--primary:active::before {
367
+ background: rgba(255, 255, 255, 0.3);
368
+ }
369
+
370
+ .btn--primary:disabled {
371
+ opacity: 0.5;
372
+ cursor: default;
373
+ }
374
+
375
+ .btn--ghost {
376
+ background: transparent;
377
+ }
378
+
379
+ .linkbtn {
380
+ font: inherit;
381
+ background: transparent;
382
+ border: 0;
383
+ color: var(--muted);
384
+ cursor: pointer;
385
+ }
386
+
387
+ .linkbtn:hover {
388
+ color: var(--accent);
389
+ }
390
+
391
+ /* ── Pie fijo con progreso ────────────────────────────────────────────── */
392
+ .foot {
393
+ position: fixed;
394
+ left: 0;
395
+ right: 0;
396
+ bottom: 0;
397
+ display: flex;
398
+ align-items: center;
399
+ gap: 14px;
400
+ padding: 14px clamp(18px, 4vw, 44px);
401
+ background: color-mix(in srgb, var(--bg-deep) 86%, transparent);
402
+ backdrop-filter: blur(6px);
403
+ }
404
+
405
+ .foot__spacer {
406
+ flex: 1;
407
+ }
408
+
409
+ .status {
410
+ color: var(--muted);
411
+ min-width: 0;
412
+ overflow: hidden;
413
+ text-overflow: ellipsis;
414
+ white-space: nowrap;
415
+ }
416
+
417
+ .status[data-kind="ok"] {
418
+ color: var(--green);
419
+ }
420
+
421
+ .status[data-kind="err"] {
422
+ color: var(--red);
423
+ }
424
+
425
+ progress {
426
+ width: 160px;
427
+ height: 6px;
428
+ border: 0;
429
+ border-radius: 999px;
430
+ background: var(--panel-2);
431
+ overflow: hidden;
432
+ }
433
+
434
+ progress::-webkit-progress-bar {
435
+ background: var(--panel-2);
436
+ border-radius: 999px;
437
+ }
438
+
439
+ progress::-webkit-progress-value {
440
+ background: var(--accent);
441
+ border-radius: 999px;
442
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["node", "vite/client"],
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true
11
+ },
12
+ "include": ["src", "app"]
13
+ }
@@ -0,0 +1,16 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // app/main.ts — proceso principal del DESINSTALADOR (sidecar Node).
5
+ import { app, BrowserWindow } from '@owear/core'
6
+
7
+ app.whenReady().then(() => {
8
+ const win = new BrowserWindow({
9
+ title: 'Desinstalador',
10
+ width: 720,
11
+ height: 460,
12
+ titleBarStyle: 'custom',
13
+ url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
14
+ })
15
+ win.on('closed', () => app.quit())
16
+ })
@@ -0,0 +1,54 @@
1
+ <!doctype html>
2
+ <!-- Copyright 2026 Owear Contributors
3
+ SPDX-License-Identifier: Apache-2.0 -->
4
+ <html lang="es">
5
+ <head>
6
+ <meta charset="UTF-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Desinstalar · __APP_NAME__</title>
9
+ </head>
10
+ <body>
11
+ <header class="titlebar">
12
+ <div class="titlebar__drag" data-ow-drag>
13
+ <span class="brand__mark" aria-hidden="true"></span>
14
+ <span class="brand__name">Desinstalador · __APP_NAME__</span>
15
+ </div>
16
+ <div class="titlebar__controls">
17
+ <button id="win-min" class="winbtn" title="Minimizar" aria-label="Minimizar">
18
+ <svg viewBox="0 0 12 12" width="12" height="12"><path d="M2 6h8" /></svg>
19
+ </button>
20
+ <button id="win-close" class="winbtn winbtn--close" title="Cerrar" aria-label="Cerrar">
21
+ <svg viewBox="0 0 12 12" width="12" height="12"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" /></svg>
22
+ </button>
23
+ </div>
24
+ </header>
25
+
26
+ <main class="page">
27
+ <section class="hero">
28
+ <h1>Desinstalar __APP_NAME__</h1>
29
+ <p class="lead" id="summary">Localizando la instalación…</p>
30
+ </section>
31
+
32
+ <section class="board">
33
+ <article class="card">
34
+ <header class="card__head"><h2>Instalación</h2></header>
35
+ <div class="row">
36
+ <input id="dir" class="field" type="text" spellcheck="false" aria-label="Directorio de instalación" />
37
+ <button id="pick" class="btn">Elegir…</button>
38
+ </div>
39
+ <div class="row">
40
+ <label class="opt"><input type="checkbox" id="keepData" checked /> Conservar mis datos</label>
41
+ </div>
42
+ </article>
43
+ </section>
44
+ </main>
45
+
46
+ <footer class="foot">
47
+ <span id="status" class="status"></span>
48
+ <span class="foot__spacer"></span>
49
+ <button id="uninstall" class="btn btn--primary" disabled>Desinstalar</button>
50
+ </footer>
51
+
52
+ <script type="module" src="/src/uninstaller.ts"></script>
53
+ </body>
54
+ </html>