@aihu/css-engine 0.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/crates/aihu-css-core/Cargo.toml +22 -0
  4. package/crates/aihu-css-core/src/ast.rs +173 -0
  5. package/crates/aihu-css-core/src/bin/main.rs +73 -0
  6. package/crates/aihu-css-core/src/cache.rs +182 -0
  7. package/crates/aihu-css-core/src/emit.rs +236 -0
  8. package/crates/aihu-css-core/src/features/anchor.rs +41 -0
  9. package/crates/aihu-css-core/src/features/mod.rs +33 -0
  10. package/crates/aihu-css-core/src/features/popover.rs +40 -0
  11. package/crates/aihu-css-core/src/features/text_balance.rs +36 -0
  12. package/crates/aihu-css-core/src/features/view_transition.rs +38 -0
  13. package/crates/aihu-css-core/src/lib.rs +67 -0
  14. package/crates/aihu-css-core/src/progressive.rs +200 -0
  15. package/crates/aihu-css-core/src/scanner.rs +235 -0
  16. package/crates/aihu-css-core/src/theme.rs +179 -0
  17. package/crates/aihu-css-core/src/tokens.rs +470 -0
  18. package/crates/aihu-css-core/src/variants.rs +124 -0
  19. package/crates/aihu-css-core/tests/cache.rs +71 -0
  20. package/crates/aihu-css-core/tests/emit.rs +148 -0
  21. package/crates/aihu-css-core/tests/fixtures/button.ast.json +19 -0
  22. package/crates/aihu-css-core/tests/progressive_snapshot.rs +102 -0
  23. package/crates/aihu-css-core/tests/scanner.rs +99 -0
  24. package/crates/aihu-css-core/tests/scoped_snapshot.rs +73 -0
  25. package/crates/aihu-css-core/tests/snapshot.rs +24 -0
  26. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__anchor_snapshot.snap +26 -0
  27. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__popover_snapshot.snap +26 -0
  28. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__text_balance_snapshot.snap +23 -0
  29. package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap +25 -0
  30. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__flat_output_for_class_list.snap +6 -0
  31. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_output_for_sfc.snap +25 -0
  32. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap +26 -0
  33. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap +24 -0
  34. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__standard_variants.snap +33 -0
  35. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__theme_default_vs_override.snap +45 -0
  36. package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__wc_native_variants.snap +28 -0
  37. package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_basic_class.snap +5 -0
  38. package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_multiple_classes.snap +8 -0
  39. package/crates/aihu-css-core/tests/snapshots/tokens__arbitrary_values.snap +9 -0
  40. package/crates/aihu-css-core/tests/snapshots/tokens__category_borders.snap +8 -0
  41. package/crates/aihu-css-core/tests/snapshots/tokens__category_colors.snap +10 -0
  42. package/crates/aihu-css-core/tests/snapshots/tokens__category_effects.snap +8 -0
  43. package/crates/aihu-css-core/tests/snapshots/tokens__category_layout.snap +12 -0
  44. package/crates/aihu-css-core/tests/snapshots/tokens__category_spacing.snap +11 -0
  45. package/crates/aihu-css-core/tests/snapshots/tokens__category_typography.snap +11 -0
  46. package/crates/aihu-css-core/tests/tokens.rs +79 -0
  47. package/dist/index.d.ts +76 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +120 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/runtime/cn.d.ts +14 -0
  52. package/dist/runtime/cn.d.ts.map +1 -0
  53. package/dist/runtime/cn.js +107 -0
  54. package/dist/runtime/cn.js.map +1 -0
  55. package/dist/runtime/progressive.d.ts +54 -0
  56. package/dist/runtime/progressive.d.ts.map +1 -0
  57. package/dist/runtime/progressive.js +132 -0
  58. package/dist/runtime/progressive.js.map +1 -0
  59. package/package.json +54 -0
  60. package/styles/aihu-default.css +73 -0
  61. package/styles/aihu-graphite.css +71 -0
