@jxsuite/compiler 1.0.0 → 1.2.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/compiler",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,15 +45,16 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@apidevtools/json-schema-ref-parser": "^15.5.0",
48
- "@jxsuite/create": "^1.0.0",
49
- "@jxsuite/runtime": "^1.0.0",
50
- "@jxsuite/schema": "^1.0.0",
48
+ "@jxsuite/create": "^1.1.1",
49
+ "@jxsuite/runtime": "^1.2.0",
50
+ "@jxsuite/schema": "^1.2.0",
51
+ "esbuild": "^0.28.1",
51
52
  "sharp": "^0.35.3"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@happy-dom/global-registrator": "^20.10.6",
55
- "@jxsuite/connector": "^0.2.0",
56
- "@jxsuite/parser": "^1.0.0",
56
+ "@jxsuite/connector": "^0.3.1",
57
+ "@jxsuite/parser": "^1.2.0",
57
58
  "@types/mdast": "^4.0.4"
58
59
  }
59
60
  }
package/src/cli.ts CHANGED
@@ -23,6 +23,8 @@ async function main() {
23
23
 
24
24
  Commands:
25
25
  build [root] Build a Jx site to dist/
26
+ dev [root] Start the dev server (requires @jxsuite/server and Bun)
27
+ preview [root] Serve an already-built dist/ directory
26
28
  schema [root] Generate project.schema.json + document.schema.json from project.json#/extensions
27
29
  validate [root] Validate project.json against its generated project.schema.json
28
30
  db push [root] Sync the data section's tables to their connections (additive-only)
@@ -30,6 +32,7 @@ Commands:
30
32
  Options:
31
33
  --verbose Print detailed build progress
32
34
  --no-clean Don't clean outDir before building
35
+ --port <n> dev/preview: port to listen on (default 3000 / 4173)
33
36
  --dry-run db push: print the statements without executing them
34
37
  --connection db push: restrict to one connection name`);
35
38
  process.exit(0);
@@ -42,11 +45,18 @@ Options:
42
45
  const flags = new Set<string>();
43
46
  const positionals: string[] = [];
44
47
  let connectionArg: string | undefined;
48
+ let portArg: number | undefined;
45
49
  for (let index = 0; index < rest.length; index += 1) {
46
50
  const arg = rest[index]!;
47
51
  if (arg === "--connection") {
48
52
  index += 1;
49
53
  connectionArg = rest[index];
54
+ } else if (arg === "--port") {
55
+ index += 1;
56
+ const parsed = Math.trunc(Number(rest[index] ?? ""));
57
+ if (Number.isFinite(parsed) && parsed >= 0) {
58
+ portArg = parsed;
59
+ }
50
60
  } else if (arg.startsWith("--")) {
51
61
  flags.add(arg);
52
62
  } else {
@@ -118,6 +128,26 @@ Options:
118
128
  console.error(`Build failed: ${err.message}`);
119
129
  process.exit(1);
120
130
  }
131
+ } else if (command === "dev") {
132
+ try {
133
+ const { runDev } = await import("./site/dev-command.ts");
134
+ runDev(projectRoot, portArg);
135
+ } catch (error) {
136
+ const err = error as Error;
137
+ console.error(err.message);
138
+ process.exit(1);
139
+ }
140
+ } else if (command === "preview") {
141
+ const { existsSync } = await import("node:fs");
142
+ const distDir = resolve(projectRoot, "dist");
143
+ if (!existsSync(distDir)) {
144
+ console.error(`No build output at ${distDir} — run \`jx build\` first.`);
145
+ process.exit(1);
146
+ }
147
+ const { startPreviewServer } = await import("./site/preview-server.ts");
148
+ const port = portArg ?? 4173;
149
+ startPreviewServer(distDir, port);
150
+ console.log(`Previewing ${distDir} at http://localhost:${port}`);
121
151
  } else if (command === "schema") {
122
152
  try {
123
153
  const { writeProjectSchemas } = await import("./site/schema-command.ts");
package/src/compiler.ts CHANGED
@@ -46,6 +46,13 @@ export interface CompileOptions {
46
46
  litHtmlSrc?: string;
47
47
  projectStyle?: JxStyle | null;
48
48
  formats?: FormatRegistry;
49
+ /** Rewrite Function-def `$src` specifiers to served URLs (see CompileElementOptions). */
50
+ rewriteSrc?: (specifier: string) => string;
51
+ /**
52
+ * Inject the color-scheme pre-paint script when `$media` declares a pure scheme query (default
53
+ * true). The site pipeline disables this and injects via the merged <head> instead.
54
+ */
55
+ prePaintScheme?: boolean;
49
56
  [key: string]: unknown;
50
57
  }
51
58
 
@@ -107,6 +114,8 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
107
114
  litHtmlSrc,
108
115
  projectStyle,
109
116
  reactivitySrc,
117
+ ...(opts.prePaintScheme === false ? { prePaintScheme: false } : {}),
118
+ ...(opts.rewriteSrc ? { rewriteSrc: opts.rewriteSrc } : {}),
110
119
  title,
111
120
  });
