From 026f2dd0bf6e99bb392c421cf09f5d8d41bc5cda Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Wed, 22 Apr 2026 07:11:37 +1200 Subject: [PATCH 01/24] First pass of adding a locus to edges. --- .../src/svg_elements_to_svg_mapper.rs | 6 +++ .../src/taffy_to_svg_elements_mapper.rs | 2 + .../arrow_head_builder.rs | 10 ++--- .../edge_path_locus_calculator.rs | 41 +++++++++++++++++++ .../svg_edge_infos_builder.rs | 9 +++- crate/svg_model/src/svg_edge_info.rs | 9 ++++ 6 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs 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 bf03e154..3fdfcb31 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 @@ -385,6 +386,11 @@ impl SvgElementsToSvgMapper { d=\"{path_d}\" \ fill=\"none\" \ />\ + \ \ 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 3890254b..35d7825e 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 d774a6e3..e4875a8f 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 00000000..602a938b --- /dev/null +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs @@ -0,0 +1,41 @@ +use kurbo::{stroke, BezPath, Cap, Join, Stroke, StrokeOpts}; + +/// Width of the stroke expansion used to compute the edge locus, in pixels. +const LOCUS_STROKE_WIDTH: f64 = 8.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 combined = edge_path.into_iter().chain(arrow_head_path.into_iter()); + + let style = Stroke::new(LOCUS_STROKE_WIDTH) + .with_join(Join::Round) + .with_caps(Cap::Round); + + let opts = StrokeOpts::default(); + + stroke(combined, &style, &opts, LOCUS_TOLERANCE) + } +} 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 5e4e73ea..46448e89 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 @@ -250,7 +250,7 @@ impl SvgEdgeInfosBuilder { let path_d = path.to_svg(); // Compute arrowhead path. - let arrow_head_path_d = if is_interaction_edge { + let arrow_head_path = if is_interaction_edge { // Origin-centred V-shape; CSS offset-path handles // positioning and rotation. ArrowHeadBuilder::build_origin_arrow_head() @@ -258,6 +258,10 @@ impl SvgEdgeInfosBuilder { // Positioned V-shape at the `to` node end of the edge. ArrowHeadBuilder::build_static_arrow_head(&path) }; + let arrow_head_path_d = arrow_head_path.to_svg(); + + let locus_path = EdgePathLocusCalculator::calculate(&path, &arrow_head_path); + let locus_path_d = locus_path.to_svg(); let tooltip = ir_diagram .entity_tooltips @@ -272,6 +276,7 @@ impl SvgEdgeInfosBuilder { edge.to.clone(), path_d, arrow_head_path_d, + locus_path_d, tooltip, )); }); diff --git a/crate/svg_model/src/svg_edge_info.rs b/crate/svg_model/src/svg_edge_info.rs index 09ea4f3d..a9c635e0 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>` @@ -48,6 +55,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 +65,7 @@ impl<'id> SvgEdgeInfo<'id> { to_node_id, path_d, arrow_head_path_d, + locus_path_d, tooltip, } } From 657b5994e4b5106904cad3b48690fdbe9236975a Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Thu, 23 Apr 2026 06:37:27 +1200 Subject: [PATCH 02/24] Partial code to get tailwind styles added to outline elements. --- .../tailwind_class_state.rs | 179 ++++++++++++++++-- .../tailwind_classes_builder.rs | 72 +++++-- .../src/svg_elements_to_svg_mapper.rs | 2 +- .../svg_edge_infos_builder.rs | 2 +- crate/input_model/src/input_diagram.rs | 25 ++- crate/input_model/src/theme/style_alias.rs | 11 +- crate/model_common/src/entity/entity_type.rs | 46 ++++- workspace_tests/src/base_diagram.yaml | 9 +- 8 files changed, 306 insertions(+), 40 deletions(-) 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 70322c56..01894b8c 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,7 @@ use std::{borrow::Cow, fmt::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,6 +15,11 @@ 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<ThemeAttr, Cow<'tw_state, str>>, + /// The first entity type of the entity these classes are built for. + /// + /// Used to determine whether outline classes should be prefixed with + /// `[&>.edge_locus]:` (for edge entities) or applied directly (for nodes). + pub(crate) entity_type: Option<EntityType>, } impl<'tw_state> TailwindClassState<'tw_state> { @@ -225,6 +230,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 { @@ -307,33 +342,36 @@ 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}]") + writeln!(classes, "{peer_prefix_maybe}[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,7 +399,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "hover:", "fill", dark_mode_shade_config, @@ -375,7 +413,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "", "fill", dark_mode_shade_config, @@ -389,7 +427,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "focus:", "fill", dark_mode_shade_config, @@ -403,7 +441,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "active:", "fill", dark_mode_shade_config, @@ -421,7 +459,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "hover:", "stroke", dark_mode_shade_config, @@ -435,7 +473,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "", "stroke", dark_mode_shade_config, @@ -449,7 +487,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "focus:", "stroke", dark_mode_shade_config, @@ -463,7 +501,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - prefix, + peer_prefix_maybe, "active:", "stroke", dark_mode_shade_config, @@ -475,6 +513,117 @@ impl<'tw_state> TailwindClassState<'tw_state> { stroke_shade_active, ); + // === Outline classes === // + // + // For edge entities the outline classes target `.edge_locus` children + // via the `[&>.edge_locus]:` arbitrary-variant prefix. For all other + // entities the classes are applied directly. + + let outline_full_prefix = if self.entity_type.as_ref().is_some_and(EntityType::is_edge) { + Cow::Owned(format!("{peer_prefix_maybe}[&>.edge_locus]:")) + } else { + Cow::Borrowed(peer_prefix_maybe) + }; + + // Outline style (base applies to all states; per-state variants override) + let outline_style_base = self.attrs.get(&ThemeAttr::OutlineStyle); + if let Some(style) = self + .attrs + .get(&ThemeAttr::OutlineStyleNormal) + .or(outline_style_base) + .map(Cow::as_ref) + { + writeln!(classes, "{outline_full_prefix}[outline-style:{style}]") + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + if let Some(style) = self + .attrs + .get(&ThemeAttr::OutlineStyleHover) + .or(outline_style_base) + .map(Cow::as_ref) + { + writeln!( + classes, + "hover:{outline_full_prefix}[outline-style:{style}]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + if let Some(style) = self + .attrs + .get(&ThemeAttr::OutlineStyleFocus) + .or(outline_style_base) + .map(Cow::as_ref) + { + writeln!( + classes, + "focus:{outline_full_prefix}[outline-style:{style}]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + if let Some(style) = self + .attrs + .get(&ThemeAttr::OutlineStyleActive) + .or(outline_style_base) + .map(Cow::as_ref) + { + writeln!( + classes, + "active:{outline_full_prefix}[outline-style:{style}]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + + // Outline width + if let Some(width) = self.attrs.get(&ThemeAttr::OutlineWidth) { + writeln!(classes, "{outline_full_prefix}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. + + 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); + + for (state_modifier, color, shade) in [ + ("hover:", outline_color_hover, outline_shade_hover), + ("", outline_color_normal, outline_shade_normal), + ("focus:", outline_color_focus, outline_shade_focus), + ("active:", outline_color_active, outline_shade_active), + ] { + if shade.is_some() { + Self::write_shifted_shade_class( + classes, + css_theme_vars, + outline_full_prefix.as_ref(), + state_modifier, + "outline", + 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, + "{outline_full_prefix}{state_modifier}[outline-color:{color}]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + } + // === Text classes === // // Text uses shade inversion for dark mode. // The `[&>text]` selector is not prefixed with the peer prefix because 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 dcbf78fd..5cef59f2 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 3fdfcb31..229c2309 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 @@ -389,7 +389,7 @@ impl SvgElementsToSvgMapper { <path \ d=\"{locus_path_d}\" \ fill=\"none\" \ - class=\"[stroke-dasharray:3] stroke-2 stroke-blue-500 invisible group-has-[#{edge_id}:focus-within]:visible\" \ + class=\"edge_locus\" \ />\ <g \ {arrow_head_class_attr} \ 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 46448e89..57fd9b4a 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 @@ -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); diff --git a/crate/input_model/src/input_diagram.rs b/crate/input_model/src/input_diagram.rs index 68dffb4a..e8cc3710 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 7acb5f9d..03e97440 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<Id<'id>> 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 8a583150..95dcf98c 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/workspace_tests/src/base_diagram.yaml b/workspace_tests/src/base_diagram.yaml index d664c17f..c5490547 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: From 923909c90b1b383ae3a7eae3aab515b5e93e8b68 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Thu, 23 Apr 2026 07:01:55 +1200 Subject: [PATCH 03/24] Use `stroke` styles to simulate outline for edge entities. --- .../tailwind_class_state.rs | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) 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 01894b8c..685d7f4b 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 @@ -519,22 +519,33 @@ impl<'tw_state> TailwindClassState<'tw_state> { // via the `[&>.edge_locus]:` arbitrary-variant prefix. For all other // entities the classes are applied directly. - let outline_full_prefix = if self.entity_type.as_ref().is_some_and(EntityType::is_edge) { + let is_edge = self.entity_type.as_ref().is_some_and(EntityType::is_edge); + let outline_full_prefix = if is_edge { Cow::Owned(format!("{peer_prefix_maybe}[&>.edge_locus]:")) } else { Cow::Borrowed(peer_prefix_maybe) }; // 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 + // `<path>` outline does not support CSS `outline-style`; instead, + // `stroke_style_to_dasharray` converts the style to a `stroke-dasharray` + // value applied to the `.edge_locus` path element. let outline_style_base = self.attrs.get(&ThemeAttr::OutlineStyle); + let write_outline_style = if is_edge { + Self::write_outline_style_edge + } else { + Self::write_outline_style_node + }; if let Some(style) = self .attrs .get(&ThemeAttr::OutlineStyleNormal) .or(outline_style_base) .map(Cow::as_ref) { - writeln!(classes, "{outline_full_prefix}[outline-style:{style}]") - .expect(CLASSES_BUFFER_WRITE_FAIL); + write_outline_style(classes, outline_full_prefix.as_ref(), "", style); } if let Some(style) = self .attrs @@ -542,11 +553,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { .or(outline_style_base) .map(Cow::as_ref) { - writeln!( - classes, - "hover:{outline_full_prefix}[outline-style:{style}]" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + write_outline_style(classes, outline_full_prefix.as_ref(), "hover:", style); } if let Some(style) = self .attrs @@ -554,11 +561,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { .or(outline_style_base) .map(Cow::as_ref) { - writeln!( - classes, - "focus:{outline_full_prefix}[outline-style:{style}]" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + write_outline_style(classes, outline_full_prefix.as_ref(), "focus:", style); } if let Some(style) = self .attrs @@ -566,11 +569,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { .or(outline_style_base) .map(Cow::as_ref) { - writeln!( - classes, - "active:{outline_full_prefix}[outline-style:{style}]" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + write_outline_style(classes, outline_full_prefix.as_ref(), "active:", style); } // Outline width @@ -653,6 +652,38 @@ 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, + outline_full_prefix: &str, + state_modifier: &str, + style: &str, + ) { + if let Some(dasharray) = Self::stroke_style_to_dasharray(style) { + writeln!( + classes, + "{state_modifier}{outline_full_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, + state_modifier: &str, + style: &str, + outline_full_prefix: &str, + ) { + writeln!( + classes, + "{state_modifier}{outline_full_prefix}outline-{style}" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } + /// Write a shade class for fill or stroke, handling all three dark-mode /// configurations. /// From 530f68dcfc6070eb9cc2d34c565db6fb34ed3daf Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Thu, 23 Apr 2026 07:06:48 +1200 Subject: [PATCH 04/24] Use `locus` instead of `edge_locus` as workaround to escaping the underscore. --- .../input_to_ir_diagram_mapper/tailwind_class_state.rs | 10 +++++----- crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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 685d7f4b..1d46b8cf 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 @@ -18,7 +18,7 @@ pub(crate) struct TailwindClassState<'tw_state> { /// The first entity type of the entity these classes are built for. /// /// Used to determine whether outline classes should be prefixed with - /// `[&>.edge_locus]:` (for edge entities) or applied directly (for nodes). + /// `[&>.locus]:` (for edge entities) or applied directly (for nodes). pub(crate) entity_type: Option<EntityType>, } @@ -515,13 +515,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { // === Outline classes === // // - // For edge entities the outline classes target `.edge_locus` children - // via the `[&>.edge_locus]:` arbitrary-variant prefix. For all other + // 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 outline_full_prefix = if is_edge { - Cow::Owned(format!("{peer_prefix_maybe}[&>.edge_locus]:")) + Cow::Owned(format!("{peer_prefix_maybe}[&>.locus]:")) } else { Cow::Borrowed(peer_prefix_maybe) }; @@ -532,7 +532,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { // used (e.g. `outline-solid`, `outline-dashed`). For edge entities, the SVG // `<path>` outline does not support CSS `outline-style`; instead, // `stroke_style_to_dasharray` converts the style to a `stroke-dasharray` - // value applied to the `.edge_locus` path element. + // value applied to the `.locus` path element. let outline_style_base = self.attrs.get(&ThemeAttr::OutlineStyle); let write_outline_style = if is_edge { Self::write_outline_style_edge 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 229c2309..ef895ffb 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 @@ -389,7 +389,7 @@ impl SvgElementsToSvgMapper { <path \ d=\"{locus_path_d}\" \ fill=\"none\" \ - class=\"edge_locus\" \ + class=\"locus\" \ />\ <g \ {arrow_head_class_attr} \ From 359b4a9a87fa441c02722663207409e841a20801 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Thu, 23 Apr 2026 07:21:49 +1200 Subject: [PATCH 05/24] Use outline colour on edge locus' `stroke`. --- .../tailwind_class_state.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 1d46b8cf..fa9f7ae9 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 @@ -583,6 +583,12 @@ impl<'tw_state> TailwindClassState<'tw_state> { // 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` `<path>` 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_property = if is_edge { "stroke" } else { "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); @@ -605,7 +611,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { css_theme_vars, outline_full_prefix.as_ref(), state_modifier, - "outline", + outline_color_property, dark_mode_shade_config, color, shade, @@ -617,7 +623,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { } else if let Some(color) = color { writeln!( classes, - "{outline_full_prefix}{state_modifier}[outline-color:{color}]" + "{outline_full_prefix}{state_modifier}[{outline_color_css_prop}:{color}]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -673,9 +679,9 @@ impl<'tw_state> TailwindClassState<'tw_state> { /// tailwind classes. fn write_outline_style_node( classes: &mut String, + outline_full_prefix: &str, state_modifier: &str, style: &str, - outline_full_prefix: &str, ) { writeln!( classes, From dbdc1ed247b9bab85855bff23cd355bfffac86f3 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 09:14:52 +1200 Subject: [PATCH 06/24] Address clippy lints. --- .../taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs | 2 +- crate/svg_model/src/svg_edge_info.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 index 602a938b..04ae7d6a 100644 --- 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 @@ -28,7 +28,7 @@ impl EdgePathLocusCalculator { /// * `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 combined = edge_path.into_iter().chain(arrow_head_path.into_iter()); + let combined = edge_path.into_iter().chain(arrow_head_path); let style = Stroke::new(LOCUS_STROKE_WIDTH) .with_join(Join::Round) diff --git a/crate/svg_model/src/svg_edge_info.rs b/crate/svg_model/src/svg_edge_info.rs index a9c635e0..96512ee0 100644 --- a/crate/svg_model/src/svg_edge_info.rs +++ b/crate/svg_model/src/svg_edge_info.rs @@ -48,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>, From b2fe3014404d712af4d1c60d96b57f845894c094 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 09:22:01 +1200 Subject: [PATCH 07/24] Add `tabindex="-1"` to edges. --- crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ef895ffb..a899f32b 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 @@ -368,7 +368,8 @@ impl SvgElementsToSvgMapper { write!( content_buffer, "<g \ - id=\"{edge_id}\"\ + id=\"{edge_id}\" \ + tabindex=\"-1\" \ {class_attr}\ >" ) From c3410e4fa8110c0750d9ad603333dd149fdf0742 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 09:48:22 +1200 Subject: [PATCH 08/24] Add line explaining using `"none"` for stroke-style: "solid". --- .../src/input_to_ir_diagram_mapper/tailwind_class_state.rs | 1 + 1 file changed, 1 insertion(+) 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 fa9f7ae9..13152053 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 @@ -26,6 +26,7 @@ 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"), "dotted" => Some("2"), From 13d3015053207259701f2e3007a3680665024c13 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 10:11:21 +1200 Subject: [PATCH 09/24] Apply stroke styles per highlight state. --- .../tailwind_class_state.rs | 116 +++++++++++------- 1 file changed, 74 insertions(+), 42 deletions(-) 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 13152053..40b41a7c 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 @@ -308,6 +308,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. @@ -352,12 +390,22 @@ impl<'tw_state> TailwindClassState<'tw_state> { 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, "{peer_prefix_maybe}[stroke-dasharray:{dasharray}]") + // Stroke dasharray from stroke_style (per-state, with base fallback) + for (state_modifier, stroke_style) in [ + ("", self.get_stroke_style(HighlightState::Normal)), + ("hover:", self.get_stroke_style(HighlightState::Hover)), + ("focus:", self.get_stroke_style(HighlightState::Focus)), + ("active:", self.get_stroke_style(HighlightState::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 @@ -534,44 +582,28 @@ impl<'tw_state> TailwindClassState<'tw_state> { // `<path>` 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 outline_style_base = self.attrs.get(&ThemeAttr::OutlineStyle); let write_outline_style = if is_edge { Self::write_outline_style_edge } else { Self::write_outline_style_node }; - if let Some(style) = self - .attrs - .get(&ThemeAttr::OutlineStyleNormal) - .or(outline_style_base) - .map(Cow::as_ref) - { - write_outline_style(classes, outline_full_prefix.as_ref(), "", style); - } - if let Some(style) = self - .attrs - .get(&ThemeAttr::OutlineStyleHover) - .or(outline_style_base) - .map(Cow::as_ref) - { - write_outline_style(classes, outline_full_prefix.as_ref(), "hover:", style); - } - if let Some(style) = self - .attrs - .get(&ThemeAttr::OutlineStyleFocus) - .or(outline_style_base) - .map(Cow::as_ref) - { - write_outline_style(classes, outline_full_prefix.as_ref(), "focus:", style); - } - if let Some(style) = self - .attrs - .get(&ThemeAttr::OutlineStyleActive) - .or(outline_style_base) - .map(Cow::as_ref) - { - write_outline_style(classes, outline_full_prefix.as_ref(), "active:", style); - } + [ + ("", self.get_outline_style(HighlightState::Normal)), + ("hover:", self.get_outline_style(HighlightState::Hover)), + ("focus:", self.get_outline_style(HighlightState::Focus)), + ("active:", self.get_outline_style(HighlightState::Active)), + ] + .into_iter() + .for_each(|(state_modifier, outline_style)| { + if let Some(outline_style) = outline_style { + write_outline_style( + classes, + outline_full_prefix.as_ref(), + state_modifier, + outline_style, + ); + } + }); // Outline width if let Some(width) = self.attrs.get(&ThemeAttr::OutlineWidth) { @@ -665,9 +697,9 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes: &mut String, outline_full_prefix: &str, state_modifier: &str, - style: &str, + outline_style: &str, ) { - if let Some(dasharray) = Self::stroke_style_to_dasharray(style) { + if let Some(dasharray) = Self::stroke_style_to_dasharray(outline_style) { writeln!( classes, "{state_modifier}{outline_full_prefix}[stroke-dasharray:{dasharray}]" @@ -682,11 +714,11 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes: &mut String, outline_full_prefix: &str, state_modifier: &str, - style: &str, + outline_style: &str, ) { writeln!( classes, - "{state_modifier}{outline_full_prefix}outline-{style}" + "{state_modifier}{outline_full_prefix}outline-{outline_style}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } From e52a4033df87c13ea9dac9b020c3535e7ae58d99 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 10:35:20 +1200 Subject: [PATCH 10/24] Don't write stroke/outline colour when `stroke_style` is `"none"`. --- .../tailwind_class_state.rs | 184 ++++++++++-------- 1 file changed, 102 insertions(+), 82 deletions(-) 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 40b41a7c..517a5839 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 @@ -390,12 +390,17 @@ impl<'tw_state> TailwindClassState<'tw_state> { writeln!(classes, "{peer_prefix_maybe}{visibility}").expect(CLASSES_BUFFER_WRITE_FAIL); } + 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 [ - ("", self.get_stroke_style(HighlightState::Normal)), - ("hover:", self.get_stroke_style(HighlightState::Hover)), - ("focus:", self.get_stroke_style(HighlightState::Focus)), - ("active:", self.get_stroke_style(HighlightState::Active)), + ("", 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) @@ -504,63 +509,54 @@ impl<'tw_state> TailwindClassState<'tw_state> { // === Stroke classes === // // Stroke also uses shade shifting for dark mode. - - Self::write_shifted_shade_class( - classes, - css_theme_vars, - peer_prefix_maybe, - "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, - peer_prefix_maybe, - "", - "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, - peer_prefix_maybe, - "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, - peer_prefix_maybe, - "active:", - "stroke", - dark_mode_shade_config, - stroke_color_active, - stroke_shade_active, - stroke_shade_normal, - stroke_shade_hover, - stroke_shade_focus, - stroke_shade_active, - ); + // + // 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, + state_modifier, + "stroke", + dark_mode_shade_config, + color, + shade, + stroke_shade_normal, + stroke_shade_hover, + stroke_shade_focus, + stroke_shade_active, + ); + } + } // === Outline classes === // // @@ -575,6 +571,11 @@ impl<'tw_state> TailwindClassState<'tw_state> { Cow::Borrowed(peer_prefix_maybe) }; + 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 @@ -587,23 +588,16 @@ impl<'tw_state> TailwindClassState<'tw_state> { } else { Self::write_outline_style_node }; - [ - ("", self.get_outline_style(HighlightState::Normal)), - ("hover:", self.get_outline_style(HighlightState::Hover)), - ("focus:", self.get_outline_style(HighlightState::Focus)), - ("active:", self.get_outline_style(HighlightState::Active)), - ] - .into_iter() - .for_each(|(state_modifier, outline_style)| { - if let Some(outline_style) = outline_style { - write_outline_style( - classes, - outline_full_prefix.as_ref(), - state_modifier, - outline_style, - ); + 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, outline_full_prefix.as_ref(), state_modifier, style); } - }); + } // Outline width if let Some(width) = self.attrs.get(&ThemeAttr::OutlineWidth) { @@ -632,12 +626,38 @@ impl<'tw_state> TailwindClassState<'tw_state> { let outline_color_active = self.get_outline_color(HighlightState::Active); let outline_shade_active = self.get_outline_shade(HighlightState::Active); - for (state_modifier, color, shade) in [ - ("hover:", outline_color_hover, outline_shade_hover), - ("", outline_color_normal, outline_shade_normal), - ("focus:", outline_color_focus, outline_shade_focus), - ("active:", outline_color_active, outline_shade_active), + // Skip color/shade classes for states where outline_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, 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") { + continue; + } if shade.is_some() { Self::write_shifted_shade_class( classes, From dc7db23074e71b6fd20cfcd5f4dfc2f6fed6317c Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 12:02:11 +1200 Subject: [PATCH 11/24] Use `[stroke:none]` for edge locus to suppress stroke drawing. --- .../tailwind_class_state.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 517a5839..85c80b34 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 @@ -626,9 +626,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { let outline_color_active = self.get_outline_color(HighlightState::Active); let outline_shade_active = self.get_outline_shade(HighlightState::Active); - // Skip color/shade classes for states where outline_style is "none": in - // SVG specifying a stroke color will draw the stroke regardless of - // style, unlike HTML where `border-style: none` prevents drawing. + // 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:", @@ -656,6 +660,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { ), ] { if outline_style == Some("none") { + if is_edge { + writeln!( + classes, + "{outline_full_prefix}{state_modifier}[stroke:none]" + ) + .expect(CLASSES_BUFFER_WRITE_FAIL); + } continue; } if shade.is_some() { From 0c4041c524268f349b14a8b804560817e941dd8a Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 12:08:53 +1200 Subject: [PATCH 12/24] Move highlight state modifiers before prefix to get outline strokes to use correct colour. --- .../tailwind_class_state.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 85c80b34..9316f901 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 @@ -663,7 +663,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { if is_edge { writeln!( classes, - "{outline_full_prefix}{state_modifier}[stroke:none]" + "{state_modifier}{outline_full_prefix}[stroke:none]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -687,7 +687,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { } else if let Some(color) = color { writeln!( classes, - "{outline_full_prefix}{state_modifier}[{outline_color_css_prop}:{color}]" + "{state_modifier}{outline_full_prefix}[{outline_color_css_prop}:{color}]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -818,7 +818,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { // No dark mode -- emit plain tailwind class. writeln!( classes, - "{prefix}{state_modifier}{property}-{color}-{shade}" + "{state_modifier}{prefix}{property}-{color}-{shade}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -827,13 +827,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { writeln!( classes, - "{prefix}{state_modifier}{property}-[var({var_name})]" + "{state_modifier}{prefix}{property}-[var({var_name})]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } else { writeln!( classes, - "{prefix}{state_modifier}{property}-{color}-{shade}" + "{state_modifier}{prefix}{property}-{color}-{shade}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -850,13 +850,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { writeln!( classes, - "{prefix}{state_modifier}{property}-[var({var_name})]" + "{state_modifier}{prefix}{property}-[var({var_name})]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } else { writeln!( classes, - "{prefix}{state_modifier}{property}-{color}-{shade}" + "{state_modifier}{prefix}{property}-{color}-{shade}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } From 69868cc3f19c2162978d09a8fc89fa1ead674a29 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 12:58:20 +1200 Subject: [PATCH 13/24] Split tailwind peer prefix from subelement selector prefix so highlight state can always be after peer prefix. --- .../tailwind_class_state.rs | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) 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 9316f901..37519fc1 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 @@ -454,6 +454,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes, css_theme_vars, peer_prefix_maybe, + None, "hover:", "fill", dark_mode_shade_config, @@ -468,6 +469,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes, css_theme_vars, peer_prefix_maybe, + None, "", "fill", dark_mode_shade_config, @@ -482,6 +484,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes, css_theme_vars, peer_prefix_maybe, + None, "focus:", "fill", dark_mode_shade_config, @@ -496,6 +499,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes, css_theme_vars, peer_prefix_maybe, + None, "active:", "fill", dark_mode_shade_config, @@ -545,6 +549,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { classes, css_theme_vars, peer_prefix_maybe, + None, state_modifier, "stroke", dark_mode_shade_config, @@ -565,11 +570,8 @@ impl<'tw_state> TailwindClassState<'tw_state> { // entities the classes are applied directly. let is_edge = self.entity_type.as_ref().is_some_and(EntityType::is_edge); - let outline_full_prefix = if is_edge { - Cow::Owned(format!("{peer_prefix_maybe}[&>.locus]:")) - } else { - Cow::Borrowed(peer_prefix_maybe) - }; + 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); @@ -595,14 +597,23 @@ impl<'tw_state> TailwindClassState<'tw_state> { ("active:", outline_style_active), ] { if let Some(style) = outline_style { - write_outline_style(classes, outline_full_prefix.as_ref(), state_modifier, 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) { - writeln!(classes, "{outline_full_prefix}outline-{width}") - .expect(CLASSES_BUFFER_WRITE_FAIL); + 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) @@ -663,7 +674,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { if is_edge { writeln!( classes, - "{state_modifier}{outline_full_prefix}[stroke:none]" + "{peer_prefix_maybe}{state_modifier}{locus_selector_prefix_str}[stroke:none]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -673,7 +684,8 @@ impl<'tw_state> TailwindClassState<'tw_state> { Self::write_shifted_shade_class( classes, css_theme_vars, - outline_full_prefix.as_ref(), + peer_prefix_maybe, + locus_selector_prefix, state_modifier, outline_color_property, dark_mode_shade_config, @@ -687,7 +699,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { } else if let Some(color) = color { writeln!( classes, - "{state_modifier}{outline_full_prefix}[{outline_color_css_prop}:{color}]" + "{peer_prefix_maybe}{state_modifier}{locus_selector_prefix_str}[{outline_color_css_prop}:{color}]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -726,14 +738,16 @@ impl<'tw_state> TailwindClassState<'tw_state> { /// `stroke-dasharray` to simulate an outline. fn write_outline_style_edge( classes: &mut String, - outline_full_prefix: &str, + 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, - "{state_modifier}{outline_full_prefix}[stroke-dasharray:{dasharray}]" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}[stroke-dasharray:{dasharray}]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -743,13 +757,15 @@ impl<'tw_state> TailwindClassState<'tw_state> { /// tailwind classes. fn write_outline_style_node( classes: &mut String, - outline_full_prefix: &str, + 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, - "{state_modifier}{outline_full_prefix}outline-{outline_style}" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}outline-{outline_style}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -801,7 +817,8 @@ 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, dark_mode_shade_config: DarkModeShadeConfig, @@ -812,13 +829,14 @@ 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) { match dark_mode_shade_config { DarkModeShadeConfig::Disable => { // No dark mode -- emit plain tailwind class. writeln!( classes, - "{state_modifier}{prefix}{property}-{color}-{shade}" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}{property}-{color}-{shade}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -827,13 +845,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { writeln!( classes, - "{state_modifier}{prefix}{property}-[var({var_name})]" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}{property}-[var({var_name})]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } else { writeln!( classes, - "{state_modifier}{prefix}{property}-{color}-{shade}" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}{property}-{color}-{shade}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } @@ -850,13 +868,13 @@ impl<'tw_state> TailwindClassState<'tw_state> { if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { writeln!( classes, - "{state_modifier}{prefix}{property}-[var({var_name})]" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}{property}-[var({var_name})]" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } else { writeln!( classes, - "{state_modifier}{prefix}{property}-{color}-{shade}" + "{peer_prefix}{state_modifier}{subelement_selector_prefix}{property}-{color}-{shade}" ) .expect(CLASSES_BUFFER_WRITE_FAIL); } From e56d97475b36b79cfe11624f793e0306f01099d4 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 12:59:00 +1200 Subject: [PATCH 14/24] Update `example_ir.yaml`. --- workspace_tests/src/example_ir.yaml | 74 ++++++++++++++--------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/workspace_tests/src/example_ir.yaml b/workspace_tests/src/example_ir.yaml index 03120513..b5007d8f 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:3]\nhover:[stroke-dasharray:3]\nfocus:[stroke-dasharray:3]\nactive:[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]\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:3]\nhover:[stroke-dasharray:3]\nfocus:[stroke-dasharray:3]\nactive:[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]\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:3]\npeer-[:focus-within]/tag_deployment:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:[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]\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:3]\nhover:[stroke-dasharray:3]\nfocus:[stroke-dasharray:3]\nactive:[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]\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:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:[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]\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:3]\npeer-[:focus-within]/tag_app_development:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:[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]\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:3]\npeer-[:focus-within]/tag_app_development:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_project_build:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_project_build:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:[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]\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" From 9b4f4775a2ece8a72e5543367cd2aca02b05a29d Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 13:37:31 +1200 Subject: [PATCH 15/24] Workaround for outline style for nodes from weird tailwind / encre-css spec. --- .../tailwind_class_state.rs | 116 ++++++++++++------ 1 file changed, 81 insertions(+), 35 deletions(-) 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 37519fc1..54e50176 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,4 +1,7 @@ -use std::{borrow::Cow, fmt::Write}; +use std::{ + borrow::Cow, + fmt::{self, Write}, +}; use disposition_input_model::theme::{DarkModeShadeConfig, ThemeAttr}; use disposition_model_common::{entity::EntityType, Map}; @@ -456,7 +459,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { peer_prefix_maybe, None, "hover:", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_hover, fill_shade_hover, @@ -471,7 +474,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { peer_prefix_maybe, None, "", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_normal, fill_shade_normal, @@ -486,7 +489,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { peer_prefix_maybe, None, "focus:", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_focus, fill_shade_focus, @@ -501,7 +504,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { peer_prefix_maybe, None, "active:", - "fill", + ColorTarget::Fill, dark_mode_shade_config, fill_color_active, fill_shade_active, @@ -551,7 +554,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { peer_prefix_maybe, None, state_modifier, - "stroke", + ColorTarget::Stroke, dark_mode_shade_config, color, shade, @@ -625,7 +628,11 @@ impl<'tw_state> TailwindClassState<'tw_state> { // For edge entities, the `.edge_locus` `<path>` 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_property = if is_edge { "stroke" } else { "outline" }; + let outline_color_property = 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); @@ -801,7 +808,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. @@ -820,7 +828,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { 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>, @@ -831,29 +839,28 @@ impl<'tw_state> TailwindClassState<'tw_state> { ) { 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, - "{peer_prefix}{state_modifier}{subelement_selector_prefix}{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, - "{peer_prefix}{state_modifier}{subelement_selector_prefix}{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, - "{peer_prefix}{state_modifier}{subelement_selector_prefix}{property}-{color}-{shade}" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + color_target + .write_color_shade(classes, color, shade) + .expect(CLASSES_BUFFER_WRITE_FAIL); } } DarkModeShadeConfig::Shift { levels } => { @@ -866,25 +873,22 @@ impl<'tw_state> TailwindClassState<'tw_state> { shade_active, ); if let Some(var_name) = css_theme_vars.register(color, shade, dark_shade) { - writeln!( - classes, - "{peer_prefix}{state_modifier}{subelement_selector_prefix}{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, - "{peer_prefix}{state_modifier}{subelement_selector_prefix}{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, @@ -892,3 +896,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})]"), + } + } +} From 76f073be5bbccb41b9d763dd523f9954643e9e71 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 13:52:34 +1200 Subject: [PATCH 16/24] Correctly apply `stroke-` class for edge locus outline path. --- .../tailwind_class_state.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 54e50176..58591325 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 @@ -612,11 +612,19 @@ impl<'tw_state> TailwindClassState<'tw_state> { // Outline width if let Some(width) = self.attrs.get(&ThemeAttr::OutlineWidth) { - writeln!( - classes, - "{peer_prefix_maybe}{locus_selector_prefix_str}outline-{width}" - ) - .expect(CLASSES_BUFFER_WRITE_FAIL); + 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) From e678282f6c8122b466610f63d9d72521739afe92 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 13:53:02 +1200 Subject: [PATCH 17/24] Rename `outline_color_property` to `outline_color_target`. --- .../src/input_to_ir_diagram_mapper/tailwind_class_state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 58591325..41f174e2 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 @@ -636,7 +636,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { // For edge entities, the `.edge_locus` `<path>` 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_property = if is_edge { + let outline_color_target = if is_edge { ColorTarget::Stroke } else { ColorTarget::Outline @@ -702,7 +702,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { peer_prefix_maybe, locus_selector_prefix, state_modifier, - outline_color_property, + outline_color_target, dark_mode_shade_config, color, shade, From 88b4ad69ded5f09b645b1e3d4bd248f84bbd5694 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 13:56:44 +1200 Subject: [PATCH 18/24] Update edge locus distance to `10.0`. --- .../taffy_to_svg_elements_mapper/edge_path_locus_calculator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 04ae7d6a..91fbf8a1 100644 --- 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 @@ -1,7 +1,7 @@ use kurbo::{stroke, BezPath, Cap, Join, Stroke, StrokeOpts}; /// Width of the stroke expansion used to compute the edge locus, in pixels. -const LOCUS_STROKE_WIDTH: f64 = 8.0; +const LOCUS_STROKE_WIDTH: f64 = 10.0; /// Accuracy tolerance for path approximation when computing the locus. const LOCUS_TOLERANCE: f64 = 0.1; From decab38af834ea617f10ffa0ac65d4f1b2268aa6 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 13:57:05 +1200 Subject: [PATCH 19/24] Map `dashed` stroke style to `dasharray:4`. --- .../tailwind_class_state.rs | 2 +- workspace_tests/src/example_ir.yaml | 14 +++++++------- .../src/input_ir_rt/input_to_ir_diagram_mapper.rs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) 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 41f174e2..8a14a4ca 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 @@ -31,7 +31,7 @@ impl<'tw_state> TailwindClassState<'tw_state> { 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, diff --git a/workspace_tests/src/example_ir.yaml b/workspace_tests/src/example_ir.yaml index b5007d8f..dd4aa0d5 100644 --- a/workspace_tests/src/example_ir.yaml +++ b/workspace_tests/src/example_ir.yaml @@ -311,18 +311,18 @@ entity_types: - type_interaction_edge_sequence_forward_default tailwind_classes: 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:3]\nhover:[stroke-dasharray:3]\nfocus:[stroke-dasharray:3]\nactive:[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: "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:3]\nhover:[stroke-dasharray:3]\nfocus:[stroke-dasharray:3]\nactive:[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]\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:3]\npeer-[:focus-within]/tag_deployment:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:[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: "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:3]\nhover:[stroke-dasharray:3]\nfocus:[stroke-dasharray:3]\nactive:[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]\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:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_i12e_region_tier_app_deploy_step_ecs_cluster_update:active:[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: "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:3]\npeer-[:focus-within]/tag_app_development:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_deployment:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_build:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_gh_actions_publish:active:[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]\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:3]\npeer-[:focus-within]/tag_app_development:hover:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:focus:[stroke-dasharray:3]\npeer-[:focus-within]/tag_app_development:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_repository_clone:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_project_build:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_dev_step_project_build:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_crate_version_update:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_pull_request_open:active:[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:hover:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:focus:[stroke-dasharray:3]\npeer-[:focus-within]/proc_app_release_step_tag_and_push:active:[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_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" 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 f0aa55af..c3d6d23b 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}" ); } From 8f2387302680e6ec0db9c6bf4584d91c44b787be Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 14:35:15 +1200 Subject: [PATCH 20/24] Extract `css_animation_append_arrowhead_classes` logic in `SvgEdgeInfosBuilder`. --- .../src/svg_elements_to_svg_mapper.rs | 2 +- .../svg_edge_infos_builder.rs | 45 ++++++++++++++----- .../taffy_to_svg_elements_mapper.rs | 9 ++-- 3 files changed, 39 insertions(+), 17 deletions(-) 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 a899f32b..ed26be30 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 @@ -343,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) { 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 57fd9b4a..d691ebd6 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 @@ -1027,12 +1027,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_anim.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_anim.keyframe_css); + css.push_str(&edge_anim.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!( @@ -1044,7 +1071,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]" )); }); } @@ -1052,18 +1079,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/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 a83f6423..9da8a11c 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"); From ef1e8cf27c14f180bea57e4cc41834963fa41180 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 14:47:18 +1200 Subject: [PATCH 21/24] Fix position of locus for interaction edges' arrow head. --- .../svg_edge_infos_builder.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 d691ebd6..febd8682 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 @@ -250,17 +250,25 @@ impl SvgEdgeInfosBuilder { let path_d = path.to_svg(); // Compute arrowhead path. - let arrow_head_path = 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 = EdgePathLocusCalculator::calculate(&path, &arrow_head_path); let locus_path_d = locus_path.to_svg(); let tooltip = ir_diagram From aeb7517f73291c49fcbeb27fa349910596464671 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 16:19:33 +1200 Subject: [PATCH 22/24] Use `linesweeper` to merge edge path and arrow head loci. --- Cargo.lock | 30 +++++++++++++++++++ Cargo.toml | 1 + crate/input_ir_rt/Cargo.toml | 1 + .../edge_path_locus_calculator.rs | 21 +++++++++++-- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c9b1e95..6d58e860 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 d0be963e..fabd348a 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 06f25672..b8bfa9f1 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/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 index 91fbf8a1..98861045 100644 --- 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 @@ -1,4 +1,5 @@ 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; @@ -28,14 +29,28 @@ impl EdgePathLocusCalculator { /// * `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 combined = edge_path.into_iter().chain(arrow_head_path); - 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:?}")); - stroke(combined, &style, &opts, LOCUS_TOLERANCE) + // 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) } } From 391c4caee077b8bbadada8c524932e03618c7245 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 16:39:32 +1200 Subject: [PATCH 23/24] Only animate `edge_body` in interaction animations. --- .../src/svg_elements_to_svg_mapper.rs | 39 ++++++++++++++----- .../svg_edge_infos_builder.rs | 18 ++++----- 2 files changed, 38 insertions(+), 19 deletions(-) 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 ed26be30..d6a3d0b1 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 @@ -386,6 +386,7 @@ impl SvgElementsToSvgMapper { "<path \ d=\"{path_d}\" \ fill=\"none\" \ + class=\"edge_body\" />\ <path \ d=\"{locus_path_d}\" \ @@ -434,9 +435,10 @@ impl SvgElementsToSvgMapper { /// (e.g. `#some_id`), we preserve the literal underscore in the generated /// CSS. /// - /// Only underscores that are part of an ID selector (starting with `#`) are - /// escaped. For example: + /// Only underscores that are part of an ID selector (starting with `#`) or + /// class selector (starting with `.`) are escaped. For example: /// - `group-has-[#some_id:focus]` -> `group-has-[#some_id:focus]` + /// - `[&>.edge_body]` -> `[&>.edge_body]` /// - `peer/some-peer:animate-[animation-name_2s_linear_infinite]` -> /// unchanged /// @@ -460,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( @@ -484,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() @@ -493,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 => { @@ -517,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/svg_edge_infos_builder.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs index febd8682..367c3e83 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 @@ -975,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, @@ -989,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]" )); }); } @@ -1040,7 +1040,7 @@ impl SvgEdgeInfosBuilder { edge_path_info, edge_animation_active, associated_process_steps, - &edge_anim.arrow_head_animation_name, + &edge_animation.arrow_head_animation_name, animation_duration, forward_path_svg, ); @@ -1049,8 +1049,8 @@ impl SvgEdgeInfosBuilder { if !css.is_empty() { css.push('\n'); } - css.push_str(&edge_anim.keyframe_css); - css.push_str(&edge_anim.arrow_head_keyframe_css); + 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 From 546d868f393f161e996c510a78a822e7465494ba Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Sat, 25 Apr 2026 16:41:54 +1200 Subject: [PATCH 24/24] Update `CHANGELOG.md`. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2df0843..5a37cc2a 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)