Progress source

Inspect and copy the reviewed source included with this Free component.

Free source · 4 files

recipe.rs

// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Progress recipe — static named slots for the track and indicator.
//!
//! Progress has no visual axes today, but it is still a multipart component:
//! the public component owns the track while the indicator is a distinct
//! styled part. Keeping both slots in one recipe prevents their styling from
//! drifting apart and leaves a typed extension point for future axes.

use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};

mod base {
    use super::*;

    pub const PROGRESS_TRACK: &str = tw_join!(
        "relative",
        "h-2",
        "w-full",
        "overflow-hidden",
        "rounded-full",
        "bg-primary/20"
    );

    pub const PROGRESS_INDICATOR: &str = tw_join!(
        "h-full",
        "w-full",
        "flex-1",
        "rounded-full",
        "bg-primary",
        "transition-all"
    );

    pub const CIRCULAR_ROOT: &str =
        tw_join!("inline-flex", "shrink-0", "text-primary", "-rotate-90");

    pub const CIRCULAR_TRACK: &str = tw_join!("fill-none", "stroke-current", "opacity-20");

    pub const CIRCULAR_INDICATOR: &str = tw_join!(
        "fill-none",
        "stroke-current",
        "transition-[stroke-dashoffset]",
        "duration-300"
    );
}

/// Diameter variants for [`super::CircularProgress`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "size")]
pub enum ProgressSize {
    Sm,
    #[default]
    Md,
    Lg,
}

impl ProgressSize {
    pub const fn classes(self) -> &'static str {
        match self {
            Self::Sm => "size-6",
            Self::Md => "size-10",
            Self::Lg => "size-16",
        }
    }

    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Sm => "sm",
            Self::Md => "md",
            Self::Lg => "lg",
        }
    }
}

/// Optional inputs accepted by [`ProgressRecipe::resolve`].
///
/// This is intentionally empty until Progress gains a genuine visual axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ProgressRecipeProps;

/// Static named slots for the Progress compound component.
#[derive(Debug, Clone, Copy)]
pub struct ProgressRecipe {
    pub root: &'static str,
    pub indicator: &'static str,
    pub circular_root: &'static str,
    pub circular_track: &'static str,
    pub circular_indicator: &'static str,
}

impl ProgressRecipe {
    /// Resolve the track style without allocating class storage.
    pub const fn resolve(&'static self, _props: ProgressRecipeProps) -> ProgressStyle {
        ProgressStyle { recipe: self }
    }

    pub const fn resolve_circular(
        &'static self,
        size: Option<ProgressSize>,
    ) -> CircularProgressStyle {
        CircularProgressStyle {
            recipe: self,
            size: match size {
                Some(size) => size,
                None => ProgressSize::Md,
            },
        }
    }
}

/// Fully resolved, streamable Progress track style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct ProgressStyle {
    recipe: &'static ProgressRecipe,
}

impl AttrValue for ProgressStyle {
    #[inline(always)]
    fn should_render(&self) -> bool {
        true
    }

    #[inline(always)]
    fn render_attr_value<B: WriteBuf, L: DataLoader>(
        &self,
        cx: &mut WebContext<L, B>,
    ) -> Result<(), WriteError> {
        cx.out.extend_static(self.recipe.root.as_bytes())
    }
}

/// Fully resolved, streamable circular progress root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct CircularProgressStyle {
    recipe: &'static ProgressRecipe,
    size: ProgressSize,
}

impl CircularProgressStyle {
    pub const fn size(self) -> ProgressSize {
        self.size
    }
}

impl AttrValue for CircularProgressStyle {
    #[inline(always)]
    fn should_render(&self) -> bool {
        true
    }

    #[inline(always)]
    fn render_attr_value<B: WriteBuf, L: DataLoader>(
        &self,
        cx: &mut WebContext<L, B>,
    ) -> Result<(), WriteError> {
        cx.out.extend_static(self.recipe.circular_root.as_bytes())?;
        cx.out.extend_static(b" ")?;
        cx.out.extend_static(self.size.classes().as_bytes())
    }
}

/// Global, zero-initialization Progress recipe.
pub const PROGRESS_RECIPE: ProgressRecipe = ProgressRecipe {
    root: base::PROGRESS_TRACK,
    indicator: base::PROGRESS_INDICATOR,
    circular_root: base::CIRCULAR_ROOT,
    circular_track: base::CIRCULAR_TRACK,
    circular_indicator: base::CIRCULAR_INDICATOR,
};

pub const fn progress_recipe() -> &'static ProgressRecipe {
    &PROGRESS_RECIPE
}

impl crate::variant_spec::ComponentSpec for ProgressRecipe {
    const NAME: &'static str = "Progress";

    fn base_classes(&self) -> &'static str {
        self.root
    }

    fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
        Vec::new()
    }

    fn anatomy(&self) -> Option<crate::Anatomy> {
        Some(super::PROGRESS_ANATOMY)
    }
}

/// Compatibility resolver for the former track helper.
pub const fn progress_track_classes() -> ProgressStyle {
    PROGRESS_RECIPE.resolve(ProgressRecipeProps)
}

/// Compatibility accessor for the former indicator helper.
pub const fn progress_indicator_classes() -> &'static str {
    PROGRESS_RECIPE.indicator
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn recipe_exposes_track_and_indicator_slots() {
        let recipe = progress_recipe();
        assert!(recipe.root.contains("relative"));
        assert!(recipe.root.contains("rounded-full"));
        assert!(recipe.root.contains("bg-primary/20"));
        assert!(recipe.indicator.contains("h-full"));
        assert!(recipe.indicator.contains("bg-primary"));
        assert!(recipe.indicator.contains("transition-all"));
        assert!(recipe.circular_root.contains("text-primary"));
        assert!(recipe.circular_track.contains("opacity-20"));
        assert!(recipe.circular_indicator.contains("stroke-dashoffset"));
    }

    #[test]
    fn recipe_and_resolved_style_are_const_and_copy() {
        const RECIPE: &ProgressRecipe = &PROGRESS_RECIPE;
        const STYLE: ProgressStyle = RECIPE.resolve(ProgressRecipeProps);
        let copied = STYLE;
        assert!(copied.recipe.root.contains("w-full"));
    }

    #[test]
    fn circular_recipe_resolves_sizes_without_allocation() {
        let style = PROGRESS_RECIPE.resolve_circular(Some(ProgressSize::Lg));
        assert_eq!(style.size(), ProgressSize::Lg);
        assert_eq!(style.size().classes(), "size-16");
    }
}

Copy this source with the Free Proa UI components it imports; those component files remain available from their own source pages.

Licensed under MIT OR Apache-2.0. Third-party notices remain attached to the installed payload.

Open Markdown
FreeProgress

Linear and circular task completion indicators.

Install with the Proa CLI
proa ui add progress

Installs the reviewed component source, dependencies, shared support files, and required legal notices.

CLI and registry setup
recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Progress recipe — static named slots for the track and indicator.
//!
//! Progress has no visual axes today, but it is still a multipart component:
//! the public component owns the track while the indicator is a distinct
//! styled part. Keeping both slots in one recipe prevents their styling from
//! drifting apart and leaves a typed extension point for future axes.

use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};

mod base {
    use super::*;

    pub const PROGRESS_TRACK: &str = tw_join!(
        "relative",
        "h-2",
        "w-full",
        "overflow-hidden",

Search

Type at least 2 characters

Customize

Make it yours

Palette

Radius

Overview
Components
Settings

Build your own

Themed Proa UI surface

Preview
Components52
RecipesTyped
customer-portal