@yakuzaa/jade 0.1.11 → 0.1.13

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 (2) hide show
  1. package/commands/html.js +231 -80
  2. package/package.json +2 -2
package/commands/html.js CHANGED
@@ -90,58 +90,86 @@ function gerarCSS(tema = {}) {
90
90
  ${gerarVariaveisCSS(tema)}
91
91
  }
92
92
 
93
- body {
93
+ html, body {
94
+ height: 100%;
94
95
  font-family: var(--jade-fonte);
95
96
  background: var(--jade-cor-fundo);
96
97
  color: var(--jade-cor-texto);
97
- min-height: 100dvh;
98
+ overflow: hidden;
98
99
  }
99
100
 
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;
101
+ /* ── Header fixo ─────────────────────────────── */
102
+ #jade-header {
103
+ position: fixed;
104
+ top: 0; left: 0; right: 0;
105
+ height: 52px;
110
106
  background: var(--jade-cor-fundo-nav);
111
107
  display: flex;
112
- flex-direction: column;
108
+ align-items: center;
109
+ gap: 10px;
110
+ padding: 0 16px 0 8px;
111
+ z-index: 300;
112
+ box-shadow: 0 1px 0 rgba(255,255,255,0.06), 0 2px 8px rgba(0,0,0,0.2);
113
113
  flex-shrink: 0;
114
- position: sticky;
115
- top: 0;
116
- height: 100dvh;
117
- overflow-y: auto;
118
114
  }
119
115
 
120
- #jade-nav-header {
121
- padding: 20px 16px 12px;
122
- border-bottom: 1px solid rgba(255,255,255,0.08);
116
+ #jade-hamburger {
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 40px;
121
+ height: 40px;
122
+ flex-shrink: 0;
123
+ border: none;
124
+ border-radius: var(--jade-raio);
125
+ background: transparent;
126
+ color: rgba(255,255,255,0.85);
127
+ cursor: pointer;
128
+ transition: background 0.15s;
123
129
  }
130
+ #jade-hamburger:hover { background: rgba(255,255,255,0.1); }
124
131
 
125
- #jade-nav-titulo {
132
+ #jade-header-titulo {
126
133
  font-size: 0.875rem;
127
134
  font-weight: 700;
128
135
  color: #fff;
129
- letter-spacing: 0.04em;
136
+ letter-spacing: 0.05em;
130
137
  text-transform: uppercase;
138
+ white-space: nowrap;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ }
142
+
143
+ /* ── Layout principal (abaixo do header) ─────── */
144
+ #jade-app {
145
+ display: flex;
146
+ height: calc(100dvh - 52px);
147
+ margin-top: 52px;
148
+ overflow: hidden;
131
149
  }
132
150
 
133
- #jade-nav-versao {
134
- font-size: 0.7rem;
135
- color: rgba(255,255,255,0.35);
136
- margin-top: 2px;
151
+ /* ── Nav lateral ─────────────────────────────── */
152
+ #jade-nav {
153
+ width: 240px;
154
+ height: 100%;
155
+ background: var(--jade-cor-fundo-nav);
156
+ display: flex;
157
+ flex-direction: column;
158
+ flex-shrink: 0;
159
+ overflow-y: auto;
160
+ transition: width 0.2s ease;
137
161
  }
138
162
 
163
+ /* O nav-header foi movido para o #jade-header global */
164
+ #jade-nav-header { display: none; }
165
+
139
166
  #jade-nav-lista {
140
167
  flex: 1;
141
- padding: 8px 8px;
168
+ padding: 8px;
142
169
  display: flex;
143
170
  flex-direction: column;
144
171
  gap: 2px;
172
+ overflow-y: auto;
145
173
  }
146
174
 
