@jxsuite/runtime 0.22.0 → 0.23.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/runtime",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "JSON-native reactive web component runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "type": "module",
16
16
  "exports": {
17
- ".": "./src/runtime.js"
17
+ ".": "./src/runtime.js",
18
+ "./expression": "./src/expression.js"
18
19
  },
19
20
  "publishConfig": {
20
21
  "provenance": true
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Jx Declarative Expression Engine (spec §19)
3
+ *
4
+ * Shared module used by both runtime (evaluateExpression) and compiler (compileExpression).
5
+ *
6
+ * @module expression
7
+ */
8
+
9
+ /** @typedef {{ operator: string; target: any; value?: any; initial?: any }} ExpressionNode */
10
+
11
+ const MUTATING_OPS = new Set([
12
+ "=",
13
+ "+=",
14
+ "-=",
15
+ "*=",
16
+ "/=",
17
+ "push",
18
+ "pop",
19
+ "shift",
20
+ "unshift",
21
+ "splice",
22
+ ]);
23
+ const UNARY_OPS = new Set(["!", "-"]);
24
+ const BINARY_OPS = new Set([
25
+ "+",
26
+ "-",
27
+ "*",
28
+ "/",
29
+ "%",
30
+ "===",
31
+ "!==",
32
+ "<",
33
+ "<=",
34
+ ">",
35
+ ">=",
36
+ "&&",
37
+ "||",
38
+ ]);
39
+ const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/="]);
40
+ const ARRAY_METHOD_OPS = new Set(["push", "pop", "shift", "unshift", "splice"]);
41
+ const AGGREGATE_OPS = new Set(["reduce", "map", "filter"]);
42
+
43
+ export const BLESSED_OPERATORS = new Set([
44
+ ...MUTATING_OPS,
45
+ ...UNARY_OPS,
46
+ ...BINARY_OPS,
47
+ ...AGGREGATE_OPS,
48
+ ]);
49
+
50
+ /**
51
+ * @param {string} op
52
+ * @returns {boolean}
53
+ */
54
+ export function isMutating(op) {
55
+ return MUTATING_OPS.has(op);
56
+ }
57
+
58
+ // ─── Runtime Evaluation ──────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Resolve an operand to its runtime value.
62
+ *
63
+ * @param {any} operand
64
+ * @param {Record<string, any>} state
65
+ * @param {Event | null} event
66
+ * @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
67
+ * @returns {any}
68
+ */
69
+ function resolveOperand(operand, state, event, iterCtx) {
70
+ if (operand === null || operand === undefined) return operand;
71
+
72
+ // Nested expression node
73
+ if (typeof operand === "object" && !Array.isArray(operand) && "operator" in operand) {
74
+ return evaluateExpression(operand, state, event, iterCtx);
75
+ }
76
+
77
+ // $ref pointer
78
+ if (typeof operand === "object" && !Array.isArray(operand) && "$ref" in operand) {
79
+ return resolveExprRef(operand.$ref, state, event, iterCtx);
80
+ }
81
+
82
+ // Array of operands (e.g., splice args)
83
+ if (Array.isArray(operand)) {
84
+ return operand.map((o) => resolveOperand(o, state, event, iterCtx));
85
+ }
86
+
87
+ // Literal
88
+ return operand;
89
+ }
90
+
91
+ /**
92
+ * Resolve a $ref string within expression context. Handles event#/, $reduce/acc, $map/, #/state/,
93
+ * and other schemes.
94
+ *
95
+ * @param {string} ref
96
+ * @param {Record<string, any>} state
97
+ * @param {Event | null} event
98
+ * @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
99
+ * @returns {any}
100
+ */
101
+ function resolveExprRef(ref, state, event, iterCtx) {
102
+ if (ref === "$reduce/acc") {
103
+ return iterCtx?.acc;
104
+ }
105
+ if (ref.startsWith("$reduce/")) {
106
+ return iterCtx?.acc;
107
+ }
108
+ if (ref.startsWith("event#/")) {
109
+ const path = ref.slice("event#/".length);
110
+ return getPath(event, path);
111
+ }
112
+ if (ref.startsWith("$map/")) {
113
+ const parts = ref.split("/");
114
+ const key = parts[1];
115
+ let base;
116
+ if (key === "item") base = iterCtx?.item ?? state.$map?.item ?? state["$map/item"];
117
+ else if (key === "index") base = iterCtx?.index ?? state.$map?.index ?? state["$map/index"];
118
+ else base = state.$map?.[key] ?? state["$map/" + key];
119
+ return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
120
+ }
121
+ if (ref.startsWith("#/state/")) {
122
+ const sub = ref.slice("#/state/".length);
123
+ const slash = sub.indexOf("/");
124
+ if (slash < 0) return state[sub];
125
+ return getPath(state[sub.slice(0, slash)], sub.slice(slash + 1));
126
+ }
127
+ if (ref.startsWith("parent#/")) return state[ref.slice("parent#/".length)];
128
+ if (ref.startsWith("window#/")) return getPath(globalThis.window, ref.slice("window#/".length));
129
+ if (ref.startsWith("document#/"))
130
+ return getPath(globalThis.document, ref.slice("document#/".length));
131
+ return state[ref] ?? null;
132
+ }
133
+
134
+ /**
135
+ * Resolve a $ref to a writable location — returns { obj, key } for assignment.
136
+ *
137
+ * @param {string} ref
138
+ * @param {Record<string, any>} state
139
+ * @param {Event | null} event
140
+ * @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
141
+ * @returns {{ obj: any; key: string }}
142
+ */
143
+ function resolveWritableRef(ref, state, event, iterCtx) {
144
+ if (ref.startsWith("$map/")) {
145
+ const parts = ref.split("/");
146
+ const key = parts[1];
147
+ let base;
148
+ if (key === "item") base = iterCtx?.item ?? state.$map?.item ?? state["$map/item"];
149
+ else if (key === "index") base = iterCtx?.index ?? state.$map?.index ?? state["$map/index"];
150
+ else base = state.$map?.[key] ?? state["$map/" + key];
151
+ if (parts.length > 2) {
152
+ const pathParts = parts.slice(2);
153
+ const lastKey = pathParts.pop();
154
+ const obj = pathParts.length > 0 ? getPath(base, pathParts.join("/")) : base;
155
+ return { obj, key: /** @type {string} */ (lastKey) };
156
+ }
157
+ return { obj: state.$map ?? state, key: key === "item" ? "$map/item" : "$map/index" };
158
+ }
159
+ if (ref.startsWith("#/state/")) {
160
+ const sub = ref.slice("#/state/".length);
161
+ const slash = sub.indexOf("/");
162
+ if (slash < 0) return { obj: state, key: sub };
163
+ const parts = sub.split("/");
164
+ const lastKey = parts.pop();
165
+ let obj = state;
166
+ for (const p of parts) obj = obj[p];
167
+ return { obj, key: /** @type {string} */ (lastKey) };
168
+ }
169
+ return { obj: state, key: ref };
170
+ }
171
+
172
+ /**
173
+ * Evaluate an expression node at runtime.
174
+ *
175
+ * @param {ExpressionNode} node
176
+ * @param {Record<string, any>} state
177
+ * @param {Event | null} event
178
+ * @param {{ acc?: any; item?: any; index?: number }} [iterCtx]
179
+ * @returns {any}
180
+ */
181
+ export function evaluateExpression(node, state, event, iterCtx) {
182
+ const { operator, target, value, initial } = node;
183
+
184
+ if (!BLESSED_OPERATORS.has(operator)) {
185
+ throw new Error(`$expression: unknown operator "${operator}"`);
186
+ }
187
+
188
+ // ─── Unary ───
189
+ if (UNARY_OPS.has(operator) && !("value" in node)) {
190
+ const operand = resolveOperand(target, state, event, iterCtx);
191
+ if (operator === "!") return !operand;
192
+ if (operator === "-") return -operand;
193
+ }
194
+
195
+ // ─── Binary (pure) ───
196
+ if (BINARY_OPS.has(operator)) {
197
+ const left = resolveOperand(target, state, event, iterCtx);
198
+ const right = resolveOperand(value, state, event, iterCtx);
199
+ switch (operator) {
200
+ case "+":
201
+ return left + right;
202
+ case "-":
203
+ return left - right;
204
+ case "*":
205
+ return left * right;
206
+ case "/":
207
+ return left / right;
208
+ case "%":
209
+ return left % right;
210
+ case "===":
211
+ return left === right;
212
+ case "!==":
213
+ return left !== right;
214
+ case "<":
215
+ return left < right;
216
+ case "<=":
217
+ return left <= right;
218
+ case ">":
219
+ return left > right;
220
+ case ">=":
221
+ return left >= right;
222
+ case "&&":
223
+ return left && right;
224
+ case "||":
225
+ return left || right;
226
+ }
227
+ }
228
+
229
+ // ─── Assignment ───
230
+ if (ASSIGNMENT_OPS.has(operator)) {
231
+ const rhs = resolveOperand(value, state, event, iterCtx);
232
+ const { obj, key } = resolveWritableRef(target.$ref, state, event, iterCtx);
233
+ switch (operator) {
234
+ case "=":
235
+ obj[key] = rhs;
236
+ break;
237
+ case "+=":
238
+ obj[key] += rhs;
239
+ break;
240
+ case "-=":
241
+ obj[key] -= rhs;
242
+ break;
243
+ case "*=":
244
+ obj[key] *= rhs;
245
+ break;
246
+ case "/=":
247
+ obj[key] /= rhs;
248
+ break;
249
+ }
250
+ return;
251
+ }
252
+
253
+ // ─── Array methods ───
254
+ if (ARRAY_METHOD_OPS.has(operator)) {
255
+ const arr = resolveOperand(target, state, event, iterCtx);
256
+ switch (operator) {
257
+ case "push":
258
+ return arr.push(resolveOperand(value, state, event, iterCtx));
259
+ case "unshift":
260
+ return arr.unshift(resolveOperand(value, state, event, iterCtx));
261
+ case "pop":
262
+ return arr.pop();
263
+ case "shift":
264
+ return arr.shift();
265
+ case "splice": {
266
+ const args = resolveOperand(value, state, event, iterCtx);
267
+ return arr.splice(...args);
268
+ }
269
+ }
270
+ }
271
+
272
+ // ─── Aggregates (pure) ───
273
+ if (AGGREGATE_OPS.has(operator)) {
274
+ const arr = resolveOperand(target, state, event, iterCtx);
275
+ if (operator === "reduce") {
276
+ const seed = resolveOperand(initial, state, event, iterCtx);
277
+ return arr.reduce(
278
+ (/** @type {any} */ acc, /** @type {any} */ item, /** @type {number} */ index) => {
279
+ return evaluateExpression(value, state, event, { acc, item, index });
280
+ },
281
+ seed,
282
+ );
283
+ }
284
+ if (operator === "map") {
285
+ return arr.map((/** @type {any} */ item, /** @type {number} */ index) => {
286
+ return evaluateExpression(value, state, event, { ...iterCtx, item, index });
287
+ });
288
+ }
289
+ if (operator === "filter") {
290
+ return arr.filter((/** @type {any} */ item, /** @type {number} */ index) => {
291
+ return evaluateExpression(value, state, event, { ...iterCtx, item, index });
292
+ });
293
+ }
294
+ }
295
+ }
296
+
297
+ // ─── Compiler: Expression → JS Source ────────────────────────────────────────
298
+
299
+ /** @typedef {{ statePrefix?: string; eventParam?: string }} CompileOpts */
300
+
301
+ /**
302
+ * Compile an operand to a JS source string.
303
+ *
304
+ * @param {any} operand
305
+ * @param {CompileOpts} opts
306
+ * @returns {string}
307
+ */
308
+ function compileOperand(operand, opts) {
309
+ if (operand === null) return "null";
310
+ if (operand === undefined) return "undefined";
311
+
312
+ // Nested expression node
313
+ if (typeof operand === "object" && !Array.isArray(operand) && "operator" in operand) {
314
+ return compileExpression(operand, opts);
315
+ }
316
+
317
+ // $ref pointer
318
+ if (typeof operand === "object" && !Array.isArray(operand) && "$ref" in operand) {
319
+ return compileRef(operand.$ref, opts);
320
+ }
321
+
322
+ // Array of operands
323
+ if (Array.isArray(operand)) {
324
+ return operand.map((o) => compileOperand(o, opts)).join(", ");
325
+ }
326
+
327
+ // Literal
328
+ return JSON.stringify(operand);
329
+ }
330
+
331
+ /**
332
+ * Compile a $ref to its JS equivalent.
333
+ *
334
+ * @param {string} ref
335
+ * @param {CompileOpts} opts
336
+ * @returns {string}
337
+ */
338
+ function compileRef(ref, opts) {
339
+ const s = opts.statePrefix ?? "state";
340
+ const e = opts.eventParam ?? "event";
341
+
342
+ if (ref === "$reduce/acc") return "_acc";
343
+ if (ref.startsWith("$reduce/")) return "_acc";
344
+
345
+ if (ref.startsWith("event#/")) {
346
+ const path = ref.slice("event#/".length);
347
+ return `${e}.${path.replace(/\//g, ".")}`;
348
+ }
349
+
350
+ if (ref.startsWith("$map/")) {
351
+ const parts = ref.split("/");
352
+ const key = parts[1];
353
+ if (key === "item") {
354
+ return parts.length > 2 ? `_item.${parts.slice(2).join(".")}` : "_item";
355
+ }
356
+ if (key === "index") return "_index";
357
+ return `_${key}`;
358
+ }
359
+
360
+ if (ref.startsWith("#/state/")) {
361
+ const path = ref.slice("#/state/".length);
362
+ return `${s}.${path.replace(/\//g, ".")}`;
363
+ }
364
+
365
+ if (ref.startsWith("parent#/")) return `${s}.${ref.slice("parent#/".length)}`;
366
+ if (ref.startsWith("window#/"))
367
+ return `window.${ref.slice("window#/".length).replace(/\//g, ".")}`;
368
+ if (ref.startsWith("document#/"))
369
+ return `document.${ref.slice("document#/".length).replace(/\//g, ".")}`;
370
+
371
+ return `${s}.${ref}`;
372
+ }
373
+
374
+ /**
375
+ * Compile a writable $ref target to its JS equivalent (for LHS of assignment).
376
+ *
377
+ * @param {any} target
378
+ * @param {CompileOpts} opts
379
+ * @returns {string}
380
+ */
381
+ function compileTarget(target, opts) {
382
+ if (typeof target === "object" && "$ref" in target) {
383
+ return compileRef(target.$ref, opts);
384
+ }
385
+ return compileOperand(target, opts);
386
+ }
387
+
388
+ /**
389
+ * Compile an expression node to a JS source string. Returns a statement for mutating ops, a value
390
+ * expression for pure ops.
391
+ *
392
+ * @param {ExpressionNode} node
393
+ * @param {CompileOpts} [opts]
394
+ * @returns {string}
395
+ */
396
+ export function compileExpression(node, opts = {}) {
397
+ const { operator, target, value, initial } = node;
398
+
399
+ // ─── Unary ───
400
+ if (UNARY_OPS.has(operator) && !("value" in node)) {
401
+ const operand = compileOperand(target, opts);
402
+ if (operator === "!") return `!(${operand})`;
403
+ if (operator === "-") return `-(${operand})`;
404
+ }
405
+
406
+ // ─── Binary (pure) ───
407
+ if (BINARY_OPS.has(operator)) {
408
+ const left = compileOperand(target, opts);
409
+ const right = compileOperand(value, opts);
410
+ return `(${left} ${operator} ${right})`;
411
+ }
412
+
413
+ // ─── Assignment ───
414
+ if (ASSIGNMENT_OPS.has(operator)) {
415
+ const lhs = compileTarget(target, opts);
416
+ const rhs = compileOperand(value, opts);
417
+ return `${lhs} ${operator} ${rhs}`;
418
+ }
419
+
420
+ // ─── Array methods ───
421
+ if (ARRAY_METHOD_OPS.has(operator)) {
422
+ const arr = compileTarget(target, opts);
423
+ switch (operator) {
424
+ case "push":
425
+ return `${arr}.push(${compileOperand(value, opts)})`;
426
+ case "unshift":
427
+ return `${arr}.unshift(${compileOperand(value, opts)})`;
428
+ case "pop":
429
+ return `${arr}.pop()`;
430
+ case "shift":
431
+ return `${arr}.shift()`;
432
+ case "splice": {
433
+ const args = Array.isArray(value)
434
+ ? value.map((o) => compileOperand(o, opts)).join(", ")
435
+ : compileOperand(value, opts);
436
+ return `${arr}.splice(${args})`;
437
+ }
438
+ }
439
+ }
440
+
441
+ // ─── Aggregates (pure) ───
442
+ if (AGGREGATE_OPS.has(operator)) {
443
+ const arr = compileOperand(target, opts);
444
+ const itemExpr = compileExpression(value, opts);
445
+ if (operator === "reduce") {
446
+ const seed = compileOperand(initial, opts);
447
+ return `${arr}.reduce((_acc, _item, _index) => ${itemExpr}, ${seed})`;
448
+ }
449
+ if (operator === "map") {
450
+ return `${arr}.map((_item, _index) => ${itemExpr})`;
451
+ }
452
+ if (operator === "filter") {
453
+ return `${arr}.filter((_item, _index) => ${itemExpr})`;
454
+ }
455
+ }
456
+
457
+ return "undefined";
458
+ }
459
+
460
+ // ─── Utilities ───────────────────────────────────────────────────────────────
461
+
462
+ /**
463
+ * @param {any} obj
464
+ * @param {string} path
465
+ * @returns {any}
466
+ */
467
+ function getPath(obj, path) {
468
+ return path.split(/[./]/).reduce((o, k) => /** @type {any} */ (o)?.[k], obj);
469
+ }
package/src/runtime.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { reactive, ref, computed, effect, isRef, onEffectCleanup } from "@vue/reactivity";
16
+ import { evaluateExpression, isMutating } from "./expression.js";
16
17
 
17
18
  // ─── Public API ───────────────────────────────────────────────────────────────
18
19
 
@@ -43,6 +44,10 @@ export async function Jx(source, target = document.body, options) {
43
44
  injectHead(doc.$head, base);
44
45
  }
45
46
 
47
+ if (doc.$media) {
48
+ _rootMedia = doc.$media;
49
+ }
50
+
46
51
  const state = await buildScope(doc, {}, base);
47
52
  target.appendChild(renderNode(doc, state, options));
48
53
  if (typeof state.onMount === "function") state.onMount(state);
@@ -157,6 +162,7 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
157
162
  // 4. Object
158
163
  if (typeof def === "object") {
159
164
  if (def.$prototype) continue; // handled in later passes
165
+ if ("$expression" in def) continue; // handled in pass 2.5
160
166
  if (def.timing === "server" && def.$src && def.$export) continue; // handled in fifth pass
161
167
  if ("default" in def) {
162
168
  raw[key] = def.default;
@@ -177,6 +183,19 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
177
183
  }
178
184
  }
179
185
 
186
+ // Pass 2.5: $expression entries (Shape 5)
187
+ for (const [key, def] of Object.entries(defs)) {
188
+ if (def && typeof def === "object" && !Array.isArray(def) && "$expression" in def) {
189
+ const node = /** @type {any} */ (def).$expression;
190
+ if (isMutating(node.operator)) {
191
+ state[key] = (/** @type {any} */ s, /** @type {any} */ event) =>
192
+ evaluateExpression(node, s, event);
193
+ } else {
194
+ state[key] = computed(() => evaluateExpression(node, state, null));
195
+ }
196
+ }
197
+ }
198
+
180
199
  // Third pass: $prototype: "Function" entries
181
200
  for (const [key, def] of Object.entries(defs)) {
182
201
  if (typeof def === "object" && def?.$prototype === "Function") {
@@ -209,6 +228,8 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
209
228
 
210
229
  if (doc.$media) {
211
230
  state["$media"] = doc.$media;
231
+ } else if (!state["$media"] && Object.keys(_rootMedia).length > 0) {
232
+ state["$media"] = _rootMedia;
212
233
  }
213
234
 
214
235
  return state;
@@ -489,6 +510,13 @@ function applyProperties(el, def, state) {
489
510
  el.addEventListener(key.slice(2), (e) => fn(scope, e));
490
511
  continue;
491
512
  }
513
+ // Event handler: inline $expression
514
+ if (val && typeof val === "object" && "$expression" in val) {
515
+ const node = /** @type {any} */ (val).$expression;
516
+ const scope = state;
517
+ el.addEventListener(key.slice(2), (e) => evaluateExpression(node, scope, e));
518
+ continue;
519
+ }
492
520
  }
493
521
 
494
522
  bindProperty(el, key, val, state);
@@ -542,6 +570,27 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
542
570
  const nested = {};
543
571
  /** @type {Record<string, any>} */
544
572
  const media = {};
573
+ /** @type {Record<string, string>} */
574
+ const baseDecls = {};
575
+
576
+ // Collect properties overridden by media queries so we can avoid inline styles for them
577
+ /** @type {Set<string>} */
578
+ const mediaOverriddenProps = new Set();
579
+ for (const [prop, val] of Object.entries(styleDef)) {
580
+ if (prop.startsWith("@") && val && typeof val === "object") {
581
+ for (const k of Object.keys(val)) {
582
+ if (
583
+ !k.startsWith(":") &&
584
+ !k.startsWith(".") &&
585
+ !k.startsWith("&") &&
586
+ !k.startsWith("[") &&
587
+ !k.startsWith("@")
588
+ ) {
589
+ mediaOverriddenProps.add(k);
590
+ }
591
+ }
592
+ }
593
+ }
545
594
 
546
595
  for (const [prop, val] of Object.entries(styleDef)) {
547
596
  if (prop.startsWith("@")) media[prop] = val;
@@ -558,17 +607,21 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
558
607
  /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] =
559
608
  evaluateTemplate(val, state);
560
609
  });
