@yakuzaa/jade 0.1.5 → 0.1.7

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.
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { spawn } from 'child_process';
15
15
  import { resolve, basename, dirname, join } from 'path';
16
- import { existsSync } from 'fs';
16
+ import { existsSync, readFileSync } from 'fs';
17
17
  import { createRequire } from 'module';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { gerarHTML_dist } from './html.js';
@@ -99,7 +99,33 @@ export async function compilar(args) {
99
99
 
100
100
  // Geração de HTML (a menos que --so-wasm)
101
101
  if (!soWasm) {
102
- const nomeProjeto = basename(arquivo, '.jd');
103
- await gerarHTML_dist({ prefixo, nome: nomeProjeto });
102
+ // jade.config.json se existir (tema, nome do projeto)
103
+ let tema = {};
104
+ let nomeProjeto = basename(arquivo, '.jd');
105
+ const configCandidatos = [
106
+ join(process.cwd(), 'config', 'jade.config.json'),
107
+ join(process.cwd(), 'jade.config.json'),
108
+ ];
109
+ for (const cfg of configCandidatos) {
110
+ if (existsSync(cfg)) {
111
+ try {
112
+ const c = JSON.parse(readFileSync(cfg, 'utf-8'));
113
+ if (c.nome) nomeProjeto = c.nome;
114
+ if (c.tema) tema = c.tema;
115
+ } catch { /* config inválido — ignora */ }
116
+ break;
117
+ }
118
+ }
119
+
120
+ // Seeds: procura seeds.json na raiz do projeto
121
+ const seedsOrigem = (() => {
122
+ const candidatos = [
123
+ join(process.cwd(), 'seeds.json'),
124
+ join(process.cwd(), 'seeds', 'seeds.json'),
125
+ ];
126
+ return candidatos.find(p => existsSync(p)) ?? null;
127
+ })();
128
+
129
+ await gerarHTML_dist({ prefixo, nome: nomeProjeto, tema, seedsOrigem });
104
130
  }
105
131
  }
package/commands/html.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * Não depende de bundler no projeto do usuário.
12
12
  */
13
13
 
14
- import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'fs';
14
+ import { writeFileSync, copyFileSync, existsSync, mkdirSync } from 'fs';
15
15
  import { resolve, dirname, join, basename } from 'path';
16
16
  import { createRequire } from 'module';
17
17
 
@@ -19,25 +19,22 @@ const require = createRequire(import.meta.url);
19
19
 
20
20
  // ── Cores ─────────────────────────────────────────────────────────────────────
21
21
 
22
- const verde = (s) => `\x1b[1;32m${s}\x1b[0m`;
23
- const azul = (s) => `\x1b[34m${s}\x1b[0m`;
24
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
22
+ const verde = (s) => `\x1b[1;32m${s}\x1b[0m`;
23
+ const azul = (s) => `\x1b[34m${s}\x1b[0m`;
24
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
25
25
  const amarelo = (s) => `\x1b[1;33m${s}\x1b[0m`;
26
- const ok = () => verde('✓');
27
- const aviso = () => amarelo('⚠');
26
+ const ok = () => verde('✓');
27
+ const aviso = () => amarelo('⚠');
28
28
 
29
29
  // ── Localiza o browser.js do runtime instalado ────────────────────────────────
30
30
 
31
31
  function localizarRuntime() {
32
- // Tenta via require.resolve (funciona quando @yakuzaa/jade-runtime está instalado)
33
32
  try {
34
33
  const pkgPath = require.resolve('@yakuzaa/jade-runtime/package.json');
35
34
  const pkgDir = dirname(pkgPath);
36
35
  const browser = join(pkgDir, 'dist', 'browser.js');
37
36
  if (existsSync(browser)) return browser;
38
- } catch {
39
- // não instalado via npm — tenta caminho relativo ao monorepo
40
- }
37
+ } catch { /* não instalado via npm */ }
41
38
 
42
39
  // Fallback: monorepo local (desenvolvimento)
