@aihu/css-engine 0.2.5 → 0.4.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.
Files changed (52) hide show
  1. package/README.md +27 -22
  2. package/crates/aihu-css-core/src/apply.rs +314 -0
  3. package/crates/aihu-css-core/src/bin/main.rs +8 -7
  4. package/crates/aihu-css-core/src/cache.rs +8 -5
  5. package/crates/aihu-css-core/src/emit.rs +195 -36
  6. package/crates/aihu-css-core/src/lib.rs +15 -2
  7. package/crates/aihu-css-core/src/palette.rs +301 -0
  8. package/crates/aihu-css-core/src/style_parser.rs +587 -0
  9. package/crates/aihu-css-core/src/theme.rs +14 -0
  10. package/crates/aihu-css-core/src/tokens.rs +1196 -29
  11. package/crates/aihu-css-core/src/variants.rs +251 -3
  12. package/crates/aihu-css-core/tests/apply.rs +203 -0
  13. package/crates/aihu-css-core/tests/apply_regression.rs +150 -0
  14. package/crates/aihu-css-core/tests/binary_error.rs +61 -0
  15. package/crates/aihu-css-core/tests/cache.rs +8 -8
  16. package/crates/aihu-css-core/tests/emit.rs +284 -17
  17. package/crates/aihu-css-core/tests/parity.rs +274 -0
  18. package/crates/aihu-css-core/tests/progressive_snapshot.rs +8 -8
  19. package/crates/aihu-css-core/tests/scoped_snapshot.rs +80 -8
  20. package/crates/aihu-css-core/tests/snapshots/apply__apply_inside_nested_rule.snap +11 -0
  21. package/crates/aihu-css-core/tests/snapshots/apply__arbitrary_value_utility_in_apply.snap +8 -0
  22. package/crates/aihu-css-core/tests/snapshots/apply__arbitrary_value_variant_in_apply.snap +10 -0
  23. package/crates/aihu-css-core/tests/snapshots/apply__base_utility_inlines_declarations.snap +9 -0
  24. package/crates/aihu-css-core/tests/snapshots/apply__dark_variant_cascade_in_apply.snap +10 -0
  25. package/crates/aihu-css-core/tests/snapshots/apply__data_attribute_variant.snap +10 -0
  26. package/crates/aihu-css-core/tests/snapshots/apply__multi_token_apply.snap +11 -0
  27. package/crates/aihu-css-core/tests/snapshots/apply__multiple_apply_directives_per_rule.snap +12 -0
  28. package/crates/aihu-css-core/tests/snapshots/apply__responsive_variant_wraps_media.snap +12 -0
  29. package/crates/aihu-css-core/tests/snapshots/apply__single_variant_lifts_to_nested_rule.snap +10 -0
  30. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__anchor_snapshot.snap +1 -0
  31. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__popover_snapshot.snap +1 -0
  32. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__text_balance_snapshot.snap +1 -0
  33. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap +1 -0
  34. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_animate_spin_hoists_keyframes.snap +25 -0
  35. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_divide_y_nested_rule.snap +24 -0
  36. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_output_for_sfc.snap +1 -0
  37. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_space_y_nested_rule.snap +24 -0
  38. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_transition_and_transform.snap +26 -0
  39. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap +6 -2
  40. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap +5 -2
  41. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__standard_variants.snap +1 -0
  42. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__theme_default_vs_override.snap +2 -0
  43. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__wc_native_variants.snap +1 -0
  44. package/crates/aihu-css-core/tests/style_parser.rs +257 -0
  45. package/crates/aihu-css-core/tests/tokens.rs +526 -7
  46. package/dist/index.d.ts +0 -9
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +26 -18
  49. package/dist/index.js.map +1 -1
  50. package/dist/runtime/cn.js +13 -0
  51. package/dist/runtime/cn.js.map +1 -1
  52. package/package.json +6 -6
