@constela/core 0.17.0 → 0.17.1
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 +187 -3
- package/dist/index.js +19 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ String state can use a cookie expression to read initial value from cookies (SSR
|
|
|
60
60
|
|
|
61
61
|
## Expression Types
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
19 expression types for constrained computation:
|
|
64
64
|
|
|
65
65
|
| Type | JSON Example | Description |
|
|
66
66
|
|------|-------------|-------------|
|
|
@@ -82,6 +82,7 @@ String state can use a cookie expression to read initial value from cookies (SSR
|
|
|
82
82
|
| `call` | `{ "expr": "call", "target": ..., "method": "filter", "args": [...] }` | Method call |
|
|
83
83
|
| `lambda` | `{ "expr": "lambda", "param": "item", "body": ... }` | Anonymous function |
|
|
84
84
|
| `array` | `{ "expr": "array", "elements": [...] }` | Array construction |
|
|
85
|
+
| `index` | `{ "expr": "index", "base": ..., "key": ... }` | Dynamic property/array access |
|
|
85
86
|
|
|
86
87
|
**Binary Operators:** `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`
|
|
87
88
|
|
|
@@ -125,7 +126,7 @@ Construct arrays dynamically from expressions:
|
|
|
125
126
|
|
|
126
127
|
## View Node Types
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
12 node types for building UI:
|
|
129
130
|
|
|
130
131
|
```json
|
|
131
132
|
// Element node
|
|
@@ -155,11 +156,40 @@ Construct arrays dynamically from expressions:
|
|
|
155
156
|
|
|
156
157
|
// Code block node
|
|
157
158
|
{ "kind": "code", "code": { ... }, "language": { ... } }
|
|
159
|
+
|
|
160
|
+
// Portal node - renders children to a different DOM location
|
|
161
|
+
{ "kind": "portal", "target": "body", "children": [ ... ] }
|
|
162
|
+
|
|
163
|
+
// Island node - partial hydration boundary
|
|
164
|
+
{
|
|
165
|
+
"kind": "island",
|
|
166
|
+
"id": "counter",
|
|
167
|
+
"strategy": "visible",
|
|
168
|
+
"strategyOptions": { "threshold": 0.5 },
|
|
169
|
+
"content": { ... },
|
|
170
|
+
"state": { ... },
|
|
171
|
+
"actions": [ ... ]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Suspense node - async content with fallback
|
|
175
|
+
{
|
|
176
|
+
"kind": "suspense",
|
|
177
|
+
"id": "async-data",
|
|
178
|
+
"fallback": { "kind": "text", "value": { "expr": "lit", "value": "Loading..." } },
|
|
179
|
+
"content": { ... }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ErrorBoundary node - error handling with fallback UI
|
|
183
|
+
{
|
|
184
|
+
"kind": "errorBoundary",
|
|
185
|
+
"fallback": { "kind": "text", "value": { "expr": "lit", "value": "Something went wrong" } },
|
|
186
|
+
"content": { ... }
|
|
187
|
+
}
|
|
158
188
|
```
|
|
159
189
|
|
|
160
190
|
## Action Step Types
|
|
161
191
|
|
|
162
|
-
|
|
192
|
+
27 step types for declarative actions:
|
|
163
193
|
|
|
164
194
|
```json
|
|
165
195
|
// Set state value
|
|
@@ -205,6 +235,66 @@ Construct arrays dynamically from expressions:
|
|
|
205
235
|
|
|
206
236
|
// WebSocket close
|
|
207
237
|
{ "do": "close", "connection": "chat" }
|
|
238
|
+
|
|
239
|
+
// Delay (setTimeout equivalent)
|
|
240
|
+
{ "do": "delay", "ms": { "expr": "lit", "value": 1000 }, "then": [ ... ] }
|
|
241
|
+
|
|
242
|
+
// Interval (setInterval equivalent)
|
|
243
|
+
{ "do": "interval", "ms": { "expr": "lit", "value": 5000 }, "action": "refresh", "result": "intervalId" }
|
|
244
|
+
|
|
245
|
+
// Clear timer (clearTimeout/clearInterval)
|
|
246
|
+
{ "do": "clearTimer", "target": { "expr": "state", "name": "intervalId" } }
|
|
247
|
+
|
|
248
|
+
// Focus management
|
|
249
|
+
{ "do": "focus", "target": { "expr": "ref", "name": "inputEl" }, "operation": "focus" }
|
|
250
|
+
|
|
251
|
+
// Conditional execution
|
|
252
|
+
{ "do": "if", "condition": { ... }, "then": [ ... ], "else": [ ... ] }
|
|
253
|
+
|
|
254
|
+
// SSE connection (Server-Sent Events)
|
|
255
|
+
{
|
|
256
|
+
"do": "sseConnect",
|
|
257
|
+
"connection": "notifications",
|
|
258
|
+
"url": { "expr": "lit", "value": "/api/events" },
|
|
259
|
+
"eventTypes": ["message", "update"],
|
|
260
|
+
"reconnect": { "enabled": true, "strategy": "exponential", "maxRetries": 5, "baseDelay": 1000 },
|
|
261
|
+
"onOpen": [ ... ],
|
|
262
|
+
"onMessage": [ ... ],
|
|
263
|
+
"onError": [ ... ]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// SSE close
|
|
267
|
+
{ "do": "sseClose", "connection": "notifications" }
|
|
268
|
+
|
|
269
|
+
// Optimistic update (apply UI update immediately, rollback on failure)
|
|
270
|
+
{
|
|
271
|
+
"do": "optimistic",
|
|
272
|
+
"target": "posts",
|
|
273
|
+
"path": { "expr": "var", "name": "index" },
|
|
274
|
+
"value": { "expr": "lit", "value": { "liked": true } },
|
|
275
|
+
"result": "updateId",
|
|
276
|
+
"timeout": 5000
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Confirm optimistic update
|
|
280
|
+
{ "do": "confirm", "id": { "expr": "var", "name": "updateId" } }
|
|
281
|
+
|
|
282
|
+
// Reject optimistic update (rollback)
|
|
283
|
+
{ "do": "reject", "id": { "expr": "var", "name": "updateId" } }
|
|
284
|
+
|
|
285
|
+
// Bind connection messages to state
|
|
286
|
+
{
|
|
287
|
+
"do": "bind",
|
|
288
|
+
"connection": "notifications",
|
|
289
|
+
"eventType": "update",
|
|
290
|
+
"target": "messages",
|
|
291
|
+
"path": { "expr": "var", "name": "payload", "path": "id" },
|
|
292
|
+
"transform": { "expr": "get", "base": { "expr": "var", "name": "payload" }, "path": "data" },
|
|
293
|
+
"patch": false
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Unbind connection from state
|
|
297
|
+
{ "do": "unbind", "connection": "notifications", "target": "messages" }
|
|
208
298
|
```
|
|
209
299
|
|
|
210
300
|
## Connections
|
|
@@ -301,6 +391,100 @@ Use styles with `StyleExpr`:
|
|
|
301
391
|
}
|
|
302
392
|
```
|
|
303
393
|
|
|
394
|
+
## Theme System
|
|
395
|
+
|
|
396
|
+
Configure application theming with CSS variables:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"theme": {
|
|
401
|
+
"mode": "system",
|
|
402
|
+
"colors": {
|
|
403
|
+
"primary": "hsl(220 90% 56%)",
|
|
404
|
+
"primary-foreground": "hsl(0 0% 100%)",
|
|
405
|
+
"background": "hsl(0 0% 100%)",
|
|
406
|
+
"foreground": "hsl(222 47% 11%)",
|
|
407
|
+
"muted": "hsl(210 40% 96%)",
|
|
408
|
+
"muted-foreground": "hsl(215 16% 47%)",
|
|
409
|
+
"border": "hsl(214 32% 91%)"
|
|
410
|
+
},
|
|
411
|
+
"darkColors": {
|
|
412
|
+
"background": "hsl(222 47% 11%)",
|
|
413
|
+
"foreground": "hsl(210 40% 98%)",
|
|
414
|
+
"muted": "hsl(217 33% 17%)",
|
|
415
|
+
"muted-foreground": "hsl(215 20% 65%)",
|
|
416
|
+
"border": "hsl(217 33% 17%)"
|
|
417
|
+
},
|
|
418
|
+
"fonts": {
|
|
419
|
+
"sans": "Inter, system-ui, sans-serif",
|
|
420
|
+
"mono": "JetBrains Mono, monospace"
|
|
421
|
+
},
|
|
422
|
+
"cssPrefix": "app"
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**ThemeConfig:**
|
|
428
|
+
|
|
429
|
+
| Property | Type | Description |
|
|
430
|
+
|----------|------|-------------|
|
|
431
|
+
| `mode` | `'light' \| 'dark' \| 'system'` | Color scheme mode |
|
|
432
|
+
| `colors` | `ThemeColors` | Light mode color tokens |
|
|
433
|
+
| `darkColors` | `ThemeColors` | Dark mode color tokens |
|
|
434
|
+
| `fonts` | `ThemeFonts` | Font family definitions |
|
|
435
|
+
| `cssPrefix` | `string` | CSS variable prefix (e.g., `--app-primary`) |
|
|
436
|
+
|
|
437
|
+
**ColorScheme:** `'light'`, `'dark'`, `'system'`
|
|
438
|
+
|
|
439
|
+
## Islands Architecture
|
|
440
|
+
|
|
441
|
+
Define interactive islands with partial hydration strategies:
|
|
442
|
+
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"kind": "island",
|
|
446
|
+
"id": "interactive-chart",
|
|
447
|
+
"strategy": "visible",
|
|
448
|
+
"strategyOptions": {
|
|
449
|
+
"threshold": 0.5,
|
|
450
|
+
"rootMargin": "100px"
|
|
451
|
+
},
|
|
452
|
+
"content": {
|
|
453
|
+
"kind": "component",
|
|
454
|
+
"name": "Chart",
|
|
455
|
+
"props": { ... }
|
|
456
|
+
},
|
|
457
|
+
"state": {
|
|
458
|
+
"data": { "type": "list", "initial": [] }
|
|
459
|
+
},
|
|
460
|
+
"actions": [
|
|
461
|
+
{ "name": "loadData", "steps": [ ... ] }
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Hydration Strategies:**
|
|
467
|
+
|
|
468
|
+
| Strategy | Description | Options |
|
|
469
|
+
|----------|-------------|---------|
|
|
470
|
+
| `load` | Hydrate immediately on page load | - |
|
|
471
|
+
| `idle` | Hydrate when browser is idle | `timeout` (ms) |
|
|
472
|
+
| `visible` | Hydrate when element enters viewport | `threshold` (0-1), `rootMargin` |
|
|
473
|
+
| `interaction` | Hydrate on first user interaction | - |
|
|
474
|
+
| `media` | Hydrate when media query matches | `media` (query string) |
|
|
475
|
+
| `never` | Never hydrate (static only) | - |
|
|
476
|
+
|
|
477
|
+
**IslandNode Properties:**
|
|
478
|
+
|
|
479
|
+
| Property | Type | Description |
|
|
480
|
+
|----------|------|-------------|
|
|
481
|
+
| `id` | `string` | Unique island identifier |
|
|
482
|
+
| `strategy` | `IslandStrategy` | Hydration strategy |
|
|
483
|
+
| `strategyOptions` | `IslandStrategyOptions` | Strategy-specific options |
|
|
484
|
+
| `content` | `ViewNode` | Island content |
|
|
485
|
+
| `state` | `Record<string, StateField>` | Island-local state |
|
|
486
|
+
| `actions` | `ActionDefinition[]` | Island-local actions |
|
|
487
|
+
|
|
304
488
|
## Error Codes
|
|
305
489
|
|
|
306
490
|
| Code | Description |
|
package/dist/index.js
CHANGED
|
@@ -1525,11 +1525,26 @@ function validateStateField(field, path) {
|
|
|
1525
1525
|
return { path: path + "/initial", message: "must be a number" };
|
|
1526
1526
|
}
|
|
1527
1527
|
break;
|
|
1528
|
-
case "string":
|
|
1529
|
-
|
|
1530
|
-
|
|
1528
|
+
case "string": {
|
|
1529
|
+
const initial = field["initial"];
|
|
1530
|
+
if (typeof initial === "string") {
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
if (typeof initial === "object" && initial !== null) {
|
|
1534
|
+
const obj = initial;
|
|
1535
|
+
if (obj["expr"] !== "cookie") {
|
|
1536
|
+
return { path: path + "/initial", message: "must be a string or a valid cookie expression" };
|
|
1537
|
+
}
|
|
1538
|
+
if (typeof obj["key"] !== "string") {
|
|
1539
|
+
return { path: path + "/initial/key", message: "key must be a string" };
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof obj["default"] !== "string") {
|
|
1542
|
+
return { path: path + "/initial/default", message: "default must be a string" };
|
|
1543
|
+
}
|
|
1544
|
+
break;
|
|
1531
1545
|
}
|
|
1532
|
-
|
|
1546
|
+
return { path: path + "/initial", message: "must be a string or a valid cookie expression" };
|
|
1547
|
+
}
|
|
1533
1548
|
case "list":
|
|
1534
1549
|
if (!Array.isArray(field["initial"])) {
|
|
1535
1550
|
return { path: path + "/initial", message: "must be an array" };
|