From f28c457eccdd8ed53dfe4cfd33ce31636ed4685d Mon Sep 17 00:00:00 2001 From: msynk Date: Sat, 18 Apr 2026 15:05:21 +0330 Subject: [PATCH 1/5] add missing features to BitPersona #12250 --- .../Notifications/Persona/BitPersona.razor | 4 +- .../Notifications/Persona/BitPersona.razor.cs | 90 +++++++++++++++---- .../Notifications/Persona/BitPersona.scss | 75 +--------------- .../Persona/BitPersonaCoinShape.cs | 14 --- .../Persona/BitPersonaDemo.razor | 81 +++++++++++++++-- .../Persona/BitPersonaDemo.razor.cs | 69 ++++++++------ .../Persona/BitPersonaDemo.razor.samples.cs | 90 +++++++++++++++++-- 7 files changed, 275 insertions(+), 148 deletions(-) delete mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersonaCoinShape.cs diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor index 716d079478..517dd018af 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor @@ -82,8 +82,10 @@ alt="@ImageAlt" width="@dimension" height="@dimension" + loading="@ImageLoading" + srcset="@ImageSrcSet" class="bit-prs-img @Classes?.Image" - style="display:@(_isLoaded ? "unset" : "none");@Styles?.Image" /> + style="@(_isLoaded ? null : "opacity:0;" + Styles?.Image)" /> } } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor.cs index 2b195f054a..8f4d4f261b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.razor.cs @@ -1,4 +1,6 @@ -namespace Bit.BlazorUI; +using ErrorEventArgs = Microsoft.AspNetCore.Components.Web.ErrorEventArgs; + +namespace Bit.BlazorUI; /// /// A BitPersona is a visual representation of a person across products, typically showcasing the image that person has chosen to upload themselves. The control can also be used to show that person's online status. @@ -38,6 +40,13 @@ public partial class BitPersona : BitComponentBase /// [Parameter] public RenderFragment? ActionTemplate { get; set; } + /// + /// If true, automatically generates a coin background color derived from the person's name or initials. + /// When set, this takes effect only when is not explicitly provided. + /// + [Parameter, ResetClassBuilder] + public bool AutoCoinColor { get; set; } + /// /// Custom CSS classes for different parts of the BitPersona component. /// @@ -49,12 +58,6 @@ public partial class BitPersona : BitComponentBase [Parameter, ResetClassBuilder] public BitColor? CoinColor { get; set; } - /// - /// The shape of the coin. - /// - [Parameter, ResetClassBuilder] - public BitPersonaCoinShape? CoinShape { get; set; } - /// /// Optional custom persona coin size in pixel. /// @@ -89,7 +92,8 @@ public partial class BitPersona : BitComponentBase /// /// The user's initials to display in the image area when there is no image. /// - [Parameter] public string? ImageInitials { get; set; } + [Parameter, ResetClassBuilder] + public string? ImageInitials { get; set; } /// /// Optional Custom template for the image overlay. @@ -101,6 +105,16 @@ public partial class BitPersona : BitComponentBase /// [Parameter] public string ImageOverlayText { get; set; } = "Edit image"; + /// + /// Specifies the loading behavior of the image (e.g., "lazy" or "eager"). + /// + [Parameter] public string? ImageLoading { get; set; } + + /// + /// A set of image source URLs for different display densities or sizes (maps to the img srcset attribute). + /// + [Parameter] public string? ImageSrcSet { get; set; } + /// /// Url to the image to use, should be a square aspect ratio and big enough to fit in the image area. /// @@ -119,6 +133,16 @@ public partial class BitPersona : BitComponentBase [Parameter, ResetClassBuilder] public EventCallback OnImageClick { get; set; } + /// + /// Callback for when the image fails to load. + /// + [Parameter] public EventCallback OnImageError { get; set; } + + /// + /// Callback for when the image successfully loads. + /// + [Parameter] public EventCallback OnImageLoad { get; set; } + /// /// Optional text to display, usually a custom message set. /// The optional text will only be shown when using size100. @@ -184,7 +208,8 @@ public partial class BitPersona : BitComponentBase /// /// Primary text to display, usually the name of the person. /// - [Parameter] public string? PrimaryText { get; set; } + [Parameter, ResetClassBuilder] + public string? PrimaryText { get; set; } /// /// Custom primary text template. @@ -206,6 +231,12 @@ public partial class BitPersona : BitComponentBase /// [Parameter] public bool ShowInitialsUntilImageLoads { get; set; } + /// + /// If true, renders the coin with a square shape instead of the default circular shape. + /// + [Parameter, ResetClassBuilder] + public bool Squared { get; set; } + /// /// If true, show the special coin for unknown persona. /// It has '?' in place of initials, with static font and background colors. @@ -308,15 +339,11 @@ protected override void RegisterCssClasses() BitColor.PrimaryBorder => "bit-prs-pbr", BitColor.SecondaryBorder => "bit-prs-sbr", BitColor.TertiaryBorder => "bit-prs-tbr", + null when AutoCoinColor => GetAutoCoinColorClass(), _ => "bit-prs-inf" }); - ClassBuilder.Register(() => CoinShape switch - { - BitPersonaCoinShape.Circular => "bit-prs-crl", - BitPersonaCoinShape.Square => "bit-prs-sqr", - _ => "bit-prs-crl" - }); + ClassBuilder.Register(() => Squared ? "bit-prs-sqr" : null); } protected override void RegisterCssStyles() @@ -346,7 +373,7 @@ protected override void RegisterCssStyles() string? position = null; var presentationSize = CoinSize.Value / 3D; - if (CoinShape == BitPersonaCoinShape.Square) + if (Squared) { var presentationPosition = presentationSize / 3D; position = FormattableString.Invariant($"right:-{presentationPosition}px;bottom:-{presentationPosition}px;"); @@ -387,6 +414,23 @@ protected override void RegisterCssStyles() return $"width:{CoinSize.Value}px;"; } + private static readonly string[] _autoCoinColorClasses = ["bit-prs-pri", "bit-prs-sec", "bit-prs-ter", "bit-prs-suc", "bit-prs-wrn", "bit-prs-err", "bit-prs-inf"]; + + private string GetAutoCoinColorClass() + { + var text = (ImageInitials.HasValue() ? ImageInitials : PrimaryText)?.Trim() ?? string.Empty; + if (text.HasNoValue()) return "bit-prs-inf"; + + // Stable DJB2 hash — not affected by .NET's randomized GetHashCode + uint hash = 5381; + foreach (var c in text) + { + hash = ((hash << 5) + hash) + c; + } + + return _autoCoinColorClasses[hash % (uint)_autoCoinColorClasses.Length]; + } + private string GetInitials() { if (ImageInitials.HasValue()) return ImageInitials!; @@ -440,6 +484,13 @@ private string GetPersonaImageDimension() }; } + private string? GetImageStyle() + { + var hidden = _isLoaded ? null : "opacity:0;"; + var style = $"{hidden}{Styles?.Image}"; + return style.HasValue() ? style : null; + } + private string? GetImageContainerClass() { var klass = $"{(CoinTemplate is null ? "bit-prs-imc" : null)} {GetCoinClass()} {Classes?.ImageContainer}".Trim(); @@ -463,21 +514,24 @@ private async Task HandleImageClick(MouseEventArgs e) await OnImageClick.InvokeAsync(e); } - private void HandleOnError(Microsoft.AspNetCore.Components.Web.ErrorEventArgs e) + private async Task HandleOnError(ErrorEventArgs e) { _hasError = true; _isLoaded = true; + await OnImageError.InvokeAsync(e); StateHasChanged(); } - private void HandleOnLoad(ProgressEventArgs e) + private async Task HandleOnLoad(ProgressEventArgs e) { _isLoaded = true; + await OnImageLoad.InvokeAsync(e); StateHasChanged(); } private void OnSetImageUrl() { _hasError = false; + _isLoaded = false; } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.scss b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.scss index 5bfd5908d1..952331af86 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersona.scss @@ -37,6 +37,7 @@ overflow: hidden; font-weight: 600; position: relative; + border-radius: 50%; align-items: center; justify-content: center; border-width: $shp-border-width; @@ -54,12 +55,6 @@ } } -.bit-prs-crl { - .bit-prs-imc { - border-radius: 50%; - } -} - .bit-prs-sqr { .bit-prs-imc { border-radius: $shp-border-radius; @@ -85,10 +80,13 @@ } .bit-prs-img { + top: 0; + left: 0; opacity: 1; width: 100%; height: 100%; display: block; + position: absolute; object-fit: cover; } @@ -130,7 +128,6 @@ .bit-prs-ttx, .bit-prs-otx { overflow: hidden; - color: $clr-fg-pri; white-space: nowrap; text-overflow: ellipsis; } @@ -234,14 +231,6 @@ } .bit-prs-s24 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(3); - height: spacing(3); - position: relative; - text-align: center; - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(3); @@ -260,14 +249,6 @@ } .bit-prs-s32 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(4); - height: spacing(4); - position: relative; - text-align: center; - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(4); @@ -286,14 +267,6 @@ } .bit-prs-s40 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(5); - height: spacing(5); - position: relative; - text-align: center; - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(5); @@ -345,14 +318,6 @@ } .bit-prs-s48 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(6); - height: spacing(6); - position: relative; - text-align: center; - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(6); @@ -403,14 +368,6 @@ } .bit-prs-s56 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(7); - height: spacing(7); - position: relative; - text-align: center; - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(7); @@ -461,14 +418,6 @@ } .bit-prs-s72 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(9); - height: spacing(9); - position: relative; - text-align: center; - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(9); @@ -521,14 +470,6 @@ } .bit-prs-s100 { - .bit-prs-ima { - flex: 0 0 auto; - position: relative; - text-align: center; - width: spacing(12.5); - height: spacing(12.5); - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(12.5); @@ -583,14 +524,6 @@ } .bit-prs-s120 { - .bit-prs-ima { - flex: 0 0 auto; - width: spacing(15); - position: relative; - text-align: center; - height: spacing(15); - } - .bit-prs-imc { aspect-ratio: 1; width: spacing(15); diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersonaCoinShape.cs b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersonaCoinShape.cs deleted file mode 100644 index 7dc54ab1c1..0000000000 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Persona/BitPersonaCoinShape.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Bit.BlazorUI; - -public enum BitPersonaCoinShape -{ - /// - /// Represents the traditional round shape of a coin. - /// - Circular, - - /// - /// Represents a square-shaped coin. - /// - Square -} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor index 0bb006dc65..c5b67df988 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor @@ -125,17 +125,16 @@ CoinVariant="BitVariant.Text" /> - +

