A Blazor-style Razor UI component for sandboxed UI, defines a RichTextBase panel that renders an icon/image/label and exposes many styling properties and animations. It parses properties in ParseRichText to apply them to the Panel Style and computes a build hash combining text, icon and image path.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@implements IRichTextPanel
<root class="richtext">
@* @if ( Stats.Length > 0 && Player.Local is Player localPlayer )
{
<RazorTooltip>
<Content class="richtext">
<label>@Text</label>
@if ( !string.IsNullOrEmpty( Input ) )
{
<Image [email protected](Input, InputGlyphSize.Small, false) />
}
else if ( !string.IsNullOrEmpty( ImagePath ) )
{
<img src="@ImagePath" />
}
@if ( ShowIcon && !string.IsNullOrEmpty( Icon ) )
{
<i>@Icon</i>
}
</Content>
</RazorTooltip>
}
else
{ *@
@if ( PrependIcon )
{
@if ( !string.IsNullOrEmpty( Input ) )
{
<Image [email protected](Input, InputGlyphSize.Small, false) />
}
else if ( !string.IsNullOrEmpty( ImagePath ) )
{
var imgSize = ImageSize ?? 20f;
<img src="@ImagePath" style="width: @(imgSize)px; height: @(imgSize)px;" />
}
@if ( ShowIcon && !string.IsNullOrEmpty( Icon ) )
{
<i>@Icon</i>
}
}
@if ( !HideText )
{
<label>@Text</label>
}
@if ( !PrependIcon )
{
@if ( !string.IsNullOrEmpty( Input ) )
{
<Image [email protected](Input, InputGlyphSize.Small, false) />
}
else if ( !string.IsNullOrEmpty( ImagePath ) )
{
var imgSize = ImageSize ?? 20f;
<img src="@ImagePath" style="width: @(imgSize)px; height: @(imgSize)px;" />
}
@if ( ShowIcon && !string.IsNullOrEmpty( Icon ) )
{
<i>@Icon</i>
}
}
@* } *@
</root>
<style>
.richtext {
align-items: center;
gap: 2px;
color: white;
overflow: visible;
label {
overflow: visible;
}
}
img, Image {
width: 16px;
height: 16px;
}
i {
position: relative;
top: 2px;
}
@@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@@keyframes wiggle {
0%, 100% { transform: rotate(-2deg); }
50% { transform: rotate(2deg); }
}
@@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@@keyframes hop {
0% { transform: translateY(-4px); }
100% { transform: translateY(1px); }
}
@@keyframes jump {
0% { transform: translateY(0) scaleX(1); }
1% { transform: translateY(-3px) scaleX(1); }
4% { transform: translateY(-7px) scaleX(1); }
8% { transform: translateY(-9px) scaleX(1); }
12% { transform: translateY(-7px) scaleX(1); }
16% { transform: translateY(-2px) scaleX(1); }
19% { transform: translateY(0) scaleX(1.1); }
21%, 100% { transform: translateY(0) scaleX(1); }
}
@@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
@@keyframes glow {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.3); }
}
@@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
@@keyframes turn {
0% { transform: rotate(-8deg); }
100% { transform: rotate(8deg); }
}
@@keyframes heartbeat {
0% { transform: scale(1); }
25% { transform: scale(1.1); }
50% { transform: scale(1); }
75% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@@keyframes tada {
0%, 100% { transform: scale(1) rotate(0deg); }
10%, 20% { transform: scale(0.9) rotate(-3deg); }
30%, 50%, 70%, 90% { transform: scale(1.1) rotate(3deg); }
40%, 60%, 80% { transform: scale(1.1) rotate(-3deg); }
}
@@keyframes rainbow {
0% { filter: hue-rotate(0deg); }
100% { filter: hue-rotate(360deg); }
}
@@keyframes god {
0%, { transform: scale(1); }
100% { transform: scale(1.05); }
}
@@keyframes stretch-y {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.2); }
}
@@keyframes recoil {
0% { transform: translateX(0) scale(1); }
15% { transform: translateX(-6px) scale(0.95); }
30% { transform: translateX(1px) scale(1.02); }
45% { transform: translateX(0) scale(1); }
100% { transform: translateX(0) scale(1); }
}
@@keyframes wave-up {
0%, 100% { transform: translateY(5px); }
50% { transform: translateY(-5px); }
}
@@keyframes wave-down {
0%, 100% { transform: translateY(-5px); }
50% { transform: translateY(5px); }
}
@@keyframes jitter {
0% { transform: translate(0, 0); }
10% { transform: translate(-1px, 1px); }
20% { transform: translate(1px, -1px); }
30% { transform: translate(-1px, -1px); }
40% { transform: translate(1px, 1px); }
50% { transform: translate(-1px, 0); }
60% { transform: translate(1px, 0); }
70% { transform: translate(0, -1px); }
80% { transform: translate(0, 1px); }
90% { transform: translate(-1px, 1px); }
100% { transform: translate(0, 0); }
}
@@keyframes throb {
0%, 100% { transform: scale(0.95); opacity: 0.4; }
50% { transform: scale(1.05); opacity: 1; }
}
@@keyframes curse {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
10% { transform: translate(-0.5px, -0.5px) rotate(-0.5deg) scale(1.02); }
20% { transform: translate(0.5px, 0.5px) rotate(0.5deg) scale(0.98); }
30% { transform: translate(-0.5px, 0.5px) rotate(-0.3deg) scale(1.01); }
40% { transform: translate(0.5px, -0.5px) rotate(0.3deg) scale(0.99); }
50% { transform: translate(-0.3px, -0.3px) rotate(-0.2deg) scale(1.02); }
60% { transform: translate(0.3px, 0.3px) rotate(0.2deg) scale(0.98); }
70% { transform: translate(-0.5px, 0.3px) rotate(-0.4deg) scale(1.01); }
80% { transform: translate(0.5px, -0.3px) rotate(0.4deg) scale(0.99); }
90% { transform: translate(-0.3px, 0.5px) rotate(-0.1deg) scale(1); }
100% { transform: translate(0, 0) rotate(0deg) scale(1); }
}
@@keyframes track {
0% { transform: translate(0, 0) rotate(0deg); }
15% { transform: translate(2px, -1.5px) rotate(3deg); }
16% { transform: translate(2px, -1.5px) rotate(3deg); }
35% { transform: translate(-1.5px, 1px) rotate(-4deg); }
36% { transform: translate(-1.5px, 1px) rotate(-4deg); }
55% { transform: translate(0.5px, -2px) rotate(2deg); }
56% { transform: translate(0.5px, -2px) rotate(2deg); }
75% { transform: translate(1px, 1.5px) rotate(-2deg); }
76% { transform: translate(1px, 1.5px) rotate(-2deg); }
95% { transform: translate(-0.5px, -0.5px) rotate(1deg); }
100% { transform: translate(0, 0) rotate(0deg); }
}
@@keyframes echo-clone {
0% { transform: translateX(0); opacity: 1; }
10% { transform: translateX(2px); opacity: 0.5; }
15% { transform: translateX(4px); opacity: 0.25; }
20% { transform: translateX(6px); opacity: 0; }
25% { transform: translateX(0); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
@@keyframes slide-return {
0% { transform: translateX(-12px); }
100% { transform: translateX(12px); }
}
@@keyframes wobble {
0% { transform: rotate(0deg); }
20% { transform: rotate(-8deg); }
21% { transform: rotate(-8deg); }
40% { transform: rotate(12deg); }
41% { transform: rotate(12deg); }
60% { transform: rotate(-5deg); }
61% { transform: rotate(-5deg); }
80% { transform: rotate(6deg); }
81% { transform: rotate(6deg); }
100% { transform: rotate(0deg); }
}
@@keyframes steer {
0% { transform: rotate(-12deg); }
100% { transform: rotate(12deg); }
}
@@keyframes zoom {
0% { transform: scale(1.3); }
100% { transform: scale(0.7); }
}
@@keyframes oscillate {
0% { transform: scale(0.85); }
100% { transform: scale(1.15); }
}
@@keyframes dvd {
0% { transform: translate(-3px, -2px); }
20% { transform: translate(0px, 1px); }
40% { transform: translate(2px, -3px); }
60% { transform: translate(-1px, 2px); }
80% { transform: translate(-3px, -3px); }
100% { transform: translate(0px, 2px); }
}
</style>
@code
{
public virtual string Icon => "";
public virtual string Input => "";
public virtual string ImagePath => "";
public virtual string TextOverride => null;
public virtual Color Color => Color.Transparent;
public virtual bool HideText => false;
public virtual bool PrependIcon => false;
public virtual RichTextStatEntry[] Stats => [];
public virtual int? FontWeight => null;
public virtual float? FontSize => null;
public virtual bool Italic => false;
public virtual bool Underline => false;
public virtual Sandbox.UI.TextDecorationStyle? UnderlineStyle => null;
public virtual Color? UnderlineColor => null;
public virtual float? UnderlineWidth => null;
public virtual float? TextShadowBlur => null;
public virtual float? TextShadowOffsetY => null;
public virtual Color? TextShadowColor => null;
public virtual Color? BackgroundColor => null;
public virtual float? BorderRadius => null;
public virtual float? BorderWidth => null;
public virtual Color? BorderColor => null;
public virtual string BorderStyle => null;
public virtual float? SkewX => null;
public virtual float? SkewY => null;
public virtual float? TextStrokeWidth => null;
public virtual Color? TextStrokeColor => null;
public virtual Color? BackgroundGradientStart => null;
public virtual Color? BackgroundGradientEnd => null;
public virtual float? BackgroundGradientAngle => null;
public virtual string AnimationName => null;
public virtual float? AnimationDuration => null;
public virtual string AnimationTimingFunction => null;
/// <summary>
/// normal, reverse, alternate, alternate-reverse
/// </summary>
public virtual string AnimationDirection => null;
public virtual float? MarginLeft => null;
public virtual float? MarginRight => null;
public virtual float? MarginTop => null;
public virtual float? MarginBottom => null;
public float? ImageSize { get; set; }
// "normal" - plays forward(0% → 100%), then jumps back to 0% and repeats
// "reverse" - plays backward(100% → 0%), then jumps to 100% and repeats
// "alternate" - plays forward then backward(0% → 100% → 0%), creating a smooth loop
// "alternate-reverse" - plays backward then forward(100% → 0% → 100%)
bool ShowIcon = true;
string Text;
public void ParseRichText(string text)
{
Text = !string.IsNullOrEmpty(TextOverride) ? TextOverride : text;
if(Color != Color.Transparent)
{
Style.FontColor = Color;
}
if(FontWeight.HasValue)
Style.FontWeight = FontWeight.Value;
if(FontSize.HasValue)
Style.FontSize = Length.Pixels(FontSize.Value);
if(Italic)
Style.FontStyle = Sandbox.UI.FontStyle.Italic;
if(Underline)
Style.TextDecorationLine = Sandbox.UI.TextDecoration.Underline;
if(UnderlineStyle.HasValue)
Style.TextDecorationStyle = UnderlineStyle.Value;
if(UnderlineColor.HasValue)
Style.TextDecorationColor = UnderlineColor.Value;
if(UnderlineWidth.HasValue)
Style.Set("text-decoration-thickness", $"{UnderlineWidth.Value}px");
if(TextShadowBlur.HasValue && TextShadowOffsetY.HasValue && TextShadowColor.HasValue)
{
Style.TextShadow.Clear();
Style.TextShadow.Add(new Shadow
{
OffsetX = 0,
OffsetY = TextShadowOffsetY.Value,
Blur = TextShadowBlur.Value,
Color = TextShadowColor.Value
});
}
if(BackgroundColor.HasValue)
Style.BackgroundColor = BackgroundColor.Value;
if(BorderRadius.HasValue)
{
var radius = Length.Pixels(BorderRadius.Value);
Style.BorderTopLeftRadius = radius;
Style.BorderTopRightRadius = radius;
Style.BorderBottomRightRadius = radius;
Style.BorderBottomLeftRadius = radius;
}
if(BorderWidth.HasValue)
{
var width = Length.Pixels(BorderWidth.Value);
Style.BorderTopWidth = width;
Style.BorderRightWidth = width;
Style.BorderBottomWidth = width;
Style.BorderLeftWidth = width;
}
if(BorderColor.HasValue)
{
Style.BorderTopColor = BorderColor.Value;
Style.BorderRightColor = BorderColor.Value;
Style.BorderBottomColor = BorderColor.Value;
Style.BorderLeftColor = BorderColor.Value;
}
if(!string.IsNullOrEmpty(BorderStyle))
{
Style.Set("border-style", BorderStyle);
}
if(SkewX.HasValue || SkewY.HasValue)
{
var transform = new PanelTransform();
transform.AddSkew(SkewX ?? 0, SkewY ?? 0, 0);
Style.Transform = transform;
}
if(TextStrokeWidth.HasValue)
Style.TextStrokeWidth = Length.Pixels(TextStrokeWidth.Value);
if(TextStrokeColor.HasValue)
Style.TextStrokeColor = TextStrokeColor.Value;
if(MarginLeft.HasValue)
Style.MarginLeft = Length.Pixels(MarginLeft.Value);
if(MarginRight.HasValue)
Style.MarginRight = Length.Pixels(MarginRight.Value);
if(MarginTop.HasValue)
Style.MarginTop = Length.Pixels(MarginTop.Value);
if(MarginBottom.HasValue)
Style.MarginBottom = Length.Pixels(MarginBottom.Value);
if(BackgroundGradientStart.HasValue && BackgroundGradientEnd.HasValue)
{
var angle = BackgroundGradientAngle ?? 90f;
Style.Set("background-image", $"linear-gradient({angle}deg, {BackgroundGradientStart.Value.Hex}, {BackgroundGradientEnd.Value.Hex})");
}
if(BackgroundColor.HasValue || (BackgroundGradientStart.HasValue && BackgroundGradientEnd.HasValue))
{
Style.PaddingLeft = Length.Pixels(3);
Style.PaddingRight = Length.Pixels(3);
}
if(!string.IsNullOrEmpty(AnimationName))
{
Style.AnimationName = AnimationName;
Style.AnimationIterationCount = float.PositiveInfinity;
if(AnimationDuration.HasValue)
Style.AnimationDuration = AnimationDuration.Value;
if(!string.IsNullOrEmpty(AnimationTimingFunction))
Style.AnimationTimingFunction = AnimationTimingFunction;
if(!string.IsNullOrEmpty(AnimationDirection))
Style.AnimationDirection = AnimationDirection;
}
if(HideText)
{
Style.PaddingLeft = Length.Pixels(3);
Style.PaddingRight = Length.Pixels(3);
}
if(!string.IsNullOrEmpty(ImagePath))
{
Style.PaddingLeft = Length.Pixels(0);
Style.PaddingRight = Length.Pixels(0);
}
}
protected override int BuildHash()
{
return System.HashCode.Combine( Text, Icon, ImagePath );
}
}