@@ -0,0 +1,274 @@
1
+ //! Tailwind-v4 parity round: coverage for the utility families, the
2
+ //! `(--var)` shorthand, the arbitrary border-color typing fix, palette-token
3
+ //! injection, and the pseudo-class/element variants added to render a
4
+ //! full Tailwind-authored landing page on the engine.
5
+
6
+ use aihu_css_core::{compile_classes, compile_sfc_scoped, parse_ast};
7
+
8
+ fn css(classes: &[&str]) -> String {
9
+ compile_classes(&classes.iter().map(|s| s.to_string()).collect::<Vec<_>>())
10
+ }
11
+
12
+ fn one(class: &str) -> String {
13
+ compile_classes(&[class.to_string()])
14
+ }
15
+
16
+ fn sfc(classes: &str) -> aihu_css_core::SfcAst {
17
+ let json = format!(
18
+ r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
19
+ "template":[{{"kind":"element","tag":"div","attrs":[
20
+ {{"kind":"static","name":"class","value":"{classes}"}}
21
+ ],"children":[]}}]}}"#
22
+ );
23
+ parse_ast(&json).unwrap()
24
+ }
25
+
26
+ // ── The root-cause fix: arbitrary border value typed as color, not width ─────
27
+
28
+ #[test]
29
+ fn arbitrary_border_value_is_color_typed_by_value() {
30
+ // A color-looking value → border-color (previously always border-width).
31
+ assert_eq!(
32
+ one("border-[var(--border)]"),
33
+ ".border-[var(--border)] { border-color: var(--border); }\n"
34
+ );
35
+ assert_eq!(
36
+ one("border-[#abc]"),
37
+ ".border-[#abc] { border-color: #abc; }\n"
38
+ );
39
+ // A length value still maps to width.
40
+ assert_eq!(
41
+ one("border-[2px]"),
42
+ ".border-[2px] { border-width: 2px; }\n"
43
+ );
44
+ // Explicit data-type hints override the heuristic.
45
+ assert_eq!(
46
+ one("outline-[color:var(--x)]"),
47
+ ".outline-[color:var(--x)] { outline-color: var(--x); }\n"
48
+ );
49
+ }
50
+
51
+ // ── The CSS-variable shorthand `prefix-(--token)` ───────────────────────────
52
+
53
+ #[test]
54
+ fn var_shorthand_keeps_prefix_property_typing() {
55
+ assert_eq!(
56
+ one("bg-(--background)"),
57
+ ".bg-(--background) { background-color: var(--background); }\n"
58
+ );
59
+ assert_eq!(
60
+ one("text-(--muted-fg)"),
61
+ ".text-(--muted-fg) { color: var(--muted-fg); }\n"
62
+ );
63
+ // border keeps COLOR typing (not width) through the shorthand.
64
+ assert_eq!(
65
+ one("border-(--border)"),
66
+ ".border-(--border) { border-color: var(--border); }\n"
67
+ );
68
+ }
69
+
70
+ #[test]
71
+ fn var_shorthand_opacity_modifier_color_mixes() {
72
+ assert_eq!(
73
+ one("bg-(--background)/90"),
74
+ ".bg-(--background)/90 { background-color: color-mix(in oklab, var(--background) 90%, transparent); }\n"
75
+ );
76
+ }
77
+
78
+ // ── Sizing / aspect / order / fractions ─────────────────────────────────────
79
+
80
+ #[test]
81
+ fn size_emits_width_and_height() {
82
+ assert_eq!(one("size-16"), ".size-16 { width: 4rem; height: 4rem; }\n");
83
+ assert_eq!(
84
+ one("size-full"),
85
+ ".size-full { width: 100%; height: 100%; }\n"
86
+ );
87
+ }
88
+
89
+ #[test]
90
+ fn aspect_ratio_keywords_and_bare_ratio() {
91
+ assert_eq!(
92
+ one("aspect-square"),
93
+ ".aspect-square { aspect-ratio: 1 / 1; }\n"
94
+ );
95
+ assert_eq!(
96
+ one("aspect-video"),
97
+ ".aspect-video { aspect-ratio: 16 / 9; }\n"
98
+ );
99
+ assert_eq!(
100
+ one("aspect-1108/632"),
101
+ ".aspect-1108/632 { aspect-ratio: 1108 / 632; }\n"
102
+ );
103
+ }
104
+
105
+ #[test]
106
+ fn fractional_position_and_negative_translate() {
107
+ assert_eq!(one("left-1/2"), ".left-1/2 { left: 50%; }\n");
108
+ assert_eq!(one("top-1/2"), ".top-1/2 { top: 50%; }\n");
109
+ assert_eq!(
110
+ one("-translate-x-1/2"),
111
+ ".-translate-x-1/2 { transform: translateX(-50%); }\n"
112
+ );
113
+ }
114
+
115
+ #[test]
116
+ fn negative_margin_and_z_index() {
117
+ assert_eq!(one("-ml-4"), ".-ml-4 { margin-left: -1rem; }\n");
118
+ assert_eq!(one("-z-10"), ".-z-10 { z-index: -10; }\n");
119
+ assert_eq!(one("order-2"), ".order-2 { order: 2; }\n");
120
+ }
121
+
122
+ // ── Gradients ───────────────────────────────────────────────────────────────
123
+
124
+ #[test]
125
+ fn linear_gradient_direction_and_stops() {
126
+ assert_eq!(
127
+ one("bg-linear-to-r"),
128
+ ".bg-linear-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }\n"
129
+ );
130
+ assert!(one("from-amber-200").contains("--tw-gradient-from: var(--color-amber-200)"));
131
+ assert!(one("to-amber-600").contains("--tw-gradient-to: var(--color-amber-600)"));
132
+ assert!(one("via-(--accent)").contains("var(--accent)"));
133
+ }
134
+
135
+ // ── Effects / typography long-tail ──────────────────────────────────────────
136
+
137
+ #[test]
138
+ fn blur_mask_shadow_outline_scales() {
139
+ assert_eq!(one("blur-3xl"), ".blur-3xl { filter: blur(64px); }\n");
140
+ assert!(one("backdrop-blur-sm").contains("backdrop-filter: blur(8px)"));
141
+ assert!(one("mask-[radial-gradient(circle,white,transparent)]")
142
+ .contains("mask-image: radial-gradient(circle,white,transparent)"));
143
+ assert!(one("shadow-xl").contains("box-shadow:"));
144
+ assert_eq!(
145
+ one("outline-2"),
146
+ ".outline-2 { outline-style: solid; outline-width: 2px; }\n"
147
+ );
148
+ assert_eq!(
149
+ one("-outline-offset-1"),
150
+ ".-outline-offset-1 { outline-offset: -1px; }\n"
151
+ );
152
+ }
153
+
154
+ #[test]
155
+ fn type_scale_slash_line_height_and_family() {
156
+ assert_eq!(
157
+ one("text-7xl"),
158
+ ".text-7xl { font-size: 4.5rem; line-height: 1; }\n"
159
+ );
160
+ assert_eq!(
161
+ one("text-sm/6"),
162
+ ".text-sm/6 { font-size: 0.875rem; line-height: 1.5rem; }\n"
163
+ );
164
+ assert_eq!(
165
+ one("font-serif"),
166
+ ".font-serif { font-family: var(--font-serif); }\n"
167
+ );
168
+ assert_eq!(one("text-pretty"), ".text-pretty { text-wrap: pretty; }\n");
169
+ }
170
+
171
+ #[test]
172
+ fn long_tail_fixed_utilities() {
173
+ assert_eq!(one("isolate"), ".isolate { isolation: isolate; }\n");
174
+ assert_eq!(
175
+ one("cursor-pointer"),
176
+ ".cursor-pointer { cursor: pointer; }\n"
177
+ );
178
+ assert_eq!(
179
+ one("rounded-3xl"),
180
+ ".rounded-3xl { border-radius: 1.5rem; }\n"
181
+ );
182
+ assert_eq!(one("list-none"), ".list-none { list-style-type: none; }\n");
183
+ assert_eq!(one("shrink-0"), ".shrink-0 { flex-shrink: 0; }\n");
184
+ assert_eq!(
185
+ one("self-start"),
186
+ ".self-start { align-self: flex-start; }\n"
187
+ );
188
+ assert!(one("sr-only").contains("position: absolute"));
189
+ }
190
+
191
+ // ── Palette injection: referenced palette tokens resolve at :host ────────────
192
+
193
+ #[test]
194
+ fn scoped_injects_used_palette_tokens() {
195
+ let out = compile_sfc_scoped(&sfc("bg-amber-500 text-stone-300")).unwrap();
196
+ // The utility refs the palette token…
197
+ assert!(out.contains("background-color: var(--color-amber-500)"));
198
+ // …and the scoped emitter registers its oklch value at :host.
199
+ assert!(out.contains("--color-amber-500: oklch("));
200
+ assert!(out.contains("--color-stone-300: oklch("));
201
+ // Only USED palette tokens are injected — an unrelated family is absent.
202
+ assert!(!out.contains("--color-rose-500"));
203
+ }
204
+
205
+ // ── Variants: pseudo-classes, pseudo-elements, relational open ──────────────
206
+
207
+ #[test]
208
+ fn pseudo_class_variants_first_last_open() {
209
+ let out = compile_sfc_scoped(&sfc("first:mt-0 last:mb-0 open:block")).unwrap();
210
+ assert!(out.contains(":first-child"));
211
+ assert!(out.contains(":last-child"));
212
+ assert!(out.contains(":open"));
213
+ }
214
+
215
+ #[test]
216
+ fn pseudo_element_variants_marker_and_placeholder() {
217
+ let out = compile_sfc_scoped(&sfc("marker:text-primary placeholder:text-stone-400")).unwrap();
218
+ assert!(out.contains("::marker"));
219
+ assert!(out.contains("::placeholder"));
220
+ }
221
+
222
+ #[test]
223
+ fn group_open_relational_variant() {
224
+ let out = compile_sfc_scoped(&sfc("group-open:rotate-180")).unwrap();
225
+ assert!(out.contains(".group:open"));
226
+ }
227
+
228
+ // ── A representative slice of the real landing compiles end to end ───────────
229
+
230
+ #[test]
231
+ fn landing_utility_slice_all_emit() {
232
+ // Base (non-variant) utilities only — flat `compile_classes` does not emit
233
+ // variant-prefixed classes (those are exercised by the variant tests above).
234
+ let slice = [
235
+ "relative",
236
+ "isolate",
237
+ "overflow-hidden",
238
+ "bg-(--background)",
239
+ "size-full",
240
+ "mask-[radial-gradient(100%_100%_at_top_right,white,transparent)]",
241
+ "stroke-(--border)",
242
+ "fill-(--muted)",
243
+ "aspect-1108/632",
244
+ "bg-linear-to-r",
245
+ "from-amber-200",
246
+ "to-amber-600",
247
+ "opacity-20",
248
+ "max-w-7xl",
249
+ "px-6",
250
+ "text-7xl",
251
+ "font-serif",
252
+ "text-pretty",
253
+ "text-(--foreground)",
254
+ "text-lg/8",
255
+ "rounded-md",
256
+ "bg-(--primary)",
257
+ "text-(--primary-fg)",
258
+ "shadow-xs",
259
+ "size-3",
260
+ "rounded-full",
261
+ "bg-red-400",
262
+ "ring-1",
263
+ "ring-gray-900/10",
264
+ ];
265
+ let out = css(&slice);
266
+ // Every class in the slice must produce a rule (no silently-dropped utility).
267
+ for class in slice {
268
+ let sel = format!(".{class} {{");
269
+ assert!(
270
+ out.contains(&sel),
271
+ "landing utility `{class}` produced no rule"
272
+ );
273
+ }
274
+ }
@@ -22,7 +22,7 @@ fn sfc(classes: &str) -> aihu_css_core::SfcAst {
22
22
 
23
23
  #[test]
24
24
  fn view_transition_is_supports_gated_css_only() {
25
- let css = compile_sfc_scoped(&sfc("view-transition:hero"));
25
+ let css = compile_sfc_scoped(&sfc("view-transition:hero")).unwrap();
26
26
  assert!(
27
27
  css.contains("@supports (view-transition-name: none)"),
28
28
  "view-transition gated behind @supports: {css}"
@@ -36,14 +36,14 @@ fn view_transition_is_supports_gated_css_only() {
36
36
 
37
37
  #[test]
38
38
  fn view_transition_snapshot() {
39
- insta::assert_snapshot!(compile_sfc_scoped(&sfc("view-transition:hero")));
39
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("view-transition:hero")).unwrap());
40
40
  }
41
41
 
42
42
  // ── Task 6: anchor: (@supports gate + JS fallback marker) ────────────────────
43
43
 
44
44
  #[test]
45
45
  fn anchor_is_supports_gated_with_js_marker() {
46
- let css = compile_sfc_scoped(&sfc("anchor:tooltip"));
46
+ let css = compile_sfc_scoped(&sfc("anchor:tooltip")).unwrap();
47
47
  assert!(
48
48
  css.contains("@supports (anchor-name: --a)"),
49
49
  "anchor gated behind @supports (anchor-name): {css}"
@@ -57,14 +57,14 @@ fn anchor_is_supports_gated_with_js_marker() {
57
57
 
58
58
  #[test]
59
59
  fn anchor_snapshot() {
60
- insta::assert_snapshot!(compile_sfc_scoped(&sfc("anchor:tooltip")));
60
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("anchor:tooltip")).unwrap());
61
61
  }
62
62
 
63
63
  // ── Task 7: popover: (@supports gate + portal JS fallback marker) ────────────
64
64
 
65
65
  #[test]
66
66
  fn popover_is_supports_gated_with_js_marker() {
67
- let css = compile_sfc_scoped(&sfc("popover:menu"));
67
+ let css = compile_sfc_scoped(&sfc("popover:menu")).unwrap();
68
68
  assert!(
69
69
  css.contains("@supports (selector(:popover-open))"),
70
70
  "popover gated behind @supports selector(:popover-open): {css}"
@@ -77,14 +77,14 @@ fn popover_is_supports_gated_with_js_marker() {
77
77
 
78
78
  #[test]
79
79
  fn popover_snapshot() {
80
- insta::assert_snapshot!(compile_sfc_scoped(&sfc("popover:menu")));
80
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("popover:menu")).unwrap());
81
81
  }
82
82
 
83
83
  // ── Task 8: text-balance: (no gate, no JS) ───────────────────────────────────
84
84
 
85
85
  #[test]
86
86
  fn text_balance_no_gate_no_js() {
87
- let css = compile_sfc_scoped(&sfc("text-balance:"));
87
+ let css = compile_sfc_scoped(&sfc("text-balance:")).unwrap();
88
88
  assert!(css.contains("text-wrap: balance"), "emits text-wrap: balance: {css}");
89
89
  assert!(
90
90
  !css.contains("@supports"),
@@ -98,5 +98,5 @@ fn text_balance_no_gate_no_js() {
98
98
 
99
99
  #[test]
100
100
  fn text_balance_snapshot() {
101
- insta::assert_snapshot!(compile_sfc_scoped(&sfc("text-balance:")));
101
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("text-balance:")).unwrap());
102
102
  }
@@ -25,7 +25,7 @@ fn flat_output_for_class_list() {
25
25
 
26
26
  #[test]
27
27
  fn scoped_output_for_sfc() {
28
- insta::assert_snapshot!(compile_sfc_scoped(&sfc("bg-primary p-4 rounded-lg")));
28
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("bg-primary p-4 rounded-lg")).unwrap());
29
29
  }
30
30
 
31
31
  #[test]
@@ -35,7 +35,7 @@ fn scoped_with_authored_style_block() {
35
35
  "meta":{"name":"Card"},
36
36
  "template":[{"kind":"element","tag":"div","attrs":[
37
37
  {"kind":"static","name":"class","value":"p-4 shadow-md"}],"children":[]}]}"#;
38
- insta::assert_snapshot!(compile_sfc_scoped(&ast(json)));
38
+ insta::assert_snapshot!(compile_sfc_scoped(&ast(json)).unwrap());
39
39
  }
40
40
 
41
41
  #[test]
@@ -43,31 +43,103 @@ fn scoped_with_global_style_block() {
43
43
  let json = r#"{"tag":"X","astVersion":1,
44
44
  "style":{"content":"body { margin: 0; }","scope":"global"},
45
45
  "meta":{"name":"X"},"template":null}"#;
46
- insta::assert_snapshot!(compile_sfc_scoped(&ast(json)));
46
+ insta::assert_snapshot!(compile_sfc_scoped(&ast(json)).unwrap());
47
+ }
48
+
49
+ #[test]
50
+ fn scoped_space_y_nested_rule() {
51
+ // Locks in the nested `& > * + *` sibling-margin shape inside component
52
+ // scope (Round 1: tailwind-support `space-x/y-*` family).
53
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("space-y-4")).unwrap());
54
+ }
55
+
56
+ #[test]
57
+ fn scoped_divide_y_nested_rule() {
58
+ // Locks in the nested `& > * + *` sibling-border shape inside component
59
+ // scope (Round 2: tailwind-support `divide-x/y-*` family). Confirms the
60
+ // nested rule survives the scoped CSS-nesting emission path, mirroring
61
+ // `scoped_space_y_nested_rule`.
62
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("divide-y-2")).unwrap());
63
+ }
64
+
65
+ #[test]
66
+ fn scoped_animate_spin_hoists_keyframes() {
67
+ // Locks the `animate-*` emission shape: the scoped `.animate-spin` rule
68
+ // followed by a hoisted top-level `@keyframes spin` sibling (Round 2:
69
+ // tailwind-support `motion` track).
70
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("animate-spin")).unwrap());
71
+ }
72
+
73
+ #[test]
74
+ fn scoped_transition_and_transform() {
75
+ // Locks transition shorthand + a transform utility under component scope.
76
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc(
77
+ "transition-transform duration-300 hover:scale-105"
78
+ )).unwrap());
47
79
  }
