@jxsuite/runtime 3.0.0 → 3.1.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": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "JSON-native reactive web component runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,6 +15,7 @@
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": "./src/runtime.ts",
18
+ "./css": "./src/css.ts",
18
19
  "./expression": "./src/expression.ts",
19
20
  "./pointer": "./src/pointer.ts",
20
21
  "./statements": "./src/statements.ts",
@@ -32,10 +33,10 @@
32
33
  "upgrade": "bun update --latest && bun install"
33
34
  },
34
35
  "dependencies": {
35
- "@jxsuite/schema": "^2.0.0",
36
- "@vue/reactivity": "3.5.41"
36
+ "@jxsuite/schema": "^2.1.0",
37
+ "@vue/reactivity": "3.5.42"
37
38
  },
38
39
  "devDependencies": {
39
- "@happy-dom/global-registrator": "^20.11.8"
40
+ "@happy-dom/global-registrator": "^20.12.0"
40
41
  }
41
42
  }
package/src/css.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The CSS-authoring rules that are not the DOM.
3
+ *
4
+ * `@jxsuite/runtime` renders a document in a browser, and every consumer of its root export pays
5
+ * for that: the element registry, the reactive scope, `@vue/reactivity`. These six exports need
6
+ * none of it. They are pure string and regex math over what a `style` block MEANS — how a property
7
+ * name is spelled in CSS, which media query an `@`-key resolves to, and which selector pair a
8
+ * scheme-conditional rule dual-emits as.
9
+ *
10
+ * They are a subpath of their own because of who else needs them. `@jxsuite/site/site-style` turns
11
+ * `project.json`'s `style` into a stylesheet, and it runs in places the DOM runtime cannot go — a
12
+ * static build, and a Cloudflare Worker serving a live preview. Importing the root export for
13
+ * `camelToKebab` would put the whole renderer plus `@vue/reactivity` inside a Worker script for
14
+ * four pure functions.
15
+ *
16
+ * `./runtime` re-exports all of these, so nothing that already imported them from the root export
17
+ * has to change. This is a narrow door beside the wide one, not a replacement for it.
18
+ */
19
+
20
+ /**
21
+ * Convert camelCase to kebab-case.
22
+ *
23
+ * @param {string} s
24
+ * @returns {string}
25
+ */
26
+ export function camelToKebab(s: string) {
27
+ return s.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
28
+ }
29
+
30
+ // ─── Style At-Rules ───────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * The four CSS media types. A media _type_ is bare — `@media print` — while a media _feature_ is
34
+ * parenthesised, so `@media (print)` reads as a boolean feature named `print`, which does not exist
35
+ * and evaluates false.
36
+ */
37
+ const MEDIA_TYPES = new Set(["all", "print", "screen", "speech"]);
38
+
39
+ /**
40
+ * The media query an `@`-prefixed style key names, or null when the key is some other at-rule
41
+ * (`@supports`, `@starting-style`) that is emitted verbatim.
42
+ *
43
+ * `@--name` resolves through the project's `$media` map. `@(…)` carries its own parentheses, so
44
+ * they are stripped only when what they wrap is a bare media type: `@(print)` must become `@media
45
+ * print`, while `@(min-width: 40rem)` keeps them.
46
+ *
47
+ * @param {string} atKey
48
+ * @param {Record<string, string>} mediaQueries
49
+ * @returns {string | null}
50
+ * @docs framework/concepts/styling
51
+ */
52
+ export function resolveAtQuery(atKey: string, mediaQueries: Record<string, string>): string | null {
53
+ if (atKey.startsWith("@--")) {
54
+ return mediaQueries[atKey.slice(1)] ?? atKey.slice(1);
55
+ }
56
+ if (atKey.startsWith("@(")) {
57
+ const inner = atKey.slice(2, -1).trim();
58
+ return MEDIA_TYPES.has(inner.toLowerCase()) ? inner : atKey.slice(1);
59
+ }
60
+ return null;
61
+ }
62
+
63
+ // ─── Color Schemes ────────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Attribute on the root element (`<html>`) that forces a color scheme. Absent = auto (follow the OS
67
+ * `prefers-color-scheme`). Values: "light" | "dark".
68
+ *
69
+ * @docs framework/concepts/color-schemes
70
+ */
71
+ export const COLOR_SCHEME_ATTR = "data-color-scheme";
72
+
73
+ /**
74
+ * The localStorage key a site switcher persists the visitor's forced scheme under; read by the
75
+ * compiler-injected pre-paint script. Values: "light" | "dark" (absent = auto).
76
+ *
77
+ * @docs framework/concepts/color-schemes
78
+ */
79
+ export const COLOR_SCHEME_STORAGE_KEY = "jx-color-scheme";
80
+
81
+ /**
82
+ * The scheme a _pure_ `prefers-color-scheme` media query targets. Compound queries (any other
83
+ * conditions attached) return null — they are not eligible for forced-scheme dual emission.
84
+ *
85
+ * @param {string} query
86
+ * @returns {"light" | "dark" | null}
87
+ * @docs framework/concepts/color-schemes
88
+ */
89
+ export function pureSchemeOf(query: string): "light" | "dark" | null {
90
+ const m = /^\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)$/.exec(query.trim());
91
+ return (m?.[1] as "light" | "dark") ?? null;
92
+ }
93
+
94
+ /**
95
+ * Selector pair for a scheme-conditional rule: `auto` applies inside the scheme's media query only
96
+ * while no scheme is forced; `forced` applies unconditionally when the root attribute forces the
97
+ * scheme. Guards are wrapped in `:where()` so specificity matches the unguarded selector and source
98
+ * order decides the cascade.
99
+ *
100
+ * @param {string} selector
101
+ * @param {"light" | "dark"} scheme
102
+ * @returns {{ auto: string; forced: string }}
103
+ * @docs framework/concepts/color-schemes
104
+ */
105
+ export function schemeSelectors(
106
+ selector: string,
107
+ scheme: "light" | "dark",
108
+ ): { auto: string; forced: string } {
109
+ if (selector === ":root" || selector === "html") {
110
+ return {
111
+ auto: `${selector}:where(:not([${COLOR_SCHEME_ATTR}]))`,
112
+ forced: `${selector}:where([${COLOR_SCHEME_ATTR}="${scheme}"])`,
113
+ };
114
+ }
115
+ return {
116
+ auto: `:where(:root:not([${COLOR_SCHEME_ATTR}])) ${selector}`,
117
+ forced: `:where(:root[${COLOR_SCHEME_ATTR}="${scheme}"]) ${selector}`,
118
+ };
119
+ }
package/src/runtime.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  ref,
24
24
  toRaw,