112
121
  }
@@ -115,7 +124,7 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
115
124
  if (raw.tagName && raw.tagName.includes("-")) {
116
125
  const { tagName } = raw;
117
126
  const className = tagNameToClassName(tagName);
118
- const moduleContent = emitElementModule(raw, className, []);
127
+ const moduleContent = emitElementModule(raw, className, [], opts.rewriteSrc);
119
128
  const moduleFile = {
120
129
  content: moduleContent,
121
130
  path: `${tagName}.js`,
@@ -149,7 +158,14 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
149
158
  }
150
159
 
151
160
  // Route 3: Dynamic with standard tagName → pre-rendered HTML + reactive bindings
152
- return compileClient(raw, { litHtmlSrc, projectStyle, reactivitySrc, title });
161
+ return compileClient(raw, {
162
+ litHtmlSrc,
163
+ projectStyle,
164
+ reactivitySrc,
165
+ ...(opts.prePaintScheme === false ? { prePaintScheme: false } : {}),
166
+ ...(opts.rewriteSrc ? { rewriteSrc: opts.rewriteSrc } : {}),
167
+ title,
168
+ });
153
169
  }
154
170
 
155
171
  // ─── CLI ──────────────────────────────────────────────────────────────────────
package/src/shared.ts CHANGED
@@ -6,13 +6,24 @@
6
6
  * compilation targets (static, client, element, server).
7
7
  */
8
8
 
