Singleton modal state and builder for UI modals. Exposes Modal.Show(title, body) and a Modal.New() fluent builder to set title, body, footer, close behavior and on-close callbacks, and commits state used by a ModalHost/ModalPanel component.
namespace Sunless.Libraries.UI;
using System;
using Microsoft.AspNetCore.Components;
using Sandbox;
// Singleton modal state. Host <ModalHost /> once. Call:
//
// Modal.Show( "#dialog.delete.title", "#dialog.delete.body" );
//
// Modal.New()
// .WithTitle( "#dialog.preset.title" )
// .WithBody( b => b.OpenElement(0, "div").AddContent(1, "hello").CloseElement() )
// .WithFooter( ... )
// .OnClose( () => StudioSounds.Cancel() )
// .Show();
//
// For arbitrary Razor markup in slots, use the stateless <ModalPanel> component directly
// and pass RenderFragment slots inline — this builder is for one-liner messages.
public static class Modal
{
public static string Title { get; private set; }
public static string BodyText { get; private set; }
public static RenderFragment BodyFragment { get; private set; }
public static RenderFragment FooterFragment { get; private set; }
public static bool ShowClose { get; private set; } = true;
public static bool DismissOnScrim { get; private set; } = true;
public static bool IsOpen { get; private set; }
public static int Version { get; private set; }
static Action _onClose;
public static Action OnOpenSound { get; set; }
public static Action OnCloseSound { get; set; }
public static ModalBuilder New() => new();
public static void Show( string title, string body )
=> New().WithTitle( title ).WithBody( body ).Show();
public static void Close()
{
if ( !IsOpen ) return;
var cb = _onClose;
IsOpen = false;
_onClose = null;
Version++;
try { OnCloseSound?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Modal] close sound threw: {e.Message}" ); }
try { cb?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Modal] OnClose threw: {e.Message}" ); }
}
internal static void Commit( ModalBuilder b )
{
Title = b._title;
BodyText = b._bodyText;
BodyFragment = b._bodyFragment;
FooterFragment = b._footerFragment;
ShowClose = b._showClose;
DismissOnScrim = b._dismissOnScrim;
_onClose = b._onClose;
IsOpen = true;
Version++;
try { OnOpenSound?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Modal] open sound threw: {e.Message}" ); }
}
}
public sealed class ModalBuilder
{
internal string _title;
internal string _bodyText;
internal RenderFragment _bodyFragment;
internal RenderFragment _footerFragment;
internal bool _showClose = true;
internal bool _dismissOnScrim = true;
internal Action _onClose;
public ModalBuilder WithTitle( string title ) { _title = title; return this; }
public ModalBuilder WithBody( string text ) { _bodyText = text; _bodyFragment = null; return this; }
public ModalBuilder WithBody( RenderFragment body ) { _bodyFragment = body; _bodyText = null; return this; }
public ModalBuilder WithFooter( RenderFragment footer ) { _footerFragment = footer; return this; }
public ModalBuilder NoClose() { _showClose = false; return this; }
public ModalBuilder LockedScrim() { _dismissOnScrim = false; return this; }
public ModalBuilder OnClose( Action cb ) { _onClose = cb; return this; }
public void Show() => Modal.Commit( this );
}