@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.
Files changed (3) hide show
  1. package/README.md +187 -3
  2. package/dist/index.js +62 -5
  3. 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
- 18 expression types for constrained computation:
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
- 8 node types for building UI:
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
- 14 step types for declarative actions:
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
- if (typeof field["initial"] !== "string") {
1530
- return { path: path + "/initial", message: "must be a string" };
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
- break;
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" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/core",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Core types, schema, and validator for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",