@constela/core 0.17.0 → 0.17.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 +187 -3
- package/dist/index.js +62 -5
- 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
|
@@ -990,7 +990,7 @@ function isObject3(value) {
|
|
|
990
990
|
var VALID_VIEW_KINDS = ["element", "text", "if", "each", "component", "slot", "markdown", "code", "portal", "island"];
|
|
991
991
|
var VALID_EXPR_TYPES = ["lit", "state", "var", "bin", "not", "param", "cond", "get", "style", "validity", "index", "call", "lambda", "array"];
|
|
992
992
|
var VALID_PARAM_TYPES = ["string", "number", "boolean", "json"];
|
|
993
|
-
var VALID_ACTION_TYPES = ["set", "update", "setPath", "fetch", "delay", "interval", "clearTimer", "focus", "if"];
|
|
993
|
+
var VALID_ACTION_TYPES = ["set", "update", "setPath", "fetch", "delay", "interval", "clearTimer", "focus", "if", "storage", "dom"];
|
|
994
994
|
var VALID_STATE_TYPES = ["number", "string", "list", "boolean", "object"];
|
|
995
995
|
var VALID_BIN_OPS = BINARY_OPERATORS;
|
|
996
996
|
var VALID_UPDATE_OPS = UPDATE_OPERATIONS;
|
|
@@ -1502,6 +1502,48 @@ function validateActionStep(step, path) {
|
|
|
1502
1502
|
}
|
|
1503
1503
|
}
|
|
1504
1504
|
break;
|
|
1505
|
+
case "storage":
|
|
1506
|
+
if (!("operation" in step)) {
|
|
1507
|
+
return { path: path + "/operation", message: "operation is required" };
|
|
1508
|
+
}
|
|
1509
|
+
if (!["get", "set", "remove"].includes(step["operation"])) {
|
|
1510
|
+
return { path: path + "/operation", message: "must be one of: get, set, remove" };
|
|
1511
|
+
}
|
|
1512
|
+
if (!("key" in step)) {
|
|
1513
|
+
return { path: path + "/key", message: "key is required" };
|
|
1514
|
+
}
|
|
1515
|
+
{
|
|
1516
|
+
const keyError = validateExpression(step["key"], path + "/key");
|
|
1517
|
+
if (keyError) return keyError;
|
|
1518
|
+
if (step["operation"] === "set" && "value" in step) {
|
|
1519
|
+
const valueError = validateExpression(step["value"], path + "/value");
|
|
1520
|
+
if (valueError) return valueError;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
break;
|
|
1524
|
+
case "dom":
|
|
1525
|
+
if (!("operation" in step)) {
|
|
1526
|
+
return { path: path + "/operation", message: "operation is required" };
|
|
1527
|
+
}
|
|
1528
|
+
if (!["addClass", "removeClass", "toggleClass", "setAttribute", "removeAttribute"].includes(step["operation"])) {
|
|
1529
|
+
return { path: path + "/operation", message: "must be one of: addClass, removeClass, toggleClass, setAttribute, removeAttribute" };
|
|
1530
|
+
}
|
|
1531
|
+
if (!("selector" in step)) {
|
|
1532
|
+
return { path: path + "/selector", message: "selector is required" };
|
|
1533
|
+
}
|
|
1534
|
+
{
|
|
1535
|
+
const selectorError = validateExpression(step["selector"], path + "/selector");
|
|
1536
|
+
if (selectorError) return selectorError;
|
|
1537
|
+
if ("value" in step) {
|
|
1538
|
+
const valueError = validateExpression(step["value"], path + "/value");
|
|
1539
|
+
if (valueError) return valueError;
|
|
1540
|
+
}
|
|
1541
|
+
if ("attribute" in step) {
|
|
1542
|
+
const attrError = validateExpression(step["attribute"], path + "/attribute");
|
|
1543
|
+
if (attrError) return attrError;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
break;
|
|
1505
1547
|
}
|
|
1506
1548
|
return null;
|
|
1507
1549
|
}
|
|
@@ -1525,11 +1567,26 @@ function validateStateField(field, path) {
|
|
|
1525
1567
|
return { path: path + "/initial", message: "must be a number" };
|
|
1526
1568
|
}
|
|
1527
1569
|
break;
|
|
1528
|
-
case "string":
|
|
1529
|
-
|
|
1530
|
-
|
|
1570
|
+
case "string": {
|
|
1571
|
+
const initial = field["initial"];
|
|
1572
|
+
if (typeof initial === "string") {
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
if (typeof initial === "object" && initial !== null) {
|
|
1576
|
+
const obj = initial;
|
|
1577
|
+
if (obj["expr"] !== "cookie") {
|
|
1578
|
+
return { path: path + "/initial", message: "must be a string or a valid cookie expression" };
|
|
1579
|
+
}
|
|
1580
|
+
if (typeof obj["key"] !== "string") {
|
|
1581
|
+
return { path: path + "/initial/key", message: "key must be a string" };
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof obj["default"] !== "string") {
|
|
1584
|
+
return { path: path + "/initial/default", message: "default must be a string" };
|
|
1585
|
+
}
|
|
1586
|
+
break;
|
|
1531
1587
|
}
|
|
1532
|
-
|
|
1588
|
+
return { path: path + "/initial", message: "must be a string or a valid cookie expression" };
|
|
1589
|
+
}
|
|
1533
1590
|
case "list":
|
|
1534
1591
|
if (!Array.isArray(field["initial"])) {
|
|
1535
1592
|
return { path: path + "/initial", message: "must be an array" };
|