diff --git a/CHANGELOG.md b/CHANGELOG.md index c2df084..5a37cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,11 @@ ## unreleased * Support rendering tooltips. ([#26][#26]) +* Add focus outlines around nodes and edges. ([#27][#27]) +* Update `stroke_style: dashed` to mean `dasharray:4`. ([#27][#27]) [#26]: https://github.com/azriel91/disposition/pull/26 +[#27]: https://github.com/azriel91/disposition/pull/27 ## 0.1.0 (2026-04-11) diff --git a/Cargo.lock b/Cargo.lock index 4c9b1e9..6d58e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,9 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "askama_escape" @@ -1722,6 +1725,7 @@ dependencies = [ "emojis", "encre-css", "kurbo", + "linesweeper", "serde", "taffy", "typed-builder", @@ -3203,6 +3207,7 @@ checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" dependencies = [ "arrayvec", "euclid", + "serde", "smallvec", ] @@ -3302,6 +3307,19 @@ dependencies = [ "x11", ] +[[package]] +name = "linesweeper" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8421b276e96af0ace5f3d8d2d165d0dea07fe764d2fe94ec06bb1acaf8a1e759" +dependencies = [ + "arrayvec", + "kurbo", + "polycool", + "rustc-hash 2.1.2", + "smallvec", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4261,6 +4279,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -5268,6 +5295,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smithay-client-toolkit" diff --git a/Cargo.toml b/Cargo.toml index d0be963..fabd348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ gloo-timers = "0.4.0" id_newtype = "0.3.0" indexmap = "2.14.0" kurbo = "0.13.0" +linesweeper = "0.3.0" miette = "7.6.0" ordermap = "1.1.0" pretty_assertions = "1.4.1" diff --git a/crate/input_ir_rt/Cargo.toml b/crate/input_ir_rt/Cargo.toml index 06f2567..b8bfa9f 100644 --- a/crate/input_ir_rt/Cargo.toml +++ b/crate/input_ir_rt/Cargo.toml @@ -31,6 +31,7 @@ disposition_taffy_model = { workspace = true } emojis = { workspace = true } encre-css = { workspace = true } kurbo = { workspace = true } +linesweeper = { workspace = true } serde = { workspace = true, features = ["derive"] } taffy = { workspace = true } typed-builder = { workspace = true } diff --git a/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_class_state.rs b/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_class_state.rs index 70322c5..8a14a4c 100644 --- a/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_class_state.rs +++ b/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_class_state.rs @@ -1,7 +1,10 @@ -use std::{borrow::Cow, fmt::Write}; +use std::{ + borrow::Cow, + fmt::{self, Write}, +}; use disposition_input_model::theme::{DarkModeShadeConfig, ThemeAttr}; -use disposition_model_common::Map; +use disposition_model_common::{entity::EntityType, Map}; use super::{css_theme_vars::CssThemeVars, tailwind_color_shade::TailwindColorShade}; @@ -15,14 +18,20 @@ const CLASSES_BUFFER_WRITE_FAIL: &str = "Failed to write string to buffer"; pub(crate) struct TailwindClassState<'tw_state> { /// Map of theme attributes to their resolved values. pub(crate) attrs: Map>, + /// The first entity type of the entity these classes are built for. + /// + /// Used to determine whether outline classes should be prefixed with + /// `[&>.locus]:` (for edge entities) or applied directly (for nodes). + pub(crate) entity_type: Option, } impl<'tw_state> TailwindClassState<'tw_state> { /// Convert stroke style to stroke-dasharray value. fn stroke_style_to_dasharray(style: &str) -> Option<&str> { match style { + // `stroke-dasharray: none` in CSS produces a solid line. "solid" => Some("none"), - "dashed" => Some("3"), + "dashed" => Some("4"), "dotted" => Some("2"), s if s.starts_with("dasharray:") => Some(&s["dasharray:".len()..]), _ => None, @@ -225,6 +234,36 @@ impl<'tw_state> TailwindClassState<'tw_state> { .map(|c| c.as_ref()) } + /// Get the resolved outline color for a state. + fn get_outline_color(&self, state: HighlightState) -> Option<&str> { + let (state_specific, base) = match state { + HighlightState::Normal => (ThemeAttr::OutlineColorNormal, ThemeAttr::OutlineColor), + HighlightState::Focus => (ThemeAttr::OutlineColorFocus, ThemeAttr::OutlineColor), + HighlightState::Hover => (ThemeAttr::OutlineColorHover, ThemeAttr::OutlineColor), + HighlightState::Active => (ThemeAttr::OutlineColorActive, ThemeAttr::OutlineColor), + }; + + self.attrs + .get(&state_specific) + .or_else(|| self.attrs.get(&base)) + .map(|c| c.as_ref()) + } + + /// Get the resolved outline shade for a state. + fn get_outline_shade(&self, state: HighlightState) -> Option<&str> { + let (state_specific, base) = match state { + HighlightState::Normal => (ThemeAttr::OutlineShadeNormal, ThemeAttr::OutlineShade), + HighlightState::Focus => (ThemeAttr::OutlineShadeFocus, ThemeAttr::OutlineShade), + HighlightState::Hover => (ThemeAttr::OutlineShadeHover, ThemeAttr::OutlineShade), + HighlightState::Active => (ThemeAttr::OutlineShadeActive, ThemeAttr::OutlineShade), + }; + + self.attrs + .get(&state_specific) + .or_else(|| self.attrs.get(&base)) + .map(|c| c.as_ref()) + } + /// Get the resolved stroke color for a state. fn get_stroke_color(&self, state: HighlightState) -> Option<&str> { let (state_specific, base, shape) = match state { @@ -272,6 +311,44 @@ impl<'tw_state> TailwindClassState<'tw_state> { .map(|c| c.as_ref()) } + /// Get the resolved stroke style for a state. + /// + /// Looks up the state-specific attribute first (e.g. + /// [`ThemeAttr::StrokeStyleHover`]) and falls back to the base + /// [`ThemeAttr::StrokeStyle`] if the state-specific one is absent. + fn get_stroke_style(&self, state: HighlightState) -> Option<&str> { + let (state_specific, base) = match state { + HighlightState::Normal => (ThemeAttr::StrokeStyleNormal, ThemeAttr::StrokeStyle), + HighlightState::Focus => (ThemeAttr::StrokeStyleFocus, ThemeAttr::StrokeStyle), + HighlightState::Hover => (ThemeAttr::StrokeStyleHover, ThemeAttr::StrokeStyle), + HighlightState::Active => (ThemeAttr::StrokeStyleActive, ThemeAttr::StrokeStyle), + }; + + self.attrs + .get(&state_specific) + .or_else(|| self.attrs.get(&base)) + .map(|c| c.as_ref()) + } + + /// Get the resolved outline style for a state. + /// + /// Looks up the state-specific attribute first (e.g. + /// [`ThemeAttr::OutlineStyleHover`]) and falls back to the base + /// [`ThemeAttr::OutlineStyle`] if the state-specific one is absent. + fn get_outline_style(&self, state: HighlightState) -> Option<&str> { + let (state_specific, base) = match state { + HighlightState::Normal => (ThemeAttr::OutlineStyleNormal, ThemeAttr::OutlineStyle), + HighlightState::Focus => (ThemeAttr::OutlineStyleFocus, ThemeAttr::OutlineStyle), + HighlightState::Hover => (ThemeAttr::OutlineStyleHover, ThemeAttr::OutlineStyle), + HighlightState::Active => (ThemeAttr::OutlineStyleActive, ThemeAttr::OutlineStyle), + }; + + self.attrs + .get(&state_specific) + .or_else(|| self.attrs.get(&base)) + .map(|c| c.as_ref()) + } + // === Class Writers === // /// Write tailwind classes to the given string. @@ -307,33 +384,51 @@ impl<'tw_state> TailwindClassState<'tw_state> { pub(crate) fn write_peer_classes( &self, classes: &mut String, - prefix: &str, + peer_prefix_maybe: &str, css_theme_vars: &mut CssThemeVars, dark_mode_shade_config: DarkModeShadeConfig, ) { // Visibility if let Some(visibility) = self.attrs.get(&ThemeAttr::Visibility) { - writeln!(classes, "{prefix}{visibility}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(classes, "{peer_prefix_maybe}{visibility}").expect(CLASSES_BUFFER_WRITE_FAIL); } - // Stroke dasharray from stroke_style - if let Some(style) = self.attrs.get(&ThemeAttr::StrokeStyle) - && let Some(dasharray) = Self::stroke_style_to_dasharray(style) - { - writeln!(classes, "{prefix}[stroke-dasharray:{dasharray}]") + let stroke_style_normal = self.get_stroke_style(HighlightState::Normal); + let stroke_style_hover = self.get_stroke_style(HighlightState::Hover); + let stroke_style_focus = self.get_stroke_style(HighlightState::Focus); + let stroke_style_active = self.get_stroke_style(HighlightState::Active); + + // Stroke dasharray from stroke_style (per-state, with base fallback) + for (state_modifier, stroke_style) in [ + ("", stroke_style_normal), + ("hover:", stroke_style_hover), + ("focus:", stroke_style_focus), + ("active:", stroke_style_active), + ] { + if let Some(style) = stroke_style + && let Some(dasharray) = Self::stroke_style_to_dasharray(style) + { + writeln!( + classes, + "{peer_prefix_maybe}{state_modifier}[stroke-dasharray:{dasharray}]" + ) .expect(CLASSES_BUFFER_WRITE_FAIL); + } } // Stroke width if let Some(width) = self.attrs.get(&ThemeAttr::StrokeWidth) { - writeln!(classes, "{prefix}stroke-{width}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(classes, "{peer_prefix_maybe}stroke-{width}") + .expect(CLASSES_BUFFER_WRITE_FAIL); } if let Some(opacity) = self.attrs.get(&ThemeAttr::Opacity) { - writeln!(classes, "{prefix}opacity-{opacity}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(classes, "{peer_prefix_maybe}opacity-{opacity}") + .expect(CLASSES_BUFFER_WRITE_FAIL); } if let Some(animate) = self.attrs.get(&ThemeAttr::Animate) { - writeln!(classes, "{prefix}animate-{animate}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(classes, "{peer_prefix_maybe}animate-{animate}") + .expect(CLASSES_BUFFER_WRITE_FAIL); } let fill_color_hover = self.get_fill_color(HighlightState::Hover); @@ -361,9 +456,10 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, + None, "hover:", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_hover, fill_shade_hover, @@ -375,9 +471,10 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, + None, "", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_normal, fill_shade_normal, @@ -389,9 +486,10 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, + None, "focus:", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_focus, fill_shade_focus, @@ -403,9 +501,10 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, + None, "active:", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_active, fill_shade_active, @@ -417,63 +516,209 @@ impl<'tw_state> TailwindClassState<'tw_state> { // === Stroke classes === // // Stroke also uses shade shifting for dark mode. + // + // Skip color/shade classes for states where stroke_style is "none": in + // SVG specifying a stroke color will draw the stroke regardless of + // style, unlike HTML where `border-style: none` prevents drawing. + + for (state_modifier, color, shade, stroke_style) in [ + ( + "hover:", + stroke_color_hover, + stroke_shade_hover, + stroke_style_hover, + ), + ( + "", + stroke_color_normal, + stroke_shade_normal, + stroke_style_normal, + ), + ( + "focus:", + stroke_color_focus, + stroke_shade_focus, + stroke_style_focus, + ), + ( + "active:", + stroke_color_active, + stroke_shade_active, + stroke_style_active, + ), + ] { + if stroke_style != Some("none") { + Self::write_shifted_shade_class( + classes, + css_theme_vars, + peer_prefix_maybe, + None, + state_modifier, + ColorTarget::Stroke, + dark_mode_shade_config, + color, + shade, + stroke_shade_normal, + stroke_shade_hover, + stroke_shade_focus, + stroke_shade_active, + ); + } + } - Self::write_shifted_shade_class( - classes, - css_theme_vars, - prefix, - "hover:", - "stroke", - dark_mode_shade_config, - stroke_color_hover, - stroke_shade_hover, - stroke_shade_normal, - stroke_shade_hover, - stroke_shade_focus, - stroke_shade_active, - ); - Self::write_shifted_shade_class( - classes, - css_theme_vars, - prefix, - "", - "stroke", - dark_mode_shade_config, - stroke_color_normal, - stroke_shade_normal, - stroke_shade_normal, - stroke_shade_hover, - stroke_shade_focus, - stroke_shade_active, - ); - Self::write_shifted_shade_class( - classes, - css_theme_vars, - prefix, - "focus:", - "stroke", - dark_mode_shade_config, - stroke_color_focus, - stroke_shade_focus, - stroke_shade_normal, - stroke_shade_hover, - stroke_shade_focus, - stroke_shade_active, - ); - Self::write_shifted_shade_class( - classes, - css_theme_vars, - prefix, - "active:", - "stroke", - dark_mode_shade_config, - stroke_color_active, - stroke_shade_active, - stroke_shade_normal, - stroke_shade_hover, - stroke_shade_focus, - stroke_shade_active, - ); + // === Outline classes === // + // + // For edge entities the outline classes target `.locus` children + // via the `[&>.locus]:` arbitrary-variant prefix. For all other + // entities the classes are applied directly. + + let is_edge = self.entity_type.as_ref().is_some_and(EntityType::is_edge); + let locus_selector_prefix = if is_edge { Some("[&>.locus]:") } else { None }; + let locus_selector_prefix_str = locus_selector_prefix.unwrap_or(""); + + let outline_style_normal = self.get_outline_style(HighlightState::Normal); + let outline_style_hover = self.get_outline_style(HighlightState::Hover); + let outline_style_focus = self.get_outline_style(HighlightState::Focus); + let outline_style_active = self.get_outline_style(HighlightState::Active); + + // Outline style (base applies to all states; per-state variants override) + // + // For non-edge entities, the standard `outline-{style}` tailwind class is + // used (e.g. `outline-solid`, `outline-dashed`). For edge entities, the SVG + // `` outline does not support CSS `outline-style`; instead, + // `stroke_style_to_dasharray` converts the style to a `stroke-dasharray` + // value applied to the `.locus` path element. + let write_outline_style = if is_edge { + Self::write_outline_style_edge + } else { + Self::write_outline_style_node + }; + for (state_modifier, outline_style) in [ + ("", outline_style_normal), + ("hover:", outline_style_hover), + ("focus:", outline_style_focus), + ("active:", outline_style_active), + ] { + if let Some(style) = outline_style { + write_outline_style( + classes, + peer_prefix_maybe, + locus_selector_prefix, + state_modifier, + style, + ); + } + } + + // Outline width + if let Some(width) = self.attrs.get(&ThemeAttr::OutlineWidth) { + if is_edge { + writeln!( + classes, + "{peer_prefix_maybe}{locus_selector_prefix_str}stroke-{width}" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } else { + writeln!( + classes, + "{peer_prefix_maybe}{locus_selector_prefix_str}outline-{width}" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + } + + // Outline color and shade (similar to stroke, using "outline" as the property) + // + // When a shade is also available, `write_shifted_shade_class` is used for + // dark-mode support. When only a color is specified (no shade), an arbitrary + // CSS property class `[outline-color:{color}]` is written instead. + // + // For edge entities, the `.edge_locus` `` element is styled via SVG + // `stroke` rather than CSS `outline`, so `"stroke"` is used as the property + // and `[stroke:{color}]` as the color-only fallback. + let outline_color_target = if is_edge { + ColorTarget::Stroke + } else { + ColorTarget::Outline + }; + let outline_color_css_prop = if is_edge { "stroke" } else { "outline-color" }; + + let outline_color_hover = self.get_outline_color(HighlightState::Hover); + let outline_shade_hover = self.get_outline_shade(HighlightState::Hover); + let outline_color_normal = self.get_outline_color(HighlightState::Normal); + let outline_shade_normal = self.get_outline_shade(HighlightState::Normal); + let outline_color_focus = self.get_outline_color(HighlightState::Focus); + let outline_shade_focus = self.get_outline_shade(HighlightState::Focus); + let outline_color_active = self.get_outline_color(HighlightState::Active); + let outline_shade_active = self.get_outline_shade(HighlightState::Active); + + // When outline_style is "none" for a state: + // - For edge entities: emit `[stroke:none]` on the `.locus` prefix so the SVG + // stroke is explicitly cleared for that state. Without this, a stroke color + // inherited or set by another state would still be visible, because in SVG + // there is no equivalent of `border-style: none` to suppress drawing. + // - For non-edge entities: skip entirely, since CSS `outline-style: none` + // already prevents the outline from being drawn. + for (state_modifier, color, shade, outline_style) in [ + ( + "hover:", + outline_color_hover, + outline_shade_hover, + outline_style_hover, + ), + ( + "", + outline_color_normal, + outline_shade_normal, + outline_style_normal, + ), + ( + "focus:", + outline_color_focus, + outline_shade_focus, + outline_style_focus, + ), + ( + "active:", + outline_color_active, + outline_shade_active, + outline_style_active, + ), + ] { + if outline_style == Some("none") { + if is_edge { + writeln!( + classes, + "{peer_prefix_maybe}{state_modifier}{locus_selector_prefix_str}[stroke:none]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + continue; + } + if shade.is_some() { + Self::write_shifted_shade_class( + classes, + css_theme_vars, + peer_prefix_maybe, + locus_selector_prefix, + state_modifier, + outline_color_target, + dark_mode_shade_config, + color, + shade, + outline_shade_normal, + outline_shade_hover, + outline_shade_focus, + outline_shade_active, + ); + } else if let Some(color) = color { + writeln!( + classes, + "{peer_prefix_maybe}{state_modifier}{locus_selector_prefix_str}[{outline_color_css_prop}:{color}]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + } // === Text classes === // // Text uses shade inversion for dark mode. @@ -504,6 +749,42 @@ impl<'tw_state> TailwindClassState<'tw_state> { } } + /// Writes outline style classes for edge entities, which uses + /// `stroke-dasharray` to simulate an outline. + fn write_outline_style_edge( + classes: &mut String, + peer_prefix: &str, + subelement_selector_prefix: Option<&str>, + state_modifier: &str, + outline_style: &str, + ) { + let subelement_selector_prefix = subelement_selector_prefix.unwrap_or(""); + if let Some(dasharray) = Self::stroke_style_to_dasharray(outline_style) { + writeln!( + classes, + "{peer_prefix}{state_modifier}{subelement_selector_prefix}[stroke-dasharray:{dasharray}]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + } + + /// Writes outline style classes for node entities, which uses the `outline` + /// tailwind classes. + fn write_outline_style_node( + classes: &mut String, + peer_prefix: &str, + subelement_selector_prefix: Option<&str>, + state_modifier: &str, + outline_style: &str, + ) { + let subelement_selector_prefix = subelement_selector_prefix.unwrap_or(""); + writeln!( + classes, + "{peer_prefix}{state_modifier}{subelement_selector_prefix}outline-{outline_style}" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + /// Write a shade class for fill or stroke, handling all three dark-mode /// configurations. /// @@ -535,7 +816,8 @@ impl<'tw_state> TailwindClassState<'tw_state> { /// `"peer-[:focus-within]/tag:"` or `""`. /// * `state_modifier`: The highlight state modifier, e.g. `"hover:"`, /// `"focus:"`, `"active:"`, or `""` for normal. - /// * `property`: `"fill"` or `"stroke"`. + /// * `color_target`: The color target, e.g. `"fill"`, `"stroke"`, + /// `"outline"`. /// * `color`: The resolved colour name, e.g. `"yellow"`, `"slate"`. /// * `shade`: The resolved shade value for this state, e.g. `"100"`. /// * `dark_mode_shade_config`: Controls how dark-mode shades are computed. @@ -551,9 +833,10 @@ impl<'tw_state> TailwindClassState<'tw_state> { fn write_shifted_shade_class( classes: &mut String, css_theme_vars: &mut CssThemeVars, - prefix: &str, + peer_prefix: &str, + subelement_selector_prefix: Option<&str>, state_modifier: &str, - property: &str, + color_target: ColorTarget, dark_mode_shade_config: DarkModeShadeConfig, color: Option<&str>, shade: Option<&str>, @@ -562,30 +845,30 @@ impl<'tw_state> TailwindClassState<'tw_state> { shade_focus: Option<&str>, shade_active: Option<&str>, ) { + let subelement_selector_prefix = subelement_selector_prefix.unwrap_or(""); if let Some((color, shade)) = color.zip(shade) { + write!( + classes, + "{peer_prefix}{state_modifier}{subelement_selector_prefix}" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); match dark_mode_shade_config { DarkModeShadeConfig::Disable => { // No dark mode -- emit plain tailwind class. - writeln!( - classes, - "{prefix}{state_modifier}{property}-{color}-{shade}" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + color_target + .write_color_shade(classes, color, shade) + .expect(CLASSES_BUFFER_WRITE_FAIL); } DarkModeShadeConfig::Invert => { let dark_shade = Self::shade_inverted(shade); if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { - writeln!( - classes, - "{prefix}{state_modifier}{property}-[var({var_name})]" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + color_target + .write_css_var(classes, &var_name) + .expect(CLASSES_BUFFER_WRITE_FAIL); } else { - writeln!( - classes, - "{prefix}{state_modifier}{property}-{color}-{shade}" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + color_target + .write_color_shade(classes, color, shade) + .expect(CLASSES_BUFFER_WRITE_FAIL); } } DarkModeShadeConfig::Shift { levels } => { @@ -598,25 +881,22 @@ impl<'tw_state> TailwindClassState<'tw_state> { shade_active, ); if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { - writeln!( - classes, - "{prefix}{state_modifier}{property}-[var({var_name})]" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + color_target + .write_css_var(classes, &var_name) + .expect(CLASSES_BUFFER_WRITE_FAIL); } else { - writeln!( - classes, - "{prefix}{state_modifier}{property}-{color}-{shade}" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + color_target + .write_color_shade(classes, color, shade) + .expect(CLASSES_BUFFER_WRITE_FAIL); } } } + writeln!(classes).expect(CLASSES_BUFFER_WRITE_FAIL); } } } -/// States for fill and stroke colors. +/// States for fill, stroke, and outline colors. #[derive(Clone, Copy)] pub(crate) enum HighlightState { Normal, @@ -624,3 +904,45 @@ pub(crate) enum HighlightState { Hover, Active, } + +/// Whether it is the fill, stroke, or outline color. +#[derive(Clone, Copy)] +pub(crate) enum ColorTarget { + Fill, + Stroke, + Outline, +} + +impl ColorTarget { + pub(crate) fn write_color_shade( + self, + buffer: &mut String, + color: &str, + shade: &str, + ) -> fmt::Result { + match self { + ColorTarget::Fill => write!(buffer, "fill-{color}-{shade}"), + ColorTarget::Stroke => write!(buffer, "stroke-{color}-{shade}"), + ColorTarget::Outline => write!(buffer, "outline-{color}-{shade}"), + } + } + + /// Writes the CSS var for the color target to the buffer. + /// + /// This is needed because tailwind v4 docs say to use the same + /// `outline-[..]` syntax for arbitrary widths and arbitrary colors. + /// + /// For the outline widths, it should generate `outline-width: ..px` and for + /// colors, it should generate `outline-color: var(--color-)`. + /// + /// However, in practice when we use that syntax, encre-css only generates + /// the `outline-width: ..` CSS style for `outline-[..]` and the color var + /// is not generated. + pub(crate) fn write_css_var(self, buffer: &mut String, var_name: &str) -> fmt::Result { + match self { + ColorTarget::Fill => write!(buffer, "fill-[var({var_name})]"), + ColorTarget::Stroke => write!(buffer, "stroke-[var({var_name})]"), + ColorTarget::Outline => write!(buffer, "[outline-color:var({var_name})]"), + } + } +} diff --git a/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_classes_builder.rs b/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_classes_builder.rs index dcbf78f..5cef59f 100644 --- a/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_classes_builder.rs +++ b/crate/input_ir_rt/src/input_to_ir_diagram_mapper/tailwind_classes_builder.rs @@ -259,16 +259,23 @@ impl TailwindClassesBuilder { /// Build tailwind classes for a tag node. fn build_tag_tailwind_classes<'id>( - id: &Id<'id>, + tag_id: &Id<'id>, entity_types: &EntityTypes<'id>, theme_default: &ThemeDefault<'id>, theme_types_styles: &ThemeTypesStyles<'id>, css_theme_vars: &mut CssThemeVars, ) -> String { - let mut state = TailwindClassState::default(); + let entity_type = entity_types + .get(tag_id) + .and_then(|types| types.iter().next()) + .cloned(); + let mut state = TailwindClassState { + entity_type, + ..Default::default() + }; Self::resolve_tailwind_attrs( - id, + tag_id, entity_types, theme_default, theme_types_styles, @@ -284,23 +291,30 @@ impl TailwindClassesBuilder { ); // Tags get peer/{id} class - writeln!(&mut classes, "peer/{id}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(&mut classes, "peer/{tag_id}").expect(CLASSES_BUFFER_WRITE_FAIL); classes } /// Build tailwind classes for a process node. fn build_process_tailwind_classes<'id>( - id: &Id<'id>, + process_id: &Id<'id>, entity_types: &EntityTypes<'id>, theme_default: &ThemeDefault<'id>, theme_types_styles: &ThemeTypesStyles<'id>, css_theme_vars: &mut CssThemeVars, ) -> String { - let mut state = TailwindClassState::default(); + let entity_type = entity_types + .get(process_id) + .and_then(|types| types.iter().next()) + .cloned(); + let mut state = TailwindClassState { + entity_type, + ..Default::default() + }; Self::resolve_tailwind_attrs( - id, + process_id, entity_types, theme_default, theme_types_styles, @@ -316,24 +330,31 @@ impl TailwindClassesBuilder { ); // Processes get `peer/{id}` class - writeln!(&mut classes, "peer/{id}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(&mut classes, "peer/{process_id}").expect(CLASSES_BUFFER_WRITE_FAIL); classes } /// Build tailwind classes for a process step node. fn build_process_step_tailwind_classes<'id>( - id: &Id<'id>, + process_step_id: &Id<'id>, parent_process_id_and_diagram: Option<(&ProcessId<'id>, &ProcessDiagram<'id>)>, entity_types: &EntityTypes<'id>, theme_default: &ThemeDefault<'id>, theme_types_styles: &ThemeTypesStyles<'id>, css_theme_vars: &mut CssThemeVars, ) -> String { - let mut state = TailwindClassState::default(); + let entity_type = entity_types + .get(process_step_id) + .and_then(|types| types.iter().next()) + .cloned(); + let mut state = TailwindClassState { + entity_type, + ..Default::default() + }; Self::resolve_tailwind_attrs( - id, + process_step_id, entity_types, theme_default, theme_types_styles, @@ -379,7 +400,7 @@ impl TailwindClassesBuilder { }); } - writeln!(&mut classes, "peer/{id}").expect(CLASSES_BUFFER_WRITE_FAIL); + writeln!(&mut classes, "peer/{process_step_id}").expect(CLASSES_BUFFER_WRITE_FAIL); classes } @@ -397,7 +418,14 @@ impl TailwindClassesBuilder { thing_to_interaction_steps: &Map<&'f NodeId<'id>, Set<&'f ProcessStepId<'id>>>, css_theme_vars: &mut CssThemeVars, ) -> String { - let mut state = TailwindClassState::default(); + let entity_type = entity_types + .get(node_id.as_ref()) + .and_then(|types| types.iter().next()) + .cloned(); + let mut state = TailwindClassState { + entity_type, + ..Default::default() + }; Self::resolve_tailwind_attrs( node_id.as_ref(), @@ -601,7 +629,14 @@ impl TailwindClassesBuilder { interaction_process_step_ids: &[&ProcessStepId<'id>], css_theme_vars: &mut CssThemeVars, ) -> String { - let mut state = TailwindClassState::default(); + let entity_type = entity_types + .get(edge_group_id.as_ref()) + .and_then(|types| types.iter().next()) + .cloned(); + let mut state = TailwindClassState { + entity_type, + ..Default::default() + }; Self::resolve_tailwind_attrs( edge_group_id, @@ -666,7 +701,14 @@ impl TailwindClassesBuilder { theme_types_styles: &ThemeTypesStyles<'id>, css_theme_vars: &mut CssThemeVars, ) -> String { - let mut state = TailwindClassState::default(); + let entity_type = entity_types + .get(edge_id) + .and_then(|types| types.iter().next()) + .cloned(); + let mut state = TailwindClassState { + entity_type, + ..Default::default() + }; Self::resolve_tailwind_attrs( edge_id, diff --git a/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs b/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs index bf03e15..d6a3d0b 100644 --- a/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs +++ b/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs @@ -311,6 +311,7 @@ impl SvgElementsToSvgMapper { let edge_group_id = &svg_edge_info.edge_group_id; let path_d = &svg_edge_info.path_d; let arrow_head_path_d = &svg_edge_info.arrow_head_path_d; + let locus_path_d = &svg_edge_info.locus_path_d; // Build class attribute from tailwind_classes for the edge // First check for edge-specific classes, then fall back to edge group classes @@ -342,7 +343,7 @@ impl SvgElementsToSvgMapper { // animation tailwind classes under the key // `{edge_id}__arrow_head`. For dependency edges no such entry // exists, so we fall back to a plain `arrow_head` class. - let arrow_head_entity_key = format!("{edge_id}_arrow_head"); + let arrow_head_entity_key = format!("{edge_id}__arrow_head"); let arrow_head_class_attr = if let Ok(arrow_head_id) = disposition_model_common::Id::try_from(arrow_head_entity_key) { @@ -367,7 +368,8 @@ impl SvgElementsToSvgMapper { write!( content_buffer, "" ) @@ -384,6 +386,12 @@ impl SvgElementsToSvgMapper { "\ + \ `group-has-[#some_id:focus]` + /// - `[&>.edge_body]` -> `[&>.edge_body]` /// - `peer/some-peer:animate-[animation-name_2s_linear_infinite]` -> /// unchanged /// @@ -453,6 +462,12 @@ impl SvgElementsToSvgMapper { /// "group-has-[#my_element_id:hover]:fill-red-500" /// ); /// + /// // Class selectors have underscores escaped + /// assert_eq!( + /// SvgElementsToSvgMapper::escape_ids_in_brackets("[&>.edge_body]:stroke-blue-500"), + /// "[&>.edge_body]:stroke-blue-500" + /// ); + /// /// // Animation values are NOT escaped (no ID selector) /// assert_eq!( /// SvgElementsToSvgMapper::escape_ids_in_brackets( @@ -477,7 +492,8 @@ impl SvgElementsToSvgMapper { /// ``` pub fn escape_ids_in_brackets(classes: &str) -> String { let mut bracket_depth: u32 = 0; - let mut is_parsing_id = false; + let mut is_parsing_id_or_class = false; + let mut is_last_character_non_id = true; classes .chars() @@ -486,16 +502,20 @@ impl SvgElementsToSvgMapper { match c { '[' => { bracket_depth += 1; - is_parsing_id = false; + is_parsing_id_or_class = false; result.push(c); } ']' => { bracket_depth = bracket_depth.saturating_sub(1); - is_parsing_id = false; + is_parsing_id_or_class = false; result.push(c); } '#' if bracket_depth > 0 => { - is_parsing_id = true; + is_parsing_id_or_class = true; + result.push(c); + } + '.' if bracket_depth > 0 && is_last_character_non_id => { + is_parsing_id_or_class = true; result.push(c); } '"' if bracket_depth > 0 => { @@ -510,18 +530,24 @@ impl SvgElementsToSvgMapper { ')' if bracket_depth > 0 => { result.push_str(")"); } - '_' if bracket_depth > 0 && is_parsing_id => { + '_' if bracket_depth > 0 && is_parsing_id_or_class => { result.push_str("_"); } - // Characters that end an ID context (not valid in CSS IDs) - ':' | ' ' | ',' | '.' | '>' | '+' | '~' | '(' | ')' if is_parsing_id => { - is_parsing_id = false; + // Characters that end an ID or CSS class context (not valid in CSS IDs) + ':' | ' ' | ',' | '.' | '>' | '+' | '~' | '(' | ')' | '&' + if is_parsing_id_or_class => + { + is_parsing_id_or_class = false; result.push(c); } _ => { result.push(c); } } + + is_last_character_non_id = + matches!(c, ':' | ' ' | ',' | '.' | '>' | '+' | '~' | '(' | ')' | '&'); + result }) } diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper.rs index 3890254..35d7825 100644 --- a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper.rs +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper.rs @@ -12,6 +12,7 @@ use self::{ edge_animation_calculator::EdgeAnimationCalculator, edge_path_builder_pass_1::EdgePathBuilderPass1, edge_path_builder_pass_2::EdgePathBuilderPass2, + edge_path_locus_calculator::EdgePathLocusCalculator, process_step_heights::ProcessStepsHeight, process_step_heights_calculator::ProcessStepHeightsCalculator, string_char_replacer::StringCharReplacer, @@ -29,6 +30,7 @@ mod edge_face_contact_tracker; mod edge_model; mod edge_path_builder_pass_1; mod edge_path_builder_pass_2; +mod edge_path_locus_calculator; mod ortho_protrusion_calculator; mod process_step_heights; mod process_step_heights_calculator; diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/arrow_head_builder.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/arrow_head_builder.rs index d774a6e..e4875a8 100644 --- a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/arrow_head_builder.rs +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/arrow_head_builder.rs @@ -25,14 +25,14 @@ impl ArrowHeadBuilder { /// The arrowhead is a closed V whose tip sits at the `to` node end of the /// edge (the first point of the SVG path, since edge paths are built in /// reverse order). - pub(super) fn build_static_arrow_head(edge_path: &BezPath) -> String { + pub(super) fn build_static_arrow_head(edge_path: &BezPath) -> BezPath { let (tip, direction) = Self::tip_and_direction(edge_path); // Normalise the direction vector. let len = (direction.x * direction.x + direction.y * direction.y).sqrt(); if len < 1e-9 { // Degenerate – fall back to an invisible arrow. - return String::new(); + return BezPath::new(); } let dx = direction.x / len; let dy = direction.y / len; @@ -57,7 +57,7 @@ impl ArrowHeadBuilder { path.line_to(wing2); path.close_path(); - path.to_svg() + path } /// Returns an origin-centred arrowhead path string for an **interaction** @@ -65,14 +65,14 @@ impl ArrowHeadBuilder { /// /// The V-shape points in the +X direction so that CSS `offset-rotate: auto` /// will orient it correctly along the motion path. - pub(super) fn build_origin_arrow_head() -> String { + pub(super) fn build_origin_arrow_head() -> BezPath { let mut path = BezPath::new(); path.move_to(Point::new(-ARROW_HEAD_LENGTH, -ARROW_HEAD_HALF_WIDTH)); path.line_to(Point::ZERO); path.line_to(Point::new(-ARROW_HEAD_LENGTH, ARROW_HEAD_HALF_WIDTH)); path.close_path(); - path.to_svg() + path } // ------------------------------------------------------------------ diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs new file mode 100644 index 0000000..9886104 --- /dev/null +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs @@ -0,0 +1,56 @@ +use kurbo::{stroke, BezPath, Cap, Join, Stroke, StrokeOpts}; +use linesweeper::{BinaryOp, FillRule}; + +/// Width of the stroke expansion used to compute the edge locus, in pixels. +const LOCUS_STROKE_WIDTH: f64 = 10.0; + +/// Accuracy tolerance for path approximation when computing the locus. +const LOCUS_TOLERANCE: f64 = 0.1; + +/// Computes the locus (parallel curve / offset curve) around an edge path and +/// its arrow head, for use as a focus indicator. +/// +/// The locus is the outline of a stroke expansion that wraps both the edge +/// body and the arrow head, suitable for rendering as a dashed highlight when +/// the edge is focused. +#[derive(Clone, Copy, Debug)] +pub(super) struct EdgePathLocusCalculator; + +impl EdgePathLocusCalculator { + /// Computes the locus `BezPath` for the given edge body path and arrow + /// head path. + /// + /// The two paths are chained and then expanded via [`kurbo::stroke`] to + /// produce a filled shape whose outline represents the parallel curves + /// around the combined edge and arrow head. + /// + /// # Parameters + /// + /// * `edge_path` -- the `BezPath` for the edge body. + /// * `arrow_head_path` -- the `BezPath` for the edge's arrow head. + pub(super) fn calculate(edge_path: &BezPath, arrow_head_path: &BezPath) -> BezPath { + let style = Stroke::new(LOCUS_STROKE_WIDTH) + .with_join(Join::Round) + .with_caps(Cap::Round); + + let opts = StrokeOpts::default(); + let edge_locus = stroke(edge_path, &style, &opts, LOCUS_TOLERANCE); + let arrow_head_locus = stroke(arrow_head_path, &style, &opts, LOCUS_TOLERANCE); + + let contours = linesweeper::binary_op( + &edge_locus, + &arrow_head_locus, + FillRule::NonZero, + BinaryOp::Union, + ) + .unwrap_or_else(|e| panic!("Failed to compute union of locus paths: {e:?}")); + + // We expect only one contour, since the arrow head should overlap with the + // edge path body. + contours + .contours() + .next() + .map(|contour| contour.path.clone()) + .unwrap_or(edge_locus) + } +} diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs index 5e4e73e..367c3e8 100644 --- a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs @@ -25,7 +25,7 @@ use crate::taffy_to_svg_elements_mapper::{ edge_path_builder_pass_2::edge_path_builder_pass_2_ortho::OrthoProtrusionParams, ortho_protrusion_calculator::OrthoProtrusionCalculator, ArrowHeadBuilder, EdgeAnimationCalculator, EdgePathBuilderPass1, EdgePathBuilderPass2, - StringCharReplacer, + EdgePathLocusCalculator, StringCharReplacer, }; /// Builds [`SvgEdgeInfo`]s for all edges in the diagram from edge groups and @@ -220,7 +220,7 @@ impl SvgEdgeInfosBuilder { .map(|edge_entity_types| { edge_entity_types .iter() - .any(EntityType::is_interaction_edge_type) + .any(EntityType::is_interaction_edge) }) .unwrap_or(false); @@ -250,14 +250,26 @@ impl SvgEdgeInfosBuilder { let path_d = path.to_svg(); // Compute arrowhead path. - let arrow_head_path_d = if is_interaction_edge { + let (arrow_head_path, locus_path) = if is_interaction_edge { // Origin-centred V-shape; CSS offset-path handles // positioning and rotation. - ArrowHeadBuilder::build_origin_arrow_head() + let arrow_head_path = ArrowHeadBuilder::build_origin_arrow_head(); + // Positioned V-shape at the `to` node end of the edge. + let arrow_head_path_at_to_node = + ArrowHeadBuilder::build_static_arrow_head(&path); + let locus_path = + EdgePathLocusCalculator::calculate(&path, &arrow_head_path_at_to_node); + + (arrow_head_path, locus_path) } else { // Positioned V-shape at the `to` node end of the edge. - ArrowHeadBuilder::build_static_arrow_head(&path) + let arrow_head_path = ArrowHeadBuilder::build_static_arrow_head(&path); + let locus_path = EdgePathLocusCalculator::calculate(&path, &arrow_head_path); + + (arrow_head_path, locus_path) }; + let arrow_head_path_d = arrow_head_path.to_svg(); + let locus_path_d = locus_path.to_svg(); let tooltip = ir_diagram .entity_tooltips @@ -272,6 +284,7 @@ impl SvgEdgeInfosBuilder { edge.to.clone(), path_d, arrow_head_path_d, + locus_path_d, tooltip, )); }); @@ -962,7 +975,7 @@ impl SvgEdgeInfosBuilder { edge_animation_active, associated_process_steps, } = css_animation_append_params; - let edge_anim = EdgeAnimationCalculator::calculate( + let edge_animation = EdgeAnimationCalculator::calculate( edge_animation_params, edge_path_info, edge_group_path_or_visible_segments_length_max, @@ -976,24 +989,24 @@ impl SvgEdgeInfosBuilder { .get(&edge_id_owned) .cloned() .unwrap_or_default(); - let dasharray = edge_anim.dasharray; - let animation_name = edge_anim.animation_name; + let dasharray = edge_animation.dasharray; + let animation_name = edge_animation.animation_name; let animation_duration = - EdgeAnimationCalculator::format_duration(edge_anim.edge_animation_duration_s); + EdgeAnimationCalculator::format_duration(edge_animation.edge_animation_duration_s); let animation_classes = { let mut classes = format!("[stroke-dasharray:{dasharray}]"); match edge_animation_active { EdgeAnimationActive::Always => { classes.push_str(&format!( - "\nanimate-[{animation_name}_{animation_duration}s_linear_infinite]" + "\n[&>.edge_body]:animate-[{animation_name}_{animation_duration}s_linear_infinite]" )); } EdgeAnimationActive::OnProcessStepFocus => { associated_process_steps.iter().for_each(|process_step_id| { classes.push_str(&format!( "\ngroup-has-[#{process_step_id}:focus-within]:\ - animate-[{animation_name}_{animation_duration}s_linear_infinite]" + [&>.edge_body]:animate-[{animation_name}_{animation_duration}s_linear_infinite]" )); }); } @@ -1022,12 +1035,39 @@ impl SvgEdgeInfosBuilder { // (encre-css transforms these to spaces in the actual CSS value). StringCharReplacer::replace_inplace(&mut forward_path_svg, ' ', '_'); - let arrow_head_animation_name = &edge_anim.arrow_head_animation_name; + Self::css_animation_append_arrowhead_classes( + tailwind_classes, + edge_path_info, + edge_animation_active, + associated_process_steps, + &edge_animation.arrow_head_animation_name, + animation_duration, + forward_path_svg, + ); + + // Append CSS keyframes for both edge stroke and arrowhead. + if !css.is_empty() { + css.push('\n'); + } + css.push_str(&edge_animation.keyframe_css); + css.push_str(&edge_animation.arrow_head_keyframe_css); + } + /// Appends CSS classes for the arrowhead animation to the diagram's + /// tailwind classes. + fn css_animation_append_arrowhead_classes<'id>( + tailwind_classes: &mut EntityTailwindClasses<'id>, + edge_path_info: &EdgePathInfo<'_, 'id>, + edge_animation_active: EdgeAnimationActive, + associated_process_steps: &[&NodeId<'id>], + arrow_head_animation_name: &str, + animation_duration: String, + forward_path_svg: String, + ) { let arrow_head_classes = { let mut classes = format!( "[offset-path:path('{forward_path_svg}')]\n\ - [stroke-dasharray:none]" + [stroke-dasharray:none]" ); match edge_animation_active { EdgeAnimationActive::Always => classes.push_str(&format!( @@ -1039,7 +1079,7 @@ impl SvgEdgeInfosBuilder { .for_each(|process_step_id| { classes.push_str(&format!( "\ngroup-has-[#{process_step_id}:focus-within]:\ - animate-[{arrow_head_animation_name}_{animation_duration}s_linear_infinite]" + animate-[{arrow_head_animation_name}_{animation_duration}s_linear_infinite]" )); }); } @@ -1047,18 +1087,12 @@ impl SvgEdgeInfosBuilder { classes }; - let arrow_head_entity_id_str = format!("{}_arrow_head", edge_path_info.edge_id.as_str()); - let arrow_head_entity_id: Id<'id> = Id::try_from(arrow_head_entity_id_str) + let edge_id = &edge_path_info.edge_id; + let arrow_head_entity_id_str = format!("{edge_id}__arrow_head"); + let arrow_head_entity_id: Id<'static> = Id::try_from(arrow_head_entity_id_str) .expect("arrow head entity ID should be valid") .into_static(); tailwind_classes.insert(arrow_head_entity_id, arrow_head_classes); - - // Append CSS keyframes for both edge stroke and arrowhead. - if !css.is_empty() { - css.push('\n'); - } - css.push_str(&edge_anim.keyframe_css); - css.push_str(&edge_anim.arrow_head_keyframe_css); } /// Generates an edge ID from the edge group ID and edge index. diff --git a/crate/input_model/src/input_diagram.rs b/crate/input_model/src/input_diagram.rs index 68dffb4..e8cc371 100644 --- a/crate/input_model/src/input_diagram.rs +++ b/crate/input_model/src/input_diagram.rs @@ -472,6 +472,20 @@ fn base_style_aliases() -> StyleAliases<'static> { ], ), ), + // focus_outline + ( + StyleAlias::FocusOutline, + css_class_partials( + vec![], + vec![ + (ThemeAttr::OutlineStyle, "dashed"), + (ThemeAttr::OutlineStyleNormal, "none"), + (ThemeAttr::OutlineWidth, "2"), + (ThemeAttr::OutlineColor, "blue"), + (ThemeAttr::OutlineShade, "500"), + ], + ), + ), ] .into_iter() .collect() @@ -483,7 +497,11 @@ fn base_theme_styles() -> ThemeStyles<'static> { ( IdOrDefaults::NodeDefaults, css_class_partials( - vec![StyleAlias::ShadeLight, StyleAlias::PaddingNormal], + vec![ + StyleAlias::ShadeLight, + StyleAlias::PaddingNormal, + StyleAlias::FocusOutline, + ], vec![ (ThemeAttr::ShapeColor, "slate"), (ThemeAttr::StrokeStyle, "solid"), @@ -497,7 +515,10 @@ fn base_theme_styles() -> ThemeStyles<'static> { // edge_defaults ( IdOrDefaults::EdgeDefaults, - css_class_partials(vec![], vec![(ThemeAttr::TextColor, "neutral")]), + css_class_partials( + vec![StyleAlias::FocusOutline], + vec![(ThemeAttr::TextColor, "neutral")], + ), ), ] .into_iter() diff --git a/crate/input_model/src/theme/style_alias.rs b/crate/input_model/src/theme/style_alias.rs index 7acb5f9..03e9744 100644 --- a/crate/input_model/src/theme/style_alias.rs +++ b/crate/input_model/src/theme/style_alias.rs @@ -78,6 +78,11 @@ pub enum StyleAlias<'id> { StrokeDashedAnimatedRequest, /// Dashed stroke with animation for response direction. StrokeDashedAnimatedResponse, + /// Focus outline for nodes and edges. + /// + /// Applies a visible outline when the entity is focused or hovered, + /// and hides it otherwise. + FocusOutline, /// Custom user-defined style alias. Custom(Id<'id>), } @@ -123,6 +128,7 @@ impl<'id> StyleAlias<'id> { StyleAlias::StrokeDashedAnimated => "stroke_dashed_animated", StyleAlias::StrokeDashedAnimatedRequest => "stroke_dashed_animated_request", StyleAlias::StrokeDashedAnimatedResponse => "stroke_dashed_animated_response", + StyleAlias::FocusOutline => "focus_outline", StyleAlias::Custom(id) => id.as_str(), } } @@ -177,6 +183,7 @@ impl<'id> StyleAlias<'id> { StyleAlias::StrokeDashedAnimated => StyleAlias::StrokeDashedAnimated, StyleAlias::StrokeDashedAnimatedRequest => StyleAlias::StrokeDashedAnimatedRequest, StyleAlias::StrokeDashedAnimatedResponse => StyleAlias::StrokeDashedAnimatedResponse, + StyleAlias::FocusOutline => StyleAlias::FocusOutline, StyleAlias::Custom(id) => StyleAlias::Custom(id.into_static()), } } @@ -210,6 +217,7 @@ impl<'id> From> for StyleAlias<'id> { "stroke_dashed_animated" => StyleAlias::StrokeDashedAnimated, "stroke_dashed_animated_request" => StyleAlias::StrokeDashedAnimatedRequest, "stroke_dashed_animated_response" => StyleAlias::StrokeDashedAnimatedResponse, + "focus_outline" => StyleAlias::FocusOutline, _ => StyleAlias::Custom(id), } } @@ -252,7 +260,7 @@ impl Visitor<'_> for StyleAliasVisitor { `rounded_xl`, `rounded_2xl`, `rounded_3xl`, `rounded_4xl`, `fill_pale`, \ `shade_pale`, `shade_light`, `shade_medium`, `shade_dark`, \ `stroke_dashed_animated`, `stroke_dashed_animated_request`, \ - `stroke_dashed_animated_response`, or a custom identifier", + `stroke_dashed_animated_response`, `focus_outline`, or a custom identifier", ) } @@ -286,6 +294,7 @@ impl Visitor<'_> for StyleAliasVisitor { "stroke_dashed_animated" => StyleAlias::StrokeDashedAnimated, "stroke_dashed_animated_request" => StyleAlias::StrokeDashedAnimatedRequest, "stroke_dashed_animated_response" => StyleAlias::StrokeDashedAnimatedResponse, + "focus_outline" => StyleAlias::FocusOutline, _ => { let id = Id::try_from(value.to_owned()).map_err(serde::de::Error::custom)?; StyleAlias::Custom(id) diff --git a/crate/model_common/src/entity/entity_type.rs b/crate/model_common/src/entity/entity_type.rs index 8a58315..95dcf98 100644 --- a/crate/model_common/src/entity/entity_type.rs +++ b/crate/model_common/src/entity/entity_type.rs @@ -265,17 +265,55 @@ impl EntityType { } } - /// Returns `true` if this is an `Interaction*` variant. + /// Returns `true` if this is a `DependencyEdge*` or `InteractionEdge*` + /// variant. /// /// # Examples /// /// ```rust /// use disposition_model_common::entity::EntityType; /// - /// assert!(EntityType::InteractionEdgeSequenceForwardDefault.is_interaction_edge_type()); - /// assert!(!EntityType::ThingDefault.is_interaction_edge_type()); + /// assert!(EntityType::DependencyEdgeSequenceForwardDefault.is_edge()); + /// assert!(!EntityType::ThingDefault.is_edge()); /// ``` - pub fn is_interaction_edge_type(&self) -> bool { + pub fn is_edge(&self) -> bool { + self.is_dependency_edge() || self.is_interaction_edge() + } + + /// Returns `true` if this is a `DependencyEdge*` variant. + /// + /// # Examples + /// + /// ```rust + /// use disposition_model_common::entity::EntityType; + /// + /// assert!(EntityType::DependencyEdgeSequenceForwardDefault.is_dependency_edge()); + /// assert!(!EntityType::ThingDefault.is_dependency_edge()); + /// ``` + pub fn is_dependency_edge(&self) -> bool { + matches!( + self, + EntityType::DependencyEdgeSequenceDefault + | EntityType::DependencyEdgeCyclicDefault + | EntityType::DependencyEdgeSymmetricDefault + | EntityType::DependencyEdgeSequenceForwardDefault + | EntityType::DependencyEdgeCyclicForwardDefault + | EntityType::DependencyEdgeSymmetricForwardDefault + | EntityType::DependencyEdgeSymmetricReverseDefault + ) + } + + /// Returns `true` if this is an `InteractionEdge*` variant. + /// + /// # Examples + /// + /// ```rust + /// use disposition_model_common::entity::EntityType; + /// + /// assert!(EntityType::InteractionEdgeSequenceForwardDefault.is_interaction_edge()); + /// assert!(!EntityType::ThingDefault.is_interaction_edge()); + /// ``` + pub fn is_interaction_edge(&self) -> bool { matches!( self, EntityType::InteractionEdgeSequenceDefault diff --git a/crate/svg_model/src/svg_edge_info.rs b/crate/svg_model/src/svg_edge_info.rs index 09ea4f3..96512ee 100644 --- a/crate/svg_model/src/svg_edge_info.rs +++ b/crate/svg_model/src/svg_edge_info.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; /// * The `` element's coordinates and its `d` attribute. /// * Tailwind classes to define its styling and visibility. /// * The arrowhead `` element's `d` attribute. +/// * The locus `` element's `d` attribute for the focus indicator. #[cfg_attr( all(feature = "schemars", not(feature = "test")), derive(schemars::JsonSchema) @@ -32,6 +33,12 @@ pub struct SvgEdgeInfo<'id> { /// origin-centred V-shape that is animated along the edge path via CSS /// `offset-path`. pub arrow_head_path_d: String, + /// The SVG path `d` attribute for the edge locus (focus indicator). + /// + /// This is the outline of the stroke expansion around both the edge body + /// and the arrow head, rendered as a dashed highlight when the edge is + /// focused. Example value: `"M10,20 C30,40 50,60 70,80"`. + pub locus_path_d: String, /// Tooltip text to display when the edge is hovered. /// /// When non-empty, rendered as a `` element inside the edge's `<g>` @@ -41,6 +48,7 @@ pub struct SvgEdgeInfo<'id> { impl<'id> SvgEdgeInfo<'id> { /// Creates a new `SvgEdgeInfo`. + #[allow(clippy::too_many_arguments)] pub fn new( edge_id: EdgeId<'id>, edge_group_id: EdgeGroupId<'id>, @@ -48,6 +56,7 @@ impl<'id> SvgEdgeInfo<'id> { to_node_id: NodeId<'id>, path_d: String, arrow_head_path_d: String, + locus_path_d: String, tooltip: String, ) -> Self { Self { @@ -57,6 +66,7 @@ impl<'id> SvgEdgeInfo<'id> { to_node_id, path_d, arrow_head_path_d, + locus_path_d, tooltip, } } diff --git a/workspace_tests/src/base_diagram.yaml b/workspace_tests/src/base_diagram.yaml index d664c17..c549054 100644 --- a/workspace_tests/src/base_diagram.yaml +++ b/workspace_tests/src/base_diagram.yaml @@ -130,6 +130,12 @@ theme_default: stroke_style: "dashed" stroke_width: "2" animate: "[stroke-dashoffset-move_2s_linear_infinite]" + focus_outline: + outline_style: "dashed" + outline_style_normal: "none" + outline_width: "2" + outline_color: "blue" + outline_shade: "500" # The keys in this map can be: # @@ -141,7 +147,7 @@ theme_default: base_styles: node_defaults: # Vector of style aliases to apply. - style_aliases_applied: [shade_light, padding_normal] + style_aliases_applied: [shade_light, padding_normal, focus_outline] # Used for both fill and stroke colors. shape_color: "slate" stroke_style: "solid" @@ -150,6 +156,7 @@ theme_default: visibility: "visible" gap: "24.0" edge_defaults: + style_aliases_applied: [focus_outline] text_color: "neutral" process_step_selected_styles: diff --git a/workspace_tests/src/example_ir.yaml b/workspace_tests/src/example_ir.yaml index 0312051..dd4aa0d 100644 --- a/workspace_tests/src/example_ir.yaml +++ b/workspace_tests/src/example_ir.yaml @@ -310,57 +310,57 @@ entity_types: edge_ix_t_aws_ecr_repo__t_aws_ecs_cluster__push__0: - type_interaction_edge_sequence_forward_default tailwind_classes: - t_aws: "visible\n[stroke-dasharray:2]\nstroke-2\nhover:fill-[var(--tw-yellow-50-950)]\nfill-[var(--tw-yellow-100-900)]\nfocus:fill-[var(--tw-yellow-200-800)]\nactive:fill-[var(--tw-yellow-300-700)]\nhover:stroke-[var(--tw-yellow-100-900)]\nstroke-[var(--tw-yellow-200-800)]\nfocus:stroke-[var(--tw-yellow-300-700)]\nactive:stroke-[var(--tw-yellow-400-600)]\n[&>text]:fill-[var(--tw-neutral-800-200)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_iam: "visible\n[stroke-dasharray:3]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_iam_ecs_policy: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_ecr: "visible\n[stroke-dasharray:3]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_ecr_repo: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:stroke-2\npeer-[:focus-within]/tag_deployment:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_deployment:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_deployment:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_deployment:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_deployment:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:stroke-2\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:stroke-2\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:fill-[var(--tw-slate-300-700)]\n" - t_aws_ecr_repo_image_1: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-200-800)]\nfill-[var(--tw-sky-300-700)]\nfocus:fill-[var(--tw-sky-400-600)]\nactive:fill-[var(--tw-sky-500-500)]\nhover:stroke-[var(--tw-sky-300-700)]\nstroke-[var(--tw-sky-400-600)]\nfocus:stroke-[var(--tw-sky-500-500)]\nactive:stroke-[var(--tw-sky-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_ecr_repo_image_2: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-200-800)]\nfill-[var(--tw-sky-300-700)]\nfocus:fill-[var(--tw-sky-400-600)]\nactive:fill-[var(--tw-sky-500-500)]\nhover:stroke-[var(--tw-sky-300-700)]\nstroke-[var(--tw-sky-400-600)]\nfocus:stroke-[var(--tw-sky-500-500)]\nactive:stroke-[var(--tw-sky-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_ecs: "visible\n[stroke-dasharray:3]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_aws_ecs_cluster: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:stroke-2\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:fill-[var(--tw-slate-300-700)]\n" - t_aws_ecs_cluster_task: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_github: "visible\n[stroke-dasharray:2]\nstroke-2\nhover:fill-[var(--tw-neutral-50-950)]\nfill-[var(--tw-neutral-100-900)]\nfocus:fill-[var(--tw-neutral-200-800)]\nactive:fill-[var(--tw-neutral-300-700)]\nhover:stroke-[var(--tw-neutral-100-900)]\nstroke-[var(--tw-neutral-200-800)]\nfocus:stroke-[var(--tw-neutral-300-700)]\nactive:stroke-[var(--tw-neutral-400-600)]\n[&>text]:fill-[var(--tw-neutral-800-200)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_github_user_repo: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:stroke-2\npeer-[:focus-within]/tag_app_development:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_app_development:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_app_development:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_app_development:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_app_development:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/tag_deployment:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:stroke-2\npeer-[:focus-within]/tag_deployment:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_deployment:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_deployment:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_deployment:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_deployment:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:stroke-2\npeer-[:focus-within]/proc_app_dev_step_repository_clone:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:stroke-2\npeer-[:focus-within]/proc_app_release_step_pull_request_open:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:stroke-2\npeer-[:focus-within]/proc_app_release_step_tag_and_push:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:stroke-2\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:stroke-2\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:fill-[var(--tw-slate-300-700)]\n" - t_localhost: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:stroke-2\npeer-[:focus-within]/tag_app_development:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_app_development:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_app_development:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_app_development:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_app_development:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/tag_deployment:opacity-75\npeer-[:focus-within]/proc_app_dev_step_repository_clone:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:stroke-2\npeer-[:focus-within]/proc_app_dev_step_repository_clone:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_dev_step_project_build:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_project_build:stroke-2\npeer-[:focus-within]/proc_app_dev_step_project_build:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_dev_step_project_build:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_dev_step_project_build:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_dev_step_project_build:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_dev_step_project_build:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:stroke-2\npeer-[:focus-within]/proc_app_release_step_crate_version_update:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:stroke-2\npeer-[:focus-within]/proc_app_release_step_pull_request_open:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:stroke-2\npeer-[:focus-within]/proc_app_release_step_tag_and_push:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:fill-[var(--tw-slate-300-700)]\n" - t_localhost_repo: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_localhost_repo_src: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_localhost_repo_target: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_localhost_repo_target_file_zip: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - t_localhost_repo_target_dist_dir: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" - tag_app_development: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-emerald-400-600)]\nfill-[var(--tw-emerald-500-500)]\nfocus:fill-[var(--tw-emerald-600-400)]\nactive:fill-[var(--tw-emerald-700-300)]\nhover:stroke-[var(--tw-emerald-500-500)]\nstroke-[var(--tw-emerald-600-400)]\nfocus:stroke-[var(--tw-emerald-700-300)]\nactive:stroke-[var(--tw-emerald-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/tag_app_development\n" - tag_deployment: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-emerald-400-600)]\nfill-[var(--tw-emerald-500-500)]\nfocus:fill-[var(--tw-emerald-600-400)]\nactive:fill-[var(--tw-emerald-700-300)]\nhover:stroke-[var(--tw-emerald-500-500)]\nstroke-[var(--tw-emerald-600-400)]\nfocus:stroke-[var(--tw-emerald-700-300)]\nactive:stroke-[var(--tw-emerald-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/tag_deployment\n" - proc_app_dev: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-blue-400-600)]\nfill-[var(--tw-blue-500-500)]\nfocus:fill-[var(--tw-blue-600-400)]\nactive:fill-[var(--tw-blue-700-300)]\nhover:stroke-[var(--tw-blue-500-500)]\nstroke-[var(--tw-blue-600-400)]\nfocus:stroke-[var(--tw-blue-700-300)]\nactive:stroke-[var(--tw-blue-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/proc_app_dev\n" - proc_app_dev_step_repository_clone: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_dev:focus-within]:visible\ngroup-has-[#proc_app_dev_step_repository_clone:focus-within]:visible\ngroup-has-[#proc_app_dev_step_project_build:focus-within]:visible\npeer/proc_app_dev_step_repository_clone\n" - proc_app_dev_step_project_build: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_dev:focus-within]:visible\ngroup-has-[#proc_app_dev_step_repository_clone:focus-within]:visible\ngroup-has-[#proc_app_dev_step_project_build:focus-within]:visible\npeer/proc_app_dev_step_project_build\n" - proc_app_release: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-blue-400-600)]\nfill-[var(--tw-blue-500-500)]\nfocus:fill-[var(--tw-blue-600-400)]\nactive:fill-[var(--tw-blue-700-300)]\nhover:stroke-[var(--tw-blue-500-500)]\nstroke-[var(--tw-blue-600-400)]\nfocus:stroke-[var(--tw-blue-700-300)]\nactive:stroke-[var(--tw-blue-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/proc_app_release\n" - proc_app_release_step_crate_version_update: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_crate_version_update\n" - proc_app_release_step_pull_request_open: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_pull_request_open\n" - proc_app_release_step_tag_and_push: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_tag_and_push\n" - proc_app_release_step_gh_actions_build: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_gh_actions_build\n" - proc_app_release_step_gh_actions_publish: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_gh_actions_publish\n" - proc_i12e_region_tier_app_deploy: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-blue-400-600)]\nfill-[var(--tw-blue-500-500)]\nfocus:fill-[var(--tw-blue-600-400)]\nactive:fill-[var(--tw-blue-700-300)]\nhover:stroke-[var(--tw-blue-500-500)]\nstroke-[var(--tw-blue-600-400)]\nfocus:stroke-[var(--tw-blue-700-300)]\nactive:stroke-[var(--tw-blue-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/proc_i12e_region_tier_app_deploy\n" - proc_i12e_region_tier_app_deploy_step_ecs_cluster_update: "invisible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_i12e_region_tier_app_deploy:focus-within]:visible\ngroup-has-[#proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus-within]:visible\npeer/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update\n" - edge_dep_t_localhost__t_github_user_repo__pull: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" + t_aws: "visible\n[stroke-dasharray:2]\nhover:[stroke-dasharray:2]\nfocus:[stroke-dasharray:2]\nactive:[stroke-dasharray:2]\nstroke-2\nhover:fill-[var(--tw-yellow-50-950)]\nfill-[var(--tw-yellow-100-900)]\nfocus:fill-[var(--tw-yellow-200-800)]\nactive:fill-[var(--tw-yellow-300-700)]\nhover:stroke-[var(--tw-yellow-100-900)]\nstroke-[var(--tw-yellow-200-800)]\nfocus:stroke-[var(--tw-yellow-300-700)]\nactive:stroke-[var(--tw-yellow-400-600)]\n[&>text]:fill-[var(--tw-neutral-800-200)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_iam: "visible\n[stroke-dasharray:4]\nhover:[stroke-dasharray:4]\nfocus:[stroke-dasharray:4]\nactive:[stroke-dasharray:4]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_iam_ecs_policy: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_ecr: "visible\n[stroke-dasharray:4]\nhover:[stroke-dasharray:4]\nfocus:[stroke-dasharray:4]\nactive:[stroke-dasharray:4]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_ecr_repo: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:hover:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:focus:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:active:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:stroke-2\npeer-[:focus-within]/tag_deployment:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_deployment:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_deployment:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_deployment:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_deployment:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:stroke-2\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:stroke-2\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:fill-[var(--tw-slate-300-700)]\n" + t_aws_ecr_repo_image_1: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-200-800)]\nfill-[var(--tw-sky-300-700)]\nfocus:fill-[var(--tw-sky-400-600)]\nactive:fill-[var(--tw-sky-500-500)]\nhover:stroke-[var(--tw-sky-300-700)]\nstroke-[var(--tw-sky-400-600)]\nfocus:stroke-[var(--tw-sky-500-500)]\nactive:stroke-[var(--tw-sky-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_ecr_repo_image_2: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-200-800)]\nfill-[var(--tw-sky-300-700)]\nfocus:fill-[var(--tw-sky-400-600)]\nactive:fill-[var(--tw-sky-500-500)]\nhover:stroke-[var(--tw-sky-300-700)]\nstroke-[var(--tw-sky-400-600)]\nfocus:stroke-[var(--tw-sky-500-500)]\nactive:stroke-[var(--tw-sky-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_ecs: "visible\n[stroke-dasharray:4]\nhover:[stroke-dasharray:4]\nfocus:[stroke-dasharray:4]\nactive:[stroke-dasharray:4]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_aws_ecs_cluster: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:stroke-2\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:fill-[var(--tw-slate-300-700)]\n" + t_aws_ecs_cluster_task: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_github: "visible\n[stroke-dasharray:2]\nhover:[stroke-dasharray:2]\nfocus:[stroke-dasharray:2]\nactive:[stroke-dasharray:2]\nstroke-2\nhover:fill-[var(--tw-neutral-50-950)]\nfill-[var(--tw-neutral-100-900)]\nfocus:fill-[var(--tw-neutral-200-800)]\nactive:fill-[var(--tw-neutral-300-700)]\nhover:stroke-[var(--tw-neutral-100-900)]\nstroke-[var(--tw-neutral-200-800)]\nfocus:stroke-[var(--tw-neutral-300-700)]\nactive:stroke-[var(--tw-neutral-400-600)]\n[&>text]:fill-[var(--tw-neutral-800-200)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_github_user_repo: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:hover:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:focus:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:active:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:stroke-2\npeer-[:focus-within]/tag_app_development:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_app_development:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_app_development:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_app_development:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_app_development:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/tag_deployment:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:hover:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:focus:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:active:[stroke-dasharray:4]\npeer-[:focus-within]/tag_deployment:stroke-2\npeer-[:focus-within]/tag_deployment:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_deployment:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_deployment:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_deployment:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_deployment:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:stroke-2\npeer-[:focus-within]/proc_app_dev_step_repository_clone:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:stroke-2\npeer-[:focus-within]/proc_app_release_step_pull_request_open:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:stroke-2\npeer-[:focus-within]/proc_app_release_step_tag_and_push:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:stroke-2\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:stroke-2\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:fill-[var(--tw-slate-300-700)]\n" + t_localhost: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:hover:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:focus:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:active:[stroke-dasharray:4]\npeer-[:focus-within]/tag_app_development:stroke-2\npeer-[:focus-within]/tag_app_development:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/tag_app_development:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/tag_app_development:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/tag_app_development:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/tag_app_development:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/tag_deployment:opacity-75\npeer-[:focus-within]/proc_app_dev_step_repository_clone:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:stroke-2\npeer-[:focus-within]/proc_app_dev_step_repository_clone:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_dev_step_project_build:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_project_build:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_project_build:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_project_build:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_dev_step_project_build:stroke-2\npeer-[:focus-within]/proc_app_dev_step_project_build:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_dev_step_project_build:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_dev_step_project_build:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_dev_step_project_build:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_dev_step_project_build:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:stroke-2\npeer-[:focus-within]/proc_app_release_step_crate_version_update:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:stroke-2\npeer-[:focus-within]/proc_app_release_step_pull_request_open:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:fill-[var(--tw-slate-300-700)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:hover:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:[stroke-dasharray:4]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:stroke-2\npeer-[:focus-within]/proc_app_release_step_tag_and_push:animate-[stroke-dashoffset-move_2s_linear_infinite]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:hover:fill-[var(--tw-slate-50-950)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:fill-[var(--tw-slate-100-900)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:fill-[var(--tw-slate-200-800)]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:fill-[var(--tw-slate-300-700)]\n" + t_localhost_repo: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_localhost_repo_src: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_localhost_repo_target: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_localhost_repo_target_file_zip: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + t_localhost_repo_target_dist_dir: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-slate-200-800)]\nfill-[var(--tw-slate-300-700)]\nfocus:fill-[var(--tw-slate-400-600)]\nactive:fill-[var(--tw-slate-500-500)]\nhover:stroke-[var(--tw-slate-300-700)]\nstroke-[var(--tw-slate-400-600)]\nfocus:stroke-[var(--tw-slate-500-500)]\nactive:stroke-[var(--tw-slate-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/tag_app_development:opacity-50\npeer-[:focus-within]/tag_deployment:opacity-75\n" + tag_app_development: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-emerald-400-600)]\nfill-[var(--tw-emerald-500-500)]\nfocus:fill-[var(--tw-emerald-600-400)]\nactive:fill-[var(--tw-emerald-700-300)]\nhover:stroke-[var(--tw-emerald-500-500)]\nstroke-[var(--tw-emerald-600-400)]\nfocus:stroke-[var(--tw-emerald-700-300)]\nactive:stroke-[var(--tw-emerald-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/tag_app_development\n" + tag_deployment: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-emerald-400-600)]\nfill-[var(--tw-emerald-500-500)]\nfocus:fill-[var(--tw-emerald-600-400)]\nactive:fill-[var(--tw-emerald-700-300)]\nhover:stroke-[var(--tw-emerald-500-500)]\nstroke-[var(--tw-emerald-600-400)]\nfocus:stroke-[var(--tw-emerald-700-300)]\nactive:stroke-[var(--tw-emerald-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/tag_deployment\n" + proc_app_dev: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-blue-400-600)]\nfill-[var(--tw-blue-500-500)]\nfocus:fill-[var(--tw-blue-600-400)]\nactive:fill-[var(--tw-blue-700-300)]\nhover:stroke-[var(--tw-blue-500-500)]\nstroke-[var(--tw-blue-600-400)]\nfocus:stroke-[var(--tw-blue-700-300)]\nactive:stroke-[var(--tw-blue-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/proc_app_dev\n" + proc_app_dev_step_repository_clone: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_dev:focus-within]:visible\ngroup-has-[#proc_app_dev_step_repository_clone:focus-within]:visible\ngroup-has-[#proc_app_dev_step_project_build:focus-within]:visible\npeer/proc_app_dev_step_repository_clone\n" + proc_app_dev_step_project_build: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_dev:focus-within]:visible\ngroup-has-[#proc_app_dev_step_repository_clone:focus-within]:visible\ngroup-has-[#proc_app_dev_step_project_build:focus-within]:visible\npeer/proc_app_dev_step_project_build\n" + proc_app_release: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-blue-400-600)]\nfill-[var(--tw-blue-500-500)]\nfocus:fill-[var(--tw-blue-600-400)]\nactive:fill-[var(--tw-blue-700-300)]\nhover:stroke-[var(--tw-blue-500-500)]\nstroke-[var(--tw-blue-600-400)]\nfocus:stroke-[var(--tw-blue-700-300)]\nactive:stroke-[var(--tw-blue-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/proc_app_release\n" + proc_app_release_step_crate_version_update: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_crate_version_update\n" + proc_app_release_step_pull_request_open: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_pull_request_open\n" + proc_app_release_step_tag_and_push: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_tag_and_push\n" + proc_app_release_step_gh_actions_build: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_gh_actions_build\n" + proc_app_release_step_gh_actions_publish: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_app_release:focus-within]:visible\ngroup-has-[#proc_app_release_step_crate_version_update:focus-within]:visible\ngroup-has-[#proc_app_release_step_pull_request_open:focus-within]:visible\ngroup-has-[#proc_app_release_step_tag_and_push:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_build:focus-within]:visible\ngroup-has-[#proc_app_release_step_gh_actions_publish:focus-within]:visible\npeer/proc_app_release_step_gh_actions_publish\n" + proc_i12e_region_tier_app_deploy: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-blue-400-600)]\nfill-[var(--tw-blue-500-500)]\nfocus:fill-[var(--tw-blue-600-400)]\nactive:fill-[var(--tw-blue-700-300)]\nhover:stroke-[var(--tw-blue-500-500)]\nstroke-[var(--tw-blue-600-400)]\nfocus:stroke-[var(--tw-blue-700-300)]\nactive:stroke-[var(--tw-blue-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\npeer/proc_i12e_region_tier_app_deploy\n" + proc_i12e_region_tier_app_deploy_step_ecs_cluster_update: "invisible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-sky-400-600)]\nfill-[var(--tw-sky-500-500)]\nfocus:fill-[var(--tw-sky-600-400)]\nactive:fill-[var(--tw-sky-700-300)]\nhover:stroke-[var(--tw-sky-500-500)]\nstroke-[var(--tw-sky-600-400)]\nfocus:stroke-[var(--tw-sky-700-300)]\nactive:stroke-[var(--tw-sky-800-200)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\ngroup-has-[#proc_i12e_region_tier_app_deploy:focus-within]:visible\ngroup-has-[#proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus-within]:visible\npeer/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update\n" + edge_dep_t_localhost__t_github_user_repo__pull: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" edge_dep_t_localhost__t_github_user_repo__pull__0: | stroke-2 edge_dep_t_localhost__t_github_user_repo__pull__1: | stroke-2 - edge_dep_t_localhost__t_github_user_repo__push: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" + edge_dep_t_localhost__t_github_user_repo__push: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" edge_dep_t_localhost__t_github_user_repo__push__0: | stroke-2 - edge_dep_t_localhost__t_localhost__within: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" + edge_dep_t_localhost__t_localhost__within: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" edge_dep_t_localhost__t_localhost__within__0: | stroke-2 - edge_dep_t_github_user_repo__t_github_user_repo__within: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" + edge_dep_t_github_user_repo__t_github_user_repo__within: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" edge_dep_t_github_user_repo__t_github_user_repo__within__0: | stroke-2 edge_dep_t_github_user_repo__t_github_user_repo__within__1: | stroke-2 - edge_dep_t_github_user_repo__t_aws_ecr_repo__push: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" + edge_dep_t_github_user_repo__t_aws_ecr_repo__push: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" edge_dep_t_github_user_repo__t_aws_ecr_repo__push__0: | stroke-2 - edge_dep_t_aws_ecr_repo__t_aws_ecs_cluster__push: "visible\n[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" + edge_dep_t_aws_ecr_repo__t_aws_ecs_cluster__push: "visible\n[stroke-dasharray:none]\nhover:[stroke-dasharray:none]\nfocus:[stroke-dasharray:none]\nactive:[stroke-dasharray:none]\nstroke-2\nhover:fill-[var(--tw-neutral-500-500)]\nfill-[var(--tw-neutral-600-400)]\nfocus:fill-[var(--tw-neutral-700-300)]\nactive:fill-[var(--tw-neutral-800-200)]\nhover:stroke-[var(--tw-neutral-600-400)]\nstroke-[var(--tw-neutral-700-300)]\nfocus:stroke-[var(--tw-neutral-800-200)]\nactive:stroke-[var(--tw-neutral-900-100)]\n[&>text]:fill-[var(--tw-neutral-950-50)]\n" edge_dep_t_aws_ecr_repo__t_aws_ecs_cluster__push__0: | stroke-2 edge_ix_t_localhost__t_github_user_repo__pull: "invisible\nstroke-2\nhover:fill-[var(--tw-blue-200-800)]\nfill-[var(--tw-blue-300-700)]\nfocus:fill-[var(--tw-blue-400-600)]\nactive:fill-[var(--tw-blue-500-500)]\nhover:stroke-[var(--tw-blue-300-700)]\nstroke-[var(--tw-blue-400-600)]\nfocus:stroke-[var(--tw-blue-500-500)]\nactive:stroke-[var(--tw-blue-600-400)]\n[&>text]:fill-[var(--tw-neutral-900-100)]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:visible\npeer-[:focus-within]/proc_app_release_step_pull_request_open:visible\n" diff --git a/workspace_tests/src/input_ir_rt/input_to_ir_diagram_mapper.rs b/workspace_tests/src/input_ir_rt/input_to_ir_diagram_mapper.rs index f0aa55a..c3d6d23 100644 --- a/workspace_tests/src/input_ir_rt/input_to_ir_diagram_mapper.rs +++ b/workspace_tests/src/input_ir_rt/input_to_ir_diagram_mapper.rs @@ -732,13 +732,13 @@ fn test_tailwind_classes_stroke_style() { ); // t_aws_iam has type_service which has stroke_style: "dashed" - // dashed should map to stroke-dasharray:3 + // dashed should map to stroke-dasharray:4 let t_aws_iam_id = id!("t_aws_iam"); let t_aws_iam_classes = String::from("\n") + diagram.tailwind_classes.get(&t_aws_iam_id).unwrap(); assert!( - t_aws_iam_classes.contains("\n[stroke-dasharray:3]"), - "t_aws_iam should have stroke-dasharray:3 from dashed stroke_style. Got: {t_aws_iam_classes}" + t_aws_iam_classes.contains("\n[stroke-dasharray:4]"), + "t_aws_iam should have stroke-dasharray:4 from dashed stroke_style. Got: {t_aws_iam_classes}" ); } diff --git a/workspace_tests/src/input_ir_rt/taffy_to_svg_elements_mapper.rs b/workspace_tests/src/input_ir_rt/taffy_to_svg_elements_mapper.rs index a83f642..9da8a11 100644 --- a/workspace_tests/src/input_ir_rt/taffy_to_svg_elements_mapper.rs +++ b/workspace_tests/src/input_ir_rt/taffy_to_svg_elements_mapper.rs @@ -447,16 +447,17 @@ fn test_svg_edge_infos_interaction_arrow_head_is_origin_centred() -> Result<(), #[test] fn test_svg_edge_infos_interaction_arrow_head_tailwind_classes() -> Result<(), TaffyError> { for svg_elements in build_svg_elements_from_example_ir() { - let ix_edges: Vec<_> = svg_elements + let svg_edge_infos_ix: Vec<_> = svg_elements .svg_edge_infos .iter() - .filter(|e| e.edge_id.as_str().starts_with("edge_ix_")) + .filter(|svg_edge_info| svg_edge_info.edge_id.as_str().starts_with("edge_ix_")) .collect(); - for edge_info in &ix_edges { + for svg_edge_info in &svg_edge_infos_ix { // The arrowhead entity ID is `{edge_id}__arrow_head` (with // underscores, since `Id` only allows [a-zA-Z0-9_]). - let arrow_head_key_str = format!("{}_arrow_head", edge_info.edge_id.as_str()); + let edge_id = &svg_edge_info.edge_id; + let arrow_head_key_str = format!("{edge_id}__arrow_head"); let arrow_head_key = Id::try_from(arrow_head_key_str.clone()).expect("arrow head ID should be valid");