aiplang 2.10.2 → 2.10.3
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 +84 -2
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +30 -3
- package/server/server.js +13 -2
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.10.
|
|
8
|
+
const VERSION = '2.10.3'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -768,6 +768,42 @@ function parseBlock(line) {
|
|
|
768
768
|
return{kind:'if',cond:line.slice(3,bi).trim(),inner:line.slice(bi+1,line.lastIndexOf('}')).trim(),extraClass,animate}
|
|
769
769
|
}
|
|
770
770
|
|
|
771
|
+
// ── chart{} — data visualization ───────────────────────────────
|
|
772
|
+
if(line.startsWith('chart{') || line.startsWith('chart ')) {
|
|
773
|
+
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
774
|
+
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
775
|
+
const parts=body.split('|').map(x=>x.trim())
|
|
776
|
+
const type=parts.find(p=>['bar','line','pie','area','donut'].includes(p))||'bar'
|
|
777
|
+
const binding=parts.find(p=>p.startsWith('@'))||''
|
|
778
|
+
const labels=parts.find(p=>p.startsWith('x:'))?.slice(2)||'label'
|
|
779
|
+
const values=parts.find(p=>p.startsWith('y:'))?.slice(2)||'value'
|
|
780
|
+
const title=parts.find(p=>!p.startsWith('@')&&!['bar','line','pie','area','donut'].includes(p)&&!p.startsWith('x:')&&!p.startsWith('y:'))||''
|
|
781
|
+
return{kind:'chart',type,binding,labels,values,title,extraClass,animate,variant,style}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ── kanban{} — drag-and-drop board ───────────────────────────────
|
|
785
|
+
if(line.startsWith('kanban{') || line.startsWith('kanban ')) {
|
|
786
|
+
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
787
|
+
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
788
|
+
const parts=body.split('|').map(x=>x.trim())
|
|
789
|
+
const binding=parts.find(p=>p.startsWith('@'))||''
|
|
790
|
+
const cols=parts.filter(p=>!p.startsWith('@')&&!p.startsWith('status:'))
|
|
791
|
+
const statusField=parts.find(p=>p.startsWith('status:'))?.slice(7)||'status'
|
|
792
|
+
const updatePath=parts.find(p=>p.startsWith('PUT ')||p.startsWith('PATCH '))||''
|
|
793
|
+
return{kind:'kanban',binding,cols,statusField,updatePath,extraClass,animate,style}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ── editor{} — rich text editor ──────────────────────────────────
|
|
797
|
+
if(line.startsWith('editor{') || line.startsWith('editor ')) {
|
|
798
|
+
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
799
|
+
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
800
|
+
const parts=body.split('|').map(x=>x.trim())
|
|
801
|
+
const name=parts[0]||'content'
|
|
802
|
+
const placeholder=parts[1]||'Start writing...'
|
|
803
|
+
const submitPath=parts.find(p=>p.startsWith('POST ')||p.startsWith('PUT '))||''
|
|
804
|
+
return{kind:'editor',name,placeholder,submitPath,extraClass,animate,style}
|
|
805
|
+
}
|
|
806
|
+
|
|
771
807
|
// ── each @list { template } — loop como React .map() ────────
|
|
772
808
|
if(line.startsWith('each ')) {
|
|
773
809
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
@@ -824,7 +860,7 @@ function applyMods(html, b) {
|
|
|
824
860
|
function renderPage(page, allPages) {
|
|
825
861
|
const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
|
|
826
862
|
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('')
|
|
827
|
-
const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries}):''
|
|
863
|
+
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||{}}):''
|
|
828
864
|
const hydrate=needsJS?`\n<script>window.__AIPLANG_PAGE__=${config};</script>\n<script src="./aiplang-hydrate.js" defer></script>`:''
|
|
829
865
|
const customVars=page.customTheme?genCustomThemeVars(page.customTheme):''
|
|
830
866
|
const themeVarCSS=page.themeVars?genThemeVarCSS(page.themeVars):''
|
|
@@ -875,12 +911,55 @@ function renderBlock(b, page) {
|
|
|
875
911
|
case 'badge': return `<div class="fx-badge-row"><span class="fx-badge-tag">${esc(b.content||'')}</span></div>\n`
|
|
876
912
|
case 'card': return rCardBlock(b)
|
|
877
913
|
case 'cols': return rColsBlock(b)
|
|
914
|
+
case 'chart': return rChart(b)
|
|
915
|
+
case 'kanban': return rKanban(b)
|
|
916
|
+
case 'editor': return rEditor(b)
|
|
878
917
|
case 'each': return `<div class="fx-each fx-each-${b.variant||'list'}" data-fx-each="${esc(b.binding||'')}" data-fx-tpl="${esc(b.tpl||'')}"${b.style?` style="${b.style.replace(/,/g,';')}"`:''}>\n<div class="fx-each-empty fx-td-empty">Loading...</div></div>\n`
|
|
879
918
|
case 'if': return `<div class="fx-if-wrap" data-fx-if="${esc(b.cond)}" style="display:none"></div>\n`
|
|
880
919
|
default: return ''
|
|
881
920
|
}
|
|
882
921
|
}
|
|
883
922
|
|
|
923
|
+
// ── Chart — lazy-loads Chart.js from CDN ────────────────────────
|
|
924
|
+
function rChart(b) {
|
|
925
|
+
const id = 'chart_' + Math.random().toString(36).slice(2,8)
|
|
926
|
+
const binding = b.binding || ''
|
|
927
|
+
const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
928
|
+
return `<div class="fx-chart-wrap"${style}>
|
|
929
|
+
${b.title ? `<div class="fx-chart-title">${esc(b.title)}</div>` : ''}
|
|
930
|
+
<canvas id="${id}" class="fx-chart" data-fx-chart="${esc(binding)}" data-chart-type="${esc(b.type||'bar')}" data-chart-labels="${esc(b.labels||'label')}" data-chart-values="${esc(b.values||'value')}"></canvas>
|
|
931
|
+
</div>\n`
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ── Kanban — drag-and-drop board ─────────────────────────────────
|
|
935
|
+
function rKanban(b) {
|
|
936
|
+
const cols = (b.cols||['Todo','In Progress','Done'])
|
|
937
|
+
const colsHtml = cols.map(col => `
|
|
938
|
+
<div class="fx-kanban-col" data-col="${esc(col)}">
|
|
939
|
+
<div class="fx-kanban-col-title">${esc(col)}</div>
|
|
940
|
+
<div class="fx-kanban-cards" data-status="${esc(col)}"></div>
|
|
941
|
+
</div>`).join('')
|
|
942
|
+
const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
943
|
+
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`
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ── Rich text editor ──────────────────────────────────────────────
|
|
947
|
+
function rEditor(b) {
|
|
948
|
+
const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
949
|
+
return `<div class="fx-editor-wrap"${style}>
|
|
950
|
+
<div class="fx-editor-toolbar">
|
|
951
|
+
<button type="button" onclick="document.execCommand('bold')" class="fx-editor-btn" title="Bold"><b>B</b></button>
|
|
952
|
+
<button type="button" onclick="document.execCommand('italic')" class="fx-editor-btn" title="Italic"><i>I</i></button>
|
|
953
|
+
<button type="button" onclick="document.execCommand('underline')" class="fx-editor-btn" title="Underline"><u>U</u></button>
|
|
954
|
+
<button type="button" onclick="document.execCommand('insertUnorderedList')" class="fx-editor-btn" title="List">≡</button>
|
|
955
|
+
<button type="button" onclick="document.execCommand('createLink',false,prompt('URL:'))" class="fx-editor-btn" title="Link">🔗</button>
|
|
956
|
+
${b.submitPath ? `<button type="button" class="fx-editor-save fx-btn" data-editor-save="${esc(b.submitPath)}" data-editor-field="${esc(b.name||'content')}">Save</button>` : ''}
|
|
957
|
+
</div>
|
|
958
|
+
<div class="fx-editor" contenteditable="true" data-fx-editor="${esc(b.name||'content')}" placeholder="${esc(b.placeholder||'Start writing...')}"></div>
|
|
959
|
+
<input type="hidden" name="${esc(b.name||'content')}" class="fx-editor-hidden">
|
|
960
|
+
</div>\n`
|
|
961
|
+
}
|
|
962
|
+
|
|
884
963
|
function rCardBlock(b) {
|
|
885
964
|
const img=b.img?`<img src="${esc(b.img)}" class="fx-card-img" alt="${esc(b.title||'')}" loading="lazy">`:'';
|
|
886
965
|
const badge=b.badge?`<span class="fx-card-badge">${esc(b.badge)}</span>`:'';
|
|
@@ -1129,6 +1208,9 @@ function css(theme) {
|
|
|
1129
1208
|
.fx-pricing-compact{border-radius:.875rem;padding:1.25rem;display:flex;align-items:center;gap:1rem;border:1px solid rgba(255,255,255,.08)}
|
|
1130
1209
|
.fx-pricing-price-sm{font-size:1.5rem;font-weight:800;letter-spacing:-.04em}
|
|
1131
1210
|
.fx-grid-numbered>.fx-card{counter-increment:card-counter}
|
|
1211
|
+
.fx-chart-wrap{padding:1rem 2.5rem;position:relative}.fx-chart-title{font-family:monospace;font-size:.65rem;letter-spacing:.1em;text-transform:uppercase;color:#475569;margin-bottom:.75rem}.fx-chart{max-height:320px}
|
|
1212
|
+
.fx-kanban{display:flex;gap:1rem;padding:1rem 2.5rem;overflow-x:auto;align-items:flex-start}.fx-kanban-col{flex:0 0 280px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:.875rem;padding:1rem}.fx-kanban-col-title{font-family:monospace;font-size:.65rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#64748b;margin-bottom:.875rem}.fx-kanban-cards{min-height:80px;display:flex;flex-direction:column;gap:.5rem}.fx-kanban-card{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:.5rem;padding:.75rem;cursor:grab;font-size:.8125rem;line-height:1.5;transition:transform .15s,box-shadow .15s}.fx-kanban-card:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.3)}.fx-kanban-card.dragging{opacity:.5;cursor:grabbing}
|
|
1213
|
+
.fx-editor-wrap{padding:.75rem 2.5rem}.fx-editor-toolbar{display:flex;gap:.25rem;margin-bottom:.5rem;flex-wrap:wrap}.fx-editor-btn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#e2e8f0;border-radius:.375rem;padding:.25rem .625rem;cursor:pointer;font-size:.8125rem;font-family:inherit;transition:background .1s}.fx-editor-btn:hover{background:rgba(255,255,255,.12)}.fx-editor{min-height:160px;padding:1rem;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:.625rem;color:#e2e8f0;font-size:.875rem;line-height:1.7;outline:none}.fx-editor:empty::before{content:attr(placeholder);color:#475569;pointer-events:none}.fx-editor-save{margin-left:auto}
|
|
1132
1214
|
.fx-grid-numbered>.fx-card::before{content:counter(card-counter,decimal-leading-zero);font-size:2rem;font-weight:900;opacity:.15;font-family:monospace;line-height:1}
|
|
1133
1215
|
.fx-grid-bordered>.fx-card{border:1px solid rgba(255,255,255,.08)}@keyframes fx-fade-up{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}@keyframes fx-fade-in{from{opacity:0}to{opacity:1}}@keyframes fx-slide-left{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:none}}@keyframes fx-slide-right{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:none}}@keyframes fx-zoom-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fx-blur-in{from{opacity:0;filter:blur(8px)}to{opacity:1;filter:blur(0)}}.fx-anim-fade-up{animation:fx-fade-up .6s cubic-bezier(.4,0,.2,1) both}.fx-anim-fade-in{animation:fx-fade-in .6s ease both}.fx-anim-slide-left{animation:fx-slide-left .6s cubic-bezier(.4,0,.2,1) both}.fx-anim-slide-right{animation:fx-slide-right .6s cubic-bezier(.4,0,.2,1) both}.fx-anim-zoom-in{animation:fx-zoom-in .5s cubic-bezier(.4,0,.2,1) both}.fx-anim-blur-in{animation:fx-blur-in .7s ease both}.fx-anim-stagger>.fx-card:nth-child(1){animation:fx-fade-up .5s 0s both}.fx-anim-stagger>.fx-card:nth-child(2){animation:fx-fade-up .5s .1s both}.fx-anim-stagger>.fx-card:nth-child(3){animation:fx-fade-up .5s .2s both}.fx-anim-stagger>.fx-card:nth-child(4){animation:fx-fade-up .5s .3s both}.fx-anim-stagger>.fx-card:nth-child(5){animation:fx-fade-up .5s .4s both}.fx-anim-stagger>.fx-card:nth-child(6){animation:fx-fade-up .5s .5s both}`
|
|
1134
1216
|
|
package/package.json
CHANGED
|
@@ -6,21 +6,45 @@
|
|
|
6
6
|
const cfg = window.__AIPLANG_PAGE__
|
|
7
7
|
if (!cfg) return
|
|
8
8
|
|
|
9
|
+
// ── Global Store — cross-page state (like React Context / Zustand) ─
|
|
10
|
+
const _STORE_KEY = 'aiplang_store_v1'
|
|
11
|
+
const _globalStore = (() => {
|
|
12
|
+
try { return JSON.parse(sessionStorage.getItem(_STORE_KEY) || '{}') } catch { return {} }
|
|
13
|
+
})()
|
|
14
|
+
function syncStore(key, value) {
|
|
15
|
+
_globalStore[key] = value
|
|
16
|
+
try { sessionStorage.setItem(_STORE_KEY, JSON.stringify(_globalStore)) } catch {}
|
|
17
|
+
try { new BroadcastChannel(_STORE_KEY).postMessage({ key, value }) } catch {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Page-level State ─────────────────────────────────────────────
|
|
9
21
|
const _state = {}
|
|
10
22
|
const _watchers = {}
|
|
23
|
+
const _storeKeys = new Set((cfg.stores || []).map(s => s.key))
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
// Bootstrap state: SSR data > global store > page state declarations
|
|
26
|
+
const _boot = { ...(window.__SSR_DATA__ || {}), ..._globalStore }
|
|
27
|
+
for (const [k, v] of Object.entries({ ...(cfg.state || {}), ..._boot })) {
|
|
28
|
+
try { _state[k] = typeof v === 'string' && (v.startsWith('[') || v.startsWith('{') || v === 'true' || v === 'false' || !isNaN(v)) ? JSON.parse(v) : v } catch { _state[k] = v }
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
function get(key) { return _state[key] }
|
|
17
32
|
|
|
18
|
-
function set(key, value) {
|
|
33
|
+
function set(key, value, _persist) {
|
|
19
34
|
if (JSON.stringify(_state[key]) === JSON.stringify(value)) return
|
|
20
35
|
_state[key] = value
|
|
36
|
+
if (_storeKeys.has(key) || _persist) syncStore(key, value)
|
|
21
37
|
notify(key)
|
|
22
38
|
}
|
|
23
39
|
|
|
40
|
+
// Cross-tab store sync (other pages update when store changes)
|
|
41
|
+
try {
|
|
42
|
+
const _bc = new BroadcastChannel(_STORE_KEY)
|
|
43
|
+
_bc.onmessage = ({ data: { key, value } }) => {
|
|
44
|
+
_state[key] = value; notify(key)
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
24
48
|
function watch(key, cb) {
|
|
25
49
|
if (!_watchers[key]) _watchers[key] = []
|
|
26
50
|
_watchers[key].push(cb)
|
|
@@ -741,6 +765,9 @@ function boot() {
|
|
|
741
765
|
hydrateSelects()
|
|
742
766
|
hydrateIfs()
|
|
743
767
|
hydrateEach()
|
|
768
|
+
hydrateCharts()
|
|
769
|
+
hydrateKanban()
|
|
770
|
+
hydrateEditors()
|
|
744
771
|
mountQueries()
|
|
745
772
|
}
|
|
746
773
|
|
package/server/server.js
CHANGED
|
@@ -761,7 +761,18 @@ function parseFrontPage(src) {
|
|
|
761
761
|
// Auto-detect target from path if not specified: ~mount GET /api/users → @users
|
|
762
762
|
const autoTarget = pts[3] || ('@' + (pts[2]?.split('/').filter(Boolean).pop()?.split('?')[0]||'data'))
|
|
763
763
|
p.queries.push({trigger:'mount',method:pts[1],path:pts[2],target:ai===-1?autoTarget:null,action:ai!==-1?pts.slice(ai+1).join(' '):null})
|
|
764
|
-
} else if(pts[0]==='interval')
|
|
764
|
+
} else if(pts[0]==='interval') {
|
|
765
|
+
p.queries.push({trigger:'interval',interval:parseInt(pts[1]),method:pts[2],path:pts[3],target:ai===-1?pts[4]:null,action:ai!==-1?pts.slice(ai+1).join(' '):null})
|
|
766
|
+
} else if(pts[0]==='ssr') {
|
|
767
|
+
const target = (ai!==-1 ? pts[ai+1] : pts[3] || ('@'+(pts[2]?.split('/').filter(Boolean).pop()||'data'))).replace(/^@/,'')
|
|
768
|
+
p.ssr = p.ssr || []
|
|
769
|
+
p.ssr.push({ method: pts[1]||'GET', path: pts[2], target })
|
|
770
|
+
} else if(pts[0]==='store') {
|
|
771
|
+
const sk = (pts[1]||'').replace(/^@/,'')
|
|
772
|
+
p.stores = p.stores || []
|
|
773
|
+
p.stores.push({ key: sk, method: pts[2], path: pts[3], persist: pts.find(x=>x.startsWith('persist='))?.slice(8)||'session' })
|
|
774
|
+
p.queries.push({ trigger:'mount', method:pts[2], path:pts[3], target:'@'+sk })
|
|
775
|
+
}}
|
|
765
776
|
else p.blocks.push({kind:blockKind(line),rawLine:line})
|
|
766
777
|
}
|
|
767
778
|
return p
|
|
@@ -1828,7 +1839,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1828
1839
|
|
|
1829
1840
|
// Health
|
|
1830
1841
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1831
|
-
status:'ok', version:'2.10.
|
|
1842
|
+
status:'ok', version:'2.10.3',
|
|
1832
1843
|
models: app.models.map(m=>m.name),
|
|
1833
1844
|
routes: app.apis.length, pages: app.pages.length,
|
|
1834
1845
|
admin: app.admin?.prefix || null,
|