on master help sign in
Add @in-front and @behind directives for overlay positioning
6ac9439iko
DESIGN.md+26
@@ -59,6 +59,8 @@ A minimalist layout language inspired by elm-ui that compiles to static HTML.
59| `@address` | address | Contact information |
59| `@address` | address | Contact information |
60| `@search` | search | Search landmark (HTML5) |
60| `@search` | search | Search landmark (HTML5) |
61| `@breadcrumb` | nav>ol | Semantic breadcrumb navigation |
61| `@breadcrumb` | nav>ol | Semantic breadcrumb navigation |
62+| `@in-front` | div (absolute) | Overlay layer painted on top of parent |
63+| `@behind` | div (absolute) | Layer painted behind the parent's content |
62
64
63Bare lines (not starting with `@` or `[`) are text nodes.
65Bare lines (not starting with `@` or `[`) are text nodes.
64
66
@@ -477,6 +479,30 @@ Progress bar and meter elements. Use `value`, `max`, `min` attributes.
477@meter [value 6, min 0, max 10, low 3, high 8]
479@meter [value 6, min 0, max 10, low 3, high 8]
478```
480```
479
481
482+### `@in-front` / `@behind`
483+
484+Overlay layers, inspired by elm-ui's `inFront` and `behind`. Children of these
485+directives are absolutely positioned to fill the parent's bounds; the parent
486+automatically becomes a positioning context (`position: relative` and
487+`isolation: isolate`).
488+
489+```
490+@el [width 200, height 200, background blue]
491+ @text [color white] Main content
492+ @in-front
493+ @text [color yellow] Painted on top
494+ @behind
495+ @el [background red, width fill, height fill]
496+```
497+
498+`@in-front` paints on top of the parent's content (DOM order ensures it
499+overlays static siblings). `@behind` paints behind (`z-index: -1` keeps it
500+under the siblings, while `isolation: isolate` on the parent prevents it from
501+slipping behind the parent's own background).
502+
503+If you set `position` explicitly on the parent (e.g., `position absolute` for a
504+popup), that wins over the auto-applied `position: relative`.
505+
480## `@each` destructuring
506## `@each` destructuring
481
507
482When `@each` has more than one variable and items contain spaces, values are destructured:
508When `@each` has more than one variable and items contain spaces, values are destructured:
README.md+14
@@ -109,6 +109,8 @@ A VS Code extension with syntax highlighting and LSP integration is available in
109| `@figcaption`| Caption for `@figure` |
109| `@figcaption`| Caption for `@figure` |
110| `@progress` | Progress bar |
110| `@progress` | Progress bar |
111| `@meter` | Meter/gauge |
111| `@meter` | Meter/gauge |
112+| `@in-front` | Overlay layer on top of parent |
113+| `@behind` | Layer behind parent content |
112
114
113### Layout attributes
115### Layout attributes
114
116
@@ -139,6 +141,18 @@ A VS Code extension with syntax highlighting and LSP integration is available in
139 Overlay content
141 Overlay content
140```
142```
141
143
144+For overlays, prefer `@in-front` and `@behind` — they make the parent a
145+positioning context and stretch to its bounds automatically:
146+
147+```
148+@el [width 200, height 200, background blue]
149+ @text [color white] Main content
150+ @in-front
151+ @text [color yellow] Top layer
152+ @behind
153+ @el [background red, width fill, height fill]
154+```
155+
142### Pseudo-states and media prefixes
156### Pseudo-states and media prefixes
143
157
144Prefix any style attribute with `hover:`, `active:`, or `focus:`:
158Prefix any style attribute with `hover:`, `active:`, or `focus:`:
crates/htmlang-core/src/ast.rs+3
@@ -155,6 +155,9 @@ pub enum ElementKind {
155 Carousel,
155 Carousel,
156 Chip,
156 Chip,
157 Tag,
157 Tag,
158+ // Overlay positioning (elm-ui inspired)
159+ InFront,
160+ Behind,
158 // HTML5 elements
161 // HTML5 elements
159 Script,
162 Script,
160 Noscript,
163 Noscript,
crates/htmlang-core/src/codegen.rs+59−17
@@ -1571,6 +1571,18 @@ fn strip_string_quotes(val: &str) -> &str {
1571 val
1571 val
1572}
1572}
1573
1573
1574+// True if any direct child is `@in-front` or `@behind`. Such children render
1575+// as absolutely positioned overlays, so the parent automatically becomes a
1576+// positioning context (position:relative + isolation:isolate).
1577+fn has_overlay_children(elem: &Element) -> bool {
1578+ elem.children.iter().any(|child| {
1579+ matches!(
1580+ child,
1581+ Node::Element(e) if matches!(e.kind, ElementKind::InFront | ElementKind::Behind)
1582+ )
1583+ })
1584+}
1585+
1574fn generate_element(
1586fn generate_element(
1575 elem: &Element,
1587 elem: &Element,
1576 parent_kind: Option<&ElementKind>,
1588 parent_kind: Option<&ElementKind>,
@@ -1641,7 +1653,7 @@ fn generate_element(
1641 }
1653 }
1642 // @breadcrumb generates semantic <nav aria-label="breadcrumb"><ol>...</ol></nav>
1654 // @breadcrumb generates semantic <nav aria-label="breadcrumb"><ol>...</ol></nav>
1643 if elem.kind == ElementKind::Breadcrumb {
1655 if elem.kind == ElementKind::Breadcrumb {
1644− let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles);
1656+ let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles, false);
1645 let (id, user_class) = extract_id_class(&elem.attrs);
1657 let (id, user_class) = extract_id_class(&elem.attrs);
1646 out.push_str(&ctx.indent());
1658 out.push_str(&ctx.indent());
1647 out.push_str("<nav aria-label=\"breadcrumb\"");
1659 out.push_str("<nav aria-label=\"breadcrumb\"");
@@ -1696,7 +1708,9 @@ fn generate_element(
1696 | ElementKind::Column
1708 | ElementKind::Column
1697 | ElementKind::El
1709 | ElementKind::El
1698 | ElementKind::Grid
1710 | ElementKind::Grid
1699− | ElementKind::Stack => "div",
1711+ | ElementKind::Stack
1712+ | ElementKind::InFront
1713+ | ElementKind::Behind => "div",
1700 ElementKind::Text => "span",
1714 ElementKind::Text => "span",
1701 ElementKind::Paragraph => "p",
1715 ElementKind::Paragraph => "p",
1702 ElementKind::Link => "a",
1716 ElementKind::Link => "a",
@@ -1853,6 +1867,8 @@ fn generate_element(
1853 ElementKind::Noscript => "noscript",
1867 ElementKind::Noscript => "noscript",
1854 ElementKind::Address => "address",
1868 ElementKind::Address => "address",
1855 ElementKind::Search => "search",
1869 ElementKind::Search => "search",
1870+ ElementKind::InFront => "in-front",
1871+ ElementKind::Behind => "behind",
1856 ElementKind::H1 => "h1",
1872 ElementKind::H1 => "h1",
1857 ElementKind::H2 => "h2",
1873 ElementKind::H2 => "h2",
1858 ElementKind::H3 => "h3",
1874 ElementKind::H3 => "h3",
@@ -1875,7 +1891,14 @@ fn generate_element(
1875 }
1891 }
1876
1892
1877 // Compute CSS for each state and get a class name
1893 // Compute CSS for each state and get a class name
1894+ let overlay_children = has_overlay_children(elem);
1878− let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles);
1895+ let gen_class = compute_class(
1896+ &elem.attrs,
1897+ &elem.kind,
1898+ parent_kind,
1899+ styles,
1900+ overlay_children,
1901+ );
1879 let (id, user_class) = extract_id_class(&elem.attrs);
1902 let (id, user_class) = extract_id_class(&elem.attrs);
1880
1903
1881 if ctx.dev && elem.line_num > 0 {
1904 if ctx.dev && elem.line_num > 0 {
@@ -1953,7 +1976,7 @@ fn generate_element(
1953 // Critical CSS: inline styles directly instead of using a class
1976 // Critical CSS: inline styles directly instead of using a class
1954 let is_critical = elem.attrs.iter().any(|a| a.key == "critical");
1977 let is_critical = elem.attrs.iter().any(|a| a.key == "critical");
1955 if is_critical {
1978 if is_critical {
1956− let inline_css = attrs_to_css(&elem.attrs, "", &elem.kind, parent_kind);
1979+ let inline_css = attrs_to_css(&elem.attrs, "", &elem.kind, parent_kind, overlay_children);
1957 if !inline_css.is_empty() {
1980 if !inline_css.is_empty() {
1958 out.push_str(" style=\"");
1981 out.push_str(" style=\"");
1959 out.push_str(&html_escape(&inline_css));
1982 out.push_str(&html_escape(&inline_css));
@@ -2059,7 +2082,7 @@ fn generate_self_closing(
2059 styles: &mut StyleCollector,
2082 styles: &mut StyleCollector,
2060 ctx: &mut GenContext,
2083 ctx: &mut GenContext,
2061) {
2084) {
2062− let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles);
2085+ let gen_class = compute_class(&elem.attrs, &elem.kind, parent_kind, styles, false);
2063 let (id, user_class) = extract_id_class(&elem.attrs);
2086 let (id, user_class) = extract_id_class(&elem.attrs);
2064
2087
2065 let (tag, kind_label) = match elem.kind {
2088 let (tag, kind_label) = match elem.kind {
@@ -2255,13 +2278,14 @@ fn compute_class(
2255 kind: &ElementKind,
2278 kind: &ElementKind,
2256 parent_kind: Option<&ElementKind>,
2279 parent_kind: Option<&ElementKind>,
2257 styles: &mut StyleCollector,
2280 styles: &mut StyleCollector,
2281+ has_overlay_children: bool,
2258) -> Option<String> {
2282) -> Option<String> {
2259− let base = attrs_to_css(attrs, "", kind, parent_kind);
2283+ let base = attrs_to_css(attrs, "", kind, parent_kind, has_overlay_children);
2260
2284
2261 // Collect pseudo-state overrides
2285 // Collect pseudo-state overrides
2262 let mut pseudo = Vec::new();
2286 let mut pseudo = Vec::new();
2263 for &(prefix, selector) in PSEUDO_PREFIXES {
2287 for &(prefix, selector) in PSEUDO_PREFIXES {
2264− let css = attrs_to_css(attrs, prefix, kind, parent_kind);
2288+ let css = attrs_to_css(attrs, prefix, kind, parent_kind, has_overlay_children);
2265 if !css.is_empty() {
2289 if !css.is_empty() {
2266 pseudo.push((selector.to_string(), css));
2290 pseudo.push((selector.to_string(), css));
2267 }
2291 }
@@ -2291,7 +2315,7 @@ fn compute_class(
2291 for prefix in &nth_prefixes {
2315 for prefix in &nth_prefixes {
2292 let expr = &prefix[4..prefix.len() - 1];
2316 let expr = &prefix[4..prefix.len() - 1];
2293 let selector = format!(":nth-child({})", expr);
2317 let selector = format!(":nth-child({})", expr);
2294− let css = attrs_to_css(attrs, prefix, kind, parent_kind);
2318+ let css = attrs_to_css(attrs, prefix, kind, parent_kind, has_overlay_children);
2295 if !css.is_empty() {
2319 if !css.is_empty() {
2296 pseudo.push((selector, css));
2320 pseudo.push((selector, css));
2297 }
2321 }
@@ -2312,7 +2336,7 @@ fn compute_class(
2312 for prefix in &has_prefixes {
2336 for prefix in &has_prefixes {
2313 let inner = &prefix[4..prefix.len() - 2]; // extract selector from has(selector):
2337 let inner = &prefix[4..prefix.len() - 2]; // extract selector from has(selector):
2314 let selector = format!(":has({})", inner);
2338 let selector = format!(":has({})", inner);
2315− let css = attrs_to_css(attrs, prefix, kind, parent_kind);
2339+ let css = attrs_to_css(attrs, prefix, kind, parent_kind, has_overlay_children);
2316 if !css.is_empty() {
2340 if !css.is_empty() {
2317 pseudo.push((selector, css));
2341 pseudo.push((selector, css));
2318 }
2342 }
@@ -2322,7 +2346,7 @@ fn compute_class(
2322 let mut responsive = Vec::new();
2346 let mut responsive = Vec::new();
2323 for &(bp_name, _) in BREAKPOINTS {
2347 for &(bp_name, _) in BREAKPOINTS {
2324 let prefix = format!("{}:", bp_name);
2348 let prefix = format!("{}:", bp_name);
2325− let css = attrs_to_css(attrs, &prefix, kind, parent_kind);
2349+ let css = attrs_to_css(attrs, &prefix, kind, parent_kind, has_overlay_children);
2326 if !css.is_empty() {
2350 if !css.is_empty() {
2327 responsive.push((bp_name.to_string(), css));
2351 responsive.push((bp_name.to_string(), css));
2328 }
2352 }
@@ -2332,18 +2356,19 @@ fn compute_class(
2332 let mut container = Vec::new();
2356 let mut container = Vec::new();
2333 for &(bp_name, _) in BREAKPOINTS {
2357 for &(bp_name, _) in BREAKPOINTS {
2334 let prefix = format!("cq-{}:", bp_name);
2358 let prefix = format!("cq-{}:", bp_name);
2335− let css = attrs_to_css(attrs, &prefix, kind, parent_kind);
2359+ let css = attrs_to_css(attrs, &prefix, kind, parent_kind, has_overlay_children);
2336 if !css.is_empty() {
2360 if !css.is_empty() {
2337 container.push((bp_name.to_string(), css));
2361 container.push((bp_name.to_string(), css));
2338 }
2362 }
2339 }
2363 }
2340
2364
2341− let dark = attrs_to_css(attrs, "dark:", kind, parent_kind);
2365+ let dark = attrs_to_css(attrs, "dark:", kind, parent_kind, has_overlay_children);
2342− let print = attrs_to_css(attrs, "print:", kind, parent_kind);
2366+ let print = attrs_to_css(attrs, "print:", kind, parent_kind, has_overlay_children);
2343− let motion_safe = attrs_to_css(attrs, "motion-safe:", kind, parent_kind);
2367+ let motion_safe = attrs_to_css(attrs, "motion-safe:", kind, parent_kind, has_overlay_children);
2368+ let motion_reduce =
2344− let motion_reduce = attrs_to_css(attrs, "motion-reduce:", kind, parent_kind);
2369+ attrs_to_css(attrs, "motion-reduce:", kind, parent_kind, has_overlay_children);
2345− let landscape = attrs_to_css(attrs, "landscape:", kind, parent_kind);
2370+ let landscape = attrs_to_css(attrs, "landscape:", kind, parent_kind, has_overlay_children);
2346− let portrait = attrs_to_css(attrs, "portrait:", kind, parent_kind);
2371+ let portrait = attrs_to_css(attrs, "portrait:", kind, parent_kind, has_overlay_children);
2347
2372
2348 // Dedupe: if a property is declared twice within a single rule, keep only
2373 // Dedupe: if a property is declared twice within a single rule, keep only
2349 // the last occurrence (element-kind defaults are written before
2374 // the last occurrence (element-kind defaults are written before
@@ -2545,11 +2570,18 @@ fn attrs_to_css(
2545 state_prefix: &str,
2570 state_prefix: &str,
2546 kind: &ElementKind,
2571 kind: &ElementKind,
2547 parent_kind: Option<&ElementKind>,
2572 parent_kind: Option<&ElementKind>,
2573+ has_overlay_children: bool,
2548) -> String {
2574) -> String {
2549 let mut css = String::new();
2575 let mut css = String::new();
2550
2576
2551 // Base element styles only for the default (non-state) pass
2577 // Base element styles only for the default (non-state) pass
2552 if state_prefix.is_empty() {
2578 if state_prefix.is_empty() {
2579+ // Elements with @in-front / @behind children become positioning
2580+ // contexts. Pushed before user attrs so an explicit `position` wins
2581+ // via dedupe (only the last declaration of a property is kept).
2582+ if has_overlay_children {
2583+ css.push_str("position:relative;isolation:isolate;");
2584+ }
2553 match kind {
2585 match kind {
2554 ElementKind::Row => css.push_str("display:flex;flex-direction:row;"),
2586 ElementKind::Row => css.push_str("display:flex;flex-direction:row;"),
2555 ElementKind::Column
2587 ElementKind::Column
@@ -2613,6 +2645,16 @@ fn attrs_to_css(
2613 ElementKind::Avatar => css.push_str("display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;overflow:hidden;flex-shrink:0;"),
2645 ElementKind::Avatar => css.push_str("display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;overflow:hidden;flex-shrink:0;"),
2614 ElementKind::Carousel => css.push_str("display:flex;flex-direction:row;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;"),
2646 ElementKind::Carousel => css.push_str("display:flex;flex-direction:row;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;"),
2615 ElementKind::Chip => css.push_str("display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:9999px;font-size:0.875rem;border:1px solid currentColor;"),
2647 ElementKind::Chip => css.push_str("display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:9999px;font-size:0.875rem;border:1px solid currentColor;"),
2648+ // Overlay layers fill the parent. Parent gets position:relative
2649+ // applied automatically (see has_overlay_children check below).
2650+ ElementKind::InFront => {
2651+ css.push_str(FLEX_COLUMN);
2652+ css.push_str("position:absolute;inset:0;");
2653+ }
2654+ ElementKind::Behind => {
2655+ css.push_str(FLEX_COLUMN);
2656+ css.push_str("position:absolute;inset:0;z-index:-1;");
2657+ }
2616 _ => {}
2658 _ => {}
2617 }
2659 }
2618 // Children of @carousel get scroll-snap-align and flex-shrink
2660 // Children of @carousel get scroll-snap-align and flex-shrink
crates/htmlang-core/src/parser.rs+8
@@ -3321,6 +3321,8 @@ const KNOWN_ELEMENTS: &[&str] = &[
3321 "address",
3321 "address",
3322 "search",
3322 "search",
3323 "breadcrumb",
3323 "breadcrumb",
3324+ "in-front",
3325+ "behind",
3324];
3326];
3325
3327
3326const KNOWN_DIRECTIVES: &[&str] = &[
3328const KNOWN_DIRECTIVES: &[&str] = &[
@@ -3454,6 +3456,8 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
3454 "address" => Ok(ElementKind::Address),
3456 "address" => Ok(ElementKind::Address),
3455 "search" => Ok(ElementKind::Search),
3457 "search" => Ok(ElementKind::Search),
3456 "breadcrumb" => Ok(ElementKind::Breadcrumb),
3458 "breadcrumb" => Ok(ElementKind::Breadcrumb),
3459+ "in-front" => Ok(ElementKind::InFront),
3460+ "behind" => Ok(ElementKind::Behind),
3457 _ => {
3461 _ => {
3458 let all_known: Vec<&str> = KNOWN_ELEMENTS
3462 let all_known: Vec<&str> = KNOWN_ELEMENTS
3459 .iter()
3463 .iter()
@@ -5025,6 +5029,8 @@ fn element_kind_name(kind: &ElementKind) -> &'static str {
5025 ElementKind::Address => "@address",
5029 ElementKind::Address => "@address",
5026 ElementKind::Search => "@search",
5030 ElementKind::Search => "@search",
5027 ElementKind::Breadcrumb => "@breadcrumb",
5031 ElementKind::Breadcrumb => "@breadcrumb",
5032+ ElementKind::InFront => "@in-front",
5033+ ElementKind::Behind => "@behind",
5028 }
5034 }
5029}
5035}
5030
5036
@@ -5062,6 +5068,8 @@ fn is_container(kind: &ElementKind) -> bool {
5062 | ElementKind::Address
5068 | ElementKind::Address
5063 | ElementKind::Search
5069 | ElementKind::Search
5064 | ElementKind::Breadcrumb
5070 | ElementKind::Breadcrumb
5071+ | ElementKind::InFront
5072+ | ElementKind::Behind
5065 )
5073 )
5066}
5074}
5067
5075
src/bin/htmlang_lsp/completion.rs+9
@@ -236,6 +236,15 @@ fn element_completions(range: Range) -> Vec<CompletionItem> {
236 ("@address", "Contact information (address)"),
236 ("@address", "Contact information (address)"),
237 ("@search", "Search section (search)"),
237 ("@search", "Search section (search)"),
238 ("@breadcrumb", "Breadcrumb navigation (nav with aria)"),
238 ("@breadcrumb", "Breadcrumb navigation (nav with aria)"),
239+ // Overlay positioning
240+ (
241+ "@in-front",
242+ "Overlay layer rendered on top of the parent (absolute, inset 0)",
243+ ),
244+ (
245+ "@behind",
246+ "Background layer rendered behind the parent's content (absolute, inset 0, z-index -1)",
247+ ),
239 ]
248 ]
240 .iter()
249 .iter()
241 .map(|(name, detail)| item(name, CompletionItemKind::KEYWORD, detail, name, range))
250 .map(|(name, detail)| item(name, CompletionItemKind::KEYWORD, detail, name, range))
src/bin/htmlang_lsp/hover.rs+6
@@ -908,6 +908,12 @@ fn hover_builtin(word: &str) -> Option<String> {
908 "@stack" => {
908 "@stack" => {
909 "**@stack** \u{2014} Stack container\n\nRenders as `<div>` with `position: relative`. Children can be absolutely positioned on top of each other."
909 "**@stack** \u{2014} Stack container\n\nRenders as `<div>` with `position: relative`. Children can be absolutely positioned on top of each other."
910 }
910 }
911+ "@in-front" => {
912+ "**@in-front** \u{2014} Overlay layer\n\nRenders as `<div>` with `position: absolute; inset: 0`, painted on top of the parent's content. The parent automatically becomes a positioning context (`position: relative; isolation: isolate`).\n\nUsage:\n```\n@el [width 200, height 200, background blue]\n Main content\n @in-front\n @text [color white] Overlay\n```"
913+ }
914+ "@behind" => {
915+ "**@behind** \u{2014} Background layer\n\nRenders as `<div>` with `position: absolute; inset: 0; z-index: -1`, painted behind the parent's content. The parent automatically becomes a positioning context (`position: relative; isolation: isolate`).\n\nUsage:\n```\n@el [padding 40]\n @behind\n @el [background #fef3c7, width fill, height fill]\n Foreground text\n```"
916+ }
911 "@spacer" => {
917 "@spacer" => {
912 "**@spacer** \u{2014} Flexible spacer\n\nRenders as `<div>` with `flex: 1`. Pushes siblings apart in flex containers."
918 "**@spacer** \u{2014} Flexible spacer\n\nRenders as `<div>` with `flex: 1`. Pushes siblings apart in flex containers."
913 }
919 }
tests/snapshots.rs+78
@@ -2551,6 +2551,84 @@ fn element_stack() {
2551 );
2551 );
2552}
2552}
2553
2553
2554+// --- @in-front / @behind overlay layers ---
2555+
2556+#[test]
2557+fn element_in_front() {
2558+ let output = compile(
2559+ "@page T\n@el [width 200, height 200]\n Main\n @in-front\n @text Overlay",
2560+ );
2561+ assert!(
2562+ output.contains("position:absolute"),
2563+ "@in-front should have position:absolute: {}",
2564+ output
2565+ );
2566+ assert!(
2567+ output.contains("inset:0"),
2568+ "@in-front should have inset:0: {}",
2569+ output
2570+ );
2571+}
2572+
2573+#[test]
2574+fn element_behind() {
2575+ let output =
2576+ compile("@page T\n@el [width 200, height 200]\n @behind\n @text Bg\n Foreground");
2577+ assert!(
2578+ output.contains("z-index:-1"),
2579+ "@behind should have z-index:-1: {}",
2580+ output
2581+ );
2582+ assert!(
2583+ output.contains("position:absolute"),
2584+ "@behind should have position:absolute: {}",
2585+ output
2586+ );
2587+}
2588+
2589+#[test]
2590+fn in_front_makes_parent_positioning_context() {
2591+ let output =
2592+ compile("@page T\n@el [width 200, height 200]\n Main\n @in-front\n Overlay");
2593+ assert!(
2594+ output.contains("position:relative"),
2595+ "parent of @in-front should be position:relative: {}",
2596+ output
2597+ );
2598+ assert!(
2599+ output.contains("isolation:isolate"),
2600+ "parent of @in-front should isolate stacking context: {}",
2601+ output
2602+ );
2603+}
2604+
2605+#[test]
2606+fn in_front_does_not_override_explicit_position() {
2607+ let output = compile(
2608+ "@page T\n@el [position absolute, top 0, left 0]\n Main\n @in-front\n Overlay",
2609+ );
2610+ assert!(
2611+ !output.contains("position:relative"),
2612+ "explicit position:absolute should win over auto position:relative: {}",
2613+ output
2614+ );
2615+ assert!(
2616+ output.contains("position:absolute"),
2617+ "explicit position:absolute should be present: {}",
2618+ output
2619+ );
2620+}
2621+
2622+#[test]
2623+fn parent_without_overlay_children_stays_static() {
2624+ let output = compile("@page T\n@el [width 200, height 200]\n Main");
2625+ assert!(
2626+ !output.contains("isolation:isolate"),
2627+ "elements without @in-front/@behind children should not get isolation: {}",
2628+ output
2629+ );
2630+}
2631+
2554// --- Spacer element ---
2632// --- Spacer element ---
2555
2633
2556#[test]
2634#[test]