9
- import { RESERVED_KEYS, camelToKebab, toCSSText } from "@jxsuite/runtime";
9
+ import {
10
+ COLOR_SCHEME_ATTR,
11
+ COLOR_SCHEME_STORAGE_KEY,
12
+ RESERVED_KEYS,
13
+ camelToKebab,
14
+ pureSchemeOf,
15
+ schemeSelectors,
16
+ toCSSText,
17
+ } from "@jxsuite/runtime";
10
18
  import { evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
19
+ import { runStatements } from "@jxsuite/runtime/statements";
11
20
  import {
12
21
  childrenContainArray,
22
+ hasStructuredBody,
13
23
  isExpressionDef,
14
24
  isFunctionDef,
15
25
  isMappedArray,
26
+ isNamedFormulaDef,
16
27
  isPrototypeDef,
17
28
  isRef,
18
29
  isSchemaOnlyDef as isSchemaOnly,
@@ -33,8 +44,38 @@ import type {
33
44
  } from "@jxsuite/schema/types";
34
45
 
35
46
  // Re-export runtime utilities used by submodules
36
- export { RESERVED_KEYS, camelToKebab, toCSSText } from "@jxsuite/runtime";
47
+ export {
48
+ COLOR_SCHEME_ATTR,
49
+ COLOR_SCHEME_STORAGE_KEY,
50
+ RESERVED_KEYS,
51
+ camelToKebab,
52
+ pureSchemeOf,
53
+ schemeSelectors,
54
+ toCSSText,
55
+ } from "@jxsuite/runtime";
37
56
  export { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
57
+ export { compileStatements, runStatements } from "@jxsuite/runtime/statements";
58
+
59
+ /**
60
+ * Emit the JS source of a named formula's scope callable: positional arguments are mapped onto the
61
+ * declared parameter names (honoring CemParameter defaults) as the `_args` object the compiled
62
+ * body's `$args/` refs read from. Call sites stay positional, matching the interpreter.
63
+ */
64
+ export function emitFormulaFn(def: { parameters?: unknown[] }, compiledBody: string): string {
65
+ const entries: string[] = [];
66
+ for (const [i, p] of (def.parameters ?? []).entries()) {
67
+ const name = typeof p === "string" ? p : ((p as { name?: string } | null)?.name ?? "");
68
+ if (!name) {
69
+ continue;
70
+ }
71
+ const hasDefault = typeof p === "object" && p !== null && "default" in p;
72
+ const arg = hasDefault
73
+ ? `_a[${i}] === undefined ? ${JSON.stringify((p as { default?: unknown }).default)} : _a[${i}]`
74
+ : `_a[${i}]`;
75
+ entries.push(`${JSON.stringify(name)}: ${arg}`);
76
+ }
77
+ return `(..._a) => { const _args = { ${entries.join(", ")} }; return ${compiledBody}; }`;
78
+ }
38
79
  export type { ExpressionNode } from "@jxsuite/runtime/expression";
39
80
 
40
81
  // CDN defaults
@@ -283,7 +324,24 @@ export function buildInitialScope(
283
324
  }
284
325
  if (isExpressionDef(def)) {
285
326
  const node = def.$expression as ExpressionNode;
286
- if (isMutating(node.operator)) {
327
+ if (isNamedFormulaDef(def)) {
328
+ // Named formula: callable, positional args mapped onto its declared parameters.
329
+ const params = def.parameters as (string | { name?: string; default?: unknown })[];
330
+ setOwnScopeValue(scope, key, (...argValues: unknown[]) => {
331
+ const args: Record<string, unknown> = {};
332
+ for (const [i, p] of params.entries()) {
333
+ const name = typeof p === "string" ? p : (p?.name ?? "");
334
+ if (!name) {
335
+ continue;
336
+ }
337
+ args[name] =
338
+ argValues[i] === undefined && typeof p === "object" && p !== null && "default" in p
339
+ ? p.default
340
+ : argValues[i];
341
+ }
342
+ return evaluateExpression(node, scope, null, { args });
343
+ });
344
+ } else if (isMutating(node.operator)) {
287
345
  setOwnScopeValue(scope, key, (s: Record<string, unknown>, event: Event) =>
288
346
  evaluateExpression(node, s, event),
289
347
  );
@@ -293,12 +351,19 @@ export function buildInitialScope(
293
351
  continue;
294
352
  }
295
353
  if (isFunctionDef(def)) {
296
- if (def.body) {
354
+ if (hasStructuredBody(def)) {
355
+ // Structured body (spec §20): a side-effecting handler in the build-time scope.
356
+ const { body } = def;
357
+ setOwnScopeValue(scope, key, (s: Record<string, unknown>, event?: Event) => {
358
+ void runStatements(body, s ?? scope, event ?? null);
359
+ });
360
+ } else if (typeof def.body === "string") {
297
361
  const names = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
298
- const fn = new Function("state", ...names, def.body) as (
362
+ const { body } = def;
363
+ const fn = new Function("state", ...names, body) as (
299
364
  state: Record<string, unknown>,
300
365
  ) => unknown;
301
- if (def.body.includes("return")) {
366
+ if (body.includes("return")) {
302
367
  defineLazyScopeValue(scope, key, () => fn(scope));
303
368
  } else {
304
369
  setOwnScopeValue(scope, key, fn);
@@ -593,6 +658,94 @@ export function buildInner(
593
658
 
594
659
  // ─── CSS extraction ───────────────────────────────────────────────────────────
595
660
 
661
+ /**
662
+ * The inline pre-paint script injected into <head> when a project declares a pure color-scheme
663
+ * media query: restores the visitor's persisted forced scheme before first paint (no FOUC).
664
+ *
665
+ * @returns {string}
666
+ * @docs framework/concepts/color-schemes
667
+ */
668
+ export function colorSchemePrePaintScript(): string {
669
+ const key = JSON.stringify(COLOR_SCHEME_STORAGE_KEY);
670
+ const attr = JSON.stringify(COLOR_SCHEME_ATTR);
671
+ return `(function(){try{var s=localStorage.getItem(${key});if(s==="light"||s==="dark"){document.documentElement.setAttribute(${attr},s)}}catch(e){}})()`;
672
+ }
673
+
674
+ /**
675
+ * Resolve an `@`-prefixed style key into an emit function that pushes conditional rules. Pure
676
+ * color-scheme queries dual-emit per the forced-scheme contract (spec §9.5): a media-guarded copy
677
+ * that applies while no scheme is forced plus an unconditional copy under the forced root
678
+ * attribute.
679
+ *
680
+ * @param {string} atKey
681
+ * @param {Record<string, string>} mediaQueries
682
+ * @param {string[]} rules
683
+ * @returns {(selector: string, props: string) => void}
684
+ * @docs framework/concepts/color-schemes
685
+ */
686
+ function conditionalRuleEmitter(
687
+ atKey: string,
688
+ mediaQueries: Record<string, string>,
689
+ rules: string[],
690
+ ): (selector: string, props: string) => void {
691
+ const query = atKey.startsWith("@--")
692
+ ? (mediaQueries[atKey.slice(1)] ?? atKey.slice(1))
693
+ : atKey.startsWith("@(")
694
+ ? atKey.slice(1)
695
+ : null;
696
+ const atRule = query === null ? atKey : `@media ${query}`;
697
+ const scheme = query === null ? null : pureSchemeOf(query);
698
+ return (selector: string, props: string) => {
699
+ if (!props) {
700
+ return;
701
+ }
702
+ if (scheme) {
703
+ const { auto, forced } = schemeSelectors(selector, scheme);
704
+ rules.push(`${atRule} { ${auto} { ${props} } }`, `${forced} { ${props} }`);
705
+ } else {
706
+ rules.push(`${atRule} { ${selector} { ${props} } }`);
707
+ }
708
+ };
709
+ }
710
+
711
+ /**
712
+ * Push the rules for one `@`-prefixed style block scoped to `selector`: flat props plus one level
713
+ * of nested selectors, routed through conditionalRuleEmitter (scheme-aware).
714
+ *
715
+ * @param {string[]} rules
716
+ * @param {string} atKey
717
+ * @param {Record<string, string>} mediaQueries
718
+ * @param {string} selector
719
+ * @param {Record<string, unknown>} obj
720
+ */
721
+ function pushConditionalRule(
722
+ rules: string[],
723
+ atKey: string,
724
+ mediaQueries: Record<string, string>,
725
+ selector: string,
726
+ obj: Record<string, unknown>,
727
+ ) {
728
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
729
+ return;
730
+ }
731
+ const emit = conditionalRuleEmitter(atKey, mediaQueries, rules);
732
+ emit(selector, toCSSText(obj));
733
+ for (const [sel, sub] of Object.entries(obj)) {
734
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
735
+ continue;
736
+ }
737
+ if (sel.startsWith("@")) {
738
+ continue;
739
+ }
740
+ const resolved = sel.startsWith("&")
741
+ ? sel.replace("&", selector)
742
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
743
+ ? `${selector}${sel}`
744
+ : `${selector} ${sel}`;
745
+ emit(resolved, toCSSText(sub));
746
+ }
747
+ }
748
+
596
749
  /**
597
750
  * Walk the entire document tree and collect all static nested CSS rules.
598
751
  *
@@ -622,30 +775,7 @@ export function compileStyles(
622
775
  continue;
623
776
  }
624
777
  if (key.startsWith("@")) {
625
- const atRule = key.startsWith("@--")
626
- ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
627
- : key;
628
- const mProps = toCSSText(val);
629
- if (mProps) {
630
- rules.push(`${atRule} { ${selector} { ${mProps} } }`);
631
- }
632
- for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
633
- if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
634
- continue;
635
- }
636
- if (sel.startsWith("@")) {
637
- continue;
638
- }
639
- const resolved = sel.startsWith("&")
640
- ? sel.replace("&", selector)
641
- : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
642
- ? `${selector}${sel}`
643
- : `${selector} ${sel}`;
644
- const subProps = toCSSText(sub);
645
- if (subProps) {
646
- rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
647
- }
648
- }
778
+ pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
649
779
  continue;
650
780
  }
651
781
  const resolved = key.startsWith("&")
@@ -657,27 +787,6 @@ export function compileStyles(
657
787
  }
658
788
  };
659
789
 
660
- for (const [key, val] of Object.entries(projectStyle)) {
661
- if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
662
- emitProjectRules(key, val as Record<string, unknown>);
663
- } else if (
664
- val !== null &&
665
- typeof val === "object" &&
666
- !Array.isArray(val) &&
667
- !key.startsWith("@") &&
668
- !key.startsWith("--")
669
- ) {
670
- emitProjectRules(key, val as Record<string, unknown>);
671
- } else if (key.startsWith("@")) {
672
- // @media block at top level
673
- const atRule = key.startsWith("@--")
674
- ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
675
- : key.startsWith("@(")
676
- ? `@media ${key.slice(1)}`
677
- : key;
678
- rules.push(`${atRule} { body { ${toCSSText(val as Record<string, unknown>)} } }`);
679
- }
680
- }
681
790
  // Collect CSS custom properties into :root {}
682
791
  const rootProps: Record<string, unknown> = {};
683
792
  // Collect direct CSS properties into body {}
@@ -700,6 +809,7 @@ export function compileStyles(
700
809
  bodyProps[key] = val;
701
810
  }
702
811
  }
812
+ // Base rules precede conditional blocks so equal-specificity overrides win by source order.
703
813
  const rootCSS = toCSSText(rootProps);
704
814
  if (rootCSS) {
705
815
  rules.push(`:root { ${rootCSS} }`);
@@ -708,6 +818,62 @@ export function compileStyles(
708
818
  if (bodyCSS) {
709
819
  rules.push(`body { ${bodyCSS} }`);
710
820
  }
821
+
822
+ for (const [key, val] of Object.entries(projectStyle)) {
823
+ if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
824
+ emitProjectRules(key, val as Record<string, unknown>);
825
+ } else if (
826
+ val !== null &&
827
+ typeof val === "object" &&
828
+ !Array.isArray(val) &&
829
+ !key.startsWith("@") &&
830
+ !key.startsWith("--")
831
+ ) {
832
+ emitProjectRules(key, val as Record<string, unknown>);
833
+ } else if (
834
+ key.startsWith("@") &&
835
+ val !== null &&
836
+ typeof val === "object" &&
837
+ !Array.isArray(val)
838
+ ) {
839
+ // Conditional block at project top level: custom properties override :root, direct
840
+ // Properties override body, selector-keyed sub-objects their own selector.
841
+ const emit = conditionalRuleEmitter(key, mediaQueries, rules);
842
+ const condRoot: Record<string, unknown> = {};
843
+ const condBody: Record<string, unknown> = {};
844
+ const condSubs: [string, Record<string, unknown>][] = [];
845
+ for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
846
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
847
+ if (!k.startsWith("@")) {
848
+ condSubs.push([k, v as Record<string, unknown>]);
849
+ }
850
+ continue;
851
+ }
852
+ if (k.startsWith("--")) {
853
+ condRoot[k] = v;
854
+ } else {
855
+ condBody[k] = v;
856
+ }
857
+ }
858
+ emit(":root", toCSSText(condRoot));
859
+ emit("body", toCSSText(condBody));
860
+ for (const [sel, sub] of condSubs) {
861
+ emit(sel, toCSSText(sub));
862
+ }
863
+ }
864
+ }
865
+ }
866
+
867
+ // Forced-scheme UA hint: native widgets follow the forced attribute, not only the OS scheme.
868
+ if (
869
+ Object.values(mediaQueries).some((q) => pureSchemeOf(q) !== null) &&
870
+ !(projectStyle && typeof projectStyle === "object" && "colorScheme" in projectStyle)
871
+ ) {
872
+ rules.push(
873
+ ":root { color-scheme: light dark }",
874
+ `:root:where([${COLOR_SCHEME_ATTR}="light"]) { color-scheme: light }`,
875
+ `:root:where([${COLOR_SCHEME_ATTR}="dark"]) { color-scheme: dark }`,
876
+ );
711
877
  }
712
878
 
713
879
  const counter = { n: 0 };
@@ -741,32 +907,7 @@ function emitNestedElement(
741
907
  continue;
742
908
  }
743
909
  if (key.startsWith("@")) {
744
- const atRule = key.startsWith("@--")
745
- ? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
746
- : key.startsWith("@(")
747
- ? `@media ${key.slice(1)}`
748
- : key;
749
- const mProps = toCSSText(val);
750
- if (mProps) {
751
- rules.push(`${atRule} { ${selector} { ${mProps} } }`);
752
- }
753
- for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
754
- if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
755
- continue;
756
- }
757
- if (sel.startsWith("@")) {
758
- continue;
759
- }
760
- const resolved = sel.startsWith("&")
761
- ? sel.replace("&", selector)
762
- : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
763
- ? `${selector}${sel}`
764
- : `${selector} ${sel}`;
765
- const subProps = toCSSText(sub);
766
- if (subProps) {
767
- rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
768
- }
769
- }
910
+ pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
770
911
  continue;
771
912
  }
772
913
  const resolved = key.startsWith("&")
@@ -838,35 +979,7 @@ export function collectStyles(
838
979
  continue;
839
980
  }
840
981
  if (prop.startsWith("@")) {
841
- const atRule = prop.startsWith("@--")
842
- ? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
843
- : prop.startsWith("@(")
844
- ? `@media ${prop.slice(1)}`
845
- : prop;
846
- const atBaseProps = toCSSText(val);
847
- if (atBaseProps) {
848
- rules.push(`${atRule} { ${selector} { ${atBaseProps} } }`);
849
- }
850
- for (const [sel, nestedRules] of Object.entries(
851
- /** @type {Record<string, unknown>} */ val,
852
- )) {
853
- if (
854
- nestedRules === null ||
855
- typeof nestedRules !== "object" ||
856
- Array.isArray(nestedRules)
857
- ) {
858
- continue;
859
- }
860
- if (sel.startsWith("@")) {
861
- continue;
862
- }
863
- const resolved = sel.startsWith("&")
864
- ? sel.replace("&", selector)
865
- : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
866
- ? `${selector}${sel}`
867
- : `${selector} ${sel}`;
868
- rules.push(`${atRule} { ${resolved} { ${toCSSText(nestedRules)} } }`);
869
- }
982
+ pushConditionalRule(rules, prop, mediaQueries, selector, val as Record<string, unknown>);
870
983
  } else {
871
984
  const resolved = prop.startsWith("&")
872
985
  ? prop.replace("&", selector)
@@ -1276,12 +1389,7 @@ export function buildComponentCSS(
1276
1389
 
1277
1390
  for (const [prop, val] of Object.entries(styleDef)) {
1278
1391
  if (prop.startsWith("@")) {
1279
- const atRule = prop.startsWith("@--")
1280
- ? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
1281
- : prop.startsWith("@(")
1282
- ? `@media ${prop.slice(1)}`
1283
- : prop;
1284
- rules.push(`${atRule} { ${tagName} { ${toCSSText(val as Record<string, unknown>)} } }`);
1392
+ pushConditionalRule(rules, prop, mediaQueries, tagName, val as Record<string, unknown>);
1285
1393
  } else if (
1286
1394
  prop.startsWith(":") ||
1287
1395
  prop.startsWith(".") ||