48
80
 
49
81
  #[test]
50
82
  fn wc_native_variants() {
51
83
  insta::assert_snapshot!(compile_sfc_scoped(&sfc(
52
84
  "host:bg-primary slotted:p-4 slotted-img:rounded-lg part-thumb:bg-accent host-context-dark:bg-surface"
53
- )));
85
+ )).unwrap());
54
86
  }
55
87
 
56
88
  #[test]
57
89
  fn standard_variants() {
58
90
  insta::assert_snapshot!(compile_sfc_scoped(&sfc(
59
91
  "hover:bg-primary focus:text-accent dark:bg-surface md:p-8 [&>div]:text-primary md:hover:bg-primary"
60
- )));
92
+ )).unwrap());
93
+ }
94
+
95
+ #[test]
96
+ fn style_block_does_not_suppress_scanned_utilities() {
97
+ // Regression for #278: an `@style` block must NOT make the utility-class
98
+ // scanner mutually exclusive. Both the scanned template utilities AND the
99
+ // authored `@style` content must land in the same scoped stylesheet.
100
+ //
101
+ // Repro: `<div class="text-3xl gap-4">` + `@style { .__probe__ { ... } }`.
102
+ // Before the fix the sheet contained ONLY `.__probe__`; the utilities
103
+ // vanished. Assert all three rules coexist (concatenated).
104
+ let json = r#"{"tag":"Probe","astVersion":1,
105
+ "style":{"content":".__probe__ { color: rgb(1,2,3); }","scope":"scoped"},
106
+ "meta":{"name":"Probe"},
107
+ "template":[{"kind":"element","tag":"div","attrs":[
108
+ {"kind":"static","name":"class","value":"text-3xl gap-4"}],"children":[]}]}"#;
109
+ // `.unwrap()` added in the PR-1 merge: compile_sfc_scoped now returns Result.
110
+ let css = compile_sfc_scoped(&ast(json)).unwrap();
111
+
112
+ // Scanned template utilities survive.
113
+ assert!(
114
+ css.contains(".text-3xl"),
115
+ "scanned utility .text-3xl missing when @style present:\n{css}"
116
+ );
117
+ assert!(
118
+ css.contains(".gap-4"),
119
+ "scanned utility .gap-4 missing when @style present:\n{css}"
120
+ );
121
+ // Authored @style content survives. (PR-1 merge: authored @style is now
122
+ // re-rendered through the shared @style parser to enable @apply, so it is
123
+ // whitespace-normalized rather than byte-identical — assert the selector
124
+ // and declaration are present, not the exact single-line form. The #278
125
+ // regression intent (scanned utilities AND authored @style coexist) holds.)
126
+ assert!(css.contains(".__probe__"), "authored @style selector missing:\n{css}");
127
+ assert!(
128
+ css.contains("color: rgb(1,2,3)"),
129
+ "authored @style declaration missing:\n{css}"
130
+ );
61
131
  }
