aiplang 2.11.9 → 2.11.11

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/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.11.9'
8
+ const VERSION = '2.11.11'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -686,7 +686,7 @@ function generateTypes(app, srcFile) {
686
686
  }
687
687
 
688
688
  lines.push(`// ── aiplang version ──────────────────────────────────────────`)
689
- lines.push(`export const AIPLANG_VERSION = '2.11.9'`)
689
+ lines.push(`export const AIPLANG_VERSION = '2.11.11'`)
690
690
  lines.push(``)
691
691
  return lines.join('\n')
692
692
  }
@@ -936,7 +936,18 @@ console.error(`\n ✗ Unknown command: ${cmd}\n Run aiplang --help\n`)
936
936
  process.exit(1)
937
937
 
938
938
  function parsePages(src) {
939
- return src.split(/\n---\n/).map(s=>parsePage(s.trim())).filter(Boolean)
939
+ // Extrair variáveis globais ~var name = value
940
+ const gVars = {}
941
+ src.split('\n').forEach(line => {
942
+ const vm = line.trim().match(/^~var\s+(\w+)\s*=\s*(.+)$/)
943
+ if (vm) gVars[vm[1].trim()] = vm[2].trim().replace(/^["']|["']$/g,'')
944
+ })
945
+ // Expandir $var em todo o src
946
+ function expand(s) {
947
+ if (!Object.keys(gVars).length) return s
948
+ return s.replace(/\$(\w+)/g, (m,k) => gVars[k]!==undefined ? gVars[k] : m)
949
+ }
950
+ return src.split(/\n---\n/).map(s=>parsePage(expand(s.trim()))).filter(Boolean)
940
951
  }
941
952
 
942
953
  function parsePage(src) {
@@ -944,6 +955,7 @@ function parsePage(src) {
944
955
  if(!lines.length) return null
945
956
  const p={id:'page',theme:'dark',route:'/',customTheme:null,themeVars:null,state:{},queries:[],blocks:[]}
946
957
  for(const line of lines) {
958
+ if(line.startsWith('~var ')) continue // já processado globalmente
947
959
  if(line.startsWith('%')) {
948
960
  const pts=line.slice(1).trim().split(/\s+/)
949
961
  p.id=pts[0]||'page'; p.route=pts[2]||'/'
@@ -1049,6 +1061,12 @@ function parseBlock(line) {
1049
1061
  if(line.startsWith('steps{')) { const m=line.match(/^steps\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'steps',items:m[1].split('|').map(it=>{const p=it.trim().split('>');return{num:p[0]?.trim(),title:p[1]?.trim(),desc:p[2]?.trim()}}),variant:vm?.[1]}} }
1050
1062
  if(line.startsWith('compare{')) { const m=line.match(/^compare\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'compare',rows:m[1].split('|').map(r=>r.trim().split(':').map(c=>c.trim())),variant:vm?.[1]}} }
1051
1063
  if(line.startsWith('video{')) { const m=line.match(/^video\{([^}]*)\}/);if(m){const pts=m[1].split('|');return{kind:'video',url:pts[0]?.trim(),poster:pts[1]?.trim()}} }
1064
+ if(line.startsWith('bento{')) {
1065
+ const m=line.match(/^bento\{([^}]*)\}/)
1066
+ if(m){const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/)
1067
+ const items=m[1].split('|').map(function(it){const p=it.trim().split('>');return{title:p[0]&&p[0].trim(),desc:p[1]&&p[1].trim(),icon:p[2]&&p[2].trim(),link:p[3]&&p[3].trim()}})
1068
+ return{kind:'bento',items:items,variant:vm&&vm[1],animate:am&&am[1]}}
1069
+ }
1052
1070
  if(line.startsWith('faq{')) {
1053
1071
  const body=line.slice(4,line.lastIndexOf('}')).trim()
1054
1072
  const items=body.split('|').map(i=>{const idx=i.indexOf('>');return{q:i.slice(0,idx).trim(),a:i.slice(idx+1).trim()}}).filter(i=>i.q&&i.a)
@@ -1200,6 +1218,18 @@ function applyMods(html, b) {
1200
1218
 
1201
1219
  function renderPage(page, allPages) {
1202
1220
  const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
1221
+ // Expandir variáveis $var no conteúdo da página
1222
+ const _vars = (page.appVars||{})
1223
+ function expandVars(s) {
1224
+ if (!s || typeof s !== 'string' || !Object.keys(_vars).length) return s
1225
+ return s.replace(/\$(\w+)/g, (m, k) => _vars[k] !== undefined ? _vars[k] : m)
1226
+ }
1227
+ // Pré-processar rawLine de cada bloco
1228
+ page.blocks.forEach(b => {
1229
+ if (b.rawLine) b.rawLine = expandVars(b.rawLine)
1230
+ if (b.title) b.title = expandVars(b.title)
1231
+ if (b.sub) b.sub = expandVars(b.sub)
1232
+ })
1203
1233
  const body=page.blocks.map(b=>{try{return applyMods(renderBlock(b,page),b)}catch(e){console.error('[aiplang] Block render error:',b.kind,e.message);return ''}}).join('')
1204
1234
 
1205
1235
  const tableBlocks = page.blocks.filter(b => b.kind === 'table' && b.binding && b.cols && b.cols.length)
@@ -1254,8 +1284,12 @@ function renderPage(page, allPages) {
1254
1284
  <meta name="description" content="${esc((()=>{const h=page.blocks.find(b=>b.kind==='hero');if(!h)return '';const m=h.rawLine&&h.rawLine.match(/\{([^}]+)\}/);if(!m)return '';const p=m[1].split('>');const s=p[0].split('|')[1];return s?s.trim():''})())}">
1255
1285
  <meta property="og:title" content="${_title}">
1256
1286
  <meta property="og:type" content="website">
1287
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1288
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1257
1289
  <meta property="og:type" content="website">
1258
- <style>${css(page.theme)}${customVars}${themeVarCSS}</style>
1290
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1291
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1292
+ <style>${minCSS(css(page.theme)+customVars+themeVarCSS)}</style>
1259
1293
  </head>
1260
1294
  <body>
1261
1295
  ${body}${hydrate}
@@ -1288,6 +1322,7 @@ function renderBlock(b, page) {
1288
1322
  case 'compare': return rCompare(b)
1289
1323
  case 'video': return rVideo(b)
1290
1324
  case 'gallery': return rGallery(b)
1325
+ case 'bento': return rBento(b)
1291
1326
  case 'raw': return (b.html||'')+'\n'
1292
1327
  case 'html': return `<div class="fx-html">${b.content||''}</div>\n`
1293
1328
  case 'spacer': return `<div class="fx-spacer" style="height:${esc(b.height||'2rem')}"></div>\n`
@@ -1417,8 +1452,11 @@ function rHero(b) {
1417
1452
  else if(f.isLink) ctas+=`<a href="${esc(f.path)}" class="fx-cta">${esc(f.label)}</a>`
1418
1453
  else if(!h1) {
1419
1454
  // *texto* entre asteriscos = gradient text
1420
- const gt = f.text.match(/^\*(.*?)\*$/)
1421
- if(gt) h1=`<h1 class="fx-title"><span class="fx-gradient-text">${esc(gt[1])}</span></h1>`
1455
+ const gt = f.text.match(/^\*(.*?)\*(:warm|:ocean|:purple)?$/)
1456
+ if(gt) {
1457
+ const gradClass = gt[2]=== ':warm' ? 'fx-gradient-text-warm' : gt[2]===':ocean' ? 'fx-gradient-text-ocean' : gt[2]===':purple' ? 'fx-gradient-text-purple' : 'fx-gradient-text'
1458
+ h1=`<h1 class="fx-title"><span class="${gradClass}">${esc(gt[1])}</span></h1>`
1459
+ }
1422
1460
  else h1=`<h1 class="fx-title">${esc(f.text)}</h1>`
1423
1461
  }
1424
1462
  else sub+=`<p class="fx-sub">${esc(f.text)}</p>`
@@ -1428,6 +1466,17 @@ function rHero(b) {
1428
1466
  const inlineStyle = b.style && !b.bg ? ` style="${b.style.replace(/,/g,';')}"` : ''
1429
1467
  if (v === 'landing') {
1430
1468
  return `<section class="fx-hero fx-hero-landing"${bgStyle}><div class="fx-hero-grid"></div><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>
1469
+ `
1470
+ }
1471
+ if (v === 'mesh') {
1472
+ // Lovable-style warm gradient blobs
1473
+ const meshStyle = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
1474
+ return `<section class="fx-hero fx-hero-mesh"${bgStyle}${meshStyle}>
1475
+ <div class="fx-mesh-blob"></div>
1476
+ <div class="fx-mesh-blob"></div>
1477
+ <div class="fx-mesh-blob"></div>
1478
+ <div class="fx-hero-inner">${h1}${sub}${ctas}</div>
1479
+ </section>
1431
1480
  `
1432
1481
  }
1433
1482
  if (h1) h1 = heroBadge + h1
@@ -1481,8 +1530,11 @@ function rRow(b) {
1481
1530
  if(fi===1) return`<h3 class="fx-card-title">${esc(f.text)}</h3>`
1482
1531
  return`<p class="fx-card-body">${esc(f.text)}</p>`
1483
1532
  }).join('')
1533
+ let extraClass=''
1534
+ if(b.variant==='glass'||b.variant==='glassmorphism') extraClass=' fx-card-glass'
1535
+ if(b.variant==='shine') extraClass=' fx-card-shine'
1484
1536
  const bgStyle=b.bg?` style="background:${b.bg}"`:(b.variant==='bordered'?` style="border:1px solid var(--accent,#2563eb)22"`:colorStyle)
1485
- return`<div class="fx-card"${bgStyle}>${inner}</div>`
1537
+ return`<div class="fx-card${extraClass}"${bgStyle}>${inner}</div>`
1486
1538
  }).join('')
1487
1539
  const v=b.variant||''
1488
1540
  const wrapStyle=b.style?` style="${b.style.replace(/,/g,';')}"`:''
@@ -1585,6 +1637,28 @@ function rStatsUpgraded(b) {
1585
1637
 
1586
1638
 
1587
1639
 
1640
+
1641
+ // ── rBento: Lovable-style bento grid ─────────────────────────────
1642
+ function rBento(b) {
1643
+ const v = b.variant||'default'
1644
+ const glassClass = v==='glass' ? ' fx-bento-glass' : ''
1645
+ const cards = (b.items||[]).map(function(item, i) {
1646
+ const large = i===0 && (b.items||[]).length>=4
1647
+ const icon = item.icon ? '<div class="fx-bento-icon">'+esc(item.icon)+'</div>' : ''
1648
+ const title = item.title ? '<div class="fx-bento-title">'+esc(item.title)+'</div>' : ''
1649
+ const desc = item.desc ? '<div class="fx-bento-desc">'+esc(item.desc)+'</div>' : ''
1650
+ let link = ''
1651
+ if(item.link) {
1652
+ const lparts = item.link.split(':'); const lhref=lparts[0]; const llabel=lparts.slice(1).join(':')||item.link
1653
+ link = '<a href="'+esc(lhref)+'" class="fx-bento-link">'+esc(llabel)+' →</a>'
1654
+ }
1655
+ const largeClass = large ? ' fx-bento-large' : ''
1656
+ const shineClass = v==='shine' ? ' fx-card-shine' : ''
1657
+ return '<div class="fx-bento-card'+glassClass+largeClass+shineClass+'">'+icon+'<div class="fx-bento-body">'+title+desc+link+'</div></div>'
1658
+ }).join('')
1659
+ const animC = b.animate==='stagger'?' fx-animate-stagger':b.animate?' fx-animate':''
1660
+ return '<div class="fx-bento'+animC+'">'+cards+'</div>\n'
1661
+ }
1588
1662
  // ── rMarquee: faixa de logos/texto em loop infinito ───────────────
1589
1663
  function rMarquee(b) {
1590
1664
  const speed = b.variant === 'fast' ? '15s' : b.variant === 'slow' ? '40s' : '25s'
@@ -1816,6 +1890,16 @@ function genThemeVarCSS(t) {
1816
1890
  }
1817
1891
 
1818
1892
 
1893
+ function minCSS(s) {
1894
+ if(!s) return ''
1895
+ return s
1896
+ .replace(/\/\*[\s\S]*?\*\//g,'')
1897
+ .replace(/\s*([{};:,>~+|])\s*/g,'$1')
1898
+ .replace(/\s+/g,' ')
1899
+ .replace(/;}/g,'}')
1900
+ .trim()
1901
+ }
1902
+
1819
1903
  function css(theme) {
1820
1904
  // ════════════════════════════════════════════════════════════════
1821
1905
  // GEIST DESIGN SYSTEM — Vercel's design language for aiplang
@@ -1823,7 +1907,7 @@ function css(theme) {
1823
1907
  // Fonts: Geist Sans + Geist Mono (Google Fonts)
1824
1908
  // ════════════════════════════════════════════════════════════════
1825
1909
  const base = `
1826
- @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800;900&family=Geist+Mono:wght@400;500;700&display=swap');
1910
+ @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800;900&family=Geist+Mono:wght@400;500;700&display=swap&display=swap');
1827
1911
 
1828
1912
  :root {
1829
1913
  --geist-font:'Geist','Geist Sans',-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
@@ -2570,6 +2654,70 @@ textarea.fx-input{height:auto;padding:10px 12px;resize:vertical;min-height:80px;
2570
2654
  .fx-footer-note{font-size:12px;color:var(--ds-gray-600);font-family:var(--geist-mono)}
2571
2655
  .fx-footer-text{font-size:13px;color:var(--ds-gray-700)}
2572
2656
  @media(max-width:640px){.fx-footer{padding:24px}.fx-footer-inner{flex-direction:column;align-items:flex-start;gap:12px}}
2657
+
2658
+ /* ══════════════════════════════════════════
2659
+ BENTO GRID — Lovable-style irregular grid
2660
+ ══════════════════════════════════════════ */
2661
+ .fx-bento{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:min-content;gap:12px;padding:0 40px 80px}
2662
+ @media(max-width:900px){.fx-bento{grid-template-columns:repeat(2,1fr)}}
2663
+ @media(max-width:600px){.fx-bento{grid-template-columns:1fr;padding:0 24px 48px}}
2664
+ .fx-bento-card{border-radius:var(--radius-lg);padding:28px;background:var(--ds-background-200);border:1px solid var(--ds-gray-alpha-400);display:flex;flex-direction:column;gap:12px;transition:border-color var(--duration-normal) var(--ease-in-out),transform var(--duration-slow) var(--ease-out);position:relative;overflow:hidden;min-height:160px}
2665
+ .fx-bento-card:hover{border-color:var(--ds-gray-alpha-700);transform:translateY(-2px)}
2666
+ .fx-bento-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(var(--aip-accent-rgb),.4),transparent);opacity:0;transition:opacity .3s}
2667
+ .fx-bento-card:hover::before{opacity:1}
2668
+ .fx-bento-large{grid-column:span 2;min-height:220px}
2669
+ @media(max-width:600px){.fx-bento-large{grid-column:span 1}}
2670
+ .fx-bento-glass{background:rgba(255,255,255,.04)!important;backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border:1px solid rgba(255,255,255,.1)!important;box-shadow:0 4px 24px rgba(0,0,0,.2)}
2671
+ .fx-bento-glass:hover{background:rgba(255,255,255,.07)!important;border-color:rgba(255,255,255,.2)!important}
2672
+ .fx-bento-icon{font-size:28px;line-height:1;margin-bottom:4px}
2673
+ .fx-bento-body{display:flex;flex-direction:column;gap:8px;flex:1}
2674
+ .fx-bento-title{font-size:15px;font-weight:700;letter-spacing:-.025em;color:var(--ds-gray-1000);line-height:1.3}
2675
+ .fx-bento-desc{font-size:13px;line-height:1.6;color:var(--ds-gray-900)}
2676
+ .fx-bento-link{font-size:12px;font-weight:600;color:var(--ds-blue-700);margin-top:auto;padding-top:8px}
2677
+
2678
+ /* ══════════════════════════════════════════
2679
+ GLASSMORPHISM — cards e hero
2680
+ ══════════════════════════════════════════ */
2681
+ .fx-card-glass{background:rgba(255,255,255,.04)!important;backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border:1px solid rgba(255,255,255,.1)!important;box-shadow:0 8px 32px rgba(0,0,0,.25)}
2682
+ .fx-card-glass:hover{background:rgba(255,255,255,.07)!important;transform:translateY(-3px);box-shadow:0 12px 48px rgba(0,0,0,.35)}
2683
+
2684
+ /* ══════════════════════════════════════════
2685
+ MESH GRADIENT — Lovable-style blobs animados
2686
+ ══════════════════════════════════════════ */
2687
+ .fx-hero-mesh{overflow:hidden}
2688
+ .fx-mesh-blob{position:absolute;pointer-events:none;border-radius:50%;filter:blur(80px);animation:fx-blob-float 12s ease-in-out infinite}
2689
+ .fx-mesh-blob:nth-child(1){width:600px;height:500px;left:-100px;top:-100px;animation-delay:0s;background:radial-gradient(circle,rgba(var(--mesh-c1,99,102,241),.35),transparent 70%)}
2690
+ .fx-mesh-blob:nth-child(2){width:500px;height:400px;right:-100px;top:100px;animation-delay:-4s;background:radial-gradient(circle,rgba(var(--mesh-c2,236,72,153),.25),transparent 70%)}
2691
+ .fx-mesh-blob:nth-child(3){width:400px;height:400px;left:30%;bottom:-100px;animation-delay:-8s;background:radial-gradient(circle,rgba(var(--mesh-c3,251,146,60),.2),transparent 70%)}
2692
+ @keyframes fx-blob-float{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(30px,-30px) scale(1.05)}66%{transform:translate(-20px,20px) scale(.97)}}
2693
+
2694
+ /* ══════════════════════════════════════════
2695
+ ANIMATED GRADIENT BUTTON — Vercel style
2696
+ ══════════════════════════════════════════ */
2697
+ .fx-cta-glow-btn{position:relative;border-radius:var(--radius-md);padding:2px;background:linear-gradient(-90deg,#007cf0,#00dfd8,rgba(var(--aip-accent-rgb),1),#007cf0);background-size:400% 100%;animation:fx-grad-btn 8s ease-in-out infinite;display:inline-block}
2698
+ @keyframes fx-grad-btn{50%{background-position:140% 50%}}
2699
+ .fx-cta-glow-btn-inner{display:block;height:38px;padding:0 20px;line-height:38px;background:var(--ds-background-100);border-radius:calc(var(--radius-md) - 2px);font-size:14px;font-weight:600;color:var(--ds-gray-1000);transition:background var(--duration-normal);white-space:nowrap}
2700
+ .fx-cta-glow-btn:hover .fx-cta-glow-btn-inner{background:var(--ds-background-200)}
2701
+
2702
+ /* ══════════════════════════════════════════
2703
+ GRADIENT TEXT VARIANTS — Lovable paletas
2704
+ ══════════════════════════════════════════ */
2705
+ .fx-gradient-text-warm{background:linear-gradient(135deg,#6366f1,#ec4899,#f97316);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
2706
+ .fx-gradient-text-ocean{background:linear-gradient(135deg,#06b6d4,#3b82f6,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
2707
+ .fx-gradient-text-purple{background:linear-gradient(135deg,#a855f7,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
2708
+
2709
+ /* ══════════════════════════════════════════
2710
+ FLOATING ANIMATION — micro-interaction
2711
+ ══════════════════════════════════════════ */
2712
+ .fx-float{animation:fx-floating 4s ease-in-out infinite}
2713
+ .fx-float-2{animation:fx-floating 5s ease-in-out infinite;animation-delay:-1.5s}
2714
+ .fx-float-3{animation:fx-floating 6s ease-in-out infinite;animation-delay:-3s}
2715
+ @keyframes fx-floating{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
2716
+
2717
+ /* Shine sweep nos cards */
2718
+ .fx-card-shine{overflow:hidden}
2719
+ .fx-card-shine::after{content:'';position:absolute;inset:0;background:linear-gradient(135deg,transparent 30%,rgba(255,255,255,.04) 50%,transparent 70%);transform:translateX(-100%);transition:transform .6s var(--ease-out);pointer-events:none}
2720
+ .fx-card-shine:hover::after{transform:translateX(100%)}
2573
2721
  `
2574
2722
 
2575
2723
  // ════════════════════════════════════════════════════════════════
@@ -2613,6 +2761,54 @@ textarea.fx-input{height:auto;padding:10px 12px;resize:vertical;min-height:80px;
2613
2761
  --ds-red-700:#ff0000;
2614
2762
  --ds-green-100:rgba(0,188,112,.08);
2615
2763
  --ds-green-700:#00bc70;
2764
+ }`,
2765
+ warm: `
2766
+ :root{
2767
+ --ds-background-100:#0a0010;--ds-background-100-rgb:10,0,16;--ds-background-200:#110018;
2768
+ --ds-gray-100:#110018;--ds-gray-200:#1a0025;--ds-gray-300:#250032;--ds-gray-400:#2e0040;
2769
+ --ds-gray-600:#9d6cb5;--ds-gray-700:#b87fd4;--ds-gray-800:#c896e8;--ds-gray-900:#d4aef0;--ds-gray-1000:#f0e0ff;
2770
+ --ds-gray-alpha-100:rgba(200,100,255,.04);--ds-gray-alpha-200:rgba(200,100,255,.07);
2771
+ --ds-gray-alpha-400:rgba(200,100,255,.12);--ds-gray-alpha-700:rgba(200,100,255,.22);
2772
+ --ds-blue-700:#a78bfa;--ds-blue-rgb:167,139,250;
2773
+ --ds-green-700:#4ade80;--ds-red-700:#f87171;
2774
+ --aip-accent:#ec4899;--aip-accent-rgb:236,72,153;
2775
+ --mesh-c1:99,102,241;--mesh-c2:236,72,153;--mesh-c3:251,146,60;
2776
+ }`,
2777
+ ocean: `
2778
+ :root{
2779
+ --ds-background-100:#00080f;--ds-background-100-rgb:0,8,15;--ds-background-200:#000d18;
2780
+ --ds-gray-100:#000d18;--ds-gray-200:#001524;--ds-gray-300:#001d30;--ds-gray-400:#00253e;
2781
+ --ds-gray-600:#4a8fab;--ds-gray-700:#5ba8c8;--ds-gray-800:#70bfe0;--ds-gray-900:#8dd0f0;--ds-gray-1000:#d0efff;
2782
+ --ds-gray-alpha-100:rgba(100,200,255,.04);--ds-gray-alpha-200:rgba(100,200,255,.07);
2783
+ --ds-gray-alpha-400:rgba(100,200,255,.12);--ds-gray-alpha-700:rgba(100,200,255,.22);
2784
+ --ds-blue-700:#38bdf8;--ds-blue-rgb:56,189,248;
2785
+ --ds-green-700:#34d399;--ds-red-700:#f87171;
2786
+ --aip-accent:#0ea5e9;--aip-accent-rgb:14,165,233;
2787
+ --mesh-c1:6,182,212;--mesh-c2:59,130,246;--mesh-c3:139,92,246;
2788
+ }`,
2789
+ purple: `
2790
+ :root{
2791
+ --ds-background-100:#05000f;--ds-background-100-rgb:5,0,15;--ds-background-200:#0a0018;
2792
+ --ds-gray-100:#0a0018;--ds-gray-200:#120022;--ds-gray-300:#1a002e;--ds-gray-400:#22003c;
2793
+ --ds-gray-600:#7c5caf;--ds-gray-700:#9b72d8;--ds-gray-800:#b38ef5;--ds-gray-900:#c9a8ff;--ds-gray-1000:#eddeff;
2794
+ --ds-gray-alpha-100:rgba(180,130,255,.04);--ds-gray-alpha-200:rgba(180,130,255,.07);
2795
+ --ds-gray-alpha-400:rgba(180,130,255,.12);--ds-gray-alpha-700:rgba(180,130,255,.22);
2796
+ --ds-blue-700:#a78bfa;--ds-blue-rgb:167,139,250;
2797
+ --ds-green-700:#4ade80;--ds-red-700:#f87171;
2798
+ --aip-accent:#8b5cf6;--aip-accent-rgb:139,92,246;
2799
+ --mesh-c1:139,92,246;--mesh-c2:236,72,153;--mesh-c3:99,102,241;
2800
+ }`,
2801
+ midnight: `
2802
+ :root{
2803
+ --ds-background-100:#010309;--ds-background-100-rgb:1,3,9;--ds-background-200:#030610;
2804
+ --ds-gray-100:#060c1a;--ds-gray-200:#0d1629;--ds-gray-300:#152033;--ds-gray-400:#1c2a3e;
2805
+ --ds-gray-600:#4c6480;--ds-gray-700:#6580a0;--ds-gray-800:#7d98bc;--ds-gray-900:#a0b4d0;--ds-gray-1000:#d8e8f8;
2806
+ --ds-gray-alpha-100:rgba(100,160,220,.04);--ds-gray-alpha-200:rgba(100,160,220,.07);
2807
+ --ds-gray-alpha-400:rgba(100,160,220,.12);--ds-gray-alpha-700:rgba(100,160,220,.22);
2808
+ --ds-blue-700:#60a5fa;--ds-blue-rgb:96,165,250;
2809
+ --ds-green-700:#4ade80;--ds-red-700:#f87171;
2810
+ --aip-accent:#3b82f6;--aip-accent-rgb:59,130,246;
2811
+ --mesh-c1:59,130,246;--mesh-c2:99,102,241;--mesh-c3:14,165,233;
2616
2812
  }`,
2617
2813
  light: `
2618
2814
  :root {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.9",
3
+ "version": "2.11.11",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
package/server/server.js CHANGED
@@ -1806,6 +1806,25 @@ class AiplangServer {
1806
1806
  if (this._requestLogger) this._requestLogger(req, Date.now() - _start)
1807
1807
  return
1808
1808
  }
1809
+ // Servir arquivos estaticos /public/*
1810
+ const _sext = req.path && req.path.match(/\.([a-zA-Z0-9]+)$/)
1811
+ if (_sext && req.method === 'GET') {
1812
+ const _appDir = server && server._appDir ? server._appDir : process.cwd()
1813
+ const _cands = [
1814
+ require('path').join(_appDir,'public',req.path),
1815
+ require('path').join(process.cwd(),'public',req.path),
1816
+ require('path').join(_appDir,req.path.slice(1)),
1817
+ ]
1818
+ for (const _fp of _cands) {
1819
+ if (require('fs').existsSync(_fp)) {
1820
+ const _mime = getMimeType(_fp)
1821
+ const _buf = require('fs').readFileSync(_fp)
1822
+ res.setHeader('Cache-Control','public,max-age=86400')
1823
+ res.writeHead(200,{'Content-Type':_mime,'Content-Length':_buf.length})
1824
+ return res.end(_buf)
1825
+ }
1826
+ }
1827
+ }
1809
1828
  const _404b='{"error":"Not found"}'; res.writeHead(404,{'Content-Type':'application/json','Content-Length':18}); res.end(_404b)
1810
1829
  }
1811
1830
 
@@ -2137,6 +2156,17 @@ function getMime(filename) {
2137
2156
  return types[ext] || 'application/octet-stream'
2138
2157
  }
2139
2158
 
2159
+ // Cache-Control helper
2160
+ function _cacheHeaders(res, type, maxAge=0) {
2161
+ if (type && (type.startsWith('image/') || type.startsWith('font/') || type.endsWith('javascript') || type.endsWith('css'))) {
2162
+ res.setHeader('Cache-Control', `public, max-age=${maxAge||86400}, immutable`)
2163
+ } else if (type && type.includes('html')) {
2164
+ res.setHeader('Cache-Control', 'no-cache')
2165
+ } else {
2166
+ res.setHeader('Cache-Control', 'public, max-age=3600')
2167
+ }
2168
+ }
2169
+
2140
2170
  // ═══════════════════════════════════════════════════════════════════
2141
2171
  // PLUGIN SYSTEM — ~plugin ./my-plugin.js | ~use rate-limit max=100
2142
2172
  // ═══════════════════════════════════════════════════════════════════
@@ -2387,7 +2417,7 @@ async function startServer(aipFile, port = 3000) {
2387
2417
  // Static assets
2388
2418
  srv.addRoute('GET', '/aiplang-hydrate.js', (req, res) => {
2389
2419
  const p = path.join(__dirname, '..', 'runtime', 'aiplang-hydrate.js')
2390
- if (fs.existsSync(p)) { res.writeHead(200,{'Content-Type':'application/javascript'}); res.end(fs.readFileSync(p)) }
2420
+ if (fs.existsSync(p)) { res.setHeader('Cache-Control','public,max-age=86400'); res.writeHead(200,{'Content-Type':'application/javascript'}); res.end(fs.readFileSync(p)) }
2391
2421
  else { res.writeHead(404); res.end('// not found') }
2392
2422
  })
2393
2423
 
@@ -2432,7 +2462,7 @@ async function startServer(aipFile, port = 3000) {
2432
2462
  })
2433
2463
 
2434
2464
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2435
- status:'ok', version:'2.11.9',
2465
+ status:'ok', version:'2.11.11',
2436
2466
  models: app.models.map(m=>m.name),
2437
2467
  routes: app.apis.length, pages: app.pages.length,
2438
2468
  admin: app.admin?.prefix || null,