@yakuzaa/jade 0.1.12 → 0.1.14

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 +230 -76
  2. package/package.json +3 -3
package/commands/html.js CHANGED
@@ -95,47 +95,126 @@ function gerarCSS(tema = {}) {
95
95
  font-family: var(--jade-fonte);
96
96
  background: var(--jade-cor-fundo);
97
97
  color: var(--jade-cor-texto);
98
+ overflow: hidden;
98
99
  }
99
100
 
100
- /* Layout principal ocupa toda a tela, sem rolar o body */
101
- #jade-app {
102
- display: flex;
103
- height: 100dvh;
101
+ /* ── Header fixo ─────────────────────────────── */
102
+ /* ── Banner de notificação (empurra layout) ─── */
103
+ #jade-banner {
104
+ position: fixed;
105
+ top: 0; left: 0; right: 0;
106
+ height: 0;
104
107
  overflow: hidden;
108
+ z-index: 400;
109
+ transition: height 0.28s cubic-bezier(0.4,0,0.2,1);
110
+ }
111
+ #jade-banner.jade-banner-visivel { height: 48px; }
112
+ body.jade-com-banner #jade-header {
113
+ top: 48px;
114
+ transition: top 0.28s cubic-bezier(0.4,0,0.2,1);
115
+ }
116
+ body.jade-com-banner #jade-app {
117
+ margin-top: calc(52px + 48px);
118
+ transition: margin-top 0.28s cubic-bezier(0.4,0,0.2,1);
105
119
  }
106
120
 
107
- /* Nav lateral (desktop sempre visível) */
108
- #jade-nav {
109
- width: 240px;
110
- height: 100%;
121
+ #jade-header {
122
+ position: fixed;
123
+ top: 0; left: 0; right: 0;
124
+ height: 52px;
111
125
  background: var(--jade-cor-fundo-nav);
112
126
  display: flex;
113
- flex-direction: column;
127
+ align-items: center;
128
+ gap: 10px;
129
+ padding: 0 16px 0 8px;
130
+ z-index: 300;
131
+ box-shadow: 0 1px 0 rgba(255,255,255,0.06), 0 2px 8px rgba(0,0,0,0.2);
114
132
  flex-shrink: 0;
115
- overflow-y: auto;
116
- z-index: 10;
133
+ transition: top 0.28s cubic-bezier(0.4,0,0.2,1);
117
134
  }
118
135
 
119
- #jade-nav-header {
120
- padding: 20px 16px 12px;
121
- border-bottom: 1px solid rgba(255,255,255,0.08);
136
+ #jade-hamburger {
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ width: 40px;
141
+ height: 40px;
122
142
  flex-shrink: 0;
143
+ border: none;
144
+ border-radius: var(--jade-raio);
145
+ background: transparent;
146
+ color: rgba(255,255,255,0.85);
147
+ cursor: pointer;
148
+ transition: background 0.15s;
123
149
  }
150
+ #jade-hamburger:hover { background: rgba(255,255,255,0.1); }
124
151
 
125
- #jade-nav-titulo {
152
+ #jade-header-titulo {
126
153
  font-size: 0.875rem;
127
154
  font-weight: 700;
128
155
  color: #fff;
129
- letter-spacing: 0.04em;
156
+ letter-spacing: 0.05em;
130
157
  text-transform: uppercase;
158
+ white-space: nowrap;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ transition: opacity 0.2s;
162
+ }
163
+ body.jade-com-busca #jade-header-titulo { opacity: 0; pointer-events: none; }
164
+
165
+ /* ── Search bar centralizado no header ─────── */
166
+ #jade-header-busca-wrapper {
167
+ position: absolute;
168
+ left: 50%;
169
+ transform: translateX(-50%);
170
+ width: min(380px, calc(100% - 180px));
171
+ display: flex;
172
+ align-items: center;
173
+ }
174
+ #jade-header-busca {
175
+ width: 100%;
176
+ height: 34px;
177
+ padding: 0 14px;
178
+ border: none;
179
+ border-radius: 999px;
180
+ background: rgba(255,255,255,0.13);
181
+ color: #fff;
182
+ font-size: 0.875rem;
183
+ font-family: var(--jade-fonte);
184
+ outline: none;
185
+ transition: background 0.15s;
131
186
  }
