@yakuzaa/jade 0.1.12 → 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 +155 -76
  2. package/package.json +2 -2
package/commands/html.js CHANGED
@@ -95,47 +95,74 @@ 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
- }
99
-
100
- /* Layout principal — ocupa toda a tela, sem rolar o body */
101
- #jade-app {
102
- display: flex;
103
- height: 100dvh;
104
98
  overflow: hidden;
105
99
  }
106
100
 
107
- /* Nav lateral (desktop sempre visível) */
108
- #jade-nav {
109
- width: 240px;
110
- height: 100%;
101
+ /* ── Header fixo ─────────────────────────────── */
102
+ #jade-header {
103
+ position: fixed;
104
+ top: 0; left: 0; right: 0;
105
+ height: 52px;
111
106
  background: var(--jade-cor-fundo-nav);
112
107
  display: flex;
113
- 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);
114
113
  flex-shrink: 0;
115
- overflow-y: auto;
116
- z-index: 10;
117
114
  }
118
115
 
119
- #jade-nav-header {
120
- padding: 20px 16px 12px;
121
- 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
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;
131
141
  }
132
142
 
133
- #jade-nav-versao {
134
- font-size: 0.7rem;
135
- color: rgba(255,255,255,0.35);
136
- margin-top: 2px;
143
+ /* ── Layout principal (abaixo do header) ─────── */
144
+ #jade-app {
145
+ display: flex;
146
+ height: calc(100dvh - 52px);
147
+ margin-top: 52px;
148
+ overflow: hidden;
137
149
  }
138
150
 
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;
161
+ }
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
168
  padding: 8px;
@@ -162,20 +189,17 @@ function gerarCSS(tema = {}) {
162
189
  transition: background 0.15s, color 0.15s;
163
190
  flex-shrink: 0;
164
191
  }
165
-
166
192
  .jade-nav-item:hover {
167
193
  background: rgba(255,255,255,0.07);
168
194
  color: rgba(255,255,255,0.9);
169
195
  }
170
-
171
196
  .jade-nav-ativo {
172
197
  background: var(--jade-cor-primaria) !important;
173
198
  color: #fff !important;
174
199
  }
175
-
176
200
  .jade-nav-icone { display: flex; align-items: center; }
177
201
 
178
- /* Área de conteúdo rola de forma independente */
202
+ /* ── Área de conteúdo ────────────────────────── */
179
203
  #jade-conteudo {
180
204
  flex: 1;
181
205
  min-width: 0;
@@ -185,11 +209,12 @@ function gerarCSS(tema = {}) {
185
209
  overflow-x: hidden;
186
210
  }
187
211
 
188
- /* Overlay escuro para o drawer no mobile */
212
+ /* ── Overlay drawer (mobile) ─────────────────── */
189
213
  #jade-overlay {
190
214
  display: none;
191
215
  position: fixed;
192
216
  inset: 0;
217
+ top: 52px;
193
218
  background: rgba(0,0,0,0.45);
194
219
  z-index: 199;
195
220
  opacity: 0;
@@ -200,26 +225,6 @@ function gerarCSS(tema = {}) {
200
225
  opacity: 1;
201
226
  }
202
227
 
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
228
  /* Toolbar */
224
229
  .jade-toolbar {
225
230
  display: flex;
@@ -302,28 +307,29 @@ function gerarCSS(tema = {}) {
302
307
  @keyframes jade-giro { to { transform: rotate(360deg); } }
303
308
 
304
309
  /* Mobile: hamburger + drawer overlay */
305
- @media (max-width: 768px) {
306
- #jade-hamburger { display: flex; }
310
+ /* ── Desktop: sidebar pode ser colapsada ─────── */
311
+ #jade-nav.jade-nav-colapsada {
312
+ width: 0;
313
+ overflow: hidden;
314
+ }
307
315
 
316
+ /* ── Mobile: drawer slide-in (abaixo do header) ─ */
317
+ @media (max-width: 768px) {
308
318
  #jade-nav {
309
319
  position: fixed;
310
- top: 0;
320
+ top: 52px;
311
321
  left: 0;
312
- height: 100dvh;
322
+ height: calc(100dvh - 52px);
313
323
  z-index: 200;
314
324
  transform: translateX(-100%);
315
325
  transition: transform 0.25s ease;
316
326
  box-shadow: 4px 0 16px rgba(0,0,0,0.3);
327
+ width: 240px !important;
317
328
  }
318
-
319
329
  #jade-nav.jade-nav-aberto {
320
330
  transform: translateX(0);
321
331
  }
322
-
323
- #jade-conteudo {
324
- padding: 16px;
325
- padding-top: 64px;
326
- }
332
+ #jade-conteudo { padding: 16px; }
327
333
  }