610
+ else if (mediaOverriddenProps.has(prop)) baseDecls[prop] = val;
561
611
  else /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] = val;
562
612
  }
563
613
 
564
614
  const hasNested = Object.keys(nested).length > 0;
565
615
  const hasMedia = Object.keys(media).length > 0;
566
- if (!hasNested && !hasMedia) return;
616
+ const hasBaseDecls = Object.keys(baseDecls).length > 0;
617
+ if (!hasNested && !hasMedia && !hasBaseDecls) return;
567
618
 
568
619
  const uid = `jx-${Math.random().toString(36).slice(2, 7)}`;
569
620
  el.dataset.jx = uid;
570
621
 
571
622
  let css = "";
623
+ const baseCSS = toCSSText(baseDecls);
624
+ if (baseCSS) css += `[data-jx="${uid}"] { ${baseCSS} }\n`;
572
625
 
573
626
  function emitNested(/** @type {string} */ scope, /** @type {Record<string, any>} */ rules) {
574
627
  const props = toCSSText(rules);
@@ -600,11 +653,13 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
600
653
 
601
654
  for (const [key, rules] of Object.entries(media)) {
602
655
  if (key === "@--") continue; // base canvas width, not a real media query
603
- const query = key.startsWith("@--")
604
- ? (mediaQueries[key.slice(1)] ?? key.slice(1))
605
- : key.slice(1);
656
+ const atRule = key.startsWith("@--")
657
+ ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
658
+ : key.startsWith("@(")
659
+ ? `@media ${key.slice(1)}`
660
+ : key;
606
661
  const scope = `[data-jx="${uid}"]`;
607
- css += `@media ${query} { ${scope} { ${toCSSText(rules)} } }\n`;
662
+ css += `${atRule} { ${scope} { ${toCSSText(rules)} } }\n`;
608
663
 
609
664
  function emitMediaNested(
610
665
  /** @type {string} */ parentSel,
@@ -621,7 +676,7 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
621
676
  ? `${parentSel}${sel}`
622
677
  : `${parentSel} ${sel}`;
623
678
  const props = toCSSText(sub);
624
- if (props) css += `@media ${query} { ${resolved} { ${props} } }\n`;
679
+ if (props) css += `${atRule} { ${resolved} { ${props} } }\n`;
625
680
  emitMediaNested(resolved, sub);
626
681
  }
627
682
  }
@@ -667,8 +722,7 @@ function renderMappedArray(def, state, options) {
667
722
  applyProperties(container, def, state);
668
723
  applyStyle(container, def.style ?? {}, state["$media"] ?? {}, state);
669
724
  applyAttributes(container, def.attributes ?? {}, state);
670
- const { items: _items, of: _of, map: mapDef, filter: filterRef, sort: sortRef } = def.children;
671
- const itemsSrc = _items ?? _of;
725
+ const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef } = def.children;
672
726
 
673
727
  effect(() => {
674
728
  container.innerHTML = "";
@@ -1562,6 +1616,7 @@ export function toCSSText(rules) {
1562
1616
 
1563
1617
  // ─── Custom Element Registration ──────────────────────────────────────────────
1564
1618
 
1619
+ let _rootMedia = {};
1565
1620
  const _elementDefs = new Map();
1566
1621
 
1567
1622
  /**