aiplang 2.11.4 → 2.11.6

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.4'
8
+ const VERSION = '2.11.6'
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.4'`)
689
+ lines.push(`export const AIPLANG_VERSION = '2.11.6'`)
690
690
  lines.push(``)
691
691
  return lines.join('\n')
692
692
  }
@@ -1040,6 +1040,10 @@ function parseBlock(line) {
1040
1040
  return{kind:'pricing',plans,extraClass,animate,variant,style,bg}
1041
1041
  }
1042
1042
 
1043
+ if(line.startsWith('code{')) { const m=line.match(/^code\{([^}]*)\}/);if(m){const pts=m[1].split('|');const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'code',lang:pts[0]?.trim()||'aip',lines:pts.slice(1).map(l=>l.trim()),variant:vm?.[1],animate:am?.[1]}} }
1044
+ if(line.startsWith('benchmark{')) { const m=line.match(/^benchmark\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'benchmark',items:m[1].split('|').map(it=>{const p=it.trim().split(':');return{num:p[0]?.trim(),label:p[1]?.trim(),vs:p[2]?.trim(),pct:parseInt(p[3])||0}}),variant:vm?.[1],animate:am?.[1]}} }
1045
+ if(line.startsWith('install{')) { const m=line.match(/^install\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'install',cmds:m[1].split('|').map(c=>c.trim()).filter(Boolean),variant:vm?.[1]}} }
1046
+ if(line.startsWith('feature{')) { const b=parseBlock(line.replace(/^feature/,'row3'));if(b){b.variant='feature'};return b }
1043
1047
  if(line.startsWith('faq{')) {
1044
1048
  const body=line.slice(4,line.lastIndexOf('}')).trim()
1045
1049
  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)
