aiplang 2.10.8 → 2.11.0
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 +131 -90
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +246 -101
- package/server/server.js +112 -10
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.
|
|
8
|
+
const VERSION = '2.11.0'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -24,10 +24,6 @@ const ic = n => ICONS[n] || n
|
|
|
24
24
|
const isDyn = s => s&&(s.includes('@')||s.includes('$'))
|
|
25
25
|
const hSize = n => n<1024?`${n}B`:`${(n/1024).toFixed(1)}KB`
|
|
26
26
|
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────
|
|
28
|
-
// CLI COMMANDS
|
|
29
|
-
// ─────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
27
|
if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
32
28
|
console.log(`
|
|
33
29
|
aiplang v${VERSION}
|
|
@@ -40,6 +36,8 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
40
36
|
npx aiplang init [name] --template my-custom use a saved custom template
|
|
41
37
|
npx aiplang serve [dir] dev server + hot reload
|
|
42
38
|
npx aiplang build [dir/file] compile → static HTML
|
|
39
|
+
npx aiplang validate <app.aip> validate syntax with AI-friendly errors
|
|
40
|
+
npx aiplang context [app.aip] dump minimal AI context (<500 tokens)
|
|
43
41
|
npx aiplang new <page> new page template
|
|
44
42
|
npx aiplang --version
|
|
45
43
|
|
|
@@ -74,18 +72,12 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
74
72
|
}
|
|
75
73
|
if (cmd==='--version'||cmd==='-v') { console.log(`aiplang v${VERSION}`); process.exit(0) }
|
|
76
74
|
|
|
77
|
-
// ─────────────────────────────────────────────────────────────────
|
|
78
|
-
// TEMPLATE SYSTEM
|
|
79
|
-
// Custom templates stored at ~/.aip/templates/<name>.aip
|
|
80
|
-
// ─────────────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
75
|
const TEMPLATES_DIR = path.join(require('os').homedir(), '.aip', 'templates')
|
|
83
76
|
|
|
84
77
|
function ensureTemplatesDir() {
|
|
85
78
|
if (!fs.existsSync(TEMPLATES_DIR)) fs.mkdirSync(TEMPLATES_DIR, { recursive: true })
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
// Built-in templates (interpolate {{name}} and {{year}})
|
|
89
81
|
const BUILTIN_TEMPLATES = {
|
|
90
82
|
saas: `# {{name}}
|
|
91
83
|
~db sqlite ./app.db
|
|
@@ -255,24 +247,20 @@ function applyTemplateVars(src, name, year) {
|
|
|
255
247
|
function getTemplate(tplName, name, year) {
|
|
256
248
|
ensureTemplatesDir()
|
|
257
249
|
|
|
258
|
-
// 1. Local file path: --template ./my-template.aip or --template /abs/path.aip
|
|
259
250
|
if (tplName.startsWith('./') || tplName.startsWith('../') || tplName.startsWith('/')) {
|
|
260
251
|
const full = path.resolve(tplName)
|
|
261
252
|
if (!fs.existsSync(full)) { console.error(`\n ✗ Template file not found: ${full}\n`); process.exit(1) }
|
|
262
253
|
return applyTemplateVars(fs.readFileSync(full, 'utf8'), name, year)
|
|
263
254
|
}
|
|
264
255
|
|
|
265
|
-
// 2. User custom template: ~/.aip/templates/<name>.aip
|
|
266
256
|
const customPath = path.join(TEMPLATES_DIR, tplName + '.aip')
|
|
267
257
|
if (fs.existsSync(customPath)) {
|
|
268
258
|
return applyTemplateVars(fs.readFileSync(customPath, 'utf8'), name, year)
|
|
269
259
|
}
|
|
270
260
|
|
|
271
|
-
// 3. Built-in template
|
|
272
261
|
const builtin = BUILTIN_TEMPLATES[tplName]
|
|
273
262
|
if (builtin) return applyTemplateVars(builtin, name, year)
|
|
274
263
|
|
|
275
|
-
// Not found — show what's available
|
|
276
264
|
const customs = fs.existsSync(TEMPLATES_DIR)
|
|
277
265
|
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
278
266
|
: []
|
|
@@ -297,17 +285,14 @@ function listTemplates() {
|
|
|
297
285
|
console.log()
|
|
298
286
|
}
|
|
299
287
|
|
|
300
|
-
// ── template subcommand ──────────────────────────────────────────
|
|
301
288
|
if (cmd === 'template') {
|
|
302
289
|
const sub = args[0]
|
|
303
290
|
ensureTemplatesDir()
|
|
304
291
|
|
|
305
|
-
// aiplang template list
|
|
306
292
|
if (!sub || sub === 'list' || sub === 'ls') {
|
|
307
293
|
listTemplates(); process.exit(0)
|
|
308
294
|
}
|
|
309
295
|
|
|
310
|
-
// aiplang template save <name> [--from <file>]
|
|
311
296
|
if (sub === 'save' || sub === 'add') {
|
|
312
297
|
const tname = args[1]
|
|
313
298
|
if (!tname) { console.error('\n ✗ Usage: aiplang template save <name> [--from <file>]\n'); process.exit(1) }
|
|
@@ -318,7 +303,7 @@ if (cmd === 'template') {
|
|
|
318
303
|
if (!fs.existsSync(fp)) { console.error(`\n ✗ File not found: ${fp}\n`); process.exit(1) }
|
|
319
304
|
src = fs.readFileSync(fp, 'utf8')
|
|
320
305
|
} else {
|
|
321
|
-
|
|
306
|
+
|
|
322
307
|
const sources = ['pages', 'app.aip', 'index.aip']
|
|
323
308
|
const found = sources.find(s => fs.existsSync(s))
|
|
324
309
|
if (!found) { console.error('\n ✗ No .aip files found. Use --from <file> to specify source.\n'); process.exit(1) }
|
|
@@ -335,7 +320,6 @@ if (cmd === 'template') {
|
|
|
335
320
|
process.exit(0)
|
|
336
321
|
}
|
|
337
322
|
|
|
338
|
-
// aiplang template remove <name>
|
|
339
323
|
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
340
324
|
const tname = args[1]
|
|
341
325
|
if (!tname) { console.error('\n ✗ Usage: aiplang template remove <name>\n'); process.exit(1) }
|
|
@@ -345,13 +329,12 @@ if (cmd === 'template') {
|
|
|
345
329
|
console.log(`\n ✓ Removed template: ${tname}\n`); process.exit(0)
|
|
346
330
|
}
|
|
347
331
|
|
|
348
|
-
// aiplang template edit <name>
|
|
349
332
|
if (sub === 'edit' || sub === 'open') {
|
|
350
333
|
const tname = args[1]
|
|
351
334
|
if (!tname) { console.error('\n ✗ Usage: aiplang template edit <name>\n'); process.exit(1) }
|
|
352
335
|
let dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
353
336
|
if (!fs.existsSync(dest)) {
|
|
354
|
-
|
|
337
|
+
|
|
355
338
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
356
339
|
if (builtin) { fs.writeFileSync(dest, builtin); console.log(`\n ✓ Copied built-in "${tname}" to custom templates.\n`) }
|
|
357
340
|
else { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
@@ -362,7 +345,6 @@ if (cmd === 'template') {
|
|
|
362
345
|
process.exit(0)
|
|
363
346
|
}
|
|
364
347
|
|
|
365
|
-
// aiplang template show <name>
|
|
366
348
|
if (sub === 'show' || sub === 'cat') {
|
|
367
349
|
const tname = args[1] || 'default'
|
|
368
350
|
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
@@ -372,7 +354,6 @@ if (cmd === 'template') {
|
|
|
372
354
|
console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1)
|
|
373
355
|
}
|
|
374
356
|
|
|
375
|
-
// aiplang template export <name> [--out <file>]
|
|
376
357
|
if (sub === 'export') {
|
|
377
358
|
const tname = args[1]
|
|
378
359
|
if (!tname) { console.error('\n ✗ Usage: aiplang template export <name>\n'); process.exit(1) }
|
|
@@ -390,7 +371,6 @@ if (cmd === 'template') {
|
|
|
390
371
|
process.exit(1)
|
|
391
372
|
}
|
|
392
373
|
|
|
393
|
-
// ── Init ─────────────────────────────────────────────────────────
|
|
394
374
|
if (cmd==='init') {
|
|
395
375
|
const tplIdx = args.indexOf('--template')
|
|
396
376
|
const tplName = tplIdx !== -1 ? args[tplIdx+1] : 'default'
|
|
@@ -399,15 +379,13 @@ if (cmd==='init') {
|
|
|
399
379
|
|
|
400
380
|
if (fs.existsSync(dir)) { console.error(`\n ✗ Directory "${name}" already exists.\n`); process.exit(1) }
|
|
401
381
|
|
|
402
|
-
// Get template source (built-in, custom, or file path)
|
|
403
382
|
const tplSrc = getTemplate(tplName, name, year)
|
|
404
383
|
|
|
405
|
-
// Check if template has full-stack backend (models/api blocks)
|
|
406
384
|
const isFullStack = tplSrc.includes('\nmodel ') || tplSrc.includes('\napi ')
|
|
407
385
|
const isMultiFile = tplSrc.includes('\n---\n')
|
|
408
386
|
|
|
409
387
|
if (isFullStack) {
|
|
410
|
-
|
|
388
|
+
|
|
411
389
|
fs.mkdirSync(dir, { recursive: true })
|
|
412
390
|
fs.writeFileSync(path.join(dir, 'app.aip'), tplSrc)
|
|
413
391
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
|
@@ -421,7 +399,7 @@ if (cmd==='init') {
|
|
|
421
399
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
422
400
|
console.log(`\n ✓ Created ${name}/${label}\n\n app.aip ← full-stack app (backend + frontend)\n\n Next:\n cd ${name} && npx aiplang start app.aip\n`)
|
|
423
401
|
} else if (isMultiFile) {
|
|
424
|
-
|
|
402
|
+
|
|
425
403
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
426
404
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
427
405
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -439,7 +417,7 @@ if (cmd==='init') {
|
|
|
439
417
|
const files = fs.readdirSync(path.join(dir,'pages')).map(f=>f).join(', ')
|
|
440
418
|
console.log(`\n ✓ Created ${name}/${label}\n\n pages/{${files}} ← edit these\n\n Next:\n cd ${name} && npx aiplang serve\n`)
|
|
441
419
|
} else {
|
|
442
|
-
|
|
420
|
+
|
|
443
421
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
444
422
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
445
423
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -454,7 +432,6 @@ if (cmd==='init') {
|
|
|
454
432
|
process.exit(0)
|
|
455
433
|
}
|
|
456
434
|
|
|
457
|
-
// ── New ───────────────────────────────────────────────────────────
|
|
458
435
|
if (cmd==='new') {
|
|
459
436
|
const name=args[0]; if(!name){console.error('\n ✗ Usage: aiplang new <page>\n');process.exit(1)}
|
|
460
437
|
const dir=fs.existsSync('pages')?'pages':'.'
|
|
@@ -466,7 +443,104 @@ if (cmd==='new') {
|
|
|
466
443
|
process.exit(0)
|
|
467
444
|
}
|
|
468
445
|
|
|
469
|
-
|
|
446
|
+
function validateAipSrc(source) {
|
|
447
|
+
const errors = []
|
|
448
|
+
const lines = source.split('\n')
|
|
449
|
+
const knownDirs = new Set(['db','auth','env','mail','s3','stripe','plan','admin','realtime','use','plugin','import','store','ssr','interval','mount','theme','guard','validate','unique','hash','check','cache','rateLimit','broadcast','soft-delete','belongs'])
|
|
450
|
+
for (let i=0; i<lines.length; i++) {
|
|
451
|
+
const line = lines[i].trim()
|
|
452
|
+
if (!line || line.startsWith('#')) continue
|
|
453
|
+
const dm = line.match(/^(guard|validate|unique|hash|check|cache|mount|store|ssr|interval|auth|db|env|use|plugin|import|theme|rateLimit|broadcast)\b/)
|
|
454
|
+
if (dm && !line.startsWith('~') && !line.startsWith('api ') && !line.startsWith('model ') && !line.startsWith('%')) {
|
|
455
|
+
errors.push({ line:i+1, code:line, message:`Missing ~ before '${dm[1]}'`, fix:`~${line}`, severity:'error' })
|
|
456
|
+
}
|
|
457
|
+
if (line.startsWith('api ') && !line.includes('{')) {
|
|
458
|
+
errors.push({ line:i+1, code:line, message:"api block missing '{'", fix:line+' { return {} }', severity:'error' })
|
|
459
|
+
}
|
|
460
|
+
if (/^[a-z_]+\s+-\s+[a-z]/.test(line) && !line.startsWith('api') && !line.startsWith('model')) {
|
|
461
|
+
errors.push({ line:i+1, code:line, message:"Use ':' not '-' in field definitions", fix:line.replace(/\s*-\s*/g,' : '), severity:'error' })
|
|
462
|
+
}
|
|
463
|
+
if (line.startsWith('~')) {
|
|
464
|
+
const dir = line.slice(1).split(/\s/)[0]
|
|
465
|
+
if (!knownDirs.has(dir)) errors.push({ line:i+1, code:line, message:`Unknown directive ~${dir}`, severity:'warning' })
|
|
466
|
+
}
|
|
467
|
+
if (/^table\s*\{/.test(line)) {
|
|
468
|
+
errors.push({ line:i+1, code:line, message:"table missing @binding — e.g.: table @users { Name:name | ... }", severity:'error' })
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return errors
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (cmd==='validate'||cmd==='check'||cmd==='lint') {
|
|
475
|
+
const file = args[0]
|
|
476
|
+
if (!file) { console.error('\n Usage: aiplang validate <app.aip>\n'); process.exit(1) }
|
|
477
|
+
if (!require('fs').existsSync(file)) { console.error(`\n ✗ File not found: ${file}\n`); process.exit(1) }
|
|
478
|
+
const src = require('fs').readFileSync(file,'utf8')
|
|
479
|
+
const errs = validateAipSrc(src)
|
|
480
|
+
if (!errs.length) { console.log('\n ✓ Syntax OK — safe to run\n'); process.exit(0) }
|
|
481
|
+
console.log(`\n ✗ ${errs.length} issue(s) found in ${file}:\n`)
|
|
482
|
+
errs.forEach(e => {
|
|
483
|
+
const icon = e.severity==='error' ? '✗' : '⚠'
|
|
484
|
+
console.log(` ${icon} Line ${e.line}: ${e.message}`)
|
|
485
|
+
console.log(` ${e.code}`)
|
|
486
|
+
if (e.fix) console.log(` Fix: ${e.fix}`)
|
|
487
|
+
})
|
|
488
|
+
console.log()
|
|
489
|
+
process.exit(errs.some(e=>e.severity==='error') ? 1 : 0)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (cmd==='context'||cmd==='ctx') {
|
|
493
|
+
const file = args[0] || 'app.aip'
|
|
494
|
+
const exists = require('fs').existsSync
|
|
495
|
+
const src = exists(file) ? require('fs').readFileSync(file,'utf8') : null
|
|
496
|
+
if (!src) { console.log('\n Usage: aiplang context [app.aip]\n Dumps minimal AI context (~200 tokens).\n'); process.exit(0) }
|
|
497
|
+
// Use server's parseApp for full app structure
|
|
498
|
+
const serverPath = require('path').join(__dirname,'../server/server.js')
|
|
499
|
+
let app = { models:[], apis:[], pages:[], db:null, auth:null }
|
|
500
|
+
try {
|
|
501
|
+
const srv = require(serverPath)
|
|
502
|
+
if (srv.parseApp) app = srv.parseApp(src)
|
|
503
|
+
} catch {
|
|
504
|
+
// Fallback: basic parse for models + routes
|
|
505
|
+
const modelRx = /^model\s+(\w+)/gm
|
|
506
|
+
const apiRx = /^api\s+(\w+)\s+(\S+)/gm
|
|
507
|
+
let m
|
|
508
|
+
while((m=modelRx.exec(src))) app.models.push({name:m[1],fields:[]})
|
|
509
|
+
while((m=apiRx.exec(src))) app.apis.push({method:m[1],path:m[2],guards:[]})
|
|
510
|
+
const pageRx = /^%(\w+)\s+(\w+)\s+(\S+)/gm
|
|
511
|
+
while((m=pageRx.exec(src))) app.pages.push({id:m[1],theme:m[2],route:m[3],state:{},queries:[]})
|
|
512
|
+
}
|
|
513
|
+
const out = [
|
|
514
|
+
`# aiplang app — ${file}`,
|
|
515
|
+
'# paste into AI for maintenance/customization',
|
|
516
|
+
'',
|
|
517
|
+
'## MODELS'
|
|
518
|
+
]
|
|
519
|
+
for (const m of app.models||[]) {
|
|
520
|
+
const fields = m.fields.map(f=>`${f.name}:${f.type}${f.modifiers?.length?':'+f.modifiers.join(':'):''}`).join(' ')
|
|
521
|
+
out.push(`model ${m.name} { ${fields} }`)
|
|
522
|
+
}
|
|
523
|
+
out.push('')
|
|
524
|
+
out.push('## ROUTES')
|
|
525
|
+
for (const r of app.apis||[]) {
|
|
526
|
+
const g = r.guards?.length ? ` [${r.guards.join(',')}]` : ''
|
|
527
|
+
const v = r.validate?.length ? ` validate:${r.validate.length}` : ''
|
|
528
|
+
out.push(`${r.method.padEnd(7)}${r.path}${g}${v}`)
|
|
529
|
+
}
|
|
530
|
+
out.push('')
|
|
531
|
+
out.push('## PAGES')
|
|
532
|
+
for (const p of app.pages||[]) {
|
|
533
|
+
const state = Object.keys(p.state||{}).map(k=>`@${k}`).join(' ')
|
|
534
|
+
const queries = (p.queries||[]).map(q=>`${q.trigger}:${q.path}`).join(' ')
|
|
535
|
+
out.push(`%${p.id} ${p.theme||'dark'} ${p.route} | state:${state||'none'} | queries:${queries||'none'}`)
|
|
536
|
+
}
|
|
537
|
+
if (app.db) { out.push(''); out.push(`## CONFIG\ndb:${app.db.driver} auth:${app.auth?'jwt':'none'}`) }
|
|
538
|
+
const ctx = out.join('\n')
|
|
539
|
+
console.log(ctx)
|
|
540
|
+
console.log(`\n# ~${Math.ceil(ctx.length/4)} tokens`)
|
|
541
|
+
process.exit(0)
|
|
542
|
+
}
|
|
543
|
+
|
|
470
544
|
if (cmd==='build') {
|
|
471
545
|
const outIdx=args.indexOf('--out')
|
|
472
546
|
const outDir=outIdx!==-1?args[outIdx+1]:'dist'
|
|
@@ -476,7 +550,7 @@ if (cmd==='build') {
|
|
|
476
550
|
fs.readdirSync(input).filter(f=>f.endsWith('.aip')).forEach(f=>files.push(path.join(input,f)))
|
|
477
551
|
} else if(input.endsWith('.aip')&&fs.existsSync(input)){ files.push(input) }
|
|
478
552
|
if(!files.length){console.error(`\n ✗ No .aip files in: ${input}\n`);process.exit(1)}
|
|
479
|
-
|
|
553
|
+
|
|
480
554
|
function resolveImports(content, baseDir, seen=new Set()) {
|
|
481
555
|
return content.replace(/^~import\s+["']?([^"'\n]+)["']?$/mg, (_, importPath) => {
|
|
482
556
|
const resolved = path.resolve(baseDir, importPath.trim())
|
|
@@ -511,7 +585,6 @@ if (cmd==='build') {
|
|
|
511
585
|
process.exit(0)
|
|
512
586
|
}
|
|
513
587
|
|
|
514
|
-
// ── Serve (hot reload) ────────────────────────────────────────────
|
|
515
588
|
if (cmd==='serve'||cmd==='dev') {
|
|
516
589
|
const root=path.resolve(args[0]||'.')
|
|
517
590
|
const port=parseInt(process.env.PORT||'3000')
|
|
@@ -549,7 +622,6 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
549
622
|
return
|
|
550
623
|
}
|
|
551
624
|
|
|
552
|
-
// ── Dev server (full-stack) ──────────────────────────────────────
|
|
553
625
|
if (cmd === 'start' || cmd === 'run') {
|
|
554
626
|
const aipFile = args[0]
|
|
555
627
|
if (!aipFile || !fs.existsSync(aipFile)) {
|
|
@@ -573,10 +645,6 @@ if (cmd === 'start' || cmd === 'run') {
|
|
|
573
645
|
console.error(`\n ✗ Unknown command: ${cmd}\n Run aiplang --help\n`)
|
|
574
646
|
process.exit(1)
|
|
575
647
|
|
|
576
|
-
// ═════════════════════════════════════════════════════════════════
|
|
577
|
-
// PARSER
|
|
578
|
-
// ═════════════════════════════════════════════════════════════════
|
|
579
|
-
|
|
580
648
|
function parsePages(src) {
|
|
581
649
|
return src.split(/\n---\n/).map(s=>parsePage(s.trim())).filter(Boolean)
|
|
582
650
|
}
|
|
@@ -619,8 +687,7 @@ function parseQuery(s) {
|
|
|
619
687
|
}
|
|
620
688
|
|
|
621
689
|
function parseBlock(line) {
|
|
622
|
-
|
|
623
|
-
// animate:fade-up class:my-class (can appear at end of any block line)
|
|
690
|
+
|
|
624
691
|
let extraClass=null, animate=null
|
|
625
692
|
const _cm=line.match(/\bclass:(\S+)/)
|
|
626
693
|
if(_cm){extraClass=_cm[1];line=line.replace(_cm[0],'').trim()}
|
|
@@ -636,12 +703,10 @@ function parseBlock(line) {
|
|
|
636
703
|
const _bgm=line.match(/\bbg:(#[0-9a-fA-F]+|[a-z]+)/)
|
|
637
704
|
if(_bgm){bg=_bgm[1];line=line.replace(_bgm[0],'').trim()}
|
|
638
705
|
|
|
639
|
-
// ── raw{} HTML passthrough ──────────────────────────────────
|
|
640
706
|
if(line.startsWith('raw{')) {
|
|
641
707
|
return{kind:'raw',html:line.slice(4,line.lastIndexOf('}')),extraClass,animate}
|
|
642
708
|
}
|
|
643
709
|
|
|
644
|
-
// ── table ───────────────────────────────────────────────────
|
|
645
710
|
if(line.startsWith('table ') || line.startsWith('table{')) {
|
|
646
711
|
const idx=line.indexOf('{');if(idx===-1) return null
|
|
647
712
|
const start=line.startsWith('table{')?6:6
|
|
@@ -659,7 +724,6 @@ function parseBlock(line) {
|
|
|
659
724
|
return{kind:'table',binding,cols:Array.isArray(cols)?cols:[],empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',fallback:fallbackM?.[1]?.trim()||null,retry:retryM?.[1]||null,extraClass,animate,variant,style,bg}
|
|
660
725
|
}
|
|
661
726
|
|
|
662
|
-
// ── form ────────────────────────────────────────────────────
|
|
663
727
|
if(line.startsWith('form ') || line.startsWith('form{')) {
|
|
664
728
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
665
729
|
let head=line.slice(line.startsWith('form{')?4:5,bi).trim()
|
|
@@ -667,7 +731,7 @@ function parseBlock(line) {
|
|
|
667
731
|
let action='', optimistic=false; const ai=head.indexOf('=>')
|
|
668
732
|
if(ai!==-1){
|
|
669
733
|
action=head.slice(ai+2).trim()
|
|
670
|
-
|
|
734
|
+
|
|
671
735
|
if(action.includes('.optimistic(')){optimistic=true;action=action.replace('.optimistic','')}
|
|
672
736
|
head=head.slice(0,ai).trim()
|
|
673
737
|
}
|
|
@@ -677,7 +741,6 @@ function parseBlock(line) {
|
|
|
677
741
|
return{kind:'form',method,bpath,action,optimistic,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
|
|
678
742
|
}
|
|
679
743
|
|
|
680
|
-
// ── pricing ─────────────────────────────────────────────────
|
|
681
744
|
if(line.startsWith('pricing{')) {
|
|
682
745
|
const body=line.slice(8,line.lastIndexOf('}')).trim()
|
|
683
746
|
const plans=body.split('|').map(p=>{
|
|
@@ -687,14 +750,12 @@ function parseBlock(line) {
|
|
|
687
750
|
return{kind:'pricing',plans,extraClass,animate,variant,style,bg}
|
|
688
751
|
}
|
|
689
752
|
|
|
690
|
-
// ── faq ─────────────────────────────────────────────────────
|
|
691
753
|
if(line.startsWith('faq{')) {
|
|
692
754
|
const body=line.slice(4,line.lastIndexOf('}')).trim()
|
|
693
755
|
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)
|
|
694
756
|
return{kind:'faq',items,extraClass,animate}
|
|
695
757
|
}
|
|
696
758
|
|
|
697
|
-
// ── testimonial ──────────────────────────────────────────────
|
|
698
759
|
if(line.startsWith('testimonial{')) {
|
|
699
760
|
const body=line.slice(12,line.lastIndexOf('}')).trim()
|
|
700
761
|
const parts=body.split('|').map(x=>x.trim())
|
|
@@ -702,12 +763,10 @@ function parseBlock(line) {
|
|
|
702
763
|
return{kind:'testimonial',author:parts[0],quote:parts[1]?.replace(/^"|"$/g,''),img:imgPart?.slice(4)||null,extraClass,animate}
|
|
703
764
|
}
|
|
704
765
|
|
|
705
|
-
// ── gallery ──────────────────────────────────────────────────
|
|
706
766
|
if(line.startsWith('gallery{')) {
|
|
707
767
|
return{kind:'gallery',imgs:line.slice(8,line.lastIndexOf('}')).trim().split('|').map(x=>x.trim()).filter(Boolean),extraClass,animate}
|
|
708
768
|
}
|
|
709
769
|
|
|
710
|
-
// ── btn ──────────────────────────────────────────────────────
|
|
711
770
|
if(line.startsWith('btn{')) {
|
|
712
771
|
const parts=line.slice(4,line.lastIndexOf('}')).split('>').map(p=>p.trim())
|
|
713
772
|
const label=parts[0]||'Click', method=parts[1]?.split(' ')[0]||'POST'
|
|
@@ -717,7 +776,6 @@ function parseBlock(line) {
|
|
|
717
776
|
return{kind:'btn',label,method,bpath,action,confirm,extraClass,animate}
|
|
718
777
|
}
|
|
719
778
|
|
|
720
|
-
// ── card{} — standalone card customizável ──────────────────
|
|
721
779
|
if(line.startsWith('card{') || line.startsWith('card ')) {
|
|
722
780
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
723
781
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -731,7 +789,6 @@ function parseBlock(line) {
|
|
|
731
789
|
return{kind:'card',title,subtitle,img:imgPart?.slice(4)||null,link:linkPart||null,badge,bind,extraClass,animate,variant,style,bg}
|
|
732
790
|
}
|
|
733
791
|
|
|
734
|
-
// ── cols{} — grid de conteúdo livre ─────────────────────────
|
|
735
792
|
if(line.startsWith('cols{') || (line.startsWith('cols ') && line.includes('{'))) {
|
|
736
793
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
737
794
|
const head=line.slice(0,bi).trim()
|
|
@@ -742,19 +799,16 @@ function parseBlock(line) {
|
|
|
742
799
|
return{kind:'cols',n,items,extraClass,animate,variant,style,bg}
|
|
743
800
|
}
|
|
744
801
|
|
|
745
|
-
// ── divider{} — separador visual ─────────────────────────────
|
|
746
802
|
if(line.startsWith('divider') || line.startsWith('hr{')) {
|
|
747
803
|
const label=line.match(/\{([^}]*)\}/)?.[1]?.trim()||null
|
|
748
804
|
return{kind:'divider',label,extraClass,animate,variant,style}
|
|
749
805
|
}
|
|
750
806
|
|
|
751
|
-
// ── badge{} — label/tag destacado ───────────────────────────
|
|
752
807
|
if(line.startsWith('badge{') || line.startsWith('tag{')) {
|
|
753
808
|
const content=line.slice(line.indexOf('{')+1,line.lastIndexOf('}')).trim()
|
|
754
809
|
return{kind:'badge',content,extraClass,animate,variant,style}
|
|
755
810
|
}
|
|
756
811
|
|
|
757
|
-
// ── select ───────────────────────────────────────────────────
|
|
758
812
|
if(line.startsWith('select ')) {
|
|
759
813
|
const bi=line.indexOf('{')
|
|
760
814
|
const varName=bi!==-1?line.slice(7,bi).trim():line.slice(7).trim()
|
|
@@ -762,13 +816,11 @@ function parseBlock(line) {
|
|
|
762
816
|
return{kind:'select',binding:varName,options:body.split('|').map(o=>o.trim()).filter(Boolean),extraClass,animate}
|
|
763
817
|
}
|
|
764
818
|
|
|
765
|
-
// ── if ───────────────────────────────────────────────────────
|
|
766
819
|
if(line.startsWith('if ')) {
|
|
767
820
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
768
821
|
return{kind:'if',cond:line.slice(3,bi).trim(),inner:line.slice(bi+1,line.lastIndexOf('}')).trim(),extraClass,animate}
|
|
769
822
|
}
|
|
770
823
|
|
|
771
|
-
// ── chart{} — data visualization ───────────────────────────────
|
|
772
824
|
if(line.startsWith('chart{') || line.startsWith('chart ')) {
|
|
773
825
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
774
826
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -781,7 +833,6 @@ function parseBlock(line) {
|
|
|
781
833
|
return{kind:'chart',type,binding,labels,values,title,extraClass,animate,variant,style}
|
|
782
834
|
}
|
|
783
835
|
|
|
784
|
-
// ── kanban{} — drag-and-drop board ───────────────────────────────
|
|
785
836
|
if(line.startsWith('kanban{') || line.startsWith('kanban ')) {
|
|
786
837
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
787
838
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -793,7 +844,6 @@ function parseBlock(line) {
|
|
|
793
844
|
return{kind:'kanban',binding,cols,statusField,updatePath,extraClass,animate,style}
|
|
794
845
|
}
|
|
795
846
|
|
|
796
|
-
// ── editor{} — rich text editor ──────────────────────────────────
|
|
797
847
|
if(line.startsWith('editor{') || line.startsWith('editor ')) {
|
|
798
848
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
799
849
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -804,7 +854,6 @@ function parseBlock(line) {
|
|
|
804
854
|
return{kind:'editor',name,placeholder,submitPath,extraClass,animate,style}
|
|
805
855
|
}
|
|
806
856
|
|
|
807
|
-
// ── each @list { template } — loop como React .map() ────────
|
|
808
857
|
if(line.startsWith('each ')) {
|
|
809
858
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
810
859
|
const binding=line.slice(5,bi).trim()
|
|
@@ -812,18 +861,15 @@ function parseBlock(line) {
|
|
|
812
861
|
return{kind:'each',binding,tpl,extraClass,animate,variant}
|
|
813
862
|
}
|
|
814
863
|
|
|
815
|
-
// ── spacer{} — espaçamento customizável ──────────────────────
|
|
816
864
|
if(line.startsWith('spacer{') || line.startsWith('spacer ')) {
|
|
817
865
|
const h=line.match(/[{\s](\S+)[}]?/)?.[1]||'3rem'
|
|
818
866
|
return{kind:'spacer',height:h,extraClass,animate}
|
|
819
867
|
}
|
|
820
868
|
|
|
821
|
-
// ── html{} — HTML inline com interpolação de @state ──────────
|
|
822
869
|
if(line.startsWith('html{')) {
|
|
823
870
|
return{kind:'html',content:line.slice(5,line.lastIndexOf('}')),extraClass,animate}
|
|
824
871
|
}
|
|
825
872
|
|
|
826
|
-
// ── regular blocks (nav, hero, stats, rowN, sect, foot) ──────
|
|
827
873
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
828
874
|
const head=line.slice(0,bi).trim()
|
|
829
875
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -846,41 +892,43 @@ function parseCols(s){return s.split('|').map(c=>{c=c.trim();if(c.startsWith('em
|
|
|
846
892
|
function parseEmpty(s){const m=s.match(/empty:\s*([^|]+)/);return m?m[1].trim():'No data.'}
|
|
847
893
|
function parseFields(s){return s.split('|').map(f=>{const[label,type,ph]=f.split(':').map(x=>x.trim());return label?{label,type:type||'text',placeholder:ph||'',name:label.toLowerCase().replace(/\s+/g,'_')}:null}).filter(Boolean)}
|
|
848
894
|
|
|
849
|
-
// ═════════════════════════════════════════════════════════════════
|
|
850
|
-
// RENDERER
|
|
851
|
-
// ═════════════════════════════════════════════════════════════════
|
|
852
|
-
|
|
853
895
|
function applyMods(html, b) {
|
|
854
896
|
if(!html||(!b.extraClass&&!b.animate)) return html
|
|
855
897
|
const cls=[(b.extraClass||''),(b.animate?'fx-anim-'+b.animate:'')].filter(Boolean).join(' ')
|
|
856
|
-
|
|
898
|
+
|
|
857
899
|
return html.replace(/class="([^"]*)"/, (_,c)=>`class="${c} ${cls}"`)
|
|
858
900
|
}
|
|
859
901
|
|
|
860
902
|
function renderPage(page, allPages) {
|
|
861
903
|
const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
|
|
862
904
|
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('')
|
|
863
|
-
|
|
905
|
+
|
|
864
906
|
const tableBlocks = page.blocks.filter(b => b.kind === 'table' && b.binding && b.cols && b.cols.length)
|
|
865
907
|
const numericKeys = ['score','count','total','amount','price','value','qty','age','rank','num','int','float','rate','pct','percent']
|
|
866
908
|
const compiledDiffs = tableBlocks.map(b => {
|
|
867
909
|
const binding = b.binding.replace(/^@/, '')
|
|
910
|
+
|
|
911
|
+
const safeId = s => (s||'').replace(/[^a-zA-Z0-9_]/g, '_').slice(0,64) || 'col'
|
|
912
|
+
const safeBinding = safeId(binding)
|
|
868
913
|
const colDefs = b.cols.map((col, j) => ({
|
|
869
|
-
key: col.key,
|
|
914
|
+
key: safeId(col.key),
|
|
915
|
+
origKey: col.key,
|
|
870
916
|
idx: j,
|
|
871
917
|
numeric: numericKeys.some(kw => col.key.toLowerCase().includes(kw))
|
|
872
918
|
}))
|
|
873
919
|
const initParts = colDefs.map(d =>
|
|
874
|
-
d.numeric ? `c${d.idx}:new Float64Array(rows.map(r=>+(r.${d.
|
|
875
|
-
: `c${d.idx}:rows.map(r=>r
|
|
920
|
+
d.numeric ? `c${d.idx}:new Float64Array(rows.map(r=>+(r.${JSON.stringify(d.origKey)}===undefined?r['${d.origKey}']:r[${JSON.stringify(d.origKey)}])||0))`
|
|
921
|
+
: `c${d.idx}:rows.map(r=>r[${JSON.stringify(d.origKey)}]??'')`
|
|
876
922
|
).join(',')
|
|
877
|
-
const diffParts = colDefs.map(d =>
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
923
|
+
const diffParts = colDefs.map(d => {
|
|
924
|
+
const k = JSON.stringify(d.origKey)
|
|
925
|
+
return d.numeric
|
|
926
|
+
? `if(c${d.idx}[i]!==(r[${k}]||0)){c${d.idx}[i]=r[${k}]||0;p.push(i<<4|${d.idx})}`
|
|
927
|
+
: `if(c${d.idx}[i]!==r[${k}]){c${d.idx}[i]=r[${k}];p.push(i<<4|${d.idx})}`
|
|
928
|
+
}).join(';')
|
|
881
929
|
return [
|
|
882
|
-
`window.__aip_init_${
|
|
883
|
-
`window.__aip_diff_${
|
|
930
|
+
`window.__aip_init_${safeBinding}=function(rows){return{${initParts}}};`,
|
|
931
|
+
`window.__aip_diff_${safeBinding}=function(rows,cache){`,
|
|
884
932
|
`const n=rows.length,p=[],${colDefs.map(d=>`c${d.idx}=cache.c${d.idx}`).join(',')};`,
|
|
885
933
|
`for(let i=0;i<n;i++){const r=rows[i];${diffParts}}return p};`
|
|
886
934
|
].join('')
|
|
@@ -888,11 +936,11 @@ function renderPage(page, allPages) {
|
|
|
888
936
|
const compiledScript = compiledDiffs.length
|
|
889
937
|
? `<script>/* aiplang compiled-diffs */\n${compiledDiffs}\n</script>`
|
|
890
938
|
: ''
|
|
891
|
-
const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries,stores:page.stores||[],computed:page.computed||{},compiledTables:tableBlocks.map(b=>b.binding.replace(/^@/,''))}):''
|
|
939
|
+
const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries,stores:page.stores||[],computed:page.computed||{},compiledTables:tableBlocks.map(b=>(b.binding||'').replace(/^@/,'').replace(/[^a-zA-Z0-9_]/g,'_').slice(0,64))}):''
|
|
892
940
|
const hydrate=needsJS?`\n<script>window.__AIPLANG_PAGE__=${config};</script>\n<script src="./aiplang-hydrate.js" defer></script>`:''
|
|
893
941
|
const customVars=page.customTheme?genCustomThemeVars(page.customTheme):''
|
|
894
942
|
const themeVarCSS=page.themeVars?genThemeVarCSS(page.themeVars):''
|
|
895
|
-
|
|
943
|
+
|
|
896
944
|
const _navBlock = page.blocks.find(b=>b.kind==='nav')
|
|
897
945
|
const _brand = _navBlock?.brand || ''
|
|
898
946
|
const _title = _brand ? `${esc(_brand)} — ${esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))}` : esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))
|
|
@@ -948,7 +996,6 @@ function renderBlock(b, page) {
|
|
|
948
996
|
}
|
|
949
997
|
}
|
|
950
998
|
|
|
951
|
-
// ── Chart — lazy-loads Chart.js from CDN ────────────────────────
|
|
952
999
|
function rChart(b) {
|
|
953
1000
|
const id = 'chart_' + Math.random().toString(36).slice(2,8)
|
|
954
1001
|
const binding = b.binding || ''
|
|
@@ -959,7 +1006,6 @@ function rChart(b) {
|
|
|
959
1006
|
</div>\n`
|
|
960
1007
|
}
|
|
961
1008
|
|
|
962
|
-
// ── Kanban — drag-and-drop board ─────────────────────────────────
|
|
963
1009
|
function rKanban(b) {
|
|
964
1010
|
const cols = (b.cols||['Todo','In Progress','Done'])
|
|
965
1011
|
const colsHtml = cols.map(col => `
|
|
@@ -971,7 +1017,6 @@ function rKanban(b) {
|
|
|
971
1017
|
return `<div class="fx-kanban" data-fx-kanban="${esc(b.binding||'')}" data-status-field="${esc(b.statusField||'status')}" data-update-path="${esc(b.updatePath||'')}"${style}>${colsHtml}</div>\n`
|
|
972
1018
|
}
|
|
973
1019
|
|
|
974
|
-
// ── Rich text editor ──────────────────────────────────────────────
|
|
975
1020
|
function rEditor(b) {
|
|
976
1021
|
const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
977
1022
|
return `<div class="fx-editor-wrap"${style}>
|
|
@@ -1057,7 +1102,7 @@ function rRow(b) {
|
|
|
1057
1102
|
pink:'#ec4899',cyan:'#06b6d4',lime:'#84cc16',amber:'#f59e0b'
|
|
1058
1103
|
}
|
|
1059
1104
|
const cards=(b.items||[]).map(item=>{
|
|
1060
|
-
|
|
1105
|
+
|
|
1061
1106
|
let colorStyle='', firstIdx=0
|
|
1062
1107
|
if(item[0]&&!item[0].isImg&&!item[0].isLink){
|
|
1063
1108
|
const colorKey=item[0].text?.toLowerCase()
|
|
@@ -1115,7 +1160,8 @@ function rTable(b) {
|
|
|
1115
1160
|
const span=cols.length+((b.editPath||b.deletePath)?1:0)
|
|
1116
1161
|
const fallbackAttr=b.fallback?` data-fx-fallback="${esc(b.fallback)}"`:''
|
|
1117
1162
|
const retryAttr=b.retry?` data-fx-retry="${esc(b.retry)}"`:''
|
|
1118
|
-
|
|
1163
|
+
const exitAttr=b.animateExit?` data-fx-exit="${esc(b.animateExit)}"`:'';
|
|
1164
|
+
return `<div class="fx-table-wrap"><table class="fx-table"${exitAttr} data-fx-table="${esc(b.binding)}" data-fx-cols='${keys}' data-fx-col-map='${cm}'${ea}${da}${fallbackAttr}${retryAttr}><thead><tr>${ths}${at}</tr></thead><tbody class="fx-tbody"><tr><td colspan="${span}" class="fx-td-empty">${esc(b.empty)}</td></tr></tbody></table></div>\n`
|
|
1119
1165
|
}
|
|
1120
1166
|
|
|
1121
1167
|
function rForm(b) {
|
|
@@ -1178,7 +1224,6 @@ function rGallery(b) {
|
|
|
1178
1224
|
return `<div class="fx-gallery">${imgs}</div>\n`
|
|
1179
1225
|
}
|
|
1180
1226
|
|
|
1181
|
-
// ── Theme helpers ─────────────────────────────────────────────────
|
|
1182
1227
|
function genCustomThemeVars(ct) {
|
|
1183
1228
|
return `body{background:${ct.bg};color:${ct.text}}.fx-nav{background:${ct.bg}cc;border-bottom:1px solid ${ct.text}18}.fx-cta,.fx-btn{background:${ct.accent};color:#fff}.fx-card{background:${ct.surface||ct.bg};border:1px solid ${ct.text}15}.fx-form{background:${ct.surface||ct.bg};border:1px solid ${ct.text}15}.fx-input{background:${ct.bg};border:1px solid ${ct.text}30;color:${ct.text}}.fx-stat-lbl,.fx-card-body,.fx-sub,.fx-sect-body,.fx-footer-text{color:${ct.text}88}.fx-th,.fx-nav-link{color:${ct.text}77}.fx-footer{border-top:1px solid ${ct.text}15}.fx-th{border-bottom:1px solid ${ct.text}15}`
|
|
1184
1229
|
}
|
|
@@ -1198,10 +1243,6 @@ function genThemeVarCSS(t) {
|
|
|
1198
1243
|
return r.join('')
|
|
1199
1244
|
}
|
|
1200
1245
|
|
|
1201
|
-
// ═════════════════════════════════════════════════════════════════
|
|
1202
|
-
// CSS
|
|
1203
|
-
// ═════════════════════════════════════════════════════════════════
|
|
1204
|
-
|
|
1205
1246
|
function css(theme) {
|
|
1206
1247
|
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}
|
|
1207
1248
|
.fx-hero-minimal{min-height:50vh!important}
|