25
25
  } from "@vue/reactivity";
26
+ import { camelToKebab, pureSchemeOf, resolveAtQuery, schemeSelectors } from "./css.ts";
26
27
  import { evaluateExpression, evaluateOperand, isMutating } from "./expression.ts";
27
28
  import { readPath } from "./pointer.ts";
28
29
  import type { DynamicClass, JxEventHandler, JxPath, JxRenderOptions, JxScope } from "./types.ts";
@@ -2559,15 +2560,19 @@ function mergeProps(def: JxElement, parentState: JxScope): JxScope {
2559
2560
  return child;
2560
2561
  }
2561
2562
 
2562
- /**
2563
- * Convert camelCase to kebab-case.
2564
- *
2565
- * @param {string} s
2566
- * @returns {string}
2563
+ /*
2564
+ * The CSS-authoring rules live in `./css.ts`, which imports nothing at all. They are re-exported
2565
+ * here because they always were part of this module's surface and moving them must not break a
2566
+ * consumer; a host that cannot afford the DOM runtime imports `@jxsuite/runtime/css` instead.
2567
2567
  */
2568
- export function camelToKebab(s: string) {
2569
- return s.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
2570
- }
2568
+ export {
2569
+ camelToKebab,
2570
+ COLOR_SCHEME_ATTR,
2571
+ COLOR_SCHEME_STORAGE_KEY,
2572
+ pureSchemeOf,
2573
+ resolveAtQuery,
2574
+ schemeSelectors,
2575
+ } from "./css.ts";
2571
2576
 
