neuron-dsl 1.0.1 → 1.0.2
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/README.md +13 -0
- package/REFERENCE.md +369 -0
- package/dist/index.js +301 -0
- package/package.json +3 -2
- package/templates/neuron.json +2 -1
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@ AI 에이전트를 위한 선언적 웹 앱 DSL 컴파일러.
|
|
|
8
8
|
|
|
9
9
|
`.neuron` 파일을 작성하면 SPA(Single Page Application)로 컴파일합니다. 프레임워크 없이 순수 HTML/CSS/JS를 생성합니다.
|
|
10
10
|
|
|
11
|
+
## 설치
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g neuron-dsl
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
## Quick Start
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -19,6 +25,13 @@ cd my-shop
|
|
|
19
25
|
neuron build
|
|
20
26
|
```
|
|
21
27
|
|
|
28
|
+
`dist/` 폴더의 결과물을 아무 정적 서버에 배포하면 됩니다:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 로컬에서 바로 확인 (npx 활용)
|
|
32
|
+
npx serve dist
|
|
33
|
+
```
|
|
34
|
+
|
|
22
35
|
`dist/` 폴더에 배포 가능한 SPA가 생성됩니다:
|
|
23
36
|
|
|
24
37
|
```
|
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Neuron DSL Reference
|
|
2
|
+
|
|
3
|
+
> This file is the complete reference for the Neuron DSL.
|
|
4
|
+
> If you are an AI agent working on a Neuron project, read this file to understand the full syntax.
|
|
5
|
+
> The project config file `neuron.json` points here via the `"reference"` field.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Neuron is a declarative DSL that compiles `.neuron` files into single-page applications (HTML + CSS + JS). It uses exactly **4 keywords**: `STATE`, `ACTION`, `API`, `PAGE`.
|
|
10
|
+
|
|
11
|
+
Build command: `neuron build` (run from project root)
|
|
12
|
+
|
|
13
|
+
## Project Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
project/
|
|
17
|
+
├── neuron.json # Project config (required)
|
|
18
|
+
├── app.neuron # Global state & actions (required)
|
|
19
|
+
├── pages/
|
|
20
|
+
│ ├── home.neuron # One file per page (required: at least one)
|
|
21
|
+
│ └── *.neuron
|
|
22
|
+
├── apis/
|
|
23
|
+
│ └── *.neuron # API definitions (optional)
|
|
24
|
+
├── themes/
|
|
25
|
+
│ └── theme.json # Design tokens (optional, has defaults)
|
|
26
|
+
└── assets/ # Static files (copied to dist/assets/)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## neuron.json
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"name": "Project Name",
|
|
34
|
+
"reference": "node_modules/neuron-dsl/REFERENCE.md"
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Keyword 1: STATE (app.neuron)
|
|
39
|
+
|
|
40
|
+
Defines global application state. Each field has a name and default value.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
STATE
|
|
44
|
+
fieldName: defaultValue
|
|
45
|
+
cart: []
|
|
46
|
+
products: []
|
|
47
|
+
user: null
|
|
48
|
+
query: ""
|
|
49
|
+
count: 0
|
|
50
|
+
active: false
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Types are inferred from default values: `[]` = array, `null` = nullable, `""` = string, `0` = number, `false` = boolean.
|
|
54
|
+
|
|
55
|
+
## Keyword 2: ACTION (app.neuron)
|
|
56
|
+
|
|
57
|
+
Defines state mutations. Must come after `STATE`, separated by `---`.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
STATE
|
|
61
|
+
cart: []
|
|
62
|
+
products: []
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
ACTION action-name
|
|
67
|
+
step-key: step-value
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Action patterns
|
|
71
|
+
|
|
72
|
+
**append** - Add item to array:
|
|
73
|
+
```
|
|
74
|
+
ACTION add-to-cart
|
|
75
|
+
append: product -> cart
|
|
76
|
+
```
|
|
77
|
+
Generated: `function(item) { _setState('cart', [..._state.cart, item]); }`
|
|
78
|
+
|
|
79
|
+
**remove** - Remove by id match:
|
|
80
|
+
```
|
|
81
|
+
ACTION remove-from-cart
|
|
82
|
+
remove: cart where id matches
|
|
83
|
+
```
|
|
84
|
+
Generated: `function(id) { _setState('cart', _state.cart.filter(i => i.id !== id)); }`
|
|
85
|
+
|
|
86
|
+
**call** - Call an API:
|
|
87
|
+
```
|
|
88
|
+
ACTION checkout
|
|
89
|
+
call: orders
|
|
90
|
+
on_success: -> /complete
|
|
91
|
+
on_error: show-error
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Call options:
|
|
95
|
+
- `on_success: -> /route` - navigate on success
|
|
96
|
+
- `on_success: stateName` - set state with response data
|
|
97
|
+
- `on_error: label` - console.error label
|
|
98
|
+
- `query: stateField` - append `?q=stateValue` to URL
|
|
99
|
+
- `target: stateField` - set state with response data
|
|
100
|
+
|
|
101
|
+
## Keyword 3: API (apis/*.neuron)
|
|
102
|
+
|
|
103
|
+
Defines HTTP endpoints. One API per file. File goes in `apis/` directory.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
API name
|
|
107
|
+
METHOD /endpoint
|
|
108
|
+
option: value
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
| Option | Values | Description |
|
|
114
|
+
|--------|--------|-------------|
|
|
115
|
+
| `on_load` | `true` | Auto-fetch when page loads |
|
|
116
|
+
| `body` | state field name | Send state as JSON body (POST) |
|
|
117
|
+
| `returns` | type name | Return type (documentation only) |
|
|
118
|
+
|
|
119
|
+
### Example
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
API products
|
|
123
|
+
GET /api/products
|
|
124
|
+
on_load: true
|
|
125
|
+
returns: Product[]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Important:** API name must match a STATE field name for auto-binding. `API products` binds to `STATE.products`.
|
|
129
|
+
|
|
130
|
+
## Keyword 4: PAGE (pages/*.neuron)
|
|
131
|
+
|
|
132
|
+
Defines a page with components. One page per file. File goes in `pages/` directory.
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
PAGE id "Display Title" /route
|
|
136
|
+
|
|
137
|
+
component-type
|
|
138
|
+
property: value
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Indentation rules
|
|
142
|
+
- `PAGE` line: no indentation
|
|
143
|
+
- Components: 2 spaces
|
|
144
|
+
- Properties: 4 spaces
|
|
145
|
+
- Blank lines between components are optional but recommended
|
|
146
|
+
|
|
147
|
+
### Example
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
PAGE home "Home" /
|
|
151
|
+
|
|
152
|
+
header
|
|
153
|
+
title: "My App"
|
|
154
|
+
links: [Products>/products, Cart>/cart]
|
|
155
|
+
|
|
156
|
+
hero
|
|
157
|
+
title: "Welcome"
|
|
158
|
+
subtitle: "Get started"
|
|
159
|
+
cta: "Shop Now" -> /products
|
|
160
|
+
|
|
161
|
+
footer
|
|
162
|
+
text: "© 2026 My App"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Components
|
|
166
|
+
|
|
167
|
+
### Layout
|
|
168
|
+
|
|
169
|
+
**header**
|
|
170
|
+
```
|
|
171
|
+
header
|
|
172
|
+
title: "App Name"
|
|
173
|
+
logo: "logo.png" # optional
|
|
174
|
+
links: [Label>/route, Label>/route] # optional
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**footer**
|
|
178
|
+
```
|
|
179
|
+
footer
|
|
180
|
+
text: "Footer text"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**section** - Generic container, supports nested components
|
|
184
|
+
```
|
|
185
|
+
section
|
|
186
|
+
text
|
|
187
|
+
content: "Hello"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**grid** - CSS grid layout
|
|
191
|
+
```
|
|
192
|
+
grid
|
|
193
|
+
cols: 3
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**hero** - Full-width banner
|
|
197
|
+
```
|
|
198
|
+
hero
|
|
199
|
+
title: "Heading"
|
|
200
|
+
subtitle: "Subheading"
|
|
201
|
+
cta: "Button Label" -> /route # link-style CTA
|
|
202
|
+
cta: "Button Label" -> action-name # action-style CTA
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Data Display
|
|
206
|
+
|
|
207
|
+
**product-grid** - Renders items from state as a card grid
|
|
208
|
+
```
|
|
209
|
+
product-grid
|
|
210
|
+
data: stateField # required: which state array to render
|
|
211
|
+
cols: 3 # optional: grid columns (default: 3)
|
|
212
|
+
on_click: action-name # optional: action when card button clicked
|
|
213
|
+
image: fieldName # optional: item field for image URL
|
|
214
|
+
title: fieldName # optional: item field for title text
|
|
215
|
+
subtitle: fieldName # optional: item field for subtitle text
|
|
216
|
+
price: fieldName # optional: item field for price (auto-formatted with toLocaleString)
|
|
217
|
+
id: fieldName # optional: item field for unique ID (default: "id")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Without field mappings, cards render empty. Always specify field mappings for your data shape.
|
|
221
|
+
|
|
222
|
+
**list** - Generic list
|
|
223
|
+
```
|
|
224
|
+
list
|
|
225
|
+
data: stateField
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**table** - Tabular data
|
|
229
|
+
```
|
|
230
|
+
table
|
|
231
|
+
data: stateField
|
|
232
|
+
cols: 3
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**text** - Text content
|
|
236
|
+
```
|
|
237
|
+
text
|
|
238
|
+
content: "Some text"
|
|
239
|
+
size: sm|md|lg|xl # optional (default: md)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**image**
|
|
243
|
+
```
|
|
244
|
+
image
|
|
245
|
+
src: "path/to/image.png"
|
|
246
|
+
alt: "Description"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### State-Bound Components
|
|
250
|
+
|
|
251
|
+
**cart-icon** - Badge showing item count
|
|
252
|
+
```
|
|
253
|
+
cart-icon
|
|
254
|
+
state: stateField # which state array to count
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**cart-list** - Renders items with remove buttons
|
|
258
|
+
```
|
|
259
|
+
cart-list
|
|
260
|
+
state: stateField # required: which state array to render
|
|
261
|
+
on_remove: action-name # optional: action for remove button
|
|
262
|
+
image: fieldName # optional: item field for image
|
|
263
|
+
title: fieldName # optional: item field for title
|
|
264
|
+
subtitle: fieldName # optional: item field for subtitle
|
|
265
|
+
price: fieldName # optional: item field for price
|
|
266
|
+
id: fieldName # optional: item field for ID (default: "id")
|
|
267
|
+
empty_text: "Custom text" # optional: text shown when list is empty
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**cart-summary** - Shows item count and total
|
|
271
|
+
```
|
|
272
|
+
cart-summary
|
|
273
|
+
state: stateField # required: which state array
|
|
274
|
+
price: fieldName # optional: item field to sum for total
|
|
275
|
+
count_label: "Items" # optional: label for count row
|
|
276
|
+
total_label: "Total" # optional: label for total row
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Interactive Components
|
|
280
|
+
|
|
281
|
+
**button** - Two syntax forms:
|
|
282
|
+
|
|
283
|
+
Inline (requires action):
|
|
284
|
+
```
|
|
285
|
+
button "Label" -> /route
|
|
286
|
+
button "Label" -> action-name
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Property-style:
|
|
290
|
+
```
|
|
291
|
+
button
|
|
292
|
+
label: "Label"
|
|
293
|
+
variant: primary|secondary|danger|ghost|default
|
|
294
|
+
action: action-name # optional
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**form**
|
|
298
|
+
```
|
|
299
|
+
form
|
|
300
|
+
field_name: "Placeholder"
|
|
301
|
+
field_email: "Email"
|
|
302
|
+
submit: "Submit" -> action-name
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**search**
|
|
306
|
+
```
|
|
307
|
+
search
|
|
308
|
+
placeholder: "Search..."
|
|
309
|
+
state: stateField
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**modal**
|
|
313
|
+
```
|
|
314
|
+
modal
|
|
315
|
+
state: stateField
|
|
316
|
+
title: "Modal Title"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**tabs**
|
|
320
|
+
```
|
|
321
|
+
tabs
|
|
322
|
+
items: [Tab1, Tab2, Tab3]
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Theme (themes/theme.json)
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"colors": {
|
|
330
|
+
"primary": "#2E86AB",
|
|
331
|
+
"secondary": "#A23B72",
|
|
332
|
+
"danger": "#E84855",
|
|
333
|
+
"bg": "#FFFFFF",
|
|
334
|
+
"text": "#1A1A2E",
|
|
335
|
+
"border": "#E0E0E0"
|
|
336
|
+
},
|
|
337
|
+
"font": {
|
|
338
|
+
"family": "Inter",
|
|
339
|
+
"size": { "sm": 14, "md": 16, "lg": 20, "xl": 28 }
|
|
340
|
+
},
|
|
341
|
+
"radius": 8,
|
|
342
|
+
"shadow": "0 2px 8px rgba(0,0,0,0.1)",
|
|
343
|
+
"spacing": { "sm": 8, "md": 16, "lg": 24, "xl": 48 }
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Components use `variant` property to reference theme colors: `primary`, `secondary`, `danger`, `ghost`, `default`.
|
|
348
|
+
|
|
349
|
+
## Build Output
|
|
350
|
+
|
|
351
|
+
`neuron build` produces:
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
dist/
|
|
355
|
+
├── index.html # SPA with all pages (hidden/shown via router)
|
|
356
|
+
├── style.css # Theme variables + component styles
|
|
357
|
+
├── main.js # State management, routing, runtime renderers
|
|
358
|
+
└── assets/ # Copied from project assets/
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Key Constraints
|
|
362
|
+
|
|
363
|
+
1. **API name = STATE field name** for auto-binding (e.g., `API products` binds to `STATE.products`)
|
|
364
|
+
2. **One page per file** in `pages/` directory
|
|
365
|
+
3. **One API per file** in `apis/` directory
|
|
366
|
+
4. **Indentation matters**: 2 spaces for components, 4 spaces for properties
|
|
367
|
+
5. **STATE and ACTION** share `app.neuron`, separated by `---`
|
|
368
|
+
6. **Field mappings** on data components (`product-grid`, `cart-list`, `cart-summary`) must match your data's actual field names
|
|
369
|
+
7. **Static assets** in `assets/` are copied to `dist/assets/` on build
|
package/dist/index.js
CHANGED
|
@@ -563,6 +563,97 @@ a { color: var(--color-primary); text-decoration: none; }
|
|
|
563
563
|
.neuron-grid { display: grid; gap: var(--spacing-md); padding: var(--spacing-md); }
|
|
564
564
|
.neuron-list { padding: var(--spacing-md); }
|
|
565
565
|
.neuron-section { padding: var(--spacing-md); }
|
|
566
|
+
|
|
567
|
+
.neuron-product-card {
|
|
568
|
+
border: 1px solid var(--color-border);
|
|
569
|
+
border-radius: var(--radius);
|
|
570
|
+
overflow: hidden;
|
|
571
|
+
background: var(--color-bg);
|
|
572
|
+
box-shadow: var(--shadow);
|
|
573
|
+
transition: transform 0.2s;
|
|
574
|
+
}
|
|
575
|
+
.neuron-product-card:hover { transform: translateY(-4px); }
|
|
576
|
+
.neuron-product-card__img {
|
|
577
|
+
width: 100%;
|
|
578
|
+
aspect-ratio: 1;
|
|
579
|
+
object-fit: cover;
|
|
580
|
+
background: #f5f5f7;
|
|
581
|
+
}
|
|
582
|
+
.neuron-product-card__body { padding: var(--spacing-md); }
|
|
583
|
+
.neuron-product-card__category {
|
|
584
|
+
font-size: var(--font-size-sm);
|
|
585
|
+
color: var(--color-secondary);
|
|
586
|
+
margin-bottom: 4px;
|
|
587
|
+
}
|
|
588
|
+
.neuron-product-card__name {
|
|
589
|
+
font-size: var(--font-size-md);
|
|
590
|
+
color: var(--color-text);
|
|
591
|
+
margin-bottom: var(--spacing-sm);
|
|
592
|
+
}
|
|
593
|
+
.neuron-product-card__price {
|
|
594
|
+
font-size: calc(var(--font-size-md) + 2px);
|
|
595
|
+
font-weight: 600;
|
|
596
|
+
color: var(--color-text);
|
|
597
|
+
margin-bottom: var(--spacing-sm);
|
|
598
|
+
}
|
|
599
|
+
.neuron-product-card__btn { width: 100%; }
|
|
600
|
+
|
|
601
|
+
.neuron-cart-item {
|
|
602
|
+
display: flex;
|
|
603
|
+
align-items: center;
|
|
604
|
+
padding: var(--spacing-md) 0;
|
|
605
|
+
border-bottom: 1px solid var(--color-border);
|
|
606
|
+
gap: var(--spacing-md);
|
|
607
|
+
}
|
|
608
|
+
.neuron-cart-item__img {
|
|
609
|
+
width: 80px; height: 80px;
|
|
610
|
+
object-fit: cover;
|
|
611
|
+
border-radius: var(--radius);
|
|
612
|
+
background: #f5f5f7;
|
|
613
|
+
}
|
|
614
|
+
.neuron-cart-item__info { flex: 1; }
|
|
615
|
+
.neuron-cart-item__info h4 { color: var(--color-text); margin-bottom: 4px; }
|
|
616
|
+
.neuron-cart-item__info p { color: var(--color-secondary); font-size: var(--font-size-sm); }
|
|
617
|
+
.neuron-cart-item__price {
|
|
618
|
+
font-size: var(--font-size-md);
|
|
619
|
+
font-weight: 600;
|
|
620
|
+
color: var(--color-text);
|
|
621
|
+
margin: 0 var(--spacing-md);
|
|
622
|
+
}
|
|
623
|
+
.neuron-cart-item__remove {
|
|
624
|
+
padding: var(--spacing-sm) var(--spacing-md);
|
|
625
|
+
font-size: var(--font-size-sm);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.neuron-cart-summary__content {
|
|
629
|
+
padding: var(--spacing-lg);
|
|
630
|
+
background: #f5f5f7;
|
|
631
|
+
border-radius: var(--radius);
|
|
632
|
+
margin: var(--spacing-md) 0;
|
|
633
|
+
}
|
|
634
|
+
.neuron-cart-summary__row {
|
|
635
|
+
display: flex;
|
|
636
|
+
justify-content: space-between;
|
|
637
|
+
margin-bottom: var(--spacing-sm);
|
|
638
|
+
color: var(--color-secondary);
|
|
639
|
+
}
|
|
640
|
+
.neuron-cart-summary__total {
|
|
641
|
+
display: flex;
|
|
642
|
+
justify-content: space-between;
|
|
643
|
+
padding-top: var(--spacing-sm);
|
|
644
|
+
border-top: 1px solid var(--color-border);
|
|
645
|
+
font-size: var(--font-size-lg);
|
|
646
|
+
font-weight: 600;
|
|
647
|
+
color: var(--color-text);
|
|
648
|
+
}
|
|
649
|
+
.neuron-cart-summary__total span:last-child { color: var(--color-primary); }
|
|
650
|
+
|
|
651
|
+
.neuron-empty {
|
|
652
|
+
text-align: center;
|
|
653
|
+
padding: var(--spacing-xl);
|
|
654
|
+
color: var(--color-secondary);
|
|
655
|
+
font-size: var(--font-size-lg);
|
|
656
|
+
}
|
|
566
657
|
`;
|
|
567
658
|
function generateCSS(theme) {
|
|
568
659
|
return themeToCSS(theme) + "\n" + BASE_STYLES;
|
|
@@ -578,6 +669,9 @@ function generateJS(ast) {
|
|
|
578
669
|
lines.push(generateActions(ast));
|
|
579
670
|
lines.push(generateFormHandling());
|
|
580
671
|
lines.push(generateAutoLoad(ast));
|
|
672
|
+
const renderers2 = generateRuntimeRenderers(ast);
|
|
673
|
+
if (renderers2) lines.push(renderers2);
|
|
674
|
+
lines.push(generateInitBindings(ast));
|
|
581
675
|
lines.push(generateInit());
|
|
582
676
|
return lines.join("\n\n");
|
|
583
677
|
}
|
|
@@ -750,8 +844,215 @@ function generateAutoLoad(ast) {
|
|
|
750
844
|
${calls.join("\n")}
|
|
751
845
|
}`;
|
|
752
846
|
}
|
|
847
|
+
function getProp2(comp, key) {
|
|
848
|
+
return comp.properties.find((p) => p.key === key)?.value;
|
|
849
|
+
}
|
|
850
|
+
function collectBoundComponents(ast) {
|
|
851
|
+
const results = [];
|
|
852
|
+
const seen = /* @__PURE__ */ new Set();
|
|
853
|
+
let counter = 0;
|
|
854
|
+
function walk(components) {
|
|
855
|
+
for (const comp of components) {
|
|
856
|
+
const type = comp.componentType;
|
|
857
|
+
if (type === "product-grid") {
|
|
858
|
+
const dataField = getProp2(comp, "data") || "items";
|
|
859
|
+
const key = `product-grid:${dataField}`;
|
|
860
|
+
if (!seen.has(key)) {
|
|
861
|
+
seen.add(key);
|
|
862
|
+
results.push({
|
|
863
|
+
type,
|
|
864
|
+
stateField: dataField,
|
|
865
|
+
rendererId: `_renderGrid${counter++}`,
|
|
866
|
+
config: {
|
|
867
|
+
image: getProp2(comp, "image") || "",
|
|
868
|
+
title: getProp2(comp, "title") || "",
|
|
869
|
+
subtitle: getProp2(comp, "subtitle") || "",
|
|
870
|
+
price: getProp2(comp, "price") || "",
|
|
871
|
+
id: getProp2(comp, "id") || "id"
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (type === "cart-list") {
|
|
877
|
+
const stateField = getProp2(comp, "state") || "cart";
|
|
878
|
+
const key = `cart-list:${stateField}`;
|
|
879
|
+
if (!seen.has(key)) {
|
|
880
|
+
seen.add(key);
|
|
881
|
+
results.push({
|
|
882
|
+
type,
|
|
883
|
+
stateField,
|
|
884
|
+
rendererId: `_renderList${counter++}`,
|
|
885
|
+
config: {
|
|
886
|
+
image: getProp2(comp, "image") || "",
|
|
887
|
+
title: getProp2(comp, "title") || "",
|
|
888
|
+
subtitle: getProp2(comp, "subtitle") || "",
|
|
889
|
+
price: getProp2(comp, "price") || "",
|
|
890
|
+
id: getProp2(comp, "id") || "id",
|
|
891
|
+
empty_text: getProp2(comp, "empty_text") || ""
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (type === "cart-summary") {
|
|
897
|
+
const stateField = getProp2(comp, "state") || "cart";
|
|
898
|
+
const key = `cart-summary:${stateField}`;
|
|
899
|
+
if (!seen.has(key)) {
|
|
900
|
+
seen.add(key);
|
|
901
|
+
results.push({
|
|
902
|
+
type,
|
|
903
|
+
stateField,
|
|
904
|
+
rendererId: `_renderSummary${counter++}`,
|
|
905
|
+
config: {
|
|
906
|
+
price: getProp2(comp, "price") || "",
|
|
907
|
+
count_label: getProp2(comp, "count_label") || "",
|
|
908
|
+
total_label: getProp2(comp, "total_label") || ""
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (type === "cart-icon") {
|
|
914
|
+
const stateField = getProp2(comp, "state") || "cart";
|
|
915
|
+
const key = `cart-icon:${stateField}`;
|
|
916
|
+
if (!seen.has(key)) {
|
|
917
|
+
seen.add(key);
|
|
918
|
+
results.push({
|
|
919
|
+
type,
|
|
920
|
+
stateField,
|
|
921
|
+
rendererId: `_renderIcon${counter++}`,
|
|
922
|
+
config: {}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
walk(comp.children);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
for (const page of ast.pages) {
|
|
930
|
+
walk(page.components);
|
|
931
|
+
}
|
|
932
|
+
return results;
|
|
933
|
+
}
|
|
934
|
+
function generateRuntimeRenderers(ast) {
|
|
935
|
+
const bound = collectBoundComponents(ast);
|
|
936
|
+
const parts = [];
|
|
937
|
+
for (const bc of bound) {
|
|
938
|
+
if (bc.type === "product-grid") {
|
|
939
|
+
const { image, title, subtitle, price, id } = bc.config;
|
|
940
|
+
const imgPart = image ? `'<img class="neuron-product-card__img" src="' + p['${image}'] + '" alt="' + (p['${title}'] || '') + '">'` : `''`;
|
|
941
|
+
const titlePart = title ? `'<h3 class="neuron-product-card__name">' + p['${title}'] + '</h3>'` : `''`;
|
|
942
|
+
const subtitlePart = subtitle ? `'<p class="neuron-product-card__category">' + p['${subtitle}'] + '</p>'` : `''`;
|
|
943
|
+
const pricePart = price ? `'<p class="neuron-product-card__price">' + (typeof p['${price}'] === "number" ? p['${price}'].toLocaleString() : p['${price}']) + '</p>'` : `''`;
|
|
944
|
+
const idField = id || "id";
|
|
945
|
+
parts.push(`function ${bc.rendererId}(items) {
|
|
946
|
+
document.querySelectorAll('.neuron-product-grid').forEach(function(grid) {
|
|
947
|
+
var cols = grid.getAttribute('data-cols') || '3';
|
|
948
|
+
var action = grid.getAttribute('data-action');
|
|
949
|
+
grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
|
950
|
+
if (!items || items.length === 0) {
|
|
951
|
+
grid.innerHTML = '<div class="neuron-empty">No items</div>';
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
grid.innerHTML = items.map(function(p) {
|
|
955
|
+
var html = '<article class="neuron-product-card">';
|
|
956
|
+
html += ${imgPart};
|
|
957
|
+
html += '<div class="neuron-product-card__body">';
|
|
958
|
+
html += ${subtitlePart};
|
|
959
|
+
html += ${titlePart};
|
|
960
|
+
html += ${pricePart};
|
|
961
|
+
if (action) html += '<button class="neuron-btn neuron-btn--primary neuron-product-card__btn" data-product-id="' + p['${idField}'] + '">+</button>';
|
|
962
|
+
html += '</div></article>';
|
|
963
|
+
return html;
|
|
964
|
+
}).join('');
|
|
965
|
+
if (action) {
|
|
966
|
+
grid.querySelectorAll('[data-product-id]').forEach(function(btn) {
|
|
967
|
+
btn.addEventListener('click', function(e) {
|
|
968
|
+
e.stopPropagation();
|
|
969
|
+
var pid = this.getAttribute('data-product-id');
|
|
970
|
+
var source = grid.getAttribute('data-source');
|
|
971
|
+
var product = _state[source].find(function(p) { return String(p['${idField}']) === pid; });
|
|
972
|
+
if (product && _actions[action]) _actions[action](product);
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}`);
|
|
978
|
+
}
|
|
979
|
+
if (bc.type === "cart-list") {
|
|
980
|
+
const { image, title, subtitle, price, id, empty_text } = bc.config;
|
|
981
|
+
const emptyMsg = empty_text || "Empty";
|
|
982
|
+
const idField = id || "id";
|
|
983
|
+
const imgPart = image ? `'<img class="neuron-cart-item__img" src="' + item['${image}'] + '" alt="' + (item['${title}'] || '') + '">'` : `''`;
|
|
984
|
+
const titlePart = title ? `'<h4>' + item['${title}'] + '</h4>'` : `''`;
|
|
985
|
+
const subtitlePart = subtitle ? `'<p>' + item['${subtitle}'] + '</p>'` : `''`;
|
|
986
|
+
const pricePart = price ? `'<p class="neuron-cart-item__price">' + (typeof item['${price}'] === "number" ? item['${price}'].toLocaleString() : item['${price}']) + '</p>'` : `''`;
|
|
987
|
+
parts.push(`function ${bc.rendererId}(items) {
|
|
988
|
+
document.querySelectorAll('.neuron-cart-list').forEach(function(list) {
|
|
989
|
+
var removeAction = list.getAttribute('data-remove-action');
|
|
990
|
+
if (!items || items.length === 0) {
|
|
991
|
+
list.innerHTML = '<div class="neuron-empty">${emptyMsg}</div>';
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
list.innerHTML = items.map(function(item) {
|
|
995
|
+
var html = '<div class="neuron-cart-item">';
|
|
996
|
+
html += ${imgPart};
|
|
997
|
+
html += '<div class="neuron-cart-item__info">';
|
|
998
|
+
html += ${titlePart};
|
|
999
|
+
html += ${subtitlePart};
|
|
1000
|
+
html += '</div>';
|
|
1001
|
+
html += ${pricePart};
|
|
1002
|
+
if (removeAction) html += '<button class="neuron-btn neuron-btn--danger neuron-cart-item__remove" data-remove-id="' + item['${idField}'] + '">X</button>';
|
|
1003
|
+
html += '</div>';
|
|
1004
|
+
return html;
|
|
1005
|
+
}).join('');
|
|
1006
|
+
if (removeAction) {
|
|
1007
|
+
list.querySelectorAll('[data-remove-id]').forEach(function(btn) {
|
|
1008
|
+
btn.addEventListener('click', function() {
|
|
1009
|
+
var rid = this.getAttribute('data-remove-id');
|
|
1010
|
+
var item = items.find(function(i) { return String(i['${idField}']) === rid; });
|
|
1011
|
+
if (item && _actions[removeAction]) _actions[removeAction](item['${idField}']);
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (bc.type === "cart-summary") {
|
|
1019
|
+
const { price, count_label, total_label } = bc.config;
|
|
1020
|
+
const countLbl = count_label || "Items";
|
|
1021
|
+
const totalLbl = total_label || "Total";
|
|
1022
|
+
const totalExpr = price ? `items.reduce(function(s, i) { return s + (Number(i['${price}']) || 0); }, 0).toLocaleString()` : `items.length`;
|
|
1023
|
+
parts.push(`function ${bc.rendererId}(items) {
|
|
1024
|
+
document.querySelectorAll('.neuron-cart-summary').forEach(function(el) {
|
|
1025
|
+
if (!items) items = [];
|
|
1026
|
+
var total = ${totalExpr};
|
|
1027
|
+
el.innerHTML = '<div class="neuron-cart-summary__content">' +
|
|
1028
|
+
'<div class="neuron-cart-summary__row"><span>${countLbl}</span><span>' + items.length + '</span></div>' +
|
|
1029
|
+
'<div class="neuron-cart-summary__total"><span>${totalLbl}</span><span>' + total + '</span></div>' +
|
|
1030
|
+
'</div>';
|
|
1031
|
+
});
|
|
1032
|
+
}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (bc.type === "cart-icon") {
|
|
1035
|
+
parts.push(`function ${bc.rendererId}(items) {
|
|
1036
|
+
document.querySelectorAll('.neuron-cart-icon .badge').forEach(function(badge) {
|
|
1037
|
+
badge.textContent = items ? items.length : 0;
|
|
1038
|
+
});
|
|
1039
|
+
}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return parts.join("\n\n");
|
|
1043
|
+
}
|
|
1044
|
+
function generateInitBindings(ast) {
|
|
1045
|
+
const bound = collectBoundComponents(ast);
|
|
1046
|
+
const registrations = bound.map(
|
|
1047
|
+
(bc) => ` _bindings['${bc.stateField}'].push(${bc.rendererId});`
|
|
1048
|
+
);
|
|
1049
|
+
return `function _initBindings() {
|
|
1050
|
+
${registrations.join("\n")}
|
|
1051
|
+
}`;
|
|
1052
|
+
}
|
|
753
1053
|
function generateInit() {
|
|
754
1054
|
return `document.addEventListener('DOMContentLoaded', function() {
|
|
1055
|
+
_initBindings();
|
|
755
1056
|
_initRouter();
|
|
756
1057
|
_autoLoad();
|
|
757
1058
|
});`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuron-dsl",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AI-first declarative web app DSL compiler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"templates"
|
|
11
|
+
"templates",
|
|
12
|
+
"REFERENCE.md"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"build": "tsup src/index.ts --format esm --dts",
|
package/templates/neuron.json
CHANGED