@owear/cli 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owear/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI de Owear: create, dev, build",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  "owear-build-native": "./src/owear-build-native.js"
9
9
  },
10
10
  "optionalDependencies": {
11
- "@owear/linux-x64-gnu": "0.1.0"
11
+ "@owear/win32-x64": "0.1.2",
12
+ "@owear/linux-x64-gnu": "0.1.2"
12
13
  },
13
14
  "files": [
14
15
  "src",
@@ -18,7 +19,7 @@
18
19
  "homepage": "https://owear.dev",
19
20
  "repository": {
20
21
  "type": "git",
21
- "url": "git+https://github.com/owear/owear.git",
22
+ "url": "git+https://github.com/Scrakk-Team/Owear-Framework.git",
22
23
  "directory": "packages/cli"
23
24
  },
24
25
  "publishConfig": {
package/src/ow.js CHANGED
@@ -90,9 +90,9 @@ function findKernelBin(cwd = process.cwd()) {
90
90
  const rt = runtimePackageDir()
91
91
  const candidates = [
92
92
  path.join(cwd, '.owear', 'bin', exe),
93
- ...(rt ? [path.join(rt, 'bin', exe)] : []),
94
- // monorepo en desarrollo
93
+ // monorepo en desarrollo: el kernel recién compilado manda sobre el paquete npm
95
94
  path.resolve(__dirname, '../../../build', platformPreset(), 'src', exe),
95
+ ...(rt ? [path.join(rt, 'bin', exe)] : []),
96
96
  ]
97
97
  for (const c of candidates) if (fs.existsSync(c)) return c
98
98
  return null
@@ -171,11 +171,41 @@ function cmdCreate(args) {
171
171
  die(`template no encontrado en ${templateDir} (instala @owear/cli completo)`)
172
172
  }
173
173
  fs.cpSync(templateDir, target, { recursive: true })
174
+ applyAppName(target)
174
175
  log(`app creada en ${target}`)
175
176
  log('siguientes pasos:')
176
177
  console.log(` cd ${path.basename(target)}`)
177
- console.log(' pnpm install')
178
- console.log(' pnpm dev')
178
+ console.log(' npm install')
179
+ console.log(' npm run dev')
180
+ }
181
+
182
+ /**
183
+ * Sustituye los placeholders del template por el nombre de la app
184
+ * (__APP_NAME__ legible, __PKG_NAME__ válido como nombre de paquete npm).
185
+ */
186
+ function applyAppName(target) {
187
+ const appName = path.basename(target)
188
+ const pkgName =
189
+ appName
190
+ .toLowerCase()
191
+ .replace(/[^a-z0-9._-]+/g, '-')
192
+ .replace(/^[-._]+|[-._]+$/g, '') || 'owear-app'
193
+ const extRe = /\.(ts|tsx|js|mjs|json|html|css|md|txt|ya?ml)$/
194
+ const walk = (dir) => {
195
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
196
+ if (entry.name === 'node_modules') continue
197
+ const p = path.join(dir, entry.name)
198
+ if (entry.isDirectory()) walk(p)
199
+ else if (extRe.test(entry.name)) {
200
+ const src = fs.readFileSync(p, 'utf8')
201
+ const out = src
202
+ .replaceAll('__APP_NAME__', appName)
203
+ .replaceAll('__PKG_NAME__', pkgName)
204
+ if (out !== src) fs.writeFileSync(p, out)
205
+ }
206
+ }
207
+ }
208
+ walk(target)
179
209
  }
180
210
 
181
211
  // ── ow api — gestión de las APIs del framework ──────────────────────────────
@@ -0,0 +1,77 @@
1
+ <!-- Copyright 2026 Owear Contributors
2
+ SPDX-License-Identifier: Apache-2.0 -->
3
+
4
+ # __APP_NAME__
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.
8
+
9
+ ## Empezar
10
+
11
+ ```bash
12
+ npm install
13
+ npm run dev # ≡ ow dev
14
+ ```
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.
18
+
19
+ ```bash
20
+ npm run build # ≡ ow build → dist/
21
+ ```
22
+
23
+ ## Estructura
24
+
25
+ ```
26
+ ├── index.html UI (titlebar propia, hero, tarjetas)
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
31
+ └── app/
32
+ └── main.ts proceso principal (sidecar Node): sólo la ventana
33
+ ```
34
+
35
+ ## Cómo se llama al sistema
36
+
37
+ Sin `ipcRenderer` ni `ipcMain`: el renderer invoca los módulos nativos del
38
+ kernel directo.
39
+
40
+ ```ts
41
+ const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
42
+ const text = await ow.invoke('fs', 'readText', path)
43
+ await ow.invoke('notification', 'show', 'Listo', `Leídos ${text.length} bytes`)
44
+ ```
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`.
50
+
51
+ ## Añadir un módulo nativo propio (C++)
52
+
53
+ 1. Crea `native/mi_modulo.cpp`:
54
+
55
+ ```cpp
56
+ #include <ow/Json.h>
57
+ #include <ow/Module.h>
58
+ #include "ow_api.h"
59
+
60
+ static void ping(const ow_request_t*, ow_response_t* res) {
61
+ ow::Module::RespondOk(res, "\"pong\"");
62
+ }
63
+
64
+ OW_MODULE_BEGIN(mi_modulo, "1.0.0")
65
+ OW_FN(ping)
66
+ OW_MODULE_END()
67
+ ```
68
+
69
+ 2. `ow dev` lo compila a `.owm` y genera el binding. Úsalo en el renderer:
70
+
71
+ ```ts
72
+ import { mi_modulo } from '@owear/native'
73
+ await mi_modulo.ping()
74
+ ```
75
+
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.
@@ -1,29 +1,23 @@
1
1
  // Copyright 2026 Owear Contributors
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  //
4
- // main.ts — proceso principal (sidecar Node). Estilo Electron.
4
+ // app/main.ts — proceso principal (sidecar Node), estilo Electron.
5
5
  //
6
- import { app, BrowserWindow } from '@owear/core'
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).
7
9
 
8
- let windows = 0
10
+ import { app, BrowserWindow } from '@owear/core'
9
11
 
10
12
  app.whenReady().then(() => {
11
13
  const win = new BrowserWindow({
12
- title: 'Mi App Owear',
13
- width: 1100,
14
+ title: '__APP_NAME__',
15
+ width: 1080,
14
16
  height: 720,
15
- titleBarStyle: 'custom', // titlebar custom + drag regions CSS
17
+ titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
16
18
  url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
17
19
  })
18
- windows++
19
20
 
20
- win.on('closed', () => {
21
- windows--
22
- if (windows === 0) app.quit()
23
- })
24
-
25
- // ejemplo: evaluar JS en la página
26
- win.on('ready-to-show', async () => {
27
- console.log('[main] ventana lista, id =', win.id)
28
- })
21
+ win.on('ready-to-show', () => console.log('[main] ventana lista, id =', win.id))
22
+ win.on('closed', () => app.quit())
29
23
  })
@@ -1,24 +1,85 @@
1
1
  <!doctype html>
2
+ <!-- Copyright 2026 Owear Contributors
3
+ SPDX-License-Identifier: Apache-2.0 -->
4
+
2
5
  <html lang="es">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Mi App Owear</title>
7
- </head>
8
- <body>
9
- <header data-ow-drag class="titlebar">
10
- <span>Mi App Owear</span>
11
- <nav>
12
- <button data-ow-no-drag onclick="ow.invoke('ow-window','minimize', window.__owWindowId)">–</button>
13
- <button data-ow-no-drag onclick="ow.invoke('ow-window','maximize', window.__owWindowId, true)">□</button>
14
- <button data-ow-no-drag onclick="ow.invoke('ow-window','close', window.__owWindowId)">✕</button>
15
- </nav>
16
- </header>
17
- <main>
18
- <h1>Funciona 🚀</h1>
19
- <p id="demo">invocando módulo nativo…</p>
20
- <pre id="out"></pre>
21
- </main>
22
- <script type="module" src="/src/renderer.ts"></script>
23
- </body>
6
+ <head>
7
+ <meta charset="UTF-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
+ <title>__APP_NAME__</title>
10
+ </head>
11
+ <body>
12
+ <!-- Titlebar propia: el kernel convierte [data-ow-drag] en zona de arrastre.
13
+ Los controles van FUERA de esa zona para no iniciar drag al pulsarlos. -->
14
+ <header class="titlebar">
15
+ <div class="titlebar__drag" data-ow-drag>
16
+ <span class="brand__mark" aria-hidden="true"></span>
17
+ <span class="brand__name">__APP_NAME__</span>
18
+ </div>
19
+ <div class="titlebar__controls">
20
+ <button id="win-min" class="winbtn" title="Minimizar" aria-label="Minimizar">
21
+ <svg viewBox="0 0 12 12" width="12" height="12"><path d="M2 6h8" /></svg>
22
+ </button>
23
+ <button id="win-max" class="winbtn" title="Maximizar" aria-label="Maximizar">
24
+ <svg viewBox="0 0 12 12" width="12" height="12"><rect x="2.5" y="2.5" width="7" height="7" rx="1" /></svg>
25
+ </button>
26
+ <button id="win-close" class="winbtn winbtn--close" title="Cerrar" aria-label="Cerrar">
27
+ <svg viewBox="0 0 12 12" width="12" height="12"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" /></svg>
28
+ </button>
29
+ </div>
30
+ </header>
31
+
32
+ <main class="page">
33
+ <section class="hero">
34
+ <span class="eyebrow">Owear</span>
35
+ <h1>__APP_NAME__</h1>
36
+ <p class="lead">
37
+ Frontend web, sistema nativo. Estos botones llaman <strong>directo</strong> a los
38
+ módulos del kernel — sin <code>ipcRenderer</code> ni <code>ipcMain</code>.
39
+ </p>
40
+ <div class="actions">
41
+ <button class="btn btn--primary" data-action="open">Abrir archivo…</button>
42
+ <button class="btn" data-action="notify">Notificar</button>
43
+ <button class="btn" data-action="copy">Copiar ruta</button>
44
+ <button class="btn" data-action="reveal">Mostrar en carpeta</button>
45
+ <button class="btn btn--ghost" data-action="about">Acerca de</button>
46
+ </div>
47
+ </section>
48
+
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>
62
+
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>
71
+
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')
78
+ const text = await ow.invoke('fs', 'readText', path)
79
+ await ow.invoke('notification', 'show', 'Listo', 'Leídos ' + text.length + ' bytes')</code></pre>
80
+ </section>
81
+ </main>
82
+
83
+ <script type="module" src="/src/renderer.ts"></script>
84
+ </body>
24
85
  </html>
@@ -1,11 +1,13 @@
1
1
  {
2
- "name": "mi-app-owear",
2
+ "name": "__PKG_NAME__",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "description": "App de escritorio construida con Owear.",
6
7
  "scripts": {
7
8
  "dev": "ow dev",
8
- "build": "ow build"
9
+ "build": "ow build",
10
+ "typecheck": "tsc --noEmit"
9
11
  },
10
12
  "dependencies": {
11
13
  "@owear/core": "^0.1.0"
@@ -13,6 +15,7 @@
13
15
  "devDependencies": {
14
16
  "@owear/cli": "^0.1.0",
15
17
  "@owear/vite-plugin": "^0.1.0",
18
+ "@types/node": "^20.14.0",
16
19
  "esbuild": "^0.25.0",
17
20
  "typescript": "^5.6.0",
18
21
  "vite": "^6.0.0"
@@ -0,0 +1,25 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // Tipos de la API que el kernel inyecta en cada documento (`window.ow`).
5
+ // Mantenidos a mano contra docs/API.md §1.
6
+
7
+ export {}
8
+
9
+ declare global {
10
+ interface Window {
11
+ __owWindowId: number
12
+ ow: {
13
+ invoke(module: string, fn: string, ...args: unknown[]): Promise<unknown>
14
+ invokeSync(module: string, fn: string, ...args: unknown[]): unknown
15
+ readShared(handle: { id: string; size: number }): Promise<ArrayBuffer>
16
+ on(name: string, cb: (payload: unknown) => void): () => void
17
+ emit(name: string, payload?: unknown): void
18
+ emitTo(targetWindowId: number, name: string, payload?: unknown): void
19
+ findInPage(
20
+ text: string,
21
+ opts?: { matchCase?: boolean; backwards?: boolean }
22
+ ): { matches: number; active: number }
23
+ }
24
+ }
25
+ }
@@ -1,30 +1,144 @@
1
1
  // Copyright 2026 Owear Contributors
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  //
4
- // renderer.ts — corre dentro del WebView. `ow` lo inyecta el kernel.
4
+ // src/renderer.ts — corre dentro del WebView. `ow` lo inyecta el kernel.
5
5
  //
6
- import { files } from '@owear/native'
6
+ // Aquí no hay `ipcRenderer` ni handlers en el main: el renderer llama a los
7
+ // módulos nativos del framework DIRECTO (mismo salto WebView → kernel).
7
8
 
8
- const out = document.getElementById('out')!
9
- const demo = document.getElementById('demo')!
9
+ import './style.css'
10
10
 
11
- async function main() {
12
- try {
13
- // módulo nativo PROPIO (native/files.cpp)
14
- const text = await files.readText('/etc/hostname')
15
- demo.textContent = `hostname leído desde C++ nativo: ${text.trim()}`
11
+ const APP = document.title || 'Owear App'
16
12
 
17
- // módulo stock fs
18
- const st = await ow.invoke('fs', 'stat', '/etc/hosts')
19
- out.textContent = JSON.stringify(st, null, 2)
13
+ const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
14
+ document.querySelector(sel) as T
20
15
 
21
- // eventos de ventana
22
- ow.on('resize', (b) => {
23
- document.title = `${b.width}×${b.height}`
16
+ const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
17
+ window.ow.invoke(module, fn, ...args) as Promise<T>
18
+
19
+ let lastPath: string | null = null
20
+
21
+ // ── Consola ─────────────────────────────────────────────────────────────
22
+ const consoleEl = $('#console')
23
+
24
+ function log(msg: string, kind: 'ok' | 'err' | 'muted' = 'muted'): void {
25
+ const time = new Date().toLocaleTimeString()
26
+ const line = document.createElement('span')
27
+ line.className = `line--${kind}`
28
+ line.textContent = `[${time}] ${msg}\n`
29
+ consoleEl.appendChild(line)
30
+ consoleEl.scrollTop = consoleEl.scrollHeight
31
+ }
32
+
33
+ function logError(err: unknown): void {
34
+ log(err instanceof Error ? err.message : String(err), 'err')
35
+ }
36
+
37
+ // ── Entorno ─────────────────────────────────────────────────────────────
38
+ function renderEnvironment(): void {
39
+ const ua = navigator.userAgent
40
+ const engine = /AppleWebKit\/([\d.]+)/.exec(ua)?.[1]
41
+ $('#env-platform').textContent = navigator.platform || '—'
42
+ $('#env-engine').textContent = engine ? `WebKit ${engine}` : '—'
43
+ $('#env-dpr').textContent = `${window.devicePixelRatio}×`
44
+ $('#env-window').textContent = `#${window.__owWindowId}`
45
+ $('#env-ua').textContent = ua
46
+ }
47
+
48
+ // ── Acciones (módulos nativos) ──────────────────────────────────────────
49
+ async function openFile(): Promise<void> {
50
+ const path = await invoke<string | null>('dialog', 'open', 'open', 'Abrir un archivo')
51
+ if (!path) {
52
+ log('dialog.open → cancelado')
53
+ return
54
+ }
55
+ lastPath = path
56
+ const stat = await invoke<{ size: number }>('fs', 'stat', path)
57
+ const text = await invoke<string>('fs', 'readText', path)
58
+ log(`dialog.open → ${path}`, 'ok')
59
+ log(`fs.readText → ${stat.size} bytes`, 'ok')
60
+
61
+ const clean = text.replace(/\u0000/g, '')
62
+ const preview = clean.length > 6000 ? `${clean.slice(0, 6000)}\n… (vista truncada)` : clean
63
+ consoleEl.textContent = ''
64
+ log(`── ${path} (${stat.size} bytes) ──`, 'muted')
65
+ const body = document.createElement('span')
66
+ body.style.color = '#c8d4e0'
67
+ body.textContent = preview + '\n'
68
+ consoleEl.appendChild(body)
69
+ consoleEl.scrollTop = 0
70
+ }
71
+
72
+ async function notifyUser(): Promise<void> {
73
+ await invoke('notification', 'show', APP, 'Los módulos nativos funcionan sin IPC.', APP)
74
+ log('notification.show → notificación enviada', 'ok')
75
+ }
76
+
77
+ async function copyPath(): Promise<void> {
78
+ if (!lastPath) {
79
+ log('Abre un archivo antes de copiar.', 'err')
80
+ return
81
+ }
82
+ await invoke('clipboard', 'writeText', lastPath)
83
+ log(`clipboard.writeText → ${lastPath}`, 'ok')
84
+ }
85
+
86
+ async function revealPath(): Promise<void> {
87
+ if (!lastPath) {
88
+ log('Abre un archivo antes de mostrarlo.', 'err')
89
+ return
90
+ }
91
+ await invoke('shell', 'showItemInFolder', lastPath)
92
+ log(`shell.showItemInFolder → ${lastPath}`, 'ok')
93
+ }
94
+
95
+ async function about(): Promise<void> {
96
+ await invoke('dialog', 'messageBox', 'info', `Acerca de ${APP}`, `${APP}\n\nConstruido con Owear.`, [
97
+ 'Cerrar',
98
+ ])
99
+ log('dialog.messageBox → cerrado', 'ok')
100
+ }
101
+
102
+ // ── Titlebar (builtin ow-window) ────────────────────────────────────────
103
+ function wireTitlebar(): void {
104
+ const id = window.__owWindowId
105
+ $('#win-min').addEventListener('click', () => void invoke('ow-window', 'minimize', id))
106
+ $('#win-max').addEventListener('click', async () => {
107
+ const max = await invoke<boolean>('ow-window', 'isMaximized', id)
108
+ void invoke('ow-window', 'maximize', id, !max)
109
+ })
110
+ $('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', id))
111
+ }
112
+
113
+ // ── Arranque ────────────────────────────────────────────────────────────
114
+ const actions: Record<string, () => void | Promise<void>> = {
115
+ open: openFile,
116
+ notify: notifyUser,
117
+ copy: copyPath,
118
+ reveal: revealPath,
119
+ about,
120
+ clear: () => {
121
+ consoleEl.textContent = ''
122
+ log('Consola limpia.')
123
+ },
124
+ }
125
+
126
+ function boot(): void {
127
+ wireTitlebar()
128
+ renderEnvironment()
129
+
130
+ for (const btn of document.querySelectorAll<HTMLElement>('[data-action]')) {
131
+ const action = actions[btn.dataset.action ?? '']
132
+ if (!action) continue
133
+ btn.addEventListener('click', () => {
134
+ Promise.resolve(action()).catch(logError)
24
135
  })
25
- } catch (e) {
26
- demo.textContent = '✗ ' + (e as Error).message
27
136
  }
137
+
138
+ window.addEventListener('resize', renderEnvironment)
139
+ window.ow.on('resize', renderEnvironment)
140
+
141
+ log(`Renderer listo. Ventana ${window.__owWindowId}.`, 'ok')
28
142
  }