43
40
  const mono = resolve(dirname(new URL(import.meta.url).pathname), '..', '..', 'jade-runtime', 'dist', 'browser.js');
@@ -46,85 +43,472 @@ function localizarRuntime() {
46
43
  return null;
47
44
  }
48
45
 
46
+ // ── CSS custom properties (tema em português) ─────────────────────────────────
47
+
48
+ function gerarVariaveisCSS(tema = {}) {
49
+ const t = {
50
+ corPrimaria: tema.corPrimaria ?? '#2563eb',
51
+ corSecundaria: tema.corSecundaria ?? '#7c3aed',
52
+ corTexto: tema.corTexto ?? '#0f172a',
53
+ corTextoMuted: tema.corTextoMuted ?? '#64748b',
54
+ corFundo: tema.corFundo ?? '#f8fafc',
55
+ corFundoCard: tema.corFundoCard ?? '#ffffff',
56
+ corFundoNav: tema.corFundoNav ?? '#1e293b',
57
+ corBorda: tema.corBorda ?? '#e2e8f0',
58
+ corDestaque: tema.corDestaque ?? '#dbeafe',
59
+ corSucesso: tema.corSucesso ?? '#16a34a',
60
+ corErro: tema.corErro ?? '#dc2626',
61
+ corAviso: tema.corAviso ?? '#d97706',
62
+ raio: tema.raio ?? '8px',
63
+ fonte: tema.fonte ?? "system-ui, -apple-system, 'Segoe UI', sans-serif",
64
+ };
65
+
66
+ return `
67
+ --jade-cor-primaria: ${t.corPrimaria};
68
+ --jade-cor-secundaria: ${t.corSecundaria};
69
+ --jade-cor-texto: ${t.corTexto};
70
+ --jade-cor-texto-muted: ${t.corTextoMuted};
71
+ --jade-cor-fundo: ${t.corFundo};
72
+ --jade-cor-fundo-card: ${t.corFundoCard};
73
+ --jade-cor-fundo-nav: ${t.corFundoNav};
74
+ --jade-cor-borda: ${t.corBorda};
75
+ --jade-cor-destaque: ${t.corDestaque};
76
+ --jade-cor-sucesso: ${t.corSucesso};
77
+ --jade-cor-erro: ${t.corErro};
78
+ --jade-cor-aviso: ${t.corAviso};
79
+ --jade-raio: ${t.raio};
80
+ --jade-fonte: ${t.fonte};`.trim();
81
+ }
82
+
83
+ // ── CSS do shell (layout, nav, conteúdo) ──────────────────────────────────────
84
+
85
+ function gerarCSS(tema = {}) {
86
+ return `
87
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
88
+
89
+ :root {
90
+ ${gerarVariaveisCSS(tema)}
91
+ }
92
+
93
+ body {
94
+ font-family: var(--jade-fonte);
95
+ background: var(--jade-cor-fundo);
96
+ color: var(--jade-cor-texto);
97
+ min-height: 100dvh;
98
+ }
99
+
100
+ /* Layout principal */
101
+ #jade-app {
102
+ display: flex;
103
+ min-height: 100dvh;
104
+ }
105
+
106
+ /* Nav lateral */
107
+ #jade-nav {
108
+ width: 240px;
109
+ min-height: 100dvh;
110
+ background: var(--jade-cor-fundo-nav);
111
+ display: flex;
112
+ flex-direction: column;
113
+ flex-shrink: 0;
114
+ position: sticky;
115
+ top: 0;
116
+ height: 100dvh;
117
+ overflow-y: auto;
118
+ }
119
+
120
+ #jade-nav-header {
121
+ padding: 20px 16px 12px;
122
+ border-bottom: 1px solid rgba(255,255,255,0.08);
123
+ }
124
+
125
+ #jade-nav-titulo {
126
+ font-size: 0.875rem;
127
+ font-weight: 700;
128
+ color: #fff;
129
+ letter-spacing: 0.04em;
130
+ text-transform: uppercase;
131
+ }
132
+
133
+ #jade-nav-versao {
134
+ font-size: 0.7rem;
135
+ color: rgba(255,255,255,0.35);
136
+ margin-top: 2px;
137
+ }
138
+
139
+ #jade-nav-lista {
140
+ flex: 1;
141
+ padding: 8px 8px;
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 2px;
145
+ }
146
+
147
+ .jade-nav-item {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 10px;
151
+ width: 100%;
152
+ padding: 9px 12px;
153
+ border: none;
154
+ border-radius: var(--jade-raio);
155
+ background: transparent;
156
+ color: rgba(255,255,255,0.6);
157
+ font-size: 0.875rem;
158
+ font-family: var(--jade-fonte);
159
+ cursor: pointer;
160
+ text-align: left;
161
+ transition: background 0.15s, color 0.15s;
162
+ }
163
+
164
+ .jade-nav-item:hover {
165
+ background: rgba(255,255,255,0.07);
166
+ color: rgba(255,255,255,0.9);
167
+ }
168
+
169
+ .jade-nav-ativo {
170
+ background: var(--jade-cor-primaria) !important;
171
+ color: #fff !important;
172
+ }
173
+
174
+ .jade-nav-icone { display: flex; align-items: center; }
175
+
176
+ /* Toolbar */
177
+ .jade-toolbar {
178
+ display: flex;
179
+ flex-wrap: wrap;
180
+ gap: 8px;
181
+ margin-bottom: 16px;
182
+ }
183
+
184
+ /* Busca */
185
+ .jade-busca-wrapper { margin-bottom: 16px; }
186
+ .jade-busca-form { display: flex; gap: 0; }
187
+ .jade-busca-input {
188
+ flex: 1;
189
+ min-height: 44px;
190
+ padding: 10px 14px;
191
+ border: 1.5px solid var(--jade-cor-borda);
192
+ border-right: none;
193
+ border-radius: var(--jade-raio) 0 0 var(--jade-raio);
194
+ font-size: 1rem;
195
+ background: #fff;
196
+ }
197
+ .jade-busca-btn {
198
+ min-height: 44px;
199
+ padding: 0 14px;
200
+ border: 1.5px solid var(--jade-cor-primaria);
201
+ background: var(--jade-cor-primaria);
202
+ color: #fff;
203
+ border-radius: 0 var(--jade-raio) var(--jade-raio) 0;
204
+ cursor: pointer;
205
+ display: flex;
206
+ align-items: center;
207
+ }
208
+
209
+ /* Divisor */
210
+ .jade-divisor { border: none; border-top: 1px solid var(--jade-cor-borda); margin: 20px 0 12px; }
211
+ .jade-divisor-rotulo {
212
+ position: relative;
213
+ text-align: center;
214
+ margin: 20px 0 12px;
215
+ }
216
+ .jade-divisor-rotulo::before {
217
+ content: '';
218
+ position: absolute;
219
+ top: 50%;
220
+ left: 0; right: 0;
221
+ border-top: 1px solid var(--jade-cor-borda);
222
+ }
223
+ .jade-divisor-rotulo::after {
224
+ content: attr(data-rotulo);
225
+ position: relative;
226
+ background: var(--jade-cor-fundo);
227
+ padding: 0 12px;
228
+ font-size: 0.8125rem;
229
+ font-weight: 600;
230
+ color: var(--jade-cor-texto-muted);
231
+ text-transform: uppercase;
232
+ letter-spacing: 0.05em;
233
+ }
234
+
235
+ /* Área de conteúdo */
236
+ #jade-conteudo {
237
+ flex: 1;
238
+ min-width: 0;
239
+ padding: 24px;
240
+ overflow-y: auto;
241
+ }
242
+
243
+ /* Carregando */
244
+ #jade-carregando {
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ min-height: 100dvh;
249
+ color: var(--jade-cor-texto-muted);
250
+ font-size: 0.9rem;
251
+ gap: 10px;
252
+ }
253
+
254
+ .jade-spinner {
255
+ width: 20px; height: 20px;
256
+ border: 2px solid var(--jade-cor-borda);
257
+ border-top-color: var(--jade-cor-primaria);
258
+ border-radius: 50%;
259
+ animation: jade-giro 0.7s linear infinite;
260
+ }
261
+
262
+ @keyframes jade-giro { to { transform: rotate(360deg); } }
263
+
264
+ /* Mobile: nav vira barra inferior */
265
+ @media (max-width: 640px) {
266
+ #jade-app { flex-direction: column-reverse; }
267
+
268
+ #jade-nav {
269
+ width: 100%;
270
+ min-height: auto;
271
+ height: auto;
272
+ position: sticky;
273
+ bottom: 0;
274
+ top: auto;
275
+ border-top: 1px solid rgba(255,255,255,0.1);
276
+ }
277
+
278
+ #jade-nav-header { display: none; }
279
+
280
+ #jade-nav-lista {
281
+ flex-direction: row;
282
+ overflow-x: auto;
283
+ padding: 6px 8px;
284
+ gap: 4px;
285
+ }
286
+
287
+ .jade-nav-item {
288
+ flex-direction: column;
289
+ gap: 2px;
290
+ padding: 6px 12px;
291
+ font-size: 0.7rem;
292
+ white-space: nowrap;
293
+ flex-shrink: 0;
294
+ }
295
+
296
+ .jade-nav-icone { font-size: 1.2rem; }
297
+
298
+ #jade-conteudo { padding: 16px; }
299
+ }
300
+ `.trim();
301
+ }
302
+
49
303
  // ── Bootstrap JS inline no index.html ────────────────────────────────────────
