@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
@@ -3,54 +3,54 @@
3
3
 
4
4
  # __APP_NAME__
5
5
 
6
- App de escritorio construida con **[Owear](https://owear.dev)**: el WebView del
7
- sistema renderiza tu frontend web y el kernel nativo te da el sistema operativo.
6
+ Desktop app built with **[Owear](https://owear.dev)**: the system WebView renders
7
+ your web frontend and the native kernel gives you the operating system.
8
8
 
9
- ## Empezar
9
+ ## Get started
10
10
 
11
11
  ```bash
12
12
  npm install
13
13
  npm run dev # ≡ ow dev
14
14
  ```
15
15
 
16
- Se abre una ventana con el WebView del sistema. Edita `src/renderer.ts` y el
17
- hot-reload de Vite lo refleja al instante.
16
+ A window with the system WebView opens. Edit `src/renderer.ts` and Vite's
17
+ hot-reload reflects it instantly.
18
18
 
19
19
  ```bash
20
20
  npm run build # ≡ ow build → dist/
21
21
  ```
22
22
 
23
- ## Estructura
23
+ ## Structure
24
24
 
25
25
  ```
26
- ├── index.html UI (titlebar propia, hero, tarjetas)
26
+ ├── index.html UI (custom title bar, hero, cards)
27
27
  ├── src/
28
- │ ├── renderer.ts lógica del renderer + llamadas a módulos nativos
29
- │ ├── style.css estilos
30
- │ └── ow.d.ts tipos de window.ow
28
+ │ ├── renderer.ts renderer logic + calls to native modules
29
+ │ ├── style.css styles
30
+ │ └── ow.d.ts types for window.ow
31
31
  └── app/
32
- └── main.ts proceso principal (sidecar Node): sólo la ventana
32
+ └── main.ts main process (Node sidecar): just the window
33
33
  ```
34
34
 
35
- ## Cómo se llama al sistema
35
+ ## How it talks to the system
36
36
 
37
- Sin `ipcRenderer` ni `ipcMain`: el renderer invoca los módulos nativos del
38
- kernel directo.
37
+ No `ipcRenderer` and no `ipcMain`: the renderer calls the kernel's native modules
38
+ directly.
39
39
 
40
40
  ```ts
41
- const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
41
+ const path = await ow.invoke('dialog', 'open', 'open', 'Open a file')
42
42
  const text = await ow.invoke('fs', 'readText', path)
43
- await ow.invoke('notification', 'show', 'Listo', `Leídos ${text.length} bytes`)
43
+ await ow.invoke('notification', 'show', 'Done', `Read ${text.length} bytes`)
44
44
  ```
45
45
 
46
- Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
47
- `clipboard` · `shell` · `screen` · `net` · `notification` · `power` · `menu` ·
48
- `globalshortcut` · `updater` · `capturer`, más los builtins `ow-window` (ventana),
49
- `window` (extras del WebView), `session`, `app` y `crashreporter`.
46
+ Stock modules: `fs` · `path` · `process` (PTY) · `dialog` · `clipboard` ·
47
+ `shell` · `screen` · `net` · `notification` · `power` · `menu` ·
48
+ `globalshortcut` · `updater` · `capturer`, plus the builtins `ow-window`
49
+ (window), `window` (WebView extras), `session`, `app`, and `crashreporter`.
50
50
 
51
- ## Añadir un módulo nativo propio (C++)
51
+ ## Adding your own native module (C++)
52
52
 
53
- 1. Crea `native/mi_modulo.cpp`:
53
+ 1. Create `native/my_module.cpp`:
54
54
 
55
55
  ```cpp
56
56
  #include <ow/Json.h>
@@ -61,17 +61,23 @@ Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
61
61
  ow::Module::RespondOk(res, "\"pong\"");
62
62
  }
63
63
 
64
- OW_MODULE_BEGIN(mi_modulo, "1.0.0")
64
+ OW_MODULE_BEGIN(my_module, "1.0.0")
65
65
  OW_FN(ping)
66
66
  OW_MODULE_END()
67
67
  ```
68
68
 
69
- 2. `ow dev` lo compila a `.owm` y genera el binding. Úsalo en el renderer:
69
+ 2. `ow dev` compiles it to `.owm` and generates the binding. Use it in the
70
+ renderer:
70
71
 
71
72
  ```ts
72
- import { mi_modulo } from '@owear/native'
73
- await mi_modulo.ping()
73
+ import { my_module } from '@owear/native'
74
+ await my_module.ping()
74
75
  ```
75
76
 
76
- > Requiere un compilador de C++ en el sistema. Si no lo tienes, no añadas
77
- > `native/` y la app sigue funcionando con los módulos stock.
77
+ > Requires a C++ compiler on the system. If you do not have one, do not add
78
+ > `native/` and the app keeps working with the stock modules.
79
+
80
+ ## Learn more
81
+
82
+ The full documentation lives in the Owear repository under
83
+ [`docs/`](https://github.com/owear/owear/tree/main/docs).
@@ -3,21 +3,290 @@
3
3
  //
4
4
  // app/main.ts — proceso principal (sidecar Node), estilo Electron.
5
5
  //
6
- // Sólo gestiona el ciclo de vida de la ventana. La app en sí vive en el
7
- // renderer, que llama a los módulos nativos directo. Aquí podrías añadir
8
- // lógica privilegiada (por ejemplo, exponerla vía un módulo nativo propio).
6
+ // Sólo gestiona el ciclo de vida de la ventana. La app vive en el renderer,
7
+ // que llama a los módulos nativos directo.
9
8
 
10
- import { app, BrowserWindow } from '@owear/core'
9
+ import { app, BrowserWindow, Menu, Tray, invokeNative, nativeImage, theme, screen, powerMonitor, powerSaveBlocker } from '@owear/core'
10
+ import * as fs from 'node:fs'
11
+ import * as os from 'node:os'
12
+ import * as path from 'node:path'
11
13
 
12
14
  app.whenReady().then(() => {
15
+ // Icono de la app: `favicon.svg` del proyecto (Vite lo copia a dist/ en build).
16
+ // `app.setIcon` fija el icono por defecto de las ventanas creadas después.
17
+ // (API de icono del kernel: `window.setIcon`.)
18
+ const iconPath = [
19
+ path.join(app.getAppPath(), 'favicon.svg'), // empaquetado: dist/ → app/
20
+ path.join(process.cwd(), 'public', 'favicon.svg'), // dev: public/ del proyecto
21
+ ].find((p) => fs.existsSync(p))
22
+ if (iconPath) app.setIcon(iconPath)
23
+
13
24
  const win = new BrowserWindow({
14
25
  title: '__APP_NAME__',
15
26
  width: 1080,
16
27
  height: 720,
17
28
  titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
29
+ // Botones de ventana nativos (min/max/close) dentro de la titlebar custom.
30
+ // En Linux son los del TEMA (la distro define forma/tamaño/hover).
31
+ // Opcional: color = fondo de la banda · symbolColor = glifo ·
32
+ // buttonColor = fondo interno del círculo (si no, los del tema).
33
+ titleBarOverlay: { height: 40, color: '#21252b' },
18
34
  url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
19
35
  })
20
36
 
21
37
  win.on('ready-to-show', () => console.log('[main] ventana lista, id =', win.id))
22
38
  win.on('closed', () => app.quit())
39
+
40
+ // ── C3: webContents (capturePage, eventos, setWindowOpenHandler) ─────────
41
+ app.handle('starter.capture', async () => {
42
+ const img = await win.webContents.capturePage()
43
+ return { data: img.toPNG().toString('base64') }
44
+ })
45
+ win.webContents.on('did-finish-load', () =>
46
+ void app.send('webContents.event', 'did-finish-load')
47
+ )
48
+ win.webContents.on('before-input-event', (e: { key?: string }) =>
49
+ void app.send('webContents.event', `before-input-event ${e?.key ?? ''}`)
50
+ )
51
+ win.webContents.setWindowOpenHandler(({ url }) => {
52
+ void app.send('webContents.event', `window-open ${url}`)
53
+ return { action: 'deny' }
54
+ })
55
+
56
+ // ── C4: nativeImage (captura → resize/crop/toPNG/dataURL) ────────────────
57
+ app.handle('starter.nativeImage', async () => {
58
+ const shot = await win.webContents.capturePage() // NativeImage
59
+ const small = shot.resize({ width: 64 })
60
+ const fromBuffer = nativeImage.createFromBuffer(shot.toPNG())
61
+ const fromUrl = nativeImage.createFromDataURL(small.toDataURL())
62
+ const cropped = shot.crop({ x: 0, y: 0, width: 16, height: 16 })
63
+ return {
64
+ original: shot.getSize(),
65
+ resized: small.getSize(),
66
+ cropped: cropped.getSize(),
67
+ pngBytes: small.toPNG().length,
68
+ reDecoded: fromBuffer.getSize(),
69
+ fromDataURL: fromUrl.getSize(),
70
+ empty: small.isEmpty(),
71
+ }
72
+ })
73
+
74
+ // ── C5: menu (contextual + menubar) ──────────────────────────────────────
75
+ // Menubar (Windows nativo; noop en Linux/GNOME por diseño).
76
+ Menu.setApplicationMenu(
77
+ Menu.buildFromTemplate([
78
+ {
79
+ label: 'Archivo',
80
+ submenu: [
81
+ { label: 'Nuevo', accelerator: 'CmdOrCtrl+N', click: () => void app.send('menu.event', 'Nuevo') },
82
+ { type: 'separator' },
83
+ { role: 'quit' },
84
+ ],
85
+ },
86
+ {
87
+ label: 'Ver',
88
+ submenu: [
89
+ {
90
+ label: 'Barra lateral',
91
+ type: 'checkbox',
92
+ checked: true,
93
+ click: (mi) => void app.send('menu.event', `sidebar=${mi.checked}`),
94
+ },
95
+ { type: 'separator' },
96
+ { role: 'toggleDevTools' },
97
+ { role: 'reload' },
98
+ ],
99
+ },
100
+ ])
101
+ )
102
+
103
+ // Menú contextual (popup) disparado por un botón del starter.
104
+ app.handle('starter.menu', () => {
105
+ Menu.buildFromTemplate([
106
+ { label: 'Nuevo', click: () => void app.send('menu.event', 'Nuevo') },
107
+ { label: 'Abrir', accelerator: 'CmdOrCtrl+O', click: () => void app.send('menu.event', 'Abrir') },
108
+ { type: 'separator' },
109
+ {
110
+ label: 'Barra lateral',
111
+ type: 'checkbox',
112
+ checked: true,
113
+ click: (mi) => void app.send('menu.event', `checkbox=${mi.checked}`),
114
+ },
115
+ { label: 'Zona A', type: 'radio', checked: true, click: () => void app.send('menu.event', 'radio A') },
116
+ { label: 'Zona B', type: 'radio', click: () => void app.send('menu.event', 'radio B') },
117
+ { type: 'separator' },
118
+ { role: 'copy' },
119
+ { role: 'toggleDevTools' },
120
+ { role: 'quit' },
121
+ ]).popup({ window: win })
122
+ return null
123
+ })
124
+
125
+ // ── C6: tray (icono de bandeja) ──────────────────────────────────────────
126
+ let tray: Tray | null = null
127
+ app.handle('starter.tray', async () => {
128
+ if (tray) return { created: false, id: tray.id }
129
+ const icon = (await win.webContents.capturePage()).resize({ width: 16, height: 16 })
130
+ tray = new Tray(icon)
131
+ tray.setToolTip('__APP_NAME__')
132
+ tray.setTitle('__APP_NAME__')
133
+ tray.setContextMenu(
134
+ Menu.buildFromTemplate([
135
+ { label: 'Mostrar ventana', click: () => void win.show() },
136
+ { label: 'Capturar página', click: () => void app.send('tray.event', 'capturar') },
137
+ { type: 'separator' },
138
+ { role: 'quit' },
139
+ ])
140
+ )
141
+ tray.on('click', () => {
142
+ void win.show()
143
+ void app.send('tray.event', 'click')
144
+ })
145
+ tray.on('right-click', () => void app.send('tray.event', 'right-click'))
146
+ tray.on('double-click', () => void app.send('tray.event', 'double-click'))
147
+ return { created: true, id: tray.id }
148
+ })
149
+ app.handle('starter.trayDestroy', () => {
150
+ tray?.destroy()
151
+ tray = null
152
+ return null
153
+ })
154
+
155
+ // ── C7: nativeTheme (leer/forzar/escuchar) ───────────────────────────────
156
+ app.handle('starter.themeGet', () => theme.get())
157
+ app.handle('starter.themeSet', async (src: string) => {
158
+ await theme.setSource((src as 'system' | 'light' | 'dark') ?? 'system')
159
+ return theme.get()
160
+ })
161
+ void theme.watch() // cambios del sistema → ow.on('theme.changed')
162
+
163
+ // ── C8: print / printToPDF ───────────────────────────────────────────────
164
+ app.handle('starter.printPdf', async () => {
165
+ try {
166
+ const pdf = await win.webContents.printToPDF()
167
+ const out = path.join(os.tmpdir(), 'owear-starter.pdf')
168
+ fs.writeFileSync(out, pdf)
169
+ void invokeNative('shell', 'showItemInFolder', out)
170
+ return { ok: true, bytes: pdf.length, path: out }
171
+ } catch (e) {
172
+ return { ok: false, error: e instanceof Error ? e.message : String(e) }
173
+ }
174
+ })
175
+ app.handle('starter.print', () => {
176
+ win.webContents.print()
177
+ return null
178
+ })
179
+
180
+ // ── C9: BrowserWindow completo ───────────────────────────────────────────
181
+ let child: BrowserWindow | null = null
182
+ app.handle('starter.childWindow', () => {
183
+ if (child) {
184
+ void child.focus()
185
+ return { id: child.id }
186
+ }
187
+ child = new BrowserWindow({
188
+ title: 'Ventana secundaria',
189
+ width: 520,
190
+ height: 360,
191
+ parent: win.id ?? undefined,
192
+ backgroundColor: '#12161c',
193
+ url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
194
+ })
195
+ child.on('closed', () => {
196
+ child = null
197
+ })
198
+ return { id: child.id }
199
+ })
200
+ app.handle('starter.toggleOnTop', async () => {
201
+ const on = await win.isAlwaysOnTop()
202
+ await win.setAlwaysOnTop(!on)
203
+ return { on: !on }
204
+ })
205
+ app.handle('starter.progress', async () => {
206
+ await win.setProgressBar(0.5)
207
+ setTimeout(() => void win.setProgressBar(-1, { mode: 'none' }), 4000)
208
+ return null
209
+ })
210
+ app.handle('starter.ignoreMouse', () => {
211
+ void win.setIgnoreMouseEvents(true, { forward: true })
212
+ setTimeout(() => void win.setIgnoreMouseEvents(false), 3000)
213
+ return null
214
+ })
215
+ app.handle('starter.windows', async () => {
216
+ const focus = await BrowserWindow.getFocusedWindow()
217
+ const all = await BrowserWindow.getAllWindows()
218
+ return { focused: focus?.id ?? null, count: all.length, ids: all.map((w) => w.id) }
219
+ })
220
+
221
+ // ── C10: pantallas (multi-monitor) y energía ─────────────────────────────
222
+ app.handle('starter.screens', async () => {
223
+ await screen.watch()
224
+ const displays = await screen.getAllDisplays()
225
+ const primary = displays.find((d) => d.primary) ?? displays[0] ?? null
226
+ const cursor = await screen.getCursorScreenPoint()
227
+ return { displays, primaryId: primary?.id ?? null, cursor }
228
+ })
229
+ app.handle('starter.nearest', async () => {
230
+ const cursor = await screen.getCursorScreenPoint()
231
+ const display = await screen.getDisplayNearestPoint(cursor)
232
+ return { cursor, display }
233
+ })
234
+ app.handle('starter.powerOn', async () => {
235
+ await powerMonitor.watch()
236
+ const onBattery = await powerMonitor.isOnBatteryPower()
237
+ const idle = await powerMonitor.getIdleTime()
238
+ return { onBattery, idle }
239
+ })
240
+ app.handle('starter.idle', async () => {
241
+ const idleTime = await powerMonitor.getIdleTime()
242
+ const state = await powerMonitor.getIdleState(30)
243
+ return { idleTime, state }
244
+ })
245
+ let blockerId: number | null = null
246
+ app.handle('starter.blocker', () => {
247
+ if (blockerId != null) {
248
+ const ok = powerSaveBlocker.stop(blockerId)
249
+ blockerId = null
250
+ return { on: false, ok }
251
+ }
252
+ blockerId = powerSaveBlocker.start('prevent-display-sleep')
253
+ return { on: true, id: blockerId }
254
+ })
255
+ })
256
+
257
+ // ── C1: exponer `app` al renderer para probarlo en el starter ──────────────
258
+ // El renderer llama `ow.invoke('node','call',{fn:'starter.*'})`.
259
+ app.handle('starter.appInfo', () => ({
260
+ name: app.getName(),
261
+ version: app.getVersion(),
262
+ packaged: app.isPackaged(),
263
+ appPath: app.getAppPath(),
264
+ }))
265
+
266
+ const APP_PATH_NAMES = [
267
+ 'home',
268
+ 'userData',
269
+ 'temp',
270
+ 'logs',
271
+ 'downloads',
272
+ 'documents',
273
+ 'exe',
274
+ 'appPath',
275
+ ] as const
276
+
277
+ app.handle('starter.appPaths', () => {
278
+ const out: Record<string, string> = {}
279
+ for (const name of APP_PATH_NAMES) out[name] = app.getPath(name)
280
+ return out
23
281
  })
282
+
283
+ // Ciclo de vida de la app → renderer (app.send → ow.on('app.event')).
284
+ for (const ev of [
285
+ 'window-all-closed',
286
+ 'before-quit',
287
+ 'will-quit',
288
+ 'second-instance',
289
+ 'child-process-gone',
290
+ ] as const) {
291
+ app.on(ev, () => app.send('app.event', ev))
292
+ }
@@ -31,7 +31,6 @@
31
31
 
32
32
  <main class="page">
33
33
  <section class="hero">
34
- <span class="eyebrow">Owear</span>
35
34
  <h1>__APP_NAME__</h1>
36
35
  <p class="lead">
37
36
  Frontend web, sistema nativo. Estos botones llaman <strong>directo</strong> a los
@@ -39,6 +38,28 @@
39
38
  </p>
40
39
  <div class="actions">
41
40
  <button class="btn btn--primary" data-action="open">Abrir archivo…</button>
41
+ <button class="btn" data-action="multi">Abrir varios…</button>
42
+ <button class="btn" data-action="save">Guardar como…</button>
43
+ <button class="btn" data-action="capture">Capturar página</button>
44
+ <button class="btn" data-action="nimage">nativeImage</button>
45
+ <button class="btn" data-action="menu">Menú contextual</button>
46
+ <button class="btn" data-action="tray">Tray</button>
47
+ <button class="btn btn--ghost" data-action="theme-system">Tema: sistema</button>
48
+ <button class="btn btn--ghost" data-action="theme-light">Claro</button>
49
+ <button class="btn btn--ghost" data-action="theme-dark">Oscuro</button>
50
+ <button class="btn" data-action="pdf">Exportar PDF</button>
51
+ <button class="btn" data-action="print">Imprimir</button>
52
+ <button class="btn" data-action="child">Ventana 2</button>
53
+ <button class="btn" data-action="ontop">AlwaysOnTop</button>
54
+ <button class="btn" data-action="prog">Progreso</button>
55
+ <button class="btn" data-action="ignore">Ignorar ratón</button>
56
+ <button class="btn" data-action="wins">Ventanas</button>
57
+ <button class="btn" data-action="screens">Pantallas</button>
58
+ <button class="btn" data-action="nearest">Monitor del cursor</button>
59
+ <button class="btn btn--ghost" data-action="power-on">Escuchar energía</button>
60
+ <button class="btn btn--ghost" data-action="blocker">Bloqueador ON/OFF</button>
61
+ <button class="btn btn--ghost" data-action="idle">Idle</button>
62
+ <button class="btn" data-action="ask">Diálogo con botones</button>
42
63
  <button class="btn" data-action="notify">Notificar</button>
43
64
  <button class="btn" data-action="copy">Copiar ruta</button>
44
65
  <button class="btn" data-action="reveal">Mostrar en carpeta</button>
@@ -46,37 +67,105 @@
46
67
  </div>
47
68
  </section>
48
69
 
49
- <section class="grid">
50
- <article class="card">
51
- <header class="card__head">
52
- <h2>Entorno</h2>
53
- <span class="badge" id="env-window">—</span>
54
- </header>
55
- <dl class="kv">
56
- <div><dt>Plataforma</dt><dd id="env-platform">—</dd></div>
57
- <div><dt>Motor</dt><dd id="env-engine">—</dd></div>
58
- <div><dt>Escala</dt><dd id="env-dpr">—</dd></div>
59
- </dl>
60
- <p class="ua" id="env-ua">—</p>
61
- </article>
70
+ <!-- Contenedor con fondo propio que agrupa las cards -->
71
+ <section class="board">
72
+ <section class="grid">
73
+ <article class="card">
74
+ <header class="card__head">
75
+ <h2>Entorno</h2>
76
+ <span class="badge" id="env-window">—</span>
77
+ </header>
78
+ <dl class="kv">
79
+ <div><dt>Plataforma</dt><dd id="env-platform">—</dd></div>
80
+ <div><dt>Motor</dt><dd id="env-engine">—</dd></div>
81
+ <div><dt>Escala</dt><dd id="env-dpr">—</dd></div>
82
+ </dl>
83
+ <p class="ua" id="env-ua">—</p>
84
+ </article>
62
85
 
63
- <article class="card card--grow">
64
- <header class="card__head">
65
- <h2>Consola</h2>
66
- <button class="linkbtn" data-action="clear">Limpiar</button>
67
- </header>
68
- <pre id="console" class="console">Listo. Pulsa «Abrir archivo…» para invocar un módulo nativo.</pre>
69
- </article>
70
- </section>
86
+ <article class="card">
87
+ <header class="card__head">
88
+ <h2>App <span class="hint">C1</span></h2>
89
+ <span class="badge" id="app-version">—</span>
90
+ </header>
91
+ <dl class="kv">
92
+ <div><dt>Nombre</dt><dd id="app-name">—</dd></div>
93
+ <div><dt>Empaquetada</dt><dd id="app-packaged">—</dd></div>
94
+ <div><dt>userData</dt><dd id="app-path-userData" class="mono">—</dd></div>
95
+ <div><dt>exe</dt><dd id="app-path-exe" class="mono">—</dd></div>
96
+ <div><dt>appPath</dt><dd id="app-path-appPath" class="mono">—</dd></div>
97
+ </dl>
98
+ </article>
71
99
 
72
- <section class="card">
73
- <header class="card__head"><h2>Cómo funciona</h2></header>
74
- <p class="hint">
75
- Cada botón invoca un módulo nativo con <code>ow.invoke()</code>.
76
- </p>
77
- <pre class="code"><code>const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
100
+ <article class="card card--grow">
101
+ <header class="card__head">
102
+ <h2>Consola</h2>
103
+ <button class="linkbtn" data-action="clear">Limpiar</button>
104
+ </header>
105
+ <pre id="console" class="console">Listo. Pulsa «Abrir archivo…» para invocar un módulo nativo.</pre>
106
+ </article>
107
+ </section>
108
+
109
+ <section class="card">
110
+ <header class="card__head"><h2>Cómo funciona</h2></header>
111
+ <p class="hint">
112
+ Cada botón invoca un módulo nativo con <code>ow.invoke()</code>.
113
+ </p>
114
+ <pre class="code"><code>const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
78
115
  const text = await ow.invoke('fs', 'readText', path)
79
116
  await ow.invoke('notification', 'show', 'Listo', 'Leídos ' + text.length + ' bytes')</code></pre>
117
+ </section>
118
+ </section>
119
+
120
+ <!-- C10: pantallas (multi-monitor) y energía -->
121
+ <section class="board">
122
+ <header class="card__head">
123
+ <h2>Pantallas y energía <span class="hint">C10</span></h2>
124
+ <span class="badge" id="mon-count">—</span>
125
+ </header>
126
+ <section class="grid">
127
+ <article class="card">
128
+ <header class="card__head">
129
+ <h2>Monitores</h2>
130
+ <span class="badge" id="mon-primary">—</span>
131
+ </header>
132
+ <div id="mon-map" class="monmap">
133
+ <span class="monmap__hint">Pulsa «Pantallas» para mapear los monitores.</span>
134
+ </div>
135
+ <div id="mon-list" class="monlist"></div>
136
+ </article>
137
+ <article class="card card--grow">
138
+ <header class="card__head">
139
+ <h2>Energía</h2>
140
+ <span class="badge" id="power-status">—</span>
141
+ </header>
142
+ <p class="hint">
143
+ Escucha suspend/resume, AC/batería y bloqueo de sesión. El bloqueador
144
+ impide que se apague la pantalla.
145
+ </p>
146
+ <pre id="power-log" class="console console--short">Sin eventos todavía.</pre>
147
+ </article>
148
+ </section>
149
+ </section>
150
+
151
+ <!-- Webview embebida: un navegador nativo hijo de la ventana (proceso
152
+ propio), controlado por la API webview.* y colocado sobre #wv-slot. -->
153
+ <section class="board">
154
+ <header class="card__head">
155
+ <h2>Navegador embebido</h2>
156
+ <span class="badge" id="wv-status">—</span>
157
+ </header>
158
+ <div class="browser">
159
+ <div class="browser__bar">
160
+ <button class="iconbtn" id="wv-back" title="Atrás" aria-label="Atrás">‹</button>
161
+ <button class="iconbtn" id="wv-fwd" title="Adelante" aria-label="Adelante">›</button>
162
+ <button class="iconbtn" id="wv-reload" title="Recargar" aria-label="Recargar">⟳</button>
163
+ <input id="wv-url" class="browser__url" type="text"
164
+ value="https://scrakk.art" spellcheck="false" />
165
+ <button class="btn btn--primary" id="wv-go">Ir</button>
166
+ </div>
167
+ <div id="wv-slot" class="browser__slot"></div>
168
+ </div>
80
169
  </section>
81
170
  </main>
82
171
 
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "description": "App de escritorio construida con Owear.",
6
+ "description": "App de escritorio construida con Owear (Starter: titlebar propia, módulos nativos y UI real).",
7
7
  "scripts": {
8
8
  "dev": "ow dev",
9
9
  "build": "ow build",
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect width="64" height="64" rx="14" fill="#282c34"/>
3
+ <rect x="18" y="18" width="28" height="28" rx="7" fill="#d3869b"/>
4
+ </svg>
@@ -2,13 +2,21 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  //
4
4
  // Tipos de la API que el kernel inyecta en cada documento (`window.ow`).
5
- // Mantenidos a mano contra docs/API.md §1.
5
+ // Types maintained by hand against docs/api/renderer.md.
6
6
 
7
7
  export {}
8
8
 
9
9
  declare global {
10
10
  interface Window {
11
11
  __owWindowId: number
12
+ /** Presente cuando la ventana usa `titleBarOverlay` (botones nativos). */
13
+ __owTitlebarOverlay?: {
14
+ enabled: boolean
15
+ height: number
16
+ width: number
17
+ top?: number
18
+ right?: number
19
+ }
12
20
  ow: {
13
21
  invoke(module: string, fn: string, ...args: unknown[]): Promise<unknown>
14
22
  invokeSync(module: string, fn: string, ...args: unknown[]): unknown