neuron-dsl 1.0.0 → 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 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,14 +1,15 @@
1
1
  {
2
2
  "name": "neuron-dsl",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI-first declarative web app DSL compiler",
5
5
  "type": "module",
6
6
  "bin": {
7
- "neuron": "./dist/index.js"
7
+ "neuron": "dist/index.js"
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",
@@ -20,7 +21,13 @@
20
21
  "type": "git",
21
22
  "url": "git@github-second:rayforvideos/neuron.git"
22
23
  },
23
- "keywords": ["neuron", "dsl", "compiler", "spa", "ai"],
24
+ "keywords": [
25
+ "neuron",
26
+ "dsl",
27
+ "compiler",
28
+ "spa",
29
+ "ai"
30
+ ],
24
31
  "author": "",
25
32
  "license": "MIT",
26
33
  "devDependencies": {
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "name": "{{PROJECT_NAME}}",
3
- "version": "1.0.0"
3
+ "version": "1.0.0",
4
+ "reference": "node_modules/neuron-dsl/REFERENCE.md"
4
5
  }