187
+ #jade-header-busca::placeholder { color: rgba(255,255,255,0.45); }
188
+ #jade-header-busca:focus { background: rgba(255,255,255,0.2); }
132
189
 
133
- #jade-nav-versao {
134
- font-size: 0.7rem;
135
- color: rgba(255,255,255,0.35);
136
- margin-top: 2px;
190
+ /* ── Layout principal (abaixo do header) ─────── */
191
+ #jade-app {
192
+ display: flex;
193
+ height: calc(100dvh - 52px);
194
+ margin-top: 52px;
195
+ overflow: hidden;
196
+ transition: margin-top 0.28s cubic-bezier(0.4,0,0.2,1),
197
+ height 0.28s cubic-bezier(0.4,0,0.2,1);
198
+ }
199
+ body.jade-com-banner #jade-app {
200
+ height: calc(100dvh - 52px - 48px);
201
+ }
202
+
203
+ /* ── Nav lateral ─────────────────────────────── */
204
+ #jade-nav {
205
+ width: 240px;
206
+ height: 100%;
207
+ background: var(--jade-cor-fundo-nav);
208
+ display: flex;
209
+ flex-direction: column;
210
+ flex-shrink: 0;
211
+ overflow-y: auto;
212
+ transition: width 0.2s ease;
137
213
  }
138
214
 
215
+ /* O nav-header foi movido para o #jade-header global */
216
+ #jade-nav-header { display: none; }
217
+
139
218
  #jade-nav-lista {
140
219
  flex: 1;
141
220
  padding: 8px;
@@ -162,20 +241,17 @@ function gerarCSS(tema = {}) {
162
241
  transition: background 0.15s, color 0.15s;
163
242
  flex-shrink: 0;
164
243
  }
165
-
166
244
  .jade-nav-item:hover {
167
245
  background: rgba(255,255,255,0.07);
168
246
  color: rgba(255,255,255,0.9);
169
247
  }
170
-
171
248
  .jade-nav-ativo {
172
249
  background: var(--jade-cor-primaria) !important;
173
250
  color: #fff !important;
174
251
  }
175
-
176
252
  .jade-nav-icone { display: flex; align-items: center; }
177
253
 
178
- /* Área de conteúdo rola de forma independente */
254
+ /* ── Área de conteúdo ────────────────────────── */
179
255
  #jade-conteudo {
180
256
  flex: 1;
181
257
  min-width: 0;
@@ -185,11 +261,12 @@ function gerarCSS(tema = {}) {
185
261
  overflow-x: hidden;
186
262
  }
187
263
 
188
- /* Overlay escuro para o drawer no mobile */
264
+ /* ── Overlay drawer (mobile) ─────────────────── */
189
265
  #jade-overlay {
190
266
  display: none;
191
267
  position: fixed;
192
268
  inset: 0;
269
+ top: 52px;
193
270
  background: rgba(0,0,0,0.45);
194
271
  z-index: 199;
195
272
  opacity: 0;
@@ -200,26 +277,6 @@ function gerarCSS(tema = {}) {
200
277
  opacity: 1;
201
278
  }
202
279
 
203
- /* Botão hambúrguer — só aparece no mobile */
204
- #jade-hamburger {
205
- display: none;
206
- position: fixed;
207
- top: 12px;
208
- left: 12px;
209
- z-index: 198;
210
- width: 40px;
211
- height: 40px;
212
- border: none;
213
- border-radius: var(--jade-raio);
214
- background: var(--jade-cor-fundo-nav);
215
- color: #fff;
216
- cursor: pointer;
217
- align-items: center;
218
- justify-content: center;
219
- box-shadow: 0 2px 8px rgba(0,0,0,0.25);
220
- flex-shrink: 0;
221
- }
222
-
223
280
  /* Toolbar */