29
143
 
30
- main()
144
+ boot()
@@ -0,0 +1,342 @@
1
+ /* Copyright 2026 Owear Contributors
2
+ * SPDX-License-Identifier: Apache-2.0
3
+ */
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+ --bg: #0d1117;
8
+ --panel: #161b22;
9
+ --panel-2: #0f141a;
10
+ --line: #262d38;
11
+ --line-2: #30363d;
12
+ --text: #e6edf3;
13
+ --muted: #8b949e;
14
+ --accent: #3b82f6;
15
+ --accent-hover: #2f6fd0;
16
+ --danger: #e5484d;
17
+ --ok: #3fb950;
18
+ --radius: 12px;
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ html,
26
+ body {
27
+ height: 100%;
28
+ }
29
+
30
+ body {
31
+ margin: 0;
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ font: 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
35
+ display: flex;
36
+ flex-direction: column;
37
+ overflow: hidden;
38
+ user-select: none;
39
+ -webkit-user-select: none;
40
+ }
41
+
42
+ /* ── Titlebar ─────────────────────────────────────────────────────────── */
43
+ .titlebar {
44
+ height: 40px;
45
+ flex: 0 0 auto;
46
+ display: flex;
47
+ align-items: stretch;
48
+ background: #010409;
49
+ border-bottom: 1px solid var(--line);
50
+ }
51
+
52
+ .titlebar__drag {
53
+ flex: 1;
54
+ min-width: 0;
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 10px;
58
+ padding: 0 14px;
59
+ }
60
+
61
+ .brand__mark {
62
+ width: 12px;
63
+ height: 12px;
64
+ border-radius: 3px;
65
+ background: var(--accent);
66
+ }
67
+
68
+ .brand__name {
69
+ font-weight: 600;
70
+ letter-spacing: 0.2px;
71
+ white-space: nowrap;
72
+ overflow: hidden;
73
+ text-overflow: ellipsis;
74
+ }
75
+
76
+ .titlebar__controls {
77
+ display: flex;
78
+ }
79
+
80
+ .winbtn {
81
+ width: 46px;
82
+ border: 0;
83
+ background: transparent;
84
+ color: var(--muted);
85
+ cursor: pointer;
86
+ display: grid;
87
+ place-items: center;
88
+ }
89
+
90
+ .winbtn svg {
91
+ fill: none;
92
+ stroke: currentColor;
93
+ stroke-width: 1.4;
94
+ }
95
+
96
+ .winbtn:hover {
97
+ background: var(--line);
98
+ color: var(--text);
99
+ }
100
+
101
+ .winbtn--close:hover {
102
+ background: var(--danger);
103
+ color: #fff;
104
+ }
105
+
106
+ /* ── Layout ───────────────────────────────────────────────────────────── */
107
+ .page {
108
+ flex: 1;
109
+ min-height: 0;
110
+ overflow: auto;
111
+ padding: 28px clamp(18px, 4vw, 44px) 36px;
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 20px;
115
+ max-width: 1040px;
116
+ width: 100%;
117
+ margin: 0 auto;
118
+ }
119
+
120
+ .hero {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 12px;
124
+ }
125
+
126
+ .eyebrow {
127
+ font-size: 12px;
128
+ font-weight: 600;
129
+ letter-spacing: 0.08em;
130
+ text-transform: uppercase;
131
+ color: var(--muted);
132
+ }
133
+
134
+ h1 {
135
+ margin: 0;
136
+ font-size: clamp(24px, 3vw, 32px);
137
+ line-height: 1.2;
138
+ letter-spacing: -0.01em;
139
+ }
140
+
141
+ .lead {
142
+ margin: 0;
143
+ max-width: 72ch;
144
+ color: var(--muted);
145
+ font-size: 15px;
146
+ }
147
+
148
+ .lead strong {
149
+ color: var(--text);
150
+ }
151
+
152
+ .actions {
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: 8px;
156
+ margin-top: 4px;
157
+ }
158
+
159
+ .btn {
160
+ font: inherit;
161
+ color: var(--text);
162
+ background: #21262d;
163
+ border: 1px solid var(--line-2);
164
+ border-radius: 8px;
165
+ padding: 8px 14px;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .btn:hover {
170
+ background: #2b323b;
171
+ }
172
+
173
+ .btn:active {
174
+ transform: translateY(1px);
175
+ }
176
+
177
+ .btn--primary {
178
+ background: var(--accent);
179
+ border-color: var(--accent);
180
+ color: #fff;
181
+ font-weight: 600;
182
+ }
183
+
184
+ .btn--primary:hover {
185
+ background: var(--accent-hover);
186
+ border-color: var(--accent-hover);
187
+ }
188
+
189
+ .btn--ghost {
190
+ background: transparent;
191
+ }
192
+
193
+ .grid {
194
+ display: grid;
195
+ grid-template-columns: minmax(240px, 300px) 1fr;
196
+ gap: 16px;
197
+ }
198
+
199
+ @media (max-width: 820px) {
200
+ .grid {
201
+ grid-template-columns: 1fr;
202
+ }
203
+ }
204
+
205
+ .card {
206
+ background: var(--panel);
207
+ border: 1px solid var(--line);
208
+ border-radius: var(--radius);
209
+ padding: 16px 18px;
210
+ }
211
+
212
+ .card--grow {
213
+ min-width: 0;
214
+ display: flex;
215
+ flex-direction: column;
216
+ }
217
+
218
+ .card__head {
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: space-between;
222
+ margin-bottom: 12px;
223
+ }
224
+
225
+ .card__head h2 {
226
+ margin: 0;
227
+ font-size: 12px;
228
+ text-transform: uppercase;
229
+ letter-spacing: 0.07em;
230
+ color: var(--muted);
231
+ }
232
+
233
+ .badge {
234
+ font-size: 12px;
235
+ color: var(--muted);
236
+ background: var(--panel-2);
237
+ border: 1px solid var(--line);
238
+ border-radius: 999px;
239
+ padding: 2px 10px;
240
+ font-variant-numeric: tabular-nums;
241
+ }
242
+
243
+ .kv {
244
+ margin: 0;
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: 8px;
248
+ }
249
+
250
+ .kv > div {
251
+ display: flex;
252
+ justify-content: space-between;
253
+ gap: 12px;
254
+ }
255
+
256
+ .kv dt {
257
+ color: var(--muted);
258
+ }
259
+
260
+ .kv dd {
261
+ margin: 0;
262
+ font-variant-numeric: tabular-nums;
263
+ }
264
+
265
+ .ua {
266
+ margin: 14px 0 0;
267
+ color: #5b6672;
268
+ font-size: 12px;
269
+ word-break: break-word;
270
+ }
271
+
272
+ .console {
273
+ flex: 1;
274
+ min-height: 210px;
275
+ max-height: 46vh;
276
+ margin: 0;
277
+ overflow: auto;
278
+ background: #010409;
279
+ border: 1px solid var(--line);
280
+ border-radius: 8px;
281
+ padding: 12px 14px;
282
+ font: 12.5px/1.6 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
283
+ white-space: pre-wrap;
284
+ word-break: break-word;
285
+ user-select: text;
286
+ -webkit-user-select: text;
287
+ }
288
+
289
+ .console .line--ok {
290
+ color: var(--ok);
291
+ }
292
+
293
+ .console .line--err {
294
+ color: #ff8085;
295
+ }
296
+
297
+ .console .line--muted {
298
+ color: var(--muted);
299
+ }
300
+
301
+ .linkbtn {
302
+ font: inherit;
303
+ background: transparent;
304
+ border: 0;
305
+ color: var(--muted);
306
+ cursor: pointer;
307
+ }
308
+
309
+ .linkbtn:hover {
310
+ color: var(--text);
311
+ }
312
+
313
+ .hint {
314
+ margin: 0 0 12px;
315
+ color: var(--muted);
316
+ }
317
+
318
+ .code {
319
+ margin: 0;
320
+ overflow: auto;
321
+ background: #010409;
322
+ border: 1px solid var(--line);
323
+ border-radius: 8px;
324
+ padding: 14px;
325
+ font: 12.5px/1.7 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
326
+ color: #c9d1d9;
327
+ user-select: text;
328
+ -webkit-user-select: text;
329
+ }
330
+
331
+ code {
332
+ color: #79c0ff;
333
+ }
334
+
335
+ .hint code,
336
+ .lead code {
337
+ background: #21262d;
338
+ border: 1px solid var(--line);
339
+ border-radius: 6px;
340
+ padding: 1px 6px;
341
+ font-size: 12.5px;
342
+ }
@@ -3,10 +3,11 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "Bundler",
6
- "lib": ["ES2022", "DOM"],
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["node", "vite/client"],
7
8
  "strict": true,
8
9
  "skipLibCheck": true,
9
- "types": ["vite/client"]
10
+ "noEmit": true
10
11
  },