147
175
  .jade-nav-item {
@@ -159,20 +187,44 @@ function gerarCSS(tema = {}) {
159
187
  cursor: pointer;
160
188
  text-align: left;
161
189
  transition: background 0.15s, color 0.15s;
190
+ flex-shrink: 0;
162
191
  }
163
-
164
192
  .jade-nav-item:hover {
165
193
  background: rgba(255,255,255,0.07);
166
194
  color: rgba(255,255,255,0.9);
167
195
  }
168
-
169
196
  .jade-nav-ativo {
170
197
  background: var(--jade-cor-primaria) !important;
171
198
  color: #fff !important;
172
199
  }
173
-
174
200
  .jade-nav-icone { display: flex; align-items: center; }
175
201
 
202
+ /* ── Área de conteúdo ────────────────────────── */
203
+ #jade-conteudo {
204
+ flex: 1;
205
+ min-width: 0;
206
+ height: 100%;
207
+ padding: 24px;
208
+ overflow-y: auto;
209
+ overflow-x: hidden;
210
+ }
211
+
212
+ /* ── Overlay drawer (mobile) ─────────────────── */
213
+ #jade-overlay {
214
+ display: none;
215
+ position: fixed;
216
+ inset: 0;
217
+ top: 52px;
218
+ background: rgba(0,0,0,0.45);
219
+ z-index: 199;
220
+ opacity: 0;
221
+ transition: opacity 0.25s;
222
+ }
223
+ #jade-overlay.visivel {
224
+ display: block;
225
+ opacity: 1;
226
+ }
227
+
176
228
  /* Toolbar */