224
281
  .jade-toolbar {
225
282
  display: flex;
@@ -302,28 +359,29 @@ function gerarCSS(tema = {}) {
302
359
  @keyframes jade-giro { to { transform: rotate(360deg); } }
303
360
 
304
361
  /* Mobile: hamburger + drawer overlay */
305
- @media (max-width: 768px) {
306
- #jade-hamburger { display: flex; }
362
+ /* ── Desktop: sidebar pode ser colapsada ─────── */
363
+ #jade-nav.jade-nav-colapsada {
364
+ width: 0;
365
+ overflow: hidden;
366
+ }
307
367
 
368
+ /* ── Mobile: drawer slide-in (abaixo do header) ─ */
369
+ @media (max-width: 768px) {
308
370
  #jade-nav {
309
371
  position: fixed;
310
- top: 0;
372
+ top: 52px;
311
373
  left: 0;
312
- height: 100dvh;
374
+ height: calc(100dvh - 52px);
313
375
  z-index: 200;
314
376
  transform: translateX(-100%);
315
377
  transition: transform 0.25s ease;
316
378
  box-shadow: 4px 0 16px rgba(0,0,0,0.3);
379
+ width: 240px !important;
317
380
  }
318
-
319
381
  #jade-nav.jade-nav-aberto {
320
382
  transform: translateX(0);
321
383
  }
322
-
323
- #jade-conteudo {
324
- padding: 16px;
325
- padding-top: 64px;
326
- }
384
+ #jade-conteudo { padding: 16px; }
327
385
  }