328
334
  `.trim();
329
335
  }
@@ -374,31 +380,98 @@ function coletarEntidades(telas) {
374
380
  for (const el of tela.elementos || []) {
375
381
  for (const prop of el.propriedades || []) {
376
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
+ }
377
388
  }
378
389
  }
379
390
  }
380
391
  return [...nomes];
381
392
  }
382
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
+
383
443
  async function mudarTela(nome, telas, db, ui, navItems) {
384
444
  const idx = telas.findIndex(t => t.nome === nome);
385
445
  if (idx < 0) return;
386
446
 
387
- 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));
388
451
 
389
452
  const tela = telas[idx];
390
453
  const container = document.getElementById('jade-conteudo');
391
454
  container.innerHTML = '';
392
455
 
456
+ // Carrega todas as entidades referenciadas (inclusive em marcadores @)
393
457
  const dadosMap = {};
394
458
  for (const el of tela.elementos || []) {
395
459
  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(() => []);
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(() => []);
398
468
  }
399
469
  }
400
470
  }
401
471
 
472
+ // Resolve @soma, @contagem, @media antes de renderizar
473
+ resolverAgregacoes(tela, dadosMap);
474
+
402
475
  ui.renderizarTela(tela, container, dadosMap);
403
476
  }
404
477
 
@@ -429,14 +502,13 @@ async function iniciar() {
429
502
 
430
503
  document.getElementById('jade-carregando')?.remove();
431
504
  document.getElementById('jade-app').style.display = '';
505
+ document.getElementById('jade-header').style.display = '';
432
506
 
433
- // ── Hamburger (mobile) ──────────────────────────────────────────────────────
507
+ // ── Header + hambúrguer ──────────────────────────────────────────────────────
434
508
  const hamburger = document.getElementById('jade-hamburger');
435
509
  const overlay = document.getElementById('jade-overlay');
436
510
  const navEl = document.getElementById('jade-nav');
437
-
438
- // Mostra o botão (estava display:none para não piscar antes do app carregar)
439
- hamburger.style.display = '';
511
+ const isMobile = () => window.innerWidth <= 768;
440
512
 
441
513
  const iconeMenu = criarElementoIcone('menu', 22);
442
514
  const iconeFechar = criarElementoIcone('fechar', 22);
@@ -446,23 +518,29 @@ async function iniciar() {
446
518
  navEl.classList.add('jade-nav-aberto');
447
519
  overlay.classList.add('visivel');
448
520
  hamburger.setAttribute('aria-expanded', 'true');
449
- if (iconeMenu && iconeFechar) {
521
+ if (iconeMenu && iconeFechar && hamburger.firstChild)
450
522
  hamburger.replaceChild(iconeFechar, hamburger.firstChild);
451
- }
452
523
  }
453
524
  function fecharDrawer() {
454
525
  navEl.classList.remove('jade-nav-aberto');
455
526
  overlay.classList.remove('visivel');
456
527
  hamburger.setAttribute('aria-expanded', 'false');
457
- if (iconeMenu && hamburger.firstChild !== iconeMenu) {
528
+ if (iconeMenu && hamburger.firstChild !== iconeMenu)
458
529
  hamburger.replaceChild(iconeMenu, hamburger.firstChild);
459
- }
530
+ }
531
+ function toggleSidebar() {
532
+ navEl.classList.toggle('jade-nav-colapsada');
460
533
  }
461
534
 
462
- hamburger.addEventListener('click', () =>
463
- navEl.classList.contains('jade-nav-aberto') ? fecharDrawer() : abrirDrawer()
464
- );
535
+ hamburger.addEventListener('click', () => {
536
+ if (isMobile()) {
537
+ navEl.classList.contains('jade-nav-aberto') ? fecharDrawer() : abrirDrawer();
538
+ } else {
539
+ toggleSidebar();
540
+ }
541
+ });
465
542
  overlay.addEventListener('click', fecharDrawer);
543
+ window.addEventListener('resize', () => { if (!isMobile()) fecharDrawer(); });
466
544
 
467
545
  if (telas.length === 0) {
468
546
  document.getElementById('jade-conteudo').innerHTML =
@@ -565,15 +643,16 @@ ${gerarCSS(tema)}
565
643
  Carregando...
566
644
  </div>
567
645
 
568
- <button id="jade-hamburger" aria-label="Abrir menu" aria-expanded="false" style="display:none"></button>
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
+
569
651
  <div id="jade-overlay" role="presentation"></div>
570
652
 
571
653
  <div id="jade-app" style="display:none">
572
654
  <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>
655
+ <div id="jade-nav-header"></div>
577
656
  <div id="jade-nav-lista" role="list"></div>
578
657
  </nav>
579
658
  <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.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": [