-
@@ -394,7 +393,73 @@ ImageUrl="/_content/Bit.BlazorUI.Demo.Client.Core/images/persona/persona-female.png" />
- + +
Different people each get a distinct, consistent coin color automatically:
+
+ +

+ +

+ +

+ +

+ +



+
Custom ImageInitials also influence the generated color:
+
+ +



+
CoinColor takes precedence over AutoCoinColor when set explicitly:
+
+ +
+ + +
OnImageLoad — callback invoked when the image loads successfully:
+
+ +

Image Load Count: @imageLoadCount

+



+
OnImageError — callback invoked when the image fails to load:
+
+ +

Image Error Count: @imageErrorCount

+
+ + +
ImageLoading controls when the browser fetches the image (maps to the HTML loading attribute):
+
+ +

+ +



+
ImageSrcSet provides alternate image sources for different screen densities (maps to the HTML srcset attribute):
+
+ +
+ +
Component's Style & Class:

- +
_icons = new() diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor.samples.cs index 49fc6f0af0..f11bcd1e3a 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Notifications/Persona/BitPersonaDemo.razor.samples.cs @@ -105,16 +105,15 @@ public partial class BitPersonaDemo private readonly string example4RazorCode = @" + ImageUrl=""/_content/Bit.BlazorUI.Demo.Client.Core/images/persona/persona-female.png"" /> -"; + ImageUrl=""/_content/Bit.BlazorUI.Demo.Client.Core/images/persona/persona-female.png"" />"; private readonly string example5RazorCode = @" + + + + + + + + + + + +"; + + private readonly string example13RazorCode = @" + imageLoadCount++"" + ImageUrl=""/images/persona/persona-female.png"" /> +

Image Load Count: @imageLoadCount

+ + imageErrorCount++"" + ImageUrl=""invalid-image-url"" /> +

Image Error Count: @imageErrorCount

"; + private readonly string example13CsharpCode = @" +private int imageLoadCount = 0; +private int imageErrorCount = 0;"; + + private readonly string example14RazorCode = @" + + + + +"; + private readonly string example15RazorCode = @"