328
386
  `.trim();
329
387
  }
@@ -374,32 +432,117 @@ function coletarEntidades(telas) {
374
432
  for (const el of tela.elementos || []) {
375
433
  for (const prop of el.propriedades || []) {
376
434
  if (prop.chave === 'entidade' && prop.valor) nomes.add(String(prop.valor));
435
+ // Agrega referências em marcadores @funcao:Entidade:campo
436
+ if (typeof prop.valor === 'string' && prop.valor.startsWith('@')) {
437
+ const partes = prop.valor.slice(1).split(':');
438
+ if (partes[1]) nomes.add(partes[1]);
439
+ }
377
440
  }
378
441
  }
379
442
  }
380
443
  return [...nomes];
381
444
  }
382
445
 
446
+ // Formata valor numérico de acordo com o campo (moeda vs número simples)
447
+ function formatarValor(v, campo) {
448
+ if (typeof v !== 'number' || isNaN(v)) return String(v ?? '');
449
+ const campoLower = (campo || '').toLowerCase();
450
+ // Campos monetários
451
+ if (/preco|total|valor|custo|receita|despesa|salario|pagamento|desconto|subtotal|moeda/.test(campoLower)) {
452
+ return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
453
+ }
454
+ // Inteiro
455
+ if (Number.isInteger(v)) return v.toLocaleString('pt-BR');
456
+ return v.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
457
+ }
458
+
459
+ // Resolve marcadores @funcao:Entidade:campo nos descritores de tela
460
+ function resolverAgregacoes(tela, dadosMap) {
461
+ for (const el of tela.elementos || []) {
462
+ for (const prop of el.propriedades) {
463
+ if (typeof prop.valor !== 'string' || !prop.valor.startsWith('@')) continue;
464
+ const [funcao, entidade, campo] = prop.valor.slice(1).split(':');
465
+ const registros = dadosMap[entidade] ?? [];
466
+ let resultado;
467
+ switch (funcao) {
468
+ case 'soma':
469
+ resultado = registros.reduce((s, r) => s + (Number(r[campo]) || 0), 0);
470
+ prop.valor = formatarValor(resultado, campo);
471
+ break;
472
+ case 'contagem':
473
+ resultado = registros.length;
474
+ prop.valor = resultado.toLocaleString('pt-BR');
475
+ break;
476
+ case 'media':
477
+ resultado = registros.length
478
+ ? registros.reduce((s, r) => s + (Number(r[campo]) || 0), 0) / registros.length
479
+ : 0;
480
+ prop.valor = formatarValor(resultado, campo);
481
+ break;
482
+ case 'maximo':
483
+ resultado = registros.length ? Math.max(...registros.map(r => Number(r[campo]) || 0)) : 0;
484
+ prop.valor = formatarValor(resultado, campo);
485
+ break;
486
+ case 'minimo':
487
+ resultado = registros.length ? Math.min(...registros.map(r => Number(r[campo]) || 0)) : 0;
488
+ prop.valor = formatarValor(resultado, campo);
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ }
494
+
383
495
  async function mudarTela(nome, telas, db, ui, navItems) {
384
496
  const idx = telas.findIndex(t => t.nome === nome);
385
497
  if (idx < 0) return;
386
498
 
387
- navItems.forEach((btn, i) => btn.classList.toggle('jade-nav-ativo', i === idx));
499
+ // navItems mapeia para telasNav (filtradas), mas idx é em telas (completa)
500
+ // Encontra o índice correto no navItems pelo nome
501
+ const navIdx = navItems.findIndex(b => b.dataset.tela === nome);
502
+ navItems.forEach((btn, i) => btn.classList.toggle('jade-nav-ativo', i === navIdx));
388
503
 
389
504
  const tela = telas[idx];
390
505
  const container = document.getElementById('jade-conteudo');
391
506
  container.innerHTML = '';
392
507
 
508
+ // Carrega todas as entidades referenciadas (inclusive em marcadores @)
393
509
  const dadosMap = {};
394
510
  for (const el of tela.elementos || []) {
395
511
  for (const prop of el.propriedades || []) {
396
- if (prop.chave === 'entidade' && prop.valor && !dadosMap[prop.valor]) {
397
- dadosMap[prop.valor] = await db.find(String(prop.valor)).catch(() => []);
512
+ const refs = [];
513
+ if (prop.chave === 'entidade' && prop.valor) refs.push(String(prop.valor));
514
+ if (typeof prop.valor === 'string' && prop.valor.startsWith('@')) {
515
+ const partes = prop.valor.slice(1).split(':');
516
+ if (partes[1]) refs.push(partes[1]);
517
+ }
518
+ for (const ref of refs) {
519
+ if (!dadosMap[ref]) dadosMap[ref] = await db.find(ref).catch(() => []);
398
520
  }
399
521
  }
400
522
  }
401
523
 
524
+ // Resolve @soma, @contagem, @media antes de renderizar
525
+ resolverAgregacoes(tela, dadosMap);
526
+
402
527
  ui.renderizarTela(tela, container, dadosMap);
528
+
529
+ // Conecta search bar do header ao filtro da tela, se existir
530
+ const buscaWrapper = document.getElementById('jade-header-busca-wrapper');
531
+ const buscaInput = document.getElementById('jade-header-busca');
532
+ if (buscaWrapper && buscaInput) {
533
+ const signal = ui.getFiltroPorTela?.(nome);
534
+ if (signal) {
535
+ buscaWrapper.style.display = '';
536
+ document.body.classList.add('jade-com-busca');
537
+ buscaInput.value = '';
538
+ signal.set('');
539
+ buscaInput.oninput = () => signal.set(buscaInput.value.toLowerCase());
540
+ } else {
541
+ buscaWrapper.style.display = 'none';
542
+ document.body.classList.remove('jade-com-busca');
543
+ buscaInput.oninput = null;
544
+ }
545
+ }
403
546
  }
404
547
 
405
548
  async function iniciar() {
@@ -429,40 +572,45 @@ async function iniciar() {
429
572
 
430
573
  document.getElementById('jade-carregando')?.remove();
431
574
  document.getElementById('jade-app').style.display = '';
575
+ document.getElementById('jade-header').style.display = '';
432
576
 
433
- // ── Hamburger (mobile) ──────────────────────────────────────────────────────
577
+ // ── Header + hambúrguer ──────────────────────────────────────────────────────
434
578
  const hamburger = document.getElementById('jade-hamburger');
435
579
  const overlay = document.getElementById('jade-overlay');
436
580
  const navEl = document.getElementById('jade-nav');
581
+ const isMobile = () => window.innerWidth <= 768;
437
582
 
438
- // Mostra o botão (estava display:none para não piscar antes do app carregar)
439
- hamburger.style.display = '';
440
-
441
- const iconeMenu = criarElementoIcone('menu', 22);
442
- const iconeFechar = criarElementoIcone('fechar', 22);
583
+ const iconeMenu = criarElementoIcone('menu', 24);
584
+ const iconeFechar = criarElementoIcone('fechar', 24);
443
585
  if (iconeMenu) hamburger.appendChild(iconeMenu);
444
586
 
445
587
  function abrirDrawer() {
446
588
  navEl.classList.add('jade-nav-aberto');
447
589
  overlay.classList.add('visivel');
448
590
  hamburger.setAttribute('aria-expanded', 'true');
449
- if (iconeMenu && iconeFechar) {
591
+ if (iconeMenu && iconeFechar && hamburger.firstChild)
450
592
  hamburger.replaceChild(iconeFechar, hamburger.firstChild);
451
- }
452
593
  }
453
594
  function fecharDrawer() {
454
595
  navEl.classList.remove('jade-nav-aberto');
455
596
  overlay.classList.remove('visivel');
456
597
  hamburger.setAttribute('aria-expanded', 'false');
457
- if (iconeMenu && hamburger.firstChild !== iconeMenu) {
598
+ if (iconeMenu && hamburger.firstChild !== iconeMenu)
458
599
  hamburger.replaceChild(iconeMenu, hamburger.firstChild);
459
- }
600
+ }
601
+ function toggleSidebar() {
602
+ navEl.classList.toggle('jade-nav-colapsada');
460
603
  }
461
604
 
462
- hamburger.addEventListener('click', () =>
463
- navEl.classList.contains('jade-nav-aberto') ? fecharDrawer() : abrirDrawer()
464
- );
605
+ hamburger.addEventListener('click', () => {
606
+ if (isMobile()) {
607
+ navEl.classList.contains('jade-nav-aberto') ? fecharDrawer() : abrirDrawer();
608
+ } else {
609
+ toggleSidebar();
610
+ }
611
+ });
465
612
  overlay.addEventListener('click', fecharDrawer);
613
+ window.addEventListener('resize', () => { if (!isMobile()) fecharDrawer(); });
466
614
 
467
615
  if (telas.length === 0) {
468
616
  document.getElementById('jade-conteudo').innerHTML =
@@ -565,15 +713,21 @@ ${gerarCSS(tema)}
565
713
  Carregando...
566
714
  </div>
567
715
 
568
- <button id="jade-hamburger" aria-label="Abrir menu" aria-expanded="false" style="display:none"></button>
716
+ <div id="jade-banner" role="status" aria-live="polite"></div>
717
+
718
+ <header id="jade-header" style="display:none">
719
+ <button id="jade-hamburger" aria-label="Abrir menu" aria-expanded="false"></button>
720
+ <span id="jade-header-titulo">${nome}</span>
721
+ <div id="jade-header-busca-wrapper" style="display:none" role="search">
722
+ <input id="jade-header-busca" type="search" placeholder="Buscar..." autocomplete="off" aria-label="Buscar na tela atual">
723
+ </div>
724
+ </header>
725
+
569
726
  <div id="jade-overlay" role="presentation"></div>
570
727
 
571
728
  <div id="jade-app" style="display:none">
572
729
  <nav id="jade-nav" aria-label="Menu de navegação">
573
- <div id="jade-nav-header">
574
- <div id="jade-nav-titulo">${nome}</div>
575
- <div id="jade-nav-versao">feito com Jade DSL</div>
576
- </div>
730
+ <div id="jade-nav-header"></div>
577
731
  <div id="jade-nav-lista" role="list"></div>
578
732
  </nav>
579
733
  <main id="jade-conteudo"></main>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yakuzaa/jade",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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.14",
21
- "@yakuzaa/jade-runtime": "^0.1.8"
20
+ "@yakuzaa/jade-compiler": "^0.1.15",
21
+ "@yakuzaa/jade-runtime": "^0.1.10"
22
22
  },
23
23
  "keywords": [
24
24
  "jade",