50
304
 
51
- function bootstrap(wasmFile, uiFile) {
305
+ function gerarBootstrap(uiArquivo, wasmArquivo, nomeApp) {
52
306
  return `
53
- import { JadeRuntime, UIEngine } from './runtime.js';
307
+ import { JadeRuntime, UIEngine, LocalDatastore, criarElementoIcone } from './runtime.js';
308
+
309
+ const NOME_APP = ${JSON.stringify(nomeApp)};
310
+ const WASM_FILE = ${JSON.stringify('./' + wasmArquivo)};
311
+ const UI_FILE = ${JSON.stringify('./' + uiArquivo)};
312
+ const SEEDS_FILE = './seeds.json';
313
+
314
+ // Mapeia nome da tela para ícone do catálogo JADE (nomes em português)
315
+ function nomeIcone(nome) {
316
+ const n = (nome || '').toLowerCase();
317
+ if (/produto|estoque|item|mercadoria/.test(n)) return 'caixa';
318
+ if (/cliente|pessoa|contato|fornecedor/.test(n)) return 'usuarios';
319
+ if (/pedido|venda|ordem|compra/.test(n)) return 'carrinho';
320
+ if (/fiscal|nota|nfe|imposto|tributo/.test(n)) return 'relatorio';
321
+ if (/relatorio|relat|estatistica/.test(n)) return 'grafico';
322
+ if (/config|configurac|preferencia/.test(n)) return 'configuracoes';
323
+ if (/dashboard|painel|resumo|inicio/.test(n)) return 'casa';
324
+ if (/caixa|pagamento|financ|receber|pagar/.test(n)) return 'dinheiro';
325
+ if (/usuario|user|acesso|perfil/.test(n)) return 'usuario';
326
+ if (/moviment|lancament|transac/.test(n)) return 'atualizar';
327
+ return 'tabela_icone';
328
+ }
329
+
330
+ // Telas que não entram no nav (shells de login/formulário/navegação)
331
+ function ehTelaDeNav(tela) {
332
+ const tipos = (tela.elementos || []).map(e => e.tipo);
333
+ if (tipos.length === 0) return false;
334
+ // Shell pura de gaveta — é o menu, não uma tela navegável
335
+ if (tipos.every(t => t === 'gaveta')) return false;
336
+ // Tela de login
337
+ if (tipos.includes('login')) return false;
338
+ // Formulários de criação (somente formulario + botao)
339
+ if (tipos.every(t => t === 'formulario' || t === 'botao')) return false;
340
+ return true;
341
+ }
54
342
 
55
- async function iniciar() {
56
- const runtime = new JadeRuntime();
57
- const ui = new UIEngine(runtime.getMemory());
343
+ function coletarEntidades(telas) {
344
+ const nomes = new Set();
345
+ for (const tela of telas) {
346
+ for (const el of tela.elementos || []) {
347
+ for (const prop of el.propriedades || []) {
348
+ if (prop.chave === 'entidade' && prop.valor) nomes.add(String(prop.valor));
349
+ }
350
+ }
351
+ }
352
+ return [...nomes];
353
+ }
58
354
 
59
- // Carrega o módulo WASM compilado
60
- const resposta = await fetch('./${wasmFile}');
61
- await runtime.load(resposta);
355
+ async function mudarTela(nome, telas, db, ui, navItems) {
356
+ const idx = telas.findIndex(t => t.nome === nome);
357
+ if (idx < 0) return;
62
358
 
63
- // Carrega descritores de tela gerados pelo compilador
64
- const telas = await fetch('./${uiFile}').then(r => r.json()).catch(() => []);
359
+ navItems.forEach((btn, i) => btn.classList.toggle('jade-nav-ativo', i === idx));
65
360
 
66
- const container = document.getElementById('app');
361
+ const tela = telas[idx];
362
+ const container = document.getElementById('jade-conteudo');
363
+ container.innerHTML = '';
67
364
 
68
- if (telas.length > 0) {
69
- // renderizarTela: o descriptor do compilador e decide O COMO automaticamente
70
- ui.renderizarTela(telas[0], container);
71
- } else {
72
- container.innerHTML = '<p style="font-family:sans-serif;padding:2rem">App JADE carregado. Nenhuma tela declarada.</p>';
365
+ const dadosMap = {};
366
+ for (const el of tela.elementos || []) {
367
+ for (const prop of el.propriedades || []) {
368
+ if (prop.chave === 'entidade' && prop.valor && !dadosMap[prop.valor]) {
369
+ dadosMap[prop.valor] = await db.find(String(prop.valor)).catch(() => []);
73
370
  }
74
371
  }
372
+ }
75
373
 
76
- iniciar().catch(e => {
77
- document.getElementById('app').innerHTML =
78
- \`<p style="font-family:sans-serif;color:#dc2626;padding:2rem">
79
- <strong>Erro ao iniciar:</strong> \${e.message}
80
- </p>\`;
81
- console.error('[JADE]', e);
82
- });
83
- `.trim();
374
+ ui.renderizarTela(tela, container, dadosMap);
375
+ }
376
+
377
+ async function iniciar() {
378
+ const telas = await fetch(UI_FILE).then(r => r.json()).catch(() => []);
379
+
380
+ const entidades = coletarEntidades(telas);
381
+ const db = new LocalDatastore(NOME_APP, entidades);
382
+ await db.init();
383
+
384
+ try {
385
+ const seeds = await fetch(SEEDS_FILE).then(r => { if (!r.ok) throw 0; return r.json(); });
386
+ for (const [entidade, registros] of Object.entries(seeds)) {
387
+ const existentes = await db.find(entidade).catch(() => []);
388
+ if (existentes.length === 0) {
389
+ for (const reg of registros) await db.insert(entidade, reg).catch(() => {});
390
+ }
391
+ }
392
+ } catch { /* sem seeds ou já populado */ }
393
+
394
+ const runtime = new JadeRuntime();
395
+ try {
396
+ const resp = await fetch(WASM_FILE);
397
+ if (resp.ok) await runtime.load(resp);
398
+ } catch { /* WASM ausente */ }
399
+
400
+ const ui = new UIEngine(runtime.getMemory());
401
+
402
+ document.getElementById('jade-carregando')?.remove();
403
+ document.getElementById('jade-app').style.display = '';
404
+
405
+ if (telas.length === 0) {
406
+ document.getElementById('jade-conteudo').innerHTML =
407
+ '<p style="color:var(--jade-cor-texto-muted);padding:2rem">Nenhuma tela declarada.</p>';
408
+ return;
409
+ }
410
+
411
+ // Constrói nav apenas com telas navegáveis
412
+ const nav = document.getElementById('jade-nav-lista');
413
+ const telasNav = telas.filter(ehTelaDeNav);
414
+ const navItems = [];
415
+
416
+ telasNav.forEach((tela, i) => {
417
+ const btn = document.createElement('button');
418
+ btn.className = 'jade-nav-item' + (i === 0 ? ' jade-nav-ativo' : '');
419
+ btn.dataset.tela = tela.nome;
420
+
421
+ const svgIcone = criarElementoIcone(nomeIcone(tela.nome), 18);
422
+ const spanIcone = document.createElement('span');
423
+ spanIcone.className = 'jade-nav-icone';
424
+ if (svgIcone) spanIcone.appendChild(svgIcone);
425
+ btn.appendChild(spanIcone);
426
+
427
+ const spanLabel = document.createElement('span');
428
+ spanLabel.textContent = tela.titulo || tela.nome;
429
+ btn.appendChild(spanLabel);
430
+
431
+ btn.addEventListener('click', () => mudarTela(tela.nome, telas, db, ui, navItems));
432
+ nav.appendChild(btn);
433
+ navItems.push(btn);
434
+ });
435
+
436
+ // Handler: jade:navegar — gaveta e navegar disparam este evento
437
+ window.addEventListener('jade:navegar', (e) => {
438
+ const nomeTela = e.detail?.tela;
439
+ if (nomeTela) mudarTela(nomeTela, telas, db, ui, navItems);
440
+ });
441
+
442
+ // Handler: jade:acao — dispara jade:acao:concluido após processar
443
+ // (evita spinner eterno em botões sem implementação WASM real)
444
+ window.addEventListener('jade:acao', (e) => {
445
+ const acao = e.detail?.acao;
446
+ // Navegar via router.navegar() é tratado pelo runtime interno —
447
+ // aqui garantimos que o botão sai do estado de carregamento
448
+ setTimeout(() => {
449
+ window.dispatchEvent(new CustomEvent('jade:acao:concluido', { detail: { acao } }));
450
+ }, 300);
451
+ });
452
+
453
+ // Renderiza primeira tela navegável
454
+ const primeiraNome = telasNav[0]?.nome ?? telas[0]?.nome;
455
+ if (primeiraNome) await mudarTela(primeiraNome, telas, db, ui, navItems);
456
+ }
457
+
458
+ iniciar().catch(e => {
459
+ document.getElementById('jade-carregando')?.remove();
460
+ const app = document.getElementById('jade-app');
461
+ app.style.display = '';
462
+ app.innerHTML =
463
+ '<p style="padding:2rem;color:var(--jade-cor-erro)">' +
464
+ '<strong>Erro ao iniciar:</strong> ' + e.message + '</p>';
465
+ console.error('[JADE]', e);
466
+ });
467
+
468
+ if ('serviceWorker' in navigator) {
469
+ navigator.serviceWorker.register('./sw.js').catch(() => {});
470
+ }
471
+ `.trim();
84
472
  }
85
473
 
86
474
  // ── HTML shell ────────────────────────────────────────────────────────────────
87
475
 
88
- function gerarHTML(nome, wasmFile, uiFile, corTema = '#2563eb') {
476
+ function gerarHTML(nome, wasmFile, uiFile, tema = {}) {
477
+ const corPrimaria = tema.corPrimaria ?? '#2563eb';
478
+
89
479
  return `<!DOCTYPE html>
90
480
  <html lang="pt-BR">
91
481
  <head>
92
482
  <meta charset="UTF-8">
93
483
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
94
- <meta name="theme-color" content="${corTema}">
484
+ <meta name="theme-color" content="${corPrimaria}">
95
485
  <meta name="mobile-web-app-capable" content="yes">
96
486
  <meta name="apple-mobile-web-app-capable" content="yes">
97
487
  <title>${nome}</title>
98
488
  <link rel="manifest" href="manifest.json">
99
489
  <style>
100
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
101
- body { font-family: system-ui, -apple-system, sans-serif; background: #f9fafb; }
102
- #app { min-height: 100dvh; }
103
- #jade-carregando {
104
- display: flex; align-items: center; justify-content: center;
105
- min-height: 100dvh; font-family: sans-serif; color: #6b7280;
106
- }
490
+ ${gerarCSS(tema)}
107
491
  </style>
108
492
  </head>
109
493
  <body>
110
- <div id="jade-carregando">Carregando...</div>
111
- <div id="app" style="display:none"></div>
112
- <script type="module">
113
- ${bootstrap(wasmFile, uiFile)}
114
-
115
- // Remove tela de carregamento quando o app montar
116
- const obs = new MutationObserver(() => {
117
- if (document.getElementById('app').children.length > 0) {
118
- document.getElementById('jade-carregando').remove();
119
- document.getElementById('app').style.display = '';
120
- obs.disconnect();
121
- }
122
- });
123
- obs.observe(document.getElementById('app'), { childList: true });
494
+ <div id="jade-carregando">
495
+ <div class="jade-spinner"></div>
496
+ Carregando...
497
+ </div>
498
+
499
+ <div id="jade-app" style="display:none">
500
+ <nav id="jade-nav">
501
+ <div id="jade-nav-header">
502
+ <div id="jade-nav-titulo">${nome}</div>
503
+ <div id="jade-nav-versao">feito com Jade DSL</div>
504
+ </div>
505
+ <div id="jade-nav-lista"></div>
506
+ </nav>
507
+ <main id="jade-conteudo"></main>
508
+ </div>
124
509
 
125
- if ('serviceWorker' in navigator) {
126
- navigator.serviceWorker.register('./sw.js').catch(() => {});
127
- }
510
+ <script type="module">
511
+ ${gerarBootstrap(uiFile, wasmFile, nome)}
128
512
  </script>
129
513
  </body>
130
514
  </html>`;
@@ -132,15 +516,15 @@ function gerarHTML(nome, wasmFile, uiFile, corTema = '#2563eb') {
132
516
 
133
517
  // ── manifest.json ─────────────────────────────────────────────────────────────
134
518
 
135
- function gerarManifest(nome) {
519
+ function gerarManifest(nome, tema = {}) {
136
520
  return JSON.stringify({
137
521
  name: nome,
138
522
  short_name: nome.slice(0, 12),
139
523
  display: 'standalone',
140
524
  start_url: '/',
141
525
  scope: '/',
142
- theme_color: '#2563eb',
143
- background_color: '#ffffff',
526
+ theme_color: tema.corPrimaria ?? '#2563eb',
527
+ background_color: tema.corFundo ?? '#f8fafc',
144
528
  icons: [
145
529
  { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
146
530
  { src: 'icon-512.png', sizes: '512x512', type: 'image/png' },
@@ -150,10 +534,10 @@ function gerarManifest(nome) {
150
534
 
151
535
  // ── service worker ────────────────────────────────────────────────────────────
152
536
 
153
- function gerarSW(nome, wasmFile) {
537
+ function gerarSW(nome, wasmFile, uiFile) {
154
538
  const cache = `jade-${nome.toLowerCase().replace(/\s+/g, '-')}-v1`;
155
539
  return `const CACHE = '${cache}';
156
- const ARQUIVOS = ['/', '/index.html', '/${wasmFile}', '/runtime.js', '/manifest.json'];
540
+ const ARQUIVOS = ['/', '/index.html', '/${wasmFile}', '/${uiFile}', '/runtime.js', '/manifest.json'];
157
541
 
158
542
  self.addEventListener('install', e => {
159
543
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(ARQUIVOS).catch(() => {})));
@@ -173,7 +557,10 @@ self.addEventListener('fetch', e => {
173
557
  if (e.request.method !== 'GET') return;
174
558
  e.respondWith(
175
559
  caches.match(e.request).then(hit => hit ?? fetch(e.request).then(res => {
176
- if (res.ok) caches.open(CACHE).then(c => c.put(e.request, res.clone()));
560
+ if (res.ok) {
561
+ const clone = res.clone();
562
+ caches.open(CACHE).then(c => c.put(e.request, clone));
563
+ }
177
564
  return res;
178
565
  })).catch(() => new Response('<h1>Sem conexão</h1>', { headers: { 'Content-Type': 'text/html' } }))
179
566
  );
@@ -182,11 +569,18 @@ self.addEventListener('fetch', e => {
182
569
 
183
570
  // ── Comando principal ─────────────────────────────────────────────────────────
184
571
 
185
- export async function gerarHTML_dist({ prefixo, nome }) {
186
- const distDir = dirname(resolve(prefixo));
187
- const baseName = basename(prefixo);
188
- const wasmFile = `${baseName}.wasm`;
189
- const uiFile = `${baseName}.jade-ui.json`;
572
+ /**
573
+ * @param {object} opts
574
+ * @param {string} opts.prefixo - caminho de saída sem extensão (ex: dist/app)
575
+ * @param {string} opts.nome - nome do app (usado no título e manifest)
576
+ * @param {object} [opts.tema] - objeto de tema do jade.config.json
577
+ * @param {string} [opts.seedsOrigem] - caminho absoluto do seeds.json a copiar para dist/
578
+ */
579
+ export async function gerarHTML_dist({ prefixo, nome, tema = {}, seedsOrigem = null }) {
580
+ const distDir = dirname(resolve(prefixo));
581
+ const baseName = basename(prefixo);
582
+ const wasmFile = `${baseName}.wasm`;
583
+ const uiFile = `${baseName}.jade-ui.json`;
190
584
 
191
585
  mkdirSync(distDir, { recursive: true });
192
586
 
@@ -202,17 +596,22 @@ export async function gerarHTML_dist({ prefixo, nome }) {
202
596
  }
203
597
 
204
598
  // 2. index.html
205
- const html = gerarHTML(nome, wasmFile, uiFile);
206
- writeFileSync(join(distDir, 'index.html'), html, 'utf-8');
599
+ writeFileSync(join(distDir, 'index.html'), gerarHTML(nome, wasmFile, uiFile, tema), 'utf-8');
207
600
  console.log(` ${ok()} index.html`);
208
601
 
209
602
  // 3. manifest.json
210
- writeFileSync(join(distDir, 'manifest.json'), gerarManifest(nome), 'utf-8');
603
+ writeFileSync(join(distDir, 'manifest.json'), gerarManifest(nome, tema), 'utf-8');
211
604
  console.log(` ${ok()} manifest.json`);
212
605
 
213
606
  // 4. sw.js
214
- writeFileSync(join(distDir, 'sw.js'), gerarSW(nome, wasmFile), 'utf-8');
607
+ writeFileSync(join(distDir, 'sw.js'), gerarSW(nome, wasmFile, uiFile), 'utf-8');
215
608
  console.log(` ${ok()} sw.js`);
216
609
 
610
+ // 5. Copia seeds.json se existir no projeto
611
+ if (seedsOrigem && existsSync(seedsOrigem)) {
612
+ copyFileSync(seedsOrigem, join(distDir, 'seeds.json'));
613
+ console.log(` ${ok()} seeds.json ${dim('(carga inicial de dados)')}`);
614
+ }
615
+
217
616
  console.log(`\n ${azul('→')} para abrir no browser: ${verde('jade servir ' + distDir)}\n`);
218
617
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yakuzaa/jade",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Jade DSL — linguagem empresarial em português compilada para WebAssembly. Instala compilador + runtime + CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,8 +17,8 @@
17
17
  "postinstall": "node postinstall.js"
18
18
  },
19
19
  "dependencies": {
20
- "@yakuzaa/jade-compiler": "^0.1.6",
21
- "@yakuzaa/jade-runtime": "^0.1.5"
20
+ "@yakuzaa/jade-compiler": "^0.1.8",
21
+ "@yakuzaa/jade-runtime": "^0.1.6"
22
22
  },
23
23
  "keywords": [
24
24
  "jade",