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