2572
2577
  /**
2573
2578
  * Convert a style rules object to a CSS text string (skipping nested selectors).
@@ -2582,97 +2587,6 @@ export function toCSSText(rules: Record<string, unknown> | object) {
2582
2587
  .join("; ");
2583
2588
  }
2584
2589
 
2585
- // ─── Style At-Rules ───────────────────────────────────────────────────────────
2586
-
2587
- /**
2588
- * The four CSS media types. A media _type_ is bare — `@media print` — while a media _feature_ is
2589
- * parenthesised, so `@media (print)` reads as a boolean feature named `print`, which does not exist
2590
- * and evaluates false.
2591
- */
2592
- const MEDIA_TYPES = new Set(["all", "print", "screen", "speech"]);
2593
-
2594
- /**
2595
- * The media query an `@`-prefixed style key names, or null when the key is some other at-rule
2596
- * (`@supports`, `@starting-style`) that is emitted verbatim.
2597
- *
2598
- * `@--name` resolves through the project's `$media` map. `@(…)` carries its own parentheses, so
2599
- * they are stripped only when what they wrap is a bare media type: `@(print)` must become `@media
2600
- * print`, while `@(min-width: 40rem)` keeps them.
2601
- *
2602
- * @param {string} atKey
2603
- * @param {Record<string, string>} mediaQueries
2604
- * @returns {string | null}
2605
- * @docs framework/concepts/styling
2606
- */
2607
- export function resolveAtQuery(atKey: string, mediaQueries: Record<string, string>): string | null {
2608
- if (atKey.startsWith("@--")) {
2609
- return mediaQueries[atKey.slice(1)] ?? atKey.slice(1);
2610
- }
2611
- if (atKey.startsWith("@(")) {
2612
- const inner = atKey.slice(2, -1).trim();
2613
- return MEDIA_TYPES.has(inner.toLowerCase()) ? inner : atKey.slice(1);
2614
- }
2615
- return null;
2616
- }
2617
-
2618
- // ─── Color Schemes ────────────────────────────────────────────────────────────
2619
-
2620
- /**
2621
- * Attribute on the root element (`<html>`) that forces a color scheme. Absent = auto (follow the OS
2622
- * `prefers-color-scheme`). Values: "light" | "dark".
2623
- *
2624
- * @docs framework/concepts/color-schemes
2625
- */
2626
- export const COLOR_SCHEME_ATTR = "data-color-scheme";
2627
-
2628
- /**
2629
- * The localStorage key a site switcher persists the visitor's forced scheme under; read by the
2630
- * compiler-injected pre-paint script. Values: "light" | "dark" (absent = auto).
2631
- *
2632
- * @docs framework/concepts/color-schemes
2633
- */
2634
- export const COLOR_SCHEME_STORAGE_KEY = "jx-color-scheme";
2635
-
2636
- /**
2637
- * The scheme a _pure_ `prefers-color-scheme` media query targets. Compound queries (any other
2638
- * conditions attached) return null — they are not eligible for forced-scheme dual emission.
2639
- *
2640
- * @param {string} query
2641
- * @returns {"light" | "dark" | null}
2642
- * @docs framework/concepts/color-schemes
2643
- */
2644
- export function pureSchemeOf(query: string): "light" | "dark" | null {
2645
- const m = /^\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)$/.exec(query.trim());
2646
- return (m?.[1] as "light" | "dark") ?? null;
2647
- }
2648
-
2649
- /**
2650
- * Selector pair for a scheme-conditional rule: `auto` applies inside the scheme's media query only
2651
- * while no scheme is forced; `forced` applies unconditionally when the root attribute forces the
2652
- * scheme. Guards are wrapped in `:where()` so specificity matches the unguarded selector and source
2653
- * order decides the cascade.
2654
- *
2655
- * @param {string} selector
2656
- * @param {"light" | "dark"} scheme
2657
- * @returns {{ auto: string; forced: string }}
2658
- * @docs framework/concepts/color-schemes
2659
- */
2660
- export function schemeSelectors(
2661
- selector: string,
2662
- scheme: "light" | "dark",
2663
- ): { auto: string; forced: string } {
2664
- if (selector === ":root" || selector === "html") {
2665
- return {
2666
- auto: `${selector}:where(:not([${COLOR_SCHEME_ATTR}]))`,
2667
- forced: `${selector}:where([${COLOR_SCHEME_ATTR}="${scheme}"])`,
2668
- };
2669
- }
2670
- return {
2671
- auto: `:where(:root:not([${COLOR_SCHEME_ATTR}])) ${selector}`,
2672
- forced: `:where(:root[${COLOR_SCHEME_ATTR}="${scheme}"]) ${selector}`,
2673
- };
2674
- }
2675
-
2676
2590
  // ─── Custom Element Registration ──────────────────────────────────────────────
2677
2591
 
2678
2592
  let _rootMedia: Record<string, string> = {};