@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.
- package/commands/compilar.js +29 -3
- package/commands/html.js +474 -75
- package/package.json +3 -3
package/commands/compilar.js
CHANGED
|
@@ -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
|
-
|
|
103
|
-
|
|
102
|
+
// Lê 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 {
|
|
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
|
|
23
|
-
const azul
|
|
24
|
-
const dim
|
|
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
|
|
27
|
-
const aviso
|
|
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
|
|
305
|
+
function gerarBootstrap(uiArquivo, wasmArquivo, nomeApp) {
|
|
52
306
|
return `
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
const tela = telas[idx];
|
|
362
|
+
const container = document.getElementById('jade-conteudo');
|
|
363
|
+
container.innerHTML = '';
|
|
67
364
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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,
|
|
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="${
|
|
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
|
-
|
|
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">
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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: '#
|
|
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)
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
21
|
-
"@yakuzaa/jade-runtime": "^0.1.
|
|
20
|
+
"@yakuzaa/jade-compiler": "^0.1.8",
|
|
21
|
+
"@yakuzaa/jade-runtime": "^0.1.6"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"jade",
|