@@ -0,0 +1,148 @@
1
+ //! Scoped-output, variant, and @theme emission tests (Plan 2 Tasks 4–7).
2
+
3
+ use aihu_css_core::{compile_sfc_scoped, parse_ast};
4
+
5
+ fn ast(json: &str) -> aihu_css_core::SfcAst {
6
+ parse_ast(json).unwrap()
7
+ }
8
+
9
+ /// Build a single-element SFC AST with a static class list.
10
+ fn sfc_with_classes(classes: &str) -> aihu_css_core::SfcAst {
11
+ let json = format!(
12
+ r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
13
+ "template":[{{"kind":"element","tag":"div","attrs":[
14
+ {{"kind":"static","name":"class","value":"{classes}"}}
15
+ ],"children":[]}}]}}"#
16
+ );
17
+ ast(&json)
18
+ }
19
+
20
+ // ── Task 4: scoped output, no global stylesheet ──────────────────────────────
21
+
22
+ #[test]
23
+ fn scoped_output_has_no_bare_global_utility_sheet() {
24
+ let css = compile_sfc_scoped(&sfc_with_classes("bg-primary p-4"));
25
+ // The utility rules are class selectors inside the shadow <style>; they are
26
+ // scoped by living in the shadow root. There is no separate global sheet.
27
+ assert!(css.contains(".bg-primary"));
28
+ assert!(css.contains("padding: 1rem"));
29
+ // Theme tokens emitted at :host so var(--color-*) resolves in the shadow.
30
+ assert!(css.contains(":host {"));
31
+ assert!(css.contains("--color-primary"));
32
+ }
33
+
34
+ #[test]
35
+ fn scoped_folds_authored_style_block() {
36
+ let json = r#"{"tag":"X","astVersion":1,
37
+ "style":{"content":".extra { color: red; }","scope":"scoped"},
38
+ "meta":{"name":"X"},
39
+ "template":[{"kind":"element","tag":"div","attrs":[
40
+ {"kind":"static","name":"class","value":"p-4"}],"children":[]}]}"#;
41
+ let css = compile_sfc_scoped(&ast(json));
42
+ assert!(css.contains(".p-4"));
43
+ assert!(css.contains(".extra { color: red; }"), "authored scoped @style folded in");
44
+ }
45
+
46
+ #[test]
47
+ fn global_style_block_passes_through_edge_e6() {
48
+ let json = r#"{"tag":"X","astVersion":1,
49
+ "style":{"content":"body { margin: 0; }","scope":"global"},
50
+ "meta":{"name":"X"},"template":null}"#;
51
+ let css = compile_sfc_scoped(&ast(json));
52
+ assert!(css.contains("body { margin: 0; }"));
53
+ assert!(css.contains("unscoped"), "global block annotated as unscoped");
54
+ }
55
+
56
+ // ── Task 5: WC-native variants ───────────────────────────────────────────────
57
+
58
+ #[test]
59
+ fn host_variant_emits_host_rule() {
60
+ let css = compile_sfc_scoped(&sfc_with_classes("host:bg-primary"));
61
+ assert!(css.contains(":host("), "host: → :host(...) selector: {css}");
62
+ assert!(css.contains("background-color: var(--color-primary)"));
63
+ }
64
+
65
+ #[test]
66
+ fn slotted_variants() {
67
+ let css = compile_sfc_scoped(&sfc_with_classes("slotted:p-4 slotted-img:rounded-lg"));
68
+ assert!(css.contains("::slotted("));
69
+ assert!(css.contains("::slotted(img"));
70
+ }
71
+
72
+ #[test]
73
+ fn part_variant_emits_part_selector() {
74
+ let css = compile_sfc_scoped(&sfc_with_classes("part-thumb:bg-accent"));
75
+ assert!(css.contains("::part(thumb)"));
76
+ }
77
+
78
+ #[test]
79
+ fn host_context_dark_uses_cascade_not_host_context() {
80
+ let css = compile_sfc_scoped(&sfc_with_classes("host-context-dark:bg-surface"));
81
+ assert!(
82
+ !css.contains(":host-context("),
83
+ "Firefox-safe: must NOT emit :host-context(): {css}"
84
+ );
85
+ assert!(css.contains("dark cascade"), "uses the documented cascade mechanism");
86
+ assert!(css.contains(":root.dark"), "dark value gated on consumer .dark scope");
87
+ }
88
+
89
+ // ── Task 6: standard variants ────────────────────────────────────────────────
90
+
91
+ #[test]
92
+ fn hover_variant_appends_pseudo_class() {
93
+ let css = compile_sfc_scoped(&sfc_with_classes("hover:bg-primary"));
94
+ assert!(css.contains(":hover"), "hover: → :hover pseudo-class: {css}");
95
+ }
96
+
97
+ #[test]
98
+ fn dark_variant_no_host_context() {
99
+ let css = compile_sfc_scoped(&sfc_with_classes("dark:bg-surface"));
100
+ assert!(!css.contains(":host-context("), "dark: must NOT emit :host-context()");
101
+ assert!(css.contains("dark cascade"));
102
+ }
103
+
104
+ #[test]
105
+ fn responsive_md_wraps_media_query() {
106
+ let css = compile_sfc_scoped(&sfc_with_classes("md:p-8"));
107
+ assert!(css.contains("@media (min-width:"), "md: → @media: {css}");
108
+ assert!(css.contains("padding: 2rem"));
109
+ }
110
+
111
+ #[test]
112
+ fn arbitrary_selector_variant() {
113
+ let css = compile_sfc_scoped(&sfc_with_classes("[&>div]:text-primary"));
114
+ assert!(css.contains(">div"), "[&>div]: → child selector: {css}");
115
+ assert!(css.contains("color: var(--color-primary)"));
116
+ }
117
+
118
+ #[test]
119
+ fn stacked_variants_md_hover() {
120
+ let css = compile_sfc_scoped(&sfc_with_classes("md:hover:bg-primary"));
121
+ assert!(css.contains("@media (min-width:"));
122
+ assert!(css.contains(":hover"));
123
+ }
124
+
125
+ // ── Task 7: @theme directive ─────────────────────────────────────────────────
126
+
127
+ #[test]
128
+ fn theme_override_registers_token() {
129
+ let json = r#"{"tag":"X","astVersion":1,
130
+ "style":{"content":"@theme { --color-primary: oklch(0.7 0.2 30); }","scope":"scoped"},
131
+ "meta":{"name":"X"},
132
+ "template":[{"kind":"element","tag":"div","attrs":[
133
+ {"kind":"static","name":"class","value":"bg-primary"}],"children":[]}]}"#;
134
+ let css = compile_sfc_scoped(&ast(json));
135
+ // The override value is registered as a :host token...
136
+ assert!(css.contains("--color-primary: oklch(0.7 0.2 30)"), "@theme override registered: {css}");
137
+ // ...and the utility references it.
138
+ assert!(css.contains("background-color: var(--color-primary)"));
139
+ // The @theme directive itself is NOT emitted as raw CSS.
140
+ assert!(!css.contains("@theme"), "@theme directive stripped from raw output");
141
+ }
142
+
143
+ #[test]
144
+ fn default_aihu_brand_tokens_present() {
145
+ let css = compile_sfc_scoped(&sfc_with_classes("bg-accent"));
146
+ // Default accent is the aihu terracotta.
147
+ assert!(css.contains("--color-accent: #c8543a"), "baked aihu brand default present: {css}");
148
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "tag": "Button",
3
+ "astVersion": 1,
4
+ "style": null,
5
+ "template": [
6
+ {
7
+ "kind": "element",
8
+ "tag": "button",
9
+ "attrs": [
10
+ { "kind": "static", "name": "class", "value": "base" },
11
+ { "kind": "binding", "name": "class", "expr": "cn('btn', size)" },
12
+ { "kind": "macro", "name": "class:loading", "value": { "form": "curly", "expr": "busy" } },
13
+ { "kind": "macro", "name": "on:click", "value": { "form": "curly", "expr": "go" } }
14
+ ],
15
+ "children": [{ "kind": "text", "value": "Go" }]
16
+ }
17
+ ],
18
+ "meta": { "name": "Button" }
19
+ }
@@ -0,0 +1,102 @@
1
+ //! Snapshot + behavior coverage for the progressive features (Plan 3 Tasks 5–8).
2
+ //!
3
+ //! Asserts each feature's `@supports`/fallback contract via `compile_sfc_scoped`
4
+ //! (the production path that routes progressive prefixes through the registry).
5
+
6
+ use aihu_css_core::{compile_sfc_scoped, parse_ast};
7
+
8
+ fn ast(json: &str) -> aihu_css_core::SfcAst {
9
+ parse_ast(json).unwrap()
10
+ }
11
+
12
+ fn sfc(classes: &str) -> aihu_css_core::SfcAst {
13
+ ast(&format!(
14
+ r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
15
+ "template":[{{"kind":"element","tag":"div","attrs":[
16
+ {{"kind":"static","name":"class","value":"{classes}"}}
17
+ ],"children":[]}}]}}"#
18
+ ))
19
+ }
20
+
21
+ // ── Task 5: view-transition: (CSS-only, no JS) ───────────────────────────────
22
+
23
+ #[test]
24
+ fn view_transition_is_supports_gated_css_only() {
25
+ let css = compile_sfc_scoped(&sfc("view-transition:hero"));
26
+ assert!(
27
+ css.contains("@supports (view-transition-name: none)"),
28
+ "view-transition gated behind @supports: {css}"
29
+ );
30
+ assert!(css.contains("view-transition-name: hero"));
31
+ assert!(
32
+ !css.contains("aihu:progressive-fallback"),
33
+ "view-transition is CSS-only — NO JS fallback marker: {css}"
34
+ );
35
+ }
36
+
37
+ #[test]
38
+ fn view_transition_snapshot() {
39
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("view-transition:hero")));
40
+ }
41
+
42
+ // ── Task 6: anchor: (@supports gate + JS fallback marker) ────────────────────
43
+
44
+ #[test]
45
+ fn anchor_is_supports_gated_with_js_marker() {
46
+ let css = compile_sfc_scoped(&sfc("anchor:tooltip"));
47
+ assert!(
48
+ css.contains("@supports (anchor-name: --a)"),
49
+ "anchor gated behind @supports (anchor-name): {css}"
50
+ );
51
+ assert!(css.contains("anchor-name: --tooltip"));
52
+ assert!(
53
+ css.contains("aihu:progressive-fallback anchorFallback"),
54
+ "anchor emits a runtime-fallback marker: {css}"
55
+ );
56
+ }
57
+
58
+ #[test]
59
+ fn anchor_snapshot() {
60
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("anchor:tooltip")));
61
+ }
62
+
63
+ // ── Task 7: popover: (@supports gate + portal JS fallback marker) ────────────
64
+
65
+ #[test]
66
+ fn popover_is_supports_gated_with_js_marker() {
67
+ let css = compile_sfc_scoped(&sfc("popover:menu"));
68
+ assert!(
69
+ css.contains("@supports (selector(:popover-open))"),
70
+ "popover gated behind @supports selector(:popover-open): {css}"
71
+ );
72
+ assert!(
73
+ css.contains("aihu:progressive-fallback popoverFallback"),
74
+ "popover emits a runtime-fallback marker: {css}"
75
+ );
76
+ }
77
+
78
+ #[test]
79
+ fn popover_snapshot() {
80
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("popover:menu")));
81
+ }
82
+
83
+ // ── Task 8: text-balance: (no gate, no JS) ───────────────────────────────────
84
+
85
+ #[test]
86
+ fn text_balance_no_gate_no_js() {
87
+ let css = compile_sfc_scoped(&sfc("text-balance:"));
88
+ assert!(css.contains("text-wrap: balance"), "emits text-wrap: balance: {css}");
89
+ assert!(
90
+ !css.contains("@supports"),
91
+ "text-balance has NO @supports gate (silently ignored if unsupported): {css}"
92
+ );
93
+ assert!(
94
+ !css.contains("aihu:progressive-fallback"),
95
+ "text-balance is CSS-only — NO JS fallback marker: {css}"
96
+ );
97
+ }
98
+
99
+ #[test]
100
+ fn text_balance_snapshot() {
101
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("text-balance:")));
102
+ }
@@ -0,0 +1,99 @@
1
+ //! Scanner tests — proves the three class-forms (Static / Binding / Macro) are
2
+ //! distinguished and that non-class macros never enter the utility set. The
3
+ //! fixture is REAL `aihu-compile --ast-json` output (not hand-authored).
4
+
5
+ use aihu_css_core::{parse_ast, scan, scan_ast, SfcAst};
6
+
7
+ fn fixture_ast() -> SfcAst {
8
+ // From `aihu-compile --ast-json` for:
9
+ // <button class="base" $class={cn('btn', size)} $class:loading={busy} $on.click={go}>Go</button>
10
+ parse_ast(include_str!("fixtures/button.ast.json")).unwrap()
11
+ }
12
+
13
+ #[test]
14
+ fn extracts_static_form_a() {
15
+ let set = scan_ast(&fixture_ast());
16
+ assert!(set.contains("base"), "Form A static class missing: {set:?}");
17
+ }
18
+
19
+ #[test]
20
+ fn extracts_binding_string_literals_form_b() {
21
+ let set = scan_ast(&fixture_ast());
22
+ assert!(set.contains("btn"), "string literal in cn('btn', size) missing");
23
+ assert!(!set.contains("size"), "bare identifier `size` must NOT be a utility");
24
+ }
25
+
26
+ #[test]
27
+ fn flags_unresolved_binding_identifiers() {
28
+ let result = scan(&fixture_ast());
29
+ assert!(
30
+ result.unresolved.contains("size"),
31
+ "unresolved identifier `size` should be flagged for diagnostics: {:?}",
32
+ result.unresolved
33
+ );
34
+ }
35
+
36
+ #[test]
37
+ fn extracts_macro_class_toggle_form_c() {
38
+ let set = scan_ast(&fixture_ast());
39
+ assert!(set.contains("loading"), "Form C $class:loading toggle missing");
40
+ }
41
+
42
+ #[test]
43
+ fn skips_non_class_macros() {
44
+ // $on.click / $if must never enter the utility set.
45
+ let set = scan_ast(&fixture_ast());
46
+ assert!(
47
+ !set.iter().any(|c| c.contains("click") || c == "if" || c.contains("on:")),
48
+ "non-class macro leaked into utility set: {set:?}"
49
+ );
50
+ }
51
+
52
+ #[test]
53
+ fn rejects_unsupported_ast_version() {
54
+ let json = r#"{"tag":"X","astVersion":2,"style":null,"template":null,"meta":{"name":"X"}}"#;
55
+ assert!(parse_ast(json).is_err(), "astVersion 2 must be rejected");
56
+ }
57
+
58
+ #[test]
59
+ fn empty_template_yields_empty_set() {
60
+ let json = r#"{"tag":"X","astVersion":1,"style":null,"template":null,"meta":{"name":"X"}}"#;
61
+ let ast = parse_ast(json).unwrap();
62
+ assert!(scan_ast(&ast).is_empty(), "no @template → empty utility set (edge E5)");
63
+ }
64
+
65
+ #[test]
66
+ fn skips_component_node_class_attrs_edge_e10() {
67
+ // A macroElement (component) node owns its own shadow scope; its class
68
+ // attrs must not be compiled into the parent sheet.
69
+ let json = r#"{
70
+ "tag":"Parent","astVersion":1,"style":null,"meta":{"name":"Parent"},
71
+ "template":[
72
+ {"kind":"macroElement","name":"UserCard","attrs":[
73
+ {"kind":"static","name":"class","value":"should-not-appear"}
74
+ ],"children":[
75
+ {"kind":"element","tag":"span","attrs":[
76
+ {"kind":"static","name":"class","value":"slotted-content"}
77
+ ],"children":[]}
78
+ ]}
79
+ ]}"#;
80
+ let ast = parse_ast(json).unwrap();
81
+ let set = scan_ast(&ast);
82
+ assert!(!set.contains("should-not-appear"), "component class must be skipped (E10)");
83
+ assert!(set.contains("slotted-content"), "child HTML element class should still scan");
84
+ }
85
+
86
+ #[test]
87
+ fn array_binding_extracts_literals_edge_e2() {
88
+ // $class={['a', cond && 'b']} → 'a','b' utilities, `cond` unresolved.
89
+ let json = r#"{
90
+ "tag":"X","astVersion":1,"style":null,"meta":{"name":"X"},
91
+ "template":[{"kind":"element","tag":"div","attrs":[
92
+ {"kind":"binding","name":"class","expr":"['a', cond && 'b']"}
93
+ ],"children":[]}]}"#;
94
+ let ast = parse_ast(json).unwrap();
95
+ let result = scan(&ast);
96
+ assert!(result.utilities.contains("a"));
97
+ assert!(result.utilities.contains("b"));
98
+ assert!(result.unresolved.contains("cond"));
99
+ }
@@ -0,0 +1,73 @@
1
+ //! Snapshot coverage for scoped output + variants + @theme (Plan 2 Tasks 4–7).
2
+
3
+ use aihu_css_core::{compile_classes, compile_sfc_scoped, parse_ast};
4
+
5
+ fn ast(json: &str) -> aihu_css_core::SfcAst {
6
+ parse_ast(json).unwrap()
7
+ }
8
+
9
+ fn sfc(classes: &str) -> aihu_css_core::SfcAst {
10
+ ast(&format!(
11
+ r#"{{"tag":"X","astVersion":1,"style":null,"meta":{{"name":"X"}},
12
+ "template":[{{"kind":"element","tag":"div","attrs":[
13
+ {{"kind":"static","name":"class","value":"{classes}"}}
14
+ ],"children":[]}}]}}"#
15
+ ))
16
+ }
17
+
18
+ #[test]
19
+ fn flat_output_for_class_list() {
20
+ insta::assert_snapshot!(compile_classes(&[
21
+ "bg-primary".to_string(),
22
+ "p-4".to_string()
23
+ ]));
24
+ }
25
+
26
+ #[test]
27
+ fn scoped_output_for_sfc() {
28
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc("bg-primary p-4 rounded-lg")));
29
+ }
30
+
31
+ #[test]
32
+ fn scoped_with_authored_style_block() {
33
+ let json = r#"{"tag":"Card","astVersion":1,
34
+ "style":{"content":".inner { display: grid; gap: 1rem; }","scope":"scoped"},
35
+ "meta":{"name":"Card"},
36
+ "template":[{"kind":"element","tag":"div","attrs":[
37
+ {"kind":"static","name":"class","value":"p-4 shadow-md"}],"children":[]}]}"#;
38
+ insta::assert_snapshot!(compile_sfc_scoped(&ast(json)));
39
+ }
40
+
41
+ #[test]
42
+ fn scoped_with_global_style_block() {
43
+ let json = r#"{"tag":"X","astVersion":1,
44
+ "style":{"content":"body { margin: 0; }","scope":"global"},
45
+ "meta":{"name":"X"},"template":null}"#;
46
+ insta::assert_snapshot!(compile_sfc_scoped(&ast(json)));
47
+ }
48
+
49
+ #[test]
50
+ fn wc_native_variants() {
51
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc(
52
+ "host:bg-primary slotted:p-4 slotted-img:rounded-lg part-thumb:bg-accent host-context-dark:bg-surface"
53
+ )));
54
+ }
55
+
56
+ #[test]
57
+ fn standard_variants() {
58
+ insta::assert_snapshot!(compile_sfc_scoped(&sfc(
59
+ "hover:bg-primary focus:text-accent dark:bg-surface md:p-8 [&>div]:text-primary md:hover:bg-primary"
60
+ )));
61
+ }
62
+
63
+ #[test]
64
+ fn theme_default_vs_override() {
65
+ let default = compile_sfc_scoped(&sfc("bg-primary"));
66
+ let json = r#"{"tag":"X","astVersion":1,
67
+ "style":{"content":"@theme { --color-primary: oklch(0.55 0.18 28); }","scope":"scoped"},
68
+ "meta":{"name":"X"},
69
+ "template":[{"kind":"element","tag":"div","attrs":[
70
+ {"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}"));
73
+ }
@@ -0,0 +1,24 @@
1
+ use aihu_css_core::compile_classes;
2
+
3
+ #[test]
4
+ fn compiles_basic_class() {
5
+ let output = compile_classes(&["bg-primary".to_string()]);
6
+ insta::assert_snapshot!(output);
7
+ }
8
+
9
+ #[test]
10
+ fn compiles_multiple_classes() {
11
+ let output = compile_classes(&[
12
+ "bg-primary".to_string(),
13
+ "text-primary-foreground".to_string(),
14
+ "rounded-md".to_string(),
15
+ "p-4".to_string(),
16
+ ]);
17
+ insta::assert_snapshot!(output);
18
+ }
19
+
20
+ #[test]
21
+ fn unknown_class_returns_empty_string() {
22
+ let output = compile_classes(&["this-class-does-not-exist".to_string()]);
23
+ assert_eq!(output, "");
24
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"anchor:tooltip\"))"
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
+ @supports (anchor-name: --a) {
24
+ .anchor\:tooltip { anchor-name: --tooltip; position-anchor: --tooltip; }
25
+ }
26
+ /* aihu:progressive-fallback anchorFallback (when not @supports anchor-name: --a) */
@@ -0,0 +1,26 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"popover:menu\"))"
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
+ @supports (selector(:popover-open)) {
24
+ .popover\:menu:popover-open { position: fixed; margin: 0; inset: auto; }
25
+ }
26
+ /* aihu:progressive-fallback popoverFallback (when not @supports selector(:popover-open)) */
@@ -0,0 +1,23 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"text-balance:\"))"
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
+ .text-balance { text-wrap: balance; }
@@ -0,0 +1,25 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/progressive_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"view-transition:hero\"))"
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
+ @supports (view-transition-name: none) {
24
+ .view-transition\:hero { view-transition-name: hero; }
25
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
3
+ expression: "compile_classes(&[\"bg-primary\".to_string(), \"p-4\".to_string()])"
4
+ ---
5
+ .bg-primary { background-color: var(--color-primary); }
6
+ .p-4 { padding: 1rem; }
@@ -0,0 +1,25 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
3
+ expression: "compile_sfc_scoped(&sfc(\"bg-primary p-4 rounded-lg\"))"
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
+ .bg-primary { background-color: var(--color-primary); }
24
+ .p-4 { padding: 1rem; }
25
+ .rounded-lg { border-radius: 0.5rem; }
@@ -0,0 +1,26 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
3
+ expression: compile_sfc_scoped(&ast(json))
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
+ .p-4 { padding: 1rem; }
24
+ .shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
25
+ /* authored @style (scoped) */
26
+ .inner { display: grid; gap: 1rem; }
@@ -0,0 +1,24 @@
1
+ ---
2
+ source: packages/css-engine/crates/aihu-css-core/tests/scoped_snapshot.rs
3
+ expression: compile_sfc_scoped(&ast(json))
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
+ /* authored @style ($global — unscoped) */
24
+ body { margin: 0; }