@@ -1268,6 +1272,10 @@ function renderBlock(b, page) {
1268
1272
  case 'select': return rSelectBlock(b)
1269
1273
  case 'pricing': return rPricing(b)
1270
1274
  case 'faq': return rFaq(b)
1275
+ case 'code': return rCode(b)
1276
+ case 'benchmark': return rBenchmark(b)
1277
+ case 'install': return rInstall(b)
1278
+ case 'feature': return rRow(b)
1271
1279
  case 'testimonial': return rTestimonial(b)
1272
1280
  case 'gallery': return rGallery(b)
1273
1281
  case 'raw': return (b.html||'')+'\n'
@@ -1347,6 +1355,49 @@ function rNav(b) {
1347
1355
  return `<nav class="fx-nav">${brand}<button class="fx-hamburger" onclick="this.classList.toggle('open');document.querySelector('.fx-nav-links').classList.toggle('open')" aria-label="Menu"><span></span><span></span><span></span></button><div class="fx-nav-links">${links}</div></nav>\n`
1348
1356
  }
1349
1357
 
1358
+
1359
+
1360
+ // ── Parser: code{lang|linha1|linha2} ─────────────────────────────
1361
+ function parseCode(line) {
1362
+ const m = line.match(/^code\{([^}]*)\}/)
1363
+ if (!m) return null
1364
+ const parts = m[1].split('|')
1365
+ const lang = parts[0]?.trim() || 'aip'
1366
+ const lines = parts.slice(1).map(l => l.trim())
1367
+ const b = parseBlockMeta(line)
1368
+ return { ...b, kind:'code', lang, lines }
1369
+ }
1370
+
1371
+ // ── Parser: benchmark{Num:Label:vs texto|...} ─────────────────────
1372
+ function parseBenchmark(line) {
1373
+ const m = line.match(/^benchmark\{([^}]*)\}/)
1374
+ if (!m) return null
1375
+ const items = m[1].split('|').map(item => {
1376
+ const parts = item.trim().split(':')
1377
+ return { num: parts[0]?.trim(), label: parts[1]?.trim(), vs: parts[2]?.trim(), pct: parts[3]?.trim() }
1378
+ })
1379
+ const b = parseBlockMeta(line)
1380
+ return { ...b, kind:'benchmark', items }
1381
+ }
1382
+
1383
+ // ── Parser: install{cmd1|cmd2|...} ───────────────────────────────
1384
+ function parseInstall(line) {
1385
+ const m = line.match(/^install\{([^}]*)\}/)
1386
+ if (!m) return null
1387
+ const cmds = m[1].split('|').map(c => c.trim()).filter(Boolean)
1388
+ const b = parseBlockMeta(line)
1389
+ return { ...b, kind:'install', cmds }
1390
+ }
1391
+
1392
+ // ── Parser: feature{emoji>Título>Desc | ...} (alias row3 otimizado)
1393
+ function parseFeature(line) {
1394
+ // Converte feature{} em row{} com cols=3 e variant=feature
1395
+ const inner = line.replace(/^feature/, 'row3')
1396
+ const b = parseRow(inner)
1397
+ if (b) b.variant = 'feature'
1398
+ return b
1399
+ }
1400
+
1350
1401
  function rHero(b) {
1351
1402
  let h1='',sub='',img='',ctas=''
1352
1403
  for(const item of (b.items||[])) for(const f of item){
@@ -1358,6 +1409,10 @@ function rHero(b) {
1358
1409
  const v = b.variant || (img ? 'split' : 'centered')
1359
1410
  const bgStyle = b.bg ? ` style="background:${b.bg}"` : b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
1360
1411
  const inlineStyle = b.style && !b.bg ? ` style="${b.style.replace(/,/g,';')}"` : ''
1412
+ if (v === 'landing') {
1413
+ return `<section class="fx-hero fx-hero-landing"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>
1414
+ `
1415
+ }
1361
1416
  if (v === 'minimal') {
1362
1417
  return `<section class="fx-hero fx-hero-minimal"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>\n`
1363
1418
  }
@@ -1430,13 +1485,113 @@ function rSect(b) {
1430
1485
  return `<section class="fx-sect${cls}"${bgStyle}>${inner}</section>\n`
1431
1486
  }
1432
1487
 
1488
+
1489
+ // ── rCode: code window com syntax highlight ───────────────────────
1490
+ function rCode(b) {
1491
+ const lang = b.lang || 'aip'
1492
+ const id = 'code_' + Math.random().toString(36).slice(2,7)
1493
+ const highlighted = (b.lines||[]).map(line => {
1494
+ let l = esc(line)
1495
+ if(lang==='aip'||lang==='aiplang') {
1496
+ l = l.replace(/^(~\w+)/g,'<span class="fx-kw">$1</span>')
1497
+ l = l.replace(/\$([\w.]+)/g,'<span class="fx-nb">$$$1</span>')
1498
+ l = l.replace(/(\{[^}]*\})/g,'<span class="fx-op">$1</span>')
1499
+ l = l.replace(/(#[\w-]+(?:\.\w+)*)/g,'<span class="fx-comment">$1</span>')
1500
+ } else if(lang==='bash'||lang==='sh') {
1501
+ l = l.replace(/^(\$ )/,'<span class="fx-kw">$1</span>')
1502
+ l = l.replace(/(#.*$)/,'<span class="fx-comment">$1</span>')
1503
+ l = l.replace(/(npx aiplang \w+)/g,'<span class="fx-fn">$1</span>')
1504
+ } else if(lang==='js'||lang==='ts') {
1505
+ l = l.replace(/\b(const|let|var|function|async|await|return|import|from|export|if|else|for)\b/g,'<span class="fx-kw">$1</span>')
1506
+ l = l.replace(/(\/\/.*$)/,'<span class="fx-comment">$1</span>')
1507
+ l = l.replace(/(['"`][^'"`]*['"`])/g,'<span class="fx-st">$1</span>')
1508
+ }
1509
+ return `<div class="fx-code-line">${l}</div>`
1510
+ }).join('')
1511
+ const copyBtn = `<button class="fx-code-copy" onclick="(function(b){navigator.clipboard&&navigator.clipboard.writeText(b.querySelectorAll('.fx-code-line').length?Array.from(b.querySelectorAll('.fx-code-line')).map(l=>l.innerText).join('\\n'):'');var t=b.querySelector('.fx-code-copy');t&&(t.textContent='copiado!',setTimeout(()=>t.textContent='copiar',1500))})(this.closest('.fx-code-window'))">copiar</button>`
1512
+ return `<div class="fx-code-window"><div class="fx-code-bar"><div class="fx-dots"><span></span><span></span><span></span></div><span class="fx-code-lang">${esc(lang)}</span>${copyBtn}</div><div class="fx-code-body" id="${id}">${highlighted}</div></div>\n`
1513
+ }
1514
+
1515
+ // ── rBenchmark: cards de benchmark com número + barra ─────────────
1516
+ function rBenchmark(b) {
1517
+ const cards = (b.items||[]).map(item => {
1518
+ const pct = item.pct || (item.num && item.num.includes('%') ? parseInt(item.num) : 85)
1519
+ const n = item.num || ''
1520
+ const isLow = pct < 20
1521
+ return `<div class="fx-bench-card">
1522
+ <div class="fx-bench-label">${esc(item.label||'')}</div>
1523
+ <div class="fx-bench-num">${esc(n)}</div>
1524
+ ${item.vs ? `<div class="fx-bench-vs">${esc(item.vs)}</div>` : ''}
1525
+ <div class="fx-bench-bar"><div class="fx-bench-fill" style="width:${isLow?pct+'%':pct+'%'}"></div></div>
1526
+ </div>`
1527
+ }).join('')
1528
+ return `<div class="fx-benchmark">${cards}</div>\n`
1529
+ }
1530
+
1531
+ // ── rInstall: multi-step code box com botões ──────────────────────
1532
+ function rInstall(b) {
1533
+ const steps = (b.cmds||[]).map((cmd,i) => {
1534
+ const isComment = cmd.startsWith('#')
1535
+ if(isComment) return `<div class="fx-install-comment">${esc(cmd.slice(1).trim())}</div>`
1536
+ return `<div class="fx-install-line"><span class="fx-install-prompt">$</span><span class="fx-install-cmd">${esc(cmd)}</span></div>`
1537
+ }).join('')
1538
+ const firstCmd = (b.cmds||[]).find(c => !c.startsWith('#')) || ''
1539
+ const copy = `navigator.clipboard&&navigator.clipboard.writeText(${JSON.stringify(firstCmd)})`
1540
+ return `<div class="fx-install-wrap">
1541
+ <div class="fx-code-bar"><div class="fx-dots"><span></span><span></span><span></span></div><span class="fx-code-lang">terminal</span><button class="fx-code-copy" onclick="${copy};var t=this;t.textContent='copiado!';setTimeout(()=>t.textContent='copiar',1500)">copiar</button></div>
1542
+ <div class="fx-install-body">${steps}</div>
1543
+ </div>\n`
1544
+ }
1545
+
1546
+ // ── rStats upgrade: suporte a subtítulo via "val:label:vs" ────────
1547
+ function rStatsUpgraded(b) {
1548
+ const cells = (b.items||[]).map(item => {
1549
+ const raw = item[0]?.text || ''
1550
+ const parts = raw.split(':')
1551
+ const val = parts[0]?.trim()
1552
+ const lbl = parts[1]?.trim()
1553
+ const vs = parts[2]?.trim()
1554
+ const bind = isDyn(val) ? ` data-fx-bind="${esc(val)}"` : ''
1555
+ return `<div class="fx-stat">
1556
+ <div class="fx-stat-val"${bind}>${esc(val)}</div>
1557
+ <div class="fx-stat-lbl">${esc(lbl||'')}</div>
1558
+ ${vs ? `<div class="fx-stat-vs">${esc(vs)}</div>` : ''}
1559
+ </div>`
1560
+ }).join('')
1561
+ return `<div class="fx-stats">${cells}</div>\n`
1562
+ }
1563
+
1564
+
1565
+ // ── autoYear: substitui © YYYY por © <span data-fx-year></span> ──
1566
+ function autoYear(text) {
1567
+ // Substitui padrões como "© 2024", "© 2025", "© 2026" ou só "©" pelo ano dinâmico
1568
+ const replaced = esc(text).replace(/©\s*(\d{4})?/g, (_, yr) =>
1569
+ `© <span class="fx-year">${yr||new Date().getFullYear()}</span>`
1570
+ )
1571
+ return replaced
1572
+ }
1573
+
1433
1574
  function rFoot(b) {
1575
+ const _yearScript = `<script>document.querySelectorAll('.fx-year').forEach(function(el){el.textContent=new Date().getFullYear()})</script>`
1576
+ let brand='', links='', note=''
1577
+ let itemIdx = 0
1578
+ for(const item of (b.items||[])) for(const f of item){
1579
+ if(f.isLink) links+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
1580
+ else if(itemIdx===0) { brand=`<span class="fx-footer-brand">${esc(f.text)}</span>`; itemIdx++ }
1581
+ else note=`<span class="fx-footer-note">${autoYear(f.text)}</span>`
1582
+ }
1583
+ if(brand||links){
1584
+ return `<footer class="fx-footer"><div class="fx-footer-inner">${brand}<div class="fx-footer-links">${links}</div>${note}</div></footer>${_yearScript}
1585
+ `
1586
+ }
1587
+ // fallback centrado
1434
1588
  let inner=''
1435
1589
  for(const item of (b.items||[])) for(const f of item){
1436
1590
  if(f.isLink) inner+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
1437
- else inner+=`<p class="fx-footer-text">${esc(f.text)}</p>`
1591
+ else inner+=`<p class="fx-footer-text">${autoYear(f.text)}</p>`
1438
1592
  }
1439
- return `<footer class="fx-footer">${inner}</footer>\n`
1593
+ return `<footer class="fx-footer">${inner}</footer>
1594
+ `
1440
1595
  }
1441
1596
 
1442
1597
  function rTable(b) {
@@ -1557,6 +1712,52 @@ function genThemeVarCSS(t) {
1557
1712
 
1558
1713
  function css(theme) {
1559
1714
  const base=`*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased;min-height:100vh}a{text-decoration:none;color:inherit}input,button,select{font-family:inherit}img{max-width:100%;height:auto}.fx-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2.5rem;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px);flex-wrap:wrap;gap:.5rem}.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}.fx-nav-links{display:flex;align-items:center;gap:1.75rem}.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s}.fx-nav-link:hover{opacity:1}.fx-hamburger{display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;padding:.25rem}.fx-hamburger span{display:block;width:22px;height:2px;background:currentColor;transition:all .2s;border-radius:1px}.fx-hamburger.open span:nth-child(1){transform:rotate(45deg) translate(5px,5px)}.fx-hamburger.open span:nth-child(2){opacity:0}.fx-hamburger.open span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px)}@media(max-width:640px){.fx-hamburger{display:flex}.fx-nav-links{display:none;width:100%;flex-direction:column;align-items:flex-start;gap:.75rem;padding:.75rem 0}.fx-nav-links.open{display:flex}}.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}.fx-hero-split{display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center;padding:4rem 2.5rem;min-height:70vh}@media(max-width:768px){.fx-hero-split{grid-template-columns:1fr}}.fx-hero-img{width:100%;border-radius:1.25rem;object-fit:cover;max-height:500px}.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}.fx-hero-split .fx-hero-inner{text-align:left;align-items:flex-start;max-width:none}.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}.fx-cta{display:inline-flex;align-items:center;padding:.875rem 2.5rem;border-radius:.75rem;font-weight:700;font-size:1rem;letter-spacing:-.01em;transition:transform .15s;margin:.25rem}.fx-cta:hover{transform:translateY(-1px)}.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}.fx-card:hover{transform:translateY(-2px)}.fx-card-img{width:100%;border-radius:.75rem;object-fit:cover;height:180px;margin-bottom:1rem}.fx-icon{font-size:2rem;margin-bottom:1rem}.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}.fx-card-body{font-size:.875rem;line-height:1.65}.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}.fx-card-link:hover{opacity:1}.fx-sect{padding:5rem 2.5rem}.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}.fx-field{margin-bottom:1.25rem}.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s}.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}.fx-btn{width:100%;padding:.875rem 1.5rem;border:none;border-radius:.625rem;font-size:.9375rem;font-weight:700;cursor:pointer;margin-top:.5rem;transition:transform .15s,opacity .15s;letter-spacing:-.01em}.fx-btn:hover{transform:translateY(-1px)}.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.fx-btn-wrap{padding:0 2.5rem 1.5rem}.fx-standalone-btn{width:auto;padding:.75rem 2rem;margin-top:0}.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}.fx-form-err{color:#f87171}.fx-form-ok{color:#4ade80}.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.fx-th-actions{opacity:.6}.fx-tr{transition:background .1s}.fx-td{padding:.875rem 1.25rem}.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}.fx-td-actions{white-space:nowrap;padding:.5rem 1rem!important}.fx-action-btn{border:none;cursor:pointer;font-size:.75rem;font-weight:600;padding:.3rem .75rem;border-radius:.375rem;margin-right:.375rem;font-family:inherit;transition:opacity .15s}.fx-action-btn:hover{opacity:.85}.fx-edit-btn{background:#1e40af;color:#93c5fd}.fx-delete-btn{background:#7f1d1d;color:#fca5a5}.fx-select-wrap{padding:.5rem 2.5rem}.fx-select-block{width:auto;min-width:200px;margin-top:0}.fx-pricing{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;padding:2rem 2.5rem 5rem;align-items:start}.fx-pricing-card{border-radius:1.25rem;padding:2rem;position:relative;transition:transform .2s}.fx-pricing-featured{transform:scale(1.03)}.fx-pricing-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#2563eb;color:#fff;font-size:.7rem;font-weight:700;padding:.25rem .875rem;border-radius:999px;white-space:nowrap;letter-spacing:.05em}.fx-pricing-name{font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem;opacity:.7}.fx-pricing-price{font-size:3rem;font-weight:900;letter-spacing:-.05em;line-height:1;margin-bottom:.75rem}.fx-pricing-desc{font-size:.875rem;line-height:1.65;margin-bottom:1.5rem;opacity:.7}.fx-pricing-cta{display:block;text-align:center;padding:.75rem;border-radius:.625rem;font-weight:700;font-size:.9rem;transition:opacity .15s}.fx-pricing-cta:hover{opacity:.85}.fx-faq{max-width:48rem;margin:0 auto}.fx-faq-item{border-radius:.75rem;margin-bottom:.625rem;cursor:pointer;overflow:hidden;transition:background .15s}.fx-faq-q{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;font-size:.9375rem;font-weight:600}.fx-faq-arrow{transition:transform .2s;font-size:.75rem;opacity:.5}.fx-faq-item.open .fx-faq-arrow{transform:rotate(90deg)}.fx-faq-a{max-height:0;overflow:hidden;padding:0 1.25rem;font-size:.875rem;line-height:1.7;transition:max-height .3s,padding .3s}.fx-faq-item.open .fx-faq-a{max-height:300px;padding:.75rem 1.25rem 1.25rem}.fx-testi-wrap{padding:5rem 2.5rem;display:flex;justify-content:center}.fx-testi{max-width:42rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.25rem}.fx-testi-img{width:64px;height:64px;border-radius:50%;object-fit:cover}.fx-testi-avatar{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;background:#1e293b}.fx-testi-quote{font-size:1.25rem;line-height:1.7;font-style:italic;opacity:.9}.fx-testi-author{font-size:.875rem;font-weight:600;opacity:.5}.fx-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;padding:1rem 2.5rem 4rem}.fx-gallery-item{border-radius:.75rem;overflow:hidden;aspect-ratio:4/3}.fx-gallery-item img{width:100%;height:100%;object-fit:cover;transition:transform .3s}.fx-gallery-item:hover img{transform:scale(1.04)}.fx-if-wrap{display:contents}.fx-footer{padding:3rem 2.5rem;text-align:center}.fx-footer-text{font-size:.8125rem}.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}.fx-footer-link:hover{opacity:1}
1715
+ /* ── code window ── */
1716
+ .fx-code-window{border-radius:.875rem;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin:0 2.5rem 2rem}
1717
+ .fx-code-bar{display:flex;align-items:center;gap:.75rem;padding:.625rem 1rem;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.06)}
1718
+ .fx-dots{display:flex;gap:.375rem}
1719
+ .fx-dots span{width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.15)}
1720
+ .fx-dots span:nth-child(1){background:#ff5f57}.fx-dots span:nth-child(2){background:#febc2e}.fx-dots span:nth-child(3){background:#28c840}
1721
+ .fx-code-lang{font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;opacity:.35;font-family:monospace;margin-left:.25rem}
1722
+ .fx-code-copy{margin-left:auto;font-family:monospace;font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;background:none;border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.4);padding:.2rem .625rem;border-radius:.3rem;cursor:pointer;transition:all .15s}
1723
+ .fx-code-copy:hover{border-color:rgba(255,255,255,.3);color:rgba(255,255,255,.7)}
1724
+ .fx-code-body{padding:1.375rem 1.5rem;overflow-x:auto}
1725
+ .fx-code-line{font-family:"JetBrains Mono","Fira Code","Courier New",monospace;font-size:.8rem;line-height:1.75;color:#8899aa;white-space:pre}
1726
+ .fx-kw{color:#c792ea}.fx-st{color:#c3e88d}.fx-fn{color:#82aaff}.fx-nb{color:#f78c6c}.fx-op{color:var(--accent,#ff5722)}.fx-comment{color:#3d5166;font-style:italic}
1727
+ /* ── benchmark ── */
1728
+ .fx-benchmark{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem;padding:1rem 2.5rem 4rem}
1729
+ .fx-bench-card{border-radius:1rem;padding:1.5rem;border:1px solid rgba(255,255,255,.06);background:rgba(255,255,255,.02);position:relative;overflow:hidden}
1730
+ .fx-bench-card::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent,#ff5722),transparent)}
1731
+ .fx-bench-label{font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;opacity:.5;margin-bottom:.5rem;font-family:monospace}
1732
+ .fx-bench-num{font-size:clamp(2rem,5vw,3.5rem);font-weight:900;letter-spacing:-.04em;line-height:1;color:var(--accent,#ff5722)}
1733
+ .fx-bench-vs{font-size:.7rem;opacity:.4;margin-top:.25rem;font-family:monospace}
1734
+ .fx-bench-bar{margin-top:1rem;height:5px;background:rgba(255,255,255,.06);border-radius:3px;overflow:hidden}
1735
+ .fx-bench-fill{height:100%;border-radius:3px;background:var(--accent,#ff5722);transition:width 1.5s cubic-bezier(.4,0,.2,1)}
1736
+ /* ── install ── */
1737
+ .fx-install-wrap{border-radius:.875rem;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin:0 2.5rem 2rem;max-width:540px}
1738
+ .fx-install-body{padding:1.25rem 1.5rem}
1739
+ .fx-install-line{display:flex;gap:.75rem;padding:.25rem 0;font-family:"JetBrains Mono","Courier New",monospace;font-size:.8rem;line-height:1.7}
1740
+ .fx-install-prompt{color:var(--accent,#ff5722);flex-shrink:0}
1741
+ .fx-install-cmd{color:rgba(255,255,255,.85)}
1742
+ .fx-install-comment{font-family:"JetBrains Mono","Courier New",monospace;font-size:.72rem;color:rgba(255,255,255,.25);padding:.25rem 0;font-style:italic}
1743
+ /* ── stats upgrade ── */
1744
+ .fx-stat-vs{font-size:.65rem;opacity:.35;margin-top:.2rem;letter-spacing:.02em}
1745
+ /* ── hero landing (dark variant) grid + glow ── */
1746
+ .fx-hero-landing{position:relative;overflow:hidden}
1747
+ .fx-hero-landing::before{content:"";position:absolute;inset:0;background:linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 70% 60% at 50% 50%,black,transparent);pointer-events:none}
1748
+ .fx-hero-landing::after{content:"";position:absolute;width:700px;height:500px;border-radius:50%;background:radial-gradient(ellipse,rgba(255,87,34,.13) 0%,transparent 70%);left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;animation:fx-breathe 6s ease-in-out infinite}
1749
+ @keyframes fx-breathe{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.1)}}
1750
+ /* ── feature grid ── */
1751
+ .fx-grid-feature{gap:1rem}
1752
+ .fx-grid-feature .fx-card{transition:border-color .2s,transform .15s}
1753
+ .fx-grid-feature .fx-card:hover{border-color:rgba(255,87,34,.25)}
1754
+ .fx-grid-feature .fx-icon{width:44px;height:44px;border-radius:.75rem;display:flex;align-items:center;justify-content:center;background:rgba(255,87,34,.08);border:1px solid rgba(255,87,34,.15)}
1755
+ /* ── footer upgrade ── */
1756
+ .fx-footer-inner{max-width:1100px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem}
1757
+ .fx-footer-brand{font-size:1rem;font-weight:800;letter-spacing:-.02em}
1758
+ .fx-footer-links{display:flex;gap:1.5rem;flex-wrap:wrap}
1759
+ .fx-footer-note{font-size:.72rem;opacity:.3;font-family:monospace}
1760
+ .fx-year{font-variant-numeric:tabular-nums}
1560
1761
  .fx-hero-minimal{min-height:50vh!important}
1561
1762
  .fx-hero-minimal .fx-hero-inner{gap:1rem}
1562
1763
  .fx-hero-tall{min-height:98vh!important}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.4",
3
+ "version": "2.11.6",
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
@@ -2432,7 +2432,7 @@ async function startServer(aipFile, port = 3000) {
2432
2432
  })
2433
2433
 
2434
2434
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2435
- status:'ok', version:'2.11.4',
2435
+ status:'ok', version:'2.11.6',
2436
2436
  models: app.models.map(m=>m.name),
2437
2437
  routes: app.apis.length, pages: app.pages.length,
2438
2438
  admin: app.admin?.prefix || null,