11
12
  "include": ["src", "app"]
12
13
  }
@@ -1,3 +1,6 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
1
4
  import { defineConfig } from 'vite'
2
5
  import owear from '@owear/vite-plugin'
3
6
 
@@ -1,42 +0,0 @@
1
- // Copyright 2026 Owear Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
- //
4
- // native/files.cpp — módulo nativo PROPIO de la app.
5
- // Se compila a .owm y se invoca desde el renderer con:
6
- // import { files } from '@owear/native'
7
- // await files.readText('/etc/hostname')
8
- //
9
- #include <ow/Json.h>
10
- #include <ow/Module.h>
11
- #include "ow_api.h"
12
-
13
- #include <filesystem>
14
- #include <fstream>
15
- #include <sstream>
16
-
17
- namespace fs = std::filesystem;
18
-
19
- static void readText(const ow_request_t* req, ow_response_t* res) {
20
- auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
21
- if (!parsed.value || !parsed.value->IsArray() || !parsed.value->AsArray()[0].IsString())
22
- return ow::Module::RespondError(res, "readText(path): path requerido");
23
- std::ifstream f(parsed.value->AsArray()[0].AsString(), std::ios::binary);
24
- if (!f) return ow::Module::RespondError(res, "no se pudo abrir");
25
- std::ostringstream ss;
26
- ss << f.rdbuf();
27
- ow::Module::RespondOk(res, ow::json::Value(ss.str()).Serialize().c_str());
28
- }
29
-
30
- static void exists(const ow_request_t* req, ow_response_t* res) {
31
- auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
32
- if (!parsed.value || !parsed.value->IsArray() || !parsed.value->AsArray()[0].IsString())
33
- return ow::Module::RespondError(res, "exists(path): path requerido");
34
- std::error_code ec;
35
- bool ok = fs::exists(parsed.value->AsArray()[0].AsString(), ec);
36
- ow::Module::RespondOk(res, ok ? "true" : "false");
37
- }
38
-
39
- OW_MODULE_BEGIN(files, "1.0.0")
40
- OW_FN(readText)
41
- OW_FN(exists)
42
- OW_MODULE_END()