177
229
  .jade-toolbar {
178
230
  display: flex;
@@ -186,6 +238,7 @@ function gerarCSS(tema = {}) {
186
238
  .jade-busca-form { display: flex; gap: 0; }
187
239
  .jade-busca-input {
188
240
  flex: 1;
241
+ min-width: 0;
189
242
  min-height: 44px;
190
243
  padding: 10px 14px;
191
244
  border: 1.5px solid var(--jade-cor-borda);
@@ -232,20 +285,12 @@ function gerarCSS(tema = {}) {
232
285
  letter-spacing: 0.05em;
233
286
  }
234
287
 
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
288
  /* Carregando */
244
289
  #jade-carregando {
245
290
  display: flex;
246
291
  align-items: center;
247
292
  justify-content: center;
248
- min-height: 100dvh;
293
+ height: 100dvh;
249
294
  color: var(--jade-cor-texto-muted);
250
295
  font-size: 0.9rem;
251
296
  gap: 10px;
@@ -261,40 +306,29 @@ function gerarCSS(tema = {}) {
261
306
 
262
307
  @keyframes jade-giro { to { transform: rotate(360deg); } }
263
308
 
264
- /* Mobile: nav vira barra inferior */
265
- @media (max-width: 640px) {
266
- #jade-app { flex-direction: column-reverse; }
309
+ /* Mobile: hamburger + drawer overlay */
310
+ /* ── Desktop: sidebar pode ser colapsada ─────── */
311
+ #jade-nav.jade-nav-colapsada {
312
+ width: 0;
313
+ overflow: hidden;
314
+ }
267
315
 
316
+ /* ── Mobile: drawer slide-in (abaixo do header) ─ */
317
+ @media (max-width: 768px) {
268
318
  #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;
319
+ position: fixed;
320
+ top: 52px;
321
+ left: 0;
322
+ height: calc(100dvh - 52px);
323
+ z-index: 200;
324
+ transform: translateX(-100%);
325
+ transition: transform 0.25s ease;
326
+ box-shadow: 4px 0 16px rgba(0,0,0,0.3);
327
+ width: 240px !important;
285
328
  }
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;
329
+ #jade-nav.jade-nav-aberto {
330
+ transform: translateX(0);
294
331
  }
295
-
296
- .jade-nav-icone { font-size: 1.2rem; }
297
-
298
332
  #jade-conteudo { padding: 16px; }
299
333
  }
300
334
  `.trim();
@@ -346,31 +380,98 @@ function coletarEntidades(telas) {
346
380
  for (const el of tela.elementos || []) {
347
381
  for (const prop of el.propriedades || []) {
348
382
  if (prop.chave === 'entidade' && prop.valor) nomes.add(String(prop.valor));
383
+ // Agrega referências em marcadores @funcao:Entidade:campo
384
+ if (typeof prop.valor === 'string' && prop.valor.startsWith('@')) {
385
+ const partes = prop.valor.slice(1).split(':');
386
+ if (partes[1]) nomes.add(partes[1]);
387
+ }
349
388
  }
350
389
  }
351
390
  }
352
391
  return [...nomes];
353
392
  }
354
393
 
394
+ // Formata valor numérico de acordo com o campo (moeda vs número simples)
395
+ function formatarValor(v, campo) {
396
+ if (typeof v !== 'number' || isNaN(v)) return String(v ?? '');
397
+ const campoLower = (campo || '').toLowerCase();
398
+ // Campos monetários
399
+ if (/preco|total|valor|custo|receita|despesa|salario|pagamento|desconto|subtotal|moeda/.test(campoLower)) {
400
+ return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
401
+ }
402
+ // Inteiro
403
+ if (Number.isInteger(v)) return v.toLocaleString('pt-BR');
404
+ return v.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
405
+ }
406
+
407
+ // Resolve marcadores @funcao:Entidade:campo nos descritores de tela
408
+ function resolverAgregacoes(tela, dadosMap) {
409
+ for (const el of tela.elementos || []) {
410
+ for (const prop of el.propriedades) {
411
+ if (typeof prop.valor !== 'string' || !prop.valor.startsWith('@')) continue;
412
+ const [funcao, entidade, campo] = prop.valor.slice(1).split(':');
413
+ const registros = dadosMap[entidade] ?? [];
414
+ let resultado;
415
+ switch (funcao) {
416
+ case 'soma':
417
+ resultado = registros.reduce((s, r) => s + (Number(r[campo]) || 0), 0);
418
+ prop.valor = formatarValor(resultado, campo);
419
+ break;
420
+ case 'contagem':
421
+ resultado = registros.length;
422
+ prop.valor = resultado.toLocaleString('pt-BR');
423
+ break;
424
+ case 'media':
425
+ resultado = registros.length
426
+ ? registros.reduce((s, r) => s + (Number(r[campo]) || 0), 0) / registros.length
427
+ : 0;
428
+ prop.valor = formatarValor(resultado, campo);
429
+ break;
430
+ case 'maximo':
431
+ resultado = registros.length ? Math.max(...registros.map(r => Number(r[campo]) || 0)) : 0;
432
+ prop.valor = formatarValor(resultado, campo);
433
+ break;
434
+ case 'minimo':
435
+ resultado = registros.length ? Math.min(...registros.map(r => Number(r[campo]) || 0)) : 0;
436
+ prop.valor = formatarValor(resultado, campo);
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ }
442
+
355
443
  async function mudarTela(nome, telas, db, ui, navItems) {
356
444
  const idx = telas.findIndex(t => t.nome === nome);
357
445
  if (idx < 0) return;
358
446
 
359
- navItems.forEach((btn, i) => btn.classList.toggle('jade-nav-ativo', i === idx));
447
+ // navItems mapeia para telasNav (filtradas), mas idx é em telas (completa)
448
+ // Encontra o índice correto no navItems pelo nome
449
+ const navIdx = navItems.findIndex(b => b.dataset.tela === nome);
450
+ navItems.forEach((btn, i) => btn.classList.toggle('jade-nav-ativo', i === navIdx));
360
451
 
361
452
  const tela = telas[idx];
362
453
  const container = document.getElementById('jade-conteudo');
363
454
  container.innerHTML = '';
364
455
 
456
+ // Carrega todas as entidades referenciadas (inclusive em marcadores @)
365
457
  const dadosMap = {};
366
458
  for (const el of tela.elementos || []) {
367
459
  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(() => []);
460
+ const refs = [];
461
+ if (prop.chave === 'entidade' && prop.valor) refs.push(String(prop.valor));
462
+ if (typeof prop.valor === 'string' && prop.valor.startsWith('@')) {
463
+ const partes = prop.valor.slice(1).split(':');
464
+ if (partes[1]) refs.push(partes[1]);
465
+ }
466
+ for (const ref of refs) {
467
+ if (!dadosMap[ref]) dadosMap[ref] = await db.find(ref).catch(() => []);
370
468
  }
371
469
  }
372
470
  }
373
471
 
472
+ // Resolve @soma, @contagem, @media antes de renderizar
473
+ resolverAgregacoes(tela, dadosMap);
474
+
374
475
  ui.renderizarTela(tela, container, dadosMap);
375
476
  }
376
477
 
@@ -401,6 +502,45 @@ async function iniciar() {
401
502
 
402
503
  document.getElementById('jade-carregando')?.remove();
403
504
  document.getElementById('jade-app').style.display = '';
505
+ document.getElementById('jade-header').style.display = '';
506
+
507
+ // ── Header + hambúrguer ──────────────────────────────────────────────────────
508
+ const hamburger = document.getElementById('jade-hamburger');
509
+ const overlay = document.getElementById('jade-overlay');
510
+ const navEl = document.getElementById('jade-nav');
511
+ const isMobile = () => window.innerWidth <= 768;
512
+
513
+ const iconeMenu = criarElementoIcone('menu', 22);
514
+ const iconeFechar = criarElementoIcone('fechar', 22);
515
+ if (iconeMenu) hamburger.appendChild(iconeMenu);
516
+
517
+ function abrirDrawer() {
518
+ navEl.classList.add('jade-nav-aberto');
519
+ overlay.classList.add('visivel');
520
+ hamburger.setAttribute('aria-expanded', 'true');
521
+ if (iconeMenu && iconeFechar && hamburger.firstChild)
522
+ hamburger.replaceChild(iconeFechar, hamburger.firstChild);
523
+ }
524
+ function fecharDrawer() {
525
+ navEl.classList.remove('jade-nav-aberto');
526
+ overlay.classList.remove('visivel');
527
+ hamburger.setAttribute('aria-expanded', 'false');
528
+ if (iconeMenu && hamburger.firstChild !== iconeMenu)
529
+ hamburger.replaceChild(iconeMenu, hamburger.firstChild);
530
+ }
531
+ function toggleSidebar() {
532
+ navEl.classList.toggle('jade-nav-colapsada');
533
+ }
534
+
535
+ hamburger.addEventListener('click', () => {
536
+ if (isMobile()) {
537
+ navEl.classList.contains('jade-nav-aberto') ? fecharDrawer() : abrirDrawer();
538
+ } else {
539
+ toggleSidebar();
540
+ }
541
+ });
542
+ overlay.addEventListener('click', fecharDrawer);
543
+ window.addEventListener('resize', () => { if (!isMobile()) fecharDrawer(); });
404
544
 
405
545
  if (telas.length === 0) {
406
546
  document.getElementById('jade-conteudo').innerHTML =
@@ -417,6 +557,7 @@ async function iniciar() {
417
557
  const btn = document.createElement('button');
418
558
  btn.className = 'jade-nav-item' + (i === 0 ? ' jade-nav-ativo' : '');
419
559
  btn.dataset.tela = tela.nome;
560
+ btn.setAttribute('role', 'listitem');
420
561
 
421
562
  const svgIcone = criarElementoIcone(nomeIcone(tela.nome), 18);
422
563
  const spanIcone = document.createElement('span');
@@ -428,7 +569,10 @@ async function iniciar() {
428
569
  spanLabel.textContent = tela.titulo || tela.nome;
429
570
  btn.appendChild(spanLabel);
430
571
 
431
- btn.addEventListener('click', () => mudarTela(tela.nome, telas, db, ui, navItems));
572
+ btn.addEventListener('click', () => {
573
+ mudarTela(tela.nome, telas, db, ui, navItems);
574
+ fecharDrawer(); // fecha o drawer no mobile após navegar
575
+ });
432
576
  nav.appendChild(btn);
433
577
  navItems.push(btn);
434
578
  });
@@ -436,7 +580,10 @@ async function iniciar() {
436
580
  // Handler: jade:navegar — gaveta e navegar disparam este evento
437
581
  window.addEventListener('jade:navegar', (e) => {
438
582
  const nomeTela = e.detail?.tela;
439
- if (nomeTela) mudarTela(nomeTela, telas, db, ui, navItems);
583
+ if (nomeTela) {
584
+ mudarTela(nomeTela, telas, db, ui, navItems);
585
+ fecharDrawer();
586
+ }
440
587
  });
441
588
 
442
589
  // Handler: jade:acao — dispara jade:acao:concluido após processar
@@ -496,13 +643,17 @@ ${gerarCSS(tema)}
496
643
  Carregando...
497
644
  </div>
498
645
 
646
+ <header id="jade-header" style="display:none">
647
+ <button id="jade-hamburger" aria-label="Abrir menu" aria-expanded="false"></button>
648
+ <span id="jade-header-titulo">${nome}</span>
649
+ </header>
650
+
651
+ <div id="jade-overlay" role="presentation"></div>
652
+
499
653
  <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>
654
+ <nav id="jade-nav" aria-label="Menu de navegação">
655
+ <div id="jade-nav-header"></div>
656
+ <div id="jade-nav-lista" role="list"></div>
506
657
  </nav>
507
658
  <main id="jade-conteudo"></main>
508
659
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yakuzaa/jade",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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,7 +17,7 @@
17
17
  "postinstall": "node postinstall.js"
18
18
  },
19
19
  "dependencies": {
20
- "@yakuzaa/jade-compiler": "^0.1.14",
20
+ "@yakuzaa/jade-compiler": "^0.1.15",
21
21
  "@yakuzaa/jade-runtime": "^0.1.8"
22
22
  },
23
23
  "keywords": [