62
132
 
63
133
  #[test]
64
134
  fn theme_default_vs_override() {
65
- let default = compile_sfc_scoped(&sfc("bg-primary"));
135
+ let default = compile_sfc_scoped(&sfc("bg-primary")).unwrap();
66
136
  let json = r#"{"tag":"X","astVersion":1,
67
137
  "style":{"content":"@theme { --color-primary: oklch(0.55 0.18 28); }","scope":"scoped"},
68
138
  "meta":{"name":"X"},
69
139
  "template":[{"kind":"element","tag":"div","attrs":[
70
140
  {"kind":"static","name":"class","value":"bg-primary"}],"children":[]}]}"#;
71
- let overridden = compile_sfc_scoped(&ast(json));
72
- insta::assert_snapshot!(format!("--- default ---\n{default}\n--- override ---\n{overridden}"));
141
+ let overridden = compile_sfc_scoped(&ast(json)).unwrap();
142
+ insta::assert_snapshot!(format!(
143
+ "--- default ---\n{default}\n--- override ---\n{overridden}"
144
+ ));
73
145
  }
@@ -0,0 +1,11 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 91
4
+ expression: css
5
+ ---
6
+ .card {
7
+ display: grid;
8
+ & .title {
9
+ font-weight: 500;
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 103
4
+ expression: css
5
+ ---
6
+ .swatch {
7
+ background-color: #fff;
8
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 111
4
+ expression: css
5
+ ---
6
+ .swatch {
7
+ &:hover {
8
+ background-color: #fff;
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 39
4
+ expression: css
5
+ ---
6
+ .btn {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 143
4
+ expression: css
5
+ ---
6
+ .panel {
7
+ :host([data-theme="dark"]) &, :root.dark & {
8
+ background-color: var(--color-surface);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 131
4
+ expression: css
5
+ ---
6
+ .btn {
7
+ &[data-state="open"] {
8
+ background-color: var(--color-accent);
9
+ }
10
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 66
4
+ expression: css
5
+ ---
6
+ .btn {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ border-radius: 0.375rem;
11
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 78
4
+ expression: css
5
+ ---
6
+ .btn {
7
+ display: inline-flex;
8
+ border-radius: 0.375rem;
9
+ &:hover {
10
+ background-color: var(--color-accent);
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 120
4
+ expression: css
5
+ ---
6
+ .grid {
7
+ @media (min-width: 48rem) {
8
+ & {
9
+ display: flex;
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/apply.rs
3
+ assertion_line: 54
4
+ expression: css
5
+ ---
6
+ .btn {
7
+ &:hover {
8
+ background-color: var(--color-accent);
9
+ }
10
+ }
@@ -20,6 +20,7 @@ expression: "compile_sfc_scoped(&sfc(\"anchor:tooltip\"))"
20
20
  --color-surface: #faf8f4;
21
21
  --color-surface-foreground: #1a1d24;
22
22
  }
23
+ *, ::before, ::after { border-style: solid; border-width: 0; }
23
24
  @supports (anchor-name: --a) {
24
25
  .anchor\:tooltip { anchor-name: --tooltip; position-anchor: --tooltip; }
25
26
  }
@@ -20,6 +20,7 @@ expression: "compile_sfc_scoped(&sfc(\"popover:menu\"))"
20
20
  --color-surface: #faf8f4;
21
21
  --color-surface-foreground: #1a1d24;
22
22
  }
23
+ *, ::before, ::after { border-style: solid; border-width: 0; }
23
24
  @supports (selector(:popover-open)) {
24
25
  .popover\:menu:popover-open { position: fixed; margin: 0; inset: auto; }
25
26
  }
@@ -20,4 +20,5 @@ expression: "compile_sfc_scoped(&sfc(\"text-balance:\"))"
20
20
  --color-surface: #faf8f4;
21
21
  --color-surface-foreground: #1a1d24;
22
22
  }
23
+ *, ::before, ::after { border-style: solid; border-width: 0; }
23
24
  .text-balance { text-wrap: balance; }
@@ -20,6 +20,7 @@ expression: "compile_sfc_scoped(&sfc(\"view-transition:hero\"))"
20
20
  --color-surface: #faf8f4;
21
21
  --color-surface-foreground: #1a1d24;
22
22
  }
23
+ *, ::before, ::after { border-style: solid; border-width: 0; }
23
24
  @supports (view-transition-name: none) {
24
25
  .view-transition\:hero { view-transition-name: hero; }
25
26
  }
@@ -0,0 +1,25 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"animate-spin\"))"
4
+ ---
5
+ :host {
6
+ --color-accent: #c8543a;
7
+ --color-accent-foreground: #faf8f4;
8
+ --color-background: #faf8f4;
9
+ --color-border: #ddd9d2;
10
+ --color-destructive: #a8432b;
11
+ --color-destructive-foreground: #faf8f4;
12
+ --color-foreground: #1a1d24;
13
+ --color-muted: #5a5a55;
14
+ --color-muted-foreground: #8a8880;
15
+ --color-primary: #1a1d24;
16
+ --color-primary-foreground: #faf8f4;
17
+ --color-ring: #c8543a;
18
+ --color-secondary: #5a5a55;
19
+ --color-secondary-foreground: #faf8f4;
20
+ --color-surface: #faf8f4;
21
+ --color-surface-foreground: #1a1d24;
22
+ }
23
+ *, ::before, ::after { border-style: solid; border-width: 0; }
24
+ .animate-spin { animation: spin 1s linear infinite; }
25
+ @keyframes spin { to { transform: rotate(360deg); } }
@@ -0,0 +1,24 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"divide-y-2\"))"
4
+ ---
5
+ :host {
6
+ --color-accent: #c8543a;
7
+ --color-accent-foreground: #faf8f4;
8
+ --color-background: #faf8f4;
9
+ --color-border: #ddd9d2;
10
+ --color-destructive: #a8432b;
11
+ --color-destructive-foreground: #faf8f4;
12
+ --color-foreground: #1a1d24;
13
+ --color-muted: #5a5a55;
14
+ --color-muted-foreground: #8a8880;
15
+ --color-primary: #1a1d24;
16
+ --color-primary-foreground: #faf8f4;
17
+ --color-ring: #c8543a;
18
+ --color-secondary: #5a5a55;
19
+ --color-secondary-foreground: #faf8f4;
20
+ --color-surface: #faf8f4;
21
+ --color-surface-foreground: #1a1d24;
22
+ }
23
+ *, ::before, ::after { border-style: solid; border-width: 0; }
24
+ .divide-y-2 { & > * + * { border-block-width: 2px; } }